Files
Sub2Socks/main.py
T
Hermes-1 ed52257dbe 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()
2026-07-09 08:14:23 +00:00

170 lines
5.0 KiB
Python

#!/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()