{ config, lib, pkgs, ... }: with lib; with lib.my; let wgEnabled = hasAttrByPath [ "hosts" config.networking.hostName ] (importTOML ../../data/wireguard.toml); cfg = config.ragon.networking.router; waninterface = cfg.waninterface; laninterface = cfg.laninterface; prefixSize = cfg.prefixSize; statics = cfg.statics; domain = cfg.domain; disableFirewallFor = cfg.disableFirewallFor; lan = { name = "lan"; internet = true; allowipv6 = true; ipv4addr = "10.0.0.1"; netipv4addr = "10.0.0.0"; dhcpv4start = "10.0.10.1"; dhcpv4end = "10.0.255.240"; routes = [ { address = "10.12.0.0"; prefixLength = 16; via = "10.0.1.2"; } { address = "10.13.0.0"; prefixLength = 16; via = "10.0.1.2"; } ]; ipv4size = 16; vlan = 4; }; iot = { name = "iot"; internet = false; allowipv6 = false; ipv4addr = "10.1.0.1"; netipv4addr = "10.1.0.0"; dhcpv4start = "10.1.1.1"; dhcpv4end = "10.1.255.240"; routes = [ ]; ipv4size = 16; vlan = 2; }; guest = { name = "guest"; internet = true; allowipv6 = false; ipv4addr = "192.168.2.1"; netipv4addr = "192.168.2.0"; dhcpv4start = "192.168.2.10"; dhcpv4end = "192.168.2.240"; routes = [ ]; ipv4size = 24; vlan = 3; }; nets = [ lan iot guest ]; ipv6nets = builtins.filter (a: a.allowipv6) nets; interfaceGenerator = obj: { "${obj.name}".ipv4 = { addresses = [{ address = obj.ipv4addr; prefixLength = obj.ipv4size; }]; routes = [{ address = obj.netipv4addr; prefixLength = obj.ipv4size; }] ++ obj.routes; }; }; in { options.ragon.networking.router.enable = mkBoolOpt false; options.ragon.networking.router.waninterface = lib.mkOption { type = lib.types.str; default = "eth1"; }; options.ragon.networking.router.laninterface = lib.mkOption { type = lib.types.str; default = "eth0"; }; options.ragon.networking.router.domain = lib.mkOption { type = lib.types.str; default = "hailsatan.eu"; }; options.ragon.networking.router.prefixSize = lib.mkOption { type = lib.types.int; default = 59; }; options.ragon.networking.router.statics = lib.mkOption { type = lib.types.listOf lib.types.attrs; default = [ { name = "j.hailsatan.eu"; ip = "10.0.0.2"; } { name = "paperless.hailsatan.eu"; ip = "10.0.0.2"; } { name = "unifi.hailsatan.eu"; ip = "10.0.0.2"; } { name = "nix.hailsatan.eu"; ip = "10.0.0.2"; } { name = "h.hailsatan.eu"; ip = "10.0.0.1"; } { name = "grafana.hailsatan.eu"; ip = "10.0.0.2"; } { name = "nzbr.de"; ip = "10.0.1.2"; } { name = "storm.nzbr.de"; ip = "45.9.63.165"; } { name = "avalanche.nzbr.de"; ip = "202.61.247.0"; } ]; }; options.ragon.networking.router.disableFirewallFor = lib.mkOption { type = lib.types.listOf lib.types.attrs; default = [ { hostname = "enterprise"; mac = "d8:cb:8a:76:09:0a"; tcpports = [ 22 ]; udpports = [ ]; } { hostname = "earthquake"; mac = "78:24:af:bc:0c:07"; tcpports = [ 22 22000 ]; udpports = [ 22000 51820 ]; } ]; }; options.ragon.networking.router.staticDHCPs = lib.mkOption { type = lib.types.listOf lib.types.attrs; default = [ # ragon - machines { name = "enterprise"; ip = "10.0.0.9"; mac = "d8:cb:8a:76:09:0a"; } { name = "ds9"; ip = "10.0.0.2"; mac = "f4:b5:20:0e:21:d5"; } # ragon - vms { name = "homeassistant"; ip = "10.0.0.20"; mac = "52:54:00:a1:04:14"; } { name = "enterprise-win"; ip = "10.0.0.201"; mac = "52:54:00:f3:ab:dd"; } # ragon - iot { name = "zbbridge"; ip = "10.1.0.5"; mac = "98:f4:ab:e2:b6:a3"; } { name = "wled-Schrank-Philipp"; ip = "10.1.0.10"; mac = "2c:f4:32:20:74:60"; } { name = "wled-Betthintergrund-Phi"; ip = "10.1.0.11"; mac = "2c:3a:e8:0e:ab:71"; } # nzbr - machines { name = "earthquake"; ip = "10.0.1.2"; mac = "78:24:af:bc:0c:07"; } { name = "comet"; ip = "10.0.1.4"; mac = "0c:98:38:d3:16:8f"; } { name = "meteor"; ip = "10.0.1.8"; mac = "54:27:1e:5c:1f:ed"; } # Wireless { name = "meteor"; ip = "10.0.1.16"; mac = "00:21:cc:5c:f5:dc"; } # Wired { name = "hurricane"; ip = "10.0.1.32"; mac = "f0:2f:74:1b:af:e0"; } # nzbr - vms { name = "earthquake-macos"; ip = "10.0.1.201"; mac = "52:54:00:8e:e2:66"; } { name = "earthquake-win"; ip = "10.0.1.202"; mac = "52:54:00:97:37:69"; } # nzbr - consoles { name = "xbox"; ip = "10.0.2.1"; mac = "58:82:a8:30:2d:1c"; } { name = "wii"; ip = "10.0.2.2"; mac = "00:23:cc:50:78:00"; } { name = "switch"; ip = "10.0.2.3"; mac = "dc:68:eb:bb:01:fc"; } # Wireless ]; }; options.ragon.networking.router.forwardedPorts = lib.mkOption { type = lib.types.listOf lib.types.attrs; default = [ #{ proto = "tcp"; sourcePort = "5060-5061"; destination = "10.0.0.11"; } ]; }; config = { # https://www.willghatch.net/blog/2020/06/22/nixos-raspberry-pi-4-google-fiber-router/ # You’d better forward packets if you actually want a router. boot.kernel.sysctl = { "net.ipv4.ip_forward" = 1; "net.ipv6.conf.all.forwarding" = 1; "net.ipv6.conf.default.forwarding" = 1; "net.ipv6.conf.6rdtun.forwarding" = 1; }; networking.vlans = let genVlan = obj: { "${obj.name}" = { id = obj.vlan; interface = laninterface; }; }; in lib.foldl (a: b: a // b) { } (map genVlan nets); networking.interfaces = let genVlanConf = lib.foldl (a: b: a // b) { } (map interfaceGenerator nets); in { "${waninterface}" = { useDHCP = true; }; } // genVlanConf; networking.dhcpcd = { enable = true; allowInterfaces = [ "${waninterface}" ] ++ (map (a: a.name) ipv6nets); extraConfig = let genDesc = obj: '' # We don’t want dhcpcd to give us an address on the ${obj.name} interface. interface ${obj.name} noipv4 ''; allGenIntDescs = builtins.concatStringsSep "\n" (map genDesc ipv6nets); in '' # The man page says that ipv6rs should be disabled globally when # using a prefix delegation. noipv6rs interface ${waninterface} # On the wan interface, we want to ask for a prefix delegation. iaid 0 ipv6rs ia_pd 0/::/${toString prefixSize} lan/0/${toString prefixSize} ${allGenIntDescs} ''; runHook = '' if [[ "$reason" == "BOUND6" ]] || [[ "$reason" == "REBIND6" ]]; then ${pkgs.python3}/bin/python3 ${pkgs.writeScript "dhcpcd-runHook.py" '' import json import sys import subprocess import os prefix = os.environ.get("new_dhcp6_ia_pd1_prefix1")[:-1] # https://stackoverflow.com/a/37316533/12852285 def mac2ipv6(mac): # only accept MACs separated by a colon parts = mac.split(":") # modify parts to match IPv6 value parts.insert(3, "ff") parts.insert(4, "fe") parts[0] = "%x" % (int(parts[0], 16) ^ 2) # format output ipv6Parts = [] for i in range(0, len(parts), 2): ipv6Parts.append("".join(parts[i:i+2])) ipv6 = "%s%s" % (prefix, ":".join(ipv6Parts)) return ipv6 data = json.loads("""${builtins.toJSON disableFirewallFor}""") for host in data: print('setting firewall rules for ' + host["hostname"]) IP = mac2ipv6(host["mac"]) if len(host["tcpports"]) > 0: subprocess.run(["${pkgs.nftables}/bin/nft", "insert", "rule", "inet", "filter", "forward", "ip6", "daddr", IP, "tcp", "dport", f'{{ {", ".join(map(str, host["tcpports"]))} }}', "accept" ]) if len(host["udpports"]) > 0: subprocess.run(["${pkgs.nftables}/bin/nft", "insert", "rule", "inet", "filter", "forward", "ip6", "daddr", IP, "udp", "dport", f'{{ {", ".join(map(str, host["udpports"]))} }}', "accept" ]) subprocess.run(["${pkgs.nftables}/bin/nft", "insert", "rule", "inet", "filter", "forward", "ip6", "daddr", IP, "icmpv6", "type", "{ destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert }", "accept"]) ''} fi ''; }; networking.firewall.enable = false; # disable iptables cause it's ass to set up networking.nftables.enable = true; networking.nftables.ruleset = let unsafeInterfaces = (map (x: x.name) (filter (x: x.internet == false) nets)); safeInterfaces = (map (x: x.name) (filter (x: x.internet == true) nets)) ++ [ "lo" ] ++ (optionals (wgEnabled) [ "wg0" ]); allInternalInterfaces = (map (x: x.name) nets) ++ [ "lo" ]; portForwards = concatStringsSep "\n" (map (x: "iifname ${waninterface} ${x.proto} dport ${toString x.sourcePort} dnat ${x.destination}") cfg.forwardedPorts); dropUnsafe = concatStringsSep "\n" (map (x: "iifname ${x} drop") unsafeInterfaces); allowSafe = concatStringsSep "\n" (map (x: "iifname ${x} accept") safeInterfaces); allowSafeOif = concatStringsSep "\n" (map (x: "oifname ${x} ct state { established, related } accept") safeInterfaces); allowAll = concatStringsSep "\n" (map (x: "iifname ${x} accept") (allInternalInterfaces ++ (optionals (wgEnabled) [ "wg0" ]))); in '' define unsafe_interfaces = { ${concatStringsSep ",\n" unsafeInterfaces} } define safe_interfaces = { lo, ${concatStringsSep ",\n" safeInterfaces} } define all_interfaces = { lo, ${concatStringsSep ",\n" allInternalInterfaces} } table inet filter { chain input { type filter hook input priority 0; # allow established/related connections ct state { established, related } accept # early drop of invalid connections ct state invalid drop # allow from loopback and internal nic ${allowAll} # allow icmp ip protocol icmp icmp type echo-request limit rate over 1/second burst 5 packets drop ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate over 1/second burst 5 packets drop ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } accept ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, echo-reply, echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert, packet-too-big, parameter-problem, time-exceeded } accept # open port 22, but only allow 2 new connections per minute from each ip tcp dport 22 ct state new flow table ssh-ftable { ip saddr limit rate 2/minute } accept tcp dport 80 accept tcp dport 443 accept udp dport 51820 accept # everything else reject with icmp type port-unreachable } chain forward { type filter hook forward priority 0; # allow from loopback and internal nic ${allowSafe} # allow established/related connections ${allowSafeOif} # Drop everything else drop } chain output { type filter hook output priority 0 # dont allow any trafic from iot and stuff to escape to the wild ${dropUnsafe} } } table ip nat { chain prerouting { type nat hook prerouting priority 0 ${portForwards} } chain postrouting { type nat hook postrouting priority 0 oifname ${waninterface} masquerade } } ''; services.dnsmasq = { enable = true; alwaysKeepRunning = true; extraConfig = let inherit (pkgs) runCommand; gen = obj: '' interface=${obj.name} dhcp-range=${obj.name},${obj.dhcpv4start},${obj.dhcpv4end},12h ''; genHosts = obj: '' dhcp-host=${obj.mac},${obj.ip},${obj.name} ''; genall = builtins.concatStringsSep "\n" (map gen nets); genallHosts = builtins.concatStringsSep "\n" (map genHosts cfg.staticDHCPs); genstatics = builtins.concatStringsSep "\n" (map (a: "address=/${a.name}/${a.ip}") statics); netbootxyz = builtins.fetchurl { url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.40/netboot.xyz.efi"; sha256 = "1gvgvlaxhjkr9i0b2bjq85h12ni9h5fn6r8nphsag3il9kificcc"; }; netbootxyzpath = runCommand "netbootpath" { } '' mkdir $out ln -s ${netbootxyz} $out/netbootxyz.efi ''; in '' no-resolv # unbound broke # server=127.0.0.1#5353 # unbound server=1.1.1.1 server=1.0.0.1 # https://hveem.no/using-dnsmasq-for-dhcpv6 # don't ever listen to anything on wan and stuff except-interface=${waninterface},${laninterface} listen-address=0.0.0.0,:: # don't send bogus requests out on the internets bogus-priv # enable IPv6 Route Advertisements enable-ra # Construct a valid IPv6 range from reading the address set on the interface. The :: part refers to the ifid in dhcp6c.conf. Make sure you get this right or dnsmasq will get confused. dhcp-range=lan,::,constructor:lan, ra-names,slaac, 12h # ra-names enables a mode which gives DNS names to dual-stack hosts which do SLAAC for IPv6. # Add your local-only LAN domain local=/${domain}/ # have your simple hosts expanded to domain expand-hosts # set your domain for expand-hosts domain=${domain} # forward .kube domains to coredns server=/kube/10.13.0.10 ${genall} '' + optionalString wgEnabled '' interface=wg0 no-dhcp-interface=wg0 addn-hosts=/run/wireguard-hosts '' + '' interface=lo # otherwise localhost dns does not work ${genstatics} ${genallHosts} dhcp-boot=netbootxyz.efi enable-tftp tftp-root=${netbootxyzpath} # set authoritative mode dhcp-authoritative ''; }; }; }