170 lines
5.0 KiB
Python
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 = "udp://1.1.1.1",
|
|
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", "udp://1.1.1.1"),
|
|
pick_strategy=os.environ.get("PICK_STRATEGY", "first"),
|
|
log_level=os.environ.get("LOG_LEVEL", "info"),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|