{ lib, pkgs, config, ... }: { # using https://github.com/greaka/ops/blob/818be4c4dea9129abe0f086d738df4cb0bb38288/apps/restic/options.nix as a base options = { skynet_firewall = { enable = lib.mkEnableOption { default = false; example = true; description = "Skynet Firewall"; type = lib.types.bool; }; forward = lib.mkOption { default = []; type = lib.types.listOf lib.types.str; description = '' A list of routes to forward ''; }; own = { ip = lib.mkOption { default = "127.0.0.1"; type = lib.types.str; description = '' IP of the firewall ''; }; ports = { tcp = lib.mkOption { default = []; type = lib.types.listOf lib.types.int; description = '' A list of TCP ports for the machiene running the firewall ''; }; udp = lib.mkOption { default = []; type = lib.types.listOf lib.types.int; description = '' A list of UDP ports for the machiene running the firewall ''; }; }; }; }; }; config = lib.mkIf config.skynet_firewall.enable { # disable default firewall to enable nftables networking.firewall.enable = false; networking.nftables.enable = true; # fules for the firewall # beware of EOL conversion. networking.nftables.ruleset = '' # using https://oxcrag.net/2021/12/25/build-your-own-router-with-nftables-part-1/ as a guide # Clear out any existing rules flush ruleset # Our future selves will thank us for noting what cable goes where and labeling the relevant network interfaces if it isn't already done out-of-the-box. # red cabled define WANLINK = eno1 # yellow cable define LANLINK = eno2 # We never expect to see the following address ranges on the Internet define BOGONS4 = { 0.0.0.0/8, 10.0.0.0/8, 10.64.0.0/10, 127.0.0.0/8, 127.0.53.53, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 } # Network address translation: What allows us to glue together a private network with the Internet even though we only have one routable address, as per IPv4 limitations table ip nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; # forward anything with port 2222 to this specific ip # tcp dport 2222 counter packets 0 bytes 0 dnat to 193.1.99.76:22 # forward http/s traffic from 76 to 123 # ip daddr 193.1.99.76 tcp dport 80 counter packets 0 bytes 0 dnat to 193.1.99.123:80 # ip daddr 193.1.99.76 tcp dport 443 counter packets 0 bytes 0 dnat to 193.1.99.123:443 } chain postrouting { type nat hook postrouting priority srcnat; policy accept; # the internal network ip saddr 172.20.20.0/23 counter packets 0 bytes 0 masquerade } chain output { type nat hook output priority -100; policy accept; } } # The actual firewall starts here table inet filter { # Additional rules for traffic from the Internet chain inbound_world { # this is actually a good idea # Drop obviously spoofed inbound traffic ip saddr { $BOGONS4 } drop } # Additional rules for traffic from our private network chain inbound_private { # We want to allow remote access over ssh, incoming DNS traffic, and incoming DHCP traffic # for the host machiene # hardcoded in so that we can always have access tcp dport 22 counter packets 0 bytes 0 accept # TCP ${lib.strings.concatMapStrings (x: x + "\n") (map (port: "tcp dport ${toString port} counter packets 0 bytes 0 accept") config.skynet_firewall.own.ports.tcp)} # UDP ${lib.strings.concatMapStrings (x: x + "\n") (map (port: "udp dport ${toString port} counter packets 0 bytes 0 accept") config.skynet_firewall.own.ports.udp)} } # Our funnel for inbound traffic from any network chain inbound { # Default Deny type filter hook input priority 0; policy drop; # Allow established and related connections: Allows Internet servers to respond to requests from our Internal network ct state vmap { established : accept, related : accept, invalid : drop} counter # ICMP is - mostly - our friend. Limit incoming pings somewhat but allow necessary information. ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } limit rate 5/second accept # guy must have had a lot of trouble with spoofed ips # Drop obviously spoofed loopback traffic iifname "lo" ip daddr != 127.0.0.0/8 drop # Separate rules for traffic from Internet and from the internal network iifname vmap { lo: accept, $WANLINK : jump inbound_world, $LANLINK : jump inbound_private } } # Rules for sending traffic from one network interface to another chain forward { # Default deny, again type filter hook forward priority 0; policy drop; # Accept established and related traffic ct state vmap { established : accept, related : accept, invalid : drop } # Let traffic from this router and from the Internal network get out onto the Internet iifname { lo, $LANLINK } accept # Only allow specific inbound traffic from the Internet (only relevant if we present services to the Internet). # tcp dport { $PORTFORWARDS } counter # can basically make each machiene responsibile for their own forwarding (in config at least) ${lib.strings.concatMapStrings (x: x + "\n") config.skynet_firewall.forward} } } ''; }; }