{ 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; PrivateMounts = false; 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"; }; }; }; mkLanDomainName = networkName: lanName: lanCfg: if (lanCfg.domain != null) then lanCfg.domain else "${lanName}.${networkName}.vmix"; mkLan = networkName: staticRoutes: 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 = '' ip link add ${lanInterfaceName} type bridge ip address add ${lanInterfaceIPAddress}/${networkPrefix} dev ${lanInterfaceName} ip link set ${lanInterfaceName} up ''; deleteLanInterface = '' ip link del ${lanInterfaceName} ''; lanDomainName = mkLanDomainName networkName lanName lanCfg; lanDnsmasqConf = '' # lan ${lanName} dhcp-range=${lanInterfaceName},${dhcpStartAddress},${dhcpEndAddress},${netmask},12h domain=${lanDomainName},${lanInterfaceName} dhcp-option=${lanInterfaceName},option:classless-static-route,${lib.concatStringsSep "," (builtins.map (route: "${route},${lanInterfaceIPAddress}") ([ "0.0.0.0/0" ] ++ (builtins.filter (route: route != lanCfg.ipv4.range) staticRoutes)))} '' + (lib.optionalString (lanCfg.ipv4.dhcp.dns.nameservers != []) ("dhcp-option=${lanInterfaceName},option:dns-server,${(lib.concatStringsSep "," lanCfg.ipv4.dhcp.dns.nameservers)}\n")); in lanCfg // { createIface = createLanInterface; deleteIface = deleteLanInterface; dnsmasqConf = lanDnsmasqConf; domain = lanDomainName; }; mkLansService = networkName: wanCfg: lansCfg: let dhcpLeaseFile="/tmp/vmix/lans.${networkName}.dhcp.leases"; staticRoutes = [ wanCfg.ipv4.range ] ++ (builtins.map (lanCfg: lanCfg.ipv4.range) (lib.attrValues lansCfg)); lansList = lib.attrValues(lib.mapAttrs (mkLan networkName staticRoutes) lansCfg); dnsmasqConf = pkgs.writeText "dnsmasq-${networkName}.conf" ('' dhcp-host=*:*:*:*:*:*,id:* except-interface=lo dhcp-authoritative localise-queries no-hosts expand-hosts dhcp-leasefile=${dhcpLeaseFile} filter-AAAA address=/host/${calc.cidr.host 1 wanCfg.ipv4.range} no-resolv ${lib.concatStringsSep "\n" (builtins.map (nameserver: "server=${nameserver}") wanCfg.dns.nameservers)} '' + (lib.concatMapStrings (lan: lan.dnsmasqConf) lansList) ); createLansInterfaces = pkgs.writeShellScript "create-lans-${networkName}-vmix" ('' # for dnsmasq temp files mkdir -p /tmp/vmix rm -f ${dhcpLeaseFile} '' + (lib.concatMapStrings (lan: lan.createIface) lansList) ); deleteLansInterfaces = pkgs.writeShellScript "delete-lans-${networkName}-vmix" (lib.concatMapStrings (lan: lan.deleteIface) lansList); in { "lans.net.vmix@${networkName}" = rec { bindsTo = [ "ns.net.vmix@${networkName}.service" ]; after = bindsTo; wantedBy = [ "net.vmix@${networkName}.target" ]; unitConfig.JoinsNamespaceOf = "ns.net.vmix@${networkName}.service"; path = with pkgs; [ iproute2 ]; serviceConfig = { ExecStartPre = createLansInterfaces; ExecStart = "${pkgs.dnsmasq}/bin/dnsmasq -d -C ${dnsmasqConf}"; ExecReload = pkgs.writeShellScript "reload-dnsmasq" "kill -HUP $MAINPID"; ExecStopPost = deleteLansInterfaces; 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} ${lib.concatMapStrings (lanRange: "ip r add ${lanRange} via ${vethInNSToHost.ipv4.address} \n") wanCfg.lanRanges} ''; 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; }; vethIPv4RangeForWan = mkVethIPv4Range netCfg.index vmixCfg.global.net.wan.ipv4.range; wanCfg = netCfg.wan // { ipv4.range = vethIPv4RangeForWan; lanRanges = builtins.map (lan: lan.ipv4.range) (lib.attrValues netCfg.lans); }; in (mkLansService netCfg.name wanCfg netCfg.lans) // (mkWanService netCfg.name wanCfg) // (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); networkTargets = lib.concatMapAttrs (networkName: netCfg: { "net.vmix@${networkName}" = { description = "Network ${networkName} for vmix"; bindsTo = [ "ns.net.vmix@${networkName}.service" "lans.net.vmix@${networkName}.service" "wan.net.vmix@${networkName}.service" ]; }; }) vmixCfg.networks; in { config.systemd.services = namespaceGlobalService // networkServices; config.systemd.targets = networkTargets; config.boot.kernel.sysctl."net.ipv4.ip_forward" = 1; }