Compare commits
1 Commits
48d9d3280f
...
throwaway/
| Author | SHA1 | Date | |
|---|---|---|---|
| 4598993dfe |
@@ -101,10 +101,3 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
Keep fractional scaling sharp with Hyprland's `xwayland.force_zero_scaling` and Steam's own `STEAM_FORCE_DESKTOPUI_SCALING` instead.
|
||||
- AdGuard's LAN wildcard rewrite for `*.alexion.dev` points names at the reverse-proxy guest, including non-HTTP ports.
|
||||
Keep `git.alexion.dev:2022` working by proxying that TCP stream through the reverse-proxy guest to Gitea.
|
||||
- Remote deploy helpers that run target-side `sudo` over SSH need a TTY.
|
||||
Use `ssh -t` unless the target command is guaranteed not to prompt for sudo credentials.
|
||||
- `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads top-level host `config.registry` declarations only.
|
||||
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry` and do not enter the collected fleet registry unless host-side code also publishes them or the collector is explicitly extended to recurse into guests.
|
||||
- A NixOS module function argument default such as `registry ? config.registry` is not a safe fallback for an unprovided module argument.
|
||||
The module system still queries `_module.args.registry` and fails when it is absent.
|
||||
When an optional special-argument fallback is intentionally needed, bind the module arguments through `@args` and test `args ? <name>` inside the module body.
|
||||
|
||||
554
flake.nix
554
flake.nix
@@ -102,6 +102,8 @@
|
||||
checks.x86_64-linux =
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
serviceIdentities = import ./lib/service-identities.nix { inherit lib; };
|
||||
serviceIdentityTestsPass = lib.all (result: result) (lib.attrValues serviceIdentities.tests);
|
||||
in
|
||||
(lib.mapAttrs (
|
||||
name: host:
|
||||
@@ -111,110 +113,27 @@
|
||||
throw "Host ${name} has evaluation warnings:\n${lib.concatStringsSep "\n" host.config.warnings}"
|
||||
) self.nixosConfigurations)
|
||||
// {
|
||||
copyparty-reverse-proxy-trust-config =
|
||||
let
|
||||
pikachuConfig = self.nixosConfigurations.pikachu.config;
|
||||
serviceConfig = pikachuConfig.containers.copyparty.config.systemd.services.copyparty.serviceConfig;
|
||||
configScript = builtins.readFile serviceConfig.ExecStartPre;
|
||||
plainConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
./modules/services/copyparty.nix
|
||||
{
|
||||
modules.services.copyparty = {
|
||||
enable = true;
|
||||
passwordFile = "/run/password";
|
||||
volumes."/" = {
|
||||
path = "/srv/files";
|
||||
access.A = [ "alexion" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
plainScript = builtins.readFile plainConfig.systemd.services.copyparty.serviceConfig.ExecStartPre;
|
||||
in
|
||||
pkgs.runCommand "copyparty-reverse-proxy-trust-config-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
lib.hasInfix " rproxy: 1" configScript
|
||||
&& lib.hasInfix " xff-hdr: x-forwarded-for" configScript
|
||||
&& lib.hasInfix " xff-src: 10.23.20.109/32" configScript
|
||||
&& !(lib.hasInfix "rproxy" plainScript)
|
||||
&& !(lib.hasInfix "xff-hdr" plainScript)
|
||||
&& !(lib.hasInfix "xff-src" plainScript)
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
copyparty-reverse-proxy-trust-requires-source =
|
||||
let
|
||||
testConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
./modules/services/copyparty.nix
|
||||
{
|
||||
modules.services.copyparty = {
|
||||
enable = true;
|
||||
passwordFile = "/run/password";
|
||||
reverseProxy.enable = true;
|
||||
volumes."/" = {
|
||||
path = "/srv/files";
|
||||
access.A = [ "alexion" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
hasFailedTrustedSourcesAssertion = lib.any (
|
||||
assertion:
|
||||
assertion.assertion == false
|
||||
&& assertion.message == "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled."
|
||||
) testConfig.assertions;
|
||||
in
|
||||
pkgs.runCommand "copyparty-reverse-proxy-trust-requires-source-check" { } ''
|
||||
${lib.optionalString (!hasFailedTrustedSourcesAssertion) "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
service-identities = pkgs.runCommand "service-identities-check" { } ''
|
||||
${lib.optionalString (!serviceIdentityTestsPass) "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-basic-route =
|
||||
let
|
||||
routeHost = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.test.budget = "10.23.20.42";
|
||||
registry.route.test.budget = {
|
||||
domain = "budget.alexion.dev";
|
||||
port = 5006;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect { hosts.test = routeHost; };
|
||||
testConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
my = self.lib;
|
||||
inherit registry;
|
||||
};
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy.enable = true;
|
||||
modules.reverse-proxy = {
|
||||
enable = true;
|
||||
routes."budget.alexion.dev" = {
|
||||
path = "/";
|
||||
backend = "http://10.23.20.42:5006";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
@@ -231,448 +150,59 @@
|
||||
|
||||
reverse-proxy-actualbudget-guest-route =
|
||||
let
|
||||
testConfig = self.nixosConfigurations.pikachu.config;
|
||||
route = testConfig.registry.route.pikachu.actualbudget;
|
||||
vhost = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts."budget.alexion.dev";
|
||||
testConfig =
|
||||
(self.nixosConfigurations.pikachu.extendModules {
|
||||
modules = [
|
||||
{
|
||||
guests.actualbudget.reverseProxy = {
|
||||
enable = true;
|
||||
host = "budget.alexion.dev";
|
||||
backend = "http://10.23.20.42:5006";
|
||||
};
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
route = testConfig.modules.reverse-proxy.routes.actualbudget;
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-actualbudget-guest-route-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
route.domain == "budget.alexion.dev"
|
||||
&& route.port == 5006
|
||||
&& !(route ? backend)
|
||||
&& !(route ? path)
|
||||
&& !(route ? scheme)
|
||||
&& !(route ? aliases)
|
||||
&& !(route ? tls)
|
||||
&& !(route ? websockets)
|
||||
&& !(route ? enable)
|
||||
&& vhost.locations."/".proxyPass == "http://10.23.20.42:5006"
|
||||
)
|
||||
!(route.host == "budget.alexion.dev" && route.backend == "http://10.23.20.42:5006")
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
generic-registry-fleet-view =
|
||||
reverse-proxy-secondary-path-requires-root =
|
||||
let
|
||||
hosts = {
|
||||
alpha = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.registry = registry;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.alpha.service = "10.0.0.5";
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
beta = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.registry = registry;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
(
|
||||
{ registry, ... }:
|
||||
{
|
||||
environment.etc."registry-address-check".text = registry.address.alpha.service;
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
};
|
||||
registry = self.lib.registry.collect { inherit hosts; };
|
||||
betaConfig = hosts.beta.config;
|
||||
in
|
||||
pkgs.runCommand "generic-registry-fleet-view-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
registry.address.alpha.service == "10.0.0.5"
|
||||
&& betaConfig.environment.etc."registry-address-check".text == "10.0.0.5"
|
||||
&& !(registry.address ? alpha-service)
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
shell-shortcut-registry-fish-abbreviations =
|
||||
let
|
||||
testConfig = self.nixosConfigurations.neogaia.config;
|
||||
user = testConfig.user.name;
|
||||
shortcuts = testConfig.registry.shell-shortcut.${testConfig.networking.hostName};
|
||||
fishConfig = testConfig.home-manager.users.${user}.programs.fish;
|
||||
abbrs = fishConfig.shellAbbrs;
|
||||
aliases = fishConfig.shellAliases;
|
||||
expectedAliases = {
|
||||
ls = "eza -alg --color=always --group-directories-first --icons=always";
|
||||
la = "eza -a --color=always --group-directories-first --icons=always";
|
||||
ll = "eza -lg --color=always --group-directories-first --icons=always";
|
||||
lt = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||
"l." = "eza -a | grep -e '^\\.'";
|
||||
};
|
||||
expectedAbbrs = {
|
||||
".." = "cd ..";
|
||||
"..." = "cd ../..";
|
||||
"...." = "cd ../../..";
|
||||
vi = "nvim";
|
||||
vim = "nvim";
|
||||
cp = "cp -v";
|
||||
tmx = "tmux new-session -A -s";
|
||||
hs = "herdr --session";
|
||||
tarnow = "tar -acf ";
|
||||
untar = "tar -zxvf ";
|
||||
wget = "wget -c ";
|
||||
grep = "grep --color=auto";
|
||||
fgrep = "fgrep --color=auto";
|
||||
egrep = "egrep --color=auto";
|
||||
jctl = "journalctl -p 3 -xb";
|
||||
update = "sudo nixos-rebuild switch --flake .#${testConfig.networking.hostName}";
|
||||
cleanup = "sudo nix-collect-garbage -d";
|
||||
};
|
||||
in
|
||||
pkgs.runCommand "shell-shortcut-registry-fish-abbreviations-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts == (expectedAliases // expectedAbbrs)
|
||||
&& lib.filterAttrs (name: _value: builtins.hasAttr name expectedAliases) aliases == expectedAliases
|
||||
&& lib.filterAttrs (name: _value: builtins.hasAttr name expectedAbbrs) abbrs == expectedAbbrs
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
address-registry =
|
||||
let
|
||||
hostAddressConfig =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.neogaia.self = "10.23.50.146";
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
guestAddressConfig =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
self.lib.registry.module
|
||||
./modules/network.nix
|
||||
(self.lib.guest { name = "copyparty"; })
|
||||
{
|
||||
networking.hostName = "pikachu";
|
||||
modules.network.vlans = [ 20 ];
|
||||
guests.copyparty = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
address = "10.23.20.126";
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect {
|
||||
hosts = {
|
||||
neogaia = hostAddressConfig;
|
||||
pikachu = guestAddressConfig;
|
||||
};
|
||||
};
|
||||
in
|
||||
pkgs.runCommand "address-registry-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
registry.address.neogaia.self == "10.23.50.146"
|
||||
&& registry.address.pikachu.copyparty == "10.23.20.126"
|
||||
&& guestAddressConfig.config.containers.copyparty.config.systemd.network.networks."20-eth0".networkConfig.DHCP == "yes"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
address-registry-duplicate-value-fails =
|
||||
let
|
||||
alpha = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.alpha.self = "10.23.50.146";
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
beta = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.beta.self = "10.23.50.146";
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect { hosts = { inherit alpha beta; }; };
|
||||
result = builtins.tryEval (builtins.deepSeq registry.address true);
|
||||
in
|
||||
pkgs.runCommand "address-registry-duplicate-value-fails-check" { } ''
|
||||
${lib.optionalString result.success "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
ssh-targets-use-address-registry =
|
||||
let
|
||||
neogaiaConfig = self.nixosConfigurations.neogaia.config;
|
||||
pikachuConfig = self.nixosConfigurations.pikachu.config;
|
||||
neogaiaUser = neogaiaConfig.user.name;
|
||||
neogaiaSsh = neogaiaConfig.home-manager.users.${neogaiaUser}.programs.ssh.settings;
|
||||
in
|
||||
pkgs.runCommand "ssh-targets-use-address-registry-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
neogaiaConfig.registry.address.neogaia.self == "10.23.50.146"
|
||||
&& pikachuConfig.registry.address.pikachu.self == "10.23.10.102"
|
||||
&& neogaiaConfig.modules.ssh.targets.pikachu.hostName == pikachuConfig.registry.address.pikachu.self
|
||||
&& pikachuConfig.modules.ssh.targets.neogaia.hostName == neogaiaConfig.registry.address.neogaia.self
|
||||
&& neogaiaSsh.pikachu.data.HostName == pikachuConfig.registry.address.pikachu.self
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-route-registry =
|
||||
let
|
||||
routeHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.routeHost.app = "10.23.20.127";
|
||||
registry.route.routeHost.app = {
|
||||
domain = "worker.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect {
|
||||
hosts = {
|
||||
inherit routeHost;
|
||||
pikachu = self.nixosConfigurations.pikachu;
|
||||
};
|
||||
};
|
||||
testConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
my = self.lib;
|
||||
inherit registry;
|
||||
};
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy.enable = true;
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
vhosts = testConfig.services.nginx.virtualHosts;
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-route-registry-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
registry.route ? routeHost
|
||||
&& registry.route.routeHost ? app
|
||||
&& registry.route.pikachu ? actualbudget
|
||||
&& vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.20.127:3923"
|
||||
&& vhosts."budget.alexion.dev".locations."/".proxyPass == "http://10.23.20.42:5006"
|
||||
&& vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
route-registry-duplicate-domain-fails =
|
||||
let
|
||||
alpha = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.route.alpha.app = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
beta = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.route.beta.app = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect { hosts = { inherit alpha beta; }; };
|
||||
result = builtins.tryEval (builtins.deepSeq registry.route true);
|
||||
in
|
||||
pkgs.runCommand "route-registry-duplicate-domain-fails-check" { } ''
|
||||
${lib.optionalString result.success "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-address-backends =
|
||||
let
|
||||
workerHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.worker.self = "10.23.50.146";
|
||||
registry.route.worker.self = {
|
||||
domain = "worker.alexion.dev";
|
||||
port = 8080;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
guestRouteHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
self.lib.registry.module
|
||||
./modules/network.nix
|
||||
(self.lib.guest { name = "files"; })
|
||||
{
|
||||
networking.hostName = "pikachu";
|
||||
modules.network.vlans = [ 20 ];
|
||||
guests.files = {
|
||||
modules.reverse-proxy = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
address = "10.23.20.126";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
route = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
routes.raichu-files = {
|
||||
host = "files.alexion.dev";
|
||||
path = "/raichu";
|
||||
backend = "http://10.23.20.10:3923";
|
||||
};
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect {
|
||||
hosts = {
|
||||
worker = workerHost;
|
||||
pikachu = guestRouteHost;
|
||||
};
|
||||
};
|
||||
testConfig =
|
||||
(lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
my = self.lib;
|
||||
inherit registry;
|
||||
};
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy.enable = true;
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
vhosts = testConfig.services.nginx.virtualHosts;
|
||||
workerCompositeEntry = registry.join "worker" "self";
|
||||
hasFailedRootAssertion = lib.any (
|
||||
assertion:
|
||||
assertion.assertion == false
|
||||
&& assertion.message == "modules.reverse-proxy.routes for files.alexion.dev must include a primary / route."
|
||||
) testConfig.assertions;
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-address-backends-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
workerCompositeEntry.address == "10.23.50.146"
|
||||
&& workerCompositeEntry.route.domain == "worker.alexion.dev"
|
||||
&& vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.50.146:8080"
|
||||
&& registry.route.pikachu.files.port == 3923
|
||||
&& vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
pkgs.runCommand "reverse-proxy-secondary-path-requires-root-check" { } ''
|
||||
${lib.optionalString (!hasFailedRootAssertion) "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-route-requires-address =
|
||||
let
|
||||
routeHost = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.route.raichu.files = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect { hosts.raichu = routeHost; };
|
||||
testConfig = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = {
|
||||
my = self.lib;
|
||||
inherit registry;
|
||||
};
|
||||
modules = [
|
||||
./modules/reverse-proxy.nix
|
||||
{
|
||||
modules.reverse-proxy.enable = true;
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
result = builtins.tryEval (builtins.deepSeq testConfig.config.services.nginx.virtualHosts true);
|
||||
in
|
||||
pkgs.runCommand "reverse-proxy-route-requires-address-check" { } ''
|
||||
${lib.optionalString result.success "exit 1"}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
# The modules tree reaches for these option namespaces, so they must be
|
||||
# declared for the tree to evaluate even where a guest leaves them off.
|
||||
my.registry.module
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.copyparty;
|
||||
serviceUid = my.serviceUid "copyparty";
|
||||
|
||||
volumeType = lib.types.submodule {
|
||||
options = {
|
||||
@@ -112,7 +113,7 @@ in
|
||||
${cfg.stateDir} = {
|
||||
hostPath = cfg.statePath;
|
||||
create = {
|
||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||
owner = serviceUid;
|
||||
group = "storage";
|
||||
mode = "0770";
|
||||
};
|
||||
|
||||
@@ -6,55 +6,35 @@
|
||||
}:
|
||||
let
|
||||
giteaSshBackend = "10.23.20.45:2022";
|
||||
legacyBackend = "http://10.23.20.32";
|
||||
legacyHosts = [
|
||||
"games.alexion.dev"
|
||||
"git.alexion.dev"
|
||||
"music.alexion.dev"
|
||||
"photos.alexion.dev"
|
||||
"plex.alexion.dev"
|
||||
"servers.alexion.dev"
|
||||
"todo.alexion.dev"
|
||||
"wings.alexion.dev"
|
||||
];
|
||||
legacyRoutes = lib.listToAttrs (
|
||||
map (host: {
|
||||
name = "legacy-${lib.replaceStrings [ "." ] [ "-" ] host}";
|
||||
value = {
|
||||
inherit host;
|
||||
backend = legacyBackend;
|
||||
};
|
||||
}) legacyHosts
|
||||
);
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(my.guest {
|
||||
name = "reverse-proxy";
|
||||
interior = {
|
||||
modules.reverse-proxy.enable = true;
|
||||
|
||||
services.nginx.virtualHosts = {
|
||||
"games.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"git.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"music.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"photos.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"plex.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"servers.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"todo.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
"wings.alexion.dev" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://10.23.20.32";
|
||||
};
|
||||
modules.reverse-proxy = {
|
||||
enable = true;
|
||||
routes = config.modules.reverse-proxy.routes // legacyRoutes;
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 2022 ];
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
# So wifi can be joined from the console.
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
registry.address.neogaia.self = "10.23.50.146";
|
||||
|
||||
# So setup can be driven over the network.
|
||||
# The matching host public keys sit beside this file in plaintext, since
|
||||
# publishing them is their purpose.
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
registry,
|
||||
...
|
||||
}:
|
||||
{ pkgs, ... }:
|
||||
# pikachu — AZW ME Pro server.
|
||||
# Disk layout is in ./disk.nix.
|
||||
# `fileSystems` for the root disk are derived from it.
|
||||
@@ -72,8 +67,6 @@
|
||||
pools.pikachu = { };
|
||||
};
|
||||
|
||||
registry.address.pikachu.self = "10.23.10.102";
|
||||
|
||||
modules.ssh.enable = true;
|
||||
modules.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml;
|
||||
modules.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml;
|
||||
@@ -86,13 +79,10 @@
|
||||
vlan = 20;
|
||||
mac = "BC:24:11:C1:CD:28";
|
||||
dataPath = "/pikachu/data/actualbudget";
|
||||
address = "10.23.20.42";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
route = {
|
||||
domain = "budget.alexion.dev";
|
||||
port = 5006;
|
||||
};
|
||||
host = "budget.alexion.dev";
|
||||
backend = "http://10.23.20.42:5006";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -100,7 +90,6 @@
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
mac = "02:98:64:5a:07:4f";
|
||||
address = "10.23.20.109";
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
@@ -108,14 +97,11 @@
|
||||
vlan = 20;
|
||||
mac = "02:23:20:39:23:01";
|
||||
statePath = "/pikachu/data/copyparty";
|
||||
address = "10.23.20.126";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
route = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ];
|
||||
host = "files.alexion.dev";
|
||||
backend = "http://10.23.20.126:3923";
|
||||
trustedSources = [ "10.23.20.109/32" ];
|
||||
};
|
||||
volumes = {
|
||||
"/inbox" = {
|
||||
|
||||
@@ -13,7 +13,8 @@ let
|
||||
mapAttrsToList
|
||||
;
|
||||
|
||||
registry = import ./registry.nix { inherit lib self; };
|
||||
serviceIdentityRegistry = import ./service-identities.nix { inherit lib; };
|
||||
reverseProxyRoutes = import ./reverse-proxy-routes.nix { inherit lib; };
|
||||
|
||||
collectNixFiles =
|
||||
dir:
|
||||
@@ -33,9 +34,8 @@ let
|
||||
);
|
||||
|
||||
specialArgs = {
|
||||
inherit inputs self;
|
||||
inherit inputs;
|
||||
my = self.lib;
|
||||
registry = self.lib.registry.collect { hosts = self.nixosConfigurations; };
|
||||
};
|
||||
|
||||
bridgeName = id: "br-vlan${toString id}";
|
||||
@@ -46,6 +46,7 @@ let
|
||||
self
|
||||
specialArgs
|
||||
bridgeName
|
||||
reverseProxyRoutes
|
||||
;
|
||||
};
|
||||
|
||||
@@ -64,7 +65,6 @@ let
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
registry.module
|
||||
(self + "/system.nix")
|
||||
(self + "/hosts/${hostName}")
|
||||
{ networking.hostName = hostName; }
|
||||
@@ -84,9 +84,11 @@ in
|
||||
mkHost
|
||||
mkHosts
|
||||
bridgeName
|
||||
registry
|
||||
reverseProxyRoutes
|
||||
;
|
||||
|
||||
inherit (guests) deriveMac guest;
|
||||
|
||||
inherit (serviceIdentityRegistry) serviceUid;
|
||||
serviceIdentities = serviceIdentityRegistry.identities;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
self,
|
||||
specialArgs,
|
||||
bridgeName,
|
||||
reverseProxyRoutes,
|
||||
}:
|
||||
let
|
||||
inherit (lib) escapeShellArg mapAttrsToList;
|
||||
@@ -77,7 +78,8 @@ let
|
||||
systemd.network.networks."20-eth0" = {
|
||||
matchConfig.Name = "eth0";
|
||||
linkConfig.MACAddress = cfg.mac;
|
||||
networkConfig.DHCP = "yes";
|
||||
networkConfig = lib.mkIf (cfg.address == null) { DHCP = "yes"; };
|
||||
address = lib.mkIf (cfg.address != null) [ cfg.address ];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -123,19 +125,15 @@ let
|
||||
address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.23.20.126";
|
||||
example = "10.0.10.5/24";
|
||||
description = ''
|
||||
Expected IP address assigned outside NixOS for this guest.
|
||||
Left null, this guest contributes no address to the address registry.
|
||||
The guest's static address, in CIDR form, on its VLAN. Left null, the
|
||||
guest takes its address by DHCP, keeping IP management at the router.
|
||||
'';
|
||||
};
|
||||
reverseProxy = {
|
||||
enable = lib.mkEnableOption "a reverse-proxy route for this guest";
|
||||
|
||||
route = lib.mkOption {
|
||||
type = self.lib.registry.definitions.route.type;
|
||||
description = "Route registry entry published for this guest.";
|
||||
};
|
||||
reverseProxy = reverseProxyRoutes.mkRouteOptions {
|
||||
subject = "guest route";
|
||||
enableDefault = false;
|
||||
};
|
||||
mounts = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
@@ -280,9 +278,9 @@ let
|
||||
config = lib.mkIf cfg.enable {
|
||||
sops.secrets = lib.genAttrs cfg.secrets (_: { });
|
||||
|
||||
registry.address.${config.networking.hostName}.${machineName} = lib.mkIf (cfg.address != null) cfg.address;
|
||||
|
||||
registry.route.${config.networking.hostName}.${machineName} = lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route;
|
||||
modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable (
|
||||
reverseProxyRoutes.routeFields cfg.reverseProxy
|
||||
);
|
||||
|
||||
assertions = [
|
||||
{
|
||||
|
||||
216
lib/registry.nix
216
lib/registry.nix
@@ -1,216 +0,0 @@
|
||||
{ lib, self }:
|
||||
let
|
||||
inherit (lib)
|
||||
all
|
||||
concatLists
|
||||
concatMapStringsSep
|
||||
filter
|
||||
filterAttrs
|
||||
foldl'
|
||||
hasAttr
|
||||
hasSuffix
|
||||
mapAttrs
|
||||
mapAttrs'
|
||||
mapAttrsToList
|
||||
mkOption
|
||||
nameValuePair
|
||||
removeSuffix
|
||||
types
|
||||
unique
|
||||
;
|
||||
|
||||
checkType = types.submodule {
|
||||
options = {
|
||||
assertion = mkOption {
|
||||
type = types.functionTo types.bool;
|
||||
description = ''
|
||||
Predicate run against this registry's flattened collected entries.
|
||||
'';
|
||||
};
|
||||
|
||||
message = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Error message shown when this registry validation check fails.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
predicates = {
|
||||
uniqueValue = value: entries: builtins.length (unique (map value entries)) == builtins.length entries;
|
||||
};
|
||||
|
||||
registryLib = lib // {
|
||||
registry = (lib.registry or { }) // {
|
||||
inherit predicates;
|
||||
};
|
||||
};
|
||||
|
||||
registryDefinitionModule = {
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = types.raw;
|
||||
description = ''
|
||||
NixOS option type for one registry entry.
|
||||
'';
|
||||
};
|
||||
|
||||
replicate = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether collected entries are merged and published under every host key.
|
||||
'';
|
||||
};
|
||||
|
||||
checks = mkOption {
|
||||
type = types.listOf checkType;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Validation checks for this registry's flattened collected entries.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
registryName = fileName: removeSuffix ".nix" fileName;
|
||||
|
||||
registryOption = registry: mkOption {
|
||||
type = types.attrsOf (types.attrsOf registry.type);
|
||||
default = { };
|
||||
description = ''
|
||||
Lazily collected fleet-visible registry entries, nested by host and entry name.
|
||||
'';
|
||||
};
|
||||
|
||||
mkRegistry = path:
|
||||
let
|
||||
evaluated = lib.evalModules {
|
||||
specialArgs.lib = registryLib;
|
||||
modules = [
|
||||
registryDefinitionModule
|
||||
path
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (evaluated.config) type replicate checks;
|
||||
};
|
||||
|
||||
definitions = mapAttrs' (
|
||||
name: type:
|
||||
let
|
||||
path = self + "/registry/${name}";
|
||||
in
|
||||
nameValuePair (registryName name) (mkRegistry path)
|
||||
) (filterAttrs (name: type: type == "regular" && hasSuffix ".nix" name) (builtins.readDir (self + "/registry")));
|
||||
|
||||
flattenRegistryEntries = registryName: data:
|
||||
concatLists (
|
||||
mapAttrsToList (
|
||||
host: entries:
|
||||
mapAttrsToList (entry: value: {
|
||||
path = "registry.${registryName}.${host}.${entry}";
|
||||
inherit host entry value;
|
||||
}) entries
|
||||
) data
|
||||
);
|
||||
|
||||
rawDeclarations = registryName: hosts:
|
||||
mapAttrs (_hostName: host: host.config.registry.${registryName} or { }) hosts;
|
||||
|
||||
replicatedEntries = registryName: raw:
|
||||
let
|
||||
entries = concatLists (
|
||||
concatLists (
|
||||
mapAttrsToList (
|
||||
_evalHost: hostDeclarations:
|
||||
mapAttrsToList (
|
||||
hostName: declarations:
|
||||
mapAttrsToList (entryName: value: {
|
||||
path = "registry.${registryName}.${hostName}.${entryName}";
|
||||
inherit entryName value;
|
||||
}) declarations
|
||||
) hostDeclarations
|
||||
) raw
|
||||
)
|
||||
);
|
||||
in
|
||||
foldl' (
|
||||
acc: entry:
|
||||
if hasAttr entry.entryName acc then
|
||||
if acc.${entry.entryName}.value == entry.value then
|
||||
acc
|
||||
else
|
||||
throw "conflicting replicated registry entry ${entry.path} does not match ${acc.${entry.entryName}.path}"
|
||||
else
|
||||
acc // {
|
||||
${entry.entryName} = entry;
|
||||
}
|
||||
) { } entries;
|
||||
|
||||
replicatedData = registryName: hosts: raw:
|
||||
let
|
||||
entries = mapAttrs (_entryName: entry: entry.value) (replicatedEntries registryName raw);
|
||||
in
|
||||
mapAttrs (_hostName: _host: entries) hosts;
|
||||
|
||||
validationAssertions = data:
|
||||
concatLists (
|
||||
mapAttrsToList (
|
||||
registryName: registry:
|
||||
let
|
||||
entries = flattenRegistryEntries registryName data.${registryName};
|
||||
in
|
||||
map (check: {
|
||||
assertion = check.assertion entries;
|
||||
inherit (check) message;
|
||||
}) registry.checks
|
||||
) definitions
|
||||
);
|
||||
|
||||
assertValid = data:
|
||||
let
|
||||
assertions = validationAssertions data;
|
||||
failures = filter (assertion: !assertion.assertion) assertions;
|
||||
in
|
||||
assert lib.assertMsg (all (assertion: assertion.assertion) assertions) (
|
||||
concatMapStringsSep "\n" (assertion: assertion.message) failures
|
||||
);
|
||||
data;
|
||||
|
||||
join = registry: host: entry:
|
||||
filterAttrs (_name: value: value != null) (
|
||||
mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions
|
||||
);
|
||||
|
||||
collectData =
|
||||
{ hosts }:
|
||||
let
|
||||
raw = mapAttrs (registryName: _registry: rawDeclarations registryName hosts) definitions;
|
||||
data = mapAttrs (
|
||||
registryName: registry:
|
||||
if registry.replicate or false then
|
||||
replicatedData registryName hosts raw.${registryName}
|
||||
else
|
||||
mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts
|
||||
) definitions;
|
||||
in
|
||||
assertValid data;
|
||||
|
||||
withOperations = registry:
|
||||
registry
|
||||
// {
|
||||
join = host: entry: join registry host entry;
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit definitions predicates;
|
||||
|
||||
module = {
|
||||
options.registry = mapAttrs (_name: registry: registryOption registry) definitions;
|
||||
};
|
||||
|
||||
collect = args: withOperations (collectData args);
|
||||
}
|
||||
77
lib/reverse-proxy-routes.nix
Normal file
77
lib/reverse-proxy-routes.nix
Normal file
@@ -0,0 +1,77 @@
|
||||
{ lib }:
|
||||
let
|
||||
mkRouteOptions =
|
||||
{
|
||||
name ? null,
|
||||
subject ? "route",
|
||||
enableDefault ? true,
|
||||
}:
|
||||
{
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = enableDefault;
|
||||
description = "Whether this reverse-proxy ${subject} is active.";
|
||||
};
|
||||
|
||||
host = lib.mkOption (
|
||||
{
|
||||
type = lib.types.str;
|
||||
description = "Primary hostname served by this reverse-proxy ${subject}.";
|
||||
}
|
||||
// lib.optionalAttrs (name != null) {
|
||||
default = name;
|
||||
defaultText = lib.literalMD "the route attribute name";
|
||||
}
|
||||
);
|
||||
|
||||
aliases = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "Additional hostnames served by this reverse-proxy ${subject}.";
|
||||
};
|
||||
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/";
|
||||
description = "URL path prefix proxied by this reverse-proxy ${subject}.";
|
||||
};
|
||||
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Full HTTP backend URL that receives proxied traffic.";
|
||||
};
|
||||
|
||||
tls = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Whether to serve this reverse-proxy ${subject} with ACME-backed TLS.";
|
||||
};
|
||||
|
||||
websockets = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable websocket proxy headers for this reverse-proxy ${subject}.";
|
||||
};
|
||||
};
|
||||
|
||||
routeFields = route: {
|
||||
inherit (route)
|
||||
host
|
||||
aliases
|
||||
path
|
||||
backend
|
||||
tls
|
||||
websockets
|
||||
;
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit mkRouteOptions routeFields;
|
||||
|
||||
routeType = lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = mkRouteOptions { inherit name; };
|
||||
}
|
||||
);
|
||||
}
|
||||
59
lib/service-identities.nix
Normal file
59
lib/service-identities.nix
Normal file
@@ -0,0 +1,59 @@
|
||||
{ lib }:
|
||||
let
|
||||
defaultIdentities = {
|
||||
actualbudget = 20001;
|
||||
copyparty = 20002;
|
||||
};
|
||||
|
||||
duplicateUidMessage = duplicates: ''
|
||||
Duplicate stable service UID allocation(s): ${lib.concatStringsSep ", " (
|
||||
map (entry: "${toString entry.uid} is assigned to ${lib.concatStringsSep ", " entry.names}") duplicates
|
||||
)}
|
||||
'';
|
||||
|
||||
mk = identities:
|
||||
let
|
||||
names = lib.attrNames identities;
|
||||
namesByUid = lib.foldl' (
|
||||
acc: name:
|
||||
let
|
||||
uid = toString identities.${name};
|
||||
in
|
||||
acc // {
|
||||
${uid} = (acc.${uid} or [ ]) ++ [ name ];
|
||||
}
|
||||
) { } names;
|
||||
duplicates = lib.mapAttrsToList (uid: namesForUid: {
|
||||
uid = lib.toInt uid;
|
||||
names = namesForUid;
|
||||
}) (lib.filterAttrs (_uid: namesForUid: builtins.length namesForUid > 1) namesByUid);
|
||||
checkedIdentities =
|
||||
assert lib.assertMsg (duplicates == [ ]) (duplicateUidMessage duplicates);
|
||||
identities;
|
||||
in
|
||||
rec {
|
||||
inherit checkedIdentities;
|
||||
|
||||
serviceUid = name:
|
||||
if builtins.hasAttr name checkedIdentities then
|
||||
checkedIdentities.${name}
|
||||
else
|
||||
throw "Unknown stable service identity `${name}`. Known identities: ${lib.concatStringsSep ", " (lib.attrNames checkedIdentities)}";
|
||||
};
|
||||
|
||||
default = mk defaultIdentities;
|
||||
|
||||
tests = {
|
||||
actualbudgetUid = default.serviceUid "actualbudget" == 20001;
|
||||
copypartyUid = default.serviceUid "copyparty" == 20002;
|
||||
missingLookupFails = !(builtins.tryEval (default.serviceUid "missing-service")).success;
|
||||
duplicateUidFails = !(builtins.tryEval (mk {
|
||||
first = 20003;
|
||||
second = 20003;
|
||||
}).checkedIdentities).success;
|
||||
};
|
||||
in
|
||||
default // {
|
||||
identities = default.checkedIdentities;
|
||||
inherit tests;
|
||||
}
|
||||
@@ -13,8 +13,6 @@ in
|
||||
options.modules.agents.herdr.enable = lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
||||
|
||||
home-manager.users.${user} = {
|
||||
home.packages = [ pkgs.herdr ];
|
||||
|
||||
|
||||
@@ -11,38 +11,6 @@ let
|
||||
|
||||
# The hyprlock this module installs, used by the lock keybind below.
|
||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||
|
||||
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||
|
||||
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
||||
unit=lockscreen-idle-inhibit.service
|
||||
|
||||
refresh_waybar() {
|
||||
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ${pkgs.systemd}/bin/systemd-run \
|
||||
--user \
|
||||
--collect \
|
||||
--unit=lockscreen-idle-inhibit \
|
||||
--description="Disable idle-triggered screen locking" \
|
||||
${pkgs.systemd}/bin/systemd-inhibit \
|
||||
--what=idle \
|
||||
--who=lockscreen-hotkey \
|
||||
--why="lock screen disabled by hotkey" \
|
||||
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.desktop.hyprland.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen";
|
||||
@@ -87,7 +55,6 @@ in
|
||||
# hyprlock never stacks over the first.
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||
"$mod CTRL, X, exec, ${idleLockToggle}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ let
|
||||
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||
|
||||
# Renders and toggles mako's do-not-disturb mode.
|
||||
# With no mako daemon answering, the status reads "notifications on", keeping
|
||||
@@ -39,14 +38,6 @@ let
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
fi
|
||||
'';
|
||||
|
||||
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet lockscreen-idle-inhibit.service; then
|
||||
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
||||
else
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
||||
@@ -72,7 +63,6 @@ in
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [
|
||||
"custom/recording"
|
||||
"custom/idle-lock"
|
||||
"mpris"
|
||||
"wireplumber"
|
||||
"network"
|
||||
@@ -165,13 +155,6 @@ in
|
||||
exec = "${recStatus}";
|
||||
interval = 1;
|
||||
};
|
||||
|
||||
"custom/idle-lock" = {
|
||||
return-type = "json";
|
||||
exec = "${idleLockStatus}";
|
||||
interval = "once";
|
||||
signal = 9;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
let
|
||||
cfg = config.modules.fish;
|
||||
user = config.user.name;
|
||||
shortcuts = config.registry.shell-shortcut.${config.networking.hostName};
|
||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||
in
|
||||
{
|
||||
options.modules.fish = {
|
||||
@@ -29,8 +27,10 @@ in
|
||||
|
||||
home-manager.users.${user} = {
|
||||
home.packages = with pkgs; [
|
||||
eza # backs the ls/la/ll aliases
|
||||
bat # backs the manpager
|
||||
fastfetch # the shell greeting
|
||||
wget # backs the wget abbreviation
|
||||
];
|
||||
|
||||
programs.fish = {
|
||||
@@ -51,8 +51,37 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
shellAliases = commandsOf (lib.filterAttrs (_name: shortcut: !shortcut.abbreviation) shortcuts);
|
||||
shellAbbrs = commandsOf (lib.filterAttrs (_name: shortcut: shortcut.abbreviation) shortcuts);
|
||||
shellAliases = {
|
||||
ls = "eza -alg --color=always --group-directories-first --icons=always";
|
||||
la = "eza -a --color=always --group-directories-first --icons=always";
|
||||
ll = "eza -lg --color=always --group-directories-first --icons=always";
|
||||
lt = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||
"l." = "eza -a | grep -e '^\\.'";
|
||||
};
|
||||
|
||||
shellAbbrs = {
|
||||
# Walk up the tree.
|
||||
".." = "cd ..";
|
||||
"..." = "cd ../..";
|
||||
"...." = "cd ../../..";
|
||||
|
||||
vi = "nvim";
|
||||
vim = "nvim";
|
||||
cp = "cp -v";
|
||||
tmx = "tmux new-session -A -s";
|
||||
|
||||
tarnow = "tar -acf ";
|
||||
untar = "tar -zxvf ";
|
||||
wget = "wget -c ";
|
||||
grep = "grep --color=auto";
|
||||
fgrep = "fgrep --color=auto";
|
||||
egrep = "egrep --color=auto";
|
||||
jctl = "journalctl -p 3 -xb";
|
||||
|
||||
# Rebuild the system, and reclaim disk from old generations.
|
||||
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
|
||||
cleanup = "sudo nix-collect-garbage -d";
|
||||
};
|
||||
|
||||
functions = {
|
||||
# Pick the logo that fits the terminal so the greeting never wraps.
|
||||
@@ -91,11 +120,6 @@ in
|
||||
description = "Copy a file, or recursively copy a source directory into a destination";
|
||||
body = builtins.readFile ./functions/copy.fish;
|
||||
};
|
||||
|
||||
deploy-to = {
|
||||
description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild";
|
||||
body = builtins.readFile ./functions/deploy-to.fish;
|
||||
};
|
||||
};
|
||||
|
||||
# Rendered by home-manager into ~/.config/fish/config.fish.
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
if test (count $argv) -lt 1; or test (count $argv) -gt 2
|
||||
echo "usage: deploy-to <host> [branch]" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
set host $argv[1]
|
||||
set branch $argv[2]
|
||||
|
||||
if test -z "$branch"
|
||||
set branch (git branch --show-current)
|
||||
end
|
||||
|
||||
if test -z "$branch"
|
||||
echo "deploy-to: could not determine the current branch" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||
or begin
|
||||
echo "deploy-to: run this from a git checkout" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
set dirty_lines (git status --porcelain)
|
||||
if test (count $dirty_lines) -gt 0
|
||||
echo "deploy-to: refusing to deploy with a dirty local worktree" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
git fetch origin
|
||||
or return $status
|
||||
|
||||
set remote_ref refs/remotes/origin/$branch
|
||||
git rev-parse --verify --quiet $remote_ref >/dev/null
|
||||
or begin
|
||||
echo "deploy-to: origin/$branch does not exist" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set local_head (git rev-parse HEAD)
|
||||
set remote_head (git rev-parse $remote_ref)
|
||||
|
||||
if test "$local_head" != "$remote_head"
|
||||
echo "deploy-to: local HEAD differs from origin/$branch" >&2
|
||||
echo "deploy-to: push or reset the branch before deploying" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set escaped_branch (string escape -- $branch)
|
||||
set escaped_host (string escape -- $host)
|
||||
set remote_command "cd ~/wrk/dotfiles && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
|
||||
|
||||
ssh -t $host $remote_command
|
||||
@@ -14,11 +14,6 @@ in
|
||||
options.modules.nvim.enable = lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
registry.shell-shortcut.${config.networking.hostName} = {
|
||||
vi = "nvim";
|
||||
vim = "nvim";
|
||||
};
|
||||
|
||||
home-manager.sharedModules = [ inputs.nixvim.homeModules.nixvim ];
|
||||
|
||||
home-manager.users.${user} = hm: {
|
||||
|
||||
@@ -1,71 +1,65 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
registry,
|
||||
...
|
||||
}:
|
||||
{ config, lib, my, ... }:
|
||||
let
|
||||
cfg = config.modules.reverse-proxy;
|
||||
inherit (my) reverseProxyRoutes;
|
||||
|
||||
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
|
||||
enabledRoutes = lib.filterAttrs (_: route: route.enable) cfg.routes;
|
||||
routesByHost = lib.groupBy (route: route.host) (lib.attrValues enabledRoutes);
|
||||
|
||||
mkBackend = address: route: "http://${address}:${toString route.port}";
|
||||
|
||||
mkVirtualHost = host: entry:
|
||||
rootRoutesByHost = lib.mapAttrs (
|
||||
host: routes:
|
||||
let
|
||||
compositeEntry = registry.join host entry;
|
||||
route = compositeEntry.route;
|
||||
address = compositeEntry.address or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||
roots = builtins.filter (route: route.path == "/") routes;
|
||||
in
|
||||
{
|
||||
inherit host entry;
|
||||
name = route.domain;
|
||||
value = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = mkBackend address route;
|
||||
};
|
||||
if roots == [ ] then null else builtins.head roots
|
||||
) routesByHost;
|
||||
|
||||
hasDuplicatePaths = routes:
|
||||
let
|
||||
paths = map (route: route.path) routes;
|
||||
in
|
||||
lib.length paths != lib.length (lib.unique paths);
|
||||
|
||||
mkLocation = route: {
|
||||
proxyPass = route.backend;
|
||||
proxyWebsockets = route.websockets;
|
||||
};
|
||||
|
||||
mkVirtualHost = host: routes:
|
||||
let
|
||||
rootRoute = rootRoutesByHost.${host};
|
||||
in
|
||||
lib.mkIf (rootRoute != null) {
|
||||
forceSSL = rootRoute.tls;
|
||||
enableACME = rootRoute.tls;
|
||||
serverAliases = rootRoute.aliases;
|
||||
locations = lib.listToAttrs (
|
||||
map (route: lib.nameValuePair route.path (mkLocation route)) routes
|
||||
);
|
||||
};
|
||||
|
||||
generatedVirtualHostList = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
host: routes:
|
||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry) routes
|
||||
) registry.route
|
||||
);
|
||||
|
||||
virtualHostsByDomain = lib.foldl' (
|
||||
acc: virtualHost:
|
||||
acc
|
||||
// {
|
||||
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
||||
}
|
||||
) { } generatedVirtualHostList;
|
||||
|
||||
duplicateDomainMessages = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
domain: virtualHosts:
|
||||
if lib.length virtualHosts > 1 then
|
||||
[
|
||||
"duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath "route" virtualHost.host virtualHost.entry) virtualHosts}"
|
||||
]
|
||||
else
|
||||
[ ]
|
||||
) virtualHostsByDomain
|
||||
);
|
||||
|
||||
generatedVirtualHosts =
|
||||
if duplicateDomainMessages != [ ] then
|
||||
throw (builtins.head duplicateDomainMessages)
|
||||
else
|
||||
lib.listToAttrs generatedVirtualHostList;
|
||||
in
|
||||
{
|
||||
options.modules.reverse-proxy = {
|
||||
enable = lib.mkEnableOption "the public edge reverse proxy";
|
||||
enable = lib.mkEnableOption "the Nginx reverse proxy route realizer";
|
||||
|
||||
routes = lib.mkOption {
|
||||
type = lib.types.attrsOf reverseProxyRoutes.routeType;
|
||||
default = { };
|
||||
description = "Reverse-proxy route declarations keyed by stable route name.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions =
|
||||
lib.mapAttrsToList (host: rootRoute: {
|
||||
assertion = rootRoute != null;
|
||||
message = "modules.reverse-proxy.routes for ${host} must include a primary / route.";
|
||||
}) rootRoutesByHost
|
||||
++ lib.mapAttrsToList (host: routes: {
|
||||
assertion = !hasDuplicatePaths routes;
|
||||
message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once.";
|
||||
}) routesByHost;
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
@@ -78,18 +72,11 @@ in
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
commonHttpConfig = ''
|
||||
server {
|
||||
listen 0.0.0.0:443 ssl default_server;
|
||||
listen [::0]:443 ssl default_server;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
'';
|
||||
recommendedGzipSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedProxySettings = true;
|
||||
recommendedTlsSettings = true;
|
||||
virtualHosts = generatedVirtualHosts;
|
||||
virtualHosts = lib.mapAttrs mkVirtualHost routesByHost;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -28,6 +29,12 @@ in
|
||||
description = "User account that runs Actual Budget.";
|
||||
};
|
||||
|
||||
uid = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = my.serviceUid "actualbudget";
|
||||
description = "Stable uid for the Actual Budget service user.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "storage";
|
||||
@@ -36,27 +43,21 @@ in
|
||||
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
registry."stable-uid".${config.networking.hostName}.actualbudget = 20001;
|
||||
}
|
||||
config = lib.mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
uid = cfg.uid;
|
||||
group = cfg.group;
|
||||
};
|
||||
|
||||
(lib.mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
uid = config.registry."stable-uid".${config.networking.hostName}.actualbudget;
|
||||
group = cfg.group;
|
||||
services.actual = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
settings = {
|
||||
inherit (cfg) port dataDir;
|
||||
};
|
||||
|
||||
services.actual = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
settings = {
|
||||
inherit (cfg) port dataDir;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
@@ -101,6 +102,12 @@ in
|
||||
description = "User account that runs Copyparty.";
|
||||
};
|
||||
|
||||
uid = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = my.serviceUid "copyparty";
|
||||
description = "Stable uid for the Copyparty service user.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "storage";
|
||||
@@ -141,60 +148,54 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
registry."stable-uid".${config.networking.hostName}.copyparty = 20002;
|
||||
}
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.volumes != { };
|
||||
message = "modules.services.copyparty.volumes must declare at least one served volume.";
|
||||
}
|
||||
{
|
||||
assertion = !cfg.reverseProxy.enable || cfg.reverseProxy.trustedSources != [ ];
|
||||
message = "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled.";
|
||||
}
|
||||
];
|
||||
|
||||
(lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.volumes != { };
|
||||
message = "modules.services.copyparty.volumes must declare at least one served volume.";
|
||||
}
|
||||
{
|
||||
assertion = !cfg.reverseProxy.enable || cfg.reverseProxy.trustedSources != [ ];
|
||||
message = "modules.services.copyparty.reverseProxy.trustedSources must name at least one trusted proxy source when reverse-proxy support is enabled.";
|
||||
}
|
||||
];
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
uid = cfg.uid;
|
||||
group = cfg.group;
|
||||
home = cfg.stateDir;
|
||||
};
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||
group = cfg.group;
|
||||
home = cfg.stateDir;
|
||||
systemd.services.copyparty = {
|
||||
description = "Copyparty file server";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ReloadSignal = "SIGUSR1";
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
Environment = [
|
||||
"HOME=${cfg.stateDir}"
|
||||
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||
];
|
||||
StateDirectory = "copyparty";
|
||||
RuntimeDirectory = "copyparty";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||
ExecStartPre = configScript;
|
||||
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||
Restart = "on-failure";
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ cfg.stateDir ] ++ map (volume: volume.path) (lib.attrValues cfg.volumes);
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.copyparty = {
|
||||
description = "Copyparty file server";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ReloadSignal = "SIGUSR1";
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
Environment = [
|
||||
"HOME=${cfg.stateDir}"
|
||||
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||
];
|
||||
StateDirectory = "copyparty";
|
||||
RuntimeDirectory = "copyparty";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||
ExecStartPre = configScript;
|
||||
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||
Restart = "on-failure";
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [ cfg.stateDir ] ++ map (volume: volume.path) (lib.attrValues cfg.volumes);
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
})
|
||||
];
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
registry,
|
||||
...
|
||||
}:
|
||||
# SSH on this machine, in both directions.
|
||||
@@ -190,7 +189,7 @@ in
|
||||
};
|
||||
|
||||
neogaia = {
|
||||
hostName = registry.address.neogaia.self;
|
||||
hostName = "10.23.50.146";
|
||||
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia";
|
||||
hostKeys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p neogaia"
|
||||
@@ -199,7 +198,7 @@ in
|
||||
};
|
||||
|
||||
pikachu = {
|
||||
hostName = registry.address.pikachu.self;
|
||||
hostName = "10.23.10.102";
|
||||
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu";
|
||||
hostKeys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu"
|
||||
|
||||
@@ -13,8 +13,6 @@ in
|
||||
options.modules.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";
|
||||
|
||||
home-manager.users.${user}.programs.tmux = {
|
||||
enable = true;
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{ config, lib, ... }:
|
||||
# The baseline interactive toolset as one deliberate bundle.
|
||||
let
|
||||
cfg = config.modules.toolkit;
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.toolkit.enable =
|
||||
@@ -15,52 +9,6 @@ in
|
||||
|
||||
# Each piece is turned on at default priority, so a host can still override any one of them.
|
||||
config = lib.mkIf cfg.enable {
|
||||
home-manager.users.${user}.home.packages = with pkgs; [
|
||||
eza
|
||||
wget
|
||||
];
|
||||
|
||||
registry.shell-shortcut.${config.networking.hostName} = {
|
||||
ls = {
|
||||
command = "eza -alg --color=always --group-directories-first --icons=always";
|
||||
abbreviation = false;
|
||||
};
|
||||
la = {
|
||||
command = "eza -a --color=always --group-directories-first --icons=always";
|
||||
abbreviation = false;
|
||||
};
|
||||
ll = {
|
||||
command = "eza -lg --color=always --group-directories-first --icons=always";
|
||||
abbreviation = false;
|
||||
};
|
||||
lt = {
|
||||
command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||
abbreviation = false;
|
||||
};
|
||||
"l." = {
|
||||
command = "eza -a | grep -e '^\\.'";
|
||||
abbreviation = false;
|
||||
};
|
||||
|
||||
# Walk up the tree.
|
||||
".." = "cd ..";
|
||||
"..." = "cd ../..";
|
||||
"...." = "cd ../../..";
|
||||
|
||||
cp = "cp -v";
|
||||
tarnow = "tar -acf ";
|
||||
untar = "tar -zxvf ";
|
||||
wget = "wget -c ";
|
||||
grep = "grep --color=auto";
|
||||
fgrep = "fgrep --color=auto";
|
||||
egrep = "egrep --color=auto";
|
||||
jctl = "journalctl -p 3 -xb";
|
||||
|
||||
# Rebuild the system, and reclaim disk from old generations.
|
||||
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
|
||||
cleanup = "sudo nix-collect-garbage -d";
|
||||
};
|
||||
|
||||
modules.fish.enable = lib.mkDefault true;
|
||||
# fish is also the login shell, so the bundled environment is what a shell drops into.
|
||||
modules.fish.defaultShell = lib.mkDefault true;
|
||||
|
||||
105
prototypes/copyparty-rproxy-config/run.sh
Executable file
105
prototypes/copyparty-rproxy-config/run.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir -p "$tmp/state" "$tmp/root"
|
||||
|
||||
printf '%sQuestion%s\n' "$bold" "$reset"
|
||||
printf 'Does the proposed copyparty reverse-proxy config parse and make a proxied HTTPS-origin login-shaped request pass the CORS gate?\n\n'
|
||||
|
||||
printf '%sNix-generated config fragment%s\n' "$bold" "$reset"
|
||||
config_script=$(nix eval --raw '.#nixosConfigurations.pikachu.config.containers.copyparty.config.systemd.services.copyparty.serviceConfig.ExecStartPre')
|
||||
awk '/\[global\]/{show=1} /\[accounts\]/{show=0} show {print}' "$config_script" | grep -E 'rproxy|xff-hdr|xff-src' || true
|
||||
|
||||
pkg=$(nix eval --raw '.#nixosConfigurations.pikachu.config.containers.copyparty.config.modules.services.copyparty.package.outPath')
|
||||
|
||||
make_conf() {
|
||||
local file=$1 port=$2 rproxy=$3
|
||||
cat > "$file" <<EOF
|
||||
[global]
|
||||
i: 127.0.0.1
|
||||
p: $port
|
||||
usernames
|
||||
http-only
|
||||
no-crt
|
||||
no-thumb
|
||||
no-snap
|
||||
no-rescan
|
||||
ses-db: $tmp/state/sessions-$port.db
|
||||
chpw-db: $tmp/state/chpw-$port.json
|
||||
shr-db: $tmp/state/shares-$port.db
|
||||
$rproxy
|
||||
|
||||
[accounts]
|
||||
alexion: prototype-password
|
||||
|
||||
[/]
|
||||
$tmp/root
|
||||
accs:
|
||||
A: alexion
|
||||
EOF
|
||||
}
|
||||
|
||||
start_server() {
|
||||
local conf=$1 log=$2
|
||||
"$pkg/bin/copyparty" -c "$conf" >"$log" 2>&1 &
|
||||
local pid=$!
|
||||
for _ in {1..50}; do
|
||||
if grep -q 'listening' "$log"; then
|
||||
printf '%s' "$pid"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
cat "$log"
|
||||
kill "$pid" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
request() {
|
||||
local label=$1 port=$2 host=$3 proto=${4:-}
|
||||
local headers="$tmp/$label.headers"
|
||||
local body="$tmp/$label.body"
|
||||
local args=(
|
||||
-sS
|
||||
-D "$headers"
|
||||
-o "$body"
|
||||
-X POST
|
||||
-H "Host: $host"
|
||||
-H 'Origin: https://files.alexion.dev'
|
||||
-H 'X-Forwarded-For: 10.23.20.20'
|
||||
)
|
||||
if [[ -n "$proto" ]]; then
|
||||
args+=(-H "X-Forwarded-Proto: $proto")
|
||||
fi
|
||||
curl "${args[@]}" "http://127.0.0.1:$port/?pw=alexion:prototype-password" >/dev/null || true
|
||||
printf '%-24s %s %s\n' "$label" "$(head -1 "$headers" | tr -d '\r')" "$(head -1 "$body")"
|
||||
}
|
||||
|
||||
rproxy_config=' rproxy: 1
|
||||
xff-hdr: x-forwarded-for
|
||||
xff-src: 127.0.0.1/32'
|
||||
make_conf "$tmp/rproxy.conf" 39234 "$rproxy_config"
|
||||
pid=$(start_server "$tmp/rproxy.conf" "$tmp/rproxy.log")
|
||||
|
||||
printf '\n%sCORS gate probes%s\n' "$bold" "$reset"
|
||||
request with-forwarded-https 39234 files.alexion.dev https
|
||||
request missing-forwarded-proto 39234 files.alexion.dev
|
||||
request wrong-forwarded-host 39234 '10.23.20.126:3923' https
|
||||
|
||||
kill "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true
|
||||
|
||||
printf '\n%sRelevant server log%s\n' "$bold" "$reset"
|
||||
grep -E 'cors-reject|POST|http405|got proxied request|untrusted source|listening' "$tmp/rproxy.log" || true
|
||||
|
||||
printf '\n%sNginx proxy headers%s\n' "$bold" "$reset"
|
||||
toplevel=$(nix build --print-out-paths '.#nixosConfigurations.pikachu.config.containers.reverse-proxy.config.system.build.toplevel' --no-link)
|
||||
nginx_conf=$(readlink -f "$toplevel/etc/nginx/nginx.conf")
|
||||
include_path=$(awk '/server_name files\.alexion\.dev;/{in_server=1} in_server && /include .*recommended-proxy_set_header/{print $2; exit}' "$nginx_conf" | tr -d ';')
|
||||
cat "$include_path"
|
||||
@@ -1,11 +0,0 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
type = lib.types.str;
|
||||
|
||||
checks = [
|
||||
{
|
||||
assertion = lib.registry.predicates.uniqueValue (entry: entry.value);
|
||||
message = "Address registry values must be unique.";
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
checks = [
|
||||
{
|
||||
assertion = lib.registry.predicates.uniqueValue (entry: entry.value.domain);
|
||||
message = "Route domains must be unique.";
|
||||
}
|
||||
];
|
||||
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "files.alexion.dev";
|
||||
description = "Public domain served by this reverse-proxy route entry.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.ints.between 1 65535;
|
||||
example = 3923;
|
||||
description = "Backend TCP port served by the matching address registry entry.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
entryType = lib.types.submodule {
|
||||
options = {
|
||||
command = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "nvim";
|
||||
description = "Command expanded by this shell shortcut.";
|
||||
};
|
||||
|
||||
abbreviation = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Whether fish realizes this shell shortcut as an abbreviation instead of an alias.";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) entryType;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib)
|
||||
filterAttrs
|
||||
groupBy
|
||||
mapAttrs
|
||||
unique
|
||||
;
|
||||
|
||||
serviceNamesByUid = entries:
|
||||
mapAttrs (_uid: uidEntries: unique (map (entry: entry.entry) uidEntries)) (
|
||||
groupBy (entry: toString entry.value) entries
|
||||
);
|
||||
|
||||
duplicateUids = entries:
|
||||
filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
||||
|
||||
in
|
||||
{
|
||||
# Next stable UID: 20003
|
||||
replicate = true;
|
||||
|
||||
type = lib.types.ints.positive;
|
||||
|
||||
checks = [
|
||||
{
|
||||
assertion = entries: duplicateUids entries == { };
|
||||
message = "Stable UID allocations must be unique.";
|
||||
}
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user