{ me, config, pkgs, lib, ... }: let inherit (lib) findFirst concatStringsSep; # supposedly the current gold-standard protocol for circumventing dpi! # both xray (egress-side) and sing-box (ingress-side) support various # other protocols, if roskomnadzor learns to sniff out vless fully. protocol = "vless"; inboundTag = "vless-in"; outboundTag = "direct-out"; # nginx stream listens on port 443 and routes by sni: # * mask domains (microsoft.com) go to xray for vless/reality. # * everything else goes to good old nginx! # both backends receive proxy protocol v1 with the real client ip. nginxPort = 8443; xrayPort = 8444; definition = import ./definition.nix; inherit (definition) paths mask; path = findFirst ( p: p.egress == me.name ) (throw "no egress information found for this server!") paths; maskDomains = [ "www.${mask}" mask ]; xrayConfig = { inbounds = [ { port = xrayPort; listen = "127.0.0.1"; inherit protocol; tag = inboundTag; settings = { clients = [ { id = path.info.uuid; flow = "xtls-rprx-vision"; } ]; decryption = "none"; }; streamSettings = { network = "tcp"; security = "reality"; realitySettings = { show = false; dest = "www.${mask}:443"; serverNames = maskDomains; privateKey = "@PRIVATE_KEY@"; shortIds = [ path.info.short ]; }; sockopt = { acceptProxyProtocol = true; }; }; } ]; # and we're out! outbounds = [ { protocol = "freedom"; tag = outboundTag; } ]; routing = { rules = [ { type = "field"; inboundTag = [ inboundTag ]; inherit outboundTag; } ]; }; log = { loglevel = "debug"; }; }; config-file = pkgs.writeText "xray.json" (builtins.toJSON xrayConfig); in { age.secrets.egress-key = { file = path.info.keySecret; }; systemd.services = { # we have to make an xray config on the fly because # xray does not like reading secrets from specific files, # it wants them in plain-text! generate-xray-config = { description = "Generate Xray configuration"; wantedBy = [ "multi-user.target" ]; before = [ "xray.service" ]; partOf = [ "xray.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' mkdir -p /run/xray-configuration cp ${config-file} /run/xray-configuration/xray.json egress_key=$(cat ${config.age.secrets.egress-key.path}) # use sd for replacement as a fancy new tool for this ${pkgs.sd}/bin/sd "@PRIVATE_KEY@" "$egress_key" /run/xray-configuration/xray.json ''; }; xray = { requires = [ "generate-xray-config.service" ]; after = [ "generate-xray-config.service" ]; restartTriggers = [ config-file ]; }; }; services.xray = { enable = true; settingsFile = "/run/xray-configuration/xray.json"; }; # don't unwrap cloudflare addresses, we get the real ip via proxy protocol. foundation.www.behindCloudflare = false; services.nginx = { defaultListen = [ { addr = "127.0.0.1"; port = nginxPort; ssl = true; proxyProtocol = true; } ]; # nginx stream sits on port 443 and routes traffic by sni to the two # backends we have. streamConfig = '' map $ssl_preread_server_name $backend { ${concatStringsSep "\n" (map (d: "${d} xray;") maskDomains)} default web; } upstream xray { server 127.0.0.1:${toString xrayPort}; } upstream web { server 127.0.0.1:${toString nginxPort}; } server { listen 443; listen [::]:443; ssl_preread on; proxy_pass $backend; proxy_protocol on; } ''; commonHttpConfig = '' set_real_ip_from 127.0.0.1; real_ip_header proxy_protocol; ''; }; }