Skip to content

Commit 3062b7e

Browse files
committed
fix(docker): reach a WSL 2 gateway through Docker Desktop's host alias
The Docker driver defaulted the supervisor's gateway endpoint to 127.0.0.1. When the gateway runs inside a WSL 2 distribution and the daemon is Docker Desktop, the host-networked supervisor runs in Docker Desktop's VM, whose loopback is not the distribution's, so every sandbox failed with ControlSupervisorStartFailed. When grpc_endpoint is unset, detect that combination from the gateway host's kernel release and the daemon's reported operating system, and default to host.docker.internal, which Docker Desktop routes to the Windows host that WSL forwards the gateway's loopback listener from. The generated server certificate already includes that name. Docker Engine inside WSL and Docker Desktop outside WSL keep the loopback default, and an explicit grpc_endpoint still wins. Closes #3880 Signed-off-by: fede-kamel <fkamelhar@gmail.com>
1 parent 0ea0d31 commit 3062b7e

5 files changed

Lines changed: 144 additions & 12 deletions

File tree

‎crates/openshell-driver-docker/README.md‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,19 @@ the supervisor companion. The workload never receives the sandbox JWT, gateway
168168
client TLS key, policy authority, or interception CA private key.
169169

170170
When no endpoint is configured, the supervisor connects to
171-
`127.0.0.1:<gateway-port>`. Set `grpc_endpoint` when the gateway is not on the
172-
Docker daemon host. A configured HTTPS server certificate must include the
173-
endpoint host in its subject alternative names.
174-
175-
The driver publishes host loopback as the backend address for
176-
`host.openshell.internal`. Policy DNS resolves that reserved name through the
171+
`127.0.0.1:<gateway-port>`. When the gateway runs inside a WSL 2 distribution
172+
and the daemon is Docker Desktop, the default is
173+
`host.docker.internal:<gateway-port>` instead: Docker Desktop's host network is
174+
its own VM, and WSL forwards the gateway's loopback listener to the Windows
175+
host that `host.docker.internal` names. Set `grpc_endpoint` when the gateway is
176+
not on the Docker daemon host. A configured HTTPS server certificate must
177+
include the endpoint host in its subject alternative names; the generated
178+
certificate includes `host.docker.internal`.
179+
180+
When the endpoint is an IP address or `localhost`, the driver publishes that
181+
address as the backend address for `host.openshell.internal`. A named endpoint,
182+
including the WSL 2 Docker Desktop default, is left to Docker's name
183+
resolution and does not pin `host.openshell.internal`. Policy DNS resolves that reserved name through the
177184
mediated path, so policies can reach host services without a Docker bridge,
178185
container DNS alias, or another gateway listener.
179186

‎crates/openshell-driver-docker/src/lib.rs‎

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -883,9 +883,16 @@ impl DockerComputeDriver {
883883
let gateway_port = gateway_bind_address.port();
884884
let mut docker_config = docker_config.clone();
885885
if docker_config.grpc_endpoint.trim().is_empty() {
886+
let network = DockerSupervisorNetwork::detect(&info, gateway_host_is_wsl());
886887
docker_config.grpc_endpoint = default_docker_supervisor_grpc_endpoint(
887888
gateway_port,
888889
docker_guest_tls_configured(&docker_config),
890+
network,
891+
);
892+
info!(
893+
grpc_endpoint = %docker_config.grpc_endpoint,
894+
?network,
895+
"Auto-detected Docker supervisor gRPC endpoint"
889896
);
890897
}
891898
Url::parse(&docker_config.grpc_endpoint).map_err(|error| {
@@ -6509,9 +6516,65 @@ fn docker_guest_tls_configured(docker_config: &DockerComputeConfig) -> bool {
65096516
&& docker_config.guest_tls_key.is_some()
65106517
}
65116518

6512-
fn default_docker_supervisor_grpc_endpoint(gateway_port: u16, tls: bool) -> String {
6519+
/// Where the host-networked supervisor container can reach a gateway that
6520+
/// listens on loopback.
6521+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6522+
enum DockerSupervisorNetwork {
6523+
/// The container host network shares the gateway's loopback: Docker
6524+
/// Engine on the gateway host, or Docker Desktop with host networking
6525+
/// on the machine that runs the gateway.
6526+
GatewayLoopback,
6527+
/// The gateway runs inside a WSL 2 distribution and the daemon is Docker
6528+
/// Desktop. Docker Desktop's host network is its own VM, not the
6529+
/// distribution, so loopback does not reach the gateway. WSL forwards
6530+
/// the gateway's loopback listener to Windows, which Docker Desktop
6531+
/// exposes to containers as `host.docker.internal`.
6532+
WslDockerDesktop,
6533+
}
6534+
6535+
impl DockerSupervisorNetwork {
6536+
fn detect(info: &SystemInfo, gateway_in_wsl: bool) -> Self {
6537+
if gateway_in_wsl && docker_info_reports_docker_desktop(info) {
6538+
Self::WslDockerDesktop
6539+
} else {
6540+
Self::GatewayLoopback
6541+
}
6542+
}
6543+
6544+
fn gateway_host(self) -> &'static str {
6545+
match self {
6546+
Self::GatewayLoopback => "127.0.0.1",
6547+
Self::WslDockerDesktop => HOST_DOCKER_INTERNAL,
6548+
}
6549+
}
6550+
}
6551+
6552+
fn default_docker_supervisor_grpc_endpoint(
6553+
gateway_port: u16,
6554+
tls: bool,
6555+
network: DockerSupervisorNetwork,
6556+
) -> String {
65136557
let scheme = if tls { "https" } else { "http" };
6514-
format!("{scheme}://127.0.0.1:{gateway_port}")
6558+
let host = network.gateway_host();
6559+
format!("{scheme}://{host}:{gateway_port}")
6560+
}
6561+
6562+
fn docker_info_reports_docker_desktop(info: &SystemInfo) -> bool {
6563+
info.operating_system
6564+
.as_deref()
6565+
.is_some_and(|os| os.trim().eq_ignore_ascii_case("docker desktop"))
6566+
}
6567+
6568+
/// Whether the gateway process itself runs inside WSL. This is about the
6569+
/// gateway host, not the daemon: Docker Engine installed inside a WSL
6570+
/// distribution shares the gateway's loopback and needs no alias.
6571+
fn gateway_host_is_wsl() -> bool {
6572+
std::fs::read_to_string("/proc/sys/kernel/osrelease")
6573+
.is_ok_and(|release| kernel_release_is_wsl(&release))
6574+
}
6575+
6576+
fn kernel_release_is_wsl(release: &str) -> bool {
6577+
release.to_ascii_lowercase().contains("microsoft")
65156578
}
65166579

65176580
pub(crate) fn docker_guest_tls_paths(

‎crates/openshell-driver-docker/src/tests.rs‎

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,16 +2932,77 @@ fn docker_supervisor_leaves_named_gateway_hosts_to_dns() {
29322932

29332933
#[test]
29342934
fn docker_supervisor_defaults_to_the_primary_loopback_endpoint() {
2935+
let network = DockerSupervisorNetwork::GatewayLoopback;
29352936
assert_eq!(
2936-
default_docker_supervisor_grpc_endpoint(17_670, false),
2937+
default_docker_supervisor_grpc_endpoint(17_670, false, network),
29372938
"http://127.0.0.1:17670"
29382939
);
29392940
assert_eq!(
2940-
default_docker_supervisor_grpc_endpoint(17_670, true),
2941+
default_docker_supervisor_grpc_endpoint(17_670, true, network),
29412942
"https://127.0.0.1:17670"
29422943
);
29432944
}
29442945

2946+
#[test]
2947+
fn docker_supervisor_reaches_wsl_gateway_through_docker_desktop_host_alias() {
2948+
let network = DockerSupervisorNetwork::WslDockerDesktop;
2949+
assert_eq!(
2950+
default_docker_supervisor_grpc_endpoint(17_670, true, network),
2951+
"https://host.docker.internal:17670"
2952+
);
2953+
// A named endpoint is resolved by Docker Desktop, not pinned by the driver.
2954+
assert_eq!(
2955+
docker_supervisor_host_aliases("https://host.docker.internal:17670"),
2956+
None
2957+
);
2958+
}
2959+
2960+
fn docker_desktop_wsl2_info() -> SystemInfo {
2961+
SystemInfo {
2962+
kernel_version: Some("6.6.87.2-microsoft-standard-WSL2".to_string()),
2963+
operating_system: Some("Docker Desktop".to_string()),
2964+
name: Some("docker-desktop".to_string()),
2965+
..Default::default()
2966+
}
2967+
}
2968+
2969+
#[test]
2970+
fn docker_supervisor_network_uses_host_alias_for_wsl_gateway_on_docker_desktop() {
2971+
assert_eq!(
2972+
DockerSupervisorNetwork::detect(&docker_desktop_wsl2_info(), true),
2973+
DockerSupervisorNetwork::WslDockerDesktop
2974+
);
2975+
}
2976+
2977+
#[test]
2978+
fn docker_supervisor_network_keeps_loopback_for_docker_desktop_outside_wsl() {
2979+
// macOS and Windows-native gateways share Docker Desktop's host network.
2980+
assert_eq!(
2981+
DockerSupervisorNetwork::detect(&docker_desktop_wsl2_info(), false),
2982+
DockerSupervisorNetwork::GatewayLoopback
2983+
);
2984+
}
2985+
2986+
#[test]
2987+
fn docker_supervisor_network_keeps_loopback_for_docker_engine_inside_wsl() {
2988+
let info = SystemInfo {
2989+
kernel_version: Some("6.6.87.2-microsoft-standard-WSL2".to_string()),
2990+
operating_system: Some("Ubuntu 24.04.4 LTS".to_string()),
2991+
..Default::default()
2992+
};
2993+
assert_eq!(
2994+
DockerSupervisorNetwork::detect(&info, true),
2995+
DockerSupervisorNetwork::GatewayLoopback
2996+
);
2997+
}
2998+
2999+
#[test]
3000+
fn kernel_release_detects_wsl() {
3001+
assert!(kernel_release_is_wsl("6.18.40.1-microsoft-standard-WSL2\n"));
3002+
assert!(kernel_release_is_wsl("4.4.0-19041-Microsoft"));
3003+
assert!(!kernel_release_is_wsl("6.8.0-60-generic"));
3004+
}
3005+
29453006
#[test]
29463007
fn build_container_create_body_limits_writable_runtime_storage_to_supervisor_ca() {
29473008
let create_body = build_container_create_body(&test_sandbox(), &runtime_config()).unwrap();

‎docs/how-it-works/gateways/configuration.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,8 @@ image_pull_policy = "if_not_present"
911911
# Value assigned to the openshell.sandbox_namespace label on sandbox containers.
912912
sandbox_label = "docker-dev"
913913
# Optional override. When omitted, the host-networked supervisor uses the
914-
# gateway's primary loopback endpoint.
914+
# gateway's primary loopback endpoint, or host.docker.internal when the
915+
# gateway runs inside WSL 2 with Docker Desktop.
915916
grpc_endpoint = "https://127.0.0.1:17670"
916917
# Workload-side runtime. Defaults to the gateway version.
917918
# sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:<version>"

‎docs/how-it-works/sandboxes/runtimes.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ socket_path = "/var/run/docker.sock"
9494

9595
Common options in `[openshell.drivers.docker]` are `socket_path`, `grpc_endpoint`, `sandbox_runtime_image`, `supervisor_image`, `image_pull_policy`, and `sandbox_pids_limit`. When `socket_path` is unset, the driver uses the socket found by auto-detection.
9696

97-
Docker Desktop must have host networking enabled, and it cannot use Enhanced Container Isolation. Set `grpc_endpoint` when sandboxes cannot reach the gateway on host loopback. For GPU sandboxes, configure Docker CDI before starting the gateway.
97+
Docker Desktop must have host networking enabled, and it cannot use Enhanced Container Isolation. When the gateway runs inside WSL 2 with Docker Desktop, the driver connects sandboxes to the gateway through `host.docker.internal` automatically. Set `grpc_endpoint` when sandboxes cannot reach the gateway on host loopback. For GPU sandboxes, configure Docker CDI before starting the gateway.
9898

9999
### Docker Corporate Proxy Egress
100100

0 commit comments

Comments
 (0)