Both routers: sshd takes keys only (it is reachable from the open TollGate SSID, and on the Pi from the WAN); shared %router-sysctls in router-base (no ICMP redirects taken, loose rp_filter, kptr_restrict, BPF JIT hardened). MT6000: the lab WAN SSH rule is gone; 99_zz-wired-private (a uci-defaults script, after TollGate's setup) moves lan1-lan5 into TollGate's private network (192.168.2.1), leaves br-lan to the open SSID, rejects SSH/telnet/ LuCI from it, trims nodogsplash's router ports, and turns on the private network's DHCP, which TollGate's setup left disabled (PITFALLS #55). The kernel has CONFIG_NFT_FLOW_OFFLOAD; fw4's offloading was measured and is left off -- no gain at line rate, and it would let flows bypass nodogsplash's session enforcement (PITFALLS #56). Deployed as MT6000 generation 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
353 lines
16 KiB
Scheme
353 lines
16 KiB
Scheme
;; Guix system for the Raspberry Pi 4B router testbed.
|
|
;;
|
|
;; Boot chain:
|
|
;; EEPROM -> start4.elf (FAT partition 1, see rpi4-image.scm) -> u-boot.bin
|
|
;; -> /boot/extlinux/extlinux.conf on the root partition -> linux + initrd
|
|
;;
|
|
;; extlinux.conf is generated by Guix (u-boot-bootloader), so
|
|
;; `guix system reconfigure` adds generations exactly like on other boards.
|
|
;;
|
|
;; Network:
|
|
;; WAN eth0 DHCP (netifd/udhcpc) from the upstream -- in the lab,
|
|
;; archy-dev-pa's NetworkManager shared connection on eno1
|
|
;; LAN br-lan WiFi AP on the onboard radio (netifd wireless, wpad),
|
|
;; 192.168.10.1/24, dnsmasq DHCPv4/DNS; ULA /60 (netifd),
|
|
;; odhcpd RA/DHCPv6
|
|
;; firewall4 (/etc/config/firewall, seeded from files/uci/firewall): lan
|
|
;; zone trusted, wan zone masqueraded, lan -> wan forwarding
|
|
;;
|
|
;; Build the flashable image with ./build-rpi-image.sh (uses rpi4-image.scm).
|
|
;; Load with GUIX_PACKAGE_PATH pointing at this directory's packages/.
|
|
|
|
;; Must come first: makes every cross build use a glibc whose system()/popen()
|
|
;; run /bin/sh, not the build host's bash (packages/cross-libc-fix.scm).
|
|
(use-modules (cross-libc-fix))
|
|
|
|
(use-modules (gnu)
|
|
(gnu bootloader u-boot)
|
|
(guix packages)
|
|
(guix utils)
|
|
((guix config) #:select (%guix-version))
|
|
(nongnu packages linux)
|
|
(nonguix licenses)
|
|
(ice-9 regex)
|
|
(rpi4-linux)
|
|
(u-boot-rpi-arm64-guix-router)
|
|
(wireless-regdb-signed)
|
|
(openwrt)
|
|
(openwrt-services)
|
|
(tollgate-services)
|
|
(router-base))
|
|
(use-service-modules avahi dns networking shepherd ssh sysctl)
|
|
(use-package-modules admin base bash dns linux networking ntp)
|
|
|
|
(define %rpi-linux-firmware
|
|
;; linux-firmware subset: Broadcom WiFi (brcmfmac) + Realtek USB NIC blobs.
|
|
;; The 43455 .bin/.clm_blob live in cypress/ (brcm/ has links to them), and
|
|
;; WHENCE quotes some names, e.g. "brcm/…raspberrypi,4-model-b.txt"; links
|
|
;; are kept by their own path, not their (often relative) target.
|
|
(package
|
|
(inherit linux-firmware)
|
|
(name "linux-firmware-rpi")
|
|
(arguments
|
|
(substitute-keyword-arguments (package-arguments linux-firmware)
|
|
((#:phases phases #~%standard-phases)
|
|
#~(modify-phases #$phases
|
|
(add-after 'unpack 'select-firmware
|
|
(lambda _
|
|
(use-modules (ice-9 regex))
|
|
(define (wanted? path)
|
|
(string-match "^\"?(brcm|cypress|rtl_nic)/" path))
|
|
(substitute* "WHENCE"
|
|
(("^(File|RawFile|Link|Symlink): *(.*)" _ type rest)
|
|
(string-append (if (wanted? rest) type "Skip")
|
|
": " rest)))))))))
|
|
(synopsis "Broadcom WiFi and Realtek USB NIC firmware for the Pi 4")
|
|
(description
|
|
"Subset of the linux-firmware package with the files needed on a
|
|
Raspberry Pi 4: @code{brcmfmac43455-sdio} WiFi firmware and
|
|
@code{rtl_nic} USB Ethernet firmware.")
|
|
(license (nonfree
|
|
"https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/WHENCE"))))
|
|
|
|
(define u-boot-rpi4-bootloader
|
|
;; Plain extlinux.conf for U-Boot's distro boot. Nothing is written to raw
|
|
;; disk sectors: the Pi firmware loads u-boot.bin from the FAT partition,
|
|
;; which rpi4-image.scm populates.
|
|
(bootloader
|
|
(inherit u-boot-bootloader)
|
|
(name 'u-boot-rpi4)
|
|
(package u-boot-rpi-arm64-guix-router)
|
|
(installer #f)
|
|
(disk-image-installer #f)))
|
|
|
|
;; Router interfaces. The wired WAN is netifd's (files/uci/network), the
|
|
;; onboard GENET port by name, eth0: fine while no USB NIC can race it for
|
|
;; the name (PITFALLS #26). lan-eee-off still finds the port by MAC. The
|
|
;; onboard brcmfmac radio is the LAN's WiFi AP.
|
|
|
|
;; /etc/board-base.json, as OpenWrt's board_detect would write it; wifi-setup
|
|
;; adds the radios (wlan) into /tmp/sysinfo/board.json = /etc/board.json.
|
|
(define %board-base.json
|
|
(plain-file "board-base.json" "\
|
|
{
|
|
\"model\": { \"id\": \"raspberrypi,4-model-b\",
|
|
\"name\": \"Raspberry Pi 4 Model B\" },
|
|
\"network\": {
|
|
\"lan\": { \"device\": \"br-lan\", \"protocol\": \"static\" },
|
|
\"wan\": { \"device\": \"eth0\", \"protocol\": \"dhcp\" }
|
|
}
|
|
}
|
|
"))
|
|
|
|
;; After `wifi config' on first boot: a WPA2 AP named guix-router in br-lan,
|
|
;; 5 GHz channel 36 (brcmfmac has no automatic channel selection), with a
|
|
;; random key -- read it with `uci get wireless.default_radio0.key'.
|
|
(define %wifi-first-config
|
|
(plain-file "wifi-first-config" "\
|
|
key=$(od -An -tx1 -N12 /dev/urandom | tr -d ' \\n')
|
|
uci -q batch <<EOF
|
|
set wireless.radio0.disabled='0'
|
|
set wireless.radio0.country='US'
|
|
set wireless.radio0.band='5g'
|
|
set wireless.radio0.channel='36'
|
|
set wireless.default_radio0.ssid='guix-router'
|
|
set wireless.default_radio0.encryption='psk2'
|
|
set wireless.default_radio0.key='$key'
|
|
set wireless.default_radio0.network='lan'
|
|
set wireless.default_radio0.disabled='0'
|
|
commit wireless
|
|
EOF
|
|
"))
|
|
|
|
;; Grow the root partition and its ext4 file system into free space after it,
|
|
;; so a flashed card uses the whole medium. Runs at every boot; once grown
|
|
;; there's nothing to do. Only for an ext4 root that is the last partition of
|
|
;; an MBR disk (the image's layout). Done online, like by hand (PITFALLS #31).
|
|
(define %grow-root-script
|
|
(mixed-text-file "grow-root"
|
|
"set -eu
|
|
export PATH=" util-linux "/sbin:" util-linux "/bin:" e2fsprogs "/sbin:"
|
|
coreutils "/bin:" sed "/bin:" grep "/bin
|
|
log() { echo \"grow-root: $*\"; echo \"grow-root: $*\" > /dev/kmsg; }
|
|
|
|
src=$(findmnt -no SOURCE /)
|
|
[ \"$(findmnt -no FSTYPE /)\" = ext4 ] || { log \"root is not ext4\"; exit 0; }
|
|
[ -b \"$src\" ] || { log \"root $src is not a block device\"; exit 0; }
|
|
part=${src##*/}
|
|
sys=/sys/class/block/$part
|
|
[ -e \"$sys/partition\" ] || { log \"$src is not a partition\"; exit 0; }
|
|
disk=$(basename \"$(readlink -f \"$sys/..\")\")
|
|
num=$(cat \"$sys/partition\") start=$(cat \"$sys/start\") size=$(cat \"$sys/size\")
|
|
total=$(cat /sys/class/block/$disk/size)
|
|
|
|
sfdisk -d /dev/$disk | grep -qx 'label: dos' || { log \"/dev/$disk is not MBR\"; exit 0; }
|
|
for p in /sys/class/block/$disk/$disk*/start; do
|
|
if [ \"$(cat \"$p\")\" -gt \"$start\" ]; then
|
|
log \"$src is not the last partition\"; exit 0
|
|
fi
|
|
done
|
|
free=$((total - start - size))
|
|
[ \"$free\" -ge 2048 ] || exit 0 # < 1 MiB: already grown
|
|
|
|
log \"growing $src: $((size / 2048)) MiB -> $(((total - start) / 2048)) MiB\"
|
|
echo ',+' | sfdisk --no-reread --no-tell-kernel -N \"$num\" /dev/$disk
|
|
new=$(partx -g -o SECTORS --nr \"$num\" /dev/$disk | tr -d ' ')
|
|
resizepart /dev/$disk \"$num\" \"$new\"
|
|
resize2fs \"$src\"
|
|
log \"done: $(df -h / | tail -1)\"
|
|
"))
|
|
|
|
(define %grow-root-service
|
|
(simple-service 'grow-root shepherd-root-service-type
|
|
(list (shepherd-service
|
|
(documentation "Grow the root partition to fill the disk.")
|
|
(provision '(grow-root))
|
|
(requirement '(file-systems))
|
|
(one-shot? #t)
|
|
(start #~(lambda _
|
|
(zero? (system* #$(file-append bash "/bin/bash")
|
|
#$%grow-root-script))))))))
|
|
|
|
;; Energy-Efficient Ethernet between the onboard BCM54213PE and the
|
|
;; workstation's Intel e1000e drops up to ~90% of frames (PITFALLS #30).
|
|
;; GENET only attaches its PHY when the interface is up, so this runs after
|
|
;; 'lan rather than from udev; the port is found by MAC like the bridge port.
|
|
(define %lan-eee-off-service
|
|
(simple-service 'lan-eee-off shepherd-root-service-type
|
|
(list (shepherd-service
|
|
(documentation "Disable EEE on the onboard Ethernet port.")
|
|
(provision '(lan-eee-off))
|
|
(requirement '(lan))
|
|
(one-shot? #t)
|
|
;; Only devices backed by hardware: br-lan inherits the port's
|
|
;; MAC and sorts first. Retry for 30 s in case the PHY isn't
|
|
;; attached yet.
|
|
(start #~(lambda _
|
|
(zero? (system* #$(file-append bash "/bin/bash") "-c"
|
|
"for i in {1..30}; do
|
|
for d in /sys/class/net/*; do
|
|
[ -e \"$d/device\" ] || continue
|
|
read -r mac < \"$d/address\"
|
|
[ \"$mac\" = e4:5f:01:3e:d2:15 ] &&
|
|
\"$0\" --set-eee \"${d##*/}\" eee off &&
|
|
exit 0
|
|
done
|
|
\"$1\" 1
|
|
done; exit 1"
|
|
#$(file-append ethtool "/sbin/ethtool")
|
|
#$(file-append coreutils "/bin/sleep")))))))))
|
|
|
|
;; GPIO 14/15 serial console. With the mainline device tree that is the
|
|
;; mini-UART (uart1, "serial1"); the PL011 is wired to Bluetooth.
|
|
(define %serial-console "ttyS1")
|
|
|
|
(define %rpi4-os
|
|
(operating-system
|
|
(host-name "pirouter")
|
|
(timezone "Etc/UTC")
|
|
(locale "en_US.utf8")
|
|
|
|
(bootloader (bootloader-configuration
|
|
(bootloader u-boot-rpi4-bootloader)
|
|
(targets (list "/dev/mmcblk0"))
|
|
(timeout 3)))
|
|
|
|
(kernel linux-rpi4)
|
|
(firmware (list %rpi-linux-firmware wireless-regdb-signed))
|
|
|
|
;; SD card, its clocks and regulators are built into linux-rpi4.
|
|
(initrd-modules '())
|
|
|
|
(kernel-arguments (list "console=tty1"
|
|
(string-append "console=" %serial-console ",115200")))
|
|
|
|
(file-systems (cons* (file-system
|
|
(mount-point "/")
|
|
(device (file-system-label "Guix"))
|
|
(type "ext4"))
|
|
(file-system
|
|
(mount-point "/boot/firmware")
|
|
(device (file-system-label "RPI-BOOT"))
|
|
(type "vfat")
|
|
;; Root-only: holds the WiFi password.
|
|
(options "umask=0077"))
|
|
%base-file-systems))
|
|
|
|
(swap-devices '())
|
|
|
|
(users (cons* %router-root-account
|
|
(user-account
|
|
(name "pi")
|
|
(group "users")
|
|
(supplementary-groups '("wheel" "netdev" "audio" "video"))
|
|
(home-directory "/home/pi"))
|
|
%base-user-accounts))
|
|
|
|
(packages (append (list usbutils iw ethtool
|
|
;; OpenWrt userland tools (Phase 3).
|
|
ucode jsonfilter)
|
|
%base-packages))
|
|
|
|
(services (cons*
|
|
;; No RTC: start from the last known time, not 1970. First,
|
|
;; so its activation runs before the others.
|
|
(service sysfixtime-service-type)
|
|
(service agetty-service-type
|
|
(agetty-configuration
|
|
(tty %serial-console)
|
|
(term "vt100")
|
|
(baud-rate "115200")))
|
|
(service avahi-service-type)
|
|
|
|
;; LAN and WAN: netifd from /etc/config/network (seeded
|
|
;; below): br-lan 192.168.10.1/24 (plus the WiFi AP);
|
|
;; wan = DHCP on eth0. Upstream DNS goes to
|
|
;; /etc/resolv.conf, which dnsmasq forwards to.
|
|
(service netifd-service-type
|
|
(netifd-configuration
|
|
(resolv-conf "/etc/resolv.conf")))
|
|
;; WiFi AP on the onboard radio, OpenWrt's way: netifd's
|
|
;; wireless handling configures wpad (hostapd) from
|
|
;; /etc/config/wireless, created at first boot by `wifi
|
|
;; config' plus %wifi-first-config. The AP is in br-lan.
|
|
(service openwrt-wireless-service-type
|
|
(openwrt-wireless-configuration
|
|
(board-base %board-base.json)
|
|
(first-config %wifi-first-config)))
|
|
;; DNS and DHCPv4 from /etc/config/dhcp (LuCI: Network >
|
|
;; DHCP and DNS), rendered by OpenWrt's init script. LAN
|
|
;; must come up even when the WiFi uplink is down.
|
|
(service openwrt-dnsmasq-service-type
|
|
(openwrt-dnsmasq-configuration
|
|
(dnsmasq dnsmasq/cross-fixed)))
|
|
|
|
(service firewall-service-type)
|
|
(simple-service 'router-sysctl sysctl-service-type
|
|
%router-sysctls)
|
|
|
|
;; OpenWrt userland (Phase 3/4): message bus and UCI.
|
|
(service ubus-service-type)
|
|
;; procd's ubus API: `uci commit X; reload_config' reloads
|
|
;; the services registered for X; `ubus call system board'.
|
|
(service procd-compat-service-type
|
|
(procd-compat-configuration
|
|
(release `(("distribution" . "Guix System")
|
|
("version" . ,%guix-version)
|
|
("target" . "bcm27xx/bcm2711")
|
|
("description"
|
|
. "Guix System, OpenWrt userland")))))
|
|
(service uci-service-type
|
|
(uci-configuration
|
|
(seeds `(("system" ,(local-file "files/uci/system"))
|
|
("network" ,(local-file "files/uci/network"))
|
|
("dhcp" ,(local-file "files/uci/dhcp"))
|
|
("firewall" ,(local-file "files/uci/firewall"))))
|
|
(defaults
|
|
`(("12_network-generate-ula"
|
|
,(file-append %openwrt-tree
|
|
"/package/base-files/files/etc/"
|
|
"uci-defaults/12_network-generate-ula"))))))
|
|
(service odhcpd-service-type)
|
|
;; LuCI at http(s)://192.168.10.1/ (root, system password);
|
|
;; brings in rpcd. uhttpd listens on all addresses, the
|
|
;; firewall's wan zone rejects it.
|
|
(service uhttpd-service-type)
|
|
(service luci-service-type)
|
|
;; Captive portal (TollGate's gate); seeded disabled:
|
|
;; `uci set nodogsplash.@nodogsplash[0].enabled=1'.
|
|
(service nodogsplash-service-type)
|
|
;; TollGate (Cashu/Nostr paid access through nodogsplash).
|
|
;; Its first-boot setup (99-tollgate-setup) reconfigures
|
|
;; uhttpd (:8080), WiFi (public + private SSIDs, br-private),
|
|
;; the firewall (tcp 2121) and nodogsplash.
|
|
(service tollgate-service-type)
|
|
|
|
%grow-root-service
|
|
%lan-eee-off-service
|
|
%chrony-accounts
|
|
%chrony-service
|
|
|
|
;; Keys only: sshd is reachable from the WAN (the admin path,
|
|
;; firewall rule Allow-SSH-WAN-lab) and the open TollGate SSID.
|
|
(service openssh-service-type
|
|
(openssh-configuration
|
|
(permit-root-login 'prohibit-password)
|
|
(password-authentication? #f)
|
|
(authorized-keys
|
|
`(("root" ,(local-file "files/workstation.pub"))))))
|
|
(modify-services %base-services
|
|
;; setfont: "Bad input file size" on the Pi's framebuffer
|
|
;; console; the built-in font is fine.
|
|
(delete console-font-service-type)
|
|
;; Accept store items signed by the build workstation, so
|
|
;; ./deploy-rpi.sh can `guix copy` cross-built systems.
|
|
(guix-service-type
|
|
config => (guix-configuration
|
|
(inherit config)
|
|
(authorized-keys
|
|
(cons (local-file "files/workstation-guix.pub")
|
|
%default-authorized-guix-keys)))))))))
|
|
|
|
%rpi4-os
|