WIP: router: tailscale

This commit is contained in:
2026-10-11 12:08:01 -07:00
parent 7351194fee
commit 0cbc516fb0
4 changed files with 98 additions and 1 deletions
+1
View File
@@ -9,6 +9,7 @@ in
./opts.nix ./opts.nix
./ifconfig.nix ./ifconfig.nix
./wireguard.nix ./wireguard.nix
./tailscale.nix
./firewall.nix ./firewall.nix
./dns.nix ./dns.nix
./kea.nix ./kea.nix
+45 -1
View File
@@ -15,6 +15,11 @@ let
${ifs.lan50.name}, ${ifs.lan50.name},
${ifs.wg0.name}, ${ifs.wg0.name},
} }
# Tailscale is its own zone, since tailnet traffic is already filtered by tailscale ACLs
# and needs some special handling (exit node NAT with CGNAT/ULA addrs)
define ZONE_TAILSCALE_IFS = { ${ifs.ts0.name} }
define TAILSCALE_NET4 = ${ifs.ts0.net4}
define TAILSCALE_NET6 = ${ifs.ts0.net6}
define OPNSENSE_NET6 = ${vars.extra.opnsense.net6} define OPNSENSE_NET6 = ${vars.extra.opnsense.net6}
define ZONE_LAN_EXTRA_NET6 = { define ZONE_LAN_EXTRA_NET6 = {
# TODO: reevaluate this statement # TODO: reevaluate this statement
@@ -80,6 +85,8 @@ in
# Masquerade LAN addrs # Masquerade LAN addrs
oifname $ZONE_WAN_IFS ip saddr $RFC1918 masquerade oifname $ZONE_WAN_IFS ip saddr $RFC1918 masquerade
# Masquerade tailnet addrs (tailscale exit node)
oifname $ZONE_WAN_IFS ip saddr $TAILSCALE_NET4 masquerade
} }
''; '';
}; };
@@ -97,12 +104,28 @@ in
''; '';
}; };
# IPv6 masquerading for tailnet addrs only (tailscale exit node)
# Tailscale only assigns ULAs from $TAILSCALE_NET6, so NAT is unavoidable here
networking.nftables.tables.nat6_tailscale = {
family = "ip6";
content = ''
${nftIdentifiers}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname $ZONE_WAN_IFS ip6 saddr $TAILSCALE_NET6 masquerade
}
'';
};
networking.nftables.tables.firewall = { networking.nftables.tables.firewall = {
family = "inet"; family = "inet";
content = '' content = ''
${nftIdentifiers} ${nftIdentifiers}
define ALLOWED_TCP_PORTS = { ssh } define ALLOWED_TCP_PORTS = { ssh }
define ALLOWED_UDP_PORTS = { ${toString vars.ifs.wg0.listenPort} } define ALLOWED_UDP_PORTS = {
${toString vars.ifs.wg0.listenPort},
${toString vars.ifs.ts0.listenPort}, # tailscale direct connections
}
define ALLOWED_TCP_LAN_PORTS = { ssh, https } define ALLOWED_TCP_LAN_PORTS = { ssh, https }
define ALLOWED_UDP_LAN_PORTS = { bootps, dhcpv6-server, domain, https } define ALLOWED_UDP_LAN_PORTS = { bootps, dhcpv6-server, domain, https }
set port_forward_v6 { set port_forward_v6 {
@@ -161,6 +184,8 @@ in
# iifname $ZONE_LAN_IFS accept # iifname $ZONE_LAN_IFS accept
iifname $ZONE_LAN_IFS jump zone_lan_input iifname $ZONE_LAN_IFS jump zone_lan_input
ip6 saddr $ZONE_LAN_EXTRA_NET6 jump zone_lan_input ip6 saddr $ZONE_LAN_EXTRA_NET6 jump zone_lan_input
# Tailscale zone input rules
iifname $ZONE_TAILSCALE_IFS jump zone_tailscale_input
# log # log
} }
@@ -176,6 +201,8 @@ in
# LAN zone forward rules # LAN zone forward rules
iifname $ZONE_LAN_IFS jump zone_lan_forward iifname $ZONE_LAN_IFS jump zone_lan_forward
ip6 saddr $ZONE_LAN_EXTRA_NET6 jump zone_lan_forward ip6 saddr $ZONE_LAN_EXTRA_NET6 jump zone_lan_forward
# Tailscale zone forward rules
iifname $ZONE_TAILSCALE_IFS jump zone_tailscale_forward
} }
chain zone_wan_input { chain zone_wan_input {
@@ -215,6 +242,23 @@ in
# Allow traffic between LANs # Allow traffic between LANs
oifname $ZONE_LAN_IFS accept oifname $ZONE_LAN_IFS accept
# Allow LAN to reach tailnet (no SNAT, tailnet peers see real LAN addrs)
oifname $ZONE_TAILSCALE_IFS accept
}
chain zone_tailscale_input {
# Tailnet access is restricted by tailscale ACLs, treat like LAN
jump zone_lan_input
}
chain zone_tailscale_forward {
# Subnet router: allow tailnet to LANs (advertised routes)
oifname $ZONE_LAN_IFS accept
# Exit node: allow tailnet to WAN (masqueraded in nat4 / nat6_tailscale)
oifname $ZONE_WAN_IFS ip saddr $TAILSCALE_NET4 accept
oifname $ZONE_WAN_IFS ip6 saddr $TAILSCALE_NET6 accept
} }
chain output { chain output {
+44
View File
@@ -0,0 +1,44 @@
{ config, lib, pkgs, ... }:
let
vars = import ./vars.nix config;
ifs = vars.ifs;
ts0 = ifs.ts0;
# Subnets advertised to the tailnet (still need to be approved in the admin console)
advertisedRoutes = [
"${vars.p4}.0.0/16" # all LAN v4 subnets
"${vars.ulaPrefix}::/48" # all LAN ULA subnets
];
in
{
# One-time setup after deploying: `sudo tailscale up`
# (state is persisted in /var/lib/tailscale)
services.tailscale = {
enable = true;
port = ts0.listenPort;
interfaceName = ts0.name;
# Enables ip forwarding (already enabled globally by networkd anyway).
# Not using "both", since that sets rp_filter to loose on all interfaces.
useRoutingFeatures = "server";
# Firewall is managed manually in firewall.nix (networking.firewall is disabled)
openFirewall = false;
# Applied with `tailscale set` on every start, so these stay declarative
extraSetFlags = [
# Don't let tailscale touch nftables/iptables, all rules live in firewall.nix
"--netfilter-mode=off"
# Don't SNAT tailnet -> LAN traffic, LAN hosts route back via the router anyway
# (preserves real source addrs for logging/ACLs on LAN hosts)
"--snat-subnet-routes=false"
# The router is its own DNS server, don't let tailscale mess with resolv.conf
"--accept-dns=false"
"--advertise-routes=${lib.strings.concatStringsSep "," advertisedRoutes}"
"--advertise-exit-node"
];
};
# networkd removes "foreign" ip rules when it (re)starts, which would wipe tailscale's
# policy routing rules (table 52). This is extra important since networkd is
# restarted every minute (see ifconfig.nix).
# The nixpkgs tailscale module already marks tailscale0 as unmanaged in networkd.
systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = false;
}
+8
View File
@@ -111,6 +111,14 @@ rec {
} // { } // {
listenPort = 51944; listenPort = 51944;
}; };
# Tailscale addresses are assigned by the coordination server,
# these are the fixed ranges tailscale allocates node addrs from.
ts0 = {
name = "tailscale0";
listenPort = 41641;
net4 = "100.64.0.0/10";
net6 = "fd7a:115c:a1e0::/48";
};
}; };
extra = { extra = {