From 4598993dfe670004908a50b065991ce9ec79be73 Mon Sep 17 00:00:00 2001 From: alexion Date: Mon, 17 Aug 2026 12:45:40 -0400 Subject: [PATCH] prototype copyparty reverse proxy config --- guests/copyparty.nix | 10 +++ hosts/pikachu/default.nix | 1 + modules/services/copyparty.nix | 24 ++++- prototypes/copyparty-rproxy-config/run.sh | 105 ++++++++++++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100755 prototypes/copyparty-rproxy-config/run.sh diff --git a/guests/copyparty.nix b/guests/copyparty.nix index 5ac01d0..4d5b7fb 100644 --- a/guests/copyparty.nix +++ b/guests/copyparty.nix @@ -43,6 +43,10 @@ in enable = true; inherit (cfg) accountName stateDir; passwordFile = "/run/secrets/${cfg.passwordSecret}"; + reverseProxy = { + enable = cfg.reverseProxy.enable; + inherit (cfg.reverseProxy) trustedSources; + }; volumes = lib.mapAttrs (_urlPath: volume: { inherit (volume) path access flags; }) cfg.volumes; @@ -77,6 +81,12 @@ in description = "Host path bind-mounted as Copyparty's state directory."; }; + reverseProxy.trustedSources = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "IP ranges that may supply Copyparty's forwarded client IP header."; + }; + volumes = lib.mkOption { type = lib.types.attrsOf volumeType; default = { }; diff --git a/hosts/pikachu/default.nix b/hosts/pikachu/default.nix index 60ad6d4..475268b 100644 --- a/hosts/pikachu/default.nix +++ b/hosts/pikachu/default.nix @@ -101,6 +101,7 @@ enable = true; host = "files.alexion.dev"; backend = "http://10.23.20.126:3923"; + trustedSources = [ "10.23.20.109/32" ]; }; volumes = { "/inbox" = { diff --git a/modules/services/copyparty.nix b/modules/services/copyparty.nix index 481f562..d57174d 100644 --- a/modules/services/copyparty.nix +++ b/modules/services/copyparty.nix @@ -48,6 +48,14 @@ let ''} ''; + renderTrustedSource = source: " xff-src: ${source}"; + + renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable '' + rproxy: 1 + xff-hdr: x-forwarded-for + ${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)} + ''; + renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes); configScript = pkgs.writeShellScript "copyparty-config" '' @@ -68,7 +76,7 @@ let ses-db: ${cfg.stateDir}/sessions.db chpw-db: ${cfg.stateDir}/chpw.json shr-db: ${cfg.stateDir}/shares.db - + ${renderedReverseProxy} [accounts] ${cfg.accountName}: $password @@ -128,6 +136,16 @@ in default = { }; description = "Copyparty volumes keyed by their URL path."; }; + + reverseProxy = { + enable = lib.mkEnableOption "reverse-proxy trust settings for Copyparty"; + + trustedSources = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "IP ranges that may supply Copyparty's forwarded client IP header."; + }; + }; }; config = lib.mkIf cfg.enable { @@ -136,6 +154,10 @@ in 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} = { diff --git a/prototypes/copyparty-rproxy-config/run.sh b/prototypes/copyparty-rproxy-config/run.sh new file mode 100755 index 0000000..da19ce5 --- /dev/null +++ b/prototypes/copyparty-rproxy-config/run.sh @@ -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" <"$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"