at 24.11-pre 8.8 kB view raw
1{ config, lib, pkgs, ... }: 2with lib; 3let 4 keysPath = "/var/lib/yggdrasil/keys.json"; 5 6 cfg = config.services.yggdrasil; 7 settingsProvided = cfg.settings != { }; 8 configFileProvided = cfg.configFile != null; 9 10 format = pkgs.formats.json { }; 11in 12{ 13 imports = [ 14 (mkRenamedOptionModule 15 [ "services" "yggdrasil" "config" ] 16 [ "services" "yggdrasil" "settings" ]) 17 ]; 18 19 options = with types; { 20 services.yggdrasil = { 21 enable = mkEnableOption "the yggdrasil system service"; 22 23 settings = mkOption { 24 type = format.type; 25 default = { }; 26 example = { 27 Peers = [ 28 "tcp://aa.bb.cc.dd:eeeee" 29 "tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff" 30 ]; 31 Listen = [ 32 "tcp://0.0.0.0:xxxxx" 33 ]; 34 }; 35 description = '' 36 Configuration for yggdrasil, as a Nix attribute set. 37 38 Warning: this is stored in the WORLD-READABLE Nix store! 39 Therefore, it is not appropriate for private keys. If you 40 wish to specify the keys, use {option}`configFile`. 41 42 If the {option}`persistentKeys` is enabled then the 43 keys that are generated during activation will override 44 those in {option}`settings` or 45 {option}`configFile`. 46 47 If no keys are specified then ephemeral keys are generated 48 and the Yggdrasil interface will have a random IPv6 address 49 each time the service is started. This is the default. 50 51 If both {option}`configFile` and {option}`settings` 52 are supplied, they will be combined, with values from 53 {option}`configFile` taking precedence. 54 55 You can use the command `nix-shell -p yggdrasil --run "yggdrasil -genconf"` 56 to generate default configuration values with documentation. 57 ''; 58 }; 59 60 configFile = mkOption { 61 type = nullOr path; 62 default = null; 63 example = "/run/keys/yggdrasil.conf"; 64 description = '' 65 A file which contains JSON or HJSON configuration for yggdrasil. See 66 the {option}`settings` option for more information. 67 68 Note: This file must not be larger than 1 MB because it is passed to 69 the yggdrasil process via systemds LoadCredential mechanism. For 70 details, see <https://systemd.io/CREDENTIALS/> and `man 5 71 systemd.exec`. 72 ''; 73 }; 74 75 group = mkOption { 76 type = types.nullOr types.str; 77 default = null; 78 example = "wheel"; 79 description = "Group to grant access to the Yggdrasil control socket. If `null`, only root can access the socket."; 80 }; 81 82 openMulticastPort = mkOption { 83 type = bool; 84 default = false; 85 description = '' 86 Whether to open the UDP port used for multicast peer discovery. The 87 NixOS firewall blocks link-local communication, so in order to make 88 incoming local peering work you will also need to configure 89 `MulticastInterfaces` in your Yggdrasil configuration 90 ({option}`settings` or {option}`configFile`). You will then have to 91 add the ports that you configure there to your firewall configuration 92 ({option}`networking.firewall.allowedTCPPorts` or 93 {option}`networking.firewall.interfaces.<name>.allowedTCPPorts`). 94 ''; 95 }; 96 97 denyDhcpcdInterfaces = mkOption { 98 type = listOf str; 99 default = [ ]; 100 example = [ "tap*" ]; 101 description = '' 102 Disable the DHCP client for any interface whose name matches 103 any of the shell glob patterns in this list. Use this 104 option to prevent the DHCP client from broadcasting requests 105 on the yggdrasil network. It is only necessary to do so 106 when yggdrasil is running in TAP mode, because TUN 107 interfaces do not support broadcasting. 108 ''; 109 }; 110 111 package = mkPackageOption pkgs "yggdrasil" { }; 112 113 persistentKeys = mkEnableOption '' 114 persistent keys. If enabled then keys will be generated once and Yggdrasil 115 will retain the same IPv6 address when the service is 116 restarted. Keys are stored at ${keysPath} 117 ''; 118 119 extraArgs = mkOption { 120 type = listOf str; 121 default = [ ]; 122 example = [ "-loglevel" "info" ]; 123 description = "Extra command line arguments."; 124 }; 125 126 }; 127 }; 128 129 config = mkIf cfg.enable ( 130 let 131 binYggdrasil = "${cfg.package}/bin/yggdrasil"; 132 binHjson = "${pkgs.hjson-go}/bin/hjson-cli"; 133 in 134 { 135 assertions = [{ 136 assertion = config.networking.enableIPv6; 137 message = "networking.enableIPv6 must be true for yggdrasil to work"; 138 }]; 139 140 # This needs to be a separate service. The yggdrasil service fails if 141 # this is put into its preStart. 142 systemd.services.yggdrasil-persistent-keys = lib.mkIf cfg.persistentKeys { 143 wantedBy = [ "multi-user.target" ]; 144 before = [ "yggdrasil.service" ]; 145 serviceConfig.Type = "oneshot"; 146 serviceConfig.RemainAfterExit = true; 147 script = '' 148 if [ ! -e ${keysPath} ] 149 then 150 mkdir --mode=700 -p ${builtins.dirOf keysPath} 151 ${binYggdrasil} -genconf -json \ 152 | ${pkgs.jq}/bin/jq \ 153 'to_entries|map(select(.key|endswith("Key")))|from_entries' \ 154 > ${keysPath} 155 fi 156 ''; 157 }; 158 159 systemd.services.yggdrasil = { 160 description = "Yggdrasil Network Service"; 161 after = [ "network-pre.target" ]; 162 wants = [ "network.target" ]; 163 before = [ "network.target" ]; 164 wantedBy = [ "multi-user.target" ]; 165 166 # This script first prepares the config file, then it starts Yggdrasil. 167 # The preparation could also be done in ExecStartPre/preStart but only 168 # systemd versions >= v252 support reading credentials in ExecStartPre. As 169 # of February 2023, systemd v252 is not yet in the stable branch of NixOS. 170 # 171 # This could be changed in the future once systemd version v252 has 172 # reached NixOS but it does not have to be. Config file preparation is 173 # fast enough, it does not need elevated privileges, and `set -euo 174 # pipefail` should make sure that the service is not started if the 175 # preparation fails. Therefore, it is not necessary to move the 176 # preparation to ExecStartPre. 177 script = '' 178 set -euo pipefail 179 180 # prepare config file 181 ${(if settingsProvided || configFileProvided || cfg.persistentKeys then 182 "echo " 183 184 + (lib.optionalString settingsProvided 185 "'${builtins.toJSON cfg.settings}'") 186 + (lib.optionalString configFileProvided 187 "$(${binHjson} -c \"$CREDENTIALS_DIRECTORY/yggdrasil.conf\")") 188 + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})") 189 + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf" 190 else 191 "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf"} 192 193 # start yggdrasil 194 ${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf ${lib.strings.escapeShellArgs cfg.extraArgs} 195 ''; 196 197 serviceConfig = { 198 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 199 Restart = "always"; 200 201 DynamicUser = true; 202 StateDirectory = "yggdrasil"; 203 RuntimeDirectory = "yggdrasil"; 204 RuntimeDirectoryMode = "0750"; 205 BindReadOnlyPaths = lib.optional cfg.persistentKeys keysPath; 206 LoadCredential = 207 mkIf configFileProvided "yggdrasil.conf:${cfg.configFile}"; 208 209 AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE"; 210 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE"; 211 MemoryDenyWriteExecute = true; 212 ProtectControlGroups = true; 213 ProtectHome = "tmpfs"; 214 ProtectKernelModules = true; 215 ProtectKernelTunables = true; 216 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; 217 RestrictNamespaces = true; 218 RestrictRealtime = true; 219 SystemCallArchitectures = "native"; 220 SystemCallFilter = [ "@system-service" "~@privileged @keyring" ]; 221 } // (if (cfg.group != null) then { 222 Group = cfg.group; 223 } else { }); 224 }; 225 226 networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces; 227 networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ]; 228 229 # Make yggdrasilctl available on the command line. 230 environment.systemPackages = [ cfg.package ]; 231 } 232 ); 233 meta = { 234 doc = ./yggdrasil.md; 235 maintainers = with lib.maintainers; [ gazally ehmry ]; 236 }; 237}