refactor: split monolith into typed modules with retry logic
- models.py: ProxyInfo and DNSServerConfig dataclasses with type hints - fetcher.py: subscription fetcher with exponential backoff retries (3 attempts) plus VLESS URL parser - config_builder.py: sing-box config builder decomposed into focused helpers - main.py: orchestrator with setup_logging(), print_banner(), pick_node()
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sing-box configuration builder."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from models import ProxyInfo
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Default SNI mapping for DNS-over-TLS with IP addresses
|
||||||
|
SNI_MAP: Dict[str, str] = {
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_dns_server(addr_str: str) -> Dict[str, Any]:
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr_str: DNS server address string (e.g. "tls://8.8.8.8", "udp://1.1.1.1:53").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary compatible with sing-box DNS server schema.
|
||||||
|
"""
|
||||||
|
import urllib.parse as up
|
||||||
|
|
||||||
|
stripped = addr_str.strip()
|
||||||
|
parts = up.urlparse(stripped)
|
||||||
|
scheme = parts.scheme.lower() if parts.scheme else ""
|
||||||
|
host = parts.hostname or stripped
|
||||||
|
port = parts.port or 53
|
||||||
|
|
||||||
|
config: Dict[str, Any] = {
|
||||||
|
"type": scheme or "udp",
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
if scheme == "tls":
|
||||||
|
config["tls"] = {"server_name": SNI_MAP.get(host, host)}
|
||||||
|
elif scheme == "https":
|
||||||
|
config["server_port"] = port or 443
|
||||||
|
config["tls"] = {"server_name": host}
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def build_singbox_config(
|
||||||
|
proxy: ProxyInfo,
|
||||||
|
socks_port: int = 1080,
|
||||||
|
socks_user: str = "",
|
||||||
|
socks_pass: str = "",
|
||||||
|
dns_server: str = "tls://8.8.8.8",
|
||||||
|
log_level: str = "info",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Build a complete sing-box JSON configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
proxy: Parsed VLESS proxy information.
|
||||||
|
socks_port: Local SOCKS5 listen port.
|
||||||
|
socks_user: Optional username for SOCKS auth (empty disables auth).
|
||||||
|
socks_pass: Optional password for SOCKS auth.
|
||||||
|
dns_server: Upstream DNS resolver address string.
|
||||||
|
log_level: Sing-box log level.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary representing the full sing-box configuration.
|
||||||
|
"""
|
||||||
|
outbound = _build_proxy_outbound(proxy)
|
||||||
|
inbound = _build_socks_inbound(
|
||||||
|
port=socks_port,
|
||||||
|
username=socks_user,
|
||||||
|
password=socks_pass,
|
||||||
|
)
|
||||||
|
dns = _build_dns_config(dns_server)
|
||||||
|
|
||||||
|
config: Dict[str, Any] = {
|
||||||
|
"log": {"level": log_level},
|
||||||
|
"dns": dns,
|
||||||
|
"inbounds": [inbound],
|
||||||
|
"outbounds": [
|
||||||
|
outbound,
|
||||||
|
{"type": "direct", "tag": "direct"},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"default_domain_resolver": "remote",
|
||||||
|
"rules": [{
|
||||||
|
"clash_mode": "direct",
|
||||||
|
"outbound": "direct",
|
||||||
|
}],
|
||||||
|
"final": "proxy",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Built sing-box config with outbound tag 'proxy'")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _build_proxy_outbound(proxy: ProxyInfo) -> Dict[str, Any]:
|
||||||
|
"""Build the VLESS outbound proxy object."""
|
||||||
|
outbound: Dict[str, Any] = {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "proxy",
|
||||||
|
"server": proxy.server,
|
||||||
|
"server_port": proxy.port,
|
||||||
|
"uuid": proxy.uuid,
|
||||||
|
"packet_encoding": "xudp",
|
||||||
|
}
|
||||||
|
|
||||||
|
security = proxy.security or "tls"
|
||||||
|
|
||||||
|
if security == "reality":
|
||||||
|
outbound["tls"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"server_name": proxy.sni,
|
||||||
|
"utls": {"enabled": True, "fingerprint": "chrome"},
|
||||||
|
"reality": {
|
||||||
|
"enabled": True,
|
||||||
|
"public_key": proxy.pbk,
|
||||||
|
"short_id": proxy.sid or proxy.spx,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
elif security == "tls":
|
||||||
|
tls_config: Dict[str, Any] = {
|
||||||
|
"enabled": True,
|
||||||
|
"server_name": proxy.sni,
|
||||||
|
"utls": {"enabled": True, "fingerprint": "chrome"},
|
||||||
|
}
|
||||||
|
if proxy.alpn:
|
||||||
|
tls_config["alpn"] = [a.strip() for a in proxy.alpn.split(",") if a.strip()]
|
||||||
|
outbound["tls"] = tls_config
|
||||||
|
|
||||||
|
transport = _build_transport(proxy)
|
||||||
|
if transport:
|
||||||
|
outbound["transport"] = transport
|
||||||
|
|
||||||
|
return outbound
|
||||||
|
|
||||||
|
|
||||||
|
def _build_transport(proxy: ProxyInfo) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Build the transport configuration (only for non-TCP types)."""
|
||||||
|
transport_type = proxy.type or "tcp"
|
||||||
|
transport: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
if transport_type == "ws":
|
||||||
|
ws: Dict[str, Any] = {"type": "ws", "path": proxy.path or "/"}
|
||||||
|
if proxy.host:
|
||||||
|
ws["headers"] = {"Host": proxy.host}
|
||||||
|
return ws
|
||||||
|
|
||||||
|
elif transport_type == "http":
|
||||||
|
http_cfg: Dict[str, Any] = {"type": "http"}
|
||||||
|
if proxy.host:
|
||||||
|
http_cfg["host"] = [h.strip() for h in proxy.host.split(",") if h.strip()]
|
||||||
|
http_cfg["path"] = proxy.path or "/"
|
||||||
|
return http_cfg
|
||||||
|
|
||||||
|
elif transport_type == "grpc":
|
||||||
|
grpc: Dict[str, Any] = {"type": "grpc"}
|
||||||
|
svc_name = proxy.serviceName or proxy.path
|
||||||
|
if svc_name:
|
||||||
|
grpc["service_name"] = svc_name
|
||||||
|
return grpc
|
||||||
|
|
||||||
|
elif transport_type == "httpupgrade":
|
||||||
|
hu: Dict[str, Any] = {"type": "httpupgrade", "path": proxy.path or "/"}
|
||||||
|
if proxy.host:
|
||||||
|
hu["headers"] = {"Host": proxy.host}
|
||||||
|
return hu
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_socks_inbound(
|
||||||
|
port: int,
|
||||||
|
username: str = "",
|
||||||
|
password: str = "",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Build the SOCKS5 inbound object."""
|
||||||
|
inbound: Dict[str, Any] = {
|
||||||
|
"type": "socks",
|
||||||
|
"listen": "::",
|
||||||
|
"listen_port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
if username:
|
||||||
|
inbound["users"] = [
|
||||||
|
{"username": username, "password": password},
|
||||||
|
]
|
||||||
|
|
||||||
|
return inbound
|
||||||
|
|
||||||
|
|
||||||
|
def _build_dns_config(dns_server_addr: str) -> Dict[str, Any]:
|
||||||
|
"""Build the DNS section of the config."""
|
||||||
|
dns_server_obj = make_dns_server(dns_server_addr)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"servers": [
|
||||||
|
{**dns_server_obj, "tag": "remote"},
|
||||||
|
{"type": "local", "tag": "local"},
|
||||||
|
],
|
||||||
|
"final": "remote",
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Subscription fetching and VLESS URL parsing."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse as up
|
||||||
|
import urllib.request
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from models import ProxyInfo
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_subscription(url: str, max_retries: int = 3, backoff_base: float = 1.0) -> str:
|
||||||
|
"""Download subscription content with retry and exponential backoff.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: Subscription URL to download.
|
||||||
|
max_retries: Maximum number of retry attempts (default 3).
|
||||||
|
backoff_base: Base delay in seconds for exponential backoff (default 1.0s -> 1, 2, 4).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decoded subscription text content.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
urllib.error.URLError: If all retries fail.
|
||||||
|
Exception: On non-recoverable errors.
|
||||||
|
"""
|
||||||
|
last_error: Optional[Exception] = None
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
logger.info("Fetching subscription from %s (attempt %d/%d)", url, attempt + 1, max_retries)
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "clash.meta"})
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
raw = resp.read()
|
||||||
|
try:
|
||||||
|
content = raw.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
logger.debug("Raw data is base64 encoded, decoding...")
|
||||||
|
content = base64.b64decode(raw).decode("utf-8")
|
||||||
|
|
||||||
|
if attempt > 0:
|
||||||
|
logger.info("Fetch succeeded on attempt %d", attempt + 1)
|
||||||
|
return content
|
||||||
|
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
last_error = e
|
||||||
|
wait_time = backoff_base * (2 ** attempt)
|
||||||
|
logger.warning("Fetch failed (attempt %d/%d): %s. Retrying in %.1fs...",
|
||||||
|
attempt + 1, max_retries, e, wait_time)
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Non-recoverable fetch error: %s", e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.error("All %d fetch attempts failed for %s", max_retries, url)
|
||||||
|
raise last_error # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_vless_lines(text: str) -> List[str]:
|
||||||
|
"""Extract VLESS URI lines from subscription text.
|
||||||
|
|
||||||
|
Handles both plain and base64-encoded line formats.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Raw subscription content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of VLESS URL strings.
|
||||||
|
"""
|
||||||
|
result: List[str] = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
if stripped.lower().startswith("vless://"):
|
||||||
|
result.append(stripped)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
decoded = base64.b64decode(stripped).decode("utf-8")
|
||||||
|
if decoded.lower().startswith("vless://"):
|
||||||
|
result.append(decoded)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_vless_url(url_str: str) -> Dict[str, str]:
|
||||||
|
"""Parse a VLESS URL string into a component dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url_str: VLESS subscription URI (e.g. vless://uuid@host:port?params).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with all parsed proxy fields.
|
||||||
|
"""
|
||||||
|
parsed = up.urlparse(url_str.strip())
|
||||||
|
query_params = up.parse_qs(parsed.query)
|
||||||
|
|
||||||
|
def get_param(key: str, default: Optional[str] = None) -> Optional[str]:
|
||||||
|
if key in query_params:
|
||||||
|
return query_params[key][0]
|
||||||
|
return default
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uuid": parsed.username or "",
|
||||||
|
"server": parsed.hostname or "",
|
||||||
|
"port": parsed.port or (443 if parsed.scheme == "vless+tls" else 80),
|
||||||
|
"security": get_param("security", "tls"),
|
||||||
|
"type": get_param("type", "tcp"),
|
||||||
|
"sni": get_param("sni") or "",
|
||||||
|
"alpn": get_param("alpn") or "",
|
||||||
|
"pbk": get_param("public_key") or get_param("pbk") or "",
|
||||||
|
"sid": get_param("sid") or "",
|
||||||
|
"spx": get_param("spx") or "",
|
||||||
|
"path": get_param("path") or "/",
|
||||||
|
"serviceName": get_param("serviceName") or get_param("service-name") or "",
|
||||||
|
"host": get_param("host") or "",
|
||||||
|
"flow": get_param("flow") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def url_to_proxy_info(url_str: str) -> ProxyInfo:
|
||||||
|
"""Convert a VLESS URL directly to a ProxyInfo dataclass.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url_str: VLESS subscription URI.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProxyInfo instance with parsed fields.
|
||||||
|
"""
|
||||||
|
raw = parse_vless_url(url_str)
|
||||||
|
return ProxyInfo(
|
||||||
|
uuid=raw["uuid"],
|
||||||
|
server=raw["server"],
|
||||||
|
port=int(raw["port"]),
|
||||||
|
security=raw["security"],
|
||||||
|
type=raw["type"],
|
||||||
|
sni=raw["sni"],
|
||||||
|
alpn=raw["alpn"],
|
||||||
|
pbk=raw["pbk"],
|
||||||
|
sid=raw["sid"],
|
||||||
|
spx=raw["spx"],
|
||||||
|
path=raw["path"],
|
||||||
|
serviceName=raw["serviceName"],
|
||||||
|
host=raw["host"],
|
||||||
|
flow=raw["flow"],
|
||||||
|
)
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Main orchestrator for Sub2SOCKS."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import fetcher
|
||||||
|
import config_builder
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(level: str = "info") -> None:
|
||||||
|
"""Configure root logger with proper formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: Log level string (debug, info, warn, error).
|
||||||
|
"""
|
||||||
|
numeric_level = getattr(logging, level.upper(), logging.INFO)
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
handler.setFormatter(logging.Formatter(
|
||||||
|
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
|
))
|
||||||
|
root = logging.getLogger()
|
||||||
|
root.setLevel(numeric_level)
|
||||||
|
root.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_node(vless_urls: List[str], strategy: str = "first") -> Optional[str]:
|
||||||
|
"""Select a VLESS URL from the list based on the given strategy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vless_urls: List of VLESS URL strings.
|
||||||
|
strategy: 'first' or 'random'. Defaults to 'first'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Selected VLESS URL string, or None if list is empty.
|
||||||
|
"""
|
||||||
|
if not vless_urls:
|
||||||
|
return None
|
||||||
|
if strategy == "random":
|
||||||
|
chosen = random.choice(vless_urls)
|
||||||
|
else:
|
||||||
|
chosen = vless_urls[0]
|
||||||
|
logger.debug("Chosen node URL (truncated): %s", chosen[:60] + "...")
|
||||||
|
return chosen
|
||||||
|
|
||||||
|
|
||||||
|
def run(
|
||||||
|
sub_url: str,
|
||||||
|
socks_port: int = 1080,
|
||||||
|
socks_user: str = "",
|
||||||
|
socks_pass: str = "",
|
||||||
|
dns_server: str = "tls://8.8.8.8",
|
||||||
|
pick_strategy: str = "first",
|
||||||
|
log_level: str = "info",
|
||||||
|
config_file_path: str = "/tmp/.sub2socks.json",
|
||||||
|
) -> None:
|
||||||
|
"""Execute the full Sub2SOCKS startup sequence and exec into sing-box.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sub_url: Subscription URL containing VLESS nodes.
|
||||||
|
socks_port: Local SOCKS5 listen port.
|
||||||
|
socks_user: Optional auth username.
|
||||||
|
socks_pass: Optional auth password.
|
||||||
|
dns_server: Upstream DNS resolver address.
|
||||||
|
pick_strategy: Node selection strategy ('first' or 'random').
|
||||||
|
log_level: Logging verbosity level.
|
||||||
|
config_file_path: Path where sing-box JSON config is written.
|
||||||
|
"""
|
||||||
|
setup_logging(log_level)
|
||||||
|
print_banner(sub_url, socks_port, dns_server, pick_strategy, log_level)
|
||||||
|
|
||||||
|
logger.info("Fetching subscription from %s", sub_url)
|
||||||
|
try:
|
||||||
|
sub_data = fetcher.fetch_subscription(sub_url)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Failed to reach %s: %s", sub_url, exc)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
vless_lines = fetcher.extract_vless_lines(sub_data)
|
||||||
|
if not vless_lines:
|
||||||
|
logger.error("No VLESS nodes found in subscription!")
|
||||||
|
sys.stdout.write(sub_data[:500] + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
logger.info("Found %d VLESS node(s), picking one with strategy '%s'", len(vless_lines), pick_strategy)
|
||||||
|
|
||||||
|
chosen = pick_node(vless_lines, pick_strategy)
|
||||||
|
if not chosen:
|
||||||
|
logger.error("Node selection failed.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
proxy = fetcher.url_to_proxy_info(chosen)
|
||||||
|
|
||||||
|
logger.info("Selected node: %s:%d", proxy.server, proxy.port)
|
||||||
|
|
||||||
|
singbox_config = config_builder.build_singbox_config(
|
||||||
|
proxy=proxy,
|
||||||
|
socks_port=socks_port,
|
||||||
|
socks_user=socks_user,
|
||||||
|
socks_pass=socks_pass,
|
||||||
|
dns_server=dns_server,
|
||||||
|
log_level=log_level,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(config_file_path, "w") as fp:
|
||||||
|
json.dump(singbox_config, fp, indent=2)
|
||||||
|
|
||||||
|
logger.info("Config written to %s", config_file_path)
|
||||||
|
|
||||||
|
os.execve(
|
||||||
|
"/usr/local/bin/sing-box",
|
||||||
|
["sing-box", "run", "-c", config_file_path],
|
||||||
|
dict(os.environ),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def print_banner(
|
||||||
|
sub_url: str,
|
||||||
|
socks_port: int,
|
||||||
|
dns_server: str,
|
||||||
|
pick_strategy: str,
|
||||||
|
log_level: str,
|
||||||
|
) -> None:
|
||||||
|
"""Print the ASCII startup banner to stdout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sub_url: Subscription URL being used.
|
||||||
|
socks_port: SOCKS listen port.
|
||||||
|
dns_server: DNS upstream address.
|
||||||
|
pick_strategy: Node pick strategy in use.
|
||||||
|
log_level: Active log level.
|
||||||
|
"""
|
||||||
|
banner = (
|
||||||
|
"\n"
|
||||||
|
"+-------------------------------------------------------+\n"
|
||||||
|
"| Sub2SOCKS -- bootstrapping |\n"
|
||||||
|
"+-------------------------------------------------------+\n"
|
||||||
|
f"| SUB_URL : {sub_url}\n"
|
||||||
|
f"| SOCKS port : {socks_port:>6d}\n"
|
||||||
|
f"| DNS upstream : {dns_server}\n"
|
||||||
|
f"| PICK_STRATEGY : {pick_strategy}\n"
|
||||||
|
f"| LOG_LEVEL : {log_level}\n"
|
||||||
|
"+-------------------------------------------------------+\n"
|
||||||
|
)
|
||||||
|
sys.stdout.write(banner)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Entry-point that reads env vars and calls run()."""
|
||||||
|
run(
|
||||||
|
sub_url=os.environ["SUB_URL"],
|
||||||
|
socks_port=int(os.environ.get("SOCKS_PORT", "1080")),
|
||||||
|
socks_user=os.environ.get("SOCKS_USER", ""),
|
||||||
|
socks_pass=os.environ.get("SOCKS_PASS", ""),
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Data models for Sub2SOCKS."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProxyInfo:
|
||||||
|
"""Parsed VLESS proxy configuration."""
|
||||||
|
uuid: str
|
||||||
|
server: str
|
||||||
|
port: int
|
||||||
|
security: str = "tls"
|
||||||
|
type: str = "tcp"
|
||||||
|
sni: str = ""
|
||||||
|
alpn: str = ""
|
||||||
|
pbk: str = ""
|
||||||
|
sid: str = ""
|
||||||
|
spx: str = ""
|
||||||
|
path: str = "/"
|
||||||
|
serviceName: str = ""
|
||||||
|
host: str = ""
|
||||||
|
flow: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DNSServerConfig:
|
||||||
|
"""Sing-box DNS server configuration."""
|
||||||
|
server_type: str # udp, tls, https
|
||||||
|
server: str
|
||||||
|
server_port: int = 53
|
||||||
|
tls_config: Optional[Dict[str, Any]] = None
|
||||||
Reference in New Issue
Block a user