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
2 changes: 2 additions & 0 deletions server/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:**

Expand Down
3 changes: 2 additions & 1 deletion server/docker-compose.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -64,4 +65,4 @@ services:

networks:
opensandbox-net:
driver: bridge
driver: bridge
8 changes: 8 additions & 0 deletions server/opensandbox_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,14 @@ class EgressConfig(BaseModel):
"(e.g. IPv4-only CNI or experimenting with IPv6 egress despite gaps)."
),
)
timeout_seconds: float = Field(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking): timeout_seconds is a bit generic — the [egress] section may later grow other timeouts (e.g., per-request API timeouts), and this one specifically bounds the readiness wait. Since the PR is not merged yet, renaming is nearly free. Something like ready_timeout_seconds (matches _wait_for_egress_sidecar_ready) or readiness_timeout_seconds (K8s readinessProbe terminology) would make the intent explicit. If renamed, remember to update networking.py, server/configuration.md, both example TOMLs, docker-compose.example.yaml, and the test assertions.

default=30.0,
gt=0,
Comment on lines +754 to +756

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the configured timeout during each health probe

When timeout_seconds is set below one second, which this field explicitly permits, a slow health endpoint can still block for the hard-coded one-second urlopen timeout and then sleep another 0.2 seconds. Consequently, the operation can substantially exceed the documented maximum; cap each probe and sleep to the remaining deadline, or reject values below the polling granularity.

AGENTS.md reference: server/AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

description=(
"Maximum time in seconds to wait for the egress sidecar health endpoint "
"to become ready in Docker runtime."
),
)


class RuntimeConfig(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions server/opensandbox_server/examples/example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion server/opensandbox_server/services/docker/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
32 changes: 31 additions & 1 deletion server/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

9 changes: 7 additions & 2 deletions server/tests/test_docker_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]
Expand Down
Loading