Skip to content

Conversation

@raiden-staging
Copy link
Contributor

@raiden-staging raiden-staging commented Feb 9, 2026

[feature] pac granulary proxy

granular proxy setting via api

  • api-managed pac support, with edit & toggle
  • os level via transparent traffic interception

implementation

  • new server endpoints:
    • PUT /chromium/proxy/pac
    • GET /chromium/proxy/pac
    • GET /chromium/proxy/pac/script
  • proxying details:
    • iptables nat OUTPUT redirect tcp/80 and tcp/443 to local transparent proxy (pac-proxy, port 15080)
    • pac-proxy evaluates pac per-request and forwards direct/proxy accordingly

usage

# set pac content + enable
curl -X PUT http://localhost:444/chromium/proxy/pac \
  -H 'content-type: application/json' \
  -d '{
    "enabled": true,
    "content": "function FindProxyForURL(url, host) { return \"PROXY 172.17.0.1:18080\"; }"
  }'

# status
curl http://localhost:444/chromium/proxy/pac

# read pac script
curl http://localhost:444/chromium/proxy/pac/script

demos

tested on local docker container

proxy image replacement

proxy to intercept+override images requests

demo_pac_images.mp4

additional : proxy setup snippets

# /tmp/pac_mitm_addon.py used in demo upstream proxy (mitmproxy addon)
from mitmproxy import http
from urllib.parse import urlparse

IMAGE_PATH = "/addons/image.png"

with open(IMAGE_PATH, "rb") as f:
    IMAGE_BYTES = f.read()

IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg", ".avif", ".ico")

def _looks_like_image(url: str, ctype: str) -> bool:
    p = urlparse(url)
    path = p.path.lower()
    return any(path.endswith(ext) for ext in IMAGE_EXTS) or (ctype or "").lower().startswith("image/")

def response(flow: http.HTTPFlow) -> None:
    if flow.response is None:
        return

    if flow.request.method.upper() != "GET":
        return

    url = flow.request.pretty_url
    ctype = flow.response.headers.get("content-type", "")

    if not _looks_like_image(url, ctype):
        return

    flow.response = http.Response.make(
        200,
        IMAGE_BYTES,
        {
            "Content-Type": "image/png",
            "Cache-Control": "no-store",
        },
    )
# demo upstream proxy process
docker run -d --name pac-upstream \
  -p 18080:8080 \
  -v /tmp/pac_mitm_addon.py:/addons/pac.py:ro \
  -v /tmp/pac_rep_png.png:/addons/image.png:ro \
  mitmproxy/mitmproxy:10.3.0 \
  mitmdump -p 8080 -s /addons/pac.py

note : cert

  • custom CA trust is needed when for HTTPS interception with a MITM proxy.
  • optional image support exists via PAC_MITM_CA_CERT_PATH (wrapper.sh) to pre-import CA into chromium NSS before launch.

[ @rgarcia ]


Note

High Risk
Adds OS-level traffic interception via iptables and a new transparent proxy process, which can impact all outbound networking and is sensitive to container permissions and runtime environment differences.

Overview
Adds API-managed PAC (Proxy Auto-Configuration) support for Chromium with new endpoints GET/PUT /chromium/proxy/pac and GET /chromium/proxy/pac/script, including persisted PAC content/state under /chromium and optional Chromium restart on updates.

Implements OS-level enforcement by introducing a new pac-proxy daemon and iptables rules to transparently redirect outbound TCP 80/443 (and block UDP/443 QUIC) to a local proxy, and wires this into the chromium-headful image (new binary build/copy, pacproxy user, supervisord service, required packages).

Hardens Chromium startup by serializing /chromium config writes, switching launcher to run Chromium under dbus-run-session, cleaning crash-restore artifacts, and optionally importing a custom MITM CA into Chromium’s NSS DB before launch.

Written by Cursor Bugbot for commit a209737. This will update automatically on new commits. Configure here.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

// Draining resp.Body here can block indefinitely and stall the TLS handshake.
if resp.Body != nil {
resp.Body.Close()
}
Copy link

Choose a reason for hiding this comment

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

CONNECT response body Close blocks TLS tunnel indefinitely

High Severity

In handleTLSViaHTTPProxy, after a successful CONNECT 2xx response, resp.Body.Close() is called. Go's http.ReadResponse for a CONNECT 2xx with no Content-Length header sets the body length to -1 (unknown), so Close() tries to drain the body by reading until EOF from the proxy connection. Since the proxy is waiting for the client's TLS ClientHello through the tunnel, this read blocks indefinitely and tunnel() is never reached. The comment on line 293–294 even acknowledges this risk, but the code still calls Close(). All HTTPS requests routed through an HTTP proxy will hang.

Fix in Cursor Fix in Web

r.mu.Lock()
r.cache[key] = decision
r.mu.Unlock()
return decision
Copy link

Choose a reason for hiding this comment

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

PAC cache key ignores URL path causing wrong routing

Medium Severity

The resolve cache key is scheme://host (line 67), but evalPAC is called with the full rawURL (line 76). PAC scripts receive the complete URL in FindProxyForURL(url, host) and can make routing decisions based on path or query parameters. The host-only cache key causes the first request's routing decision to be reused for all subsequent requests to the same host, silently returning wrong results for path-based PAC rules.

Fix in Cursor Fix in Web

return &value
}
return nil
}
Copy link

Choose a reason for hiding this comment

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

Unused function extractPACFlagValue is dead code

Low Severity

The unexported function extractPACFlagValue is defined but never called anywhere in the codebase. Grep confirms the only match is the definition itself. Since it's unexported, it can only be used within the api package, and no caller exists. This is dead code that adds maintenance burden.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant