nixos: Working l2mesh with IPsec
All checks were successful
CI / Check, build and cache Nix flake (push) Successful in 17m15s

This commit is contained in:
Jack O'Sullivan 2023-11-26 01:29:44 +00:00
parent 7404779c6d
commit 0cc35547f2
9 changed files with 188 additions and 76 deletions

View File

@ -53,6 +53,7 @@ rec {
pubDomain = "nul.ie"; pubDomain = "nul.ie";
colony = { colony = {
domain = "ams1.int.${pubDomain}"; domain = "ams1.int.${pubDomain}";
pubV4 = "94.142.240.44";
prefixes = with lib.my.net.cidr; rec { prefixes = with lib.my.net.cidr; rec {
all = { all = {
v4 = "10.100.0.0/16"; v4 = "10.100.0.0/16";
@ -90,6 +91,12 @@ rec {
vip1 = "94.142.241.224/30"; vip1 = "94.142.241.224/30";
vip2 = "94.142.242.254/31"; vip2 = "94.142.242.254/31";
as211024 = {
v4 = subnet 8 50 all.v4;
v6 = "2a0e:97c0:4df::/64";
};
home.v6 = "2a0e:97c0:4d0::/48";
}; };
fstrimConfig = { fstrimConfig = {
enable = true; enable = true;
@ -97,6 +104,7 @@ rec {
interval = "04:45"; interval = "04:45";
}; };
}; };
home = rec { home = rec {
domain = "h.${pubDomain}"; domain = "h.${pubDomain}";
vlans = { vlans = {
@ -110,6 +118,11 @@ rec {
"river" "river"
"stream" "stream"
]; ];
routersPubV4 = [
"109.255.252.123" # placeholder
"109.255.252.104"
];
prefixes = with lib.my.net.cidr; rec { prefixes = with lib.my.net.cidr; rec {
modem = { modem = {
v4 = "192.168.0.0/24"; v4 = "192.168.0.0/24";
@ -133,6 +146,7 @@ rec {
v4 = subnet 6 16 all.v4; v4 = subnet 6 16 all.v4;
v6 = subnet 4 3 all.v6; v6 = subnet 4 3 all.v6;
}; };
inherit (colony.prefixes) as211024;
}; };
vips = with lib.my.net.cidr; { vips = with lib.my.net.cidr; {
hi = { hi = {
@ -147,8 +161,13 @@ rec {
v4 = host 254 prefixes.untrusted.v4; v4 = host 254 prefixes.untrusted.v4;
v6 = host 65535 prefixes.untrusted.v6; v6 = host 65535 prefixes.untrusted.v6;
}; };
as211024 = {
v4 = host 4 prefixes.as211024.v4;
v6 = host ((1*65536*65536*65536) + 65535) prefixes.as211024.v6;
}; };
}; };
};
kelder = { kelder = {
groups = { groups = {
storage = 2000; storage = 2000;

View File

@ -29,7 +29,7 @@ in
define OWNNETSET6 = [ ${intnet6}, ${amsnet6}, ${homenet6} ]; define OWNNETSET6 = [ ${intnet6}, ${amsnet6}, ${homenet6} ];
#define TRANSSET6 = [ ::1/128 ]; #define TRANSSET6 = [ ::1/128 ];
define DUB1IP6 = 2a0e:97c0:4df:0:2::1; define DUB1IP6 = ${lib.my.c.home.vips.as211024.v6};
define PREFIXP = 110; define PREFIXP = 110;
define PREFPEER = 120; define PREFPEER = 120;

View File

@ -1,9 +1,8 @@
{ lib, ... }: { lib, ... }:
let let
inherit (builtins) elemAt;
inherit (lib.my) net; inherit (lib.my) net;
inherit (lib.my.c.colony) domain prefixes; inherit (lib.my.c.colony) pubV4 domain prefixes;
pubV4 = "94.142.240.44";
in in
{ {
nixos = { nixos = {
@ -11,9 +10,11 @@ in
l2 = { l2 = {
as211024 = { as211024 = {
vni = 211024; vni = 211024;
security.enable = true;
peers = { peers = {
estuary.addr = pubV4; estuary.addr = pubV4;
home.addr = "188.141.75.2"; # river.addr = elemAt lib.my.c.home.routersPubV4 0;
stream.addr = elemAt lib.my.c.home.routersPubV4 1;
}; };
}; };
}; };
@ -53,10 +54,10 @@ in
}; };
as211024 = { as211024 = {
ipv4 = { ipv4 = {
address = "10.255.3.1"; address = net.cidr.host 1 prefixes.as211024.v4;
gateway = null; gateway = null;
}; };
ipv6.address = "2a0e:97c0:4df:0:3::1"; ipv6.address = net.cidr.host 1 prefixes.as211024.v6;
}; };
}; };
@ -90,6 +91,7 @@ in
environment = { environment = {
systemPackages = with pkgs; [ systemPackages = with pkgs; [
ethtool ethtool
conntrack-tools
wireguard-tools wireguard-tools
]; ];
}; };
@ -114,34 +116,19 @@ in
}; };
systemd = { systemd = {
services = { services =
# Use this as a way to make sure the router always knows we're here (NDP seems kindy funky)
ipv6-neigh-keepalive =
let
waitOnline = "systemd-networkd-wait-online@wan.service";
in
{
description = "Frequent ICMP6 neighbour solicitations";
enable = false;
requires = [ waitOnline ];
after = [ waitOnline ];
script = ''
while true; do
${pkgs.ndisc6}/bin/ndisc6 ${assignments.internal.ipv6.gateway} wan
sleep 10
done
'';
wantedBy = [ "multi-user.target" ];
};
bird2 =
let let
waitOnline = "systemd-networkd-wait-online@wan.service"; waitOnline = "systemd-networkd-wait-online@wan.service";
in in
{ {
bird2 = {
after = [ waitOnline ]; after = [ waitOnline ];
# requires = [ waitOnline ]; # requires = [ waitOnline ];
}; };
ipsec = {
after = [ waitOnline ];
requires = [ waitOnline ];
};
}; };
}; };
@ -337,14 +324,13 @@ in
} }
]; ];
"90-l2mesh-as211024" = { "90-l2mesh-as211024" = mkMerge [
(networkdAssignment "as211024" assignments.as211024)
{
matchConfig.Name = "as211024"; matchConfig.Name = "as211024";
address = with assignments.as211024; [ networkConfig.IPv6AcceptRA = mkForce false;
(with ipv4; "${address}/${toString mask}") }
(with ipv6; "${address}/${toString mask}")
]; ];
networkConfig.IPv6AcceptRA = false;
};
"95-kelder" = { "95-kelder" = {
matchConfig.Name = "kelder"; matchConfig.Name = "kelder";
routes = [ routes = [
@ -366,10 +352,16 @@ in
"estuary/kelder-wg.key" = { "estuary/kelder-wg.key" = {
owner = "systemd-network"; owner = "systemd-network";
}; };
"l2mesh/as211024.key" = {};
}; };
}; };
server.enable = true; server.enable = true;
vpns = {
l2.pskFiles = {
as211024 = config.age.secrets."l2mesh/as211024.key".path;
};
};
firewall = { firewall = {
trustedInterfaces = [ "as211024" ]; trustedInterfaces = [ "as211024" ];
udp.allowed = [ 5353 lib.my.c.kelder.vpn.port ]; udp.allowed = [ 5353 lib.my.c.kelder.vpn.port ];

View File

@ -1,4 +1,4 @@
index: { lib, ... }: index: { lib, allAssignments, ... }:
let let
inherit (builtins) elemAt; inherit (builtins) elemAt;
inherit (lib.my) net; inherit (lib.my) net;
@ -54,6 +54,13 @@ in
}; };
ipv6.address = net.cidr.host (index + 1) prefixes.untrusted.v6; ipv6.address = net.cidr.host (index + 1) prefixes.untrusted.v6;
}; };
as211024 = {
ipv4 = {
address = net.cidr.host (index + 2) prefixes.as211024.v4;
gateway = null;
};
ipv6.address = net.cidr.host ((1*65536*65536*65536) + index + 1) prefixes.as211024.v6;
};
}; };
configuration = { lib, pkgs, config, assignments, allAssignments, ... }: configuration = { lib, pkgs, config, assignments, allAssignments, ... }:
@ -72,6 +79,7 @@ in
environment = { environment = {
systemPackages = with pkgs; [ systemPackages = with pkgs; [
ethtool ethtool
conntrack-tools
]; ];
}; };
@ -107,6 +115,17 @@ in
networking.domain = "h.${pubDomain}"; networking.domain = "h.${pubDomain}";
systemd.services = {
ipsec =
let
waitOnline = "systemd-networkd-wait-online@wan.service";
in
{
after = [ waitOnline ];
requires = [ waitOnline ];
};
};
systemd.network = { systemd.network = {
wait-online.enable = false; wait-online.enable = false;
config = { config = {
@ -277,6 +296,14 @@ in
networkConfig.IPv6AcceptRA = mkForce false; networkConfig.IPv6AcceptRA = mkForce false;
} }
]; ];
"90-l2mesh-as211024" = mkMerge [
(networkdAssignment "as211024" assignments.as211024)
{
matchConfig.Name = "as211024";
networkConfig.IPv6AcceptRA = mkForce false;
}
];
} }
(mkVLANConfig "hi" 9000) (mkVLANConfig "hi" 9000)
@ -288,12 +315,15 @@ in
my = { my = {
secrets = { secrets = {
files = { files = {
# "estuary/kelder-wg.key" = { "l2mesh/as211024.key" = {};
# owner = "systemd-network";
# };
}; };
}; };
vpns = {
l2.pskFiles = {
as211024 = config.age.secrets."l2mesh/as211024.key".path;
};
};
firewall = { firewall = {
trustedInterfaces = [ "lan-hi" "lan-lo" ]; trustedInterfaces = [ "lan-hi" "lan-lo" ];
udp.allowed = [ 5353 ]; udp.allowed = [ 5353 ];

View File

@ -49,6 +49,7 @@ in
query-local-address = [ query-local-address = [
# TODO: IPv6 # TODO: IPv6
"0.0.0.0" "0.0.0.0"
"::"
# TODO: Dynamic IPv4 WAN address? # TODO: Dynamic IPv4 WAN address?
# assignments.internal.ipv4.address # assignments.internal.ipv4.address
# assignments.internal.ipv6.address # assignments.internal.ipv6.address

View File

@ -4,9 +4,10 @@ let
inherit (lib.my) net; inherit (lib.my) net;
inherit (lib.my.c.home) prefixes vips; inherit (lib.my.c.home) prefixes vips;
vlanIface = vlan: if vlan == "as211024" then vlan else "lan-${vlan}";
vrrpIPs = family: map (vlan: { vrrpIPs = family: map (vlan: {
addr = "${vips.${vlan}.${family}}/${toString (net.cidr.length prefixes.${vlan}.${family})}"; addr = "${vips.${vlan}.${family}}/${toString (net.cidr.length prefixes.${vlan}.${family})}";
dev = "lan-${vlan}"; dev = vlanIface vlan;
}) (attrNames vips); }) (attrNames vips);
mkVRRP = family: routerId: { mkVRRP = family: routerId: {
state = if index == 0 then "MASTER" else "BACKUP"; state = if index == 0 then "MASTER" else "BACKUP";

View File

@ -125,6 +125,9 @@ let
l2MeshOpts = with lib.types; { name, ... }: { l2MeshOpts = with lib.types; { name, ... }: {
options = { options = {
interface = mkOpt' str name "Name of VXLAN interface."; interface = mkOpt' str name "Name of VXLAN interface.";
ipv6 = mkBoolOpt' false "Whether this mesh's underlay operates over IPv6.";
baseMTU = mkOpt' ints.unsigned 1500 "Base MTU to calculate VXLAN MTU with.";
l3Overhead = mkOpt' ints.unsigned 40 "Overhead of L3 header (to calculate MTU).";
firewall = mkBoolOpt' true "Whether to generate firewall rules."; firewall = mkBoolOpt' true "Whether to generate firewall rules.";
vni = mkOpt' ints.unsigned 1 "VXLAN VNI."; vni = mkOpt' ints.unsigned 1 "VXLAN VNI.";
peers = mkOpt' (attrsOf (submodule l2PeerOpts)) { } "Peers."; peers = mkOpt' (attrsOf (submodule l2PeerOpts)) { } "Peers.";

View File

@ -1,7 +1,8 @@
{ lib, pkgs, config, vpns, ... }: { lib, config, vpns, ... }:
let let
inherit (lib) optionalString mapAttrsToList concatStringsSep filterAttrs mkIf mkMerge; inherit (builtins) any attrValues;
inherit (lib.my) isIPv6; inherit (lib) optionalString mapAttrsToList concatStringsSep concatMapStringsSep filterAttrs mkIf mkMerge;
inherit (lib.my) isIPv6 mkOpt';
vxlanPort = 4789; vxlanPort = 4789;
@ -24,38 +25,34 @@ let
Local = ownAddr; Local = ownAddr;
MacLearning = true; MacLearning = true;
DestinationPort = vxlanPort; DestinationPort = vxlanPort;
PortRange = "${toString vxlanPort}-${toString (vxlanPort + 1)}";
Independent = true; Independent = true;
}; };
}; };
links."20-l2mesh-${name}" = {
matchConfig.Name = mesh.interface;
# TODO: ipv6? ipsec?
linkConfig.MTUBytes = "1450";
};
networks."90-l2mesh-${name}" = { networks."90-l2mesh-${name}" = {
matchConfig.Name = mesh.interface; matchConfig.Name = mesh.interface;
extraConfig = concatStringsSep "\n" (mapAttrsToList (n: peer: '' linkConfig.MTUBytes =
[BridgeFDB] let
MACAddress=00:00:00:00:00:00 espOverhead =
Destination=${peer.addr} if (!mesh.security.enable) then 0
'') otherPeers); else
# SPI + seq + IV + pad / header + ICV
4 + 4 + (if mesh.security.encrypt then 8 else 0) + 2 + 16;
# UDP + VXLAN + Ethernet + L3 (IPv4/IPv6)
overhead = espOverhead + 8 + 8 + 14 + mesh.l3Overhead;
in
toString (mesh.baseMTU - overhead);
bridgeFDBs = mapAttrsToList (n: peer: {
bridgeFDBConfig = {
MACAddress = "00:00:00:00:00:00";
Destination = peer.addr;
};
}) otherPeers;
}; };
}; };
mkLibreswanConfig = name: mesh: with info mesh; { vxlanAllow = vni: "udp dport ${toString vxlanPort} @th,96,24 ${toString vni} accept";
enable = true;
# TODO: finish this...
connections."l2mesh-${name}" = ''
keyexchange=ike
type=transport
left=${ownAddr}
auto=start
phase2=esp
ikev2=yes
'';
};
mkFirewallConfig = name: mesh: with info mesh; mkFirewallConfig = name: mesh: with info mesh;
let let
netProto = if (isIPv6 ownAddr) then "ip6" else "ip"; netProto = if (isIPv6 ownAddr) then "ip6" else "ip";
@ -63,8 +60,11 @@ let
'' ''
table inet filter { table inet filter {
chain l2mesh-${name} { chain l2mesh-${name} {
${optionalString mesh.security.enable "meta l4proto esp accept"} ${optionalString mesh.security.enable ''
udp dport ${toString vxlanPort} @th,96,24 ${toString mesh.vni} accept udp dport isakmp accept
meta l4proto esp accept
''}
${optionalString (!mesh.security.enable) (vxlanAllow mesh.vni)}
return return
} }
chain input { chain input {
@ -72,12 +72,63 @@ let
} }
} }
''; '';
mkLibreswanConfig = name: mesh: with info mesh; {
enable = true;
connections = mkMerge (mapAttrsToList
(pName: peer: {
"l2mesh-${name}-${pName}" = ''
keyexchange=ike
hostaddrfamily=ipv${if mesh.ipv6 then "6" else "4"}
type=transport
left=${ownAddr}
leftprotoport=udp/${toString vxlanPort}
right=${peer.addr}
rightprotoport=udp/${toString vxlanPort}
rightupdown=
auto=start
authby=secret
phase2=esp
esp=${if mesh.security.encrypt then "aes_gcm256" else "null-sha256"}
ikev2=yes
modecfgpull=no
'';
})
otherPeers);
};
genSecrets = name: mesh: with info mesh; concatMapStringsSep "\n" (p: ''
echo "${ownAddr} ${p.addr} : PSK \"$(< "${config.my.vpns.l2.pskFiles.${name}}")\"" >> /run/l2mesh.secrets
'') (attrValues otherPeers);
anySecurity = any (c: c.security.enable) (attrValues memberMeshes);
in in
{ {
options = {
my.vpns.l2 = with lib.types; {
pskFiles = mkOpt' (attrsOf str) { } "PSK files for secured meshes.";
};
};
config = { config = {
systemd.network = mkMerge (mapAttrsToList mkNetConfig memberMeshes); systemd.network = mkMerge (mapAttrsToList mkNetConfig memberMeshes);
# TODO: finish this...
#services.libreswan = mkMerge (mapAttrsToList mkLibreswanConfig (filterAttrs (_: c: c.security.enable) memberMeshes)); environment.etc."ipsec.d/l2mesh.secrets" = mkIf anySecurity {
source = "/run/l2mesh.secrets";
};
systemd.services.ipsec = mkIf anySecurity {
preStart = ''
oldUmask="$(umask)"
umask 006
> /run/l2mesh.secrets
${concatStringsSep "\n" (mapAttrsToList genSecrets memberMeshes)}
umask "$oldUmask"
'';
};
services.libreswan = mkMerge (mapAttrsToList mkLibreswanConfig (filterAttrs (_: c: c.security.enable) memberMeshes));
my.firewall.extraRules = concatStringsSep "\n" (mapAttrsToList mkFirewallConfig (filterAttrs (_: c: c.firewall) memberMeshes)); my.firewall.extraRules = concatStringsSep "\n" (mapAttrsToList mkFirewallConfig (filterAttrs (_: c: c.firewall) memberMeshes));
}; };
} }

View File

@ -0,0 +1,15 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IG44Q3BVdyBBY0ZU
RmFhZGFPdW1VTVJtYWRsUjJqdGlqVTd2Y3ovSFpQRGNJZ1pDODFvClNqQjlVK0hu
Uitvb2tydGo4OHZwRWZJMnhwYzNmdFVnaVdFWU9SYzBEYWMKLT4gc3NoLWVkMjU1
MTkgcytxUmZnIDBSUW16OGpkalA3MFp1RlBCRnlMTVQ3WDZBLzFMMjd5djF0MW1Y
eVFjUWMKOUcyazgyeWNLWWJ3czFZM1hzK2htNWlLdnk5SmNEekZVRDFHNEZ0QlhY
SQotPiBYMjU1MTkgeWVXN0dqZlZwTWpCTTU3UXFBUkljWUFtcG9yRlNXSVJvR21X
QVQ3YnRSMApzb29oeUFQZmxZTVhSU2VmaUN5MEFzVWRrSGNLV0hRUmpnWGJTU1FC
V0ZVCi0+IEE1am8tZ3JlYXNlIGpmJktlK2pRCi9VcnQyQWFtd1pNL2xINGVMNTF0
OTNkOVpIWThUcUxhdlVYTkw4NHZnWFgrZCt1SlhCcTdnOGMyazMwdWMzOXEKOVhr
NnQ3RzBCVzEvMUs1S0pkaGRXd3BBb1MwVEdKVXMKLS0tIEhMVnhDSFJRT01TS0Rp
Z2lubnRJd1cxN080YWJ0aTJidmcwR3BxdW5vUE0Kr9X2C1i5yj+gRZNFRek8b+2+
7Ll+u7AxYLEdGeB/74ehp9v7oUVTTwhRnhXCLjmixYx9PRBivObFAVswk7fr6y8W
VGNFLmp6zpMzkbI=
-----END AGE ENCRYPTED FILE-----