diff --git a/flake.nix b/flake.nix index 1951766..22f5bd5 100644 --- a/flake.nix +++ b/flake.nix @@ -84,6 +84,7 @@ modules = [ ./system/hardware/bpi_r3/emmc.nix ./system/hosts/router + ./system/modules/router { networking.hostName = "router"; } ]; }; @@ -92,6 +93,7 @@ modules = [ ./system/hardware/bpi_r3/sd.nix ./system/hosts/router + ./system/modules/router { networking.hostName = "router"; } ]; }; diff --git a/home/common/general.nix b/home/common/general.nix index 576ab1b..43be29c 100644 --- a/home/common/general.nix +++ b/home/common/general.nix @@ -47,7 +47,7 @@ }); extraPackages = with pkgs; [ # utils - gnused mktemp fzf coreutils-full findutils xdg-utils git gnupg whois curl + gnused mktemp fzf coreutils-full findutils xdg-utils gnupg whois curl file mediainfo unzip gnutar man rclone sshfs trash-cli # for preview exa bat @@ -94,6 +94,7 @@ credential.helper = "${pkgs.gitAndTools.gitFull}/bin/git-credential-libsecret"; init.defaultBranch = "master"; }; + lfs.enable = true; }; bat = { enable = true; diff --git a/system/hosts/nixserver/default.nix b/system/hosts/nixserver/default.nix index aaf014e..f20bcde 100644 --- a/system/hosts/nixserver/default.nix +++ b/system/hosts/nixserver/default.nix @@ -64,7 +64,6 @@ in { }; zramSwap.enable = true; swapDevices = [ ]; - services.tlp.enable = false; impermanence = { enable = true; path = /persist; diff --git a/system/hosts/router/default.nix b/system/hosts/router/default.nix index 1540390..7866089 100644 --- a/system/hosts/router/default.nix +++ b/system/hosts/router/default.nix @@ -4,7 +4,48 @@ let rootUuid = "44444444-4444-4444-8888-888888888888"; rootPart = "/dev/disk/by-uuid/${rootUuid}"; + cfg = config.router-settings; + hapdConfig = { + inherit (cfg) country_code wpa_passphrase; + he_su_beamformer = true; + he_su_beamformee = true; + he_mu_beamformer = true; + he_bss_color = 128; + he_spr_sr_control = 3; + he_default_pe_duration = 4; + he_rts_threshold = 1023; + he_mu_edca_qos_info_param_count = 0; + he_mu_edca_qos_info_q_ack = 0; + he_mu_edca_qos_info_queue_request = 0; + he_mu_edca_qos_info_txop_request = 0; + he_mu_edca_ac_be_aifsn = 8; + he_mu_edca_ac_be_aci = 0; + he_mu_edca_ac_be_ecwmin = 9; + he_mu_edca_ac_be_ecwmax = 10; + he_mu_edca_ac_be_timer = 255; + he_mu_edca_ac_bk_aifsn = 15; + he_mu_edca_ac_bk_aci = 1; + he_mu_edca_ac_bk_ecwmin = 9; + he_mu_edca_ac_bk_ecwmax = 10; + he_mu_edca_ac_bk_timer = 255; + he_mu_edca_ac_vi_ecwmin = 5; + he_mu_edca_ac_vi_ecwmax = 7; + he_mu_edca_ac_vi_aifsn = 5; + he_mu_edca_ac_vi_aci = 2; + he_mu_edca_ac_vi_timer = 255; + he_mu_edca_ac_vo_aifsn = 5; + he_mu_edca_ac_vo_aci = 3; + he_mu_edca_ac_vo_ecwmin = 5; + he_mu_edca_ac_vo_ecwmax = 7; + he_mu_edca_ac_vo_timer = 255; + preamble = true; + vht_oper_chwidth = 1; # 80mhz ch width + vht_oper_centr_freq_seg0_idx = 42; + vht_capab = "[RXLDPC][SHORT-GI-80][SHORT-GI-160][TX-STBC-2BY1][SU-BEAMFORMER][SU-BEAMFORMEE][MU-BEAMFORMER][MU-BEAMFORMEE][RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN][RX-STBC-1][SOUNDING-DIMENSION-4][BF-ANTENNA-4][VHT160][MAX-MPDU-11454][MAX-A-MPDU-LEN-EXP7]"; + country3 = "0x49"; # indoor + }; in { + imports = [ ./options.nix ]; system.stateVersion = "22.11"; fileSystems = { # mount root on tmpfs @@ -27,6 +68,56 @@ in { directories = [ { directory = /home/${config.common.mainUsername}; user = config.common.mainUsername; group = "users"; mode = "0700"; } { directory = /root; mode = "0700"; } + { directory = /var/db/dhcpcd; user = "root"; group = "root"; mode = "0755"; } + { directory = /var/lib/kea; user = "root"; group = "root"; mode = "0755"; } ]; }; + router.enable = true; + router.interfaces.wlan0 = { + bridge = "br0"; + hostapd.enable = true; + hostapd.settings = { + inherit (cfg) ssid; + hw_mode = "g"; + supported_rates = [ 60 90 120 180 240 360 480 540 ]; + basic_rates = [ 60 120 240 ]; + ht_capab = "[LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935]"; + } // hapdConfig; + }; + router.interfaces.wlan1 = { + bridge = "br0"; + hostapd.enable = true; + hostapd.settings = { + ssid = "${cfg.ssid} 5G"; + ieee80211h = true; + hw_mode = "a"; + tx_queue_data2_burst = 2; + ht_capab = "[HT40+][LDPC][SHORT-GI-20][SHORT-GI-40][TX-STBC][RX-STBC1][MAX-AMSDU-7935]"; + } // hapdConfig; + }; + router.interfaces.lan0 = { + matchUdevAttrs.address = "11:11:11:11:11:11"; + macAddress = "11:22:33:44:55:66"; + }; + router.interfaces.wan0 = { + matchUdevAttrs.address = "22:11:11:11:11:11"; + macAddress = "22:22:33:44:55:66"; + dhcpcd.enable = true; + }; + router.interfaces.br0 = { + ipv4.addresses = [ { + address = "10.0.0.1"; + prefixLength = 24; + dns = [ "10.0.0.1" ]; + } ]; + ipv6.addresses = [ { + address = "fd00::1"; + prefixLength = 64; + dns = [ "fd00::1" ]; + } ]; + ipv4.kea.enable = true; + ipv6.kea.enable = true; + ipv6.radvd.enable = true; + ipv6.corerad.enable = false; + }; } diff --git a/system/hosts/router/options.nix b/system/hosts/router/options.nix new file mode 100644 index 0000000..29fbf61 --- /dev/null +++ b/system/hosts/router/options.nix @@ -0,0 +1,16 @@ +{ lib +, ... }: + +{ + options.router-settings = { + country_code = lib.mkOption { + type = lib.types.str; + }; + ssid = lib.mkOption { + type = lib.types.str; + }; + wpa_passphrase = lib.mkOption { + type = lib.types.str; + }; + }; +} diff --git a/system/modules/impermanence.nix b/system/modules/impermanence.nix index 103dd14..2304b27 100644 --- a/system/modules/impermanence.nix +++ b/system/modules/impermanence.nix @@ -72,7 +72,7 @@ in { { directory = /var/lib/swtpm-localca; user = "root"; group = "root"; mode = "0750"; } ]))) ++ (lib.optionals config.networking.wireless.iwd.enable [ { directory = /var/lib/iwd; user = "root"; group = "root"; mode = "0700"; } - ]) ++ (lib.optionals (builtins.any (x: x.useDHCP) (builtins.attrValues config.networking.interfaces) || config.networking.useDHCP) [ + ]) ++ (lib.optionals (builtins.any (x: x.useDHCP != false) (builtins.attrValues config.networking.interfaces) && config.networking.useDHCP) [ { directory = /var/db/dhcpcd; user = "root"; group = "root"; mode = "0755"; } ]) ++ (lib.optionals config.services.gitea.enable [ { directory = /var/lib/gitea; user = "gitea"; group = "gitea"; mode = "0755"; } diff --git a/system/modules/router/avahi.nix b/system/modules/router/avahi.nix new file mode 100644 index 0000000..bd59d24 --- /dev/null +++ b/system/modules/router/avahi.nix @@ -0,0 +1,11 @@ +{ lib +, config +, ... }: + +let + cfg = config.router; +in { + services.avahi.enable = lib.mkDefault true; + services.avahi.publish.enable = lib.mkDefault true; + services.avahi.allowInterfaces = lib.mkDefault (builtins.attrNames cfg.interfaces); +} diff --git a/system/modules/router/corerad.nix b/system/modules/router/corerad.nix new file mode 100644 index 0000000..3a8c54d --- /dev/null +++ b/system/modules/router/corerad.nix @@ -0,0 +1,60 @@ +{ lib +, config +, pkgs +, utils +, ... }: + +let + cfg = config.router; +in { + config = lib.mkIf cfg.enable { + systemd.services = lib.mapAttrs' (interface: icfg: let + cfg = icfg.ipv6.corerad; + escapedInterface = utils.escapeSystemdPath interface; + settingsFormat = pkgs.formats.toml {}; + configFile = if cfg.configFile != null then cfg.configFile else settingsFormat.generate "corerad-${escapedInterface}.toml" ({ + interfaces = [ + (rec { + name = interface; + monitor = false; + advertise = true; + managed = icfg.ipv6.kea.enable && builtins.any (x: lib.hasInfix ":" x.address) icfg.ipv6.addresses; + other_config = managed && cfg.interfaceSettings.managed or true; + prefix = map ({ address, prefixLength, coreradSettings, ... }: { + prefix = "${address}/${toString prefixLength}"; + autonomous = !(other_config && cfg.interfaceSettings.other_config or true); + } // coreradSettings) icfg.ipv6.addresses; + route = builtins.concatLists (map ({ address, prefixLength, gateways, ... }: map (gateway: { + prefix = "${if builtins.isString gateway then gateway else gateway.address}/${toString (if gateway.prefixLength or null != null then gateway.prefixLength else prefixLength)}"; + } // (gateway.coreradSettings or { })) gateways) icfg.ipv6.addresses); + rdnss = builtins.concatLists (map ({ dns, ... }: map (dns: { + servers = if builtins.isString dns then dns else dns.address; + } // (dns.coreradSettings or { })) dns) icfg.ipv6.addresses); + } // cfg.interfaceSettings) + ]; + } // cfg.settings); + package = pkgs.corerad; + in { + name = "corerad-${escapedInterface}"; + value = lib.mkIf icfg.ipv6.corerad.enable { + description = "CoreRAD IPv6 NDP RA daemon (${interface})"; + after = [ "network.target" "sys-subsystem-net-devices-${escapedInterface}.device" ]; + bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + LimitNPROC = 512; + LimitNOFILE = 1048576; + CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW"; + AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW"; + NoNewPrivileges = true; + DynamicUser = true; + Type = "notify"; + NotifyAccess = "main"; + ExecStart = "${lib.getBin package}/bin/corerad -c=${configFile}"; + Restart = "on-failure"; + RestartKillSignal = "SIGHUP"; + }; + }; + }) cfg.interfaces; + }; +} diff --git a/system/modules/router/default.nix b/system/modules/router/default.nix new file mode 100644 index 0000000..c0d1a28 --- /dev/null +++ b/system/modules/router/default.nix @@ -0,0 +1,369 @@ +{ lib +, config +, pkgs +, ... }: + +let + cfg = config.router; +in { + imports = [ + /*./avahi.nix*/ + ./hostapd.nix + ./kea.nix + ./radvd.nix + ./corerad.nix + ]; + + options.router = { + enable = lib.mkEnableOption "router config"; + interfaces = lib.mkOption { + default = { }; + description = "All interfaces managed by the router"; + type = lib.types.attrsOf (lib.types.submodule { + options.matchUdevAttrs = lib.mkOption { + default = { }; + description = lib.mdDoc '' + When a device with those attrs is detected by udev, the device is automatically renamed to this interface name. + + See [kernel docs](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/Documentation/ABI/testing/sysfs-class-net?h=linux-6.3.y) for the list of attrs available. + ''; + example = lib.literalExpression { address = "11:22:33:44:55:66"; }; + type = lib.types.attrs; + }; + options.bridge = lib.mkOption { + description = "Add this device to this bridge"; + default = null; + type = with lib.types; nullOr str; + }; + options.macAddress = lib.mkOption { + description = "Change this device's mac address to this"; + default = null; + type = with lib.types; nullOr str; + }; + options.hostapd = lib.mkOption { + description = "hostapd options"; + default = { }; + type = lib.types.submodule { + options.enable = lib.mkEnableOption "hostapd"; + options.settings = lib.mkOption { + description = "hostapd config"; + default = { }; + type = lib.types.attrs; + }; + }; + }; + options.dhcpcd = lib.mkOption { + description = "dhcpcd options"; + default = { }; + type = lib.types.submodule { + options.enable = lib.mkEnableOption "dhcpcd"; + options.extraConfig = lib.mkOption { + description = "dhcpcd text config"; + default = ""; + type = lib.types.lines; + }; + }; + }; + options.ipv4 = lib.mkOption { + description = "IPv4 config"; + default = { }; + type = lib.types.submodule { + options.addresses = lib.mkOption { + description = "Device's IPv4 addresses"; + default = [ ]; + type = lib.types.listOf (lib.types.submodule { + options.address = lib.mkOption { + description = "IPv4 address"; + type = lib.types.str; + }; + options.prefixLength = lib.mkOption { + description = "IPv4 prefix length"; + type = lib.types.int; + }; + options.gateways = lib.mkOption { + description = "IPv4 gateway addresses (optional)"; + default = [ ]; + type = with lib.types; listOf str; + }; + options.dns = lib.mkOption { + description = "IPv4 DNS servers associated with this device"; + type = with lib.types; listOf str; + default = [ ]; + }; + options.keaSettings = lib.mkOption { + default = { }; + type = (pkgs.formats.json {}).type; + example = lib.literalExpression { + pools = [ { pool = "192.168.1.15 - 192.168.1.200"; } ]; + option-data = [ { + name = "domain-name-servers"; + code = 6; + csv-format = true; + space = "dhcp4"; + data = "8.8.8.8, 8.8.4.4"; + } ]; + }; + description = "Kea IPv4 prefix-specific settings"; + }; + }); + }; + options.kea = lib.mkOption { + description = "Kea options"; + default = { }; + type = lib.types.submodule { + options.enable = lib.mkOption { + type = lib.types.bool; + description = "Enable Kea for IPv4"; + default = true; + }; + options.extraArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = "List of additional arguments to pass to the daemon."; + }; + options.configFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = "Kea config file (takes precedence over settings)"; + }; + options.settings = lib.mkOption { + default = { }; + type = (pkgs.formats.json {}).type; + description = "Kea settings"; + }; + }; + }; + }; + }; + options.ipv6 = lib.mkOption { + description = "IPv6 config"; + default = { }; + type = lib.types.submodule { + options.addresses = lib.mkOption { + description = "Device's IPv6 addresses"; + default = [ ]; + type = lib.types.listOf (lib.types.submodule { + options.address = lib.mkOption { + description = "IPv6 address"; + type = lib.types.str; + }; + options.prefixLength = lib.mkOption { + description = "IPv6 prefix length"; + type = lib.types.int; + }; + options.gateways = lib.mkOption { + description = "IPv6 gateways information (optional)"; + default = [ ]; + type = with lib.types; listOf (either str (submodule { + options.address = lib.mkOption { + description = "Gateway's IPv6 address"; + type = str; + }; + options.prefixLength = lib.mkOption { + description = "Gateway's IPv6 prefix length (defaults to interface address's prefix length)"; + type = nullOr int; + default = null; + }; + options.radvdSettings = lib.mkOption { + default = { }; + type = attrsOf (oneOf [ bool str int ]); + example = lib.literalExpression { + AdvRoutePreference = "high"; + }; + description = "radvd prefix-specific route settings"; + }; + options.coreradSettings = lib.mkOption { + default = { }; + type = (pkgs.formats.toml {}).type; + example = lib.literalExpression { + preference = "high"; + }; + description = "CoreRAD prefix-specific route settings"; + }; + })); + }; + options.dns = lib.mkOption { + description = "IPv6 DNS servers associated with this device"; + type = with lib.types; listOf (either str (submodule { + options.address = lib.mkOption { + description = "DNS server's address"; + type = lib.types.str; + }; + options.radvdSettings = lib.mkOption { + default = { }; + type = attrsOf (oneOf [ bool str int ]); + example = lib.literalExpression { FlushRDNSS = false; }; + description = "radvd prefix-specific RDNSS settings"; + }; + options.coreradSettings = lib.mkOption { + default = { }; + type = (pkgs.formats.toml {}).type; + example = lib.literalExpression { lifetime = "auto"; }; + description = "CoreRAD prefix-specific RDNSS settings"; + }; + })); + default = [ ]; + }; + options.keaSettings = lib.mkOption { + default = { }; + type = (pkgs.formats.json {}).type; + example = lib.literalExpression { + pools = [ { + pool = "192.168.1.15 - 192.168.1.200"; + } ]; + option-data = [ { + name = "dns-servers"; + code = 23; + csv-format = true; + space = "dhcp6"; + data = "aaaa::, bbbb::"; + } ]; + }; + description = "Kea prefix-specific settings"; + }; + options.radvdSettings = lib.mkOption { + default = { }; + type = with lib.types; attrsOf (oneOf [ bool str int ]); + example = lib.literalExpression { + AdvOnLink = true; + AdvAutonomous = true; + Base6to4Interface = "ppp0"; + }; + description = "radvd prefix-specific settings"; + }; + options.coreradSettings = lib.mkOption { + default = { }; + type = (pkgs.formats.toml {}).type; + example = lib.literalExpression { + on_link = true; + autonomous = true; + }; + description = "CoreRAD prefix-specific settings"; + }; + }); + }; + options.kea = lib.mkOption { + description = "Kea options"; + default = { }; + type = lib.types.submodule { + options.enable = lib.mkEnableOption "Kea for IPv6"; + options.extraArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = "List of additional arguments to pass to the daemon."; + }; + options.configFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = "Kea config file (takes precedence over settings)"; + }; + options.settings = lib.mkOption { + default = { }; + type = (pkgs.formats.json {}).type; + description = "Kea settings"; + }; + }; + }; + options.radvd = lib.mkOption { + description = "radvd options"; + default = { }; + type = lib.types.submodule { + options.enable = lib.mkOption { + type = lib.types.bool; + description = "Enable radvd"; + default = true; + }; + options.interfaceSettings = lib.mkOption { + default = { }; + type = with lib.types; attrsOf (oneOf [ bool str int ]); + example = lib.literalExpression { + UnicastOnly = true; + }; + description = "radvd interface-specific settings"; + }; + }; + }; + options.corerad = lib.mkOption { + description = "CoreRAD options"; + default = { }; + type = lib.types.submodule { + options.enable = lib.mkEnableOption "CoreRAD (don't forget to disable radvd)"; + options.configFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = "CoreRAD config file (takes precedence over settings)"; + }; + options.interfaceSettings = lib.mkOption { + default = { }; + type = (pkgs.formats.toml {}).type; + description = "CoreRAD interface-specific settings"; + }; + options.settings = lib.mkOption { + default = { }; + type = (pkgs.formats.toml {}).type; + example = lib.literalExpression { + debug.address = "localhost:9430"; + debug.prometheus = true; + }; + description = "General CoreRAD settings"; + }; + }; + }; + }; + }; + }); + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + dig.dnsutils + ethtool + tcpdump + ]; + + # performance tweaks + powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; + services.irqbalance.enable = lib.mkDefault true; + boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod; + + boot.kernel.sysctl = { + "net.netfilter.nf_log_all_netns" = true; + "net.ipv4.conf.all.forwarding" = true; + "net.ipv4.conf.default.forwarding" = true; + "net.ipv6.conf.all.forwarding" = config.networking.enableIPv6; + "net.ipv6.conf.default.forwarding" = config.networking.enableIPv6; + }; + + networking.enableIPv6 = lib.mkDefault true; + networking.usePredictableInterfaceNames = true; + networking.firewall.allowPing = lib.mkDefault true; + networking.firewall.rejectPackets = lib.mkDefault false; # drop rather than reject + services.udev.extraRules = + let + devs = lib.filterAttrs (k: v: (v.matchUdevAttrs or { }) != { }) cfg.interfaces; + in lib.mkIf (devs != { }) + (builtins.concatStringsSep "\n" (lib.mapAttrsToList (k: v: + let + attrs = lib.mapAttrsToList (k: v: "ATTR{${k}}==${builtins.toJSON (toString v)}") v.matchUdevAttrs; + in '' + SUBSYSTEM=="net", ACTION=="add", ${builtins.concatStringsSep ", " attrs}, NAME="${k}" + '') devs)); + networking.interfaces = builtins.mapAttrs (interface: icfg: { + ipv4.addresses = map ({ address, prefixLength, ... }: { inherit address prefixLength; }) icfg.ipv4.addresses; + ipv6.addresses = map ({ address, prefixLength, ... }: { inherit address prefixLength; }) icfg.ipv6.addresses; + } // lib.optionalAttrs (icfg.macAddress != null) { + inherit (icfg) macAddress; + }) cfg.interfaces; + networking.bridges = + builtins.zipAttrsWith + (k: vs: { interfaces = vs; }) + (lib.mapAttrsToList + (interface: icfg: + if icfg.bridge != null && !icfg.hostapd.enable then { + ${icfg.bridge} = interface; + } else {}) + cfg.interfaces); + networking.useDHCP = lib.mkIf (builtins.any (x: x.dhcpcd.enable) (builtins.attrValues cfg.interfaces)) false; + }; +} diff --git a/system/modules/router/dhcpcd.nix b/system/modules/router/dhcpcd.nix new file mode 100644 index 0000000..6329b8a --- /dev/null +++ b/system/modules/router/dhcpcd.nix @@ -0,0 +1,69 @@ +{ lib +, config +, pkgs +, utils +, ... }: + +let + cfg = config.router; + exitHook = pkgs.writeText "dhcpcd.exit-hook" '' + if [ "$reason" = BOUND -o "$reason" = REBOOT ]; then + # Restart ntpd. We need to restart it to make sure that it + # will actually do something: if ntpd cannot resolve the + # server hostnames in its config file, then it will never do + # anything ever again ("couldn't resolve ..., giving up on + # it"), so we silently lose time synchronisation. This also + # applies to openntpd. + /run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd.service openntpd.service chronyd.service || true + fi + ''; +in { + config = lib.mkIf (cfg.enable && builtins.any (x: x.dhcpcd.enable) (builtins.attrValues cfg.interfaces)) { + users.users.dhcpcd = { + isSystemUser = true; + group = "dhcpcd"; + }; + users.groups.dhcpcd = {}; + environment.systemPackages = [ pkgs.dhcpcd ]; + environment.etc."dhcpcd.exit-hook".source = exitHook; + powerManagement.resumeCommands = builtins.concatStringsSep "\n" (lib.mapAttrsToList (interface: icfg: '' + # Tell dhcpcd to rebind its interfaces if it's running. + /run/current-system/systemd/bin/systemctl reload "dhcpcd-${utils.escapeSystemdPath interface}.service" + '')); + systemd.services = lib.mapAttrs' (interface: icfg: let + escapedInterface = utils.escapeSystemdPath interface; + dhcpcdConf = pkgs.writeText "dhcpcd.conf" '' + hostname + option domain_name_servers, domain_name, domain_search, host_name + option classless_static_routes, ntp_servers, interface_mtu + nohook lookup-hostname + denyinterfaces ve-* vb-* lo peth* vif* tap* tun* virbr* vnet* vboxnet* sit* + allowinterfaces ${interface} + waitip + ${icfg.dhcpcd.extraConfig} + ''; + in { + name = "dhcpcd-${escapedInterface}"; + value = lib.mkIf icfg.dhcpcd.enable { + description = "DHCP Client"; + wantedBy = [ "multi-user.target" "network-online.target" ]; + wants = [ "network.target" ]; + before = [ "network-online.target" ]; + after = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + restartTriggers = [ exitHook ]; + stopIfChanged = false; + path = [ pkgs.dhcpcd pkgs.nettools config.networking.resolvconf.package ]; + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; + serviceConfig = { + Type = "forking"; + PIDFile = "/run/dhcpcd/${interface}.pid"; + RuntimeDirectory = "dhcpcd"; + ExecStart = "@${pkgs.dhcpcd}/sbin/dhcpcd dhcpcd --quiet --config ${dhcpcdConf} ${lib.escapeShellArg interface}"; + ExecReload = "${pkgs.dhcpcd}/sbin/dhcpcd --rebind"; + Restart = "always"; + }; + }; + }) cfg.interfaces; + }; +} diff --git a/system/modules/router/hostapd.nix b/system/modules/router/hostapd.nix new file mode 100644 index 0000000..9a5192c --- /dev/null +++ b/system/modules/router/hostapd.nix @@ -0,0 +1,84 @@ +{ lib +, config +, pkgs +, utils +, ... }: + +let + cfg = config.router; +in { + config = lib.mkIf (cfg.enable && builtins.any (x: x.hostapd.enable) (builtins.attrValues cfg.interfaces)) { + environment.systemPackages = with pkgs; [ hostapd wirelesstools ]; + services.udev.packages = with pkgs; [ crda ]; + hardware.wirelessRegulatoryDatabase = true; + systemd.services = lib.mapAttrs' (interface: icfg: let + escapedInterface = utils.escapeSystemdPath interface; + compileValue = k: v: + if builtins.isBool v then (if v then "1" else "0") + else if builtins.isList v then builtins.concatStringsSep " " (map (compileValue k) v) + else if k == "ssid2" then "P${builtins.toJSON (toString v)}" + else toString v; + compileSettings = x: + let + y = builtins.removeAttrs x [ "ssid" ]; + z = if y?ssid2 then y else y // { ssid2 = x.ssid; }; + in + if !x?ssid && !x?ssid2 then + throw "Must specify ssid for hostapd" + else if x.wpa_key_mgmt == defaultSettings.wpa_key_mgmt && !x?wpa_passphrase && !x?sae_password then + throw "Either change authentication methods or specify wpa_passphrase for hostapd" + else builtins.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k}=${compileValue k v}") z); + forceSettings = { + inherit interface; + }; + defaultSettings = { + driver = "nl80211"; + logger_syslog = -1; + logger_syslog_level = 2; + logger_stdout = -1; + logger_stdout_level = 2; + # not sure if enabling it when it isn't supported is gonna break anything? + ieee80211n = true; # wifi 4 + ieee80211ac = true; # wifi 5 + ieee80211ax = true; # wifi 6 + ieee80211be = true; # wifi 7 + ctrl_interface = "/run/hostapd"; + disassoc_low_ack = true; + wmm_enabled = true; + uapsd_advertisement_enabled = true; + utf8_ssid = true; + sae_require_mfp = true; + ieee80211w = 1; # optional mfp + sae_pwe = 2; + auth_algs = 1; + wpa = 2; + wpa_pairwise = [ "CCMP" ]; + wpa_key_mgmt = [ "WPA-PSK" "WPA-PSK-SHA256" "SAE" ]; + okc = true; + group_mgmt_cipher = "AES-128-CMAC"; + qos_map_set = "0,0,2,16,1,1,255,255,18,22,24,38,40,40,44,46,48,56"; # from openwrt + # ap_isolate = true; # to isolate clients + } // lib.optionalAttrs (icfg.hostapd.settings?country_code) { + ieee80211d = true; + } // lib.optionalAttrs (icfg.bridge != null) { + inherit (icfg) bridge; + }; + settings = defaultSettings // icfg.hostapd.settings // forceSettings; + configFile = pkgs.writeText "hostapd.conf" (compileSettings settings); + in { + name = "hostapd-${escapedInterface}"; + value = lib.mkIf icfg.hostapd.enable { + description = "hostapd wireless AP (${interface})"; + path = [ pkgs.hostapd ]; + after = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + requiredBy = [ "network-link-${escapedInterface}.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.hostapd}/bin/hostapd ${configFile}"; + Restart = "always"; + }; + }; + }) cfg.interfaces; + }; +} diff --git a/system/modules/router/kea.nix b/system/modules/router/kea.nix new file mode 100644 index 0000000..c11be67 --- /dev/null +++ b/system/modules/router/kea.nix @@ -0,0 +1,258 @@ +{ lib +, config +, pkgs +, utils +, ... }: + +let + cfg = config.router; + # add x to last component of an ipv4 + addToLastComp4 = x: split: + let + n0 = lib.last split; + nx = n0 + x; + n = if nx >= 255 then 254 else if nx < 2 then 2 else nx; + in + if x > 0 && n0 >= 255 then null + else if x < 0 && n0 < 2 then null + else lib.init split ++ [ n ]; + # add x to last component of an ipv6 + addToLastComp6 = x: split: + let + n0 = lib.last split; + nx = n0 + x; + n = if nx >= 65535 then 65534 else if nx <= 2 then 2 else nx; + in + if x > 0 && n0 >= 65535 then null + else if x < 0 && n0 < 2 then null + else lib.init split ++ [ n ]; + # generate an integer of `total` bits with `set` most significant bits set + genMask = total: set: + parseBin (builtins.concatStringsSep "" (builtins.genList (i: if i < set then "1" else "0") total)); + # generate subnet mask for ipv4 + genMask4 = len: + builtins.genList (i: let + len' = len - i * 8; + in + if len' <= 0 then 0 + else if len' >= 8 then 255 + else genMask 8 len') 4; + # generate subnet mask for ipv6 + genMask6 = len: + builtins.genList (i: let + len' = len - i * 16; + in + if len' <= 0 then 0 + else if len' >= 16 then 65535 + else genMask 16 len') 8; + # invert a mask + invMask4 = map (builtins.bitXor 255); + invMask6 = map (builtins.bitXor 65535); + orMask = lib.zipListsWith builtins.bitOr; + andMask = lib.zipListsWith builtins.bitAnd; + # parses hexadecimal number + parseHex = x: (builtins.fromTOML "x=0x${x}").x; + # parses binary number + parseBin = x: (builtins.fromTOML "x=0b${x}").x; + # finds the longest zero-only sequence + # returns an attrset with maxS (start of the sequence) and max (sequence length) + longestZeroSeq = + builtins.foldl' ({ cur, max, curS, maxS, i }: elem: let self = { + i = i + 1; + cur = if elem == 0 then cur + 1 else 0; + max = if max >= self.cur then max else self.cur; + curS = if self.cur > 0 && cur > 0 then curS else if self.cur > 0 then i else -1; + maxS = if max >= self.cur then maxS else self.curS; + }; in self) { cur = 0; max = 0; curS = -1; maxS = -1; i = 0; }; + # parses an IPv4 address + parseIp4 = x: map builtins.fromJSON (lib.splitString "." x); + # serializes an IPv4 address + compIp4 = x: builtins.concatStringsSep "." (map toString x); + # parses an IPv6 address + parseIp6 = x: + let + nzparts = map (x: if x == "" then [] else map parseHex (lib.splitString ":" x)) (lib.splitString "::" x); + in + if builtins.length nzparts == 1 then builtins.head nzparts + else let a = builtins.head nzparts; b = builtins.elemAt nzparts 1; in + a ++ (builtins.genList (_: 0) (8 - builtins.length a - builtins.length b)) ++ b; + # serializes an IPv6 address + compIp6 = x: + let + long = longestZeroSeq x; + joined = builtins.concatStringsSep ":" (builtins.foldl' ({ i, res }: x: { + i = i + 1; + res = res ++ (if i >= long.maxS && i < long.maxS + long.max then [ "" ] else [ (lib.toLower (lib.toHexString x)) ]); + }) { i = 0; res = [ ]; } x).res; + fix = builtins.replaceStrings [":::"] ["::"]; + in + fix (fix (fix (fix (fix joined)))); + format = pkgs.formats.json {}; + package = pkgs.kea; + commonServiceConfig = { + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + DynamicUser = true; + User = "kea"; + ConfigurationDirectory = "kea"; + RuntimeDirectory = "kea"; + StateDirectory = "kea"; + UMask = "0077"; + }; +in { + config = lib.mkIf cfg.enable (lib.mkMerge [ + (let + configs = builtins.mapAttrs (interface: icfg: + let + escapedInterface = utils.escapeSystemdPath interface; + cfg4 = icfg.ipv4.kea; + in if cfg4.configFile != null then cfg4.configFile else (format.generate "kea-dhcp4-${escapedInterface}.conf" { + Dhcp4 = { + valid-lifetime = 4000; + interfaces-config.interfaces = [ interface ]; + lease-database = { + type = "memfile"; + persist = true; + name = "/var/lib/kea/dhcp4-${escapedInterface}.leases"; + }; + subnet4 = map ({ address, prefixLength, gateways, dns, keaSettings, ... }: + let + subnetMask = genMask4 prefixLength; + parsed = parseIp4 address; + minIp = andMask subnetMask parsed; + maxIp = orMask (invMask4 subnetMask) parsed; + in { + subnet = "${address}/${toString prefixLength}"; + option-data = + lib.optional (dns != [ ]) { + name = "domain-name-servers"; + code = 6; + csv-format = true; + space = "dhcp4"; + data = builtins.concatStringsSep ", " dns; + } + ++ lib.optional (gateways != [ ]) { + name = "routers"; + code = 3; + csv-format = true; + space = "dhcp4"; + data = builtins.concatStringsSep ", " gateways; + }; + pools = let + a = addToLastComp4 16 minIp; + b = addToLastComp4 (-16) parsed; + c = addToLastComp4 16 parsed; + d = addToLastComp4 (-16) maxIp; + in + lib.optional (a != null && b != null && a <= b) { pool = "${compIp4 a}-${compIp4 b}"; } + ++ lib.optional (c != null && d != null && c <= d) { pool = "${compIp4 c}-${compIp4 d}"; }; + } // keaSettings) icfg.ipv4.addresses; + } // cfg4.settings; + })) cfg.interfaces; + in { + environment.etc = lib.mapAttrs' (interface: icfg: { + name = "kea/dhcp4-server-${utils.escapeSystemdPath interface}.conf"; + value = lib.mkIf (icfg.ipv4.kea.enable && icfg.ipv4.addresses != [ ]) { + source = configs.${interface}; + }; + }) cfg.interfaces; + systemd.services = lib.mapAttrs' (interface: icfg: let + escapedInterface = utils.escapeSystemdPath interface; + in { + name = "kea-dhcp4-server-${escapedInterface}"; + value = lib.mkIf (icfg.ipv4.kea.enable && icfg.ipv4.addresses != [ ]) { + description = "Kea DHCP4 Server (${interface})"; + documentation = [ "man:kea-dhcp4(8)" "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html" ]; + after = [ "network-online.target" "time-sync.target" "sys-subsystem-net-devices-${escapedInterface}.device" ]; + bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + wantedBy = [ "multi-user.target" ]; + environment = { KEA_PIDFILE_DIR = "/run/kea"; KEA_LOCKFILE_DIR = "/run/kea"; }; + restartTriggers = [ configs.${interface} ]; + + serviceConfig = { + ExecStart = "${package}/bin/kea-dhcp4 -c " + + lib.escapeShellArgs ([ "/etc/kea/dhcp4-server-${escapedInterface}.conf" ]); + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ]; + } // commonServiceConfig; + }; + }) cfg.interfaces; + }) + (let + configs = builtins.mapAttrs (interface: icfg: + let + escapedInterface = utils.escapeSystemdPath interface; + cfg6 = icfg.ipv6.kea; + in if cfg6.configFile != null then cfg6.configFile else (format.generate "kea-dhcp6-${escapedInterface}.conf" { + Dhcp6 = { + valid-lifetime = 4000; + preferred-lifetime = 3000; + interfaces-config.interfaces = [ interface ]; + lease-database = { + type = "memfile"; + persist = true; + name = "/var/lib/kea/dhcp6-${escapedInterface}.leases"; + }; + subnet6 = map ({ address, prefixLength, dns, keaSettings, ... }: + let + subnetMask = genMask6 prefixLength; + parsed = parseIp6 address; + minIp = andMask subnetMask parsed; + maxIp = orMask (invMask6 subnetMask) parsed; + in { + subnet = "${address}/${toString prefixLength}"; + pools = let + a = addToLastComp6 16 minIp; + b = addToLastComp6 (-16) parsed; + c = addToLastComp6 16 parsed; + d = addToLastComp6 (-16) maxIp; + option-data = + lib.optional (dns != [ ]) { + name = "dns-servers"; + code = 23; + csv-format = true; + space = "dhcp6"; + data = builtins.concatStringsSep ", " (map (x: if builtins.isString x then x else x.address) dns); + }; + in + lib.optional (a != null && b != null && a <= b) { + pool = "${compIp6 a}-${compIp6 b}"; + inherit option-data; + } + ++ lib.optional (c != null && d != null && c <= d) { + pool = "${compIp6 c}-${compIp6 d}"; + inherit option-data; + }; + } // keaSettings) icfg.ipv6.addresses; + } // cfg6.settings; + })) cfg.interfaces; + in { + environment.etc = lib.mapAttrs' (interface: icfg: { + name = "kea/dhcp6-server-${utils.escapeSystemdPath interface}.conf"; + value = lib.mkIf (icfg.ipv6.kea.enable && icfg.ipv6.addresses != [ ]) { + source = configs.${interface}; + }; + }) cfg.interfaces; + systemd.services = lib.mapAttrs' (interface: icfg: let + escapedInterface = utils.escapeSystemdPath interface; + in { + name = "kea-dhcp6-server-${escapedInterface}"; + value = lib.mkIf (icfg.ipv6.kea.enable && icfg.ipv6.addresses != [ ]) { + description = "Kea DHCP6 Server (${interface})"; + documentation = [ "man:kea-dhcp6(8)" "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html" ]; + after = [ "network-online.target" "time-sync.target" "sys-subsystem-net-devices-${escapedInterface}.device" ]; + bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + wantedBy = [ "multi-user.target" ]; + environment = { KEA_PIDFILE_DIR = "/run/kea"; KEA_LOCKFILE_DIR = "/run/kea"; }; + restartTriggers = [ configs.${interface} ]; + + serviceConfig = { + ExecStart = "${package}/bin/kea-dhcp6 -c " + + lib.escapeShellArgs ([ "/etc/kea/dhcp6-server-${escapedInterface}.conf" ]); + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_NET_RAW" ]; + } // commonServiceConfig; + }; + }) cfg.interfaces; + }) + ]); +} diff --git a/system/modules/router/multiservice.nix b/system/modules/router/multiservice.nix new file mode 100644 index 0000000..74e1eec --- /dev/null +++ b/system/modules/router/multiservice.nix @@ -0,0 +1,81 @@ +{ lib +, pkgs +, config +, ... }: + +let + baseSystem = modules: lib.nixosSystem { + inherit (pkgs) system; + modules = [ + ({ lib, ... }: { + networking = { + firewall.enable = false; + useDHCP = false; + }; + system = { + inherit (config.system) stateVersion; + }; + }) + ] ++ modules; + }; + baseServices = builtins.concatLists (map builtins.attrNames (baseSystem [ ]).options.systemd.services.definitions); + baseEtc = builtins.concatLists (map builtins.attrNames (baseSystem [ ]).options.environment.etc.definitions); + cfg = config.multiservice; +in +{ + options.multiservice = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + etc = lib.mkOption { + default = { }; + type = lib.types.submodule { + options.enable = lib.mkEnableOption { + description = "Copy etc files"; + }; + options.fixup = lib.mkOption { + default = lib.id; + type = lib.types.function; + description = lib.mdDoc "Function applied to each etc files (must return an attrset with `name` and `value`)"; + }; + }; + }; + services = lib.mkOption { + default = { }; + type = lib.types.submodule { + options.enable = lib.mkEnableOption { + description = "Copy services"; + }; + options.fixup = lib.mkOption { + default = lib.id; + type = lib.types.function; + description = "Function applied to each systemd service"; + }; + }; + }; + config = lib.mkOption { + description = "nixpkgs instance's config"; + default = { }; + type = lib.types.attrs; + }; + }; + }); + }; + config = lib.mkIf (cfg != { }) (lib.mkMerge (lib.mapAttrsToList (instName: instCfg: + let + result = baseSystem [ ({ ... }: instCfg.config) ]; + in { + systemd.services = lib.mkIf instCfg.services.enable (lib.mkMerge (map + (services: lib.mapAttrs' (name: value: { + name = name + "-" + instName; + value = instCfg.services.fixup name value; + }) (builtins.removeAttrs services baseServices)) + result.options.systemd.services.definitions)); + environment.etc = lib.mkIf instCfg.etc.enable (lib.mkMerge + (map + (etc: + lib.mapAttrs' + (name: value: instCfg.etc.fixup { inherit name value; }) + (builtins.removeAttrs etc baseEtc)) + result.options.environment.etc.definitions)); + }) cfg)); +} diff --git a/system/modules/router/radvd.nix b/system/modules/router/radvd.nix new file mode 100644 index 0000000..a7f54ba --- /dev/null +++ b/system/modules/router/radvd.nix @@ -0,0 +1,68 @@ +{ lib +, config +, pkgs +, utils +, ... }: + +let + cfg = config.router; +in { + config = lib.mkIf (cfg.enable && builtins.any (x: x.ipv6.radvd.enable) (builtins.attrValues cfg.interfaces)) { + users.users.radvd = { + isSystemUser = true; + group = "radvd"; + description = "Router Advertisement Daemon User"; + }; + users.groups.radvd = { }; + + systemd.services = lib.mapAttrs' (interface: icfg: let + escapedInterface = utils.escapeSystemdPath interface; + ifaceOpts = rec { + AdvSendAdvert = true; + AdvManagedFlag = icfg.ipv6.kea.enable && icfg.ipv6.addresses != [ ]; + AdvOtherConfigFlag = AdvManagedFlag && icfg.ipv6.radvd.interfaceSettings.AdvManagedFlag or true; + } // icfg.ipv6.radvd.interfaceSettings; + prefixOpts = { + # if dhcp6 is enabled: don't autoconfigure addresses, ask dhcp + AdvAutonomous = !ifaceOpts.AdvManagedFlag; + }; + compileOpt = x: + if x == true then "on" + else if x == false then "off" + else toString x; + compileOpts = lib.mapAttrsToList (k: v: "${k} ${compileOpt v};"); + indent = map (x: " " + x); + confFile = pkgs.writeText "radvd-${escapedInterface}.conf" ( + builtins.concatStringsSep "\n" ( + [ "interface ${interface} {" ] + ++ indent ( + compileOpts ifaceOpts + ++ builtins.concatLists (map ({ address, gateways, prefixLength, dns, radvdSettings, ... }: + [ "prefix ${address}/${toString prefixLength} {" ] + ++ indent (compileOpts (prefixOpts // radvdSettings)) + ++ [ "};" ] + ++ (builtins.concatLists (map (gateway: + [ "route ${if builtins.isString gateway then gateway else gateway.address}/${toString (if gateway.prefixLength or null != null then gateway.prefixLength else prefixLength)} {" ] + ++ indent (compileOpts (gateway.radvdSettings or { })) + ++ [ "};" ]) gateways)) + ++ (builtins.concatLists (map (dns: + [ "RDNSS ${if builtins.isString dns then dns else dns.address} {" ] + ++ indent (compileOpts (dns.radvdSettings or { })) + ++ [ "};" ]) dns))) icfg.ipv6.addresses) + ) ++ [ "};" ])); + package = pkgs.radvd; + in { + name = "radvd-${escapedInterface}"; + value = lib.mkIf icfg.ipv6.radvd.enable { + description = "IPv6 Router Advertisement Daemon (${interface})"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "sys-subsystem-net-devices-${escapedInterface}.device" ]; + bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ]; + serviceConfig = { + ExecStart = "@${package}/bin/radvd radvd -n -u radvd -C ${confFile}"; + Restart = "always"; + }; + }; + }) cfg.interfaces; + }; +}