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