Skip to content

Commit dca78bd

Browse files
committed
fix(docker): pin Docker Desktop host gateway
Signed-off-by: Senthil Ravichandran <senthilr@nvidia.com>
1 parent eeba0e7 commit dca78bd

2 files changed

Lines changed: 234 additions & 1 deletion

File tree

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

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4547,6 +4547,7 @@ async fn prepare_docker_boundary_files(
45474547
let tls = generate_sandbox_tls_material(session_id)
45484548
.map_err(|error| Status::internal(format!("generate Docker boundary TLS: {error}")))?;
45494549
let verification_keys = gateway_verification_keys(&launch_authentication.verification_keys)?;
4550+
let host_gateway_ip = resolve_docker_supervisor_host_address(docker, sandbox, config).await?;
45504551
let provisioning = isolation::DockerBoundarySpec {
45514552
boundary_id: sandbox.id.clone(),
45524553
generation: launch_authentication
@@ -4575,7 +4576,7 @@ async fn prepare_docker_boundary_files(
45754576
// host-networked supervisor. This is normally loopback, but container
45764577
// CI reaches the gateway and host fixtures through the job
45774578
// container's bridge address.
4578-
host_gateway_ip: docker_supervisor_host_address(&config.supervisor_grpc_endpoint),
4579+
host_gateway_ip,
45794580
workload_identity: workload_identity.clone(),
45804581
child_env: docker_child_environment(sandbox),
45814582
}
@@ -5038,6 +5039,168 @@ fn docker_supervisor_host_address(grpc_endpoint: &str) -> Option<IpAddr> {
50385039
}
50395040
}
50405041

5042+
fn docker_host_gateway_resolver_body(
5043+
sandbox: &DriverSandbox,
5044+
config: &DockerDriverRuntimeConfig,
5045+
) -> ContainerCreateBody {
5046+
ContainerCreateBody {
5047+
image: Some(config.supervisor_image_id.clone()),
5048+
user: Some(format!("{SUPERVISOR_UID}:{SUPERVISOR_GID}")),
5049+
entrypoint: Some(vec![SUPERVISOR_IMAGE_CONTROL_BINARY_PATH.to_string()]),
5050+
cmd: Some(vec!["--help".to_string()]),
5051+
labels: Some(docker_auxiliary_container_labels(
5052+
sandbox,
5053+
config,
5054+
LABEL_ISOLATION_ROLE_STAGING,
5055+
)),
5056+
host_config: Some(HostConfig {
5057+
network_mode: Some("none".to_string()),
5058+
extra_hosts: Some(vec![format!("{HOST_OPEN_SHELL_INTERNAL}:host-gateway")]),
5059+
cap_drop: Some(vec!["ALL".to_string()]),
5060+
security_opt: Some(vec!["no-new-privileges:true".to_string()]),
5061+
readonly_rootfs: Some(true),
5062+
..Default::default()
5063+
}),
5064+
..Default::default()
5065+
}
5066+
}
5067+
5068+
fn parse_docker_host_gateway_hosts(contents: &[u8]) -> CoreResult<IpAddr> {
5069+
let text = std::str::from_utf8(contents).map_err(|error| {
5070+
Error::config(format!(
5071+
"Docker host gateway hosts file is not UTF-8: {error}"
5072+
))
5073+
})?;
5074+
let mut addresses = Vec::new();
5075+
for line in text.lines() {
5076+
let fields = line
5077+
.split_once('#')
5078+
.map_or(line, |(entry, _)| entry)
5079+
.split_whitespace()
5080+
.collect::<Vec<_>>();
5081+
if fields.len() < 2 || !fields[1..].contains(&HOST_OPEN_SHELL_INTERNAL) {
5082+
continue;
5083+
}
5084+
let address = fields[0].parse::<IpAddr>().map_err(|error| {
5085+
Error::config(format!(
5086+
"Docker host gateway hosts entry has an invalid address: {error}"
5087+
))
5088+
})?;
5089+
if !addresses.contains(&address) {
5090+
addresses.push(address);
5091+
}
5092+
}
5093+
5094+
let ipv4 = addresses
5095+
.iter()
5096+
.filter(|address| address.is_ipv4())
5097+
.copied()
5098+
.collect::<Vec<_>>();
5099+
let address = match ipv4.as_slice() {
5100+
[address] => *address,
5101+
[] if addresses.len() == 1 => addresses[0],
5102+
[] => {
5103+
return Err(Error::config(
5104+
"Docker host gateway resolver did not return one concrete address",
5105+
));
5106+
}
5107+
_ => {
5108+
return Err(Error::config(
5109+
"Docker host gateway resolver returned ambiguous IPv4 addresses",
5110+
));
5111+
}
5112+
};
5113+
let normalized = match address {
5114+
IpAddr::V6(value) => value.to_ipv4_mapped().map_or(address, IpAddr::V4),
5115+
IpAddr::V4(_) => address,
5116+
};
5117+
if address.is_unspecified()
5118+
|| address.is_multicast()
5119+
|| normalized == IpAddr::V4(Ipv4Addr::BROADCAST)
5120+
|| normalized == IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254))
5121+
{
5122+
return Err(Error::config(format!(
5123+
"Docker host gateway resolver returned unsafe address '{address}'"
5124+
)));
5125+
}
5126+
Ok(address)
5127+
}
5128+
5129+
async fn resolve_docker_supervisor_host_address(
5130+
docker: &Docker,
5131+
sandbox: &DriverSandbox,
5132+
config: &DockerDriverRuntimeConfig,
5133+
) -> Result<Option<IpAddr>, Status> {
5134+
if let Some(address) = docker_supervisor_host_address(&config.supervisor_grpc_endpoint) {
5135+
return Ok(Some(address));
5136+
}
5137+
let endpoint = Url::parse(&config.supervisor_grpc_endpoint).map_err(|error| {
5138+
Status::failed_precondition(format!("parse Docker supervisor endpoint: {error}"))
5139+
})?;
5140+
if !matches!(endpoint.host(), Some(url::Host::Domain(name)) if name.eq_ignore_ascii_case(HOST_DOCKER_INTERNAL))
5141+
{
5142+
return Ok(None);
5143+
}
5144+
5145+
let resolver_name = temp_extract_container_name();
5146+
let created = docker
5147+
.create_container(
5148+
Some(
5149+
CreateContainerOptionsBuilder::default()
5150+
.name(resolver_name.as_str())
5151+
.build(),
5152+
),
5153+
docker_host_gateway_resolver_body(sandbox, config),
5154+
)
5155+
.await
5156+
.map_err(|error| {
5157+
Status::internal(format!("create Docker host gateway resolver: {error}"))
5158+
})?;
5159+
5160+
let result = async {
5161+
docker
5162+
.start_container(&created.id, None)
5163+
.await
5164+
.map_err(|error| {
5165+
Status::internal(format!("start Docker host gateway resolver: {error}"))
5166+
})?;
5167+
let mut wait = docker.wait_container(
5168+
&created.id,
5169+
None::<bollard::query_parameters::WaitContainerOptions>,
5170+
);
5171+
let status = tokio::time::timeout(Duration::from_secs(10), wait.next())
5172+
.await
5173+
.map_err(|_| Status::deadline_exceeded("Docker host gateway resolver timed out"))?
5174+
.ok_or_else(|| Status::internal("Docker host gateway resolver wait ended"))?
5175+
.map_err(|error| {
5176+
Status::internal(format!("wait for Docker host gateway resolver: {error}"))
5177+
})?;
5178+
if status.status_code != 0 {
5179+
return Err(Status::failed_precondition(format!(
5180+
"Docker host gateway resolver exited with status {}",
5181+
status.status_code
5182+
)));
5183+
}
5184+
let hosts = download_path_from_container(docker, &created.id, "/etc/hosts", true)
5185+
.await
5186+
.map_err(|error| Status::failed_precondition(error.to_string()))?;
5187+
parse_docker_host_gateway_hosts(&hosts)
5188+
.map(Some)
5189+
.map_err(|error| Status::failed_precondition(error.to_string()))
5190+
}
5191+
.await;
5192+
let cleanup = docker
5193+
.remove_container(
5194+
&created.id,
5195+
Some(RemoveContainerOptionsBuilder::default().force(true).build()),
5196+
)
5197+
.await
5198+
.map_err(|error| Status::internal(format!("remove Docker host gateway resolver: {error}")));
5199+
let address = result?;
5200+
cleanup?;
5201+
Ok(address)
5202+
}
5203+
50415204
async fn spawn_docker_control_process(
50425205
docker: &Docker,
50435206
sandbox: &DriverSandbox,

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,27 @@ async fn live_docker_resource_admission_checks_native_volume_labels() {
554554
}
555555
}
556556

557+
#[tokio::test]
558+
#[ignore = "requires a local Docker daemon and supervisor image; creates and removes one confined resolver container"]
559+
async fn live_docker_host_gateway_resolution_uses_daemon_owned_address() {
560+
let image = std::env::var("OPENSHELL_TEST_DOCKER_SUPERVISOR_IMAGE")
561+
.expect("set OPENSHELL_TEST_DOCKER_SUPERVISOR_IMAGE to an existing immutable image");
562+
let mut config = runtime_config();
563+
config.supervisor_image_id = image;
564+
config.supervisor_grpc_endpoint = "https://host.docker.internal:17670".to_string();
565+
let docker = Docker::connect_with_local_defaults().unwrap();
566+
567+
let address = resolve_docker_supervisor_host_address(&docker, &test_sandbox(), &config)
568+
.await
569+
.unwrap()
570+
.expect("Docker Desktop endpoint must resolve to one concrete pin");
571+
572+
assert!(address.is_ipv4());
573+
assert!(!address.is_unspecified());
574+
assert!(!address.is_multicast());
575+
assert_ne!(address, IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254)));
576+
}
577+
557578
type TestDriverClient =
558579
openshell_core::proto::compute::v1::compute_driver_client::ComputeDriverClient<
559580
tonic::transport::Channel,
@@ -3072,6 +3093,55 @@ fn docker_supervisor_leaves_named_gateway_hosts_to_dns() {
30723093
);
30733094
}
30743095

3096+
#[test]
3097+
fn docker_host_gateway_resolver_is_confined_and_uses_engine_pin() {
3098+
let sandbox = test_sandbox();
3099+
let config = runtime_config();
3100+
let body = docker_host_gateway_resolver_body(&sandbox, &config);
3101+
let host = body.host_config.expect("resolver host config");
3102+
3103+
assert_eq!(
3104+
body.image.as_deref(),
3105+
Some(config.supervisor_image_id.as_str())
3106+
);
3107+
assert_eq!(
3108+
body.entrypoint,
3109+
Some(vec![SUPERVISOR_IMAGE_CONTROL_BINARY_PATH.to_string()])
3110+
);
3111+
assert_eq!(body.cmd, Some(vec!["--help".to_string()]));
3112+
assert_eq!(host.network_mode.as_deref(), Some("none"));
3113+
assert_eq!(
3114+
host.extra_hosts,
3115+
Some(vec!["host.openshell.internal:host-gateway".to_string()])
3116+
);
3117+
assert_eq!(host.cap_drop, Some(vec!["ALL".to_string()]));
3118+
assert_eq!(host.cap_add, None);
3119+
assert_eq!(host.readonly_rootfs, Some(true));
3120+
}
3121+
3122+
#[test]
3123+
fn docker_host_gateway_parser_prefers_one_engine_pinned_ipv4_address() {
3124+
let hosts = b"127.0.0.1 localhost\n\
3125+
172.29.0.254 host.openshell.internal\n\
3126+
fdc4:f303:9324::254 host.openshell.internal\n";
3127+
3128+
assert_eq!(
3129+
parse_docker_host_gateway_hosts(hosts).unwrap(),
3130+
"172.29.0.254".parse::<IpAddr>().unwrap()
3131+
);
3132+
}
3133+
3134+
#[test]
3135+
fn docker_host_gateway_parser_rejects_missing_ambiguous_and_metadata_addresses() {
3136+
for hosts in [
3137+
b"127.0.0.1 localhost\n".as_slice(),
3138+
b"172.29.0.254 host.openshell.internal\n172.30.0.254 host.openshell.internal\n".as_slice(),
3139+
b"169.254.169.254 host.openshell.internal\n".as_slice(),
3140+
] {
3141+
assert!(parse_docker_host_gateway_hosts(hosts).is_err());
3142+
}
3143+
}
3144+
30753145
#[test]
30763146
fn docker_supervisor_defaults_to_the_primary_loopback_endpoint() {
30773147
assert_eq!(

0 commit comments

Comments
 (0)