fix: forge URL + firewall for isolateContainers=true

When containers run in private netns (isolateContainers=true), host
loopback is unreachable so HIVE_FORGE_URL=http://127.0.0.1:3000 breaks.

- nix/modules/hive-network.nix: when isolateContainers is on + forge
  is enabled, open forge.httpPort on the bridge interface so agents
  can reach forgejo at bridgeIp:httpPort (forgejo binds 0.0.0.0)
- nix/modules/hive-c0re.nix: HIVE_FORGE_URL switches to bridge IP
  when network.enable && isolateContainers; loopback path retained
  when isolateContainers=false
- docs/network.md: add Forge access + Forge URL rows to effects table
- docs/gateway.md: rewrite HIVE_FORGE_URL section for both modes
This commit is contained in:
atlas 2026-06-03 16:01:23 +02:00 committed by mara
commit c97120f016
4 changed files with 38 additions and 11 deletions

View file

@ -200,13 +200,20 @@ dashboard reach by design — the surface is privileged (approve /
deny / destroy) and must not be exposed without a real reverse
proxy in front.
## `HIVE_FORGE_URL`: loopback for in-cluster, sub-domain for the operator
## `HIVE_FORGE_URL`: bridge gateway for isolated agents, loopback for shared-netns
Agents poll `HIVE_FORGE_URL` for Forgejo notifications + run all
`hive-forge` calls against it. `hive-c0re.nix` pins this to
`http://127.0.0.1:<forge.httpPort>` for the in-cluster path: every
agent container shares the host's network namespace, so loopback
reaches the forge container directly with no DNS lookup needed.
`hive-forge` calls against it. `hive-c0re.nix` sets this based on the
network isolation mode:
- **`network.isolateContainers = true`**: agents run in private netns,
so host loopback is unreachable. `HIVE_FORGE_URL` is set to
`http://<bridgeIp>:<forge.httpPort>`. Forgejo binds `0.0.0.0` so it's
reachable at the bridge gateway IP. `hive-network.nix` opens
`forge.httpPort` on the bridge interface automatically.
- **`network.isolateContainers = false`** (default): agents share the host's
network namespace, so loopback reaches forgejo directly. `HIVE_FORGE_URL`
is `http://127.0.0.1:<forge.httpPort>`.
The sub-domain default (`forge.<hive-domain>`) is for **operator
browsers + cross-host clients**, not in-cluster traffic. Using the

View file

@ -108,6 +108,8 @@ agent containers from shared host netns to private netns. Set only after
| IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` |
| Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC |
| Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks |
| Forge access | `networking.firewall.interfaces.<bridge>.allowedTCPPorts` — opens `forge.httpPort` on the bridge interface so isolated agents can reach forgejo at `<bridgeIp>:<httpPort>` |
| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://<bridgeIp>:3000` — forwarded to containers via meta flake |
| c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` |
`HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g.

View file

@ -557,12 +557,19 @@ in
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
}
// lib.optionalAttrs config.services.hyperhive.forge.enable {
# Loopback for in-cluster calls (agents share host netns;
# external `forge.<hive>` sub-domain isn't DNS-resolvable
# from inside nspawn). See
# `docs/gateway.md::HIVE_FORGE_URL: loopback for in-cluster,
# sub-domain for the operator`.
HIVE_FORGE_URL = "http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}";
# In-cluster forge URL. When containers are isolated (private netns),
# 127.0.0.1 is the container's own loopback — unreachable for host
# services. Use the bridge gateway IP instead; forgejo binds 0.0.0.0
# so it's reachable there. Shared-netns mode keeps loopback path.
# External `forge.<hive>` sub-domain isn't DNS-resolvable from inside
# nspawn either way. See `docs/gateway.md::HIVE_FORGE_URL`.
HIVE_FORGE_URL =
if
config.services.hyperhive.network.enable && config.services.hyperhive.network.isolateContainers
then
"http://${config.services.hyperhive.network.bridgeIp}:${toString config.services.hyperhive.forge.httpPort}"
else
"http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}";
}
// lib.optionalAttrs config.services.hyperhive.matrix.gui.enable {
# Availability flags read by the dashboard's `/api/state`.

View file

@ -215,6 +215,17 @@ in
ip saddr ${cfg.bridgeIp}/${toString cfg.bridgePrefixLength} ip daddr 127.0.0.0/8 drop
'';
# Allow isolated agents to reach the forge via the bridge gateway IP.
# Forgejo binds 0.0.0.0 so it's reachable at `bridgeIp:httpPort` from
# inside agent containers; without this rule the default INPUT policy
# drops the connection before it reaches forgejo. Only added when forge
# is enabled — no-op otherwise.
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts =
lib.optionals config.services.hyperhive.forge.enable
[
config.services.hyperhive.forge.httpPort
];
# Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each
# container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical
# network address — the Rust side normalises before subnet arithmetic.