diff --git a/modules/networking/default.nix b/modules/networking/default.nix index 1065c2688..3ef09e5a6 100644 --- a/modules/networking/default.nix +++ b/modules/networking/default.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, ... }: with lib; @@ -10,20 +10,64 @@ let emptyList = lst: if lst != [] then lst else ["empty"]; quoteStrings = concatMapStringsSep " " (str: "'${str}'"); - setNetworkServices = optionalString (cfg.knownNetworkServices != []) '' - networkservices=$(networksetup -listallnetworkservices) - ${concatMapStringsSep "\n" (srv: '' - case "$networkservices" in - *'${srv}'*) - networksetup -setdnsservers '${srv}' ${quoteStrings (emptyList cfg.dns)} - networksetup -setsearchdomains '${srv}' ${quoteStrings (emptyList cfg.search)} - ;; - esac - '') cfg.knownNetworkServices} + setLocations = optionalString (cfg.knownNetworkServices != [] && cfg.location != {}) '' + curr_location=$(networksetup -getcurrentlocation) + + readarray -t curr_locations_array < <(networksetup -listlocations) + + declare -A curr_locations + for location in "''${curr_locations_array[@]}"; do + curr_locations[$location]=1 + done + + declare -A goal_locations + for location in ${strings.escapeShellArgs (builtins.attrNames cfg.location)}; do + goal_locations[$location]=1 + done + + for location in "''${!goal_locations[@]}"; do + if [[ ! -v curr_locations[$location] ]]; then + networksetup -createlocation "$location" populate > /dev/null + fi + done + + # switch to a location that surely does not need to be deleted + networksetup -switchtolocation ${strings.escapeShellArg ( + builtins.head (builtins.attrNames cfg.location) + )} > /dev/null + + for location in "''${!curr_locations[@]}"; do + if [[ ! -v goal_locations[$location] ]]; then + networksetup -deletelocation "$location" > /dev/null + fi + done + + ${concatMapStringsSep "\n" (location: '' + networksetup -switchtolocation ${strings.escapeShellArg location} > /dev/null + + networkservices=$(networksetup -listallnetworkservices) + ${concatMapStringsSep "\n" (srv: '' + case "$networkservices" in + *'${srv}'*) + networksetup -setdnsservers '${srv}' ${quoteStrings (emptyList cfg.location.${location}.dns)} + networksetup -setsearchdomains '${srv}' ${quoteStrings (emptyList cfg.location.${location}.search)} + ;; + esac + '') cfg.knownNetworkServices} + '') (builtins.attrNames cfg.location)} + + if [[ -v goal_locations[$curr_location] ]]; then + networksetup -switchtolocation "$curr_location" > /dev/null + fi ''; in { + imports = [ + (mkAliasOptionModule ["networking" "dns"] ["networking" "location" "Automatic" "dns"]) + (mkAliasOptionModule ["networking" "search"] ["networking" "location" "Automatic" "search"]) + ]; + options = { networking.computerName = mkOption { type = types.nullOr types.str; @@ -75,32 +119,56 @@ in default = []; example = [ "Wi-Fi" "Ethernet Adaptor" "Thunderbolt Ethernet" ]; description = '' - List of networkservices that should be configured. + List of network services that should be configured. To display a list of all the network services on the server's hardware ports, use {command}`networksetup -listallnetworkservices`. ''; }; - networking.dns = mkOption { - type = types.listOf types.str; - default = []; - example = [ "8.8.8.8" "8.8.4.4" "2001:4860:4860::8888" "2001:4860:4860::8844" ]; - description = "The list of dns servers used when resolving domain names."; - }; + networking.location = mkOption { + type = types.attrsOf (types.submodule { + options = { + dns = mkOption { + type = types.listOf types.str; + default = []; + example = [ "8.8.8.8" "8.8.4.4" "2001:4860:4860::8888" "2001:4860:4860::8844" ]; + description = "The list of DNS servers used when resolving domain names."; + }; + + search = mkOption { + type = types.listOf types.str; + default = []; + description = "The list of search paths used when resolving domain names."; + }; + }; + }); + default = {}; + description = '' + Set of network locations to configure. - networking.search = mkOption { - type = types.listOf types.str; - default = []; - description = "The list of search paths used when resolving domain names."; + By default, a system comes with a single location called "Automatic", but you can + define additional locations to switch between different network configurations. + + If you define any locations here, you must also explicitly define the "Automatic" + location if you want it to exist. + ''; }; }; config = { warnings = [ - (mkIf (cfg.knownNetworkServices == [] && cfg.dns != []) "networking.knownNetworkServices is empty, dns servers will not be configured.") - (mkIf (cfg.knownNetworkServices == [] && cfg.search != []) "networking.knownNetworkServices is empty, dns searchdomains will not be configured.") + ( + mkIf (cfg.knownNetworkServices == [] && ( + builtins.any (l: l.dns != []) (builtins.attrValues cfg.location) + )) "networking.knownNetworkServices is empty, DNS servers will not be configured." + ) + ( + mkIf (cfg.knownNetworkServices == [] && ( + builtins.any (l: l.search != []) (builtins.attrValues cfg.location) + )) "networking.knownNetworkServices is empty, DNS search domains will not be configured." + ) ]; system.activationScripts.networking.text = '' @@ -116,7 +184,7 @@ in scutil --set LocalHostName ${escapeShellArg cfg.localHostName} ''} - ${setNetworkServices} + ${setLocations} ''; }; diff --git a/tests/networking-networkservices.nix b/tests/networking-networkservices.nix index 46213175d..c04c93d58 100644 --- a/tests/networking-networkservices.nix +++ b/tests/networking-networkservices.nix @@ -1,15 +1,35 @@ -{ config, pkgs, ... }: +{ lib, config, ... }: { - networking.knownNetworkServices = [ "Wi-Fi" "Thunderbolt Ethernet" ]; - networking.dns = [ "8.8.8.8" "8.8.4.4" ]; + networking.knownNetworkServices = [ + "Wi-Fi" + "Thunderbolt Ethernet" + ]; + networking.dns = [ + "8.8.8.8" + "8.8.4.4" + ]; + networking.location."Home Lab" = { + search = [ "home.lab" ]; + }; test = '' echo checking dns settings in /activate >&2 + + grep "networksetup -switchtolocation ${lib.strings.escapeShellArg "Automatic"}" ${config.out}/activate grep "networksetup -setdnsservers 'Wi-Fi' '8.8.8.8' '8.8.4.4'" ${config.out}/activate grep "networksetup -setdnsservers 'Thunderbolt Ethernet' '8.8.8.8' '8.8.4.4'" ${config.out}/activate - echo checking empty searchdomain settings in /activate >&2 + + grep "networksetup -switchtolocation 'Home Lab'" ${config.out}/activate + grep "networksetup -setdnsservers 'Wi-Fi' 'empty'" ${config.out}/activate + grep "networksetup -setdnsservers 'Thunderbolt Ethernet' 'empty'" ${config.out}/activate + + echo checking searchdomain settings in /activate >&2 + grep "networksetup -setsearchdomains 'Wi-Fi' 'empty'" ${config.out}/activate grep "networksetup -setsearchdomains 'Thunderbolt Ethernet' 'empty'" ${config.out}/activate + + grep "networksetup -setsearchdomains 'Wi-Fi' 'home.lab'" ${config.out}/activate + grep "networksetup -setsearchdomains 'Thunderbolt Ethernet' 'home.lab'" ${config.out}/activate ''; }