NixOS Raspberry Pi 4 Google Fiber Router
My home network is weird, and it’s taken me, ... uh... way too much work to get it set up how I want it. I’ve decided to post the bulk of my current router configuration as a help to other people who are similarly weird.
Currently my ISP is Google Fiber. I have some reservations about the world’s largest corporate surveillance company managing my home internet connection and therefore handling literally all of my traffic. They claim they don’t keep any of that data or correlate it with accounts. I bet the don’t now, but I don’t think we should feel comfortable putting our infrastructure eggs in a basket controlled by a tracking company that could change their minds on this in the future, or just accidentally store and correlate data.
©: Watch me move to a region where Comcast is a practical monopoly again once I graduate.
𝐆: Frankly, full gigabit requires better hardware than I use anyway – many things that are labelled as “1000 Mbps” in theory only handle a fraction of it in practice, maybe 200-700 Mpbs. Maybe I just need better cables, better than short CAT-6 cables? But I digress.
The router provided by Google was unsatisfactory. It constantly dropped wireless connections for some reason. Additionally, it can only be configured by logging into a Google account so they can send it configuration commands, and there is no way to turn off or otherwise configure its IPv6 firewall. Bogus!
π: The Raspberry Pi 4 uses around 3 watts idle and 7 watts at full load with all 4 cores. An old desktop uses something like 20 times that. Granted, the desktop was running a bunch of other things as well, but I plan to separate the various services to different, low-powered computers for both power savings and security improvements.
△: Note that I’ve slimmed this down a bit from my actual configuration – there are some extra bits and conveniences that mine includes that are not really relevant to the router aspect of the configuration, and I’ve redacted some things specific to my network and hardware. But this configuration should work, or at worst have a couple typos from editing down after copy-pasting it from my actual configuration.
#/etc/nixos/configuration.nix { lib, config, pkgs, ... }: # Here we define variables containing the names of our network interfaces. # Actually, using ‘services.udev.extraRules‘ you can make udev give # your interfaces consistent, memorable names! # However, there is some bug I can’t get things to work if I use # udev to rename the external interface. # Your interface names may vary. let INTERNALINTERFACE = "eth0"; EXTERNALINTERFACE = "enp1s0u1"; in { #### Booting # NixOS wants to enable GRUB by default, but we are using a Raspberry Pi. # Actually, this boot configuration should eventually be outdated, and # a more generic loader should work. But for now, this boot mode works. boot.loader.grub.enable = false; boot.loader.raspberryPi = { enable = true; version = 4; }; boot.kernelPackages = pkgs.linuxPackages_latest; boot.initrd.availableKernelModules = [ "usbhid" ]; fileSystems."/" = { device = "/dev/disk/by-label/NIXOS_SD"; fsType = "ext4"; }; fileSystems."/boot" = { device = "/dev/disk/by-label/FIRMWARE"; fsType = "vfat"; }; #### Networking networking.hostName = "raspberry-pi-google-fiber-router"; services.udev.extraRules = ” # By using udev extra rules, you can change interface names! # It’s really convenient... except that I can’t seem to get routing # to work if I change the name of the external interface. # Changing the name of the internal interface seems to work, though. #SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="12:34:56:78:9a:bc", NAME="better-than-enp0s2u9crap0" ”; # 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; }; # Google fiber requires your router talk to them over vlan 2. # Genius network design, or jerk lockout move to keep # people using the equipment they want you to use because # vlans are generally poorly supported, complicated to # set up, and virtually unknown except to networking # professionals? You decide. networking.vlans."wan" = { interface = EXTERNALINTERFACE; id = 2; }; # Oh, also did I mention they will throttle you down to ~10 Mbps # if you don’t apply this egress map on the vlan? # Genius networking, or jerk lockout move that is even less supported, # more complicated, and less well known than vlans? You decide. systemd.services.wan-egress-map = { enable = true; after = ["wan-netdev.service"]; wantedBy = ["multi-user.target"]; description = "set egress map required for full upload speed on Google Fiber"; serviceConfig = { ExecStart = "${pkgs.iproute}/bin/ip link set wan type vlan egress 0:3"; Type = "oneshot"; }; }; # You probably want to configure a firewall. # I think firewalls are mostly stupid. # # Actually, I would love a protocol to tell router what firewall # settings you want for each client. Then phones could say “Firewall # anything I’m not expecting so I can preserve battery life!”, while # desktops could say “Don’t bother firewalling anything because I’ll # just drop any packets I’m not expecting by myself.” Generally I # would really love for my globally unique IPv6 address to mean I can # just host any services on the internet from any device I want. Alas, # security cargo culting gives us firewalls everywhere. Unless you # explicitly turn off firewalls on the router in your path you can’t # host anything without falling back to the crazy NAT hole punching # nonsense required with IPv4. Oh, and you probably only control the # firewall on your home router. So much for mobile, roaming internet # services that you might actually, you know, make an intelligent, # informed choice to host. networking.firewall.enable = false; networking.interfaces.wan.useDHCP = true; # You don’t actually want this on a Google Fiber router, but if you were # to use a Raspberry Pi 4 router on some more sensible network, you would # want this line on. #networking.interfaces.${EXTERNALINTERFACE}.useDHCP = true; networking.dhcpcd = { enable = true; allowInterfaces = [ "wan" INTERNALINTERFACE ]; extraConfig = ” # The man page says that ipv6rs should be disabled globally when # using a prefix delegation. noipv6rs interface wan # On the wan interface, we want to ask for a prefix delegation. ipv6rs ia_pd 2/::/60 ${INTERNALINTERFACE}/0/64 # We don’t want dhcpcd to give us an address on the internal interface. interface ${INTERNALINTERFACE} noipv4 ”; }; # Now we set up our NAT for ipv4. networking.interfaces.${INTERNALINTERFACE}.ipv4 = { addresses = [{address = "192.168.1.1"; prefixLength = 24;}]; routes = [{address = "192.168.1.0"; prefixLength = 24;}]; }; # Port forwarding is a finnicky thing. # The built-in port forwarding options only get us half way there. # We need to add additional rules to get hairpin NAT working. # (Hairpin NAT means you see the same translations inside the # network as outside it.) Unfortunately, I still haven’t figured # out how to get the same NAT translations to work for the router # itself. Clearly there is room for improvement in the built-in # port forwarding configuration. But I’m not about to figure out # the hairpin-for-localhost issue and try to get things merged. networking.nat = { enable = true; #externalInterface = EXTERNALINTERFACE; externalInterface = "wan"; internalInterfaces = [INTERNALINTERFACE]; internalIPs = ["192.168.1.0/24" "127.0.0.1/32"]; forwardPorts = [ # For destination port range use "x.x.x.x:start-end". # For source port range use "start-end" in a string. {sourcePort = 19; destination = "192.168.1.19:19"; proto = "tcp";} ]; extraCommands = ” # This is the first extra rule needed for hairpin NAT. # It can be done statically. iptables -t nat -A nixos-nat-post -o ${INTERNALINTERFACE} -s 192.168.1.0/24 -d 192.168.1.19/32 -p tcp -m tcp –dport 19 -j SNAT –to-source 192.168.1.1 ”; }; # The final NAT rule requires the IP address of your external interface, # and therefore must be done dynamically if you don’t have a static IP! # Let’s run it using the dhcpcd run hook. networking.dhcpcd.runHook = "/etc/nat-hairpin-rules"; environment.etc.nat-hairpin-rules = { text = ” #!${pkgs.bash}/bin/bash # I perhaps ought to figure out for sure which packages each of these # belongs to and clutter the script with NixOS package paths. # But instead, let’s just source the profile. source /etc/profile ext_ip="$(ip -4 addr show dev wan | grep inet | awk ’{print $2}’ | cut -d / -f 1)" iptables -t nat -A nixos-nat-pre -i ${INTERNALINTERFACE} -s 192.168.1.0/24 -d "$ext_ip" -p tcp -m tcp –dport 19 -j DNAT –to-destination 192.168.1.19 ”; mode = "0555"; }; # We need to run a DHCP server. Let’s also reserve some IP addresses. # Additionally, we can set which DNS servers to use, a domain to search in, # etc. services.dhcpd4 = { enable = true; interfaces = [INTERNALINTERFACE]; machines = [ {hostName = "some-host"; ipAddress = "192.168.1.19"; ethernetAddress = "11:22:33:44:55:66";} ]; extraConfig = ” #option domain-name "my.domain.tld"; #option domain-name-servers 192.168.1.1; option domain-name-servers 1.1.1.1, 8.8.8.8; option subnet-mask 255.255.255.0; default-lease-time 604800; max-lease-time 60480000; subnet 192.168.1.0 netmask 255.255.255.0 { range 192.168.1.100 192.168.1.200; option subnet-mask 255.255.255.0; option broadcast-address 192.168.1.255; option routers 192.168.1.1; } ”; }; # We also want to supply our network with IPv6 addresses, of course, # so they can be first-class citizens of the global network. services.radvd = { enable = true; config = ” interface ${INTERNALINTERFACE} { AdvSendAdvert on; # Advertise at least every 30 seconds MaxRtrAdvInterval 30; # This special prefix detects what the actual prefix # available is dynamically. This should be the default. prefix ::/64 { }; }; ”; }; # You could set your router up to be a DNS server as well! #services.bind = { # enable = true; # # cacheNetworks controls who can use this DNS resolver RECURSIVELY. # # It should be disallowed except for local requests. # # Otherwise your DNS server could be used in bogus amplification attacks! # cacheNetworks = ["127.0.0.0/24" "192.168.1.0/24"]; # forwarders = [ # # IE where does this DNS server look up things it doesn’t know. # # Cloudflare # "1.1.1.1" # "1.0.0.1" # # Google # "8.8.8.8" # "8.8.4.4" # ]; #}; services.xserver.enable = false; services.openssh.enable = true; users.mutableUsers = false; users.users.yourusername = { isNormalUser = true; extraGroups = ["wheel"]; hashedPassword = "output the result of ‘mkpasswd -m sha512‘ here"; openssh.authorizedKeys.keys = ["your-ssh-key-here"]; }; # I could be missing some things, since I stripped some things out of my # actual configuration to arrive at something minimal for a blog post. # I think these are the packages needed for the hairpin nat script. # Of course, for your system administration convenience you likely want # some other things. Say, an editor? environment.systemPackages = [ pkgs.iproute pkgs.gawk pkgs.gnugrep pkgs.iptables ]; }