{ lib, config, vpns, ... }: let inherit (builtins) any attrValues; inherit (lib) optionalString mapAttrsToList concatStringsSep concatMapStringsSep filterAttrs mkIf mkMerge; inherit (lib.my) isIPv6 mkOpt'; vxlanPort = 4789; selfName = config.system.name; memberMeshes = filterAttrs (_: c: c.peers ? "${selfName}") vpns.l2; info = mesh: { ownAddr = mesh.peers."${selfName}".addr; otherPeers = filterAttrs (n: _: n != selfName) mesh.peers; }; mkNetConfig = name: mesh: with info mesh; { netdevs."30-l2mesh-${name}" = { netdevConfig = { Name = mesh.interface; Kind = "vxlan"; }; vxlanConfig = { VNI = mesh.vni; Local = ownAddr; MacLearning = true; DestinationPort = vxlanPort; PortRange = "${toString vxlanPort}-${toString (vxlanPort + 1)}"; Independent = true; }; }; networks."90-l2mesh-${name}" = { matchConfig.Name = mesh.interface; linkConfig.MTUBytes = let espOverhead = if (!mesh.security.enable) then 0 else # UDP encap + SPI + seq + IV + pad / header + ICV (if mesh.udpEncapsulation then 8 else 0) + 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: { MACAddress = "00:00:00:00:00:00"; Destination = peer.addr; }) otherPeers; }; }; vxlanAllow = vni: "udp dport ${toString vxlanPort} @th,96,24 ${toString vni} accept"; mkFirewallConfig = name: mesh: with info mesh; let netProto = if (isIPv6 ownAddr) then "ip6" else "ip"; in '' table inet filter { chain l2mesh-${name} { ${optionalString mesh.security.enable '' udp dport isakmp accept ${if mesh.udpEncapsulation then '' udp dport ipsec-nat-t accept '' else '' meta l4proto esp accept ''} ''} ${optionalString (!mesh.security.enable) (vxlanAllow mesh.vni)} return } chain input { ${netProto} daddr ${ownAddr} ${netProto} saddr { ${concatStringsSep ", " (mapAttrsToList (_: p: p.addr) otherPeers)} } jump l2mesh-${name} } } ''; 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 encapsulation=${if mesh.udpEncapsulation then "yes" else "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 { options = { my.vpns.l2 = with lib.types; { pskFiles = mkOpt' (attrsOf str) { } "PSK files for secured meshes."; }; }; config = { systemd.network = mkMerge (mapAttrsToList mkNetConfig 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)); }; }