From fa38ea010957a98e778c32b23a8f133b14afdef1 Mon Sep 17 00:00:00 2001 From: Mel Date: Fri, 24 Apr 2026 03:10:11 +0200 Subject: Give the VPN its final name 'Tunnel' Signed-off-by: Mel --- modules/tunnel/definition.nix | 75 ++++++++++++++++ modules/tunnel/egress.nix | 133 ++++++++++++++++++++++++++++ modules/tunnel/ingress.nix | 201 ++++++++++++++++++++++++++++++++++++++++++ modules/vpn/definition.nix | 75 ---------------- modules/vpn/egress.nix | 133 ---------------------------- modules/vpn/ingress.nix | 201 ------------------------------------------ 6 files changed, 409 insertions(+), 409 deletions(-) create mode 100644 modules/tunnel/definition.nix create mode 100644 modules/tunnel/egress.nix create mode 100644 modules/tunnel/ingress.nix delete mode 100644 modules/vpn/definition.nix delete mode 100644 modules/vpn/egress.nix delete mode 100644 modules/vpn/ingress.nix (limited to 'modules') diff --git a/modules/tunnel/definition.nix b/modules/tunnel/definition.nix new file mode 100644 index 0000000..74ae268 --- /dev/null +++ b/modules/tunnel/definition.nix @@ -0,0 +1,75 @@ +# definition of the network layout which supports our tunnel +# architecture. + +{ + # these are the available paths which a user is allowed to take + # to reach a specified egress server. + # when a user connects to a port defined here via wireguard, + # the primary ingress server (us), will establish a connection with + # the user and the backend egress server (this time, not via wireguard, + # but with a specific dpi-evading protocol), and route the users packets + # through to the egress. + paths = [ + { + port = 50501; + egress = "taupe"; + + info = { + uuid = "328c90a0-20ae-4d4c-9e54-97e9ab41c053"; + short = "b20629b505f39194"; + + public = "_837k5niQBE-qmgqpZalH3cS_fAIBwv8dwMoDW1uvgk"; + keySecret = ../../secrets/tunnel/egress-key-taupe.age; + }; + } + { + port = 50502; + egress = "taureau"; + + info = { + uuid = "826b8598-ed75-4782-9b7e-27e0e16e1141"; + short = "8f7e9f8a3fa46bf0"; + + public = "HvR4iP8URERpPBM4oG1Bjfw3mIfN0MoL2x6MHlt_TUM"; + keySecret = ../../secrets/tunnel/egress-key-taureau.age; + }; + } + ]; + + # there are our users who are allowed to connect to any of our "paths". + # their ip is always a template, with 'X' representing the path index. + users = { + mel = { + key = "vnZoHXapCLLUhZ8A8R5W0iJ8LpWVLve29z41kkoT0BU="; + ip = "10.123.X.101"; + }; + + andrei = { + key = "qqU4uYImLfUohIwl4KBshPtTINFcs0JVALjbmwpfxRg="; + ip = "10.123.X.102"; + }; + + sergo = { + key = "qbZGMNIDZFCJC6SHtlyNIlIdGWHELceXClJCcagrj2Y="; + ip = "10.123.X.103"; + }; + + fedor = { + key = "tEO9r8+jTpu8TBRmZ+/v087IgD/QfmofLUKs249i/F0="; + ip = "10.123.X.104"; + }; + }; + + # we use a website as a "mask" for vless/reality, which will tell our peers + # to pretend as if they're a user and a well-known website communicating with + # each other, even though they know that the keys don't actually match up, + # it's not possible to see that on the outside. + mask = "microsoft.com"; + + # we don't actually need this to configure the tunnel, but this is + # the public key of the ingress interface. + # when creating wireguard vpn configurations for the users, this + # is the public key of the server peer at `tunnel.rnrd.eu`. + # the matching private key of the pair is the secret `tunnel/ingress-key`. + ingress.public = "s5yyPCJiN0uqW0jzKIbYCF7I9TthymiRzpNt466XeWk="; +} diff --git a/modules/tunnel/egress.nix b/modules/tunnel/egress.nix new file mode 100644 index 0000000..7858751 --- /dev/null +++ b/modules/tunnel/egress.nix @@ -0,0 +1,133 @@ +{ + me, + config, + pkgs, + lib, + ... +}: + +let + inherit (lib) findFirst; + + # this is the https port, we use it to try to trick dpi into thinking + # we are just serving normal encrypted web traffic, nothing interesting! :) + # this does mean that our egress servers are unable to support normal www + # services which we put on machines by default, which is okay. + port = 443; + + # 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"; + + definition = import ./definition.nix; + inherit (definition) paths mask; + + path = findFirst ( + p: p.egress == me.name + ) (throw "no egress information found for this server!") paths; + + xrayConfig = { + inbounds = [ + { + inherit port 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 = [ + "www.${mask}" + mask + ]; + privateKey = "@PRIVATE_KEY@"; + shortIds = [ path.info.short ]; + }; + }; + } + ]; + + # 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 +{ + networking.firewall.allowedTCPPorts = [ port ]; + + 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"; + }; +} diff --git a/modules/tunnel/ingress.nix b/modules/tunnel/ingress.nix new file mode 100644 index 0000000..a1260c8 --- /dev/null +++ b/modules/tunnel/ingress.nix @@ -0,0 +1,201 @@ +{ + 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: "tunnel-ingress${toString index}"; + egressName = "tunnel-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/tunnel/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. + # also adapt mss to outgoing mss value, so that we don't shatter packets. + 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; + + tcp flags syn tcp option maxseg size set rt mtu + + 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 = "tunnel-in"; + outboundName = egress: "tunnel-out-${egress}"; + in + { + enable = true; + settings = { + inbounds = [ + { + type = "tun"; + tag = inboundName; + interface_name = egressName; + address = [ egressAddress ]; + mtu = egressMTU; + stack = "gvisor"; + 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; + }; + + log = { + level = "debug"; + timestamp = true; + }; + }; + }; +} diff --git a/modules/vpn/definition.nix b/modules/vpn/definition.nix deleted file mode 100644 index 60ea5d0..0000000 --- a/modules/vpn/definition.nix +++ /dev/null @@ -1,75 +0,0 @@ -# definition of the network layout which supports our vpn -# architecture. - -{ - # these are the available paths which a user is allowed to take - # to reach a specified egress server. - # when a user connects to a port defined here via wireguard, - # the primary ingress server (us), will establish a connection with - # the user and the backend egress server (this time, not via wireguard, - # but with a specific dpi-evading protocol), and route the users packets - # through to the egress. - paths = [ - { - port = 50501; - egress = "taupe"; - - info = { - uuid = "328c90a0-20ae-4d4c-9e54-97e9ab41c053"; - short = "b20629b505f39194"; - - public = "_837k5niQBE-qmgqpZalH3cS_fAIBwv8dwMoDW1uvgk"; - keySecret = ../../secrets/vpn/egress-key-taupe.age; - }; - } - { - port = 50502; - egress = "taureau"; - - info = { - uuid = "826b8598-ed75-4782-9b7e-27e0e16e1141"; - short = "8f7e9f8a3fa46bf0"; - - public = "HvR4iP8URERpPBM4oG1Bjfw3mIfN0MoL2x6MHlt_TUM"; - keySecret = ../../secrets/vpn/egress-key-taureau.age; - }; - } - ]; - - # there are our users who are allowed to connect to any of our "paths". - # their ip is always a template, with 'X' representing the path index. - users = { - mel = { - key = "vnZoHXapCLLUhZ8A8R5W0iJ8LpWVLve29z41kkoT0BU="; - ip = "10.123.X.101"; - }; - - andrei = { - key = "qqU4uYImLfUohIwl4KBshPtTINFcs0JVALjbmwpfxRg="; - ip = "10.123.X.102"; - }; - - sergo = { - key = "qbZGMNIDZFCJC6SHtlyNIlIdGWHELceXClJCcagrj2Y="; - ip = "10.123.X.103"; - }; - - fedor = { - key = "tEO9r8+jTpu8TBRmZ+/v087IgD/QfmofLUKs249i/F0="; - ip = "10.123.X.104"; - }; - }; - - # we use a website as a "mask" for vless/reality, which will tell our peers - # to pretend as if they're a user and a well-known website communicating with - # each other, even though they know that the keys don't actually match up, - # it's not possible to see that on the outside. - mask = "microsoft.com"; - - # we don't actually need this to configure the tunnel, but this is - # the public key of the ingress interface. - # when creating wireguard vpn configurations for the users, this - # is the public key of the server peer at `tunnel.rnrd.eu`. - # the matching private key of the pair is the secret `vpn/ingress-key`. - ingress.public = "s5yyPCJiN0uqW0jzKIbYCF7I9TthymiRzpNt466XeWk="; -} diff --git a/modules/vpn/egress.nix b/modules/vpn/egress.nix deleted file mode 100644 index 7858751..0000000 --- a/modules/vpn/egress.nix +++ /dev/null @@ -1,133 +0,0 @@ -{ - me, - config, - pkgs, - lib, - ... -}: - -let - inherit (lib) findFirst; - - # this is the https port, we use it to try to trick dpi into thinking - # we are just serving normal encrypted web traffic, nothing interesting! :) - # this does mean that our egress servers are unable to support normal www - # services which we put on machines by default, which is okay. - port = 443; - - # 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"; - - definition = import ./definition.nix; - inherit (definition) paths mask; - - path = findFirst ( - p: p.egress == me.name - ) (throw "no egress information found for this server!") paths; - - xrayConfig = { - inbounds = [ - { - inherit port 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 = [ - "www.${mask}" - mask - ]; - privateKey = "@PRIVATE_KEY@"; - shortIds = [ path.info.short ]; - }; - }; - } - ]; - - # 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 -{ - networking.firewall.allowedTCPPorts = [ port ]; - - 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"; - }; -} diff --git a/modules/vpn/ingress.nix b/modules/vpn/ingress.nix deleted file mode 100644 index 6c6a78e..0000000 --- a/modules/vpn/ingress.nix +++ /dev/null @@ -1,201 +0,0 @@ -{ - 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. - # also adapt mss to outgoing mss value, so that we don't shatter packets. - 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; - - tcp flags syn tcp option maxseg size set rt mtu - - 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; - stack = "gvisor"; - 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; - }; - - log = { - level = "debug"; - timestamp = true; - }; - }; - }; -} -- cgit 1.4.1