-
-
Save andir/74869877b30e26c3d851ae27a4d8d2bf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ pkgs, lib, config, ... }: | |
with lib; | |
let | |
cfg = config.router; | |
addressesOptions = types.submodule { | |
options = { | |
address = mkOption { | |
type = types.str; | |
}; | |
prefixLength = mkOption { | |
type = types.int; | |
}; | |
}; | |
}; | |
downstreamInterfaceOptions = types.submodule { | |
options = { | |
interface = mkOption { | |
type = types.str; | |
}; | |
subnetId = mkOption { | |
# type = types.nullOr types.int; | |
default = null; | |
}; | |
v6Addresses = mkOption { | |
type = types.listOf addressesOptions; | |
}; | |
v4Addresses = mkOption { | |
type = types.listOf addressesOptions; | |
}; | |
}; | |
}; | |
in { | |
options.router = { | |
enable = mkEnableOption "enable the router module"; | |
upstreamInterfaces = mkOption { | |
description = "upstream interfaces"; | |
type = types.listOf types.str; | |
}; | |
downstreamInterfaces = mkOption { | |
description = "client interfaces"; | |
type = types.listOf downstreamInterfaceOptions; | |
}; | |
}; | |
config = mkIf cfg.enable { | |
# ensure we have a few sane network debugging tools available | |
environment.systemPackages = with pkgs; [ tcpdump dnstracer ]; | |
programs.mtr.enable = true; | |
# Can not use this as it pulls in a bunch of garbage rules | |
networking.useNetworkd = false; | |
networking.dhcpcd.enable = mkForce false; | |
boot.kernel.sysctl = { | |
"net.ipv4.conf.all.forwarding" = 1; | |
"net.ipv6.conf.all.forwarding" = 1; | |
}; | |
systemd.services."systemd-networkd".environment.SYSTEMD_LOG_LEVEL = "debug"; | |
systemd.network = let | |
mkUpstreamIfConfig = name: nameValuePair "00-${name}" { | |
enable = true; | |
networkConfig = { | |
Description = "Upstream network config for ${name}"; | |
IPv6AcceptRA = true; | |
#IPMasquerade = true; # FIXME: hows does that work on the inside? | |
#IPForward = "yes"; | |
DHCP = "yes"; | |
#IPv6PrefixDelegation = "dhcpv6"; | |
}; | |
linkConfig = { | |
RequiredForOnline = "routable"; | |
}; | |
matchConfig = { | |
Name = name; | |
}; | |
dhcpV4Config = { | |
UseDNS = false; | |
UseRoutes = true; | |
}; | |
dhcpV6Config = { | |
PrefixDelegationHint= "::/1"; | |
}; | |
ipv6PrefixDelegationConfig = { | |
Managed = true; | |
OtherInformation = true; | |
}; | |
}; | |
mkClientIfConfig = conf: let v = nameValuePair "00-${conf.interface}" { | |
enable = true; | |
matchConfig = { | |
Name = conf.interface; | |
}; | |
addresses = (map (addr: | |
{ addressConfig.Address = "${addr.address}/${toString addr.prefixLength}"; } | |
) (conf.v4Addresses ++ conf.v6Addresses)) | |
++ [ { addressConfig.Address = "::/64"; } ]; | |
networkConfig = { | |
DHCPServer = true; | |
IPv6PrefixDelegation = "dhcpv6"; | |
}; | |
dhcpServerConfig = { | |
PoolOffset = 10; | |
EmitDNS = true; | |
EmitNTP = true; | |
EmitRouter = true; | |
EmitTimezone = true; | |
}; | |
ipv6PrefixDelegationConfig = { | |
RouterLifetimeSec = 300; | |
EmitDNS = true; | |
}; | |
extraConfig = if (conf.subnetId != null) then '' | |
[Network] | |
IPv6PDSubnetId=${toString conf.subnetId} | |
'' else ""; | |
ipv6Prefixes = [ | |
{ | |
ipv6PrefixConfig = { | |
AddressAutoconfiguration = true; | |
PreferredLifetimeSec = 1800; | |
ValidLifetimeSec = 1800; | |
}; | |
} | |
]; | |
}; in builtins.trace "${builtins.toJSON v.value}" v; | |
upstreamConfig = builtins.listToAttrs (map mkUpstreamIfConfig cfg.upstreamInterfaces); | |
downstreamConfig = builtins.listToAttrs (map mkClientIfConfig cfg.downstreamInterfaces); | |
in { | |
enable = true; | |
networks = mkMerge [ | |
upstreamConfig | |
downstreamConfig | |
{ | |
"99-main" = { | |
networkConfig = { | |
IPv6AcceptRA = lib.mkForce false; | |
DHCP = lib.mkForce "no"; | |
}; | |
linkConfig = { | |
Unmanaged = lib.mkForce "yes"; | |
}; | |
}; | |
} | |
]; | |
}; | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let | |
makeTest = import (<nixpkgs> + "/nixos/tests/make-test-python.nix"); | |
numVlans = 100; | |
in makeTest ({pkgs, lib, ...}: let | |
getIPv4 = nodes: node: iface: | |
(builtins.head nodes.${node}.config.networking.interfaces.${iface}.ipv4.addresses).address; | |
getIPv6 = nodes: node: iface: | |
(builtins.head nodes.${node}.config.networking.interfaces.${iface}.ipv6.addresses).address; | |
in { | |
nodes ={ | |
dns = { nodes, ...}: { | |
virtualisation.vlans = [ 4 ]; | |
networking.dhcpcd.enable = false; | |
networking.interfaces.eth0.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ | |
{ address = "100.65.0.2"; prefixLength = 24; } | |
]; | |
networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ | |
{ address = "2001:DB9::2"; prefixLength = 126; } | |
]; | |
networking.interfaces.eth1.ipv4.routes = [ | |
{ address = "100.64.100.0"; prefixLength = 24; via = "100.65.0.1"; } | |
]; | |
networking.interfaces.eth1.ipv6.routes = [ | |
{ address = "2001:DB8::"; prefixLength = 32; via = "2001:DB9::1"; } | |
]; | |
}; | |
# The upstream router providing DHPCv4, DHCPv6 and radvd | |
# It also serves as upstream router | |
isp = {nodes, pkgs, ...}:{ | |
virtualisation.vlans = [ 1 4 ]; | |
networking.dhcpcd.enable = false; | |
networking.interfaces.eth0.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ | |
{ address = "100.64.100.1"; prefixLength = 24; } | |
]; | |
networking.interfaces.eth1.ipv6.addresses = lib.mkForce [ | |
{ address = "2001:DB8::"; prefixLength = 64; } | |
]; | |
networking.interfaces.eth2.ipv4.addresses = lib.mkForce [ | |
{ address = "100.65.0.1"; prefixLength = 24; } | |
]; | |
networking.interfaces.eth2.ipv6.addresses = lib.mkForce [ | |
{ address = "2001:DB9::1"; prefixLength = 126; } | |
]; | |
networking.firewall.enable = false; | |
boot.kernel.sysctl."net.ipv4.conf.all.forwarding" = 1; | |
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1; | |
environment.systemPackages = [ pkgs.tcpdump ]; | |
# don't do this in production, this shouldn't just be executable by anyone | |
security.wrappers.add-dhcpd-lease = { | |
source = pkgs.writeScript "add-dhcpd-lease" '' | |
#!${pkgs.stdenv.shell} | |
exec ${pkgs.iproute}/bin/ip -6 route replace "$1" via "$2" | |
''; | |
capabilities = "cap_net_admin+ep"; | |
}; | |
services = { | |
dhcpd4 = { | |
enable = true; | |
interfaces = [ "eth1" ]; | |
extraConfig = '' | |
option subnet-mask 255.255.255.255; | |
option routers ${getIPv4 nodes "isp" "eth1"}; | |
option domain-name-servers ${getIPv4 nodes "dns" "eth1"}; | |
option domain-name "customers.acme.org"; | |
subnet 100.64.100.0 netmask 255.255.255.0 { | |
range 100.64.100.10 100.64.100.100; | |
} | |
''; | |
}; | |
dhcpd6 = { | |
enable = true; | |
interfaces = [ "eth1" ]; | |
extraConfig = '' | |
#default-lease-time 1800; | |
#max-lease-time 1800; | |
#min-lease-time 1800 | |
#subnet6 2001:DB8::/2 { | |
# range6 2001:DB8:0000:0000:ffff:: 2001:DB8:0000:0000:ffff::ffff; | |
# #prefix6 2001:DB8:f000:: 2001:DB8:ffff:: /48; | |
# prefix6 0:: 1000:: /1; | |
#} | |
subnet6 ::/1 { | |
range6 ffff:: ffff:ffff:0000:0000:ffff::ffff; | |
#prefix6 2001:DB8:f000:: 2001:DB8:ffff:: /48; | |
prefix6 0:: 1000:: /1; | |
} | |
on commit { | |
set IP = pick-first-value(binary-to-ascii(16, 16, ":", substring(option dhcp6.ia-na, 16, 16)), "n/a"); | |
set Prefix = pick-first-value(binary-to-ascii(16, 16, ":", suffix(option dhcp6.ia-pd, 16)), "n/a"); | |
set PrefixLength = pick-first-value(binary-to-ascii(10, 8, ":", substring(suffix(option dhcp6.ia-pd, 17), 0, 1)), "n/a"); | |
log(concat(IP, " ", Prefix, " ", PrefixLength)); | |
execute("/run/wrappers/bin/add-dhcpd-lease", concat(Prefix,"/",PrefixLength), IP); | |
} | |
''; | |
}; | |
radvd = { | |
enable = true; | |
config = '' | |
interface eth1 { | |
AdvSendAdvert on; | |
AdvManagedFlag on; | |
AdvOtherConfigFlag on; | |
prefix ::/64 { | |
AdvOnLink on; | |
AdvAutonomous on; | |
}; | |
}; | |
''; | |
}; | |
}; | |
}; | |
router = { pkgs, lib, ... }: { | |
virtualisation.vlans = [ 1 2 3 7 10 ]; | |
imports = [ ./default.nix ]; | |
networking.interfaces.eth0.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth2.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth3.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth4.ipv4.addresses = lib.mkForce []; | |
networking.firewall.enable = false; | |
environment.systemPackages = [ pkgs.iptables ]; | |
systemd.package = pkgs.systemd.overrideAttrs ({ patches ? [], postPatch ? "", ... }: { | |
patches = patches ++ [ ./systemd-ipv6-pd-prefix.patch ./safe_atoux64.patch ]; | |
postPatch = postPatch + '' | |
sed -e 's/128\*1024\*1024/1024*1024*1024/g' -i src/network/networkd-manager.c | |
grep RCVBUF_SIZE src/network/networkd-manager.c | |
''; | |
}); | |
systemd.network.netdevs = lib.listToAttrs (lib.genList (n: lib.nameValuePair "10-vlan${toString (10 +n)}" { | |
netdevConfig = { | |
Name = "vlan${toString (10 +n)}"; | |
Kind = "vlan"; | |
}; | |
vlanConfig = { | |
Id = n; | |
}; | |
}) numVlans); | |
systemd.network.networks."00-vlans" = { | |
matchConfig.Name = "eth5"; | |
vlan = lib.genList (n: "vlan${toString (n+10)}") numVlans; | |
}; | |
router = { | |
enable = true; | |
upstreamInterfaces = [ "eth1" ]; | |
downstreamInterfaces = [ | |
{ | |
interface = "eth2"; | |
# subnetId = "0x12ff"; | |
v4Addresses = [ { address = "192.168.42.1"; prefixLength = 24; } ]; | |
v6Addresses = [ { address = "fd42::1"; prefixLength = 64; } ]; | |
} | |
{ | |
interface = "eth4"; | |
subnetId = "8000000000000000"; | |
v4Addresses = [ { address = "192.168.43.1"; prefixLength = 24; } ]; | |
v6Addresses = [ { address = "fd43::1"; prefixLength = 64; } ]; | |
} | |
] ++ (lib.genList (n: { | |
interface = "vlan${toString (10 + n)}"; | |
subnetId = "0x${toString n}"; | |
v4Addresses = [ ]; | |
v6Addresses = [ ]; | |
}) numVlans); | |
}; | |
}; | |
client = {pkgs, ...}: { | |
virtualisation.vlans = [ 2 ]; | |
networking.interfaces.eth0.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth0.ipv6.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv6.addresses = lib.mkForce []; | |
networking.interfaces.eth0.useDHCP = false; | |
boot.kernel.sysctl."net.ipv6.conf.eth0.accept_ra" = 0; | |
programs.mtr.enable = true; | |
# networking.useDHCP = true; | |
# networking.dhcpcd = { | |
# enable = true; | |
# denyInterfaces = [ "eth0" ]; | |
# allowInterfaces = [ "eth1" ]; | |
# }; | |
}; | |
client2 = {pkgs, ...}: { | |
virtualisation.vlans = [ 7 ]; | |
networking.interfaces.eth0.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv4.addresses = lib.mkForce []; | |
networking.interfaces.eth0.ipv6.addresses = lib.mkForce []; | |
networking.interfaces.eth1.ipv6.addresses = lib.mkForce []; | |
networking.interfaces.eth0.useDHCP = false; | |
boot.kernel.sysctl."net.ipv6.conf.eth0.accept_ra" = 0; | |
programs.mtr.enable = true; | |
# networking.useDHCP = true; | |
# networking.dhcpcd = { | |
# enable = true; | |
# denyInterfaces = [ "eth0" ]; | |
# allowInterfaces = [ "eth1" ]; | |
# }; | |
}; | |
}; | |
testScript = { nodes }: '' | |
router.start() | |
isp.start() | |
isp.wait_for_unit("multi-user.target") | |
router.wait_for_unit("multi-user.target") | |
isp.execute("sleep 90") | |
print(router.succeed("networkctl status")) | |
print(router.succeed("ip -6 r")) | |
# import json | |
# def assert_forwarding(machine, iface): | |
# # I FUCKING HATE BLACK... This code is less readable after the forced formatting... 🤬 | |
# forwarding = machine.succeed( | |
# f"cat /proc/sys/net/ipv4/conf/{iface}/forwarding" | |
# ).strip() | |
# assert forwarding == "1" | |
# forwarding = machine.succeed( | |
# f"cat /proc/sys/net/ipv6/conf/{iface}/forwarding" | |
# ).strip() | |
# assert forwarding == "1" | |
# isp.start() | |
# dns.start() | |
# isp.wait_for_unit("multi-user.target") | |
# dns.wait_for_unit("multi-user.target") | |
# isp.succeed("sleep 15") | |
## subtest "The upstream router and DNS server should be able to reach each other", sub { | |
# isp.succeed("ping -c1 ${getIPv4 nodes "dns" "eth1" }") | |
# dns.succeed("ping -c1 ${getIPv4 nodes "isp" "eth2" }") | |
# isp.log(isp.succeed("ping -c1 2001:DB9::2 2>&1")) | |
## } | |
## subtest "The upstream router should be good and well. No failed DHCPd", sub { | |
# isp.wait_for_unit("dhcpd4") | |
# isp.wait_for_unit("dhcpd6") | |
# isp.wait_for_unit("radvd") | |
# assert_forwarding(isp, "eth1") | |
# assert_forwarding(isp, "eth2") | |
## } | |
# router.start() | |
# router.wait_for_unit("multi-user.target") | |
## FIXME: we sleep since networkd doesn't consider IPv6 required for the network-online.target… | |
# router.succeed("sleep 15") | |
## subtest "The router should receive v4 leases from upstream", sub { | |
# router.wait_for_unit("network-online.target") | |
# router.succeed("sleep 2") | |
# router.succeed("ip a show dev eth1") | |
# router_ip_address_config = json.loads(router.succeed("ip --json a show dev eth1 2>&1")) | |
# router.log(str(router_ip_address_config)) | |
# router.log(router.succeed("networkctl status -a 2>&1")) | |
# router.log(router.succeed("ip -4 r 2>&1")) | |
# router.log(router.succeed("ip -6 r 2>&1")) | |
# router.log("trying to ping isp") | |
# router.succeed("ping -c1 ${getIPv4 nodes "isp" "eth2" } 2>&1") | |
# router.log("trying to ping dns") | |
# router.succeed("ping -c1 ${getIPv4 nodes "dns" "eth1" } 2>&1") | |
# router.log(router.succeed("ss -lpn | grep systemd-network 3>&1")) | |
# assert_forwarding(router, "eth1") | |
# assert_forwarding(router, "eth2") | |
## enable NAT for our client interface, networkd is kinda stupid in this | |
## regard as it only supported source based NATs | |
# router.succeed("iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE") | |
# client.start() | |
# client.wait_for_unit("network-online.target") | |
## FIXME: we sleep since networkd doesn't consider IPv6 required for the network-online.target… | |
# router.succeed("sleep 15") | |
# client.log(client.succeed("ip a 2>&1")) | |
# client.log(client.succeed("ip -4 r 2>&1")) | |
# client.log(client.succeed("ip -6 r 2>&1")) | |
# client.log(client.succeed("ping -c1 ${getIPv4 nodes "isp" "eth2" } 2>&1")) | |
# client.log(client.succeed("ping -c1 ${getIPv6 nodes "isp" "eth2" } 2>&1")) | |
# client.log(client.succeed("ping -c1 ${getIPv4 nodes "dns" "eth1" } 2>&1")) | |
# client.log(client.succeed("ping -c1 ${getIPv6 nodes "dns" "eth1" } 2>&1")) | |
## $client->log($client->succeed("mtr -6 --report 2001:DB8:: 2>&1")) | |
# client.log(client.succeed("ping -c1 2001:DB9::2 2>&1")) | |
# client.log(client.succeed("mtr -6 --report 2001:DB9::2 2>&1")) | |
''; | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment