diff --git a/hosts/common/programs/sane-scripts.nix b/hosts/common/programs/sane-scripts.nix index d7171b61..709b40fc 100644 --- a/hosts/common/programs/sane-scripts.nix +++ b/hosts/common/programs/sane-scripts.nix @@ -229,7 +229,6 @@ in fwmark=${builtins.toString vpnCfg.fwmark} priorityMain=${builtins.toString vpnCfg.priorityMain} priorityFwMark=${builtins.toString vpnCfg.priorityFwMark} - bridgeDevice=${vpnCfg.bridgeDevice} addrV4=${vpnCfg.addrV4} name=${vpnCfg.name} dns=(${lib.concatStringsSep " " vpnCfg.dns}) diff --git a/modules/vpn.nix b/modules/vpn.nix index e7373b41..ee4f13ac 100644 --- a/modules/vpn.nix +++ b/modules/vpn.nix @@ -6,18 +6,21 @@ # - wireguard (arch): # # to route all internet traffic through a VPN endpoint, run `systemctl start vpn-${vpnName}` +# to route an application's traffic through a VPN: `sane-vpn do ${vpnName} ${command[@]}` # 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. +# 2a. create a separate routing table for each VPN, with table id = ID. +# 2b. if a packet enters the VPN's table then it will be routed via the VPN. +# 2c. 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. +# 3. to apply a VPN to internet traffic selectively, just proxy an applications traffic into the VPN device +# 3a. use a network namespace and a userspace TCP stack (e.g. pasta/slirp4netns). +# 3b. attach the VPN device to a bridge device, then connect that to a network namespace by using a veth pair. +# 3c. juse use `sanebox`, which abstracts the above options. { config, lib, pkgs, sane-lib, ... }: let @@ -41,6 +44,7 @@ let fwmark = mkOption { type = types.int; }; + # priority*: used externally, by e.g. `sane-vpn` priorityMain = mkOption { type = types.int; }; @@ -86,13 +90,6 @@ let dns servers to use for traffic associated with this VPN. ''; }; - bridgeDevice = mkOption { - type = types.str; - default = "br-${name}"; - description = '' - name of the bridge net device which will be created and configured so as to route all its outbound traffic over the VPN. - ''; - }; privateKeyFile = mkOption { type = types.either types.str types.path; description = '' @@ -111,9 +108,7 @@ let priorityFwMark = config.id + 300; }; }); - mkVpnConfig = name: { id, dns, endpoint, publicKey, addrV4, privateKeyFile, bridgeDevice, priorityMain, priorityWgTable, priorityFwMark, fwmark, ... }: let - bridgeAddrV4 = "10.20.${builtins.toString id}.1/24"; - in { + mkVpnConfig = name: { id, dns, endpoint, publicKey, addrV4, privateKeyFile, priorityMain, priorityWgTable, priorityFwMark, fwmark, ... }: { assertions = [ { assertion = (lib.count (c: c.id == id) (builtins.attrValues cfg)) == 1; @@ -153,19 +148,6 @@ let # networkConfig.DNSDefaultRoute = true; # Domains = ~.: system DNS queries are sent to this link's DNS server # networkConfig.Domains = "~."; - routingPolicyRules = [ - { - routingPolicyRuleConfig = { - # 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 = priorityWgTable; - From = bridgeAddrV4; - Table = id; - }; - } - ]; routes = [{ routeConfig.Table = id; routeConfig.Scope = "link"; @@ -176,35 +158,6 @@ let linkConfig.RequiredForOnline = false; }; - systemd.network.netdevs."99-${bridgeDevice}" = { - 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"; - networkConfig.Address = [ bridgeAddrV4 ]; - networkConfig.DNS = dns; - # ConfigureWithoutCarrier: a bridge with no attached interface has no carrier (this is to be expected). - # systemd ordinarily waits for a carrier to be present before "configuring" the bridge. - # i tell it to not do that, so that i can assign an IP address to the bridge before i connect it to anything. - # alternative may be to issue the bridge a static MACAddress? - # see: - networkConfig.ConfigureWithoutCarrier = true; - # networkConfig.DHCPServer = true; - linkConfig.RequiredForOnline = false; - }; - - networking.localCommands = with pkgs; '' - # view rules with: - # - `sudo iptables -t nat --list-rules -v` - # rewrite the source address of every packet leaving the container so that it's routable by the wg tunnel. - # note that this alone doesn't get it routed *to* the wg device. we can't, since SNAT is POSTROUTING (not PREROUTING). - # that part's done by an `ip rule` entry. - ${iptables}/bin/iptables -A POSTROUTING -t nat -s ${bridgeAddrV4} -j SNAT --to-source ${addrV4} - ''; - # linux will drop inbound packets if it thinks a reply to that packet wouldn't exit via the same interface (rpfilter). # wg-quick has a solution via `iptables -j CONNMARK`, and that does work for system-wide VPNs, # but i couldn't get that to work for netns with SNAT, so set rpfilter to "loose".