diff --git a/boxes/colony.nix b/boxes/colony.nix index e7fecfb..bc4f72b 100644 --- a/boxes/colony.nix +++ b/boxes/colony.nix @@ -8,7 +8,22 @@ }; }; + networking = {}; + my = { + firewall = { + trustedInterfaces = [ "blah" ]; + nat = { + externalInterface = "eth0"; + forwardPorts = [ + { + proto = "tcp"; + sourcePort = 2222; + destination = "127.0.0.1:22"; + } + ]; + }; + }; server.enable = true; }; } diff --git a/flake.lock b/flake.lock index 5ee71f3..4c40cdb 100644 --- a/flake.lock +++ b/flake.lock @@ -95,11 +95,11 @@ }, "impermanence": { "locked": { - "lastModified": 1644585928, - "narHash": "sha256-jOnLRLnzFI/YHE53bHgz/9QjR4Qt6dgIXLnTZOf5oLc=", + "lastModified": 1644623728, + "narHash": "sha256-aG+JnIaFXTM9YqcE5uyBgPlPrkmX4bs+yY5YCfA/vBQ=", "owner": "devplayer0", "repo": "impermanence", - "rev": "47809005570ee4d5b504e382309f5b6dcc5999e5", + "rev": "74be13a87a3bbcbbaf94aea66f9576a1163db4f0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0a69c03..35824d6 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,7 @@ common = "common.nix"; build = "build.nix"; tmproot = "tmproot.nix"; + firewall = "firewall.nix"; server = "server.nix"; }; diff --git a/modules/common.nix b/modules/common.nix index 514032a..22c81b7 100644 --- a/modules/common.nix +++ b/modules/common.nix @@ -68,13 +68,23 @@ }; }; + networking = { + useDHCP = mkDefault false; + enableIPv6 = mkDefault true; + }; + environment.systemPackages = with pkgs; [ bash-completion + tree vim htop iperf3 ]; + services.openssh = { + enable = true; + }; + system = { stateVersion = "21.11"; configurationRevision = with inputs; mkIf (self ? rev) self.rev; diff --git a/modules/firewall.nix b/modules/firewall.nix new file mode 100644 index 0000000..4310ea3 --- /dev/null +++ b/modules/firewall.nix @@ -0,0 +1,166 @@ +{ lib, options, config, ... }: + let + inherit (lib) optionalString concatStringsSep concatMapStringsSep optionalAttrs mkIf mkDefault mkMerge mkOverride; + inherit (lib.my) parseIPPort mkOpt mkBoolOpt dummyOption; + + cfg = config.my.firewall; + in { + options.my.firewall = with lib.types; { + enable = mkBoolOpt true; + trustedInterfaces = options.networking.firewall.trustedInterfaces; + tcp = { + allowed = mkOpt (listOf (either port str)) [ "ssh" ]; + }; + udp = { + allowed = mkOpt (listOf (either port str)) []; + }; + extraRules = mkOpt lines ""; + + nat = with options.networking.nat; { + enable = mkBoolOpt true; + inherit externalInterface forwardPorts; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + networking = { + firewall.enable = false; + nftables = { + enable = true; + ruleset = + let + trusted' = "{ ${concatStringsSep ", " cfg.trustedInterfaces} }"; + in + '' + table inet filter { + chain wan-tcp { + ${concatMapStringsSep "\n " (p: "tcp dport ${toString p} accept") cfg.tcp.allowed} + } + chain wan-udp { + ${concatMapStringsSep "\n " (p: "udp dport ${toString p} accept") cfg.udp.allowed} + } + + chain wan { + ip6 nexthdr icmpv6 icmpv6 type { + destination-unreachable, + packet-too-big, + time-exceeded, + parameter-problem, + mld-listener-query, + mld-listener-report, + mld-listener-reduction, + nd-router-solicit, + nd-router-advert, + nd-neighbor-solicit, + nd-neighbor-advert, + ind-neighbor-solicit, + ind-neighbor-advert, + mld2-listener-report, + echo-request + } accept + ip protocol icmp icmp type { + destination-unreachable, + router-solicitation, + router-advertisement, + time-exceeded, + parameter-problem, + echo-request + } accept + ip protocol igmp accept + + ip protocol tcp tcp flags & (fin|syn|rst|ack) == syn ct state new jump wan-tcp + ip protocol udp ct state new jump wan-udp + } + + chain input { + type filter hook input priority 0; policy drop; + + ct state established,related accept + ct state invalid drop + + iif lo accept + ${optionalString (cfg.trustedInterfaces != []) "iifname ${trusted'} accept\n"} + jump wan + } + chain forward { + type filter hook forward priority 0; policy drop; + ${optionalString (cfg.trustedInterfaces != []) "\n iifname ${trusted'} accept\n"} + ct state related,established accept + } + chain output { + type filter hook output priority 0; policy accept; + } + } + + table nat { + chain prerouting { + type nat hook prerouting priority 0; + } + + chain postrouting { + type nat hook postrouting priority 100; + } + } + + ${cfg.extraRules} + ''; + }; + }; + } + (mkIf cfg.nat.enable { + assertions = [ + { + assertion = (cfg.nat.forwardPorts != []) -> (cfg.nat.externalInterface != null); + message = "my.firewall.nat.forwardPorts requires my.firewall.nat.externalInterface"; + } + ]; + + # Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix + boot = { + kernel.sysctl = { + "net.ipv4.conf.all.forwarding" = mkOverride 99 true; + "net.ipv4.conf.default.forwarding" = mkOverride 99 true; + } // optionalAttrs config.networking.enableIPv6 { + # Do not prevent IPv6 autoconfiguration. + # See . + "net.ipv6.conf.all.accept_ra" = mkOverride 99 2; + "net.ipv6.conf.default.accept_ra" = mkOverride 99 2; + + # Forward IPv6 packets. + "net.ipv6.conf.all.forwarding" = mkOverride 99 true; + "net.ipv6.conf.default.forwarding" = mkOverride 99 true; + }; + }; + + my.firewall.extraRules = + let + makeFilter = f: + let + ipp = parseIPPort f.destination; + in + "ip${optionalString ipp.v6 "6"} daddr ${ipp.ip} ${f.proto} dport ${toString f.sourcePort} accept"; + makeForward = f: "${f.proto} dport ${toString f.sourcePort} dnat to ${f.destination}"; + in + '' + table inet filter { + chain filter-port-forwards { + ${concatMapStringsSep "\n " makeFilter cfg.nat.forwardPorts} + } + chain forward { + iifname ${cfg.nat.externalInterface} jump filter-port-forwards + } + } + + table nat { + chain port-forward { + ${concatMapStringsSep "\n " makeForward cfg.nat.forwardPorts} + } + chain prerouting { + iifname ${cfg.nat.externalInterface} jump port-forward + } + } + ''; + }) + ]); + } diff --git a/modules/tmproot.nix b/modules/tmproot.nix index c2e4c52..2b69897 100644 --- a/modules/tmproot.nix +++ b/modules/tmproot.nix @@ -1,6 +1,6 @@ -{ lib, pkgs, inputs, config, ... }@args: +{ lib, pkgs, inputs, config, ... }: let - inherit (lib) any concatStringsSep mkIf mkDefault mkMerge mkVMOverride; + inherit (lib) concatStringsSep mkIf mkDefault mkMerge mkVMOverride; inherit (lib.my) mkOpt mkBoolOpt mkVMOverride' dummyOption; cfg = config.my.tmproot; @@ -52,24 +52,26 @@ options = [ "size=${cfg.size}" ]; }; in { - imports = [ inputs.impermanence.nixosModules.impermanence ]; + imports = [ inputs.impermanence.nixosModule ]; - options.my.tmproot = with lib.types; { - enable = mkBoolOpt true; - persistDir = mkOpt str "/persist"; - size = mkOpt str "2G"; - ignoreUnsaved = mkOpt (listOf str) [ - "/tmp" - ]; + options = { + my.tmproot = with lib.types; { + enable = mkBoolOpt true; + persistDir = mkOpt str "/persist"; + size = mkOpt str "2G"; + ignoreUnsaved = mkOpt (listOf str) [ + "/tmp" + ]; + }; + + # Forward declare options that won't exist until the VM module is actually imported + virtualisation = { + diskImage = dummyOption; + }; }; - # Forward declare options that won't exist until the VM module is actually imported - options.virtualisation = { - diskImage = dummyOption; - }; - - config = mkMerge [ - (mkIf cfg.enable { + config = mkIf cfg.enable (mkMerge [ + { assertions = [ { assertion = config.fileSystems ? "${cfg.persistDir}"; @@ -96,8 +98,8 @@ virtualisation = { diskImage = "./.vms/${config.system.name}-persist.qcow2"; }; - }) - (mkIf (cfg.enable && config.my.boot.isDevVM) { + } + (mkIf config.my.boot.isDevVM { fileSystems = mkVMOverride { "/" = mkVMOverride' rootDef; # Hijack the "root" device for persistence in the VM @@ -107,5 +109,5 @@ }; }; }) - ]; + ]); } diff --git a/util.nix b/util.nix index 4c3819e..ab41242 100644 --- a/util.nix +++ b/util.nix @@ -1,9 +1,24 @@ { lib }: let + inherit (builtins) replaceStrings elemAt; inherit (lib) genAttrs mapAttrs' types mkOption mkOverride; inherit (lib.flake) defaultSystems; - in { + in rec { addPrefix = prefix: mapAttrs' (n: v: { name = "${prefix}${n}"; value = v; }); + # Yoinked from nixpkgs/nixos/modules/services/networking/nat.nix + isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2; + parseIPPort = ipp: + let + v6 = isIPv6 ipp; + matchIP = if v6 then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; + m = builtins.match "${matchIP}:([0-9-]+)" ipp; + checked = v: if m == null then throw "bad ip:ports `${ipp}'" else v; + in { + inherit v6; + ip = checked (elemAt m 0); + ports = checked (replaceStrings ["-"] [":"] (elemAt m 1)); + }; + mkPkgs = path: args: genAttrs defaultSystems (system: import path (args // { inherit system; })); mkOpt = type: default: mkOption { inherit type default; };