diff --git a/configuration.nix b/configuration.nix index 4d42a46..f461dd5 100644 --- a/configuration.nix +++ b/configuration.nix @@ -18,6 +18,7 @@ ./modules/sops.nix ./modules/power.nix ./modules/xray.nix + ./modules/dpi-tunnel.nix ./modules/ollama.nix ./modules/lemonade.nix ./modules/gaming.nix diff --git a/modules/dpi-tunnel.nix b/modules/dpi-tunnel.nix new file mode 100644 index 0000000..51bd55a --- /dev/null +++ b/modules/dpi-tunnel.nix @@ -0,0 +1,145 @@ +# Xray VLESS + REALITY + post-quantum VLESS Encryption tunnel around DPI that +# throttles WireGuard. Server side (terraform/ansible) lives in the +# dpi-tunnel repo; keys and server in secrets/dpi-tunnel.yaml. +# +# Full tunnel: everything goes via tun1 except what the main table routes +# more specifically than a default (LAN, wg0/wg2 prefixes — their WireGuard +# endpoints then go through the tunnel). Policy rules: +# 999 fwmark -> main xray's own uplink to the server +# 1000 to -> 1011 forced prefixes (beat even specific wg* routes) +# 1001 main, suppress /0 specific routes stay where they are +# 1002 -> 1011 everything else +# `systemctl stop dpi-tunnel` removes all of it (back to plain uplink). +{ config, lib, pkgs, utils, ... }: + +let + dev = "tun1"; + table = "1011"; + mark = 4113; # 0x1011 + socksPort = 11081; + sopsFile = "/etc/nixos/secrets/dpi-tunnel.yaml"; + keys = [ "server" "server_name" "uuid" "short_id" "reality_password" "vless_encryption" "routes" ]; + secret = key: config.sops.secrets."dpi-tunnel/${key}".path; + ip = "${pkgs.iproute2}/bin/ip"; + jq = lib.getExe pkgs.jq; + + settings = { + log = { loglevel = "warning"; access = "none"; }; # no per-site journal trail + inbounds = [ + { + tag = "tun-in"; + protocol = "tun"; + settings = { name = dev; MTU = 1500; }; + } + { + tag = "socks-in"; + listen = "127.0.0.1"; + port = socksPort; + protocol = "socks"; + settings.udp = true; + } + ]; + outbounds = [{ + tag = "proxy"; + protocol = "vless"; + # UDP via XUDP; allow UDP/443 (QUIC, and WireGuard endpoints on 443), + # which vision rejects by default. TCP stays unmuxed. + mux = { + enabled = true; + concurrency = -1; + xudpConcurrency = 8; + xudpProxyUDP443 = "allow"; + }; + settings.vnext = [{ + address = { _secret = secret "server"; }; + port = 443; + users = [{ + id = { _secret = secret "uuid"; }; + flow = "xtls-rprx-vision"; + encryption = { _secret = secret "vless_encryption"; }; + }]; + }]; + streamSettings = { + network = "raw"; + security = "reality"; + realitySettings = { + serverName = { _secret = secret "server_name"; }; + fingerprint = "chrome"; + password = { _secret = secret "reality_password"; }; + shortId = { _secret = secret "short_id"; }; + }; + # Marked so rule 999 keeps the uplink off tun1 (no loop). + sockopt.mark = mark; + }; + }]; + }; +in +{ + sops.secrets = lib.genAttrs (map (k: "dpi-tunnel/${k}") keys) (name: { + inherit sopsFile; + key = lib.removePrefix "dpi-tunnel/" name; + restartUnits = [ "dpi-tunnel.service" ]; + }); + + systemd.services.dpi-tunnel = { + description = "Xray VLESS Reality PQ client"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + preStart = utils.genJqSecretsReplacementSnippet settings "/run/dpi-tunnel/config.json"; + postStart = '' + for _ in $(seq 50); do [ -e /sys/class/net/${dev} ] && break; sleep 0.1; done + ${ip} link set ${dev} up + for v in -4 -6; do + ${ip} $v route replace default dev ${dev} table ${table} + ${ip} $v rule add fwmark ${toString mark} lookup main priority 999 + ${ip} $v rule add lookup main suppress_prefixlength 0 priority 1001 + ${ip} $v rule add lookup ${table} priority 1002 + done + for cidr in $(${jq} -r '.[]' ${secret "routes"}); do + ${ip} route replace "$cidr" dev ${dev} table ${table} + ${ip} rule add to "$cidr" lookup ${table} priority 1000 + done + ''; + postStop = '' + for v in -4 -6; do + for p in 999 1000 1001 1002; do + while ${ip} $v rule del priority $p 2>/dev/null; do :; done + done + ${ip} $v route flush table ${table} 2>/dev/null || true + done + ''; + serviceConfig = { + ExecStart = "${lib.getExe pkgs.xray} run -c /run/dpi-tunnel/config.json"; + RuntimeDirectory = "dpi-tunnel"; + RuntimeDirectoryMode = "0700"; + AmbientCapabilities = [ "CAP_NET_ADMIN" ]; + CapabilityBoundingSet = [ "CAP_NET_ADMIN" ]; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + Restart = "on-failure"; + RestartSec = 5; + }; + }; + + # Xray keeps tun UDP sessions alive while packets arrive, so a WireGuard + # keepalive pins a session whose mux link died (suspend, Wi-Fi loss) + # forever. Start clean after resume and on every new uplink. + systemd.services.dpi-tunnel-resume = rec { + description = "Restart dpi-tunnel after resume"; + after = [ "suspend.target" "hibernate.target" "hybrid-sleep.target" "suspend-then-hibernate.target" ]; + wantedBy = after; + serviceConfig.Type = "oneshot"; + script = "systemctl try-restart dpi-tunnel.service"; + }; + + networking.networkmanager.dispatcherScripts = [{ + type = "basic"; + source = pkgs.writeShellScript "dpi-tunnel-uplink" '' + case "$1" in lo|wg*|tun*|docker*|br-*|veth*|virbr*) exit 0 ;; esac + [ "$2" = up ] && systemctl try-restart dpi-tunnel.service + exit 0 + ''; + }]; +} diff --git a/secrets/dpi-tunnel.yaml.example b/secrets/dpi-tunnel.yaml.example new file mode 100644 index 0000000..f2b2fd7 --- /dev/null +++ b/secrets/dpi-tunnel.yaml.example @@ -0,0 +1,10 @@ +# sops-encrypted; generate with scripts/gen-secrets.sh in the dpi-tunnel repo +server: 203.0.113.10 +server_name: www.microsoft.com +uuid: 00000000-0000-0000-0000-000000000000 +short_id: 0123456789abcdef +reality_private_key: +reality_password: +vless_decryption: +vless_encryption: mlkem768x25519plus.native.0rtt.<...> +routes: '["198.51.100.1/32"]' # forced via tun1 even over specific wg* routes