wg-home: deploy so as to be compatible with sane-vpn (e.g., route *WAN* traffic through it)

This commit is contained in:
Colin 2024-07-05 14:16:27 +00:00
parent 56e488b130
commit 5d80e298b5
6 changed files with 114 additions and 117 deletions

View File

@ -106,6 +106,27 @@ in
hn = { hn = {
substitutions = mkSubstitutions "hn"; substitutions = mkSubstitutions "hn";
listenAddrsIpv4 = [ nativeAddrs."servo.hn" ]; listenAddrsIpv4 = [ nativeAddrs."servo.hn" ];
enableRecursiveResolver = true; #< allow wireguard clients to use this as their DNS resolver
# extraConfig = {
# zones = [
# {
# # forward the root zone to the local DNS resolver
# # to allow wireguard clients to use this as their DNS resolver
# zone = ".";
# zone_type = "Forward";
# stores = {
# type = "forward";
# name_servers = [
# {
# socket_addr = "127.0.0.53:53";
# protocol = "udp";
# trust_nx_responses = true;
# }
# ];
# };
# }
# ];
# };
}; };
lan = { lan = {
substitutions = mkSubstitutions "lan"; substitutions = mkSubstitutions "lan";
@ -118,43 +139,6 @@ in
# nativeAddrs."servo.lan" # nativeAddrs."servo.lan"
# ]; # ];
# }; # };
# hn-resolver = {
# # don't need %AWAN% here because we forward to the hn instance.
# listenAddrsIpv4 = [ nativeAddrs."servo.hn" ];
# extraConfig = {
# zones = [
# {
# zone = "uninsane.org";
# zone_type = "Forward";
# stores = {
# type = "forward";
# name_servers = [
# {
# socket_addr = "${nativeAddrs."servo.hn"}:1053";
# protocol = "udp";
# trust_nx_responses = true;
# }
# ];
# };
# }
# {
# # forward the root zone to the local DNS resolver
# zone = ".";
# zone_type = "Forward";
# stores = {
# type = "forward";
# name_servers = [
# {
# socket_addr = "127.0.0.53:53";
# protocol = "udp";
# trust_nx_responses = true;
# }
# ];
# };
# }
# ];
# };
# };
}; };
sane.services.dyn-dns.restartOnChange = [ sane.services.dyn-dns.restartOnChange = [
@ -162,6 +146,5 @@ in
"trust-dns-hn.service" "trust-dns-hn.service"
"trust-dns-lan.service" "trust-dns-lan.service"
# "trust-dns-wan.service" # "trust-dns-wan.service"
# "trust-dns-hn-resolver.service" # doesn't need restart because it doesn't know about WAN IP
]; ];
} }

View File

@ -26,10 +26,6 @@ let
# lazyMount: defer mounting until first access from userspace. # lazyMount: defer mounting until first access from userspace.
# see: `man systemd.automount`, `man automount`, `man autofs` # see: `man systemd.automount`, `man automount`, `man autofs`
lazyMount = noauto ++ automount; lazyMount = noauto ++ automount;
wg = [
"x-systemd.requires=wireguard-wg-home.service"
"x-systemd.after=wireguard-wg-home.service"
];
fuse = [ fuse = [
"allow_other" # allow users other than the one who mounts it to access it. needed, if systemd is the one mounting this fs (as root) "allow_other" # allow users other than the one who mounts it to access it. needed, if systemd is the one mounting this fs (as root)
@ -136,9 +132,9 @@ let
device = "ftp://servo-hn:/${subdir}"; device = "ftp://servo-hn:/${subdir}";
noCheck = true; noCheck = true;
fsType = "fuse.curlftpfs"; fsType = "fuse.curlftpfs";
options = fsOpts.ftp ++ fsOpts.noauto ++ fsOpts.wg; options = fsOpts.ftp ++ fsOpts.noauto;
# fsType = "nfs"; # fsType = "nfs";
# options = fsOpts.nfs ++ fsOpts.lazyMount ++ fsOpts.wg; # options = fsOpts.nfs ++ fsOpts.lazyMount;
}; };
systemd.services."automount-servo-${utils.escapeSystemdPath subdir}" = let systemd.services."automount-servo-${utils.escapeSystemdPath subdir}" = let
fs = config.fileSystems."/mnt/servo/${subdir}"; fs = config.fileSystems."/mnt/servo/${subdir}";

View File

@ -25,6 +25,14 @@ let
type = types.str; type = types.str;
default = "0600"; default = "0600";
}; };
acl.user = mkOption {
type = types.nullOr types.str;
default = null;
};
acl.group = mkOption {
type = types.nullOr types.str;
default = null;
};
}; };
}; };
in in
@ -51,6 +59,9 @@ in
(builtins.toString (c.len * 2)) (builtins.toString (c.len * 2))
]; ];
generated.acl.mode = c.acl.mode; generated.acl.mode = c.acl.mode;
generated.acl.user = lib.mkIf (c.acl.user != null) c.acl.user;
generated.acl.group = lib.mkIf (c.acl.group != null) c.acl.group;
wantedBeforeBy = [ "local-fs-pre.target" ];
}) cfg; }) cfg;
}; };
} }

View File

@ -9,7 +9,7 @@ let
server-cfg = config.sane.hosts.by-name."servo".wg-home; server-cfg = config.sane.hosts.by-name."servo".wg-home;
mkPeer = { ips, pubkey, endpoint }: { mkPeer = { ips, pubkey, endpoint }: {
publicKey = pubkey; publicKey = pubkey;
allowedIPs = builtins.map (k: "${k}/32") ips; allowedIPs = builtins.map (k: if builtins.match ".*/.*" k != null then k else "${k}/32") ips;
} // (lib.optionalAttrs (endpoint != null) { } // (lib.optionalAttrs (endpoint != null) {
inherit endpoint; inherit endpoint;
# send keepalives every 25 seconds to keep NAT routes live. # send keepalives every 25 seconds to keep NAT routes live.
@ -29,7 +29,7 @@ let
# make a single peer which routes all the given hosts # make a single peer which routes all the given hosts
mkServerPeer = hosts: mkPeer { mkServerPeer = hosts: mkPeer {
inherit (server-cfg) pubkey endpoint; inherit (server-cfg) pubkey endpoint;
ips = builtins.map (h: h.ip) hosts; ips = (builtins.map (h: h.ip) hosts) ++ [ "0.0.0.0/0" ];
}; };
in in
{ {
@ -69,14 +69,16 @@ in
sane.derived-secrets."/run/wg-home.priv" = { sane.derived-secrets."/run/wg-home.priv" = {
len = 32; len = 32;
encoding = "base64"; encoding = "base64";
acl.mode = "0640";
acl.group = "systemd-network";
}; };
# wireguard VPN which allows everything on my domain to speak to each other even when # wireguard VPN which allows everything on my domain to speak to each other even when
# not behind a shared LAN. # not behind a shared LAN.
# this config defines both the endpoint (server) and client configs # also allows clients to proxy WAN traffic through it.
# this config defines both the endpoint (server) and client configs.
# for convenience, have both the server and client use the same port for their wireguard connections. sane.ports.ports."51820" = lib.mkIf (!cfg.routeThroughServo) {
sane.ports.ports."51820" = {
protocol = [ "udp" ]; protocol = [ "udp" ];
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = cfg.visibleToWan; visibleTo.wan = cfg.visibleToWan;
@ -84,72 +86,51 @@ in
description = "colin-wireguard"; description = "colin-wireguard";
}; };
networking.wireguard.interfaces.wg-home = lib.mkMerge [ networking.wireguard.interfaces.wg-home = lib.mkIf (!cfg.routeThroughServo) ({
{ listenPort = 51820;
listenPort = 51820; privateKeyFile = "/run/wg-home.priv";
privateKeyFile = "/run/wg-home.priv"; # TODO: make this `wants` and `after`, instead of manually starting it
# TODO: this make this `wants` and `after`, instead of manually starting it preSetup =
preSetup = let
let gen-key = config.sane.fs."/run/wg-home.priv".unit;
gen-key = config.sane.fs."/run/wg-home.priv".unit; in
in "${pkgs.systemd}/bin/systemctl start '${gen-key}'";
"${pkgs.systemd}/bin/systemctl start '${gen-key}'";
ips = [ ips = [
"${cfg.ip}/24" "${cfg.ip}/24"
]; ];
peers = peers =
let let
all-peers = lib.mapAttrsToList (_: hostcfg: hostcfg.wg-home) config.sane.hosts.by-name; all-peers = lib.mapAttrsToList (_: hostcfg: hostcfg.wg-home) config.sane.hosts.by-name;
peer-list = builtins.filter (p: p.ip != null && p.ip != cfg.ip && p.pubkey != null) all-peers; peer-list = builtins.filter (p: p.ip != null && p.ip != cfg.ip && p.pubkey != null) all-peers;
in in
if cfg.routeThroughServo then mkClientPeers peer-list
# if acting as a client, then maintain a single peer -- the server -- which does the actual routing ;
[ (mkServerPeer peer-list) ] } // (lib.optionalAttrs cfg.forwardToWan {
else # documented here: <https://nixos.wiki/wiki/WireGuard#Server_setup_2>
# if acting as a server, route to each peer individually # TODO: don't hardcode ens1!
mkClientPeers peer-list postSetup = ''
; ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${cfg.ip}/24 -o ens1 -j MASQUERADE
} '';
(lib.mkIf cfg.forwardToWan { postShutdown = ''
# documented here: <https://nixos.wiki/wiki/WireGuard#Server_setup_2> ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s ${cfg.ip}/24 -o ens1 -j MASQUERADE
# TODO: don't hardcode eth0! '';
postSetup = '' }));
${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${cfg.ip}/24 -o eth0 -j MASQUERADE
'';
postShutdown = ''
${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s ${cfg.ip}/24 -o eth0 -j MASQUERADE
'';
})
];
# also expose a wg-quick interface, so that one may `sane-vpn up servo` to route all traffic through servo # plug into my VPN abstractions so that one may:
networking.wg-quick.interfaces.vpn-servo = { # - `sane-vpn up wg-home` to route all traffic through servo
address = [ cfg.ip ]; # - `sane-vpn do wg-home THING` to route select traffic through servo
sane.vpn.wg-home = lib.mkIf cfg.routeThroughServo {
id = 51;
endpoint = config.sane.hosts.by-name."servo".wg-home.endpoint;
publicKey = config.sane.hosts.by-name."servo".wg-home.pubkey;
addrV4 = cfg.ip;
subnetV4 = "24";
dns = [ dns = [
config.sane.hosts.by-name."servo".wg-home.ip config.sane.hosts.by-name."servo".wg-home.ip
]; ];
privateKeyFile = "/run/wg-home.priv"; privateKeyFile = "/run/wg-home.priv";
peers = [
{
endpoint = config.sane.hosts.by-name."servo".wg-home.endpoint;
publicKey = config.sane.hosts.by-name."servo".wg-home.pubkey;
allowedIPs = [
"0.0.0.0/0"
"::/0"
];
}
];
# to start: `systemctl start wg-quick-${name}`
autostart = false;
# wg-home and vpn-servo interfaces interfere with the result that when connected to both,
# other wg-home users (lappy-hn, ...) aren't visible. disabling wg-home while the full
# vpn-servo is active allows wg-home users to be reachable again
preUp = "${pkgs.iproute2}/bin/ip link set wg-home down";
postDown = "${pkgs.iproute2}/bin/ip link set wg-home up";
}; };
}; };
} }

View File

@ -87,14 +87,22 @@ let
mkSystemdService = flavor: { includes, listenAddrsIpv4, listenAddrsIpv6, port, substitutions, extraConfig, ... }: let mkSystemdService = flavor: { includes, listenAddrsIpv4, listenAddrsIpv6, port, substitutions, extraConfig, ... }: let
sed = "${pkgs.gnused}/bin/sed"; sed = "${pkgs.gnused}/bin/sed";
configTemplate = toml.generate "trust-dns-${flavor}.toml" ( baseConfig = (
( lib.filterAttrsRecursive (_: v: v != null) config.services.trust-dns.settings
lib.filterAttrsRecursive (_: v: v != null) config.services.trust-dns.settings ) // {
) // { listen_addrs_ipv4 = listenAddrsIpv4;
listen_addrs_ipv4 = listenAddrsIpv4; listen_addrs_ipv6 = listenAddrsIpv6;
listen_addrs_ipv6 = listenAddrsIpv6; };
} // extraConfig configTemplate = toml.generate "trust-dns-${flavor}.toml" (baseConfig //
); (lib.mapAttrs (k: v:
if k == "zones" then
# append to the baseConfig instead of overriding it
(baseConfig."${k}" or []) ++ v
else
v
)
extraConfig
));
configPath = "/var/lib/trust-dns/${flavor}-config.toml"; configPath = "/var/lib/trust-dns/${flavor}-config.toml";
sedArgs = builtins.map (key: ''-e "s/${key}/${substitutions."${key}"}/g"'') ( sedArgs = builtins.map (key: ''-e "s/${key}/${substitutions."${key}"}/g"'') (
# HACK: %ANATIVE% often expands to one of the other subtitutions (e.g. %AWAN%) # HACK: %ANATIVE% often expands to one of the other subtitutions (e.g. %AWAN%)

View File

@ -43,22 +43,27 @@ let
}; };
fwmark = mkOption { fwmark = mkOption {
type = types.int; type = types.int;
internal = true;
}; };
# priority*: used externally, by e.g. `sane-vpn` # priority*: used externally, by e.g. `sane-vpn`
priorityMain = mkOption { priorityMain = mkOption {
type = types.int; type = types.int;
internal = true;
}; };
priorityWgTable = mkOption { priorityWgTable = mkOption {
type = types.int; type = types.int;
internal = true;
}; };
priorityFwMark = mkOption { priorityFwMark = mkOption {
type = types.int; type = types.int;
internal = true;
}; };
isDefault = mkOption { isDefault = mkOption {
type = types.bool; type = types.bool;
description = '' description = ''
read-only value: set based on whichever VPN has the lowest id. read-only value: set based on whichever VPN has the lowest id.
''; '';
internal = true;
}; };
endpoint = mkOption { endpoint = mkOption {
type = types.str; type = types.str;
@ -80,6 +85,14 @@ let
e.g. "172.27.12.34" e.g. "172.27.12.34"
''; '';
}; };
subnetV4 = mkOption {
type = types.nullOr types.str;
description = ''
subnet dictating the range of IPs which should ALWAYS be routed through this VPN, no matter the system-wide settings.
'';
example = "24";
default = null;
};
dns = mkOption { dns = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ default = [
@ -108,7 +121,7 @@ let
priorityFwMark = config.id + 300; priorityFwMark = config.id + 300;
}; };
}); });
mkVpnConfig = name: { id, dns, endpoint, publicKey, addrV4, privateKeyFile, priorityMain, priorityWgTable, priorityFwMark, fwmark, ... }: { mkVpnConfig = name: { addrV4, dns, endpoint, fwmark, id, priorityMain, priorityWgTable, priorityFwMark, privateKeyFile, publicKey, subnetV4, ... }: {
assertions = [ assertions = [
{ {
assertion = (lib.count (c: c.id == id) (builtins.attrValues cfg)) == 1; assertion = (lib.count (c: c.id == id) (builtins.attrValues cfg)) == 1;
@ -151,10 +164,15 @@ let
Scope = "link"; Scope = "link";
Destination = "0.0.0.0/0"; Destination = "0.0.0.0/0";
Source = addrV4; Source = addrV4;
}] ++ lib.optionals (subnetV4 != null) [{
Scope = "link";
Destination = "${addrV4}/${subnetV4}";
Source = addrV4;
}]; }];
# RequiredForOnline => should `systemd-networkd-wait-online` fail if this network can't come up? # RequiredForOnline => should `systemd-networkd-wait-online` fail if this network can't come up?
linkConfig.RequiredForOnline = false; linkConfig.RequiredForOnline = false;
}; };
systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = false;
# linux will drop inbound packets if it thinks a reply to that packet wouldn't exit via the same interface (rpfilter). # 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, # wg-quick has a solution via `iptables -j CONNMARK`, and that does work for system-wide VPNs,