at 25.11-pre 17 kB view raw
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}