tailscale: clean up the IP routes so that it can coexist with by home wireguard network
This commit is contained in:
18
hosts/modules/roles/work/tailscale-iproute2/default.nix
Normal file
18
hosts/modules/roles/work/tailscale-iproute2/default.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
iproute2,
|
||||||
|
static-nix-shell,
|
||||||
|
symlinkJoin,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
ipCmd = static-nix-shell.mkYsh {
|
||||||
|
pname = "ip";
|
||||||
|
pkgs = [ "iproute2" "systemdMinimal" ];
|
||||||
|
srcRoot = ./.;
|
||||||
|
};
|
||||||
|
in symlinkJoin {
|
||||||
|
name = "tailscale-iproute2";
|
||||||
|
paths = [
|
||||||
|
ipCmd
|
||||||
|
iproute2
|
||||||
|
];
|
||||||
|
}
|
68
hosts/modules/roles/work/tailscale-iproute2/ip
Executable file
68
hosts/modules/roles/work/tailscale-iproute2/ip
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#!nix-shell -i ysh -p iproute2 -p oils-for-unix -p systemdMinimal
|
||||||
|
|
||||||
|
# this script intercepts tailscale's calls into the `ip` tool.
|
||||||
|
# example invocations:
|
||||||
|
# ip rule
|
||||||
|
# ip addr add 100.12.34.56/32 dev tailscale0
|
||||||
|
# ip -4 rule add pref 5210 fwmark 0x80000/0xff0000 table main
|
||||||
|
# ip -4 rule add pref 5270 table 52
|
||||||
|
# ip link set dev tailscale0 up
|
||||||
|
# ip route add 100.23.145.167/32 dev tailscale0 table 52
|
||||||
|
# ip route add 10.100.0.0/16 dev tailscale0 table 52
|
||||||
|
#
|
||||||
|
# ip -4 rule del pref 5250 type unreachable
|
||||||
|
# ip -4 rule del pref 5210 table main
|
||||||
|
|
||||||
|
|
||||||
|
proc log(...args) {
|
||||||
|
echo "[tailscale-iproute2]" @args | systemd-cat --identifier=tailscaled
|
||||||
|
echo "[tailscale-iproute2]" @args >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log ip @ARGV
|
||||||
|
|
||||||
|
func isPermitted(...args) {
|
||||||
|
for a in (args) {
|
||||||
|
case (a) {
|
||||||
|
route | rule {
|
||||||
|
# DON'T allow these operations:
|
||||||
|
# - modify `route`s
|
||||||
|
# - add `rule`s to perform lookups in other tables, sometimes matched by fwmark
|
||||||
|
# - these should be safe, but since the tables themselves stay empty, it's just useless spam
|
||||||
|
# and makes debugging trickier
|
||||||
|
# - note that tailscale uses empty `ip rule` invocation to test for support;
|
||||||
|
# doesn't parse the output, just the exit code.
|
||||||
|
return (false)
|
||||||
|
}
|
||||||
|
addr | link {
|
||||||
|
# DO allow these operations:
|
||||||
|
# - `add` addresses to the device
|
||||||
|
# - bring up/down tailscale0 `link` device
|
||||||
|
return (true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log "UNRECOGNIZED IP COMMAND" @args
|
||||||
|
return (false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not isPermitted(...ARGV)) {
|
||||||
|
log "command not permitted; exiting 0:" @ARGV
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var me = $(realpath "$_this_dir/ip")
|
||||||
|
for p in (ENV.PATH => split(":")) {
|
||||||
|
if test -x "$p/ip" {
|
||||||
|
var them = $(realpath "$p/ip")
|
||||||
|
if (me !== them) {
|
||||||
|
log "forwarding request:" "$them" @ARGV
|
||||||
|
exec "$them" @ARGV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log "NO IPROUTE2 FOUND"
|
||||||
|
exit 1
|
@@ -1,16 +1,151 @@
|
|||||||
# first run:
|
# first run:
|
||||||
# - `sudo tailscale login --hostname $myHostname`
|
# - `sudo tailscale login --hostname $myHostname`
|
||||||
{ config, lib, ... }:
|
#
|
||||||
|
# N.B.: manage with:
|
||||||
|
# - `systemctl {stop,start} tailscaled`
|
||||||
|
# NOT `tailscale {down,up}`
|
||||||
|
# the latter isn't compatible with my ip routing patches, below
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
ip = lib.getExe' pkgs.iproute2 "ip";
|
||||||
|
### TAILSCALE ROUTING
|
||||||
|
# - tailscale maintains the following `ip rule`s:
|
||||||
|
# - 5210: from all fwmark 0x80000/0xff0000 lookup main
|
||||||
|
# - 5230: from all fwmark 0x80000/0xff0000 lookup default
|
||||||
|
# - 5250: from all fwmark 0x80000/0xff0000 unreachable
|
||||||
|
# - 5270: from all lookup 52
|
||||||
|
# - tailscale _always_ adds its wireguard peers (100.64.0.0/10) to table 52
|
||||||
|
# - view with: `ip route showq table 52`
|
||||||
|
# - tailscale _conditionally_ adds routes (to _any_ destination address) _via_ these peers, to table 52.
|
||||||
|
# - THIS is what the `--accept-routes` argument does.
|
||||||
|
# - these routes are critical even for DNS.
|
||||||
|
# - `--accept-routes` seems to impact both tailscale-internal behavior AND iptables;
|
||||||
|
# hence it's NOT possible to omit `--accept-routes` and then manually define the routes i want.
|
||||||
|
# - peer-advertised routes (via `--accept-routes`) often conflict with pre-existing local routes.
|
||||||
|
# e.g. in the 10.0.0.0/8 range.
|
||||||
|
# - official way to handle conflicting routes is by manually making higher-precedence ip rules for what i care about:
|
||||||
|
# - <https://tailscale.com/kb/1023/troubleshooting#lan-traffic-prioritization-with-overlapping-subnet-routes>
|
||||||
|
# - workarounds for conflicting routes is to provide tailscale a custom `ip` tool for it to shell out to:
|
||||||
|
# - <https://github.com/tailscale/tailscale/issues/6231#issuecomment-1420912939>
|
||||||
|
#
|
||||||
|
# HOW I CONFIGURE TAILSCALE ROUTING:
|
||||||
|
# - provide `--accept-routes`
|
||||||
|
# - override the `ip` tool such that tailscale doesn't actually modify the routing table.
|
||||||
|
# - explicitly configure the range of routes i actually want.
|
||||||
|
tailscale = let
|
||||||
|
iproute2' = pkgs.callPackage ./tailscale-iproute2 { };
|
||||||
|
# tailscale package wraps binaries with `--prefix PATH ${iproute2}/bin`.
|
||||||
|
# tailscale takes 1m to compile, 5m to run tests => slow to iterate.
|
||||||
|
# instead, remove iproute2 from tailscale,
|
||||||
|
# then re-wrap the binaries with my custom iproute2, separately.
|
||||||
|
tailscaleNoIproute2 = pkgs.tailscale.override {
|
||||||
|
iproute2 = null;
|
||||||
|
makeWrapper = pkgs.makeBinaryWrapper; #< only BinaryWrapper handles `--inherit-argv0` correctly
|
||||||
|
};
|
||||||
|
in pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
inherit (tailscaleNoIproute2) pname version;
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.makeBinaryWrapper #< only BinaryWrapper handles `--inherit-argv0` correctly
|
||||||
|
];
|
||||||
|
dontUnpack = true;
|
||||||
|
dontBuild = true;
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
|
||||||
|
mkdir -p $out/lib/systemd/system
|
||||||
|
substitute ${tailscaleNoIproute2}/lib/systemd/system/tailscaled.service $out/lib/systemd/system/tailscaled.service \
|
||||||
|
--replace-fail ${tailscaleNoIproute2} $out
|
||||||
|
ln -s ${tailscaleNoIproute2}/share $out/share
|
||||||
|
|
||||||
|
mkdir -p $out/bin
|
||||||
|
ln -s ${tailscaleNoIproute2}/bin/get-authkey $out/bin/get-authkey
|
||||||
|
ln -s tailscaled $out/bin/tailscale
|
||||||
|
ln -s ${tailscaleNoIproute2}/bin/tailscaled $out/bin/tailscaled
|
||||||
|
ln -s ${tailscaleNoIproute2}/bin/tsidp $out/bin/tsidp
|
||||||
|
|
||||||
|
wrapProgram $out/bin/tailscaled \
|
||||||
|
--inherit-argv0 \
|
||||||
|
--prefix PATH : ${iproute2'}/bin
|
||||||
|
'';
|
||||||
|
|
||||||
|
passthru.iproute2 = iproute2';
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf config.sane.roles.work {
|
config = lib.mkIf config.sane.roles.work {
|
||||||
sane.persist.sys.byStore.private = [
|
sane.persist.sys.byStore.private = [
|
||||||
{ user = "root"; group = "root"; mode = "0700"; path = "/var/lib/tailscale"; method = "bind"; }
|
{ user = "root"; group = "root"; mode = "0700"; path = "/var/lib/tailscale"; method = "bind"; }
|
||||||
];
|
];
|
||||||
services.tailscale.enable = true;
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
|
services.tailscale.package = tailscale;
|
||||||
|
systemd.services.tailscaled.environment.TS_DEBUG_USE_IP_COMMAND = "1";
|
||||||
|
|
||||||
|
# "statically" configure the routes to tailscale.
|
||||||
|
# tailscale doesn't use the kernel wireguard module,
|
||||||
|
# but a userspace `wireguard-go` (coupled with `/dev/net/tun`, or a pure
|
||||||
|
# pasta-style TCP/UDP userspace dev).
|
||||||
|
#
|
||||||
|
# it therefore appears as an "unmanaged" device to network managers like systemd-networkd.
|
||||||
|
# in order to configure routes, we have to script it.
|
||||||
|
systemd.services.tailscaled.serviceConfig.ExecStartPost = [
|
||||||
|
(pkgs.writeShellScript "tailscaled-add-routes" ''
|
||||||
|
while ! ${lib.getExe' tailscale "tailscale"} status ; do
|
||||||
|
echo "tailscale not ready"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
${ip} route add table main 10.0.0.0/8 dev tailscale0 scope global
|
||||||
|
${ip} route add table main 100.64.0.0/10 dev tailscale0 scope global
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
systemd.services.tailscaled.preStop = ''
|
||||||
|
${ip} route del table main 10.0.0.0/8 dev tailscale0 scope global || true
|
||||||
|
${ip} route del table main 100.64.0.0/10 dev tailscale0 scope global || true
|
||||||
|
'';
|
||||||
|
# systemd.network.networks."50-tailscale" = {
|
||||||
|
# # see: `man 5 systemd.network`
|
||||||
|
# matchConfig.Name = "tailscale0";
|
||||||
|
# routes = [
|
||||||
|
# # {
|
||||||
|
# # Scope = "global";
|
||||||
|
# # # 0.0.0.0/8 is a reserved-for-local-network range in IPv4
|
||||||
|
# # Destination = "0.0.0.0/8";
|
||||||
|
# # }
|
||||||
|
# {
|
||||||
|
# Scope = "global";
|
||||||
|
# # Scope = "link";
|
||||||
|
# # 10.0.0.0/8 is a reserved-for-private-networks range in IPv4
|
||||||
|
# Destination = "10.0.0.0/8";
|
||||||
|
# }
|
||||||
|
# {
|
||||||
|
# Scope = "global";
|
||||||
|
# # Scope = "link";
|
||||||
|
# # 100.64.0.0/10 is a reserved range in IPv4
|
||||||
|
# Destination = "100.64.0.0/10";
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# # RequiredForOnline => should `systemd-networkd-wait-online` fail if this network can't come up?
|
||||||
|
# linkConfig.RequiredForOnline = false;
|
||||||
|
# linkConfig.Unmanaged = lib.mkForce false; #< tailscale nixos module declares this as unmanaged
|
||||||
|
# };
|
||||||
|
|
||||||
# services.tailscale.useRoutingFeatures = "client";
|
# services.tailscale.useRoutingFeatures = "client";
|
||||||
services.tailscale.extraSetFlags = [
|
services.tailscale.extraSetFlags = [
|
||||||
|
# --accept-routes does _two_ things:
|
||||||
|
# 1. allows tailscale to discover, internally, how to route to peers-of-peers.
|
||||||
|
# 2. instructs tailscale to tell the kernel to route discovered routes through the tailscale0 device.
|
||||||
|
# even if i disable #2, i still need --accept-routes to provide #1.
|
||||||
"--accept-routes"
|
"--accept-routes"
|
||||||
# "--operator=colin" #< this *should* allow non-root control, but fails: <https://github.com/tailscale/tailscale/issues/16080>
|
# "--operator=colin" #< this *should* allow non-root control, but fails: <https://github.com/tailscale/tailscale/issues/16080>
|
||||||
|
# lock the preferences i care about, because even if they're default i think they _might_ be conditional on admin policy:
|
||||||
|
"--accept-dns=false" #< i manage manually, with BIND
|
||||||
|
# "--accept-routes=false"
|
||||||
|
"--advertise-connector=false"
|
||||||
|
"--advertise-exit-node=false"
|
||||||
|
# "--auto-update=false" # "automatic updates are not supported on this platform"
|
||||||
|
"--ssh=false"
|
||||||
|
"--update-check=false"
|
||||||
|
"--webclient=false"
|
||||||
];
|
];
|
||||||
services.tailscale.extraDaemonFlags = [
|
services.tailscale.extraDaemonFlags = [
|
||||||
"-verbose" "7"
|
"-verbose" "7"
|
||||||
@@ -18,6 +153,7 @@
|
|||||||
services.bind.extraConfig = ''
|
services.bind.extraConfig = ''
|
||||||
include "${config.sops.secrets."tailscale-work-zones-bind.conf".path}";
|
include "${config.sops.secrets."tailscale-work-zones-bind.conf".path}";
|
||||||
'';
|
'';
|
||||||
|
|
||||||
systemd.services.tailscaled = {
|
systemd.services.tailscaled = {
|
||||||
# systemd hardening (systemd-analyze security tailscaled.service)
|
# systemd hardening (systemd-analyze security tailscaled.service)
|
||||||
serviceConfig.AmbientCapabilities = "CAP_NET_ADMIN";
|
serviceConfig.AmbientCapabilities = "CAP_NET_ADMIN";
|
||||||
|
Reference in New Issue
Block a user