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