summary refs log tree commit diff
path: root/modules/tunnel/ingress.nix
diff options
context:
space:
mode:
Diffstat (limited to 'modules/tunnel/ingress.nix')
-rw-r--r--modules/tunnel/ingress.nix201
1 files changed, 201 insertions, 0 deletions
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;
+        };
+      };
+    };
+}