{ config, pkgs, lib, vmixLib, ... }: with vmixLib.network; let vmixCfg = config.vmix; # creates a /30 network from available range for veth-pair wan interfaces mkVethIPv4Range = index: availableIPv4Range: let vethIPv4RangeLength = 30; in (calc.cidr.subnet (vethIPv4RangeLength - (calc.cidr.length availableIPv4Range)) index availableIPv4Range); namespaceGlobalService = { "ns.net.vmix@" = { description = "network namespace %I for vmix"; before = [ "network.target" ]; path = with pkgs; [ iproute2 utillinux ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; PrivateNetwork = true; ExecStart = (pkgs.writeShellScript "ns.net.vmix-start" '' NAMESPACE="$1.vmix" ip netns add $NAMESPACE umount /var/run/netns/$NAMESPACE mount --bind /proc/self/ns/net /var/run/netns/$NAMESPACE '') + " %I"; ExecStop = "${pkgs.iproute2}/bin/ip netns del %I.vmix"; }; }; }; mkLanService = networkName: lanName: cfg: let lanCfg = cfg // { name = lanName; namespace = "${networkName}"; }; lanInterfaceName = "brx-${lanCfg.name}"; lanInterfaceIPAddress = calc.cidr.host 1 lanCfg.ipv4.range; netmask = calc.cidr.netmask lanCfg.ipv4.range; networkPrefix = builtins.elemAt (lib.splitString "/" lanCfg.ipv4.range) 1; dhcpStartAddress = if (lanCfg.ipv4.dhcp.startAddress != null) then lanCfg.ipv4.dhcp.startAddress else (calc.cidr.host 2 lanCfg.ipv4.range); dhcpEndAddress = if (lanCfg.ipv4.dhcp.endAddress != null) then lanCfg.ipv4.dhcp.endAddress else (calc.cidr.host ((calc.cidr.capacity lanCfg.ipv4.range) - 2) lanCfg.ipv4.range); createLanInterface = pkgs.writeShellScript "create-lan-${lanCfg.name}-vmix" '' ip link add ${lanInterfaceName} type bridge ip address add ${lanInterfaceIPAddress}/${networkPrefix} dev ${lanInterfaceName} ip link set ${lanInterfaceName} up ''; deleteLanInterface = pkgs.writeShellScript "delete-lan-${lanCfg.name}-vmix" "ip link del ${lanInterfaceName}"; lanDomainName = "${lanCfg.name}.vmix"; lanDnsmasqConf = pkgs.writeText "dnsmasq-${lanCfg.name}.conf" ('' listen-address=${lanInterfaceIPAddress} dhcp-range=${dhcpStartAddress},${dhcpEndAddress},${netmask},12h interface=${lanInterfaceName} bind-interfaces except-interface=lo dhcp-authoritative domain=${lanDomainName} domain-needed localise-queries no-hosts expand-hosts dhcp-leasefile=/tmp/dhcp.leases '' + lib.concatStringsSep "\n" (lib.optionals (lanCfg.ipv4.dns.upstream != []) ([ "no-resolv" ] ++ (builtins.map (dnsServer: "server=${dnsServer}") lanCfg.ipv4.dns.upstream))) ); in { "lan.net.vmix@${lanCfg.name}.${lanCfg.namespace}" = rec { bindsTo = [ "ns.net.vmix@${lanCfg.namespace}.service" ]; after = bindsTo; wantedBy = [ "net.vmix@${lanCfg.namespace}.target" ]; unitConfig.JoinsNamespaceOf = "ns.net.vmix@${lanCfg.namespace}.service"; path = with pkgs; [ iproute2 ]; serviceConfig = { ExecStartPre = createLanInterface; ExecStart = "${pkgs.dnsmasq}/bin/dnsmasq -d -C ${lanDnsmasqConf}"; ExecReload = pkgs.writeShellScript "reload-dnsmasq" "kill -HUP $MAINPID"; ExecStopPost = deleteLanInterface; Restart = "on-failure"; RestartSec = "5"; PrivateTmp = true; ProtectSystem = true; ProtectHome = true; PrivateNetwork = true; }; }; }; mkWanService = networkName: cfg: let wanCfg = cfg // { namespace = networkName; }; vethInNSToHost.iface = "vhost"; vethOnHostToNS.iface = "vn-${wanCfg.namespace}"; vethOnHostToNS.ipv4.address = calc.cidr.host 1 wanCfg.ipv4.range; vethInNSToHost.ipv4.address = calc.cidr.host 2 wanCfg.ipv4.range; networkPrefix = builtins.elemAt (lib.splitString "/" wanCfg.ipv4.range) 1; iptablesMark = builtins.toString (ipv4ToInt vethOnHostToNS.ipv4.address); createWanCommands = '' ip link add ${vethOnHostToNS.iface} type veth peer name ${vethInNSToHost.iface} ip link set ${vethInNSToHost.iface} netns ${wanCfg.namespace}.vmix ip address add ${vethOnHostToNS.ipv4.address}/${networkPrefix} dev ${vethOnHostToNS.iface} ip netns exec ${wanCfg.namespace}.vmix ip address add ${vethInNSToHost.ipv4.address}/${networkPrefix} dev ${vethInNSToHost.iface} iptables -A FORWARD -i ${vethOnHostToNS.iface} -j ACCEPT iptables -A FORWARD -o ${vethOnHostToNS.iface} -j ACCEPT #iptables -A INPUT -i ${vethOnHostToNS.iface} -j DROP iptables -t mangle -A PREROUTING -i ${vethOnHostToNS.iface} -j MARK --set-mark ${iptablesMark} iptables -t nat -A POSTROUTING -m mark --mark ${iptablesMark} -j MASQUERADE ip link set ${vethOnHostToNS.iface} up ip netns exec ${wanCfg.namespace}.vmix ip link set ${vethInNSToHost.iface} up ip netns exec ${wanCfg.namespace}.vmix ip r add default via ${vethOnHostToNS.ipv4.address} ''; createWan = pkgs.writeShellScript "create-wan-${wanCfg.namespace}-vmix" createWanCommands; deleteWan = let createdIptablesRules = lib.filter (line: (lib.hasPrefix "iptables" line)) (lib.splitString "\n" createWanCommands); delIptablesRules = builtins.map (rule: lib.replaceStrings [ "-A" ] [ "-D"] rule) createdIptablesRules; in pkgs.writeShellScript "delete-wan-${wanCfg.namespace}-vmix" ('' ip link del ${vethOnHostToNS.iface} '' + (lib.concatStringsSep "\n" delIptablesRules)); in { "wan.net.vmix@${wanCfg.namespace}" = rec { bindsTo = [ "ns.net.vmix@${wanCfg.namespace}.service" ]; after = bindsTo; wantedBy = [ "net.vmix@${wanCfg.namespace}.target" ]; path = with pkgs; [ iproute2 iptables ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = createWan; ExecStop = deleteWan; }; }; }; mkMacvlanService = networkName: macvlanName: cfg: {}; mkNetworkServices = networkName: cfg: let netCfg = cfg // { name = networkName; }; in (lib.concatMapAttrs (mkLanService netCfg.name) netCfg.lans) // (mkWanService netCfg.name (netCfg.wan // { ipv4.range = (mkVethIPv4Range netCfg.index vmixCfg.global.net.wan.ipv4.range); })) // (lib.concatMapAttrs (mkMacvlanService netCfg.name) netCfg.bridges.macvlans); networkNames = builtins.attrNames vmixCfg.networks; networkServices = pkgs.unstable.lib.mergeAttrsList (lib.imap0 (index: networkName: (mkNetworkServices networkName (vmixCfg.networks.${networkName} // { inherit index;}))) networkNames); in { config.systemd.services = namespaceGlobalService // networkServices; }