{
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;
'';
};
}