diff --git a/src/lib/onboard/gateway-sandbox-reachability.test.ts b/src/lib/onboard/gateway-sandbox-reachability.test.ts index 6d5f0954179..9599f93687e 100644 --- a/src/lib/onboard/gateway-sandbox-reachability.test.ts +++ b/src/lib/onboard/gateway-sandbox-reachability.test.ts @@ -5,8 +5,8 @@ import { describe, expect, it } from "vitest"; import { __test, - isSandboxBridgeGatewayReachable, formatSandboxBridgeUnreachableMessage, + isSandboxBridgeGatewayReachable, } from "../../../dist/lib/onboard/gateway-sandbox-reachability"; describe("gateway sandbox reachability route modeling", () => { @@ -131,6 +131,17 @@ describe("formatSandboxBridgeUnreachableMessage", () => { gatewayIp: "172.19.0.1", }); expect(msg).toContain("172.19.0.1:8080"); + expect(msg).toContain("ufw allow from 172.19.0.0/16 to 172.19.0.1 port 8080"); + }); + + it("falls back to a subnet-only UFW command when the gateway IP is unavailable", () => { + const msg = formatSandboxBridgeUnreachableMessage({ + ok: false, + reason: "tcp_failed", + routeKind: "bridge_gateway", + networkName: "openshell-docker", + subnet: "172.19.0.0/16", + }); expect(msg).toContain("ufw allow from 172.19.0.0/16 to any port 8080"); }); diff --git a/src/lib/onboard/gateway-sandbox-reachability.ts b/src/lib/onboard/gateway-sandbox-reachability.ts index 4bf55975252..01641da477f 100644 --- a/src/lib/onboard/gateway-sandbox-reachability.ts +++ b/src/lib/onboard/gateway-sandbox-reachability.ts @@ -282,12 +282,15 @@ export function formatSandboxBridgeUnreachableMessage( ].join("\n"); } - const allowCmd = result.subnet - ? ` sudo ufw allow from ${result.subnet} to any port ${port} proto tcp` - : [ - ` SUBNET=$(docker network inspect ${result.networkName ?? DEFAULT_NETWORK_NAME} --format '{{(index .IPAM.Config 0).Subnet}}')`, - ` sudo ufw allow from "$SUBNET" to any port ${port} proto tcp`, - ].join("\n"); + const allowCmd = + result.subnet && result.gatewayIp + ? ` sudo ufw allow from ${result.subnet} to ${result.gatewayIp} port ${port} proto tcp` + : result.subnet + ? ` sudo ufw allow from ${result.subnet} to any port ${port} proto tcp` + : [ + ` SUBNET=$(docker network inspect ${result.networkName ?? DEFAULT_NETWORK_NAME} --format '{{(index .IPAM.Config 0).Subnet}}')`, + ` sudo ufw allow from "$SUBNET" to any port ${port} proto tcp`, + ].join("\n"); const target = result.gatewayIp ? `${HOST_INTERNAL_NAME}:${port} (${result.gatewayIp}:${port})` : `${HOST_INTERNAL_NAME}:${port}`; diff --git a/src/lib/onboard/ollama-proxy-reachability.test.ts b/src/lib/onboard/ollama-proxy-reachability.test.ts index 6d4ff7c08e9..f241d2c8605 100644 --- a/src/lib/onboard/ollama-proxy-reachability.test.ts +++ b/src/lib/onboard/ollama-proxy-reachability.test.ts @@ -16,10 +16,10 @@ vi.mock("../adapters/docker/run", () => ({ import { OLLAMA_PROXY_PORT } from "../core/ports"; import { + __test, DEFAULT_OLLAMA_PROBE_NETWORK, formatOllamaProxyUnreachableMessage, probeOllamaProxySandboxReachability, - __test, } from "./ollama-proxy-reachability"; const { parseNetworkIpamConfig } = __test; @@ -274,20 +274,33 @@ describe("formatOllamaProxyUnreachableMessage", () => { ).toBe(""); }); - it("includes subnet-specific ufw command when subnet is known", () => { + it("includes gateway-specific ufw command when subnet and gateway are known", () => { const msg = formatOllamaProxyUnreachableMessage({ ok: false, reason: "tcp_failed", networkName: "openshell-docker", subnet: "172.20.0.0/16", + gatewayIp: "172.20.0.1", }); expect(msg).toContain("172.20.0.0/16"); + expect(msg).toContain("172.20.0.1"); + expect(msg).toContain("ufw allow from 172.20.0.0/16 to 172.20.0.1 port 11435"); expect(msg).toContain(String(OLLAMA_PROXY_PORT)); expect(msg).toContain("sudo ufw allow"); expect(msg).toContain("host.openshell.internal"); expect(msg).toContain("nemoclaw onboard"); }); + it("falls back to a subnet-only UFW command when the gateway IP is unavailable", () => { + const msg = formatOllamaProxyUnreachableMessage({ + ok: false, + reason: "tcp_failed", + networkName: "openshell-docker", + subnet: "172.20.0.0/16", + }); + expect(msg).toContain("ufw allow from 172.20.0.0/16 to any port 11435"); + }); + it("includes dynamic SUBNET= fallback when subnet is unknown", () => { const msg = formatOllamaProxyUnreachableMessage({ ok: false, diff --git a/src/lib/onboard/ollama-proxy-reachability.ts b/src/lib/onboard/ollama-proxy-reachability.ts index 708fc77f680..69b728a6f52 100644 --- a/src/lib/onboard/ollama-proxy-reachability.ts +++ b/src/lib/onboard/ollama-proxy-reachability.ts @@ -15,8 +15,8 @@ * ufw remediation before declaring onboard successful. */ -import { OLLAMA_PROXY_PORT } from "../core/ports"; import { dockerCapture, dockerRun } from "../adapters/docker/run"; +import { OLLAMA_PROXY_PORT } from "../core/ports"; export const DEFAULT_OLLAMA_PROBE_NETWORK = "openshell-docker"; const HOST_INTERNAL_NAME = "host.openshell.internal"; @@ -235,12 +235,15 @@ export function formatOllamaProxyUnreachableMessage( ): string { if (result.ok || result.reason !== "tcp_failed") return ""; - const allowCmd = result.subnet - ? ` sudo ufw allow from ${result.subnet} to any port ${port} proto tcp` - : [ - ` SUBNET=$(docker network inspect ${result.networkName ?? DEFAULT_OLLAMA_PROBE_NETWORK} --format '{{(index .IPAM.Config 0).Subnet}}')`, - ` sudo ufw allow from "$SUBNET" to any port ${port} proto tcp`, - ].join("\n"); + const allowCmd = + result.subnet && result.gatewayIp + ? ` sudo ufw allow from ${result.subnet} to ${result.gatewayIp} port ${port} proto tcp` + : result.subnet + ? ` sudo ufw allow from ${result.subnet} to any port ${port} proto tcp` + : [ + ` SUBNET=$(docker network inspect ${result.networkName ?? DEFAULT_OLLAMA_PROBE_NETWORK} --format '{{(index .IPAM.Config 0).Subnet}}')`, + ` sudo ufw allow from "$SUBNET" to any port ${port} proto tcp`, + ].join("\n"); return [ ` ✗ Sandbox containers cannot reach the Ollama auth proxy at ${HOST_INTERNAL_NAME}:${port}.`,