Skip to content

Commit 4cb77a9

Browse files
fix(e2e): separate Podman Machine loopback listeners (#2622)
* fix(e2e): separate Podman Machine loopback listeners Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * test(e2e): remove shallow harness checks Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * refactor(e2e): trim Podman listener workaround Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> * fix(e2e): bypass proxies for Podman health probe Signed-off-by: Matthew Grossman <mgrossman@nvidia.com> --------- Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
1 parent 8ddd98c commit 4cb77a9

4 files changed

Lines changed: 45 additions & 20 deletions

File tree

.agents/skills/debug-openshell-cluster/SKILL.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,13 @@ Common findings:
208208
host's IPv4 default route. Rootless pasta uses the private source address
209209
selected by that route; rootful Podman uses the bridge gateway address.
210210
- Callback discovery reports that the requested address equals the primary
211-
listener: configure a distinct primary address. For Podman Machine, keep the
212-
IPv4 loopback callback separate by using an IPv6-loopback primary such as
213-
`[::1]:17670`.
211+
listener: configure a distinct primary address. For Podman Machine, bind the
212+
primary listener to IPv6 loopback, for example
213+
`bind_address = "[::1]:17670"`, and register the CLI endpoint as
214+
`https://localhost:17670`. The generated certificate includes `localhost`,
215+
while a raw `https://[::1]:17670` endpoint can fail TLS setup with
216+
`invalid dns name`. This leaves `127.0.0.1:17670` available for the
217+
callback-only listener.
214218
- Rootless slirp4netns, another named helper, or missing helper metadata
215219
requires an explicitly remote `grpc_endpoint`. An explicit `host_gateway_ip`
216220
cannot bypass slirp4netns host-loopback isolation. Do not work around

crates/openshell-core/src/forward.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -745,11 +745,11 @@ pub fn resolve_ssh_gateway(
745745
// Remote cluster: use the remote host but keep the cluster URL port.
746746
return (host.to_string(), cluster_port);
747747
}
748-
// Both endpoints loopback. The unspecified addresses (0.0.0.0 / ::)
749-
// are bind-only — they aren't valid connect targets and aren't in TLS
750-
// cert SANs, so fall back to the cluster URL's host (which the CLI
751-
// is already using to reach the gateway).
752-
if gateway_host == "0.0.0.0" || gateway_host == "::" {
748+
// Unspecified addresses are bind-only, and tonic cannot use an IPv6
749+
// literal as a TLS DNS name. In those cases, keep the cluster URL's
750+
// already-reachable authority. Other loopback addresses retain the
751+
// gateway-reported host.
752+
if matches!(gateway_host, "0.0.0.0" | "::" | "::1") {
753753
return (host.to_string(), cluster_port);
754754
}
755755
return (gateway_host.to_string(), cluster_port);
@@ -1026,6 +1026,13 @@ mod tests {
10261026
assert_eq!(port, 443);
10271027
}
10281028

1029+
#[test]
1030+
fn resolve_ssh_gateway_preserves_loopback_tls_authority() {
1031+
let (host, port) = resolve_ssh_gateway("::1", 8080, "https://localhost:8443");
1032+
assert_eq!(host, "localhost");
1033+
assert_eq!(port, 8443);
1034+
}
1035+
10291036
#[test]
10301037
fn resolve_ssh_gateway_swaps_zeros_for_loopback_cluster_host() {
10311038
// The gateway binds 0.0.0.0 but advertises that bind address via the

docs/reference/sandbox-compute-drivers.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ reflection, inference-route management, and HTTP requests. A
114114
is expected for those requests. The gateway fails startup if a callback
115115
requirement resolves to the exact primary listener address because one socket
116116
cannot preserve both authorization scopes. For the IPv4-loopback callback used
117-
by Podman Machine, bind the primary listener to a distinct address such as
118-
`[::1]:17670`.
117+
by Podman Machine, set `bind_address = "[::1]:17670"` for the primary listener
118+
and register `https://localhost:17670` as the CLI endpoint. The hostname matches
119+
the generated certificate and avoids the TLS transport error produced by a raw
120+
IPv6-literal endpoint. Do not broaden the primary listener to `0.0.0.0`.
119121

120122
## Docker Driver
121123

e2e/with-podman-gateway.sh

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ export OPENSHELL_E2E_GATEWAY_CA_CERT="${PKI_DIR}/ca.crt"
386386

387387
HOST_PORT=$(e2e_pick_port)
388388
HEALTH_PORT=$(e2e_pick_port)
389+
if [ "$(uname -s)" = "Darwin" ]; then
390+
# Podman Machine reserves IPv4 loopback for its callback-only listener.
391+
PRIMARY_BIND_IP="::1"
392+
CLI_ENDPOINT_HOST="localhost"
393+
HEALTH_ENDPOINT_HOST="[::1]"
394+
else
395+
PRIMARY_BIND_IP="127.0.0.1"
396+
CLI_ENDPOINT_HOST="127.0.0.1"
397+
HEALTH_ENDPOINT_HOST="127.0.0.1"
398+
fi
389399
STATE_DIR="${WORKDIR}/state"
390400
mkdir -p "${STATE_DIR}"
391401
export XDG_STATE_HOME="${STATE_DIR}"
@@ -415,11 +425,11 @@ toml_string() {
415425

416426
GATEWAY_CONFIG="${STATE_DIR}/gateway.toml"
417427

418-
# Start from the RPM default template so this e2e test exercises the same
419-
# TOML config path that RPM users get on first start. The template leaves
420-
# bind_address unset and sets compute_drivers = ["podman"], so this test
421-
# exercises the built-in loopback listener plus the callback listener
422-
# requested by the Podman driver.
428+
# Start from the RPM default template so this e2e test exercises the same TOML
429+
# config path that RPM users get on first start. The template leaves
430+
# bind_address unset and sets compute_drivers = ["podman"]. On Podman Machine,
431+
# the driver reserves IPv4 loopback for its callback-only listener, so the
432+
# primary listener uses IPv6 loopback. Native Linux keeps the IPv4 default.
423433
#
424434
# We append the driver-specific table and override the port via CLI flag
425435
# (CLI > TOML in the merge precedence) so the test can use an ephemeral port.
@@ -458,8 +468,9 @@ cp "${ROOT}/deploy/rpm/gateway.toml.default" "${GATEWAY_CONFIG}"
458468

459469
GATEWAY_ARGS=(
460470
--config "${GATEWAY_CONFIG}"
461-
# compute_drivers comes from the RPM template, while bind_address uses the
462-
# built-in loopback default. Override only the port for ephemeral selection.
471+
# compute_drivers comes from the RPM template. Override the loopback address
472+
# and port so Podman Machine can keep its IPv4 callback listener distinct.
473+
--bind-address "${PRIMARY_BIND_IP}"
463474
--port "${HOST_PORT}"
464475
--health-port "${HEALTH_PORT}"
465476
--tls-cert "${PKI_DIR}/server/tls.crt"
@@ -495,10 +506,10 @@ printf '%s\n' "${GATEWAY_PID}" >"${GATEWAY_PID_FILE}"
495506

496507
GATEWAY_NAME="openshell-e2e-podman-${HOST_PORT}"
497508
if [ "${OIDC_MODE}" = "1" ]; then
498-
CLI_GATEWAY_ENDPOINT="https://127.0.0.1:${HOST_PORT}"
509+
CLI_GATEWAY_ENDPOINT="https://${CLI_ENDPOINT_HOST}:${HOST_PORT}"
499510
export OPENSHELL_E2E_OIDC_GATEWAY_ENDPOINT="${CLI_GATEWAY_ENDPOINT}"
500511
else
501-
CLI_GATEWAY_ENDPOINT="https://127.0.0.1:${HOST_PORT}"
512+
CLI_GATEWAY_ENDPOINT="https://${CLI_ENDPOINT_HOST}:${HOST_PORT}"
502513
e2e_register_mtls_gateway \
503514
"${XDG_CONFIG_HOME}" \
504515
"${GATEWAY_NAME}" \
@@ -524,7 +535,8 @@ while [ "${elapsed}" -lt "${timeout}" ]; do
524535
echo "ERROR: openshell-gateway exited before becoming healthy"
525536
exit 1
526537
fi
527-
if curl -sf "http://127.0.0.1:${HEALTH_PORT}/healthz" >/dev/null 2>&1; then
538+
# Keep this loopback probe direct even when ::1 is absent from NO_PROXY.
539+
if curl --noproxy '*' -sf "http://${HEALTH_ENDPOINT_HOST}:${HEALTH_PORT}/healthz" >/dev/null 2>&1; then
528540
echo "Gateway healthy after ${elapsed}s."
529541
break
530542
fi

0 commit comments

Comments
 (0)