{ config, pkgs, lib, ... }: let wan = config.networking.nat.externalInterface; lans = config.networking.nat.internalInterfaces; lan = builtins.head lans; inherit (config.networking) domain hostName fqdn; inherit (import ../../data) ulaPrefix dnsProviders; netlib = import ../../lib/net.nix { inherit lib; }; wanPrefixLen = 56; modem = { hostname = "modem"; deviceAddr = "192.168.100.1"; ourAddr = "192.168.100.10"; }; allProviders = lib.attrValues dnsProviders; mainProvider = dnsProviders.quad9; lanCfg = config.networking.interfaces.${lan}; myAddresses = lib.filter (a: !(lib.strings.hasPrefix "fe80:" a)) ( map (lib.getAttr "address") (lanCfg.ipv4.addresses ++ lanCfg.ipv6.addresses) ); cnamesByTarget = lib.mapAttrs ( _: lib.concatMap ( v: lib.pipe v [ (lib.dropEnd 1) (map (lib.removeSuffix ".${domain}")) ] ) ) (lib.groupBy lib.last (map (lib.splitString ",") config.services.dnsmasq.settings.cname)); in { imports = [ ./router/v6only.nix ../settings/services/adguard-home.nix ]; boot.initrd = { systemd.network.enable = true; network.enable = true; }; networking = { useDHCP = false; hosts = lib.genAttrs myAddresses ( lib.const [ "${fqdn}" "${hostName}" ] ); nameservers = lib.mkForce [ "::1" ]; interfaces = { br0 = { ipv6 = { addresses = [ { address = "${ulaPrefix}:42::1"; prefixLength = 48; } { address = "fe80::1"; prefixLength = 64; } ]; }; ipv4 = { addresses = [ { address = "10.0.0.1"; prefixLength = 16; } ]; }; }; }; nat = { # TODO upstream changes somehow enable = false; # NixOS module is a bit limiting }; firewall = { enable = true; trustedInterfaces = lans; filterForward = true; extraForwardRules = '' iifname "${lan}" oifname "${wan}" accept comment "from internal interfaces" ''; interfaces = { "${lan}" = { allowedTCPPorts = [ 22 ]; }; }; }; nftables = { enable = true; tables = { nat = { family = "ip"; content = '' chain pre { type nat hook prerouting priority dstnat; policy accept; } chain post { type nat hook postrouting priority srcnat; policy accept; iifname "${lan}" oifname "${wan}" ip daddr ${modem.deviceAddr}/24 snat to ${modem.ourAddr} comment "half-bridge modem access"; iifname "${lan}" oifname "${wan}" masquerade comment "from internal interfaces" } chain out { type nat hook output priority mangle; policy accept; } ''; }; }; }; }; systemd.network = { enable = true; config = { networkConfig = { IPv6Forwarding = true; IPv6PrivacyExtensions = false; SpeedMeter = true; }; }; networks = { "40-${lan}" = { matchConfig.Name = lan; networkConfig = { IPv6AcceptRA = false; DHCPPrefixDelegation = true; ConfigureWithoutCarrier = true; MulticastDNS = true; Domains = [ config.networking.domain ]; LinkLocalAddressing = "ipv6"; # required for networkd RA despite fe80::1 static assignment DNSOverTLS = false; DNS = map (a: (netlib.wrapv6 a) + ":${toString config.services.dnsmasq.settings.port}") myAddresses; DNSDefaultRoute = false; IPv6SendRA = true; }; dhcpPrefixDelegationConfig = { UplinkInterface = wan; SubnetId = "42"; Assign = true; Announce = true; Token = "::1"; }; ipv6SendRAConfig = { Managed = true; OtherInformation = true; DNS = builtins.filter netlib.isv6 myAddresses; }; ipv6Prefixes = [ { Prefix = "${ulaPrefix}::/48"; AddressAutoconfiguration = false; Assign = false; } ]; }; "40-${wan}" = { matchConfig.Name = wan; networkConfig = { DHCP = true; # this enables DHCPv6 IPv6AcceptRA = true; IPv4Forwarding = true; MulticastDNS = false; DNSDefaultRoute = true; DNSOverTLS = true; DNS = map (p: map (ns: "${ns}#${p.hostname}") p.addresses) allProviders; }; dhcpV4Config = { UseDNS = false; SendHostname = false; UseHostname = false; Label = "${wan}:1"; }; dhcpV6Config = { UseDNS = false; UseAddress = true; # necessary for dynamic DNS SendHostname = false; PrefixDelegationHint = "::/${toString wanPrefixLen}"; }; dhcpPrefixDelegationConfig = { UplinkInterface = ":self"; }; ipv6AcceptRAConfig = { UseDNS = false; }; addresses = [ { Address = "${modem.ourAddr}/24"; Label = "${wan}:0"; Scope = "link"; } ]; routes = [ { Gateway = "_dhcp4"; InitialCongestionWindow = 30; InitialAdvertisedReceiveWindow = 30; } { Gateway = "_ipv6ra"; InitialCongestionWindow = 30; InitialAdvertisedReceiveWindow = 30; } ]; cakeConfig = { Bandwidth = "30M"; OverheadBytes = 18; MPUBytes = 64; CompensationMode = "none"; NAT = true; RTTSec = "100ms"; AckFilter = true; PriorityQueueingPreset = "diffserv8"; }; }; }; }; services.adguardhome.settings = let mkHostPort = (addr: "${netlib.wrapv6 addr}:${toString config.services.dnsmasq.settings.port}"); dnsmasqAddrs = map mkHostPort (builtins.filter netlib.isv6 myAddresses); mkTLSHost = p: "tls://${p.hostname}"; in { dns = { bind_hosts = myAddresses; upstream_dns = (map mkTLSHost allProviders) ++ [ # [/domain1/domain2/domainN/]upstreamString "[/${domain}/]${lib.strings.join " " dnsmasqAddrs}" # [//]: unqualified only ]; bootstrap_dns = mainProvider.addresses; use_private_ptr_resolvers = true; local_ptr_upstreams = dnsmasqAddrs; ratelimit = 0; refuse_any = false; # the dns query type `ANY` }; }; services.resolved = { enable = true; settings.Resolve = { DNS = myAddresses; FallbackDNS = map ( p: toString ( builtins.concatMap (ns: [ "${ns}#${p.hostname}" ns ]) p.addresses ) ) allProviders; LLMNR = false; MulticastDNS = true; DNSOverTLS = true; DNSStubListener = true; }; }; networking.resolvconf.enable = false; services.tailscale.extraSetFlags = [ "--accept-dns=false" ]; boot.kernelModules = [ "tcp_lp" ]; boot.kernel.sysctl = { "net.ipv4.tcp_ecn" = 1; "net.ipv4.tcp_fastopen" = "0x3"; "net.core.default_qdisc" = "fq"; "net.ipv6.conf.all.accept_ra" = 0; "net.ipv6.conf.all.autoconf" = 0; "net.ipv6.conf.default.autoconf" = 0; "net.ipv6.conf.${wan}.autoconf" = 0; }; networking.tempAddresses = "disabled"; # use_tempaddr = 0 services.ddclient = { usev4 = "disabled"; usev6 = "ifv6, ifv6=${wan}"; }; environment.systemPackages = [ config.services.dnsmasq.package pkgs.python3Packages.wakeonlan # first package with ipv6 support pkgs.ndisc6 ]; services.dnsmasq = { enable = true; alwaysKeepRunning = true; resolveLocalQueries = false; settings = { domain = domain; interface = lan; port = 5553; bind-interfaces = true; enable-ra = false; dhcp-fqdn = true; dhcp-authoritative = true; dhcp-rapid-commit = true; dhcp-range = [ "10.0.1.10,10.0.250.250,48h" "${ulaPrefix}:42::10,${ulaPrefix}:42::ffff:ffff,7d" # "set:ra6,::,constructor:${lan},ra-stateless" ]; no-hosts = true; # here be dragons (127.0.0.2) # configure static allocations in /etc/ethers # format: # [hwaddr] [name|ipv4 address] read-ethers = true; # use addn-hosts for resolving names of static allocations addn-hosts = [ "/etc/dnsmasq-hosts" ]; dhcp-option = [ # 0.0.0.0 means the address of the system running dnsmasq # [::] means the *global* address of the system running dnsmasq # [fd00::] refers to a ULA # [fe80::] refers to a link-local address "option:ntp-server,0.0.0.0" "option:dns-server,0.0.0.0" "option6:dns-server,[fd00::]" ]; no-resolv = true; dns-loop-detect = true; # do not forward reverse lookups for private addresses, respond nxdomain # (resolved will not know more than dnsmasq in such cases) bogus-priv = true; host-record = [ "${modem.hostname},${modem.deviceAddr},${toString (60 * 60)}" ]; local = [ "/${domain}/" ]; local-ttl = 1; # defaults to 0 (client should not cache), but 1s shouldn't hurt interface-name = [ "${fqdn},${lan}" "wan.${domain},${wan}/6" ]; }; }; systemd.services.dnsmasq = { requires = [ "systemd-networkd-wait-online@${lan}.service" ]; after = [ "systemd-networkd-wait-online@${lan}.service" ]; }; systemd.settings.Manager = { DefaultStartLimitIntervalSec = 0; }; services.caddy.virtualHosts = { "http://${hostName}" = { serverAliases = map (n: "http://${n}") cnamesByTarget.${fqdn}; extraConfig = '' tls { issuer internal { ca home } } redir https://{host}.${domain} ''; }; }; services.ntp.extraConfig = let localRestrictions = "nomodify"; in '' restrict ${ulaPrefix}/48 ${localRestrictions} restrict 10.0.0.0/16 ${localRestrictions} ''; }