summary refs log tree commit diff
path: root/modules/vpn/ingress.nix
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-03-31 22:11:10 +0200
committerMel <mel@rnrd.eu>2026-03-31 22:11:10 +0200
commit2780fc65523814564153d92ab2d0f19be4ba0e02 (patch)
tree472904f62e920551dbaba896a524e01576b5ced1 /modules/vpn/ingress.nix
parent7d899f695a1d5a448226ed9479c0e4c52454f595 (diff)
downloadnetwork-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.nix184
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;
+        };
+      };
+    };
+}