diff --git a/lib/ipv4.nix b/lib/ipv4.nix new file mode 100644 index 0000000..e269c0c --- /dev/null +++ b/lib/ipv4.nix @@ -0,0 +1,320 @@ +/* See: https://github.com/djacu/nixpkgs/blob/adb22cf/lib/network.nix */ +{ lib, ... }: + +let + /* + Converts an IP address from a list of ints to a string. + + Type: prettyIp :: [ Int ] -> String + + Examples: + prettyIp [ 192 168 70 9 ] + => "192.168.70.9" + */ + prettyIp = addr: + lib.concatStringsSep "." (builtins.map builtins.toString addr); + + /* + Given a bit mask, return the associated subnet mask. + + Type: bitMaskToSubnetMask :: Int -> [ Int ] + + Examples: + bitMaskToSubnetMask 15 + => [ 255 254 0 0 ] + bitMaskToSubnetMask 24 + => [ 255 255 255 0 ] + */ + bitMaskToSubnetMask = bitMask: let + numOctets = 4; + octetBits = 8; + octetMin = 0; + octetMax = 255; + # How many initial parts of the mask are full (=255) + fullParts = bitMask / octetBits; + in + lib.genList ( + idx: + # Fill up initial full parts + if idx < fullParts + then octetMax + # If we're above the first non-full part, fill with 0 + else if fullParts < idx + then octetMin + # First non-full part generation + else _genPartialMask (lib.mod bitMask octetBits) + ) + numOctets; + + /* + Generate a the partial portion of a subnet mask. + + Type: _genPartialMask :: Int -> Int + + Examples: + _genPartialMask 0 + => 0 + _genPartialMask 1 + => 128 + _genPartialMask 2 + => 192 + _genPartialMask 3 + => 224 + _genPartialMask 4 + => 240 + _genPartialMask 5 + => 248 + _genPartialMask 6 + => 252 + _genPartialMask 7 + => 254 + */ + _genPartialMask = n: + if n == 0 + then 0 + else _genPartialMask (n - 1) / 2 + 128; + + /* + Given a subnet mask, return the associated bit mask. + + Type: subnetMaskToBitMask :: [ Int ] -> Int + + Examples: + subnetMaskToBitMask [ 255 254 0 0 ] + => 15 + subnetMaskToBitMask [ 255 255 255 0 ] + => 24 + */ + subnetMaskToBitMask = subnetMask: let + partialBits = octet: + if octet == 0 + then 0 + else (lib.mod octet 2) + partialBits (octet / 2); + in + builtins.foldl' + (x: y: x + y) + 0 + (builtins.map partialBits subnetMask); + + /* + Given a CIDR, return the IP Address. + + Type: cidrToIpAddress :: String -> [ Int ] + + Examples: + cidrToIpAddress "192.168.70.9/15" + => [ 192 168 70 9 ] + */ + cidrToIpAddress = cidr: let + splitParts = lib.splitString "/" cidr; + addr = lib.elemAt splitParts 0; + parsed = + builtins.map + lib.toInt + (builtins.match "([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)" addr); + checkBounds = octet: + (octet >= 0) && (octet <= 255); + in + if (builtins.all checkBounds parsed) + then parsed + else builtins.throw "IP ${prettyIp addr} has out of bounds octet(s)"; + + /* + Given a CIDR, return the bitmask. + + Type: cidrToBitMask :: String -> Int + + Examples: + cidrToBitMask "192.168.70.9/15" + => 15 + */ + cidrToBitMask = cidr: let + splitParts = lib.splitString "/" cidr; + mask = lib.toInt (lib.elemAt splitParts 1); + checkBounds = mask: + (mask >= 0) && (mask <= 32); + in + if (checkBounds mask) + then mask + else builtins.throw "Bitmask ${builtins.toString mask} is invalid."; + + /* + Given a CIDR, return the associated subnet mask. + + Type: cidrToSubnetMask :: String -> [ Int ] + + Examples: + cidrToSubnetMask "192.168.70.9/15" + => [ 255 254 0 0 ] + */ + cidrToSubnetMask = cidr: + bitMaskToSubnetMask (cidrToBitMask cidr); + + /* + Given a CIDR, return the associated network ID. + + Type: cidrToNetworkId :: String -> [ Int ] + + Examples: + cidrToNetworkId "192.168.70.9/15" + => [ 192 168 0 0 ] + */ + cidrToNetworkId = cidr: let + ip = cidrToIpAddress cidr; + subnetMask = cidrToSubnetMask cidr; + in + lib.zipListsWith lib.bitAnd ip subnetMask; + + /* + Given a CIDR, return the associated first usable IP address. + + Type: cidrToFirstUsableIp :: String -> [ Int ] + + Examples: + cidrToFirstUsableIp "192.168.70.9/15" + => [ 192 168 0 1 ] + */ + cidrToFirstUsableIp = cidr: let + networkId = cidrToNetworkId cidr; + in + incrementIp networkId 1; + + /* + Given a CIDR, return the associated broadcast address. + + Type: cidrToBroadcastAddress :: String -> [ Int ] + + Examples: + cidrToBroadcastAddress "192.168.70.9/15" + => [ 192 169 255 255 ] + */ + cidrToBroadcastAddress = cidr: let + subnetMask = cidrToSubnetMask cidr; + networkId = cidrToNetworkId cidr; + in + getBroadcastAddress networkId subnetMask; + + /* + Given a network ID and subnet mask, return the associated broadcast address. + + Type: getBroadcastAddress :: [ Int ] -> [ Int ] -> [ Int ] + + Examples: + getBroadcastAddress [ 192 168 0 0 ] [ 255 254 0 0 ] + => [ 192 169 255 255 ] + */ + getBroadcastAddress = networkId: subnetMask: + lib.zipListsWith (nid: mask: 255 - mask + nid) networkId subnetMask; + + /* + Given a CIDR, return the associated last usable IP address. + + Type: cidrToLastUsableIp :: String -> [ Int ] + + Examples: + cidrToLastUsableIp "192.168.70.9/15" + => [ 192 169 255 254 ] + */ + cidrToLastUsableIp = cidr: let + broadcast = cidrToBroadcastAddress cidr; + in + incrementIp broadcast (-1); + + /* + Increment the last octet of a given IP address. + + Type: incrementIp :: [ Int ] -> Int -> [ Int ] + + Examples: + incrementIp [ 192 168 70 9 ] 3 + => [ 192 168 70 12 ] + incrementIp [ 192 168 70 9 ] (-2) + => [ 192 168 70 7 ] + */ + incrementIp = addr: offset: let + lastOctet = lib.last addr; + firstThree = lib.init addr; + in + firstThree ++ [(lastOctet + offset)]; + + /* + Given an IP address and bit mask, return the associated CIDR. + + Type: ipAndBitMaskToCidr :: [ Int ] -> Int -> String + + Examples: + ipAndBitMaskToCidr [ 192 168 70 9 ] 15 + => "192.168.70.9/15" + */ + ipAndBitMaskToCidr = addr: bitMask: + lib.concatStringsSep "/" + [ + (prettyIp addr) + (builtins.toString bitMask) + ]; + + /* + Given an IP address and subnet mask, return the associated CIDR. + + Type: ipAndSubnetMaskToCidr :: [ Int ] -> Int -> String + + Examples: + ipAndSubnetMaskToCidr [ 192 168 70 9 ] [ 255 254 0 0 ] + => "192.168.70.9/15" + */ + ipAndSubnetMaskToCidr = addr: subnetMask: + ipAndBitMaskToCidr addr (subnetMaskToBitMask subnetMask); + + /* + Given a CIDR, return an attribute set of: + the IP Address, + the bit mask, + the first usable IP address, + the last usable IP address, + the network ID, + the subnet mask, + the broadcast address. + + Type: getNetworkProperties :: str -> attrset + + Examples: + getNetworkProperties "192.168.70.9/15" + => { + bitMask = 15; + broadcast = "192.169.255.255"; + firstUsableIp = "192.168.0.1"; + ipAddress = "192.168.70.9"; + lastUsableIp = "192.169.255.254"; + networkId = "192.168.0.0"; + subnetMask = "255.254.0.0"; + } + */ + getNetworkProperties = cidr: let + ipAddress = prettyIp (cidrToIpAddress cidr); + bitMask = cidrToBitMask cidr; + firstUsableIp = prettyIp (cidrToFirstUsableIp cidr); + lastUsableIp = prettyIp (cidrToLastUsableIp cidr); + networkId = prettyIp (cidrToNetworkId cidr); + subnetMask = prettyIp (cidrToSubnetMask cidr); + broadcast = prettyIp (cidrToBroadcastAddress cidr); + in {inherit ipAddress bitMask firstUsableIp lastUsableIp networkId subnetMask broadcast;}; +in { + ipv4 = { + inherit + prettyIp + incrementIp + bitMaskToSubnetMask + subnetMaskToBitMask + ipAndBitMaskToCidr + ipAndSubnetMaskToCidr + cidrToIpAddress + cidrToBitMask + cidrToFirstUsableIp + cidrToLastUsableIp + cidrToNetworkId + cidrToSubnetMask + cidrToBroadcastAddress + getNetworkProperties + ; + }; +} diff --git a/modules/router/dnsmasq.nix b/modules/router/dnsmasq.nix index 82ee3c4..ec43f21 100644 --- a/modules/router/dnsmasq.nix +++ b/modules/router/dnsmasq.nix @@ -2,7 +2,11 @@ with lib; let + inherit (import ../../lib/ipv4.nix inputs) ipv4; + cfg = config.modules.router; + intern = cfg.interfaces.internal; + extern = cfg.interfaces.external; leaseType = types.submodule { options = { @@ -22,6 +26,14 @@ let else [ "1.1.1.1" "1.0.0.1" ]; dhcpHost = builtins.map (lease: "${lease.macAddress},${lease.ipAddress}") cfg.dnsmasq.leases; + + dhcpIPv4Range = let + subnetMask = ipv4.prettyIp (ipv4.cidrToSubnetMask intern.cidr); + firstIP = ipv4.prettyIp (ipv4.incrementIp (ipv4.cidrToFirstUsableIp intern.cidr) 1); + lastIP = ipv4.prettyIp (ipv4.cidrToLastUsableIp intern.cidr); + in "${firstIP}, ${lastIP}, ${subnetMask}, 12h"; + + localDomains = builtins.map (host: "/${host}/${cfg.address}") cfg.dnsmasq.localDomains; in { options.modules.router = { dnsmasq = { @@ -31,15 +43,37 @@ in { type = types.bool; }; - leases = lib.mkOption { + leases = mkOption { default = []; - type = lib.types.listOf leaseType; + type = types.listOf leaseType; description = "List of reserved IP address leases"; }; + + localDomains = lib.mkOption { + default = []; + type = types.listOf types.str; + }; }; }; config = mkIf cfg.dnsmasq.enable { + modules.router.dnsmasq.localDomains = [ + "time.apple.com" + "time1.apple.com" + "time2.apple.com" + "time3.apple.com" + "time4.apple.com" + "time5.apple.com" + "time6.apple.com" + "time7.apple.com" + "time.euro.apple.com" + "time.windows.com" + "0.android.pool.ntp.org" + "1.android.pool.ntp.org" + "2.android.pool.ntp.org" + "3.android.pool.ntp.org" + ]; + networking.nameservers = [ "127.0.0.1" ]; services.resolved.extraConfig = mkDefault '' @@ -69,24 +103,22 @@ in { addn-hosts = "/etc/hosts"; dhcp-range = [ - "10.0.0.2, 10.0.0.255, 255.255.255.0, 12h" - "tag:${cfg.interfaces.internal}, ::1, constructor:${cfg.interfaces.internal}, ra-names, slaac, 12h" + dhcpIPv4Range + "tag:${intern.name}, ::1, constructor:${intern.name}, ra-names, slaac, 12h" ]; dhcp-option = [ "option6:information-refresh-time, 6h" - "option:router,10.0.0.1" - "ra-param=${cfg.interfaces.internal},high,0,0" - ]; - - dhcp-option = mkIf cfg.timeserver.enable [ - "option:ntp-server,10.0.0.1" - ]; + "option:router,${cfg.address}" + "ra-param=${intern.name},high,0,0" + ] ++ ( + if cfg.timeserver.enable then [ "option:ntp-server,${cfg.address}" ] else [] + ); dhcp-host = dhcpHost; # listen only on intern0 by excluding extern0 - except-interface = cfg.interfaces.external; + except-interface = extern.name; # set the DHCP server to authoritative and rapic commit mode dhcp-authoritative = true; @@ -95,23 +127,7 @@ in { # Detect attempts by Verisign to send queries to unregistered hosts bogus-nxdomain = "64.94.110.11"; - address = [ - "/cola.fable-pancake.ts.net/10.0.0.1" - "/time.apple.com/10.0.0.1" - "/time1.apple.com/10.0.0.1" - "/time2.apple.com/10.0.0.1" - "/time3.apple.com/10.0.0.1" - "/time4.apple.com/10.0.0.1" - "/time5.apple.com/10.0.0.1" - "/time6.apple.com/10.0.0.1" - "/time7.apple.com/10.0.0.1" - "/time.euro.apple.com/10.0.0.1" - "/time.windows.com/10.0.0.1" - "/0.android.pool.ntp.org/10.0.0.1" - "/1.android.pool.ntp.org/10.0.0.1" - "/2.android.pool.ntp.org/10.0.0.1" - "/3.android.pool.ntp.org/10.0.0.1" - ]; + address = localDomains; }; }; }; diff --git a/modules/router/network.nix b/modules/router/network.nix index c941f8b..5568194 100644 --- a/modules/router/network.nix +++ b/modules/router/network.nix @@ -1,7 +1,9 @@ -{ lib, config, ... }: +{ lib, config, ... } @ inputs: with lib; let + inherit (import ../../lib/ipv4.nix inputs) ipv4; + cfg = config.modules.router; interfaceType = types.submodule { @@ -14,15 +16,23 @@ let type = types.str; example = "00:00:00:00:00:00"; }; + cidr = mkOption { + type = types.str; + default = "0.0.0.0/0"; + example = "10.0.0.1/24"; + }; }; }; - extern0 = cfg.interfaces.external.name; - extern0MAC = cfg.interfaces.external.macAddress; - intern0 = cfg.interfaces.internal.name; - intern0MAC = cfg.interfaces.internal.macAddress; + extern = cfg.interfaces.external; + intern = cfg.interfaces.internal; in { options.modules.router = { + address = { + type = types.str; + default = ipv4.prettyIp (ipv4.cidrToIpAddress intern.cidr); + example = "127.0.0.1"; + }; interfaces = { external = interfaceType; internal = interfaceType; @@ -32,31 +42,31 @@ in { config = mkIf cfg.enable { services.irqbalance.enable = true; - networking.firewall.trustedInterfaces = [ "lo" intern0 ]; + networking.firewall.trustedInterfaces = [ "lo" intern.name ]; systemd.network = { enable = true; - links."10-${extern0}" = { - matchConfig.PermanentMACAddress = extern0MAC; + links."10-${extern.name}" = { + matchConfig.PermanentMACAddress = extern.macAddress; linkConfig = { Description = "External Network Interface"; - Name = extern0; + Name = extern.name; # MACAddress = "64:20:9f:16:70:a6"; MTUBytes = "1500"; }; }; - links."11-${intern0}" = { - matchConfig.PermanentMACAddress = intern0MAC; + links."11-${intern.name}" = { + matchConfig.PermanentMACAddress = intern.macAddress; linkConfig = { Description = "Internal Network Interface"; - Name = intern0; + Name = intern.name; MTUBytes = "1500"; }; }; - networks."10-${extern0}" = { + networks."10-${extern.name}" = { name = extern0; networkConfig = { DHCP = "ipv4"; @@ -70,10 +80,10 @@ in { }; }; - networks."11-${intern0}" = { - name = intern0; + networks."11-${intern.name}" = { + name = intern.name; networkConfig = { - Address = "10.0.0.1/24"; + Address = cfg.address; DHCPServer = false; IPForward = true; ConfigureWithoutCarrier = true; diff --git a/modules/router/upnp.nix b/modules/router/upnp.nix index b74141b..01a1100 100644 --- a/modules/router/upnp.nix +++ b/modules/router/upnp.nix @@ -3,6 +3,8 @@ with lib; let cfg = config.modules.router; + extern = cfg.interfaces.external; + intern = cfg.interfaces.internal; in { options.modules.router = { upnp = { @@ -19,15 +21,15 @@ in { enable = true; upnp = true; natpmp = true; - internalIPs = [ cfg.interfaces.internal.name ]; - externalInterface = cfg.interfaces.external.name; + internalIPs = [ intern.name ]; + externalInterface = extern.name; appendConfig = '' secure_mode=yes notify_interval=60 clean_ruleset_interval=600 uuid=78b8b903-83c1-4036-8fcd-f64aee25baca - allow 1024-65535 10.0.0.0/24 1024-65535 - deny 0-65535 0.0.0.0/0 0-65535 + allow 1024-65535 ${intern.cidr} 1024-65535 + deny 0-65535 ${extern.cidr} 0-65535 ''; }; }; diff --git a/modules/server/tailscale.nix b/modules/server/tailscale.nix index ed7efb0..27994ee 100644 --- a/modules/server/tailscale.nix +++ b/modules/server/tailscale.nix @@ -4,6 +4,7 @@ with lib; let cfgRoot = config.modules.server; cfg = config.modules.server.tailscale; + address = config.modules.router.address; in { options.modules.server.tailscale = { enable = mkOption { @@ -20,10 +21,12 @@ in { }; config = mkIf cfg.enable && cfgRoot.enable { + modules.router.dnsmasq.localDomains = [ "${hostname}.fable-pancake.ts.net" ]; + networking = { domain = "fable-pancake.ts.net"; firewall.trustedInterfaces = [ "tailscale0" ]; - hosts."10.0.0.1" = [ "${hostname}.fable-pancake.ts.net" hostname ]; + hosts."${address}" = [ "${hostname}.fable-pancake.ts.net" hostname ]; }; age.secrets."tailscale" = {