Compare commits
1 Commits
6d046c190e
...
throwaway/
| Author | SHA1 | Date | |
|---|---|---|---|
| 4598993dfe |
@@ -101,8 +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.
|
||||
- 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.
|
||||
|
||||
515
flake.nix
515
flake.nix
@@ -118,108 +118,22 @@
|
||||
touch $out
|
||||
'';
|
||||
|
||||
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 = [
|
||||
./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 = [
|
||||
./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
|
||||
'';
|
||||
|
||||
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;
|
||||
@@ -236,414 +150,57 @@
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
fleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints {
|
||||
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"
|
||||
&& fleetEndpoints.neogaia.address == "10.23.50.146"
|
||||
&& fleetEndpoints."pikachu-copyparty".address == "10.23.20.126"
|
||||
&& !(fleetEndpoints ? pikachu)
|
||||
&& guestAddressConfig.config.containers.copyparty.config.systemd.network.networks."20-eth0".networkConfig.DHCP == "yes"
|
||||
)
|
||||
) "exit 1"
|
||||
}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
reverse-proxy-fleet-routes =
|
||||
let
|
||||
routeHost =
|
||||
lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.routeHost.app = "10.23.20.126";
|
||||
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-fleet-routes-check" { } ''
|
||||
${
|
||||
lib.optionalString (
|
||||
!(
|
||||
registry.route ? routeHost
|
||||
&& registry.route.routeHost ? app
|
||||
&& registry.route.pikachu ? actualbudget
|
||||
&& vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.20.126: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
|
||||
'';
|
||||
|
||||
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"
|
||||
}
|
||||
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
|
||||
'';
|
||||
|
||||
reverse-proxy-duplicate-domain-fails =
|
||||
let
|
||||
pikachu = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.pikachu.copyparty = "10.23.20.126";
|
||||
registry.route.pikachu.copyparty = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
raichu = lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs.my = self.lib;
|
||||
modules = [
|
||||
self.lib.registry.module
|
||||
{
|
||||
registry.address.raichu.files = "10.23.20.10";
|
||||
registry.route.raichu.files = {
|
||||
domain = "files.alexion.dev";
|
||||
port = 3923;
|
||||
};
|
||||
system.stateVersion = "26.11";
|
||||
}
|
||||
];
|
||||
};
|
||||
registry = self.lib.registry.collect { hosts = { inherit pikachu raichu; }; };
|
||||
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-duplicate-domain-fails-check" { } ''
|
||||
${lib.optionalString result.success "exit 1"}
|
||||
pkgs.runCommand "reverse-proxy-secondary-path-requires-root-check" { } ''
|
||||
${lib.optionalString (!hasFailedRootAssertion) "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,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 ];
|
||||
|
||||
@@ -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.
|
||||
@@ -84,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";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,7 +90,6 @@
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
mac = "02:98:64:5a:07:4f";
|
||||
address = "10.23.20.109";
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
@@ -106,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" = {
|
||||
|
||||
@@ -14,8 +14,7 @@ let
|
||||
;
|
||||
|
||||
serviceIdentityRegistry = import ./service-identities.nix { inherit lib; };
|
||||
registry = import ./registry.nix { inherit lib self; };
|
||||
networkEndpoints = import ./network-endpoints.nix { inherit lib; };
|
||||
reverseProxyRoutes = import ./reverse-proxy-routes.nix { inherit lib; };
|
||||
|
||||
collectNixFiles =
|
||||
dir:
|
||||
@@ -35,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}";
|
||||
@@ -48,6 +46,7 @@ let
|
||||
self
|
||||
specialArgs
|
||||
bridgeName
|
||||
reverseProxyRoutes
|
||||
;
|
||||
};
|
||||
|
||||
@@ -66,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; }
|
||||
@@ -86,8 +84,7 @@ in
|
||||
mkHost
|
||||
mkHosts
|
||||
bridgeName
|
||||
registry
|
||||
networkEndpoints
|
||||
reverseProxyRoutes
|
||||
;
|
||||
|
||||
inherit (guests) deriveMac guest;
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{ lib }:
|
||||
# TODO: Remove this compatibility bridge when route backend resolution reads registry.address directly.
|
||||
let
|
||||
endpointFields = address: { inherit address; };
|
||||
|
||||
collectFleetEndpoints = hosts:
|
||||
lib.concatMapAttrs (
|
||||
hostName: host:
|
||||
lib.mapAttrs' (
|
||||
entryName: address:
|
||||
let
|
||||
endpointName = if entryName == "self" then hostName else "${hostName}-${entryName}";
|
||||
in
|
||||
lib.nameValuePair endpointName (endpointFields address)
|
||||
) (host.config.registry.address.${hostName} or { })
|
||||
) hosts;
|
||||
in
|
||||
{
|
||||
inherit collectFleetEndpoints endpointFields;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{ lib, self }:
|
||||
let
|
||||
inherit (lib)
|
||||
filterAttrs
|
||||
hasSuffix
|
||||
mapAttrs
|
||||
mapAttrs'
|
||||
mkOption
|
||||
nameValuePair
|
||||
removeSuffix
|
||||
types
|
||||
;
|
||||
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
||||
definitions = mapAttrs' (
|
||||
name: type:
|
||||
let
|
||||
path = self + "/registry/${name}";
|
||||
in
|
||||
nameValuePair (registryName name) (import path { inherit lib; })
|
||||
) (filterAttrs (name: type: type == "regular" && hasSuffix ".nix" name) (builtins.readDir (self + "/registry")));
|
||||
join = registry: host: entry:
|
||||
filterAttrs (_name: value: value != null) (
|
||||
mapAttrs (registryName: _registry: (registry.${registryName}.${host} or { }).${entry} or null) definitions
|
||||
);
|
||||
|
||||
collectData =
|
||||
{ hosts }:
|
||||
mapAttrs (
|
||||
registryName: _registry:
|
||||
mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts
|
||||
) definitions;
|
||||
|
||||
withOperations = registry:
|
||||
registry
|
||||
// {
|
||||
join = host: entry: join registry host entry;
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit definitions;
|
||||
|
||||
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; };
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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; [
|
||||
bat
|
||||
fastfetch
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,4 +0,0 @@
|
||||
{ lib }:
|
||||
{
|
||||
type = lib.types.str;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{ lib }:
|
||||
{
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user