at 23.11-pre 8.5 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 (lib.mdDoc "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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc "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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = mkOption { 112 type = package; 113 default = pkgs.yggdrasil; 114 defaultText = literalExpression "pkgs.yggdrasil"; 115 description = lib.mdDoc "Yggdrasil package to use."; 116 }; 117 118 persistentKeys = mkEnableOption (lib.mdDoc '' 119 If enabled then keys will be generated once and Yggdrasil 120 will retain the same IPv6 address when the service is 121 restarted. Keys are stored at ${keysPath}. 122 ''); 123 124 }; 125 }; 126 127 config = mkIf cfg.enable ( 128 let 129 binYggdrasil = "${cfg.package}/bin/yggdrasil"; 130 binHjson = "${pkgs.hjson-go}/bin/hjson-cli"; 131 in 132 { 133 assertions = [{ 134 assertion = config.networking.enableIPv6; 135 message = "networking.enableIPv6 must be true for yggdrasil to work"; 136 }]; 137 138 system.activationScripts.yggdrasil = mkIf cfg.persistentKeys '' 139 if [ ! -e ${keysPath} ] 140 then 141 mkdir --mode=700 -p ${builtins.dirOf keysPath} 142 ${binYggdrasil} -genconf -json \ 143 | ${pkgs.jq}/bin/jq \ 144 'to_entries|map(select(.key|endswith("Key")))|from_entries' \ 145 > ${keysPath} 146 fi 147 ''; 148 149 systemd.services.yggdrasil = { 150 description = "Yggdrasil Network Service"; 151 after = [ "network-pre.target" ]; 152 wants = [ "network.target" ]; 153 before = [ "network.target" ]; 154 wantedBy = [ "multi-user.target" ]; 155 156 # This script first prepares the config file, then it starts Yggdrasil. 157 # The preparation could also be done in ExecStartPre/preStart but only 158 # systemd versions >= v252 support reading credentials in ExecStartPre. As 159 # of February 2023, systemd v252 is not yet in the stable branch of NixOS. 160 # 161 # This could be changed in the future once systemd version v252 has 162 # reached NixOS but it does not have to be. Config file preparation is 163 # fast enough, it does not need elevated privileges, and `set -euo 164 # pipefail` should make sure that the service is not started if the 165 # preparation fails. Therefore, it is not necessary to move the 166 # preparation to ExecStartPre. 167 script = '' 168 set -euo pipefail 169 170 # prepare config file 171 ${(if settingsProvided || configFileProvided || cfg.persistentKeys then 172 "echo " 173 174 + (lib.optionalString settingsProvided 175 "'${builtins.toJSON cfg.settings}'") 176 + (lib.optionalString configFileProvided 177 "$(${binHjson} -c \"$CREDENTIALS_DIRECTORY/yggdrasil.conf\")") 178 + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})") 179 + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf" 180 else 181 "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf"} 182 183 # start yggdrasil 184 ${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf 185 ''; 186 187 serviceConfig = { 188 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 189 Restart = "always"; 190 191 DynamicUser = true; 192 StateDirectory = "yggdrasil"; 193 RuntimeDirectory = "yggdrasil"; 194 RuntimeDirectoryMode = "0750"; 195 BindReadOnlyPaths = lib.optional cfg.persistentKeys keysPath; 196 LoadCredential = 197 mkIf configFileProvided "yggdrasil.conf:${cfg.configFile}"; 198 199 AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE"; 200 CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE"; 201 MemoryDenyWriteExecute = true; 202 ProtectControlGroups = true; 203 ProtectHome = "tmpfs"; 204 ProtectKernelModules = true; 205 ProtectKernelTunables = true; 206 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; 207 RestrictNamespaces = true; 208 RestrictRealtime = true; 209 SystemCallArchitectures = "native"; 210 SystemCallFilter = [ "@system-service" "~@privileged @keyring" ]; 211 } // (if (cfg.group != null) then { 212 Group = cfg.group; 213 } else { }); 214 }; 215 216 networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces; 217 networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ]; 218 219 # Make yggdrasilctl available on the command line. 220 environment.systemPackages = [ cfg.package ]; 221 } 222 ); 223 meta = { 224 doc = ./yggdrasil.md; 225 maintainers = with lib.maintainers; [ gazally ehmry ]; 226 }; 227}