modules/vpn: fix the vpn-* systemd services

This commit is contained in:
Colin 2024-02-20 20:40:46 +00:00
parent 71025329e7
commit 34524ea3e4
1 changed files with 25 additions and 6 deletions

View File

@ -4,6 +4,20 @@
# docs:
# - wireguard (nixos): <https://nixos.wiki/wiki/WireGuard#Setting_up_WireGuard_with_systemd-networkd>
# - wireguard (arch): <https://wiki.archlinux.org/title/WireGuard>
#
# to route all internet traffic through a VPN endpoint, run `systemctl start vpn-${vpnName}`
# to show the routing table: `ip rule`
# to show the NAT rules used for bridging: `sudo iptables -t nat --list-rules -v`
#
# the rough idea here is:
# 1. each VPN has an IP address: if we originate a packet, and the source address is the VPN's address, then it gets routed over the VPN trivially.
# 2a. create a bridge device for each VPN. traffic exiting that bridge is source-NAT'd to the VPN's address.
# 2b. to route all traffic for a specific application (or container), give it access to only the bridge device.
# 3a. create a separate routing table for each VPN, with table id = ID.
# 3b. if a packet enters the VPN's table then it will be routed via the VPN.
# 3c. to apply a VPN to all internet traffic, system-wide, a rule is added that forces each packet to enter that VPN's routing table.
# - that's done with `systemctl start vpn-$VPN`.
# - the VPN acts as the default route. so traffic destined to e.g. a LAN device do not traverse the VPN in this case. only internet traffic is VPN'd.
{ config, lib, pkgs, sane-lib, ... }:
let
@ -82,6 +96,9 @@ let
};
});
mkVpnConfig = name: { id, dns, endpoint, publicKey, addrV4, privateKeyFile, bridgeDevice, ... }: let
prioMain = id + 100;
prioWgTable = id + 200;
prioFwMark = id + 300;
fwmark = id + 10000;
bridgeAddrV4 = "10.20.${builtins.toString id}.1/24";
in {
@ -91,6 +108,7 @@ let
message = "multiple VPNs share id ${id}";
}
];
systemd.network.netdevs."98-${name}" = {
# see: `man 5 systemd.netdev`
netdevConfig = {
@ -99,7 +117,7 @@ let
};
wireguardConfig = {
PrivateKeyFile = privateKeyFile;
FirewallMark = id;
FirewallMark = fwmark;
};
wireguardPeers = [{
wireguardPeerConfig = {
@ -112,6 +130,7 @@ let
};
}];
};
systemd.network.networks."50-${name}" = {
# see: `man 5 systemd.network`
matchConfig.Name = name;
@ -125,11 +144,11 @@ let
routingPolicyRules = [
{
routingPolicyRuleConfig = {
# send traffic from the the container bridge out over the VPN.
# send traffic from the container bridge out over the VPN.
# the bridge itself does source nat (SNAT) to rewrite the packet source address to that of the VPNs
# -- but that happens in POSTROUTING: *after* the kernel decides which interface to give the packet to next.
# therefore, we have to route here based on the packet's address as it is in PREROUTING, i.e. the bridge address. weird!
Priority = id;
Priority = prioWgTable;
From = bridgeAddrV4;
Table = id;
};
@ -149,6 +168,7 @@ let
netdevConfig.Kind = "bridge";
netdevConfig.Name = bridgeDevice;
};
systemd.network.networks."51-${bridgeDevice}" = {
matchConfig.Name = bridgeDevice;
networkConfig.Description = "NATs inbound traffic to ${name}, intended for container isolation";
@ -163,6 +183,7 @@ let
# networkConfig.DHCPServer = true;
linkConfig.RequiredForOnline = false;
};
networking.localCommands = with pkgs; ''
# view rules with:
# - `sudo iptables -t nat --list-rules -v`
@ -194,15 +215,13 @@ let
systemd.services."vpn-${name}" = {
path = with pkgs; [ iproute2 ];
serviceConfig = let
prioMain = id - 200;
prioFwMark = id - 100;
mkScript = verb: pkgs.writeShellScript "vpn-${name}-${verb}" ''
# first, allow all non-default routes (prefix-length != 0) a chance to route the packet.
# - this allows the wireguard tunnel itself to pass traffic via our LAN gateway.
# - incidentally, it allows traffic to LAN devices and other machine-local or virtual networks.
ip rule ${verb} from all lookup main suppress_prefixlength 0 priority ${builtins.toString prioMain}
# if packet hasn't gone through the wg device yet (fwmark), then move it to the table which will cause it to.
ip rule ${verb} not from all fwmark ${builtins.toString id} lookup ${builtins.toString id} priority ${builtins.toString prioFwMark}
ip rule ${verb} not from all fwmark ${builtins.toString fwmark} lookup ${builtins.toString id} priority ${builtins.toString prioFwMark}
'';
in {
ExecStart = ''${mkScript "add"}'';