198 lines
8.1 KiB
Nix
198 lines
8.1 KiB
Nix
{
|
|
config,
|
|
inputs,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
{
|
|
imports = [
|
|
inputs.matthew-hardware.nixosModules.mt7986a-glinet-gl-mt6000
|
|
];
|
|
|
|
config = {
|
|
hardware.mt7986a-glinet-gl-mt6000.enable = true;
|
|
hardware.mt7986a-glinet-gl-mt6000.zealous = true;
|
|
hardware.mt7986a-glinet-gl-mt6000.image.repart.enable = true;
|
|
|
|
nixpkgs.hostPlatform.system = "aarch64-linux";
|
|
nixpkgs.buildPlatform.system = "x86_64-linux";
|
|
|
|
system.stateVersion = "26.11";
|
|
clan.core.sops.defaultGroups = [ "admins" ];
|
|
|
|
clan.core.settings.name = "stellio";
|
|
clan.core.settings.machine.description = "Flint-2 router";
|
|
|
|
# Root is ext4 on the Flint 2's eMMC: keep steady-state writes off it.
|
|
# The journal is persistent but bounded (256M) and synced once a minute
|
|
# (the router needs logs for pppd/kea debugging and, after the Sep 2026
|
|
# unlogged reboots, for post-mortem; volatile storage left nothing).
|
|
# /tmp is tmpfs, no core dumps, and ext4 skips atime updates and
|
|
# flushes its journal every 60 s instead of 5 s so the small periodic
|
|
# writes (Kea leases, timesyncd clock, speedtest textfile, journal)
|
|
# coalesce. A power cut loses at most a minute of that state.
|
|
fileSystems."/".options = [
|
|
"noatime"
|
|
"commit=60"
|
|
];
|
|
boot.tmp.useTmpfs = true;
|
|
services.journald.settings.Journal = {
|
|
Storage = "persistent";
|
|
SystemMaxUse = "256M";
|
|
SyncIntervalSec = "1m";
|
|
};
|
|
systemd.coredump.enable = false;
|
|
|
|
# Upstream 07d9958739 (mainline 2026-05, not in linux-6.18.y). The MT7531
|
|
# switch on the Flint 2 hangs off MDIO, and mt7530_get_stats64 took the
|
|
# MDIO mutex under the RCU read lock that /proc/net/dev holds, so any
|
|
# stats reader (zerotier-one, node exporter, `ip -s link`) that raced a
|
|
# PHY access oopsed the box: "BUG: scheduling while atomic" ->
|
|
# "Unable to handle kernel paging request" on 2026-09-20 09:02 UTC.
|
|
# Drop once boot.kernelPackages moves to a kernel that carries it.
|
|
boot.kernelPatches = [
|
|
{
|
|
name = "mt7530-get-stats64-atomic";
|
|
patch = ./mt7530-get-stats64-atomic.patch;
|
|
}
|
|
];
|
|
|
|
# nixpkgs' services.blocky check runs the aarch64 blocky in a
|
|
# system.checks derivation, which an x86_64 builder without binfmt cannot
|
|
# execute (`--max-jobs 0` -> Exec format error). Validate the same YAML
|
|
# with the build host's blocky instead. Drop once cnx-network's router
|
|
# service carries this itself (router/dns: validate the Blocky config
|
|
# with the build host's binary).
|
|
services.blocky.enableConfigCheck = false;
|
|
system.checks = [
|
|
(pkgs.runCommand "check-blocky-config" { } ''
|
|
${lib.getExe pkgs.buildPackages.blocky} --config ${
|
|
(pkgs.formats.yaml { }).generate "blocky-config.yaml" config.services.blocky.settings
|
|
} validate && touch $out
|
|
'')
|
|
];
|
|
|
|
# The router service opens SSH only to the mgmt VLAN, the staging port
|
|
# and the ZeroTier mesh (inventories/router.nix). Also admit it over
|
|
# Yggdrasil (fleet-wide instance in inventories/default.nix) so the box
|
|
# stays reachable when ZeroTier's controller/relays are down. Scoped to
|
|
# the tunnel interface (clan-core's yggdrasil service names it `ygg`):
|
|
# only decrypted 200::/7 traffic arrives there, so a spoofed source on
|
|
# the WAN never matches.
|
|
networking.firewall.interfaces.${config.services.yggdrasil.settings.IfName}.allowedTCPPorts = [
|
|
22
|
|
];
|
|
|
|
# AIS delegates a single /64 over PPPoE (seen 2026-10-02: "ppp0: DHCP:
|
|
# received delegated prefix 2405:9800:b910:1f5d::/64"). The router
|
|
# service hands out sub-prefixes with SubnetId=auto, so whichever VLAN
|
|
# interface comes up first takes the whole /64 -- on this box vlan-mgmt
|
|
# did, and vlan-lan logged "Couldn't find a suitable prefix. Ran out of
|
|
# address space": no RA prefix on lan, so clients had no global IPv6.
|
|
# Pin the ids: lan (where the clients are) always gets subnet 0, i.e.
|
|
# the entire prefix when it is a /64; mgmt asks for subnet 1, which
|
|
# fails harmlessly on a /64 (mgmt is reached over v4 and the meshes) and
|
|
# starts working unchanged if AIS ever delegates wider. Drop once
|
|
# cnx-network's router service exposes a per-VLAN subnet id.
|
|
systemd.network.networks."40-vlan-lan".dhcpPrefixDelegationConfig.SubnetId = lib.mkForce "0x0";
|
|
systemd.network.networks."40-vlan-mgmt".dhcpPrefixDelegationConfig.SubnetId = lib.mkForce "0x1";
|
|
|
|
# Interim copy of cnx-network-clan 45643d6 (router/ipv6: deprecate
|
|
# withdrawn DHCPv6-PD prefixes); drop when the input is bumped past it.
|
|
# networkd never deprecates a prefix it stops announcing (systemd#29651):
|
|
# on every reboot the first PPPoE session lands on another AIS BNG, is
|
|
# torn down minutes later, and the reconnect brings a different /64.
|
|
# networkd announces the new prefix at once but clients keep the old
|
|
# one preferred for the rest of its 1800 s lifetime, so RFC 6724 source
|
|
# selection picks the dead prefix and every v6 connection fails for up
|
|
# to 30 minutes (2026-10-08 07:57). Watch vlan-* for networkd removing a
|
|
# dynamic (PD) address and send up to three RAs with that /64 at
|
|
# preferred lifetime 0, one short-lived radvd per RA (SIGKILLed: no
|
|
# shutdown RA with router lifetime 0), stopping as soon as the prefix is
|
|
# back on the interface -- a plain flap re-delegates the same /64 within
|
|
# seconds and a deprecating RA sent after networkd re-announced it would
|
|
# leave clients deprecated until the next periodic RA (08:19 today).
|
|
systemd.services.router-radv-deprecate = {
|
|
description = "Deprecate withdrawn DHCPv6-PD prefixes on the VLANs";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "systemd-networkd.service" ];
|
|
path = [
|
|
pkgs.iproute2
|
|
pkgs.ipcalc
|
|
pkgs.radvd
|
|
];
|
|
serviceConfig = {
|
|
Restart = "always";
|
|
RestartSec = 2;
|
|
RuntimeDirectory = "router-radv-deprecate";
|
|
};
|
|
script = ''
|
|
conf=$RUNTIME_DIRECTORY/radvd.conf
|
|
pid=$RUNTIME_DIRECTORY/radvd.pid
|
|
|
|
# /64s of the global addresses currently on an interface.
|
|
prefixes_on() {
|
|
ip -o -6 addr show dev "$1" scope global | while read -r _ _ _ a _; do
|
|
echo "$(ipcalc --no-decorate -n "$a")/''${a##*/}"
|
|
done
|
|
}
|
|
|
|
# "Deleted 4: vlan-lan inet6 2001:db8::1/64 scope global dynamic mngtmpaddr \ valid_lft ..."
|
|
ip -o -6 monitor address | while read -r event _ dev _ addr _ scope rest; do
|
|
[[ $event == Deleted && $dev == vlan-* && $scope == global && $rest == *dynamic* ]] || continue
|
|
prefix="$(ipcalc --no-decorate -n "$addr")/''${addr##*/}"
|
|
cat > "$conf" <<EOF
|
|
interface $dev {
|
|
AdvSendAdvert on;
|
|
MinRtrAdvInterval 3;
|
|
MaxRtrAdvInterval 4;
|
|
AdvDefaultLifetime 1800;
|
|
AdvCurHopLimit 0;
|
|
prefix $prefix {
|
|
AdvOnLink on;
|
|
AdvAutonomous on;
|
|
AdvPreferredLifetime 0;
|
|
AdvValidLifetime 7200;
|
|
};
|
|
};
|
|
EOF
|
|
for _ in 1 2 3; do
|
|
if prefixes_on "$dev" | grep -qxF "$prefix"; then
|
|
echo "$dev: $prefix is back, leaving it announced"
|
|
break
|
|
fi
|
|
echo "$dev: deprecating withdrawn prefix $prefix"
|
|
rm -f "$pid"
|
|
# radvd sends its first RA on start; the next would follow 3-4 s later.
|
|
radvd -C "$conf" -p "$pid" -n -m stderr &
|
|
sleep 1
|
|
kill -KILL $! 2>/dev/null || true
|
|
wait $! || true
|
|
sleep 2
|
|
done
|
|
done
|
|
'';
|
|
};
|
|
|
|
systemd.services.heartbeat-push = {
|
|
description = "Heartbeat push to uptime monitor";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${pkgs.curl}/bin/curl -s -o /dev/null https://uptime.b4l.co.th/api/push/X0WCHAcY5gPf1U8If7BT1FLjpacZqGZu?status=up&msg=OK&ping=";
|
|
};
|
|
};
|
|
|
|
systemd.timers.heartbeat-push = {
|
|
description = "Heartbeat push timer";
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnBootSec = "60s";
|
|
OnUnitActiveSec = "60s";
|
|
Unit = "heartbeat-push.service";
|
|
};
|
|
};
|
|
|
|
};
|
|
}
|