diff --git a/hosts/c1/default.nix b/hosts/c1/default.nix index facd37b..76113f1 100644 --- a/hosts/c1/default.nix +++ b/hosts/c1/default.nix @@ -7,4 +7,5 @@ ]; networking.hostName = "c1"; + services.tailscaleAutoconnect.authkey = "tskey-auth-kmFvBT3CNTRL-wUbELKSd5yhuuTwTcgJZxhPUTxKgcYKF"; } diff --git a/hosts/c2/default.nix b/hosts/c2/default.nix index 52c9fc7..df4c9e4 100644 --- a/hosts/c2/default.nix +++ b/hosts/c2/default.nix @@ -7,4 +7,5 @@ ]; networking.hostName = "c2"; + services.tailscaleAutoconnect.authkey = "tskey-auth-kbYnZK2CNTRL-SpUVCuzS6P3ApJiDaB6RM3M4b8M9TXgS"; } diff --git a/hosts/c3/default.nix b/hosts/c3/default.nix index 2ef60a8..86756e7 100644 --- a/hosts/c3/default.nix +++ b/hosts/c3/default.nix @@ -7,4 +7,5 @@ ]; networking.hostName = "c3"; + services.tailscaleAutoconnect.authkey = "tskey-auth-kDNknU5CNTRL-iEGHyo8GDZBCVLaMutJjZBHH7wCuCDyFb"; } diff --git a/hosts/common/global/default.nix b/hosts/common/global/default.nix index cece19e..befc250 100644 --- a/hosts/common/global/default.nix +++ b/hosts/common/global/default.nix @@ -9,6 +9,7 @@ ./nix.nix ./packages.nix ./sudo.nix + ./tailscale.nix ]; system.copySystemConfiguration = false; # not supported with flakes diff --git a/hosts/common/global/tailscale.nix b/hosts/common/global/tailscale.nix new file mode 100644 index 0000000..c44612f --- /dev/null +++ b/hosts/common/global/tailscale.nix @@ -0,0 +1,14 @@ +{ config, pkgs, ... }: +let +in +{ + imports = [ ./tailscale_lib.nix ]; + + services.tailscaleAutoconnect.enable = true; + + services.tailscale.package = pkgs.unstable.tailscale; + + environment.persistence."/persist".directories = [ + "/var/lib/tailscale" + ]; +} diff --git a/hosts/common/global/tailscale_lib.nix b/hosts/common/global/tailscale_lib.nix new file mode 100644 index 0000000..83cbfea --- /dev/null +++ b/hosts/common/global/tailscale_lib.nix @@ -0,0 +1,103 @@ +# https://guekka.github.io/nixos-server-2/ +{ config, lib, pkgs, ... }: +with lib; let + cfg = config.services.tailscaleAutoconnect; +in { + options.services.tailscaleAutoconnect = { + enable = mkEnableOption "tailscaleAutoconnect"; + + authkey = mkOption { + type = types.str; + description = "The authkey to use for authentication with Tailscale"; + }; + + loginServer = mkOption { + type = types.str; + default = ""; + description = "The login server to use for authentication with Tailscale"; + }; + + advertiseExitNode = mkOption { + type = types.bool; + default = false; + description = "Whether to advertise this node as an exit node"; + }; + + exitNode = mkOption { + type = types.str; + default = ""; + description = "The exit node to use for this node"; + }; + + exitNodeAllowLanAccess = mkOption { + type = types.bool; + default = false; + description = "Whether to allow LAN access to this node"; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.authkey != ""; + message = "authkey must be set"; + } + { + assertion = cfg.exitNodeAllowLanAccess -> cfg.exitNode != ""; + message = "exitNodeAllowLanAccess must be false if exitNode is not set"; + } + { + assertion = cfg.advertiseExitNode -> cfg.exitNode == ""; + message = "advertiseExitNode must be false if exitNode is set"; + } + ]; + + systemd.services.tailscale-autoconnect = { + description = "Automatic connection to Tailscale"; + + # make sure tailscale is running before trying to connect to tailscale + after = ["network-pre.target" "tailscale.service"]; + wants = ["network-pre.target" "tailscale.service"]; + wantedBy = ["multi-user.target"]; + + serviceConfig.Type = "oneshot"; + + script = with pkgs; '' + # wait for tailscaled to settle + sleep 2 + + # check if we are already authenticated to tailscale + status="$(${tailscale}/bin/tailscale status -json | ${jq}/bin/jq -r .BackendState)" + # if status is not null, then we are already authenticated + echo "tailscale status: $status" + if [ "$status" != "NeedsLogin" ]; then + exit 0 + fi + + # otherwise authenticate with tailscale + # timeout after 10 seconds to avoid hanging the boot process + ${coreutils}/bin/timeout 10 ${tailscale}/bin/tailscale up \ + ${lib.optionalString (cfg.loginServer != "") "--login-server=${cfg.loginServer}"} \ + "--authkey=${cfg.authkey}" + + # we have to proceed in two steps because some options are only available + # after authentication + ${coreutils}/bin/timeout 10 ${tailscale}/bin/tailscale up \ + ${lib.optionalString (cfg.loginServer != "") "--login-server=${cfg.loginServer}"} \ + ${lib.optionalString (cfg.advertiseExitNode) "--advertise-exit-node"} \ + ${lib.optionalString (cfg.exitNode != "") "--exit-node=${cfg.exitNode}"} \ + ${lib.optionalString (cfg.exitNodeAllowLanAccess) "--exit-node-allow-lan-access"} + ''; + }; + + networking.firewall = { + trustedInterfaces = [ "tailscale0" ]; + allowedUDPPorts = [ config.services.tailscale.port ]; + }; + + services.tailscale = { + enable = true; + useRoutingFeatures = if cfg.advertiseExitNode then "server" else "client"; + }; + }; +}