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:
user
2026-02-19 11:54:30 +01:00
parent ced6232373
commit 41ba680dcb
2 changed files with 54 additions and 15 deletions

3
.gitignore vendored
View File

@@ -15,3 +15,6 @@ build/
.pytest_cache/
.mypy_cache/
*.log
# Personal config (keep example only)
config/bouncer.toml

View File

@@ -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