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:
+57
-15
@@ -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
|
||||
|
||||
# ── 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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user