diff options
| author | Mel <mel@rnrd.eu> | 2026-03-31 22:11:10 +0200 |
|---|---|---|
| committer | Mel <mel@rnrd.eu> | 2026-03-31 22:11:10 +0200 |
| commit | 2780fc65523814564153d92ab2d0f19be4ba0e02 (patch) | |
| tree | 472904f62e920551dbaba896a524e01576b5ced1 /modules/vpn/ingress.nix | |
| parent | 7d899f695a1d5a448226ed9479c0e4c52454f595 (diff) | |
| download | network-2780fc65523814564153d92ab2d0f19be4ba0e02.tar.zst network-2780fc65523814564153d92ab2d0f19be4ba0e02.zip | |
VLESS/Reality VPN configuration for DPI evasion
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'modules/vpn/ingress.nix')
| -rw-r--r-- | modules/vpn/ingress.nix | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/modules/vpn/ingress.nix b/modules/vpn/ingress.nix new file mode 100644 index 0000000..d26b1ec --- /dev/null +++ b/modules/vpn/ingress.nix @@ -0,0 +1,184 @@ +{ + config, + lib, + ... +}: + +let + inherit (lib) + imap0 + attrValues + mergeAttrsList + replaceString + concatImapStringsSep + ; + + definition = import ./definition.nix; + + inherit (definition) paths users mask; + + ownAddress = "10.123.X.1"; # ip of host running the ingress vpn (per-interface) + + addressFromTemplate = + index: template: prefix: + "${replaceString "X" (toString (index + 1)) template}/${toString prefix}"; + + ingressName = index: "vpn-ingress${toString index}"; + egressName = "vpn-egress0"; + + egressHost = name: "${name}.rnrd.eu"; +in +{ + boot.kernel.sysctl = { + "net.ipv4.ip_forward" = 1; # allow ipv4 forwarding + }; + + networking.firewall = { + allowedUDPPorts = map (x: x.port) paths; + }; + + age.secrets.ingress-key = { + file = ../../secrets/vpn/ingress-key.age; + }; + + systemd.network = + let + mkNetdev = index: path: { + "10-${ingressName index}" = { + netdevConfig = { + Kind = "wireguard"; + Name = ingressName index; + }; + wireguardConfig = { + PrivateKeyFile = config.age.secrets.ingress-key.path; + ListenPort = path.port; + }; + wireguardPeers = map (user: { + wireguardPeerConfig = { + PublicKey = user.key; + AllowedIPs = [ (addressFromTemplate index user.ip 32) ]; + }; + }) (attrValues users); + }; + }; + + mkNetwork = index: path: { + "10-${ingressName index}" = { + name = ingressName index; + address = [ (addressFromTemplate index ownAddress 24) ]; + routingPolicyRules = [ + { + routingPolicyRuleConfig = { + IncomingInterface = ingressName index; + Table = 100; + }; + } + ]; + }; + }; + + ingressNetdevs = imap0 mkNetdev paths; + + ingressNetworks = imap0 mkNetwork paths; + egressNetworks = [ + { + "20-${egressName}" = { + name = egressName; + linkConfig.ActivationPolicy = "up"; + routes = [ + { + routeConfig = { + Destination = "0.0.0.0/0"; + Table = 100; + Scope = "link"; + }; + } + ]; + }; + } + ]; + in + { + netdevs = mergeAttrsList ingressNetdevs; + networks = mergeAttrsList (ingressNetworks ++ egressNetworks); + }; + + # allow forwarding packets between egress and ingress, but avoid any snat, + # ip should always keep it's origin form, for correct egress routing. + networking.nftables.ruleset = + let + ingressInterfaces = concatImapStringsSep "\", \"" (i: _: ingressName (i - 1)) paths; + in + '' + table inet filter { + chain forward { + type filter hook forward priority 0; policy drop; + + iifname { "${ingressInterfaces}" } oifname "${egressName}" accept + iifname "${egressName}" oifname { "${ingressInterfaces}" } accept + } + } + ''; + + # sing-box is a vpn client supporting various protocols which will allow us + # to configure it in whichever way we want to avoid russian dpi. + # in this case, our communications crossing the borders are relying on vless. + services.sing-box = + let + inboundName = "vpn-in"; + outboundName = egress: "vpn-out-${egress}"; + in + { + enable = true; + settings = { + inbounds = [ + { + type = "tun"; + tag = inboundName; + interface_name = egressName; + inet4_address = "10.123.255.1/30"; + auto_route = false; # we route manually + strict_route = false; + endpoint_independent_nat = true; + } + ]; + + outbounds = map (path: { + type = "vless"; + flow = "xtls-rprx-vision"; + + server = egressHost path.egress; + server_port = 443; + + tag = outboundName path.egress; + uuid = path.info.uuid; + + tls = { + enabled = true; + server_name = "www.${mask}"; + + utls = { + enabled = true; + fingerprint = "chrome"; + }; + + reality = { + enabled = true; + public_key = path.info.public; + short_id = path.info.short; + }; + }; + }) paths; + + route = { + rules = imap0 (index: path: { + inbound = inboundName; + source_ip_cidr = [ (addressFromTemplate index "10.123.X.0" 24) ]; + outbound = outboundName path.egress; + }) paths; + + auto_detect_interface = true; + }; + }; + }; +} |
