feat: migrate to sing-box v1.13+ native DNS config

- Replace deprecated dns.servers[] with new format (type + server + server_port)
- Add _make_dns_server() to parse udp://, tls://, https:// and bare IP addresses
- Drop ENABLE_DEPRECATED_* environment variable shims — fully v1.14-ready
- Fix transport block: omit for plain TCP (sing-box default), add httpupgrade support
- Add dns.final fallback and route.default_domain_resolver
- Config file renamed to hidden /tmp/.sub2socks.json (matches README)
- Include test_proxy.py live proxy CONNECT test suite
This commit is contained in:
2026-07-03 19:52:43 +00:00
parent 5cbb3b15ae
commit d74ea6cc69
2 changed files with 128 additions and 15 deletions
+57 -15
View File
@@ -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),
)
+71
View File
@@ -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)