273 lines
9.1 KiB
Nix
273 lines
9.1 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.tailscale-netns;
|
|
|
|
instanceOptions =
|
|
{ name, ... }:
|
|
{
|
|
options = {
|
|
authKeyFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
description = "Path to file containing a Tailscale auth key.";
|
|
};
|
|
|
|
hostname = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = ''
|
|
Tailscale hostname for this instance.
|
|
Defaults to "<system-hostname>-<instance-name>".
|
|
'';
|
|
};
|
|
|
|
advertiseRoutes = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Subnet routes to advertise (e.g. [ \"10.0.0.0/24\" ]).";
|
|
};
|
|
|
|
dns = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = ''
|
|
Bootstrap DNS server for the namespace before Tailscale is connected.
|
|
Defaults to `1.1.1.1`. Only used for the initial coordination server
|
|
connection; once Tailscale is up it manages DNS inside the namespace.
|
|
'';
|
|
};
|
|
|
|
advertiseExitNode = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Whether to advertise this node as an exit node.";
|
|
};
|
|
|
|
acceptRoutes = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Whether to accept routes advertised by other nodes.";
|
|
};
|
|
|
|
exitNode = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = "Tailscale IP or hostname of exit node to use.";
|
|
};
|
|
|
|
extraUpFlags = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Extra flags passed to `tailscale up`.";
|
|
};
|
|
|
|
extraDaemonFlags = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Extra flags passed to `tailscaled`.";
|
|
};
|
|
};
|
|
};
|
|
|
|
mkUpFlags =
|
|
instanceCfg:
|
|
let
|
|
hostFlag = optional (
|
|
instanceCfg.hostname != null
|
|
) "--hostname=${escapeShellArg instanceCfg.hostname}";
|
|
routeFlags = map (r: "--advertise-routes=${escapeShellArg r}") instanceCfg.advertiseRoutes;
|
|
exitFlag = optional instanceCfg.advertiseExitNode "--advertise-exit-node";
|
|
acceptFlag = optional instanceCfg.acceptRoutes "--accept-routes";
|
|
exitNodeFlag = optional (
|
|
instanceCfg.exitNode != null
|
|
) "--exit-node=${escapeShellArg instanceCfg.exitNode}";
|
|
in
|
|
hostFlag ++ routeFlags ++ exitFlag ++ acceptFlag ++ exitNodeFlag ++ instanceCfg.extraUpFlags;
|
|
|
|
mkAutoconnectScript =
|
|
name: instanceCfg:
|
|
pkgs.writers.writeBash "tailscale-autoconnect-${name}" ''
|
|
set -euo pipefail
|
|
|
|
TS="${pkgs.tailscale}/bin/tailscale"
|
|
SOCKET="/run/tailscale-${name}/tailscaled.sock"
|
|
TS_ARGS="--socket $SOCKET"
|
|
|
|
get_state() {
|
|
$TS $TS_ARGS status --json --peers=false 2>/dev/null | ${pkgs.jq}/bin/jq -r '.BackendState // "NoState"'
|
|
}
|
|
|
|
last_state=""
|
|
while state="$(get_state)"; do
|
|
if [[ "$state" != "$last_state" ]]; then
|
|
case "$state" in
|
|
NeedsLogin|NeedsMachineAuth|Stopped)
|
|
echo "[tailscale-netns:${name}] server needs authentication, sending auth key"
|
|
$TS $TS_ARGS up \
|
|
--auth-key "$(cat ${instanceCfg.authKeyFile})" \
|
|
${escapeShellArgs (mkUpFlags instanceCfg)}
|
|
;;
|
|
Running)
|
|
echo "[tailscale-netns:${name}] tailscale is running"
|
|
${pkgs.systemd}/bin/systemd-notify --ready
|
|
exit 0
|
|
;;
|
|
NoState)
|
|
echo "[tailscale-netns:${name}] tailscaled not ready yet"
|
|
;;
|
|
*)
|
|
echo "[tailscale-netns:${name}] waiting for Running (state=$state)"
|
|
;;
|
|
esac
|
|
fi
|
|
last_state="$state"
|
|
sleep 1
|
|
done
|
|
'';
|
|
|
|
mkNetnsSetup =
|
|
index: name: instanceCfg:
|
|
let
|
|
ns = "ts-${name}ns";
|
|
hostIP = "10.201.${toString index}.1/24";
|
|
nsIP = "10.201.${toString index}.2/24";
|
|
vethHost = "veth-ts-${name}";
|
|
vethNS = "veth-ts-${name}-ns";
|
|
in
|
|
{
|
|
"netns@ts-${name}" = {
|
|
description = "Network namespace for Tailscale instance ${name}";
|
|
before = [ "network.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
ExecStart = pkgs.writers.writeBash "ts-${name}-netns-up" ''
|
|
set -euo pipefail
|
|
${pkgs.coreutils}/bin/mkdir -p /etc/netns/${ns}
|
|
|
|
# Build resolv.conf for the netns. Skip loopback addresses
|
|
# (systemd-resolved stub at 127.0.0.53 is not reachable from the netns).
|
|
NS_DNS="${if instanceCfg.dns != null then instanceCfg.dns else "1.1.1.1"}"
|
|
{
|
|
${pkgs.gawk}/bin/awk '$1 == "nameserver" && $2 !~ /^127\./ && $2 != "::1" {print}' /etc/resolv.conf
|
|
echo "nameserver $NS_DNS"
|
|
} > /etc/netns/${ns}/resolv.conf
|
|
|
|
${pkgs.iproute2}/bin/ip netns add ${ns}
|
|
${pkgs.iproute2}/bin/ip link add ${vethHost} type veth peer name ${vethNS}
|
|
${pkgs.iproute2}/bin/ip link set ${vethNS} netns ${ns}
|
|
|
|
${pkgs.iproute2}/bin/ip addr add ${hostIP} dev ${vethHost}
|
|
${pkgs.iproute2}/bin/ip link set ${vethHost} up
|
|
|
|
${pkgs.iproute2}/bin/ip -n ${ns} addr add ${nsIP} dev ${vethNS}
|
|
${pkgs.iproute2}/bin/ip -n ${ns} link set ${vethNS} up
|
|
${pkgs.iproute2}/bin/ip -n ${ns} link set lo up
|
|
|
|
${pkgs.iptables}/bin/iptables -t nat -C POSTROUTING -s ${nsIP} -j MASQUERADE 2>/dev/null \
|
|
|| ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${nsIP} -j MASQUERADE
|
|
'';
|
|
ExecStop = pkgs.writers.writeBash "ts-${name}-netns-down" ''
|
|
set -euo pipefail
|
|
${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s ${nsIP} -j MASQUERADE 2>/dev/null || true
|
|
${pkgs.iproute2}/bin/ip link del ${vethHost} 2>/dev/null || true
|
|
${pkgs.iproute2}/bin/ip netns del ${ns} 2>/dev/null || true
|
|
'';
|
|
};
|
|
};
|
|
|
|
"tailscaled-${name}" = {
|
|
description = "Tailscale daemon for instance ${name}";
|
|
bindsTo = [ "netns@ts-${name}.service" ];
|
|
after = [ "netns@ts-${name}.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
ExecStart = pkgs.writers.writeBash "tailscaled-${name}-start" ''
|
|
exec ${pkgs.iproute2}/bin/ip netns exec ${ns} \
|
|
${pkgs.tailscale}/bin/tailscaled \
|
|
--statedir=/var/lib/tailscale-${name} \
|
|
--socket=/run/tailscale-${name}/tailscaled.sock \
|
|
--tun=ts-${name} \
|
|
--port=41641 \
|
|
${escapeShellArgs instanceCfg.extraDaemonFlags}
|
|
'';
|
|
ExecStopPost = pkgs.writers.writeBash "tailscaled-${name}-stop" ''
|
|
${pkgs.iproute2}/bin/ip netns exec ${ns} \
|
|
${pkgs.tailscale}/bin/tailscaled --cleanup \
|
|
--statedir=/var/lib/tailscale-${name} \
|
|
--socket=/run/tailscale-${name}/tailscaled.sock \
|
|
2>/dev/null || true
|
|
'';
|
|
Restart = "on-failure";
|
|
RuntimeDirectory = "tailscale-${name}";
|
|
RuntimeDirectoryMode = "0755";
|
|
StateDirectory = "tailscale-${name}";
|
|
StateDirectoryMode = "0700";
|
|
CacheDirectory = "tailscale-${name}";
|
|
CacheDirectoryMode = "0750";
|
|
|
|
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW";
|
|
LimitNOFILE = "infinity";
|
|
};
|
|
};
|
|
};
|
|
|
|
in
|
|
{
|
|
options.services.tailscale-netns = {
|
|
enable = mkEnableOption "Tailscale instances in isolated network namespaces";
|
|
|
|
instances = mkOption {
|
|
type = types.attrsOf (types.submodule instanceOptions);
|
|
default = { };
|
|
description = "Tailscale instances configured in separate network namespaces.";
|
|
example = literalExpression ''
|
|
{
|
|
work = {
|
|
authKeyFile = "/run/secrets/ts-work/authkey";
|
|
acceptRoutes = true;
|
|
};
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
systemd.services = listToAttrs (
|
|
flatten (
|
|
imap1 (
|
|
index: elem:
|
|
let
|
|
name = elem.name;
|
|
instanceCfg = elem.value;
|
|
units = mkNetnsSetup index name instanceCfg;
|
|
in
|
|
[
|
|
units."netns@ts-${name}"
|
|
units."tailscaled-${name}"
|
|
]
|
|
++ optional (instanceCfg.authKeyFile != null) {
|
|
"tailscaled-autoconnect-${name}" = {
|
|
description = "Tailscale autoconnect for instance ${name}";
|
|
after = [ "tailscaled-${name}.service" ];
|
|
requires = [ "tailscaled-${name}.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "notify";
|
|
};
|
|
script = "${mkAutoconnectScript name instanceCfg}";
|
|
};
|
|
}
|
|
) (attrsToList cfg.instances)
|
|
)
|
|
);
|
|
};
|
|
}
|