summaryrefslogtreecommitdiff
path: root/modules/vpn
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-04-24 03:10:11 +0200
committerMel <mel@rnrd.eu>2026-04-24 03:18:05 +0200
commitfa38ea010957a98e778c32b23a8f133b14afdef1 (patch)
tree2548538141908ceafa25c5f8ac7371d054b7bd87 /modules/vpn
parent97e935e0ff718cbec86605bf584a5660812bdce9 (diff)
downloadnetwork-fa38ea010957a98e778c32b23a8f133b14afdef1.tar.zst
network-fa38ea010957a98e778c32b23a8f133b14afdef1.zip
Give the VPN its final name 'Tunnel'
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'modules/vpn')
-rw-r--r--modules/vpn/definition.nix75
-rw-r--r--modules/vpn/egress.nix133
-rw-r--r--modules/vpn/ingress.nix201
3 files changed, 0 insertions, 409 deletions
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;
- };
- };
- };
-}