Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions crates/openshell-driver-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,19 @@ the supervisor companion. The workload never receives the sandbox JWT, gateway
client TLS key, policy authority, or interception CA private key.

When no endpoint is configured, the supervisor connects to
`127.0.0.1:<gateway-port>`. Set `grpc_endpoint` when the gateway is not on the
Docker daemon host. A configured HTTPS server certificate must include the
endpoint host in its subject alternative names.
`127.0.0.1:<gateway-port>`. When the gateway runs inside a WSL 2 distribution
and the daemon is Docker Desktop, the default is
`host.docker.internal:<gateway-port>` instead: Docker Desktop's host network is
its own VM, and WSL forwards the gateway's loopback listener to the Windows
host that `host.docker.internal` names. Set `grpc_endpoint` when the gateway is
not on the Docker daemon host. A configured HTTPS server certificate must
include the endpoint host in its subject alternative names; the generated
certificate includes `host.docker.internal`.

In host mode, the driver publishes host loopback as the backend address for
`host.openshell.internal`. Policy DNS resolves that reserved name through the
`host.openshell.internal`. When the endpoint uses `host.docker.internal`,
including the WSL 2 Docker Desktop default, the driver publishes Docker's
`host-gateway` address instead. Policy DNS resolves that reserved name through the
mediated path, so policies can reach host services without a Docker bridge,
container DNS alias, or another gateway listener.

Expand Down
69 changes: 67 additions & 2 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,9 +943,16 @@ impl DockerComputeDriver {
);
let mut docker_config = docker_config.clone();
if docker_config.grpc_endpoint.trim().is_empty() {
let network = DockerSupervisorNetwork::detect(&info, gateway_host_is_wsl());
docker_config.grpc_endpoint = default_docker_supervisor_grpc_endpoint(
gateway_port,
docker_guest_tls_configured(&docker_config),
network,
);
info!(
grpc_endpoint = %docker_config.grpc_endpoint,
?network,
"Auto-detected Docker supervisor gRPC endpoint"
);
}
Url::parse(&docker_config.grpc_endpoint).map_err(|error| {
Expand Down Expand Up @@ -6861,9 +6868,67 @@ fn docker_guest_tls_configured(docker_config: &DockerComputeConfig) -> bool {
docker_config.guest_tls_ca.is_some()
}

fn default_docker_supervisor_grpc_endpoint(gateway_port: u16, tls: bool) -> String {
/// Where the host-networked supervisor container can reach a gateway that
/// listens on loopback.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DockerSupervisorNetwork {
/// The container host network shares the gateway's loopback: Docker
/// Engine on the gateway host, or Docker Desktop with host networking
/// on the machine that runs the gateway.
GatewayLoopback,
/// The gateway runs inside a WSL 2 distribution and the daemon is Docker
/// Desktop. Docker Desktop's host network is its own VM, not the
/// distribution, so loopback does not reach the gateway. WSL forwards
/// the gateway's loopback listener to Windows, which Docker Desktop
/// exposes to containers as `host.docker.internal`.
WslDockerDesktop,
}

impl DockerSupervisorNetwork {
fn detect(info: &SystemInfo, gateway_in_wsl: bool) -> Self {
if gateway_in_wsl && docker_info_reports_docker_desktop(info) {
Self::WslDockerDesktop
} else {
Self::GatewayLoopback
}
}

fn gateway_host(self) -> &'static str {
match self {
Self::GatewayLoopback => "127.0.0.1",
Self::WslDockerDesktop => HOST_DOCKER_INTERNAL,
}
}
}

fn default_docker_supervisor_grpc_endpoint(
gateway_port: u16,
tls: bool,
network: DockerSupervisorNetwork,
) -> String {
let scheme = if tls { "https" } else { "http" };
format!("{scheme}://127.0.0.1:{gateway_port}")
let host = network.gateway_host();
format!("{scheme}://{host}:{gateway_port}")
}

/// Docker Desktop reports `OperatingSystem` as `Docker Desktop`, or with a
/// variant suffix such as `Docker Desktop (containerized)` (4.71, WSL 2).
fn docker_info_reports_docker_desktop(info: &SystemInfo) -> bool {
info.operating_system
.as_deref()
.is_some_and(|os| os.trim().to_ascii_lowercase().starts_with("docker desktop"))
}

/// Whether the gateway process itself runs inside WSL. This is about the
/// gateway host, not the daemon: Docker Engine installed inside a WSL
/// distribution shares the gateway's loopback and needs no alias.
fn gateway_host_is_wsl() -> bool {
std::fs::read_to_string("/proc/sys/kernel/osrelease")
.is_ok_and(|release| kernel_release_is_wsl(&release))
}

fn kernel_release_is_wsl(release: &str) -> bool {
release.to_ascii_lowercase().contains("microsoft")
}

fn default_docker_bridge_endpoint(
Expand Down
69 changes: 67 additions & 2 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3237,16 +3237,81 @@ fn docker_supervisor_healthcheck_probes_fast_only_during_startup() {

#[test]
fn docker_supervisor_defaults_to_the_primary_loopback_endpoint() {
let network = DockerSupervisorNetwork::GatewayLoopback;
assert_eq!(
default_docker_supervisor_grpc_endpoint(17_670, false),
default_docker_supervisor_grpc_endpoint(17_670, false, network),
"http://127.0.0.1:17670"
);
assert_eq!(
default_docker_supervisor_grpc_endpoint(17_670, true),
default_docker_supervisor_grpc_endpoint(17_670, true, network),
"https://127.0.0.1:17670"
);
}

#[test]
fn docker_supervisor_reaches_wsl_gateway_through_docker_desktop_host_alias() {
let network = DockerSupervisorNetwork::WslDockerDesktop;
assert_eq!(
default_docker_supervisor_grpc_endpoint(17_670, true, network),
"https://host.docker.internal:17670"
);
}

fn docker_desktop_wsl2_info() -> SystemInfo {
SystemInfo {
kernel_version: Some("6.6.87.2-microsoft-standard-WSL2".to_string()),
operating_system: Some("Docker Desktop".to_string()),
name: Some("docker-desktop".to_string()),
..Default::default()
}
}

#[test]
fn docker_supervisor_network_uses_host_alias_for_wsl_gateway_on_docker_desktop() {
assert_eq!(
DockerSupervisorNetwork::detect(&docker_desktop_wsl2_info(), true),
DockerSupervisorNetwork::WslDockerDesktop
);
// Docker Desktop 4.71 on WSL 2 reports a variant suffix.
let containerized = SystemInfo {
operating_system: Some("Docker Desktop (containerized)".to_string()),
..docker_desktop_wsl2_info()
};
assert_eq!(
DockerSupervisorNetwork::detect(&containerized, true),
DockerSupervisorNetwork::WslDockerDesktop
);
}

#[test]
fn docker_supervisor_network_keeps_loopback_for_docker_desktop_outside_wsl() {
// macOS and Windows-native gateways share Docker Desktop's host network.
assert_eq!(
DockerSupervisorNetwork::detect(&docker_desktop_wsl2_info(), false),
DockerSupervisorNetwork::GatewayLoopback
);
}

#[test]
fn docker_supervisor_network_keeps_loopback_for_docker_engine_inside_wsl() {
let info = SystemInfo {
kernel_version: Some("6.6.87.2-microsoft-standard-WSL2".to_string()),
operating_system: Some("Ubuntu 24.04.4 LTS".to_string()),
..Default::default()
};
assert_eq!(
DockerSupervisorNetwork::detect(&info, true),
DockerSupervisorNetwork::GatewayLoopback
);
}

#[test]
fn kernel_release_detects_wsl() {
assert!(kernel_release_is_wsl("6.18.40.1-microsoft-standard-WSL2\n"));
assert!(kernel_release_is_wsl("4.4.0-19041-Microsoft"));
assert!(!kernel_release_is_wsl("6.8.0-60-generic"));
}

#[test]
fn docker_supervisor_honors_operator_network_mode() {
for (requested, expected) in [
Expand Down
2 changes: 2 additions & 0 deletions docs/how-it-works/gateways/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,8 @@ supervisor_network_mode = "host"
# Optional override; Docker's host address is discovered automatically.
# host_gateway_ip = "172.29.0.254"
# Optional override. Omit for automatic host or bridge callback selection.
# Host mode uses host.docker.internal when the gateway runs inside WSL 2
# with Docker Desktop.
# grpc_endpoint = "https://gateway.example.com:17670"
# Workload-side runtime. Defaults to the gateway version.
# sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:<version>"
Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works/sandboxes/runtimes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Common options in `[openshell.drivers.docker]` are `socket_path`, `grpc_endpoint

`supervisor_network_mode` defaults to `host`. Set `bridge` to use Docker bridge networking; `auto` also selects host. The workload always retains `network=none`.

Host mode on Docker Desktop requires host networking enabled. Bridge mode automatically uses `host.docker.internal` for a gateway on the Desktop host. On native Linux, the gateway and host services must listen on bridge-reachable addresses. Set `grpc_endpoint` for remote gateways, remapped published ports, or other custom layouts. Endpoint selection preserves TLS verification and does not change the gateway's listener. See [Docker configuration](/how-it-works/gateways/configuration#docker).
Host mode on Docker Desktop requires host networking enabled. When the gateway runs inside WSL 2 with Docker Desktop, host mode connects sandboxes to the gateway through `host.docker.internal` automatically. Bridge mode automatically uses `host.docker.internal` for a gateway on the Desktop host. On native Linux, the gateway and host services must listen on bridge-reachable addresses. Set `grpc_endpoint` for remote gateways, remapped published ports, or other custom layouts. Endpoint selection preserves TLS verification and does not change the gateway's listener. See [Docker configuration](/how-it-works/gateways/configuration#docker).

OpenShell discovers Docker's host address automatically in bridge mode. Set `host_gateway_ip` only to override discovery.

Expand Down
Loading