Skip to content

Conversation

@mjdaly
Copy link

@mjdaly mjdaly commented Jan 31, 2026

Summary

  • Adds ProxyChannel subclass of grpclib.client.Channel that tunnels gRPC connections through an HTTP CONNECT proxy
  • Adds grpc_proxy config setting (MODAL_GRPC_PROXY env var) with fallback to standard HTTPS_PROXY / https_proxy environment variables
  • Only applies to HTTPS connections; HTTP and Unix socket connections bypass the proxy

Motivation

Modal's Python client uses grpclib (pure-Python async gRPC) which calls asyncio.loop.create_connection() directly — with zero HTTP proxy support. Users behind corporate proxies or firewalls cannot connect to api.modal.com:443.

Approach

grpclib's Channel has no custom connection factory parameter. ProxyChannel overrides _create_connection() to:

  1. Open raw TCP to the proxy
  2. Send HTTP CONNECT handshake (with optional Basic auth)
  3. Validate 200 response
  4. Pass the tunneled socket to loop.create_connection(ssl=..., sock=...) for TLS + H2

Configuration priority

MODAL_GRPC_PROXY > TOML grpc_proxy > HTTPS_PROXY > https_proxy > None

Files changed

File Change
modal/config.py Added grpc_proxy setting + HTTPS_PROXY env var fallback + docstring
modal/_utils/grpc_utils.py Added ProxyChannel class, modified create_channel() to use it
test/proxy_channel_test.py 15 tests covering config priority, channel routing, CONNECT handshake, auth, error handling

Test plan

  • Config priority: MODAL_GRPC_PROXY > HTTPS_PROXY > https_proxy > None (4 tests)
  • Channel routing: ProxyChannel for https+proxy, standard Channel for http/unix/no-proxy (4 tests)
  • ProxyChannel construction: URL parsing, auth encoding, default port (3 tests)
  • CONNECT handshake: correct request format, auth header, 407 rejection, connection closed (4 tests)
  • Existing config tests pass (no regression)

🤖 Generated with Claude Code

Users behind corporate proxies/firewalls can now connect to Modal's API
by setting MODAL_GRPC_PROXY, HTTPS_PROXY, or https_proxy environment
variables. The proxy connection uses HTTP CONNECT tunneling through a
ProxyChannel subclass of grpclib's Channel.

Env var priority: MODAL_GRPC_PROXY > TOML config > HTTPS_PROXY > https_proxy

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@cursor
Copy link

cursor bot commented Jan 31, 2026

PR Summary

Medium Risk
Touches connection setup for all HTTPS gRPC calls and relies on grpclib private attributes, so regressions could impact client connectivity in proxied and non-proxied environments.

Overview
Adds HTTP CONNECT proxy support for HTTPS gRPC by introducing ProxyChannel and routing create_channel() through it when grpc_proxy/proxy env vars are set, while honoring NO_PROXY (including IPv6 and port-specific matching) and performing basic validation to avoid CONNECT header injection.

Introduces a new grpc_proxy configuration option (MODAL_GRPC_PROXY) with fallback to HTTPS_PROXY/https_proxy, redacts proxy credentials in modal config show, and adds a comprehensive test suite covering config precedence, proxy bypass rules, CONNECT handshake behavior, and edge cases.

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

mjdaly and others added 2 commits January 31, 2026 22:06
- Use self._loop instead of asyncio.get_event_loop() (consistency with grpclib base)
- Respect self._config.ssl_target_name_override for server_hostname
- Validate raw proxy URL for CRLF before urlparse (header injection prevention)
- Reject non-http proxy URL schemes (e.g. socks5://)
- Add bounded response buffer (16 KiB) and 30s timeout on all socket ops
- Decode status line as ASCII with error replacement
- Fix socket ownership: set to None after transport takes it, suppress OSError on close
- Strip whitespace from HTTPS_PROXY/https_proxy env var fallback
- Remove redundant `import socket as _socket`
- Fix docstring: "HTTPS gRPC connections" (not "all")
- Add tests for scheme validation, CRLF rejection, whitespace env var handling

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…vements

Refactor _create_connection into two methods with a single outer
asyncio.wait_for timeout, add IPv6 proxy resolution via getaddrinfo,
percent-decode proxy credentials, add NO_PROXY/no_proxy bypass support,
and improve test reliability by eliminating TOCTOU port races and
ensuring server cleanup via try/finally. Adds 6 new test cases.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Raise OSError instead of modal ConnectionError from ProxyChannel so
  get_or_create_channel's except OSError handler wraps proxy failures
  into user-friendly messages consistently
- Wrap DNS resolution inside connect timeout (extract _resolve_and_connect)
  so hanging resolvers don't exceed _CONNECT_TIMEOUT
- Make server_hostname conditional on SSL to prevent ValueError when
  ssl=None
- Reject missing proxy hostname instead of defaulting to localhost
- Add host whitespace/tab validation and port range (1..65535) check
- Move sock.setblocking(False) inside try block to prevent socket leak
- Add __repr__ to ProxyChannel that masks auth credentials
- Redact proxy URL credentials in modal config show
- Update NO_PROXY docstring to match actual behavior (always honored)
- Add tests: DNS timeout, getaddrinfo failure, IPv6 proxy URL, input
  validation, credential redaction, __repr__ masking, monkeypatch timeout

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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 2 potential issues.

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

- Prevent double-bracketing of already-bracketed IPv6 hosts in CONNECT
  request (e.g., [2001:db8::1] no longer becomes [[2001:db8::1]])
- Catch asyncio.TimeoutError alongside TimeoutError for Python 3.10
  compatibility (asyncio.TimeoutError became alias of TimeoutError in 3.11)
- Preserve IPv6 brackets in _redact_url_credentials output so redacted
  URLs like http://***@[::1]:3128 remain valid
- Add tests: IPv6 double-bracket prevention, IPv6 redaction preservation

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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