diff --git a/entrypoint.py b/entrypoint.py index fb7bb18..a7110de 100644 --- a/entrypoint.py +++ b/entrypoint.py @@ -30,7 +30,7 @@ DNS_SERVER = os.environ.get("DNS_SERVER", "tls://8.8.8.8") PICK_STRATEGY = os.environ.get("PICK_STRATEGY", "first") LOG_LEVEL = os.environ.get("LOG_LEVEL", "info") -CONFIG_FILE = "/tmp/sub2socks.json" +CONFIG_FILE = "/tmp/.sub2socks.json" def _fetch(url): @@ -89,6 +89,40 @@ def _parse(url_str): } +def _make_dns_server(addr_str): + """Parse common DNS address strings into native sing-box v1.13+ server dict. + + Supports: udp://host:port, tls://host(:port), https://host(:port), bare IP/hostname. + """ + addr = addr_str.strip() + + # Strip scheme to find protocol type + parts = up.urlparse(addr) + scheme = parts.scheme.lower() if parts.scheme else "" + + host = parts.hostname or addr + port = parts.port or 53 + + # Default TLS SNI (DNS-over-TLS requires server_name) + sni_map = { + "8.8.8.8": "dns.google", + "8.8.4.4": "dns.google", + "1.1.1.1": "cloudflare-dns.com", + "1.0.0.1": "cloudflare-dns.com", + } + + obj = {"type": scheme or "udp", "server": host, "server_port": port} + + if scheme == "tls": + obj["tls"] = {"server_name": sni_map.get(host, host)} + elif scheme == "https": + port = port or 443 + obj["server_port"] = port + obj["tls"] = {"server_name": host} + + return obj + + def _config(proxy): """Build sing-box JSON config.""" ob = { @@ -124,34 +158,45 @@ def _config(proxy): ob["tls"]["alpn"] = [x.strip() for x in alpn_raw.split(",") if x.strip()] ttype = proxy.get("type") or "tcp" - tp = {"type": ttype} + # Plain TCP is the default in sing-box; only add transport block for non-tcp types if ttype == "ws": - tp["path"] = proxy.get("path") or "/" + tp = {"type": "ws", "path": proxy.get("path") or "/"} hv = proxy.get("host") if hv: tp["headers"] = {"Host": hv} + ob["transport"] = tp elif ttype == "http": + tp = {"type": "http"} hv = proxy.get("host") if hv: tp["host"] = [x.strip() for x in hv.split(",") if x.strip()] tp["path"] = proxy.get("path") or "/" + ob["transport"] = tp elif ttype == "grpc": + tp = {"type": "grpc"} svc = proxy.get("serviceName") or proxy.get("path") if svc: tp["service_name"] = svc + ob["transport"] = tp + elif ttype == "httpupgrade": + tp = {"type": "httpupgrade", "path": proxy.get("path") or "/"} + hv = proxy.get("host") + if hv: + tp["headers"] = {"Host": hv} + ob["transport"] = tp - ob["transport"] = tp + # ── Build native v1.13+ DNS server object ────────────────────── + dns_server_obj = _make_dns_server(DNS_SERVER) cfg = { "log": {"level": LOG_LEVEL}, "dns": { "servers": [ - {"tag": "remote", "address": DNS_SERVER, "detour": "direct"}, - ], - "rules": [ - {"outbound": "any", "server": "remote"}, + {**dns_server_obj, "tag": "remote"}, + {"type": "local", "tag": "local"}, ], + "final": "remote", }, "inbounds": [{ "type": "socks", @@ -160,9 +205,10 @@ def _config(proxy): }], "outbounds": [ ob, # index 0 -> tag: proxy - {"type": "direct", "tag": "direct"}, # DNS detour / bypass + {"type": "direct", "tag": "direct"}, # bypass ], "route": { + "default_domain_resolver": "remote", "rules": [{ "clash_mode": "direct", "outbound": "direct", @@ -220,15 +266,11 @@ def main(): print(f" Config written -> {CONFIG_FILE}", flush=True) - # exec sing-box in-place - env_copy = dict(os.environ) - env_copy["ENABLE_DEPRECATED_LEGACY_DNS_SERVERS"] = "true" - env_copy["ENABLE_DEPRECATED_OUTBOUND_DNS_RULE_ITEM"] = "true" - + # exec sing-box in-place (native v1.13+ DNS, no shim required) os.execve( "/usr/local/bin/sing-box", ["sing-box", "run", "-c", CONFIG_FILE], - env_copy, + dict(os.environ), ) diff --git a/test_proxy.py b/test_proxy.py new file mode 100644 index 0000000..3951be3 --- /dev/null +++ b/test_proxy.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Comprehensive SOCKS5 proxy test — run inside sub2socks container.""" +import socket +import struct +import time + +print("=" * 60) +print(" Sub2SOCKS Live Proxy Test Suite") +print("=" * 60) + +# Test 1: SOCKS5 handshake +print("\n[1] SOCKS5 Handshake") +try: + s = socket.create_connection(("127.0.0.1", 2080), timeout=15) + s.sendall(b"\x05\x01\x00") + resp = s.recv(2) + ok = (resp == b"\x05\x00") + print(f" Response: {resp.hex():>6} -> {'PASS' if ok else 'FAIL'}") +except Exception as e: + print(f" ERROR: {e}") + ok = None + +# Test 2: CONNECT requests +print("\n[2] SOCKS5 CONNECT Targets") +targets = [ + ("api.ipify.org", 443, "ipify API"), + ("httpbin.org", 443, "httpbin "), + ("google.com", 80, "Google HTTP "), + ("cloudflare.com", 443, "Cloudflare "), +] + +for host, port, label in targets: + try: + s2 = socket.create_connection(("127.0.0.1", 2080), timeout=20) + s2.sendall(b"\x05\x01\x00") + s2.recv(2) + + hbytes = host.encode() + req = (b"\x05\x01\x00\x03" + + bytes([len(hbytes)]) + + hbytes + + struct.pack("!H", port)) + + t0 = time.monotonic() + s2.sendall(req) + conn_resp = s2.recv(8) + elapsed = (time.monotonic() - t0) * 1000 + status_byte = conn_resp[1] + if status_byte == 0: + print(f" {label:13s} :{port:<4} SUCCESS ({elapsed:.0f} ms)") + else: + print(f" {label:13s} :{port:<4} FAIL (code={status_byte})") + s2.close() + except socket.timeout: + print(f" {label:13s} :{port:<4} TIMEOUT") + except Exception as e: + print(f" {label:13s} :{port:<4} ERROR: {e}") + +# Test 3: Process health +print("\n[3] Process health") +try: + with open("/proc/1/cmdline", "rb") as f: + cmdline = f.read().decode("utf-8", errors="replace").replace("\x00", " ") + has_sb = "sing-box" in cmdline + print(f" PID 1: {'sing-box OK' if has_sb else cmdline[:80]}") +except Exception as e: + print(f" ERROR reading /proc/1: {e}") + +print("\n" + "=" * 60) +print(" Tests complete") +print("=" * 60)