diff --git a/server/configuration.md b/server/configuration.md index c175979f5..f892c65e1 100644 --- a/server/configuration.md +++ b/server/configuration.md @@ -204,6 +204,7 @@ Configures the **egress sidecar** image and enforcement mode. The server only at | `image` | string \| omitted | `null` | OCI image for the egress sidecar. **Required in config** when clients send **`networkPolicy`** (create request). | | `mode` | string | `"dns"` | Passed to the sidecar as `OPENSANDBOX_EGRESS_MODE`. Values: **`dns`** — DNS-proxy-based enforcement (CIDR/static IP rules **not** enforced); **`dns+nft`** — adds nftables where available so **CIDR/IP** rules can be enforced. | | `disable_ipv6` | bool | `true` | IPv6 egress is incomplete (especially on Kubernetes). **Default on**; set `false` only when you want IPv6 left up in the netns. Details in [IPv6 and egress](#ipv6-and-egress) below. | +| `timeout_seconds` | float | `30.0` | **Docker only.** Maximum time to wait for the egress sidecar health endpoint to become ready. Must be greater than `0`. | ### IPv6 and egress @@ -213,6 +214,7 @@ OpenSandbox egress does **not** treat IPv6 as a first-class, fully covered path - `egress.image` must be set when using `networkPolicy`. - Outbound policy requires **`docker.network_mode = "bridge"`**; `networkPolicy` is rejected for incompatible network modes. +- Increase `egress.timeout_seconds` when the sidecar needs more than 30 seconds to become ready in the deployment environment. **Kubernetes notes:** diff --git a/server/docker-compose.example.yaml b/server/docker-compose.example.yaml index 17b0ea316..dfe9e67c9 100644 --- a/server/docker-compose.example.yaml +++ b/server/docker-compose.example.yaml @@ -16,6 +16,7 @@ configs: [egress] image = "opensandbox/egress:v1.1.6" # image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.1.6" + timeout_seconds = 30.0 [docker] network_mode = "bridge" @@ -64,4 +65,4 @@ services: networks: opensandbox-net: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/server/opensandbox_server/config.py b/server/opensandbox_server/config.py index 717bc3e7e..121063e03 100644 --- a/server/opensandbox_server/config.py +++ b/server/opensandbox_server/config.py @@ -751,6 +751,14 @@ class EgressConfig(BaseModel): "(e.g. IPv4-only CNI or experimenting with IPv6 egress despite gaps)." ), ) + timeout_seconds: float = Field( + default=30.0, + gt=0, + description=( + "Maximum time in seconds to wait for the egress sidecar health endpoint " + "to become ready in Docker runtime." + ), + ) class RuntimeConfig(BaseModel): diff --git a/server/opensandbox_server/examples/example.config.toml b/server/opensandbox_server/examples/example.config.toml index 6a053058d..5ebe854eb 100644 --- a/server/opensandbox_server/examples/example.config.toml +++ b/server/opensandbox_server/examples/example.config.toml @@ -70,6 +70,7 @@ mode = "direct" [egress] image = "opensandbox/egress:v1.1.6" mode = "dns" +timeout_seconds = 30.0 # Renew-on-access. Off by default — see server/README.md. [renew_intent] diff --git a/server/opensandbox_server/examples/example.config.zh.toml b/server/opensandbox_server/examples/example.config.zh.toml index 080681478..e51fe474a 100644 --- a/server/opensandbox_server/examples/example.config.zh.toml +++ b/server/opensandbox_server/examples/example.config.zh.toml @@ -68,6 +68,7 @@ mode = "direct" [egress] image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.1.6" mode = "dns" +timeout_seconds = 30.0 # 按访问续期。默认关闭 — 见 server/README_zh.md。 [renew_intent] diff --git a/server/opensandbox_server/services/docker/networking.py b/server/opensandbox_server/services/docker/networking.py index 7509c788a..d0391a1ed 100644 --- a/server/opensandbox_server/services/docker/networking.py +++ b/server/opensandbox_server/services/docker/networking.py @@ -511,6 +511,7 @@ def build_sidecar_host_config(*, include_ipv6_sysctls: bool) -> Any: sandbox_id, egress_api_host_port, egress_token, + timeout_seconds=self.app_config.egress.timeout_seconds, ) return sidecar_container except Exception as exc: @@ -549,7 +550,7 @@ def _wait_for_egress_sidecar_ready( sandbox_id: str, host_port: int, egress_token: str, - timeout_seconds: float = 30.0, + timeout_seconds: float, ) -> None: deadline = time.monotonic() + timeout_seconds url = f"http://{self._resolve_proxy_host()}:{host_port}/healthz" diff --git a/server/tests/test_config.py b/server/tests/test_config.py index a450c0a1c..65c614003 100644 --- a/server/tests/test_config.py +++ b/server/tests/test_config.py @@ -891,10 +891,41 @@ def test_egress_config_mode_literal(): base = EgressConfig(image="opensandbox/egress:v1") assert base.mode == EGRESS_MODE_DNS assert base.disable_ipv6 is True + assert base.timeout_seconds == 30.0 cfg = EgressConfig(image="opensandbox/egress:v1", mode=EGRESS_MODE_DNS_NFT) assert cfg.mode == EGRESS_MODE_DNS_NFT +def test_egress_config_timeout_must_be_positive(): + cfg = EgressConfig(timeout_seconds=75.5) + assert cfg.timeout_seconds == 75.5 + + with pytest.raises(ValidationError): + EgressConfig(timeout_seconds=0) + + +def test_load_config_with_egress_timeout(tmp_path, monkeypatch): + _reset_config(monkeypatch) + toml = textwrap.dedent( + """ + [runtime] + type = "docker" + execd_image = "opensandbox/execd:test" + + [egress] + image = "opensandbox/egress:test" + timeout_seconds = 75.5 + """ + ) + config_path = tmp_path / "config.toml" + config_path.write_text(toml) + + loaded = config_module.load_config(config_path) + + assert loaded.egress is not None + assert loaded.egress.timeout_seconds == 75.5 + + def test_log_config_defaults(): """LogConfig should have sensible defaults.""" cfg = LogConfig() @@ -1555,4 +1586,3 @@ def test_env_secure_access_active_key_must_exist(self, tmp_path, monkeypatch) -> with pytest.raises(ValidationError, match="not found in secure_access.keys"): config_module.load_config(config_path) - diff --git a/server/tests/test_docker_service.py b/server/tests/test_docker_service.py index fd8699213..125b214be 100644 --- a/server/tests/test_docker_service.py +++ b/server/tests/test_docker_service.py @@ -809,7 +809,7 @@ def host_cfg_side_effect(**kwargs): cfg = _app_config() cfg.docker.network_mode = "bridge" - cfg.egress = EgressConfig(image="egress:latest") + cfg.egress = EgressConfig(image="egress:latest", timeout_seconds=75.5) service = DockerSandboxService(config=cfg) req = CreateSandboxRequest( @@ -829,14 +829,19 @@ def host_cfg_side_effect(**kwargs): return_value={ "44772": ("0.0.0.0", 44772), "8080": ("0.0.0.0", 8080), + "18080": ("0.0.0.0", 18080), }, ), patch.object(service, "_ensure_image_available"), patch.object(service, "_prepare_sandbox_runtime"), - patch.object(service, "_wait_for_egress_sidecar_ready"), + patch.object(service, "_wait_for_egress_sidecar_ready") as wait_for_egress_ready, ): await service.create_sandbox(req) + wait_for_egress_ready.assert_called_once() + assert wait_for_egress_ready.call_args.args[1:] == (18080, "egress-token") + assert wait_for_egress_ready.call_args.kwargs == {"timeout_seconds": 75.5} + assert len(mock_client.api.create_container.call_args_list) == 2 sidecar_call = mock_client.api.create_container.call_args_list[0] main_call = mock_client.api.create_container.call_args_list[1]