diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fe67d39e70f9..afb4d25de080 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1230,6 +1230,7 @@ ./services/networking/syncthing.nix ./services/networking/tailscale.nix ./services/networking/tailscale-auth.nix + ./services/networking/tailscale-derper.nix ./services/networking/tayga.nix ./services/networking/tcpcrypt.nix ./services/networking/teamspeak3.nix diff --git a/nixos/modules/services/networking/tailscale-derper.nix b/nixos/modules/services/networking/tailscale-derper.nix new file mode 100644 index 000000000000..9549cc5c6640 --- /dev/null +++ b/nixos/modules/services/networking/tailscale-derper.nix @@ -0,0 +1,132 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.tailscale.derper; +in +{ + meta.maintainers = with lib.maintainers; [ SuperSandro2000 ]; + + options = { + services.tailscale.derper = { + enable = lib.mkEnableOption "Tailscale Derper. See upstream doc how to configure it on clients"; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain name under which the derper server is reachable."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to open the firewall for the specified port. + Derper requires the used ports to be opened, otherwise it doesn't work as expected. + ''; + }; + + package = lib.mkPackageOption pkgs [ + "tailscale" + "derper" + ] { }; + + stunPort = lib.mkOption { + type = lib.types.port; + default = 3478; + description = '' + STUN port to listen on. + See online docs on how to configure a different external port. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8010; + description = "The port the derper process will listen on. This is not the port tailscale will connect to."; + }; + + verifyClients = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to verify clients against a locally running tailscale daemon if they are allowed to connect to this node or not. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + networking.firewall = lib.mkIf cfg.openFirewall { + # port 80 and 443 are opened by nginx already + allowedUDPPorts = [ cfg.stunPort ]; + }; + + services = { + nginx = { + enable = true; + upstreams.tailscale-derper = { + servers."127.0.0.1:${toString cfg.port}" = { }; + extraConfig = '' + keepalive 64; + ''; + }; + virtualHosts."${cfg.domain}" = { + addSSL = true; # this cannot be forceSSL as derper sends some information over port 80, too. + locations."/" = { + proxyPass = "http://tailscale-derper"; + proxyWebsockets = true; + extraConfig = '' + keepalive_timeout 0; + proxy_buffering off; + ''; + }; + }; + }; + + tailscale.enable = lib.mkIf cfg.verifyClients true; + }; + + systemd.services.tailscale-derper = { + serviceConfig = { + ExecStart = + "${lib.getExe' cfg.package "derper"} -a :${toString cfg.port} -c /var/lib/derper/derper.key -hostname=${cfg.domain} -stun-port ${toString cfg.stunPort}" + + lib.optionalString cfg.verifyClients " -verify-clients"; + DynamicUser = true; + Restart = "always"; + RestartSec = "5sec"; # don't crash loop immediately + StateDirectory = "derper"; + Type = "simple"; + + CapabilityBoundingSet = [ "" ]; + DeviceAllow = null; + LockPersonality = true; + NoNewPrivileges = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" ]; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; +}