diff --git a/lib/default.nix b/lib/default.nix index 9ef39ea..a0082ef 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,8 +1,10 @@ { pkgs, lib, system ? "x86_64-linux", ... }: let images = import ./images { inherit pkgs lib system; }; + network = import ./network.nix { inherit pkgs lib; }; in { inherit images; inherit (images.commons) customizeImage customizeImageFold; + inherit network; } \ No newline at end of file diff --git a/lib/network.nix b/lib/network.nix new file mode 100644 index 0000000..04c1dfe --- /dev/null +++ b/lib/network.nix @@ -0,0 +1,13 @@ +{ pkgs, lib, ... }: rec { + calc = (import ((builtins.fetchTarball "https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba/archive/3885f7cd9ed0a746a9d675da6f265d41e9fd6704.tar.gz") + "/net.nix" ) { inherit lib; }).lib.net; + regex.ipv4 = + let + compRegex = "(25[0-5]|(2[0-4]|10|1?[1-9])?[0-9])"; + in + "(${compRegex}\\.){3}${compRegex}"; + + regex.cidr4 = "${regex.ipv4}/(3[0-2]|[1-2]?[0-9])"; + regex.ipOrCidr4 = "(${regex.ipv4}|${regex.cidr4})"; + ipv4ToInt = ipv4: calc.ip.diff ipv4 "0.0.0.0"; + intToipv4 = inte: calc.ip.add "0.0.0.0" inte; +} \ No newline at end of file diff --git a/nixos/default.nix b/nixos/default.nix index 6719969..d637bef 100644 --- a/nixos/default.nix +++ b/nixos/default.nix @@ -3,27 +3,8 @@ with lib; let vmixLib = import ./../lib {inherit pkgs lib; }; vmixCfg = config.vmix; - vmixNetwork = import ./modules/network.nix { inherit config pkgs lib ;}; - vmixNetworkFunctions = import ./functions/network.nix { inherit pkgs lib ;}; - #vmixVM = import ./modules/network.nix { inherit config pkgs lib ;}; + args = { inherit config pkgs lib vmixLib; }; in { - options.vmix = { - networks = lib.mkOption { - type = types.attrsOf - (types.submodule vmixNetwork); - default = { }; - }; - }; - - config = - with vmixNetworkFunctions; - #with vmixVMFunctions; - let - networkServices = lib.concatMapAttrs mkNetworkService vmixCfg.networks; - #vmServices = lib.concatMapAttrs mkVMService vmixCfg.vms; - in - { - systemd.services = namespaceGlobalService // networkServices; - }; + imports = [ (import ./network args) ]; } \ No newline at end of file diff --git a/nixos/functions/network.nix b/nixos/functions/network.nix deleted file mode 100644 index 2c151cd..0000000 --- a/nixos/functions/network.nix +++ /dev/null @@ -1,121 +0,0 @@ -{ pkgs, lib, ... }: -let - ipcalcFn = input: command: - let - runCmd = pkgs.runCommand "ipcalc-${command}" {} "export `${pkgs.ipcalc}/bin/ipcalc ${input} --${command}`; echo \$${lib.toUpper command} > $out;"; - in - lib.removeSuffix "\n" (builtins.readFile runCmd); -in -{ - namespaceGlobalService = { - "ns.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.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"; - }; - }; - }; - - mkNetworkService = name: cfg: - let - netCfg = cfg // { inherit name; }; - lanInterfaceName = "brx-${netCfg.name}"; - lanInterfaceIPAddress = ipcalcFn netCfg.ipv4Range "minaddr"; - hostIPAddressOnLan = ipcalcFn netCfg.ipv4Range "maxaddr"; - networkAddress = ipcalcFn netCfg.ipv4Range "network"; - netmask = ipcalcFn netCfg.ipv4Range "netmask"; - networkPrefix = builtins.elemAt (lib.splitString "/" netCfg.ipv4Range) 1; - namespace = if netCfg.namespace != null then "${netCfg.namespace}.vmix" else ""; - - createLanInterface = pkgs.writeShellScript "create-lan-${netCfg.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-${netCfg.name}-vmix" "ip link del ${lanInterfaceName}"; - - lanDomainName = "${netCfg.name}.vmix"; - lanDnsmasqConf = pkgs.writeText "dnsmasq-${netCfg.name}.conf" '' - listen-address=${lanInterfaceIPAddress} - dhcp-range=${netCfg.dhcp.startAddress},${netCfg.dhcp.endAddress},${netmask},12h - dhcp-option=3,${hostIPAddressOnLan} - 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 (netCfg.dns.upstream != []) ([ "no-resolv" ] ++ (lib.map (dnsServer: "server ${dnsServer}") netCfg.dns.upstream))); - - vethToHostInNS = "vh-${netCfg.name}"; - vethOnHostToNS = "vn-${netCfg.name}"; - createWanInterface = pkgs.writeShellScript "create-wan-${netCfg.name}-vmix" '' - ip link add ${vethOnHostToNS} type veth peer name ${vethToHostInNS} - ip address add ${hostIPAddressOnLan}/${networkPrefix} dev ${vethOnHostToNS} - - #iptables -A FORWARD -i ${vethOnHostToNS} -j ACCEPT - #iptables -A FORWARD -o ${vethOnHostToNS} -j ACCEPT - #iptables -A INPUT -i ${vethOnHostToNS} -j DROP - iptables -t nat -A POSTROUTING -s ${networkAddress}/${networkPrefix} -j MASQUERADE - - ip link set ${vethToHostInNS} netns ${netCfg.namespace} - ip netns exec ${netCfg.namespace} ip link set ${vethToHostInNS} master ${lanInterfaceName} - - ip link set ${vethOnHostToNS} up - ip netns exec ${netCfg.namespace} ip link set ${vethToHostInNS} up - ''; - - deleteWanInterface = ""; - in - { - "lan.net.vmix@${netCfg.name}" = lib.recursiveUpdate { - wantedBy = lib.optional netCfg.startOnBoot [ "net.vmix.target" ]; - 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; - }; - } (lib.optionalAttrs (netCfg.namespace != null) rec { - bindsTo = [ "ns.vmix@${netCfg.namespace}.service" ]; - after = bindsTo; - unitConfig.JoinsNamespaceOf = "ns.vmix@${netCfg.namespace}.service"; - serviceConfig.PrivateNetwork = true; - }); - - "wan.net.vmix@${netCfg.name}" = rec { - wantedBy = lib.optional netCfg.startOnBoot [ "net.vmix.target" ]; - path = with pkgs; [ iproute2 iptables ]; - bindsTo = [ "lan.net.vmix@${netCfg.name}.service" ]; - after = bindsTo; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = createWanInterface; - ExecStop = deleteWanInterface; - }; - }; - }; -} \ No newline at end of file diff --git a/nixos/modules/network.nix b/nixos/modules/network.nix deleted file mode 100644 index 722deae..0000000 --- a/nixos/modules/network.nix +++ /dev/null @@ -1,79 +0,0 @@ -{ config, pkgs, lib, ... }: -with lib; -let - ipv4Regex = - let - compRegex = "(25[0-5]|(2[0-4]|10|1?[1-9])?[0-9])"; - in - "(${compRegex}\\.){3}${compRegex}"; - - cidr4Regex = "${ipv4Regex}/(3[0-2]|[1-2]?[0-9])"; -in -{ - options = { - startOnBoot = mkOption { - type = types.bool; - default = false; - description = "Whether to start this network on boot regardless if a VM is needing this network."; - }; - - namespace = mkOption { - type = types.nullOr types.str; - default = null; - description = "Linux network namespace under which this network is created. If not declared, it will create under hosts network namespace."; - }; - - type = mkOption { - type =types.enum [ "user" "nat" "natWANOnly" "routed" "routedWANOnly" "isolated" "bridge" ]; - description = '' - Network types. - - "user" is qemu slirp user network, which can be shared across multiple VMs if needed - - "nat" is a NAT with an internal network, with a DHCP/DNS server, a domainsearch name and masqueraded access to the host's network - - "natWANOnly" just like nat but no access to the host itself, or other networks on the host, while allowing WAN access through the hosts default gateway - - "routed" is an internal network, with a DHCP/DNS server, a domainsearch name and routed inbound and outbound access to the host's network - - "routedWANOnly" just like routed, but no access to the host itself, or other networks on the host, while allowing WAN inbound and outbound access through the hosts default gateway - - "isolated" creates an internal network, a DHCP/DNS server, a domainsearch name with no access to host's network or WAN - - "bridge" is a bridge with another network or a host's network interface - ''; - }; - - ipv4Range = mkOption { - type = types.strMatching cidr4Regex; - description = "IPv4 Range in x.x.x.x/y format to be assigned to the network."; - }; - - dhcp.enable = mkOption { - type = types.bool; - default = true; - description = "Whether to start a DHCP server within this network."; - }; - - dhcp.startAddress = mkOption { - type = types.strMatching ipv4Regex; - description = "Starting IP Address for DHCP clients."; - }; - - dhcp.endAddress = mkOption { - type = types.strMatching ipv4Regex; - description = "Ending IP Address for DHCP clients."; - }; - - dns.upstream = mkOption { - type = types.listOf (types.strMatching ipv4Regex); - default = []; - description = "List of IP Addresses of DNS servers to use as upstream DNS servers in the DHCP/DNS server. If left empty, it will use host's DNS servers"; - }; - - dns.zonefiles = mkOption { - description = "Additional zonefiles to add for the DNS server"; - }; - - routes.internal.add = mkOption { - description = "Additional routes to add on the internal network"; - }; - - routes.host.add = mkOption { - description = "Addtional routes to add on the host's network namespace"; - }; - }; -} \ No newline at end of file diff --git a/nixos/network/config.nix b/nixos/network/config.nix new file mode 100644 index 0000000..286645a --- /dev/null +++ b/nixos/network/config.nix @@ -0,0 +1,159 @@ +{ 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; + + 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=${lanCfg.ipv4.dhcp.startAddress},${lanCfg.ipv4.dhcp.endAddress},${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; +} \ No newline at end of file diff --git a/nixos/network/default.nix b/nixos/network/default.nix new file mode 100644 index 0000000..6c6403d --- /dev/null +++ b/nixos/network/default.nix @@ -0,0 +1,16 @@ +args@{ config, pkgs, lib, vmixLib, ... }: +with lib; +{ + options.vmix.global.net.wan.ipv4.range = lib.mkOption { + type = types.strMatching vmixLib.network.regex.cidr4; + default = "172.27.72.0/24"; # enough to create 64x /30 networks for veth pairs used for wan interfaces + }; + + options.vmix.networks = lib.mkOption { + type = types.attrsOf + (types.submodule (import ./options.nix args)); + default = { }; + }; + + imports = [ (import ./config.nix args) ]; +} \ No newline at end of file diff --git a/nixos/network/options.nix b/nixos/network/options.nix new file mode 100644 index 0000000..f69457d --- /dev/null +++ b/nixos/network/options.nix @@ -0,0 +1,156 @@ +{ config, pkgs, lib, vmixLib, ... }: +with lib; +with vmixLib.network; +{ + options = { + bridges.macvlans = mkOption { + type = types.attrsOf (types.submodule { + options = { + uplink.iface = mkOption { + type = types.str; + }; + + uplink.namespace = mkOption { + type = types.nullOr types.str; + default = null; + }; + + namespace = mkOption { + type = types.nullOr types.str; + default = null; + }; + + ipv4.static.address = mkOption { + type = types.nullOr (types.strMatching regex.ipOrCidr4); + default = null; + }; + + ipv4.static.gateway = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + default = null; + }; + + ipv4.dhcp.client = mkOption { + type = types.bool; + default = false; + }; + + ipv4.dhcp.gateway = mkOption { + type = types.bool; + default = false; + }; + }; + }); + }; + + bridges.macvtaps = mkOption { + type = types.attrsOf (types.submodule { + options = { + uplink.iface = mkOption { + type = types.str; + }; + + uplink.namespace = mkOption { + type = types.nullOr types.str; + default = null; + }; + + many = mkOption { + type = types.bool; + default = true; + }; + }; + }); + }; + + wan = { + enable = mkOption { + type = types.bool; + default = true; + }; + + host.wan.enable = mkOption { + type = types.bool; + default = true; + }; + + host.wan.masquerade = mkOption { + type = types.bool; + default = true; + }; + + host.lan.enable = mkOption { + type = types.bool; + default = true; + }; + + host.lan.masquerade = mkOption { + type = types.bool; + default = true; + }; + + host.self.enable = mkOption { + type = types.bool; + default = true; + }; + }; + + lans = mkOption { + type = types.attrsOf (types.submodule { + options.ipv4 = { + range = mkOption { + type = types.strMatching regex.cidr4; + description = "IPv4 Range in x.x.x.x/y format to be assigned to the network."; + }; + + address = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + default = null; + description = "IPv4 address to attach to the bridge interface of this Lan."; + }; + + dhcp.enable = mkOption { + type = types.bool; + default = true; + description = "Whether to start a DHCP server within this network."; + }; + + dhcp.startAddress = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + description = "Starting IP Address for DHCP clients."; + }; + + dhcp.endAddress = mkOption { + type = types.nullOr (types.strMatching regex.ipv4); + description = "Ending IP Address for DHCP clients."; + }; + + dns.upstream = mkOption { + type = types.listOf (types.strMatching regex.ipv4); + default = []; + description = "List of IP Addresses of DNS servers to use as upstream DNS servers in the DHCP/DNS server. If left empty, it will use host's DNS servers"; + }; + + dns.useHostResolvConf = mkOption { + type = types.bool; + default = true; + description = "Whether to use host's /etc/resolv.conf for upstream DNS queries."; + }; + + dns.zonefiles = mkOption { + default = null; + description = "Additional zonefiles to add for the DNS server"; + }; + }; + }); + }; + +# routes.internal.add = mkOption { +# description = "Additional routes to add on the internal network"; +# }; + +# routes.host.add = mkOption { +# description = "Addtional routes to add on the host's network namespace"; +# }; + }; +} \ No newline at end of file diff --git a/nixos/modules/vm.nix b/nixos/vm/options.nix similarity index 100% rename from nixos/modules/vm.nix rename to nixos/vm/options.nix