diff --git a/internal/httpapi/phase3.go b/internal/httpapi/phase3.go index e025167..499e677 100644 --- a/internal/httpapi/phase3.go +++ b/internal/httpapi/phase3.go @@ -421,18 +421,25 @@ func executeApprovedAction(ctx context.Context, pool *db.Pool, execID uuid.UUID, } // Pre-flight: for a static config, ping the gateway from the target - // HOST before spending 5+ minutes creating the container and waiting - // on its network. This is the check that would have caught the real - // TypeType failure immediately instead of after a full provision - // attempt: the gateway wasn't reachable because the chosen bridge - // doesn't carry that subnet on this host at all (on `strong`, vmbr0 - // only reaches 192.168.178.0/24 — a 192.168.8.0/24 gateway is simply - // not on that L2 segment, so ARP for it silently gets nothing, no - // matter which 192.168.8.x address is picked). A 1-2s ping from the - // host itself catches this class of misconfiguration before any - // container work happens. + // HOST, on the SPECIFIC BRIDGE being requested, before spending 5+ + // minutes creating the container. This is the check that would have + // caught the real TypeType failure immediately instead of after a + // full provision attempt. + // + // Binding to the bridge (`ping -I `) matters and was found + // live: a plain unqualified `ping ` from the host can succeed via + // the host's own routing table (multiple routes, possibly through an + // upstream router) even when the *container* — which only gets a + // naive on-link default route via its bridge's veth — can never ARP + // that gateway at all. Confirmed on `strong`: bare `ping 192.168.8.2` + // succeeded (via the host's default route), but a container actually + // attached to vmbr0 showed 100% packet loss trying to reach the same + // address, because vmbr0 doesn't carry that subnet's L2 segment. + // Binding to the bridge interface reproduces what the container will + // actually experience, not what the host's broader routing table can + // reach. if isStatic && cfg.GW != "" { - pingOut, pingErr := sshExec(ctx, host, user, fmt.Sprintf("ping -c1 -W2 %s >/dev/null 2>&1 && echo REACHABLE || echo UNREACHABLE", cfg.GW)) + pingOut, pingErr := sshExec(ctx, host, user, fmt.Sprintf("ping -I %s -c1 -W2 %s >/dev/null 2>&1 && echo REACHABLE || echo UNREACHABLE", cfg.Bridge, cfg.GW)) if pingErr != nil || !strings.Contains(pingOut, "REACHABLE") { msg := fmt.Sprintf( "gateway %s is not reachable from %s on bridge %s — this almost always means the bridge doesn't carry that subnet on this host (each bridge only reaches the network it's physically wired to). "+