Skip to content

feat: support plain HTTP forward proxy for private IP endpoints #155

@johntmyers

Description

@johntmyers

Problem Statement

The sandbox proxy only handles CONNECT tunneling. When a client sets HTTP_PROXY and makes a plain http:// request, standard HTTP libraries (Python's requests, httpx, aiohttp, Go's net/http, etc.) send a forward proxy request — not a CONNECT tunnel. The proxy rejects this with 403 Forbidden, forcing users to write custom CONNECT tunnel code (raw sockets, manual HTTP framing) for plain HTTP calls to internal services. This is a poor developer experience and a recurring source of confusion.

Technical Context

The sandbox proxy (crates/navigator-sandbox/src/proxy.rs) is CONNECT-tunnel-only. At line 199, any non-CONNECT method is immediately rejected with 403. For plain HTTP targets (e.g., internal tool servers on private IPs), this means the standard proxy behavior that every HTTP library implements — forward proxy requests like GET http://host/path HTTP/1.1 — doesn't work. Users must either write custom CONNECT tunnel code or use our sandbox_http.py wrapper, both of which are fragile and non-obvious.

The proxy already has the infrastructure to handle this: OPA policy evaluation, SSRF checks with allowed_ips override, and L7 relay/inspection. The missing piece is dispatching non-CONNECT requests through that same pipeline.

Affected Components

Component Key Files Role
Sandbox proxy crates/navigator-sandbox/src/proxy.rs Main proxy server — CONNECT-only dispatch, SSRF checks, OPA eval
L7 relay crates/navigator-sandbox/src/l7/relay.rs, l7/rest.rs HTTP request parsing, forwarding, and inspection
OPA engine crates/navigator-sandbox/src/opa.rs Policy evaluation for network decisions
Policy schema architecture/security-policy.md Documents endpoint config including allowed_ips

Proposed Approach

Add forward proxy support restricted to private IP endpoints that are explicitly allowed by policy. When the proxy receives a non-CONNECT request (e.g., GET http://10.86.8.223:8000/path), parse the absolute-form URI, run the same OPA + SSRF pipeline as CONNECT, and if the destination is a private IP with allowed_ips configured, connect upstream, rewrite the request to origin-form, and relay the response. Reject if the destination is public (plain HTTP should never traverse the public internet), if the scheme is https:// (use CONNECT), or if no policy matches.

No policy schema changes are needed — allowed_ips already serves as both the SSRF override and the opt-in signal for private endpoint access.

Scope Assessment

  • Complexity: Medium
  • Confidence: High — clear path, reuses existing infrastructure
  • Estimated files to change: 2-3 (proxy.rs primarily, minor touches to l7/rest.rs)
  • Issue type: feat

Risks & Open Questions

  • Connection reuse / keep-alive: Should the forward proxy support HTTP keep-alive (multiple request-response cycles on one connection), or close after each response? Keep-alive is more performant but adds complexity. L7 relay already handles multi-request loops, so reuse may come naturally.
  • Request body forwarding: Need to handle Content-Length and Transfer-Encoding: chunked on the forwarded request. The L7 rest provider already has relay_fixed and relay_chunked — can these be reused?
  • Hop-by-hop headers: Per RFC 7230, the proxy should strip hop-by-hop headers (Connection, Proxy-Authorization, etc.) when forwarding. Need to implement this correctly.
  • Via header: RFC 7230 §5.7 recommends adding a Via header. Minor but good practice.

Test Considerations

  • Unit tests for URI parsing (parse_proxy_uri) and request rewriting (rewrite_forward_request)
  • Integration tests: forward proxy to allowed private endpoint (200), to public IP (403), to private IP without allowed_ips (403), to unmatched policy (403), HTTPS forward proxy (403)
  • Integration test with L7 inspection: access: read-only allows GET, denies POST
  • Regression: all existing CONNECT tunnel behavior unchanged

Created by spike investigation. Use build-from-issue to plan and implement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:policyPolicy engine and policy lifecycle workarea:sandboxSandbox runtime and isolation workarea:supervisorProxy and routing-path workspikestate:agent-readyApproved for agent implementationstate:pr-openedPR has been opened for this issuestate:review-readyReady for human review

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions