1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.tailscale.derper;
10in
11{
12 meta.maintainers = with lib.maintainers; [ SuperSandro2000 ];
13
14 options = {
15 services.tailscale.derper = {
16 enable = lib.mkEnableOption "Tailscale Derper. See upstream doc <https://tailscale.com/kb/1118/custom-derp-servers> how to configure it on clients";
17
18 domain = lib.mkOption {
19 type = lib.types.str;
20 description = "Domain name under which the derper server is reachable.";
21 };
22
23 openFirewall = lib.mkOption {
24 type = lib.types.bool;
25 default = true;
26 description = ''
27 Whether to open the firewall for the specified port.
28 Derper requires the used ports to be opened, otherwise it doesn't work as expected.
29 '';
30 };
31
32 package = lib.mkPackageOption pkgs [
33 "tailscale"
34 "derper"
35 ] { };
36
37 stunPort = lib.mkOption {
38 type = lib.types.port;
39 default = 3478;
40 description = ''
41 STUN port to listen on.
42 See online docs <https://tailscale.com/kb/1118/custom-derp-servers#prerequisites> on how to configure a different external port.
43 '';
44 };
45
46 port = lib.mkOption {
47 type = lib.types.port;
48 default = 8010;
49 description = "The port the derper process will listen on. This is not the port tailscale will connect to.";
50 };
51
52 verifyClients = lib.mkOption {
53 type = lib.types.bool;
54 default = false;
55 description = ''
56 Whether to verify clients against a locally running tailscale daemon if they are allowed to connect to this node or not.
57 '';
58 };
59 };
60 };
61
62 config = lib.mkIf cfg.enable {
63 networking.firewall = lib.mkIf cfg.openFirewall {
64 # port 80 and 443 are opened by nginx already
65 allowedUDPPorts = [ cfg.stunPort ];
66 };
67
68 services = {
69 nginx = {
70 enable = true;
71 virtualHosts."${cfg.domain}" = {
72 addSSL = true; # this cannot be forceSSL as derper sends some information over port 80, too.
73 locations."/" = {
74 proxyPass = "http://127.0.0.1:${toString cfg.port}";
75 proxyWebsockets = true;
76 extraConfig = ''
77 keepalive_timeout 0;
78 proxy_buffering off;
79 '';
80 };
81 };
82 };
83
84 tailscale.enable = lib.mkIf cfg.verifyClients true;
85 };
86
87 systemd.services.tailscale-derper = {
88 serviceConfig = {
89 ExecStart =
90 "${lib.getExe' cfg.package "derper"} -a :${toString cfg.port} -c /var/lib/derper/derper.key -hostname=${cfg.domain} -stun-port ${toString cfg.stunPort}"
91 + lib.optionalString cfg.verifyClients " -verify-clients";
92 DynamicUser = true;
93 Restart = "always";
94 RestartSec = "5sec"; # don't crash loop immediately
95 StateDirectory = "derper";
96 Type = "simple";
97
98 CapabilityBoundingSet = [ "" ];
99 DeviceAllow = null;
100 LockPersonality = true;
101 NoNewPrivileges = true;
102 MemoryDenyWriteExecute = true;
103 PrivateDevices = true;
104 PrivateUsers = true;
105 ProcSubset = "pid";
106 ProtectClock = true;
107 ProtectControlGroups = true;
108 ProtectHostname = true;
109 ProtectKernelLogs = true;
110 ProtectKernelModules = true;
111 ProtectKernelTunables = true;
112 ProtectProc = "invisible";
113 RestrictAddressFamilies = [
114 "AF_INET"
115 "AF_INET6"
116 "AF_UNIX"
117 ];
118 RestrictNamespaces = true;
119 RestrictRealtime = true;
120 SystemCallArchitectures = "native";
121 SystemCallFilter = [ "@system-service" ];
122 };
123 wantedBy = [ "multi-user.target" ];
124 };
125 };
126}