{ config, lib, pkgs, namespace, ... }: with lib; let cfg = config.${namespace}.network; profiles = let make = name: profile: nameValuePair "${name}" { connection = { id = name; type = profile.type; autoconnect = profile.autoconnect; autoconnect-retries = profile.autoconnect-retries; autoconnect-priority = profile.priority; interface-name = profile.interface or cfg.ipv4.interface; }; ipv4 = { method = cfg.ipv4.method; } // ( if (cfg.ipv4.method == "auto") then { } else { address = cfg.ipv4.address; gateway = cfg.ipv4.gateway; dns = cfg.ipv4.dns; } ); ipv6 = { addr-gen-mode = "stable-privacy"; method = "auto"; }; wifi = mkIf (profile.type == "wifi") { mode = "infrastructure"; ssid = profile.ssid; roaming = "allowed"; }; wifi-security = mkIf (profile.type == "wifi") { key-mgmt = profile.keyMgmt; psk = profile.psk; }; }; 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 = { method = "manual"; address = cfg.ipv4.address; gateway = cfg.ipv4.gateway; dns = cfg.ipv4.dns; }; 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 { enable = true; internalInterfaces = cfg.nat.internalInterfaces; externalInterface = cfg.nat.externalInterface; enableIPv6 = cfg.nat.enableIPv6; }; # Configure firewall firewall = { enable = cfg.firewall.enable; allowPing = cfg.firewall.allowPing; allowedTCPPorts = cfg.firewall.allowedTCPPorts; allowedUDPPorts = cfg.firewall.allowedUDPPorts; trustedInterfaces = cfg.firewall.trustedInterfaces; # Default port ranges for KDE Connect allowedTCPPortRanges = lib.mkIf cfg.firewall.kdeConnect.enable [ { from = cfg.firewall.kdeConnect.tcpRange.from; to = cfg.firewall.kdeConnect.tcpRange.to; } ]; allowedUDPPortRanges = lib.mkIf cfg.firewall.kdeConnect.enable [ { from = cfg.firewall.kdeConnect.udpRange.from; to = cfg.firewall.kdeConnect.udpRange.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 = lib.mkIf cfg.iwd.enable { enable = true; settings = cfg.iwd.settings; }; # Configure NetworkManager when enabled networkmanager = mkIf cfg.networkmanager.enable { enable = true; # 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; profiles = profiles; }; }; }; }; }