zozo123 opened a new pull request, #74504:
URL: https://github.com/apache/airflow/pull/74504

   **Behaviour change.** On an OpenSandbox server with the default `[egress] 
mode = "dns"`, `OpenSandboxBackend` now refuses a deny-all or allowlist 
`SandboxSpec` at create, including the default `SandboxSpec()`. The error names 
the fix: set `[egress] mode = "dns+nft"` in the server config, or pass 
`SandboxSpec(block_network=False)` for a sandbox that may reach the network. 
The backend is Experimental, so the stability policy allows this in a minor 
release. The changelog note under the `Changelog` header announces it, as 
`stability.rst` requires.
   
   **Why.** `dns` is the server's default (`EgressConfig.mode`) and is what 
every example config ships. In that mode the egress sidecar filters name 
resolution and nothing else, so a "no network" sandbox can still connect to any 
numeric address. `_verify_network_policy` passed anyway, because 
`SandboxSync.get_egress_policy()` returns the same rules in `dns` and `dns+nft` 
mode. That breaks the `SandboxSpec` contract that a backend which cannot 
enforce a field must raise rather than silently ignore it.
   
   **Release context.** apache-airflow-providers-common-ai 0.11.0 (RC vote 
#74459, ends 2026-10-11 14:47 UTC) ships the backend without this check. The 
caveat is on the RC thread: 
https://github.com/apache/airflow/issues/74459#issuecomment-6064052952. This 
lands in the next release.
   
   **Two questions a reviewer will ask:**
   - *Why use the SDK's generated `opensandbox.api.egress` client?* The 
sidecar's `GET /policy` response carries `enforcementMode`, but the high-level 
`get_egress_policy()` keeps only the rules and drops that field.
   - *Why a separate client?* It is a short-lived client, closed after one 
request, so it never touches the SDK's shared transport. Non-200 responses go 
through the SDK's own `handle_api_error` and `require_parsed`, so an HTTP error 
is reported as one. A missing or unreadable mode also fails closed.
   
   `allow_egress_to_cidrs` is still refused. Its message no longer claims that 
the mode cannot be read. Supporting CIDRs is possible now, but it needs 
Modal-style entry validation, so it is left for a follow-up.
   
   **Measured** against OpenSandbox server 0.2.3 with egress sidecar v1.1.7, 
running on Docker (colima), with `opensandbox==1.1.0`. Each sandbox was created 
through `OpenSandboxBackend.create()`.
   
   What a sandbox can reach with the backend as it is on `main`, which is 
byte-identical to 0.11.0rc1:
   
   | From inside the sandbox | `dns`, deny-all | `dns`, allow `pypi.org` | 
`dns+nft`, deny-all | `dns+nft`, allow `pypi.org` |
   |---|---|---|---|---|
   | TCP `1.1.1.1:443`, `8.8.8.8:443` | connects | connects | times out | times 
out |
   | HTTP `GET 1.1.1.1:80` | `301` from Cloudflare | `301` from Cloudflare | 
times out | times out |
   | HTTPS `HEAD pypi.org/simple/` | fails (no DNS) | `200` | fails (no DNS) | 
`200` |
   
   In another `dns`-mode deny-all sandbox created the same way, the system 
resolver could not look up `example.com`. A DNS-over-HTTPS query to 
`https://1.1.1.1/dns-query` returned its address, and `GET 
https://example.com/` over that address returned `200 OK` with a verified 
certificate. So the DNS-only filter is easy to get around.
   
   With this PR:
   - **`dns+nft` server:** deny-all and `allow_egress_to=["pypi.org"]` are 
accepted, with the same reachability as the table above. The system test 
`example_sandbox_toolset_opensandbox.py` succeeds in 7.30 s. On 2026-10-08, 
against `dns+nft` with 0.11.0rc1, the timeout, kill and cleanup edge cases also 
passed.
   - **`dns` server:** deny-all is refused after 2.6 s and the allowlist after 
2.0 s, each with the error above. No sandbox container is left behind.
   
   Tests: `test_opensandbox.py` has 82 passing tests. The sidecar's `/policy` 
response is served by patching `httpx.Client.send`, so the generated client and 
its parsing run for real. The new and changed cases cover `dns`, an unreported 
mode, an unreachable sidecar, an HTTP 500, the request's URL, API-key header 
and timeout, and the CIDR message. All of them fail against the code on `main`. 
mypy is clean on both files.
   
   related: #71676, #74459
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes (please specify the tool below)
   
   Generated-by: Claude Code (Opus 5.5) following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   
   ---
   
   * Read the **[Pull Request 
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
 for more information. Note: commit author/co-author name and email in commits 
become permanently public when merged.
   * For fundamental code changes, an Airflow Improvement Proposal 
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
 is needed.
   * When adding dependency, check compliance with the [ASF 3rd Party License 
Policy](https://www.apache.org/legal/resolved.html#category-x).
   * For significant user-facing changes create newsfragment: 
`{pr_number}.significant.rst`, in 
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
 You can add this file in a follow-up commit after the PR is created so you 
know the PR number.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to