Make SOCKS5 exit IP detection robust

This commit is contained in:
2026-05-28 21:31:42 +08:00
parent 661078c3bf
commit dd7cc18c88
+63 -16
View File
@@ -373,35 +373,82 @@ async def socks5_connect(proxy: dict[str, Any], timeout: float) -> None:
with contextlib.suppress(Exception): await writer.wait_closed() with contextlib.suppress(Exception): await writer.wait_closed()
async def get_exit_ip_via_socks(proxy: dict[str, Any], timeout: float) -> str | None: async def http_get_via_socks(proxy: dict[str, Any], timeout: float, host: str, path: str = "/") -> str:
"""Fetch the public egress IP through the SOCKS5 proxy without failing health checks.""" reader, writer = await socks5_open_connection(proxy, timeout, host, 80)
reader, writer = await socks5_open_connection(proxy, timeout, "api.ipify.org", 80)
try: try:
req = b"GET /?format=text HTTP/1.1\r\nHost: api.ipify.org\r\nUser-Agent: socks5-monitor/1.0\r\nConnection: close\r\n\r\n" req = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
"User-Agent: socks5-monitor/1.0\r\n"
"Accept: text/plain, application/json;q=0.9, */*;q=0.1\r\n"
"Connection: close\r\n\r\n"
).encode("ascii")
writer.write(req); await writer.drain() writer.write(req); await writer.drain()
raw = await asyncio.wait_for(reader.read(8192), timeout=timeout) chunks = []
text = raw.decode("utf-8", "replace") deadline = asyncio.get_running_loop().time() + timeout
body = text.split("\r\n\r\n", 1)[-1].strip() while True:
m = re.search(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", body) remaining = max(0.1, deadline - asyncio.get_running_loop().time())
if m: if remaining <= 0.1 and chunks:
return m.group(0) break
m = re.search(r"\b[0-9a-fA-F:]{3,}\b", body) try:
return m.group(0) if m else None chunk = await asyncio.wait_for(reader.read(4096), timeout=remaining)
except asyncio.TimeoutError:
break
if not chunk:
break
chunks.append(chunk)
if sum(len(x) for x in chunks) >= 65536:
break
raw = b"".join(chunks)
return raw.decode("utf-8", "replace")
finally: finally:
writer.close() writer.close()
with contextlib.suppress(Exception): await writer.wait_closed() with contextlib.suppress(Exception): await writer.wait_closed()
def extract_ip(text: str) -> str | None:
body = text.split("\r\n\r\n", 1)[-1].strip()
m = re.search(r"\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b", body)
if m:
return m.group(0)
# Very small IPv6 matcher, enough for plain text IP services.
m = re.search(r"\b(?:[0-9a-fA-F]{1,4}:){2,}[0-9a-fA-F]{0,4}\b", body)
return m.group(0) if m else None
async def get_exit_ip_via_socks(proxy: dict[str, Any], timeout: float) -> str | None:
"""Fetch the public egress IP through the SOCKS5 proxy without failing health checks."""
endpoints = [
("api.ipify.org", "/?format=text"),
("ipv4.icanhazip.com", "/"),
("ifconfig.me", "/ip"),
("ident.me", "/"),
("checkip.amazonaws.com", "/"),
]
errors = []
per_endpoint_timeout = max(2.0, min(float(timeout), 6.0))
for host, path in endpoints:
try:
text = await http_get_via_socks(proxy, per_endpoint_timeout, host, path)
ip = extract_ip(text)
if ip:
return ip
errors.append(f"{host}: no ip in response")
except Exception as e:
errors.append(f"{host}: {e}")
proxy["last_exit_ip_error"] = "; ".join(errors[-3:])
return None
async def check_proxy(proxy: dict[str, Any]) -> tuple[bool, int | None, str | None, str | None]: async def check_proxy(proxy: dict[str, Any]) -> tuple[bool, int | None, str | None, str | None]:
timeout = float(proxy.get("timeout_seconds", 8)) timeout = float(proxy.get("timeout_seconds", 8))
start = time.perf_counter() start = time.perf_counter()
try: try:
await socks5_connect(proxy, timeout) await socks5_connect(proxy, timeout)
latency = int((time.perf_counter() - start) * 1000) latency = int((time.perf_counter() - start) * 1000)
exit_ip = None exit_ip = await get_exit_ip_via_socks(proxy, timeout)
with contextlib.suppress(Exception): err = None if exit_ip else proxy.get("last_exit_ip_error")
exit_ip = await get_exit_ip_via_socks(proxy, timeout) return True, latency, err, exit_ip
return True, latency, None, exit_ip
except Exception as e: except Exception as e:
return False, int((time.perf_counter() - start) * 1000), str(e), None return False, int((time.perf_counter() - start) * 1000), str(e), None