{ config, lib, pkgs, namespace, ... }: with lib; let cfg = config.${namespace}.network; profiles = let make = name: profile: nameValuePair "${name}" { connection = { inherit (profile) type autoconnect autoconnect-retries; id = name; autoconnect-priority = profile.priority; interface-name = profile.interface or cfg.ipv4.interface; }; ipv4 = { inherit (cfg.ipv4) method; } // ( if (cfg.ipv4.method == "auto") then { } else { inherit (cfg.ipv4) address gateway dns; } ); ipv6 = { addr-gen-mode = "stable-privacy"; method = "auto"; }; wifi = mkIf (profile.type == "wifi") { inherit (profile) ssid; mode = "infrastructure"; roaming = "allowed"; }; wifi-security = mkIf (profile.type == "wifi") { inherit (profile) psk; key-mgmt = profile.keyMgmt; }; }; userProfiles = mapAttrs' make cfg.networkmanager.profiles; # When using manual IP with NM enabled, auto-generate an ethernet profile # so NM actually assigns the static address (not just DHCP). ethernetProfile = optionalAttrs (cfg.ipv4.method == "manual" && cfg.ipv4.interface != "" && cfg.networkmanager.enable) { "static-${cfg.ipv4.interface}" = { connection = { id = "static-${cfg.ipv4.interface}"; type = "ethernet"; autoconnect = true; interface-name = cfg.ipv4.interface; }; ipv4 = { inherit (cfg.ipv4) address gateway dns; method = "manual"; }; ipv6 = { addr-gen-mode = "stable-privacy"; method = "auto"; }; }; }; in userProfiles // ethernetProfile; in { imports = [ ./options.nix ]; config = { assertions = [ { assertion = cfg.hostName != ""; message = "mjallen.network.hostName must be set to a non-empty string."; } { assertion = cfg.ipv4.method == "auto" || cfg.ipv4.method == "manual"; message = "mjallen.network.ipv4.method must be either \"auto\" or \"manual\" (got \"${cfg.ipv4.method}\")."; } { assertion = cfg.ipv4.method != "manual" || cfg.ipv4.interface != ""; message = "mjallen.network.ipv4.interface must be set when ipv4.method is \"manual\"."; } { assertion = cfg.ipv4.method != "manual" || cfg.ipv4.address != ""; message = "mjallen.network.ipv4.address must be set when ipv4.method is \"manual\"."; } { assertion = cfg.ipv4.method != "manual" || cfg.ipv4.gateway != ""; message = "mjallen.network.ipv4.gateway must be set when ipv4.method is \"manual\"."; } { assertion = cfg.nat.enable -> cfg.nat.externalInterface != ""; message = "mjallen.network.nat.externalInterface must be set when NAT is enabled."; } ]; systemd = { services = { NetworkManager-wait-online.enable = false; systemd-networkd-wait-online.enable = lib.mkForce false; systemd-networkd.stopIfChanged = false; systemd-resolved.stopIfChanged = false; }; network.wait-online.enable = false; }; # Restrict Avahi to the configured LAN interface when one is explicitly set. # This prevents Avahi from announcing on virtual/container interfaces (veth*, # podman0, virbr0, etc.) which causes hostname conflicts and suffix mangling # (e.g. "jallen-nas-4.local" instead of "jallen-nas.local"). services.avahi = lib.mkIf (cfg.ipv4.interface != "") { allowInterfaces = [ cfg.ipv4.interface ]; }; networking = { hostName = lib.mkForce cfg.hostName; # Use networkd if enabled useNetworkd = lib.mkDefault true; # Set default gateway and nameservers if in manual mode defaultGateway = lib.mkIf (cfg.ipv4.method == "manual") { address = cfg.ipv4.gateway; interface = lib.mkIf (cfg.ipv4.interface != "") cfg.ipv4.interface; }; nameservers = lib.mkIf (cfg.ipv4.method == "manual") [ cfg.ipv4.dns ]; # Set hostId if provided hostId = lib.mkIf (cfg.hostId != "") cfg.hostId; # Configure NAT if enabled nat = lib.mkIf cfg.nat.enable { inherit (cfg.nat) internalInterfaces externalInterface enableIPv6; enable = true; }; # Configure firewall firewall = { inherit (cfg.firewall) enable allowPing allowedTCPPorts allowedUDPPorts trustedInterfaces ; # Default port ranges for KDE Connect allowedTCPPortRanges = lib.mkIf cfg.firewall.kdeConnect.enable [ { inherit (cfg.firewall.kdeConnect.tcpRange) from to; } ]; allowedUDPPortRanges = lib.mkIf cfg.firewall.kdeConnect.enable [ { inherit (cfg.firewall.kdeConnect.udpRange) from to; } ]; # Extra firewall commands extraCommands = lib.mkIf (cfg.extraFirewallCommands != "") cfg.extraFirewallCommands; }; # Enable iwd daemon when requested. # When iwd is enabled alongside NetworkManager, iwd acts as the WiFi # backend for NM (iwd handles scanning/association; NM handles # connection management). They are not mutually exclusive. wireless.iwd = { inherit (cfg.iwd) enable settings ; }; # Configure NetworkManager when enabled networkmanager = { inherit (cfg.networkmanager) enable; # Use iwd as the WiFi backend when iwd is also enabled wifi.backend = mkIf cfg.iwd.enable "iwd"; wifi.powersave = cfg.networkmanager.powersave; settings.connectivity.uri = "http://nmcheck.gnome.org/check_network_status.txt"; plugins = with pkgs; [ networkmanager-fortisslvpn networkmanager-iodine networkmanager-l2tp networkmanager-openconnect networkmanager-openvpn networkmanager-sstp networkmanager-strongswan networkmanager-vpnc ]; # Configure profiles if any are defined ensureProfiles = mkIf (profiles != { }) { environmentFiles = lib.optional (config.sops.secrets ? wifi) config.sops.secrets.wifi.path; inherit profiles; }; }; }; }; }