feat: mihomo YAML subscription support + flow/fingerprint passthrough
- fetcher: parse mihomo/clash.meta YAML subscriptions (proxies: list), auto-detect format (mihomo YAML vs plain vless:// lines) - config_builder: forward VLESS flow (xtls-rprx-vision) and per-node uTLS fingerprint to sing-box (required for Reality+Vision nodes) - default DNS upstream: udp://10.35.99.172 (public UDP resolvers unreachable on this network; old-sub nodes are hostnames) - Dockerfile: install PyYAML Verified: 50-node reality+vision subscription works end-to-end (sing-box check + live SOCKS tunnel); legacy vless:// subs unaffected.
This commit is contained in:
@@ -31,23 +31,23 @@ def setup_logging(level: str = "info") -> None:
|
||||
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.
|
||||
def pick_node(proxies: List["ProxyInfo"], strategy: str = "first") -> Optional["ProxyInfo"]:
|
||||
"""Select a proxy node from the list based on the given strategy.
|
||||
|
||||
Args:
|
||||
vless_urls: List of VLESS URL strings.
|
||||
proxies: List of parsed proxy nodes.
|
||||
strategy: 'first' or 'random'. Defaults to 'first'.
|
||||
|
||||
Returns:
|
||||
Selected VLESS URL string, or None if list is empty.
|
||||
Selected ProxyInfo, or None if list is empty.
|
||||
"""
|
||||
if not vless_urls:
|
||||
if not proxies:
|
||||
return None
|
||||
if strategy == "random":
|
||||
chosen = random.choice(vless_urls)
|
||||
chosen = random.choice(proxies)
|
||||
else:
|
||||
chosen = vless_urls[0]
|
||||
logger.debug("Chosen node URL (truncated): %s", chosen[:60] + "...")
|
||||
chosen = proxies[0]
|
||||
logger.debug("Chosen node: %s:%d", chosen.server, chosen.port)
|
||||
return chosen
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ def run(
|
||||
socks_port: int = 1080,
|
||||
socks_user: str = "",
|
||||
socks_pass: str = "",
|
||||
dns_server: str = "udp://1.1.1.1",
|
||||
dns_server: str = "udp://10.35.99.172",
|
||||
pick_strategy: str = "first",
|
||||
log_level: str = "info",
|
||||
config_file_path: str = "/tmp/.sub2socks.json",
|
||||
@@ -83,23 +83,23 @@ def run(
|
||||
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:
|
||||
proxies = fetcher.extract_nodes(sub_data)
|
||||
if not proxies:
|
||||
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)
|
||||
logger.info("Found %d VLESS node(s), picking one with strategy '%s'",
|
||||
len(proxies), pick_strategy)
|
||||
|
||||
chosen = pick_node(vless_lines, pick_strategy)
|
||||
if not chosen:
|
||||
proxy = pick_node(proxies, pick_strategy)
|
||||
if not proxy:
|
||||
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)
|
||||
|
||||
logger.info("Selected node: %s:%d (security=%s, transport=%s, flow=%s)",
|
||||
proxy.server, proxy.port, proxy.security, proxy.type,
|
||||
proxy.flow or "-")
|
||||
singbox_config = config_builder.build_singbox_config(
|
||||
proxy=proxy,
|
||||
socks_port=socks_port,
|
||||
@@ -159,7 +159,7 @@ def main() -> None:
|
||||
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"),
|
||||
dns_server=os.environ.get("DNS_SERVER", "udp://10.35.99.172"),
|
||||
pick_strategy=os.environ.get("PICK_STRATEGY", "first"),
|
||||
log_level=os.environ.get("LOG_LEVEL", "info"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user