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:
@@ -15,3 +15,6 @@ build/
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Personal config (keep example only)
|
||||||
|
config/bouncer.toml
|
||||||
|
|||||||
+42
-6
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
from python_socks.async_.asyncio import Proxy
|
from python_socks.async_.asyncio import Proxy
|
||||||
@@ -13,6 +14,26 @@ from bouncer.config import ProxyConfig
|
|||||||
log = logging.getLogger(__name__)
|
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(
|
async def connect(
|
||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
@@ -21,14 +42,27 @@ async def connect(
|
|||||||
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
||||||
"""Open a TCP connection through the SOCKS5 proxy.
|
"""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.
|
Returns an (asyncio.StreamReader, asyncio.StreamWriter) pair.
|
||||||
If tls=True, the connection is wrapped in SSL after the SOCKS5 handshake.
|
If tls=True, the connection is wrapped in SSL after the SOCKS5 handshake.
|
||||||
"""
|
"""
|
||||||
|
addrs = await _resolve_all(host, port)
|
||||||
|
last_err: Exception | None = None
|
||||||
|
|
||||||
|
for dest_ip in addrs:
|
||||||
proxy = Proxy.from_url(f"socks5://{proxy_cfg.host}:{proxy_cfg.port}")
|
proxy = Proxy.from_url(f"socks5://{proxy_cfg.host}:{proxy_cfg.port}")
|
||||||
|
log.debug(
|
||||||
log.debug("connecting to %s:%d via socks5://%s:%d", host, port, proxy_cfg.host, proxy_cfg.port)
|
"trying %s (%s):%d via socks5://%s:%d",
|
||||||
|
host, dest_ip, port, proxy_cfg.host, proxy_cfg.port,
|
||||||
sock = await proxy.connect(dest_host=host, dest_port=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
|
||||||
|
|
||||||
ssl_ctx: ssl.SSLContext | None = None
|
ssl_ctx: ssl.SSLContext | None = None
|
||||||
if tls:
|
if tls:
|
||||||
@@ -37,10 +71,12 @@ async def connect(
|
|||||||
reader, writer = await asyncio.open_connection(
|
reader, writer = await asyncio.open_connection(
|
||||||
host=None,
|
host=None,
|
||||||
port=None,
|
port=None,
|
||||||
sock=sock.socket,
|
sock=sock,
|
||||||
ssl=ssl_ctx,
|
ssl=ssl_ctx,
|
||||||
server_hostname=host if tls else None,
|
server_hostname=host if tls else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
log.debug("connected to %s:%d (tls=%s)", host, port, tls)
|
log.debug("connected to %s (%s):%d (tls=%s)", host, dest_ip, port, tls)
|
||||||
return reader, writer
|
return reader, writer
|
||||||
|
|
||||||
|
raise OSError(f"all addresses for {host} failed via SOCKS5") from last_err
|
||||||
|
|||||||
Reference in New Issue
Block a user