at 25.11-pre 8.6 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 inherit (lib) types; 10 inherit (lib.attrsets) 11 filterAttrs 12 mapAttrs 13 mapAttrs' 14 mapAttrsToList 15 nameValuePair 16 ; 17 inherit (lib.lists) 18 concatMap 19 concatLists 20 filter 21 flatten 22 ; 23 inherit (lib.modules) mkIf; 24 inherit (lib.options) literalExpression mkOption; 25 inherit (lib.strings) hasInfix replaceStrings; 26 inherit (lib.trivial) flip pipe; 27 28 removeNulls = filterAttrs (_: v: v != null); 29 30 escapeCredentialName = input: replaceStrings [ "\\" ] [ "_" ] input; 31 32 privateKeyCredential = interfaceName: escapeCredentialName "wireguard-${interfaceName}-private-key"; 33 presharedKeyCredential = 34 interfaceName: peer: escapeCredentialName "wireguard-${interfaceName}-${peer.name}-preshared-key"; 35 36 interfaceCredentials = 37 interfaceName: interface: 38 [ "${privateKeyCredential interfaceName}:${interface.privateKeyFile}" ] 39 ++ pipe interface.peers [ 40 (filter (peer: peer.presharedKeyFile != null)) 41 (map (peer: "${presharedKeyCredential interfaceName peer}:${peer.presharedKeyFile}")) 42 ]; 43 44 generateNetdev = 45 name: interface: 46 nameValuePair "40-${name}" { 47 netdevConfig = removeNulls { 48 Kind = "wireguard"; 49 Name = name; 50 MTUBytes = interface.mtu; 51 }; 52 wireguardConfig = removeNulls { 53 PrivateKey = "@${privateKeyCredential name}"; 54 ListenPort = interface.listenPort; 55 FirewallMark = interface.fwMark; 56 RouteTable = if interface.allowedIPsAsRoutes then interface.table else null; 57 RouteMetric = interface.metric; 58 }; 59 wireguardPeers = map (generateWireguardPeer name) interface.peers; 60 }; 61 62 generateWireguardPeer = 63 interfaceName: peer: 64 removeNulls { 65 PublicKey = peer.publicKey; 66 PresharedKey = 67 if peer.presharedKeyFile == null then null else "@${presharedKeyCredential interfaceName peer}"; 68 AllowedIPs = peer.allowedIPs; 69 Endpoint = peer.endpoint; 70 PersistentKeepalive = peer.persistentKeepalive; 71 }; 72 73 generateNetwork = name: interface: { 74 matchConfig.Name = name; 75 address = interface.ips; 76 }; 77 78 cfg = config.networking.wireguard; 79 80 refreshEnabledInterfaces = filterAttrs ( 81 name: interface: interface.dynamicEndpointRefreshSeconds != 0 82 ) cfg.interfaces; 83 84 generateRefreshTimer = 85 name: interface: 86 nameValuePair "wireguard-dynamic-refresh-${name}" { 87 partOf = [ "wireguard-dynamic-refresh-${name}.service" ]; 88 wantedBy = [ "timers.target" ]; 89 description = "Wireguard dynamic endpoint refresh (${name}) timer"; 90 timerConfig.OnBootSec = interface.dynamicEndpointRefreshSeconds; 91 timerConfig.OnUnitInactiveSec = interface.dynamicEndpointRefreshSeconds; 92 }; 93 94 generateRefreshService = 95 name: interface: 96 nameValuePair "wireguard-dynamic-refresh-${name}" { 97 description = "Wireguard dynamic endpoint refresh (${name})"; 98 after = [ "network-online.target" ]; 99 wants = [ "network-online.target" ]; 100 path = with pkgs; [ 101 iproute2 102 systemd 103 ]; 104 # networkd doesn't provide a mechanism for refreshing endpoints. 105 # See: https://github.com/systemd/systemd/issues/9911 106 # This hack does the job but takes down the whole interface to do it. 107 script = '' 108 ip link delete ${name} 109 networkctl reload 110 ''; 111 }; 112 113in 114{ 115 meta.maintainers = [ lib.maintainers.majiir ]; 116 117 options.networking.wireguard = { 118 useNetworkd = mkOption { 119 default = config.networking.useNetworkd; 120 defaultText = literalExpression "config.networking.useNetworkd"; 121 type = types.bool; 122 description = '' 123 Whether to use networkd as the network configuration backend for 124 Wireguard instead of the legacy script-based system. 125 126 ::: {.warning} 127 Some options have slightly different behavior with the networkd and 128 script-based backends. Check the documentation for each Wireguard 129 option you use before enabling this option. 130 ::: 131 ''; 132 }; 133 }; 134 135 config = mkIf (cfg.enable && cfg.useNetworkd) { 136 137 # TODO: Some of these options may be possible to support in networkd. 138 # 139 # privateKey and presharedKey are trivial to support, but we deliberately 140 # don't in order to discourage putting secrets in the /nix store. 141 # 142 # generatePrivateKeyFile can be supported if we can order a service before 143 # networkd configures interfaces. There is also a systemd feature request 144 # for key generation: https://github.com/systemd/systemd/issues/14282 145 # 146 # preSetup, postSetup, preShutdown and postShutdown may be possible, but 147 # networkd is not likely to support script hooks like this directly. See: 148 # https://github.com/systemd/systemd/issues/11629 149 # 150 # socketNamespace and interfaceNamespace can be implemented once networkd 151 # supports setting a netdev's namespace. See: 152 # https://github.com/systemd/systemd/issues/11103 153 # https://github.com/systemd/systemd/pull/14915 154 155 assertions = concatLists ( 156 flip mapAttrsToList cfg.interfaces ( 157 name: interface: 158 [ 159 # Interface assertions 160 { 161 assertion = interface.privateKey == null; 162 message = "networking.wireguard.interfaces.${name}.privateKey cannot be used with networkd. Use privateKeyFile instead."; 163 } 164 { 165 assertion = !interface.generatePrivateKeyFile; 166 message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile cannot be used with networkd."; 167 } 168 { 169 assertion = interface.preSetup == ""; 170 message = "networking.wireguard.interfaces.${name}.preSetup cannot be used with networkd."; 171 } 172 { 173 assertion = interface.postSetup == ""; 174 message = "networking.wireguard.interfaces.${name}.postSetup cannot be used with networkd."; 175 } 176 { 177 assertion = interface.preShutdown == ""; 178 message = "networking.wireguard.interfaces.${name}.preShutdown cannot be used with networkd."; 179 } 180 { 181 assertion = interface.postShutdown == ""; 182 message = "networking.wireguard.interfaces.${name}.postShutdown cannot be used with networkd."; 183 } 184 { 185 assertion = interface.socketNamespace == null; 186 message = "networking.wireguard.interfaces.${name}.socketNamespace cannot be used with networkd."; 187 } 188 { 189 assertion = interface.interfaceNamespace == null; 190 message = "networking.wireguard.interfaces.${name}.interfaceNamespace cannot be used with networkd."; 191 } 192 { 193 assertion = interface.type == "wireguard"; 194 message = "networking.wireguard.interfaces.${name}.type value must be \"wireguard\" when used with networkd."; 195 } 196 ] 197 ++ flip concatMap interface.ips (ip: [ 198 # IP assertions 199 { 200 assertion = hasInfix "/" ip; 201 message = "networking.wireguard.interfaces.${name}.ips value \"${ip}\" requires a subnet (e.g. 192.0.2.1/32) with networkd."; 202 } 203 ]) 204 ++ flip concatMap interface.peers (peer: [ 205 # Peer assertions 206 { 207 assertion = peer.presharedKey == null; 208 message = "networking.wireguard.interfaces.${name}.peers[].presharedKey cannot be used with networkd. Use presharedKeyFile instead."; 209 } 210 { 211 assertion = peer.dynamicEndpointRefreshSeconds == null; 212 message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshSeconds cannot be used with networkd. Use networking.wireguard.interfaces.${name}.dynamicEndpointRefreshSeconds instead."; 213 } 214 { 215 assertion = peer.dynamicEndpointRefreshRestartSeconds == null; 216 message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshRestartSeconds cannot be used with networkd."; 217 } 218 ]) 219 ) 220 ); 221 222 systemd.network = { 223 enable = true; 224 netdevs = mapAttrs' generateNetdev cfg.interfaces; 225 networks = mapAttrs generateNetwork cfg.interfaces; 226 }; 227 228 systemd.timers = mapAttrs' generateRefreshTimer refreshEnabledInterfaces; 229 systemd.services = (mapAttrs' generateRefreshService refreshEnabledInterfaces) // { 230 systemd-networkd.serviceConfig.LoadCredential = flatten ( 231 mapAttrsToList interfaceCredentials cfg.interfaces 232 ); 233 }; 234 }; 235}