feat(#2363): DHCP pool for bridge service containers

Add a DHCP pool to the gateway's dnsmasq so bridge-attached service
containers (hive-ci and future equivalents) get their addresses from
a proper DHCP server instead of a brittle static derivation.

gateway (hive-gateway.nix):
- Add IPv4 arithmetic helpers (ipToInt, intToIp, pow2) to compute the
  DHCP pool range at nix eval time from bridgeIp + bridgePrefixLength.
- Reserve the last dhcpPoolSize (14) usable host addresses as the DHCP
  pool (e.g. .241-.254 on a /24 with 10.42.0.0 network).
- Add dhcp-range and dhcp-leasefile to the dnsmasq settings block.
  The pool is active whenever services.hyperhive.network.enable is true.

hive-ci (hive-ci.nix):
- Remove the ciBridgeIp / ciBridgeOctets static derivation and the
  brittle top-of-/24 comment block.
- Switch networking.interfaces.eth0 to useDHCP = true so hive-ci gets
  its address from the gateway DHCP pool.

lifecycle (mod.rs, tests.rs):
- Add DHCP_POOL_SIZE = 14 constant (must stay in sync with
  dhcpPoolSize in hive-gateway.nix).
- Remap agents whose FNV-1a hash falls in the DHCP pool into the
  agent-only window [2, dhcp_start - 1]. Only the rare agent whose
  name hashes into the pool is affected; all others keep their IPs.
- Update and extend tests: agent range is now .2-.240 on /24;
  add agent_network_ip_never_in_dhcp_pool covering 18 agent names.
This commit is contained in:
atlas 2026-07-13 10:31:44 +02:00 committed by mara
commit a3796890f5
4 changed files with 114 additions and 36 deletions

View file

@ -39,17 +39,54 @@ async fn setup_proposed_seeds_flake_nix() {
#[test]
fn agent_network_ip_is_in_subnet() {
// Default subnet 10.42.0.0/24 — agents get .2 to .254.
// Default subnet 10.42.0.0/24 — agents get .2 to .240 (last 14 are DHCP pool).
let ip = agent_network_ip("alice", "10.42.0.0/24").expect("should produce an IP");
let octets: Vec<u8> = ip.split('.').map(|o| o.parse().unwrap()).collect();
assert_eq!(&octets[..3], &[10, 42, 0], "wrong /24 prefix");
// usable=253, dhcp_start=241 → agent range is .2-.240
assert!(
octets[3] >= 2 && octets[3] <= 254,
"host byte {}",
octets[3] >= 2 && octets[3] <= 240,
"host byte {} — expected in agent-only range [2,240]",
octets[3]
);
}
#[test]
fn agent_network_ip_never_in_dhcp_pool() {
// No agent should be assigned an address in the DHCP pool
// (.241-.254 on a /24 with DHCP_POOL_SIZE=14).
let subnet = "10.42.0.0/24";
let names = [
"alice",
"bob",
"carol",
"damocles",
"iris",
"argus",
"atlas",
"ruth",
"dmatrix",
"bitburner",
"lexis",
"sock",
"triage",
"janet",
"eve",
"frank",
"grace",
"heidi",
];
for name in names {
let ip = agent_network_ip(name, subnet).unwrap_or_else(|| panic!("{name} returned None"));
let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap();
// dhcp_start_offset = usable(253) + 2 - DHCP_POOL_SIZE(14) = 241
assert!(
last < 241,
"{name} got .{last} — inside the DHCP pool [.241-.254]"
);
}
}
#[test]
fn agent_network_ip_stable() {
// Same name + subnet must always produce the same IP.
@ -121,10 +158,10 @@ fn agent_network_ip_normalizes_bridge_ip_subnet() {
from_bridge, from_canonical,
"bridge-IP and canonical-network form should normalize to the same result"
);
// Result must still be in .2-.254.
// Result must be in .2-.240 (DHCP pool .241-.254 is excluded).
let ip = from_bridge.unwrap();
let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap();
assert!((2..=254).contains(&last), "host byte {last}");
assert!((2..=240).contains(&last), "host byte {last}");
}
/// `setup_proposed` is idempotent: calling it on an existing repo is a