diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/vpn.nix | 31 | ||||
| -rw-r--r-- | modules/vpn/definition.nix | 63 | ||||
| -rw-r--r-- | modules/vpn/egress.nix | 108 | ||||
| -rw-r--r-- | modules/vpn/ingress.nix | 184 |
4 files changed, 355 insertions, 31 deletions
diff --git a/modules/vpn.nix b/modules/vpn.nix deleted file mode 100644 index 6772c2a..0000000 --- a/modules/vpn.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ ... }: - -{ - # these are the common peers that will want to access our hosts - # that run the vpn in different parts of the world. - foundation = { - wireguard.server = { - enable = true; - # has to be set by the individual server running the vpn: - # like, for example: - # externalInterface = "eth0"; - - peers = { - mel = { - key = "vnZoHXapCLLUhZ8A8R5W0iJ8LpWVLve29z41kkoT0BU="; - ip = 2; - }; - - andrei = { - key = "qqU4uYImLfUohIwl4KBshPtTINFcs0JVALjbmwpfxRg="; - ip = 3; - }; - - sergo = { - key = "qbZGMNIDZFCJC6SHtlyNIlIdGWHELceXClJCcagrj2Y="; - ip = 4; - }; - }; - }; - }; -} diff --git a/modules/vpn/definition.nix b/modules/vpn/definition.nix new file mode 100644 index 0000000..0eb2ac1 --- /dev/null +++ b/modules/vpn/definition.nix @@ -0,0 +1,63 @@ +# 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"; + }; + }; + + # 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"; +} diff --git a/modules/vpn/egress.nix b/modules/vpn/egress.nix new file mode 100644 index 0000000..699d107 --- /dev/null +++ b/modules/vpn/egress.nix @@ -0,0 +1,108 @@ +{ + 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"; + + 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 = pkgs.writeText "xray.json" ( + builtins.toJSON { + inbounds = [ + { + inherit port protocol; + + 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 = "direct"; + } + ]; + } + ); +in +{ + networking.firewall.allowedTCPPorts = [ port ]; + + age.secrets.egress-key = { + file = path.info.keySecret; + }; + + # 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! + systemd.services.generate-xray-config = { + before = [ "xray.service" ]; + requiredBy = [ "xray.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p /run/xray-configuration + cp ${xrayConfig} /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 + + chown root:xray /run/xray-configuration/xray.json + chmod 640 /run/xray-configuration/xray.json + ''; + }; + + services.xray = { + enable = true; + settingsFile = "/run/xray-configuration/xray.json"; + }; +} 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; + }; + }; + }; +} |
