From e4cdc2cae5232c3d236eb3afbd73586f4608ad96 Mon Sep 17 00:00:00 2001 From: Sagar Ch Date: Tue, 28 May 2024 21:01:46 +0000 Subject: [PATCH] WIP: network module --- nixos/default.nix | 29 +++++++++ nixos/functions/network.nix | 121 ++++++++++++++++++++++++++++++++++ nixos/modules/network.nix | 79 +++++++++++++++++++++++ nixos/modules/vm.nix | 125 ++++++++++++++++++++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 nixos/default.nix create mode 100644 nixos/functions/network.nix create mode 100644 nixos/modules/network.nix create mode 100644 nixos/modules/vm.nix diff --git a/nixos/default.nix b/nixos/default.nix new file mode 100644 index 0000000..6719969 --- /dev/null +++ b/nixos/default.nix @@ -0,0 +1,29 @@ +{ config, pkgs, lib, ... }: +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 ;}; +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; + }; +} \ No newline at end of file diff --git a/nixos/functions/network.nix b/nixos/functions/network.nix new file mode 100644 index 0000000..2c151cd --- /dev/null +++ b/nixos/functions/network.nix @@ -0,0 +1,121 @@ +{ 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 new file mode 100644 index 0000000..722deae --- /dev/null +++ b/nixos/modules/network.nix @@ -0,0 +1,79 @@ +{ 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/modules/vm.nix b/nixos/modules/vm.nix new file mode 100644 index 0000000..5f930d8 --- /dev/null +++ b/nixos/modules/vm.nix @@ -0,0 +1,125 @@ +{ config, pkgs, lib, ... }: +with lib; +let + networkModule = import ./network.nix { inherit config pkgs lib ;}; +in +{ + options = { + cpu = + mkOption { + type = types.str; + default = "host"; + description = '' + The CPU model to emulate. + ''; + }; + + smp = + mkOption { + type = types.ints.positive; + default = 1; + description = '' + Specify the number of cores the guest is permitted to use. + The number can be higher than the available cores on the + host system. + ''; + }; + + memSize = + mkOption { + type = types.ints.positive; + default = 1024; + description = '' + The memory size in megabytes of the virtual machine. + ''; + }; + + diskSize = + mkOption { + type = types.str; + default = ""; + description = '' + The disk size in qemu params. + ''; + }; + + osImage = + mkOption { + type = types.package; + description = '' + OS image stored in the nix store, either built with vmixLib.images or fetched from the internet. + ''; + }; + + persistantOverlayImagePath = + mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path where the persistant overlay is stored. If path is non-existant, a qcow2 image backed by the original osImage is created. + ''; + }; + + startOnBoot = + mkOption { + type = types.bool; + description = '' + Whether to start the VM automatically on system boot. + ''; + default = true; + }; + + sharedDirectories = + lib.mkOption { + type = types.attrsOf + (types.submodule { + options = { + source = lib.mkOption { + type = types.str; + description = "The path of the directory to share, can be a shell variable"; + }; + target = lib.mkOption { + type = types.str; + description = "The mount point of the directory inside the virtual machine"; + }; + }; + }); + example = { + my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; }; + }; + default = { }; + }; + + networks = lib.mkOption { + type = types.attrsOf + (types.submodule networkModule); + example = { + my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; }; + }; + default = { }; + }; + }; + + functions.mkVMService = name: cfg: + let + vmCfg = cfg // { inherit name }; + mkDiskImageFrom + diskImage = + qemuArgs = '' + --name ${vmCfg.name} \ + --cpu ${vmCfg.cpu} \ + --smp ${vmCfg.cores} \ + -- + ''; + in + { + + }; +} + +//storage +// +//networking - DNS, VPNs, Bridges, NATs, isolation, connecting with others,host +//run adhoc +//run and die +// \ No newline at end of file