feat: add host network support for V1 DockerSandboxService (#12445)

Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
This commit is contained in:
MkDev11
2026-01-28 11:55:38 -05:00
committed by GitHub
parent 570ab904f6
commit c483c80a3c
2 changed files with 399 additions and 34 deletions

View File

@@ -84,6 +84,7 @@ class DockerSandboxService(SandboxService):
extra_hosts: dict[str, str] = field(default_factory=dict)
docker_client: docker.DockerClient = field(default_factory=get_docker_client)
startup_grace_seconds: int = STARTUP_GRACE_SECONDS
use_host_network: bool = False
def _find_unused_port(self) -> int:
"""Find an unused port on the host machine."""
@@ -140,36 +141,61 @@ class DockerSandboxService(SandboxService):
env = self._get_container_env_vars(container)
session_api_key = env.get(SESSION_API_KEY_VARIABLE)
# Get the first exposed port mapping
# Get the exposed port mappings
exposed_urls = []
port_bindings = container.attrs.get('NetworkSettings', {}).get('Ports', {})
if port_bindings:
for container_port, host_bindings in port_bindings.items():
if host_bindings:
host_port = host_bindings[0]['HostPort']
exposed_port = next(
(
exposed_port
for exposed_port in self.exposed_ports
if container_port
== f'{exposed_port.container_port}/tcp'
),
None,
# Check if container is using host network mode
network_mode = container.attrs.get('HostConfig', {}).get('NetworkMode', '')
is_host_network = network_mode == 'host'
if is_host_network:
# Host network mode: container ports are directly accessible on host
for exposed_port in self.exposed_ports:
host_port = exposed_port.container_port
url = self.container_url_pattern.format(port=host_port)
# VSCode URLs require the api_key and working dir
if exposed_port.name == VSCODE:
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
exposed_urls.append(
ExposedUrl(
name=exposed_port.name,
url=url,
port=host_port,
)
if exposed_port:
url = self.container_url_pattern.format(port=host_port)
# VSCode URLs require the api_key and working dir
if exposed_port.name == VSCODE:
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
exposed_urls.append(
ExposedUrl(
name=exposed_port.name,
url=url,
port=host_port,
)
)
else:
# Bridge network mode: use port bindings
port_bindings = container.attrs.get('NetworkSettings', {}).get(
'Ports', {}
)
if port_bindings:
for container_port, host_bindings in port_bindings.items():
if host_bindings:
host_port = int(host_bindings[0]['HostPort'])
matching_port = next(
(
ep
for ep in self.exposed_ports
if container_port == f'{ep.container_port}/tcp'
),
None,
)
if matching_port:
url = self.container_url_pattern.format(port=host_port)
# VSCode URLs require the api_key and working dir
if matching_port.name == VSCODE:
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
exposed_urls.append(
ExposedUrl(
name=matching_port.name,
url=url,
port=host_port,
)
)
return SandboxInfo(
id=container.name,
@@ -300,6 +326,15 @@ class DockerSandboxService(SandboxService):
self, sandbox_spec_id: str | None = None, sandbox_id: str | None = None
) -> SandboxInfo:
"""Start a new sandbox."""
# Warn about port collision risk when using host network mode with multiple sandboxes
if self.use_host_network and self.max_num_sandboxes > 1:
_logger.warning(
'Host network mode is enabled with max_num_sandboxes > 1. '
'Multiple sandboxes will attempt to bind to the same ports, '
'which may cause port collision errors. Consider setting '
'max_num_sandboxes=1 when using host network mode.'
)
# Enforce sandbox limits by cleaning up old sandboxes
await self.pause_old_sandboxes(self.max_num_sandboxes - 1)
@@ -335,12 +370,20 @@ class DockerSandboxService(SandboxService):
env_vars[ALLOW_CORS_ORIGINS_VARIABLE] = self.web_url
# Prepare port mappings and add port environment variables
port_mappings = {}
for exposed_port in self.exposed_ports:
host_port = self._find_unused_port()
port_mappings[exposed_port.container_port] = host_port
# Add port as environment variable
env_vars[exposed_port.name] = str(host_port)
# When using host network, container ports are directly accessible on the host
# so we use the container ports directly instead of mapping to random host ports
port_mappings: dict[int, int] | None = None
if self.use_host_network:
# Host network mode: container ports are directly accessible
for exposed_port in self.exposed_ports:
env_vars[exposed_port.name] = str(exposed_port.container_port)
else:
# Bridge network mode: map container ports to random host ports
port_mappings = {}
for exposed_port in self.exposed_ports:
host_port = self._find_unused_port()
port_mappings[exposed_port.container_port] = host_port
env_vars[exposed_port.name] = str(host_port)
# Prepare labels
labels = {
@@ -356,6 +399,12 @@ class DockerSandboxService(SandboxService):
for mount in self.mounts
}
# Determine network mode
network_mode = 'host' if self.use_host_network else None
if self.use_host_network:
_logger.info(f'Starting sandbox {container_name} with host network mode')
try:
# Create and start the container
container = self.docker_client.containers.run( # type: ignore[call-overload]
@@ -374,7 +423,12 @@ class DockerSandboxService(SandboxService):
init=True,
# Allow agent-server containers to resolve host.docker.internal
# and other custom hostnames for LAN deployments
extra_hosts=self.extra_hosts if self.extra_hosts else None,
# Note: extra_hosts is not needed with host network mode
extra_hosts=self.extra_hosts
if self.extra_hosts and not self.use_host_network
else None,
# Network mode: 'host' for host networking, None for default bridge
network_mode=network_mode,
)
sandbox_info = await self._container_to_sandbox_info(container)
@@ -526,6 +580,21 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
'before it is considered an error'
),
)
use_host_network: bool = Field(
default=os.getenv('SANDBOX_USE_HOST_NETWORK', '').lower()
in (
'true',
'1',
'yes',
),
description=(
'Whether to use host networking mode for sandbox containers. '
'When enabled, containers share the host network namespace, '
'making all container ports directly accessible on the host. '
'This is useful for reverse proxy setups where dynamic port mapping '
'is problematic. Configure via OH_SANDBOX_USE_HOST_NETWORK environment variable.'
),
)
async def inject(
self, state: InjectorState, request: Request | None = None
@@ -558,4 +627,5 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
web_url=web_url,
extra_hosts=self.extra_hosts,
startup_grace_seconds=self.startup_grace_seconds,
use_host_network=self.use_host_network,
)