{ 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"; egressAddress = "10.123.255.1/16"; # /16 encompasses all possible subnet addresses egressMTU = 1400; 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; allowedTCPPorts = map (x: x.port) paths; checkReversePath = "loose"; }; age.secrets.ingress-key = { file = ../../secrets/vpn/ingress-key.age; owner = "systemd-network"; }; 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: { 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 = [ { IncomingInterface = ingressName index; Table = 100; } ]; }; }; ingressNetdevs = imap0 mkNetdev paths; ingressNetworks = imap0 mkNetwork paths; egressNetworks = [ { "20-${egressName}" = { name = egressName; address = [ egressAddress ]; networkConfig = { IPv4ReversePathFilter = "loose"; }; linkConfig = { ActivationPolicy = "up"; RequiredForOnline = "no"; # does not count as online MTUBytes = toString egressMTU; }; routes = [ { 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; address = [ egressAddress ]; mtu = egressMTU; auto_route = false; # we route manually strict_route = false; endpoint_independent_nat = true; } ]; outbounds = map (path: { type = "vless"; flow = "xtls-rprx-vision"; packet_encoding = "xudp"; 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; }; log = { level = "debug"; timestamp = true; }; }; }; }