1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.tinc;
11
12 mkValueString =
13 value:
14 if value == true then
15 "yes"
16 else if value == false then
17 "no"
18 else
19 generators.mkValueStringDefault { } value;
20
21 toTincConf = generators.toKeyValue {
22 listsAsDuplicateKeys = true;
23 mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } "=";
24 };
25
26 tincConfType =
27 with types;
28 let
29 valueType = oneOf [
30 bool
31 str
32 int
33 ];
34 in
35 attrsOf (either valueType (listOf valueType));
36
37 addressSubmodule = {
38 options = {
39 address = mkOption {
40 type = types.str;
41 description = "The external IP address or hostname where the host can be reached.";
42 };
43
44 port = mkOption {
45 type = types.nullOr types.port;
46 default = null;
47 description = ''
48 The port where the host can be reached.
49
50 If no port is specified, the default Port is used.
51 '';
52 };
53 };
54 };
55
56 subnetSubmodule = {
57 options = {
58 address = mkOption {
59 type = types.str;
60 description = ''
61 The subnet of this host.
62
63 Subnets can either be single MAC, IPv4 or IPv6 addresses, in which case
64 a subnet consisting of only that single address is assumed, or they can
65 be a IPv4 or IPv6 network address with a prefix length.
66
67 IPv4 subnets are notated like 192.168.1.0/24, IPv6 subnets are notated
68 like fec0:0:0:1::/64. MAC addresses are notated like 0:1a:2b:3c:4d:5e.
69
70 Note that subnets like 192.168.1.1/24 are invalid.
71 '';
72 };
73
74 prefixLength = mkOption {
75 type = with types; nullOr (addCheck int (n: n >= 0 && n <= 128));
76 default = null;
77 description = ''
78 The prefix length of the subnet.
79
80 If null, a subnet consisting of only that single address is assumed.
81
82 This conforms to standard CIDR notation as described in RFC1519.
83 '';
84 };
85
86 weight = mkOption {
87 type = types.ints.unsigned;
88 default = 10;
89 description = ''
90 Indicates the priority over identical Subnets owned by different nodes.
91
92 Lower values indicate higher priority. Packets will be sent to the
93 node with the highest priority, unless that node is not reachable, in
94 which case the node with the next highest priority will be tried, and
95 so on.
96 '';
97 };
98 };
99 };
100
101 hostSubmodule =
102 { config, ... }:
103 {
104 options = {
105 addresses = mkOption {
106 type = types.listOf (types.submodule addressSubmodule);
107 default = [ ];
108 description = ''
109 The external address where the host can be reached. This will set this
110 host's {option}`settings.Address` option.
111
112 This variable is only required if you want to connect to this host.
113 '';
114 };
115
116 subnets = mkOption {
117 type = types.listOf (types.submodule subnetSubmodule);
118 default = [ ];
119 description = ''
120 The subnets which this tinc daemon will serve. This will set this
121 host's {option}`settings.Subnet` option.
122
123 Tinc tries to look up which other daemon it should send a packet to by
124 searching the appropriate subnet. If the packet matches a subnet, it
125 will be sent to the daemon who has this subnet in his host
126 configuration file.
127 '';
128 };
129
130 rsaPublicKey = mkOption {
131 type = types.str;
132 default = "";
133 description = ''
134 Legacy RSA public key of the host in PEM format, including start and
135 end markers.
136
137 This will be appended as-is in the host's configuration file.
138
139 The ed25519 public key can be specified using the
140 {option}`settings.Ed25519PublicKey` option instead.
141 '';
142 };
143
144 settings = mkOption {
145 default = { };
146 type = types.submodule { freeformType = tincConfType; };
147 description = ''
148 Configuration for this host.
149
150 See <https://tinc-vpn.org/documentation-1.1/Host-configuration-variables.html>
151 for supported values.
152 '';
153 };
154 };
155
156 config.settings = {
157 Address = mkDefault (map (address: "${address.address} ${toString address.port}") config.addresses);
158
159 Subnet = mkDefault (
160 map (
161 subnet:
162 if subnet.prefixLength == null then
163 "${subnet.address}#${toString subnet.weight}"
164 else
165 "${subnet.address}/${toString subnet.prefixLength}#${toString subnet.weight}"
166 ) config.subnets
167 );
168 };
169 };
170
171in
172{
173
174 ###### interface
175
176 options = {
177
178 services.tinc = {
179
180 networks = mkOption {
181 default = { };
182 type =
183 with types;
184 attrsOf (
185 submodule (
186 { config, ... }:
187 {
188 options = {
189
190 extraConfig = mkOption {
191 default = "";
192 type = types.lines;
193 description = ''
194 Extra lines to add to the tinc service configuration file.
195
196 Note that using the declarative {option}`service.tinc.networks.<name>.settings`
197 option is preferred.
198 '';
199 };
200
201 name = mkOption {
202 default = null;
203 type = types.nullOr types.str;
204 description = ''
205 The name of the node which is used as an identifier when communicating
206 with the remote nodes in the mesh. If null then the hostname of the system
207 is used to derive a name (note that tinc may replace non-alphanumeric characters in
208 hostnames by underscores).
209 '';
210 };
211
212 ed25519PrivateKeyFile = mkOption {
213 default = null;
214 type = types.nullOr types.path;
215 description = ''
216 Path of the private ed25519 keyfile.
217 '';
218 };
219
220 rsaPrivateKeyFile = mkOption {
221 default = null;
222 type = types.nullOr types.path;
223 description = ''
224 Path of the private RSA keyfile.
225 '';
226 };
227
228 debugLevel = mkOption {
229 default = 0;
230 type = types.addCheck types.int (l: l >= 0 && l <= 5);
231 description = ''
232 The amount of debugging information to add to the log. 0 means little
233 logging while 5 is the most logging. {command}`man tincd` for
234 more details.
235 '';
236 };
237
238 hosts = mkOption {
239 default = { };
240 type = types.attrsOf types.lines;
241 description = ''
242 The name of the host in the network as well as the configuration for that host.
243 This name should only contain alphanumerics and underscores.
244
245 Note that using the declarative {option}`service.tinc.networks.<name>.hostSettings`
246 option is preferred.
247 '';
248 };
249
250 hostSettings = mkOption {
251 default = { };
252 example = literalExpression ''
253 {
254 host1 = {
255 addresses = [
256 { address = "192.168.1.42"; }
257 { address = "192.168.1.42"; port = 1655; }
258 ];
259 subnets = [ { address = "10.0.0.42"; } ];
260 rsaPublicKey = "...";
261 settings = {
262 Ed25519PublicKey = "...";
263 };
264 };
265 host2 = {
266 subnets = [ { address = "10.0.1.0"; prefixLength = 24; weight = 2; } ];
267 rsaPublicKey = "...";
268 settings = {
269 Compression = 10;
270 };
271 };
272 }
273 '';
274 type = types.attrsOf (types.submodule hostSubmodule);
275 description = ''
276 The name of the host in the network as well as the configuration for that host.
277 This name should only contain alphanumerics and underscores.
278 '';
279 };
280
281 interfaceType = mkOption {
282 default = "tun";
283 type = types.enum [
284 "tun"
285 "tap"
286 ];
287 description = ''
288 The type of virtual interface used for the network connection.
289 '';
290 };
291
292 listenAddress = mkOption {
293 default = null;
294 type = types.nullOr types.str;
295 description = ''
296 The ip address to listen on for incoming connections.
297 '';
298 };
299
300 bindToAddress = mkOption {
301 default = null;
302 type = types.nullOr types.str;
303 description = ''
304 The ip address to bind to (both listen on and send packets from).
305 '';
306 };
307
308 package = mkPackageOption pkgs "tinc_pre" { };
309
310 chroot = mkOption {
311 default = false;
312 type = types.bool;
313 description = ''
314 Change process root directory to the directory where the config file is located (/etc/tinc/netname/), for added security.
315 The chroot is performed after all the initialization is done, after writing pid files and opening network sockets.
316
317 Note that this currently breaks dns resolution and tinc can't run scripts anymore (such as tinc-down or host-up), unless it is setup to be runnable inside chroot environment.
318 '';
319 };
320
321 settings = mkOption {
322 default = { };
323 type = types.submodule { freeformType = tincConfType; };
324 example = literalExpression ''
325 {
326 Interface = "custom.interface";
327 DirectOnly = true;
328 Mode = "switch";
329 }
330 '';
331 description = ''
332 Configuration of the Tinc daemon for this network.
333
334 See <https://tinc-vpn.org/documentation-1.1/Main-configuration-variables.html>
335 for supported values.
336 '';
337 };
338 };
339
340 config = {
341 hosts = mapAttrs (hostname: host: ''
342 ${toTincConf host.settings}
343 ${host.rsaPublicKey}
344 '') config.hostSettings;
345
346 settings = {
347 DeviceType = mkDefault config.interfaceType;
348 Name = mkDefault (if config.name == null then "$HOST" else config.name);
349 Ed25519PrivateKeyFile = mkIf (config.ed25519PrivateKeyFile != null) (
350 mkDefault config.ed25519PrivateKeyFile
351 );
352 PrivateKeyFile = mkIf (config.rsaPrivateKeyFile != null) (mkDefault config.rsaPrivateKeyFile);
353 ListenAddress = mkIf (config.listenAddress != null) (mkDefault config.listenAddress);
354 BindToAddress = mkIf (config.bindToAddress != null) (mkDefault config.bindToAddress);
355 };
356 };
357 }
358 )
359 );
360
361 description = ''
362 Defines the tinc networks which will be started.
363 Each network invokes a different daemon.
364 '';
365 };
366 };
367
368 };
369
370 ###### implementation
371
372 config = mkIf (cfg.networks != { }) (
373 let
374 etcConfig = foldr (a: b: a // b) { } (
375 flip mapAttrsToList cfg.networks (
376 network: data:
377 flip mapAttrs' data.hosts (
378 host: text:
379 nameValuePair ("tinc/${network}/hosts/${host}") ({
380 mode = "0644";
381 user = "tinc-${network}";
382 inherit text;
383 })
384 )
385 // {
386 "tinc/${network}/tinc.conf" = {
387 mode = "0444";
388 text = ''
389 ${toTincConf ({ Interface = "tinc.${network}"; } // data.settings)}
390 ${data.extraConfig}
391 '';
392 };
393 }
394 )
395 );
396 in
397 {
398 environment.etc = etcConfig;
399
400 systemd.services = flip mapAttrs' cfg.networks (
401 network: data:
402 nameValuePair ("tinc.${network}") (
403 let
404 version = getVersion data.package;
405 in
406 {
407 description = "Tinc Daemon - ${network}";
408 documentation = [
409 "info:tinc"
410 "man:tincd(8)"
411 ];
412 wantedBy = [ "multi-user.target" ];
413 path = [ data.package ];
414 reloadTriggers = mkIf (versionAtLeast version "1.1pre") [ (builtins.toJSON etcConfig) ];
415 restartTriggers = mkIf (versionOlder version "1.1pre") [ (builtins.toJSON etcConfig) ];
416 serviceConfig = {
417 Type = "simple";
418 Restart = "always";
419 RestartSec = "3";
420 ExecReload = mkIf (versionAtLeast version "1.1pre") "${data.package}/bin/tinc -n ${network} reload";
421 ExecStart = "${data.package}/bin/tincd -D -U tinc-${network} -n ${network} ${optionalString (data.chroot) "-R"} --pidfile /run/tinc.${network}.pid -d ${toString data.debugLevel}";
422 };
423 preStart = ''
424 mkdir -p /etc/tinc/${network}/hosts
425 chown tinc-${network} /etc/tinc/${network}/hosts
426 mkdir -p /etc/tinc/${network}/invitations
427 chown tinc-${network} /etc/tinc/${network}/invitations
428
429 # Determine how we should generate our keys
430 if type tinc >/dev/null 2>&1; then
431 # Tinc 1.1+ uses the tinc helper application for key generation
432 ${
433 if data.ed25519PrivateKeyFile != null then
434 " # ed25519 Keyfile managed by nix"
435 else
436 ''
437 # Prefer ED25519 keys (only in 1.1+)
438 [ -f "/etc/tinc/${network}/ed25519_key.priv" ] || tinc -n ${network} generate-ed25519-keys
439 ''
440 }
441 ${
442 if data.rsaPrivateKeyFile != null then
443 " # RSA Keyfile managed by nix"
444 else
445 ''
446 [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tinc -n ${network} generate-rsa-keys 4096
447 ''
448 }
449 # In case there isn't anything to do
450 true
451 else
452 # Tinc 1.0 uses the tincd application
453 [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tincd -n ${network} -K 4096
454 fi
455 '';
456 }
457 )
458 );
459
460 environment.systemPackages =
461 let
462 cli-wrappers = pkgs.stdenv.mkDerivation {
463 name = "tinc-cli-wrappers";
464 nativeBuildInputs = [ pkgs.makeWrapper ];
465 buildCommand = ''
466 mkdir -p $out/bin
467 ${concatStringsSep "\n" (
468 mapAttrsToList (
469 network: data:
470 optionalString (versionAtLeast data.package.version "1.1pre") ''
471 makeWrapper ${data.package}/bin/tinc "$out/bin/tinc.${network}" \
472 --add-flags "--pidfile=/run/tinc.${network}.pid" \
473 --add-flags "--config=/etc/tinc/${network}"
474 ''
475 ) cfg.networks
476 )}
477 '';
478 };
479 in
480 [ cli-wrappers ];
481
482 users.users = flip mapAttrs' cfg.networks (
483 network: _:
484 nameValuePair ("tinc-${network}") ({
485 description = "Tinc daemon user for ${network}";
486 isSystemUser = true;
487 group = "tinc-${network}";
488 })
489 );
490 users.groups = flip mapAttrs' cfg.networks (network: _: nameValuePair "tinc-${network}" { });
491 }
492 );
493
494 meta.maintainers = with maintainers; [
495 minijackson
496 mic92
497 ];
498}