summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--machines/taupe/default.nix17
-rw-r--r--machines/taupe/devices.nix18
-rw-r--r--machines/taureau/default.nix14
-rw-r--r--machines/truite/default.nix4
-rw-r--r--modules/vpn.nix31
-rw-r--r--modules/vpn/definition.nix63
-rw-r--r--modules/vpn/egress.nix108
-rw-r--r--modules/vpn/ingress.nix184
-rw-r--r--secrets/secrets.nix12
-rw-r--r--secrets/vpn/egress-key-taupe.agebin0 -> 1137 bytes
-rw-r--r--secrets/vpn/egress-key-taureau.agebin0 -> 1136 bytes
-rw-r--r--secrets/vpn/ingress-key.age (renamed from secrets/wireguard-private-key.age)bin1357 -> 1357 bytes
12 files changed, 375 insertions, 76 deletions
diff --git a/machines/taupe/default.nix b/machines/taupe/default.nix
index 4f60eab..caa5a83 100644
--- a/machines/taupe/default.nix
+++ b/machines/taupe/default.nix
@@ -3,7 +3,7 @@
 {
   imports = [
     ../../modules/common.nix
-    ../../modules/vpn.nix
+    ../../modules/vpn/egress.nix
 
     ./hardware.nix
     ./devices.nix
@@ -11,20 +11,13 @@
 
   foundation = {
     www = {
-      enable = true;
+      enable = false;
 
-      public = true;
-      tailnet = true;
+      public = false;
+      tailnet = false;
     };
 
-    monitoring = {
-      client.enable = true;
-      services = [
-        "base"
-      ];
-    };
-
-    wireguard.server.externalInterface = "enp1s0";
+    monitoring.client.enable = false;
   };
 
   system.stateVersion = "25.05";
diff --git a/machines/taupe/devices.nix b/machines/taupe/devices.nix
index 5dcfee1..a07f794 100644
--- a/machines/taupe/devices.nix
+++ b/machines/taupe/devices.nix
@@ -3,7 +3,6 @@
 {
   boot = {
     loader.systemd-boot.enable = true;
-    kernelModules = [ "wireguard" ];
   };
 
   zramSwap = {
@@ -21,8 +20,7 @@
     ];
   };
 
-  # Static IPv6 network configuration
-  # + soliciting of IPv4 via DHCP.
+  # static ipv6 + dhcp ipv4
   systemd.network.enable = true;
   systemd.network.networks."10-wan" = {
     name = "enp1s0";
@@ -34,18 +32,6 @@
   services.resolved = {
     llmnr = "false";
     extraConfig = "MulticastDNS=no";
-    dnssec = "false"; 
-  };
-  
-  virtualisation.docker.daemon.settings = {
-    "experimental" = true;
-    "ipv6" = true;
-    "ip6tables" = true;
-    "fixed-cidr-v6" = "fc00:d0c:b1b1::/48";
-    "bip" = "172.17.0.1/24";
-    "default-address-pools" = [
-      { base = "172.17.0.0/16"; size = 24; }
-      { base = "fc00:d0c::/32"; size = 48; }
-    ];
+    dnssec = "false";
   };
 }
diff --git a/machines/taureau/default.nix b/machines/taureau/default.nix
index 38172cf..0f857a0 100644
--- a/machines/taureau/default.nix
+++ b/machines/taureau/default.nix
@@ -3,7 +3,7 @@
 {
   imports = [
     ../../modules/common.nix
-    ../../modules/vpn.nix
+    ../../modules/vpn/egress.nix
 
     ./hardware.nix
     ./devices.nix
@@ -11,19 +11,13 @@
 
   foundation = {
     www = {
-      enable = true;
+      enable = false;
 
-      public = true;
+      public = false;
       tailnet = false;
     };
 
-    monitoring = {
-      client.enable = true;
-      services = [
-        "base"
-        "tailnet"
-      ];
-    };
+    monitoring.client.enable = false;
 
     wireguard.server.externalInterface = "enp6s16";
   };
diff --git a/machines/truite/default.nix b/machines/truite/default.nix
index f8dbaaa..01f82f5 100644
--- a/machines/truite/default.nix
+++ b/machines/truite/default.nix
@@ -3,7 +3,7 @@
 {
   imports = [
     ../../modules/common.nix
-    ../../modules/vpn.nix
+    ../../modules/vpn/ingress.nix
 
     ./hardware.nix
     ./devices.nix
@@ -24,8 +24,6 @@
         "tailnet"
       ];
     };
-
-    wireguard.server.externalInterface = "eth0";
   };
 
   system.stateVersion = "25.05";
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;
+        };
+      };
+    };
+}
diff --git a/secrets/secrets.nix b/secrets/secrets.nix
index 3aa2880..7332beb 100644
--- a/secrets/secrets.nix
+++ b/secrets/secrets.nix
@@ -42,11 +42,15 @@ in
     fourmi
   ] ++ allAdmins;
 
-  "wireguard-private-key.age".publicKeys = [
-    renard
-
-    taupe
+  "vpn/ingress-key.age".publicKeys = [
     truite
+  ] ++ allAdmins;
+
+  "vpn/egress-key-taureau.age".publicKeys = [
     taureau
   ] ++ allAdmins;
+
+  "vpn/egress-key-taupe.age".publicKeys = [
+    taupe
+  ] ++ allAdmins;
 }
diff --git a/secrets/vpn/egress-key-taupe.age b/secrets/vpn/egress-key-taupe.age
new file mode 100644
index 0000000..2e9c9cd
--- /dev/null
+++ b/secrets/vpn/egress-key-taupe.age
Binary files differdiff --git a/secrets/vpn/egress-key-taureau.age b/secrets/vpn/egress-key-taureau.age
new file mode 100644
index 0000000..f3a72c2
--- /dev/null
+++ b/secrets/vpn/egress-key-taureau.age
Binary files differdiff --git a/secrets/wireguard-private-key.age b/secrets/vpn/ingress-key.age
index 1a118a7..1a118a7 100644
--- a/secrets/wireguard-private-key.age
+++ b/secrets/vpn/ingress-key.age
Binary files differ