Compare commits

..

11 Commits

17 changed files with 657 additions and 132 deletions

View File

@@ -101,3 +101,5 @@ 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.

329
flake.nix
View File

@@ -118,6 +118,79 @@
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
testConfig =
@@ -150,24 +223,254 @@
reverse-proxy-actualbudget-guest-route =
let
testConfig =
(self.nixosConfigurations.pikachu.extendModules {
modules = [
{
guests.actualbudget.reverseProxy = {
enable = true;
host = "budget.alexion.dev";
backend = "http://10.23.20.42:5006";
};
}
];
}).config;
testConfig = self.nixosConfigurations.pikachu.config;
route = testConfig.modules.reverse-proxy.routes.actualbudget;
vhost = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts."budget.alexion.dev";
in
pkgs.runCommand "reverse-proxy-actualbudget-guest-route-check" { } ''
${
lib.optionalString (
!(route.host == "budget.alexion.dev" && route.backend == "http://10.23.20.42:5006")
!(
route.host == "budget.alexion.dev"
&& route.backend == null
&& route.endpoint == "pikachu-actualbudget"
&& route.port == 5006
&& route.tls
&& route.aliases == [ ]
&& route.path == "/"
&& !route.websockets
&& vhost.locations."/".proxyPass == "http://10.23.20.42:5006"
)
) "exit 1"
}
touch $out
'';
generic-registry-fleet-view =
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
'';
network-endpoint-fleet-registry =
let
hostEndpointConfig =
lib.nixosSystem {
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
./modules/network.nix
{
modules.network.endpoint.address = "10.23.50.146";
}
];
};
guestEndpointConfig =
lib.nixosSystem {
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
inputs.sops-nix.nixosModules.sops
./modules/network.nix
./modules/reverse-proxy.nix
(self.lib.guest { name = "copyparty"; })
{
modules.network.vlans = [ 20 ];
guests.copyparty = {
enable = true;
vlan = 20;
endpoint.address = "10.23.20.126";
};
}
];
};
fleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints {
neogaia = hostEndpointConfig;
pikachu = guestEndpointConfig;
};
in
pkgs.runCommand "network-endpoint-fleet-registry-check" { } ''
${
lib.optionalString (
!(
fleetEndpoints.neogaia.address == "10.23.50.146"
&& fleetEndpoints."pikachu-copyparty".address == "10.23.20.126"
&& !(fleetEndpoints ? pikachu)
&& guestEndpointConfig.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 = [
./modules/reverse-proxy.nix
{
modules.reverse-proxy.routes.app = {
host = "worker.alexion.dev";
backend = "http://10.23.20.126:3923";
};
}
];
};
endpoints = self.lib.networkEndpoints.collectFleetEndpoints {
pikachu = self.nixosConfigurations.pikachu;
};
fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes {
inherit routeHost;
pikachu = self.nixosConfigurations.pikachu;
} endpoints;
testConfig =
(self.nixosConfigurations.pikachu.extendModules {
specialArgs.reverseProxyFleetRoutes = fleetRoutes;
}).config;
vhosts = testConfig.containers.reverse-proxy.config.services.nginx.virtualHosts;
in
pkgs.runCommand "reverse-proxy-fleet-routes-check" { } ''
${
lib.optionalString (
!(
fleetRoutes ? routeHost-app
&& fleetRoutes ? 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-endpoint-backends =
let
endpointHost =
lib.nixosSystem {
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
./modules/network.nix
{
modules.network.endpoint.address = "10.23.50.146";
}
];
};
routeHost =
lib.nixosSystem {
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
./modules/reverse-proxy.nix
{
modules.reverse-proxy.routes.app = {
host = "worker.alexion.dev";
endpoint = "worker";
port = 8080;
};
}
];
};
guestRouteHost =
lib.nixosSystem {
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
inputs.sops-nix.nixosModules.sops
./modules/network.nix
./modules/reverse-proxy.nix
(self.lib.guest { name = "files"; })
{
networking.hostName = "pikachu";
modules.network.vlans = [ 20 ];
guests.files = {
enable = true;
vlan = 20;
endpoint.address = "10.23.20.126";
reverseProxy = {
enable = true;
host = "files.alexion.dev";
port = 3923;
};
};
}
];
};
endpoints = self.lib.networkEndpoints.collectFleetEndpoints {
worker = endpointHost;
pikachu = guestRouteHost;
};
fleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes {
inherit routeHost;
pikachu = guestRouteHost;
} endpoints;
testConfig =
(lib.nixosSystem {
system = "x86_64-linux";
specialArgs.my = self.lib;
modules = [
./modules/reverse-proxy.nix
{
modules.reverse-proxy = {
enable = true;
routes = fleetRoutes;
};
}
];
}).config;
vhosts = testConfig.services.nginx.virtualHosts;
in
pkgs.runCommand "reverse-proxy-endpoint-backends-check" { } ''
${
lib.optionalString (
!(
vhosts."worker.alexion.dev".locations."/".proxyPass == "http://10.23.50.146:8080"
&& fleetRoutes."pikachu-files".endpoint == "pikachu-files"
&& vhosts."files.alexion.dev".locations."/".proxyPass == "http://10.23.20.126:3923"
)
) "exit 1"
}
touch $out

View File

@@ -2,6 +2,7 @@
config,
lib,
my,
reverseProxyFleetRoutes ? config.modules.reverse-proxy.routes,
...
}:
let
@@ -34,7 +35,7 @@ in
interior = {
modules.reverse-proxy = {
enable = true;
routes = config.modules.reverse-proxy.routes // legacyRoutes;
routes = reverseProxyFleetRoutes // legacyRoutes;
};
networking.firewall.allowedTCPPorts = [ 2022 ];

View File

@@ -1,4 +1,4 @@
{ pkgs, ... }:
{ config, pkgs, ... }:
# pikachu — AZW ME Pro server.
# Disk layout is in ./disk.nix.
# `fileSystems` for the root disk are derived from it.
@@ -79,10 +79,11 @@
vlan = 20;
mac = "BC:24:11:C1:CD:28";
dataPath = "/pikachu/data/actualbudget";
endpoint.address = "10.23.20.42";
reverseProxy = {
enable = true;
host = "budget.alexion.dev";
backend = "http://10.23.20.42:5006";
port = 5006;
};
};
@@ -90,6 +91,7 @@
enable = true;
vlan = 20;
mac = "02:98:64:5a:07:4f";
endpoint.address = "10.23.20.109";
};
guests.copyparty = {
@@ -97,11 +99,12 @@
vlan = 20;
mac = "02:23:20:39:23:01";
statePath = "/pikachu/data/copyparty";
endpoint.address = "10.23.20.126";
reverseProxy = {
enable = true;
host = "files.alexion.dev";
backend = "http://10.23.20.126:3923";
trustedSources = [ "10.23.20.109/32" ];
port = 3923;
trustedSources = [ "${config.modules.network.endpoints."reverse-proxy".address}/32" ];
};
volumes = {
"/inbox" = {

View File

@@ -14,6 +14,8 @@ 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 =
@@ -33,10 +35,16 @@ let
) (builtins.readDir dir)
);
specialArgs = {
inherit inputs;
my = self.lib;
};
specialArgs =
let
networkFleetEndpoints = self.lib.networkEndpoints.collectFleetEndpoints self.nixosConfigurations;
in
{
inherit inputs networkFleetEndpoints self;
my = self.lib;
registry = self.lib.registry.collect { hosts = self.nixosConfigurations; };
reverseProxyFleetRoutes = self.lib.reverseProxyRoutes.collectFleetRoutes self.nixosConfigurations networkFleetEndpoints;
};
bridgeName = id: "br-vlan${toString id}";
@@ -65,6 +73,7 @@ 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,6 +93,8 @@ in
mkHost
mkHosts
bridgeName
registry
networkEndpoints
reverseProxyRoutes
;

View File

@@ -131,6 +131,18 @@ let
guest takes its address by DHCP, keeping IP management at the router.
'';
};
endpoint = {
address = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "10.23.20.126";
description = ''
Expected IP address assigned outside NixOS for this guest's
network endpoint. Left null, this guest contributes no endpoint to
the fleet endpoint registry.
'';
};
};
reverseProxy = reverseProxyRoutes.mkRouteOptions {
subject = "guest route";
enableDefault = false;
@@ -278,8 +290,17 @@ let
config = lib.mkIf cfg.enable {
sops.secrets = lib.genAttrs cfg.secrets (_: { });
modules.network.endpoints.${machineName} = lib.mkIf (cfg.endpoint.address != null) {
inherit (cfg.endpoint) address;
};
modules.reverse-proxy.routes.${machineName} = lib.mkIf cfg.reverseProxy.enable (
reverseProxyRoutes.routeFields cfg.reverseProxy
reverseProxyRoutes.routeFields (
cfg.reverseProxy
// lib.optionalAttrs (cfg.reverseProxy.backend == null && cfg.reverseProxy.endpoint == null) {
endpoint = "${config.networking.hostName}-${machineName}";
}
)
);
assertions = [

42
lib/network-endpoints.nix Normal file
View File

@@ -0,0 +1,42 @@
{ lib }:
let
endpointOptions = {
address = lib.mkOption {
type = lib.types.str;
example = "10.23.20.126";
description = "Expected IP address assigned outside NixOS for this network endpoint.";
};
};
endpointType = lib.types.submodule {
options = endpointOptions;
};
endpointFields = endpoint: {
inherit (endpoint) address;
};
collectFleetEndpoints =
hosts:
lib.concatMapAttrs (
hostName: host:
let
cfg = host.config.modules.network;
hostEndpoint = lib.optionalAttrs (cfg.endpoint.address != null) {
${hostName} = endpointFields cfg.endpoint;
};
guestEndpoints = lib.mapAttrs' (
endpointName: endpoint: lib.nameValuePair "${hostName}-${endpointName}" (endpointFields endpoint)
) cfg.endpoints;
in
hostEndpoint // guestEndpoints
) hosts;
in
{
inherit
collectFleetEndpoints
endpointFields
endpointOptions
endpointType
;
}

45
lib/registry.nix Normal file
View File

@@ -0,0 +1,45 @@
{ 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")));
in
{
inherit definitions;
module = {
options.registry = mapAttrs (_name: registry: registryOption registry) definitions;
};
collect =
{ hosts }:
mapAttrs (
registryName: _registry:
mapAttrs (hostName: host: host.config.registry.${registryName}.${hostName} or { }) hosts
) definitions;
}

View File

@@ -37,10 +37,30 @@ let
};
backend = lib.mkOption {
type = lib.types.str;
type = lib.types.nullOr lib.types.str;
default = null;
description = "Full HTTP backend URL that receives proxied traffic.";
};
endpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Fleet network endpoint key used to derive the backend URL.";
};
port = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 1 65535);
default = null;
example = 3923;
description = "Backend TCP port used with the fleet network endpoint.";
};
scheme = lib.mkOption {
type = lib.types.str;
default = "http";
description = "Backend URL scheme used with the fleet network endpoint.";
};
tls = lib.mkOption {
type = lib.types.bool;
default = true;
@@ -60,13 +80,45 @@ let
aliases
path
backend
endpoint
port
scheme
tls
websockets
;
};
endpointBackend = endpoints: route:
let
endpoint = endpoints.${route.endpoint} or (
throw "Unknown reverse-proxy endpoint `${route.endpoint}`. Known endpoints: ${lib.concatStringsSep ", " (lib.attrNames endpoints)}"
);
in
"${route.scheme}://${endpoint.address}:${toString route.port}";
resolvedRouteFields = endpoints: route:
let
routeWithBackend =
if route.backend != null then
route
else if route.endpoint != null && route.port != null then
route // { backend = endpointBackend endpoints route; }
else
throw "Reverse-proxy route `${route.host}${route.path}` needs either backend or endpoint plus port.";
in
routeFields routeWithBackend;
collectFleetRoutes =
hosts: endpoints:
lib.concatMapAttrs (
hostName: host:
lib.mapAttrs' (
routeName: route: lib.nameValuePair "${hostName}-${routeName}" (resolvedRouteFields endpoints route)
) host.config.modules.reverse-proxy.routes
) hosts;
in
{
inherit mkRouteOptions routeFields;
inherit collectFleetRoutes mkRouteOptions resolvedRouteFields routeFields;
routeType = lib.types.submodule (
{ name, ... }:

View File

@@ -11,6 +11,38 @@ 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";
@@ -55,6 +87,7 @@ 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}"
];
};
};

View File

@@ -12,6 +12,7 @@ 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
@@ -38,6 +39,14 @@ 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";
@@ -63,6 +72,7 @@ in
modules-center = [ "clock" ];
modules-right = [
"custom/recording"
"custom/idle-lock"
"mpris"
"wireplumber"
"network"
@@ -155,6 +165,13 @@ in
exec = "${recStatus}";
interval = 1;
};
"custom/idle-lock" = {
return-type = "json";
exec = "${idleLockStatus}";
interval = "once";
signal = 9;
};
};
};
};

View File

@@ -69,6 +69,7 @@ in
vim = "nvim";
cp = "cp -v";
tmx = "tmux new-session -A -s";
hs = "herdr --session";
tarnow = "tar -acf ";
untar = "tar -zxvf ";
@@ -120,6 +121,11 @@ 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.

View File

@@ -0,0 +1,53 @@
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

View File

@@ -43,6 +43,28 @@ in
'';
};
endpoint = {
address = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "10.23.50.146";
description = ''
Expected IP address assigned outside NixOS for this host's network
endpoint. Left null, this host contributes no host endpoint to the
fleet endpoint registry.
'';
};
};
endpoints = lib.mkOption {
type = lib.types.attrsOf my.networkEndpoints.endpointType;
default = { };
description = ''
Network endpoint declarations contributed by placed workloads on this
host, keyed by their local stable endpoint name.
'';
};
management = {
vlan = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 1 4094);

View File

@@ -20,6 +20,8 @@ let
in
lib.length paths != lib.length (lib.unique paths);
hasUnresolvedBackend = route: route.backend == null;
mkLocation = route: {
proxyPass = route.backend;
proxyWebsockets = route.websockets;
@@ -34,7 +36,9 @@ let
enableACME = rootRoute.tls;
serverAliases = rootRoute.aliases;
locations = lib.listToAttrs (
map (route: lib.nameValuePair route.path (mkLocation route)) routes
map (route: lib.nameValuePair route.path (mkLocation route)) (
builtins.filter (route: route.backend != null) routes
)
);
};
in
@@ -58,7 +62,11 @@ in
++ lib.mapAttrsToList (host: routes: {
assertion = !hasDuplicatePaths routes;
message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once.";
}) routesByHost;
}) routesByHost
++ map (route: {
assertion = !hasUnresolvedBackend route;
message = "modules.reverse-proxy.routes for ${route.host}${route.path} must resolve to a backend URL before actualization.";
}) (builtins.filter hasUnresolvedBackend (lib.attrValues enabledRoutes));
networking.firewall.allowedTCPPorts = [
80
@@ -72,6 +80,13 @@ 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;

View File

@@ -1,105 +0,0 @@
#!/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"

4
registry/address.nix Normal file
View File

@@ -0,0 +1,4 @@
{ lib }:
{
type = lib.types.str;
}