router: more progress

This commit is contained in:
chayleaf 2023-06-20 15:11:01 +07:00
parent 818ba92987
commit a7c308a5f6
14 changed files with 1133 additions and 3 deletions

View file

@ -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"; }
];
};

View file

@ -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;

View file

@ -64,7 +64,6 @@ in {
};
zramSwap.enable = true;
swapDevices = [ ];
services.tlp.enable = false;
impermanence = {
enable = true;
path = /persist;

View file

@ -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,59 @@ 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 = cfg.network;
prefixLength = 24;
dns = [ cfg.network ];
} ];
ipv6.addresses = [ {
address = "0:0:0:5678::";
prefixLength = 64;
dns = [ "fd00::1" ];
radvdSettings = {
Base6to4Interface = "br0";
};
} ];
ipv4.kea.enable = true;
ipv6.kea.enable = false;
ipv6.radvd.enable = true;
ipv6.corerad.enable = false;
};
}

View file

@ -0,0 +1,19 @@
{ lib
, ... }:
{
options.router-settings = {
country_code = lib.mkOption {
type = lib.types.str;
};
network = lib.mkOption {
type = lib.types.str;
};
ssid = lib.mkOption {
type = lib.types.str;
};
wpa_passphrase = lib.mkOption {
type = lib.types.str;
};
};
}

View file

@ -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"; }

View file

@ -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);
}

View file

@ -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;
};
}

View file

@ -0,0 +1,387 @@
{ 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.assign = lib.mkOption {
description = "Whether to assign this address to the device. Default: no if the first hextet is zero, yes otherwise.";
type = with lib.types; nullOr bool;
default = null;
};
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.assign = lib.mkOption {
description = "Whether to assign this address to the device. Default: no if the first hextet is zero, yes otherwise";
type = with lib.types; nullOr bool;
default = null;
};
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; })
(builtins.filter
(x: x.assign == true || (x.assign == null && (lib.hasPrefix "0." x.address)))
icfg.ipv4.addresses);
ipv6.addresses = map
({ address, prefixLength, ... }: { inherit address prefixLength; })
(builtins.filter
(x: x.assign == true || (x.assign == null && (lib.hasPrefix ":" x.address || lib.hasPrefix "0:" x.address)))
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;
};
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -0,0 +1,255 @@
{ 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 {
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);
};
subnet = "${address}/${toString prefixLength}";
pools = let
a = addToLastComp6 16 minIp;
b = addToLastComp6 (-16) parsed;
c = addToLastComp6 16 parsed;
d = addToLastComp6 (-16) maxIp;
in
lib.optional (a != null && b != null && a <= b) {
pool = "${compIp6 a}-${compIp6 b}";
} ++ lib.optional (c != null && d != null && c <= d) {
pool = "${compIp6 c}-${compIp6 d}";
};
} // 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;
})
]);
}

View file

@ -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));
}

View file

@ -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;
};
}