fix: resolve DNS locally and try all IPs via SOCKS5
Many SOCKS5 proxies cannot resolve hostnames reliably. Resolve locally and iterate through all returned addresses until one succeeds. Also exclude personal config from git. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ build/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
*.log
|
||||
|
||||
# Personal config (keep example only)
|
||||
config/bouncer.toml
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from python_socks.async_.asyncio import Proxy
|
||||
@@ -13,6 +14,26 @@ from bouncer.config import ProxyConfig
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _resolve_all(host: str, port: int) -> list[str]:
|
||||
"""Resolve hostname to all IPv4 addresses locally."""
|
||||
loop = asyncio.get_running_loop()
|
||||
infos = await loop.getaddrinfo(
|
||||
host, port, family=socket.AF_INET, type=socket.SOCK_STREAM,
|
||||
)
|
||||
if not infos:
|
||||
raise OSError(f"could not resolve {host}")
|
||||
# Deduplicate while preserving order
|
||||
seen: set[str] = set()
|
||||
addrs: list[str] = []
|
||||
for info in infos:
|
||||
addr = info[4][0]
|
||||
if addr not in seen:
|
||||
seen.add(addr)
|
||||
addrs.append(addr)
|
||||
log.debug("resolved %s -> %s", host, ", ".join(addrs))
|
||||
return addrs
|
||||
|
||||
|
||||
async def connect(
|
||||
host: str,
|
||||
port: int,
|
||||
@@ -21,26 +42,41 @@ async def connect(
|
||||
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
||||
"""Open a TCP connection through the SOCKS5 proxy.
|
||||
|
||||
Resolves hostnames locally and tries all addresses, since many
|
||||
SOCKS5 proxies cannot do remote DNS resolution reliably.
|
||||
|
||||
Returns an (asyncio.StreamReader, asyncio.StreamWriter) pair.
|
||||
If tls=True, the connection is wrapped in SSL after the SOCKS5 handshake.
|
||||
"""
|
||||
proxy = Proxy.from_url(f"socks5://{proxy_cfg.host}:{proxy_cfg.port}")
|
||||
addrs = await _resolve_all(host, port)
|
||||
last_err: Exception | None = None
|
||||
|
||||
log.debug("connecting to %s:%d via socks5://%s:%d", host, port, proxy_cfg.host, proxy_cfg.port)
|
||||
for dest_ip in addrs:
|
||||
proxy = Proxy.from_url(f"socks5://{proxy_cfg.host}:{proxy_cfg.port}")
|
||||
log.debug(
|
||||
"trying %s (%s):%d via socks5://%s:%d",
|
||||
host, dest_ip, port, proxy_cfg.host, proxy_cfg.port,
|
||||
)
|
||||
try:
|
||||
sock = await proxy.connect(dest_host=dest_ip, dest_port=port)
|
||||
except Exception as e:
|
||||
log.debug("failed to connect via %s: %s", dest_ip, e)
|
||||
last_err = e
|
||||
continue
|
||||
|
||||
sock = await proxy.connect(dest_host=host, dest_port=port)
|
||||
ssl_ctx: ssl.SSLContext | None = None
|
||||
if tls:
|
||||
ssl_ctx = ssl.create_default_context()
|
||||
|
||||
ssl_ctx: ssl.SSLContext | None = None
|
||||
if tls:
|
||||
ssl_ctx = ssl.create_default_context()
|
||||
reader, writer = await asyncio.open_connection(
|
||||
host=None,
|
||||
port=None,
|
||||
sock=sock,
|
||||
ssl=ssl_ctx,
|
||||
server_hostname=host if tls else None,
|
||||
)
|
||||
|
||||
reader, writer = await asyncio.open_connection(
|
||||
host=None,
|
||||
port=None,
|
||||
sock=sock.socket,
|
||||
ssl=ssl_ctx,
|
||||
server_hostname=host if tls else None,
|
||||
)
|
||||
log.debug("connected to %s (%s):%d (tls=%s)", host, dest_ip, port, tls)
|
||||
return reader, writer
|
||||
|
||||
log.debug("connected to %s:%d (tls=%s)", host, port, tls)
|
||||
return reader, writer
|
||||
raise OSError(f"all addresses for {host} failed via SOCKS5") from last_err
|
||||
|
||||
Reference in New Issue
Block a user