A new Hetzner account is capped at 10 primary IPs (IPv4 and IPv6 both count), 20 shared vCPUs, 8 dedicated vCPUs and no Arm, and a network cannot span zones. So: one private network per zone (10.20.<zone>.0/24), the lowest-index node of each zone keeps a public IPv4 and is its gateway (NAT, MSS clamp, one DNAT port 27000+index per private node, persisted as igneum-nat.service), every other node has no public address. nodes.tsv gains access, pub and port columns; lib resolves same-zone vs cross-zone dial addresses and jumps ssh through the gateway for private nodes. All nodes.tsv loops read on fd 3 (a backgrounded ssh drained the file). TYPE_BY_INDEX puts nodes 8 to 11 on ccx13; node 12 is the last shared one the account allows. create.sh prints the plan's cost from the live API. provision.sh install takes node names and skips binaries whose sha256 matches. Binaries copied from the seed's staged v4 build (same sources), no vCPU for a builder. Results 2026-10-04: RTT matrix (hel1-fsn1 35 ms, ash-sin 289 ms) and a 10-minute propagation window of 644 blocks: p50 343 ms, p90 497 ms, p99 666 ms across 12 nodes in 5 locations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
27 lines
2 KiB
Bash
Executable file
27 lines
2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Runs ON a private node VM (no public address) as root, through the gateway jump:
|
|
# private-node.sh <hetzner-subnet-router e.g. 10.20.4.1>
|
|
# Hetzner's DHCP on the private interface pushes the network's 0.0.0.0/0 route (option 121) once the route exists
|
|
# on the network; this adds it if it is missing (and keeps adding it on every lease through a dhclient hook), sets
|
|
# the resolvers (Hetzner's recursive resolvers, reached through the NAT), then proves the uplink with a TCP connect
|
|
# to deb.debian.org:443 and a DNS lookup.
|
|
set -euo pipefail
|
|
router="$1"
|
|
privif=$(ip -4 -o addr show | awk -v p="${router%.*}." '$4 ~ "^" p { print $2; exit }')
|
|
[ -n "$privif" ] || { echo "no private interface for $router"; exit 1; }
|
|
ip -4 route show default | grep -q . || ip route add default via "$router" dev "$privif"
|
|
cat > /etc/dhcp/dhclient-exit-hooks.d/igneum-private <<HOOK
|
|
# private node of the Igneum devnet: the uplink is the zone gateway, through Hetzner's router $router
|
|
if [ "\$interface" = "$privif" ]; then
|
|
case "\$reason" in BOUND|RENEW|REBIND|REBOOT) ip -4 route show default | grep -q . || ip route add default via $router dev $privif ;; esac
|
|
fi
|
|
HOOK
|
|
if ! grep -qE '^nameserver (185\.12\.64\.|1\.1\.1\.1|8\.8\.8\.8)' /etc/resolv.conf 2>/dev/null; then
|
|
if [ -d /etc/resolvconf/resolv.conf.d ]; then printf 'nameserver 185.12.64.1\nnameserver 185.12.64.2\n' > /etc/resolvconf/resolv.conf.d/base; resolvconf -u 2>/dev/null || true; fi
|
|
grep -qE '^nameserver 185\.12\.64\.' /etc/resolv.conf 2>/dev/null || printf 'nameserver 185.12.64.1\nnameserver 185.12.64.2\n' > /etc/resolv.conf
|
|
fi
|
|
ok=1
|
|
timeout 8 bash -c 'getent hosts deb.debian.org >/dev/null' || { echo "DNS failed"; ok=0; }
|
|
timeout 8 bash -c 'exec 3<>/dev/tcp/deb.debian.org/443' 2>/dev/null || { echo "TCP to deb.debian.org:443 failed"; ok=0; }
|
|
echo "private node: default via $(ip -4 route show default | awk '{ print $3 }' | head -1) dev $privif, resolvers $(awk '/^nameserver/ { printf "%s ", $2 }' /etc/resolv.conf), uplink $([ $ok = 1 ] && echo ok || echo BROKEN)"
|
|
[ "$ok" = 1 ]
|