Add proxy subscription runtime support
Add daemon RPCs, Apple and GTK import flows, packet proxy runtime support, diagnostics, and BEPs for proxy subscription handling. Redact subscription URL secrets from fetch errors before they reach logs or UI surfaces.
This commit is contained in:
parent
97c569fb35
commit
d1638726ca
46 changed files with 15079 additions and 456 deletions
576
Scripts/burrow-proxy-ladder
Executable file
576
Scripts/burrow-proxy-ladder
Executable file
|
|
@ -0,0 +1,576 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Probe a Burrow proxy subscription node without enabling system proxying.
|
||||
|
||||
This script intentionally bypasses the Burrow daemon, Network Extension,
|
||||
routes, DNS settings, and browsers. It loads the selected proxy node from the
|
||||
Burrow SQLite database and speaks the provider protocol directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sqlite3
|
||||
import ssl
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_APP_GROUP = "2KPBQHRJ2D.com.tianruichen.burrow"
|
||||
DEFAULT_DB = (
|
||||
Path.home()
|
||||
/ "Library"
|
||||
/ "Group Containers"
|
||||
/ DEFAULT_APP_GROUP
|
||||
/ "burrow.db"
|
||||
)
|
||||
DEFAULT_TARGET = "1.1.1.1:80"
|
||||
DEFAULT_HTTP_HOST = "one.one.one.one"
|
||||
DEFAULT_DNS_SERVER = "1.1.1.1:53"
|
||||
DEFAULT_DNS_NAME = "google.com"
|
||||
DEFAULT_TIMEOUT = 5.0
|
||||
DOH_PROVIDERS = {
|
||||
"doh-google": "https://dns.google/resolve",
|
||||
"doh-cloudflare": "https://cloudflare-dns.com/dns-query",
|
||||
}
|
||||
|
||||
TROJAN_CMD_CONNECT = 0x01
|
||||
TROJAN_CMD_UDP_ASSOCIATE = 0x03
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Endpoint:
|
||||
host: str
|
||||
port: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProbeResult:
|
||||
ok: bool
|
||||
detail: str
|
||||
|
||||
|
||||
def parse_endpoint(value: str, default_port: int | None = None) -> Endpoint:
|
||||
text = value.strip()
|
||||
if not text:
|
||||
raise ValueError("endpoint must not be empty")
|
||||
|
||||
if text.startswith("["):
|
||||
end = text.find("]")
|
||||
if end == -1:
|
||||
raise ValueError(f"invalid bracketed IPv6 endpoint: {value}")
|
||||
host = text[1:end]
|
||||
rest = text[end + 1 :]
|
||||
if rest.startswith(":"):
|
||||
port = int(rest[1:])
|
||||
elif default_port is not None:
|
||||
port = default_port
|
||||
else:
|
||||
raise ValueError(f"endpoint is missing a port: {value}")
|
||||
return Endpoint(host, port)
|
||||
|
||||
if text.count(":") == 1:
|
||||
host, port_text = text.rsplit(":", 1)
|
||||
return Endpoint(host, int(port_text))
|
||||
|
||||
try:
|
||||
ipaddress.ip_address(text)
|
||||
except ValueError:
|
||||
if ":" in text:
|
||||
if default_port is None:
|
||||
raise ValueError(f"IPv6 endpoint is missing a port: {value}")
|
||||
return Endpoint(text, default_port)
|
||||
if default_port is None:
|
||||
raise ValueError(f"endpoint is missing a port: {value}")
|
||||
return Endpoint(text, default_port)
|
||||
|
||||
if default_port is None:
|
||||
raise ValueError(f"endpoint is missing a port: {value}")
|
||||
return Endpoint(text, default_port)
|
||||
|
||||
|
||||
def encode_socks_addr(endpoint: Endpoint) -> bytes:
|
||||
try:
|
||||
ip = ipaddress.ip_address(endpoint.host)
|
||||
except ValueError:
|
||||
host = endpoint.host.encode("idna")
|
||||
if len(host) > 255:
|
||||
raise ValueError(f"SOCKS domain is too long: {endpoint.host}")
|
||||
return bytes([3, len(host)]) + host + struct.pack("!H", endpoint.port)
|
||||
|
||||
if ip.version == 4:
|
||||
return bytes([1]) + ip.packed + struct.pack("!H", endpoint.port)
|
||||
return bytes([4]) + ip.packed + struct.pack("!H", endpoint.port)
|
||||
|
||||
|
||||
def read_exact(sock: ssl.SSLSocket, length: int) -> bytes:
|
||||
chunks: list[bytes] = []
|
||||
remaining = length
|
||||
while remaining:
|
||||
chunk = sock.recv(remaining)
|
||||
if not chunk:
|
||||
raise EOFError(f"expected {remaining} more bytes")
|
||||
chunks.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
return b"".join(chunks)
|
||||
|
||||
|
||||
def read_socks_addr(sock: ssl.SSLSocket) -> Endpoint:
|
||||
atyp = read_exact(sock, 1)[0]
|
||||
if atyp == 1:
|
||||
host = str(ipaddress.IPv4Address(read_exact(sock, 4)))
|
||||
elif atyp == 4:
|
||||
host = str(ipaddress.IPv6Address(read_exact(sock, 16)))
|
||||
elif atyp == 3:
|
||||
length = read_exact(sock, 1)[0]
|
||||
host = read_exact(sock, length).decode("idna")
|
||||
else:
|
||||
raise ValueError(f"unsupported SOCKS address type in response: {atyp}")
|
||||
port = struct.unpack("!H", read_exact(sock, 2))[0]
|
||||
return Endpoint(host, port)
|
||||
|
||||
|
||||
def trojan_password_hash(password: str) -> bytes:
|
||||
return hashlib.sha224(password.encode()).hexdigest().encode()
|
||||
|
||||
|
||||
def trojan_header(password: str, command: int, target: Endpoint) -> bytes:
|
||||
return (
|
||||
trojan_password_hash(password)
|
||||
+ b"\r\n"
|
||||
+ bytes([command])
|
||||
+ encode_socks_addr(target)
|
||||
+ b"\r\n"
|
||||
)
|
||||
|
||||
|
||||
def trojan_udp_frame(target: Endpoint, payload: bytes) -> bytes:
|
||||
return encode_socks_addr(target) + struct.pack("!H", len(payload)) + b"\r\n" + payload
|
||||
|
||||
|
||||
def build_dns_query(name: str) -> tuple[int, bytes]:
|
||||
query_id = int(time.time() * 1000) & 0xFFFF
|
||||
labels = [label for label in name.rstrip(".").split(".") if label]
|
||||
qname = b"".join(bytes([len(label)]) + label.encode("ascii") for label in labels) + b"\x00"
|
||||
header = struct.pack("!HHHHHH", query_id, 0x0100, 1, 0, 0, 0)
|
||||
question = qname + struct.pack("!HH", 1, 1)
|
||||
return query_id, header + question
|
||||
|
||||
|
||||
def parse_dns_response(query_id: int, payload: bytes) -> str:
|
||||
if len(payload) < 12:
|
||||
return f"truncated DNS response: {len(payload)} bytes"
|
||||
response_id, flags, questions, answers, authority, additional = struct.unpack(
|
||||
"!HHHHHH", payload[:12]
|
||||
)
|
||||
rcode = flags & 0x000F
|
||||
qr = bool(flags & 0x8000)
|
||||
id_note = "matching id" if response_id == query_id else f"id mismatch {response_id:#06x}"
|
||||
return (
|
||||
f"{len(payload)} bytes, qr={qr}, rcode={rcode}, answers={answers}, "
|
||||
f"authority={authority}, additional={additional}, questions={questions}, {id_note}"
|
||||
)
|
||||
|
||||
|
||||
def load_payload(db_path: Path, network_id: int | None) -> tuple[int, dict[str, Any]]:
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"Burrow database not found: {db_path}")
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
if network_id is None:
|
||||
row = conn.execute(
|
||||
"select id,payload from network where type = 'ProxySubscription' order by idx limit 1"
|
||||
).fetchone()
|
||||
else:
|
||||
row = conn.execute(
|
||||
"select id,payload from network where id = ? and type = 'ProxySubscription'",
|
||||
(network_id,),
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if row is None:
|
||||
selector = "first ProxySubscription" if network_id is None else f"ProxySubscription id={network_id}"
|
||||
raise LookupError(f"could not find {selector} in {db_path}")
|
||||
payload_blob = row["payload"]
|
||||
if isinstance(payload_blob, bytes):
|
||||
payload_text = payload_blob.decode("utf-8", errors="replace")
|
||||
else:
|
||||
payload_text = payload_blob or "{}"
|
||||
return int(row["id"]), json.loads(payload_text)
|
||||
|
||||
|
||||
def choose_node(
|
||||
payload: dict[str, Any],
|
||||
node_name: str | None = None,
|
||||
node_ordinal: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
nodes = payload.get("nodes") or []
|
||||
if node_name is not None:
|
||||
for node in nodes:
|
||||
if node.get("name") == node_name:
|
||||
return node
|
||||
raise LookupError(f"ProxySubscription payload has no node named {node_name!r}")
|
||||
if node_ordinal is not None:
|
||||
for node in nodes:
|
||||
if node.get("ordinal") == node_ordinal:
|
||||
return node
|
||||
raise LookupError(f"ProxySubscription payload has no node with ordinal {node_ordinal}")
|
||||
|
||||
selected_name = payload.get("selected_name")
|
||||
selected_ordinal = payload.get("selected_ordinal")
|
||||
for node in nodes:
|
||||
if selected_name is not None and node.get("name") == selected_name:
|
||||
return node
|
||||
if selected_ordinal is not None and node.get("ordinal") == selected_ordinal:
|
||||
return node
|
||||
if nodes:
|
||||
return nodes[0]
|
||||
raise LookupError("ProxySubscription payload has no compatible nodes")
|
||||
|
||||
|
||||
def resolved_stream_addresses(endpoint: Endpoint) -> list[tuple[int, tuple[Any, ...]]]:
|
||||
addresses: list[tuple[int, tuple[Any, ...]]] = []
|
||||
seen: set[tuple[Any, ...]] = set()
|
||||
for family, _, _, _, sockaddr in socket.getaddrinfo(
|
||||
endpoint.host, endpoint.port, type=socket.SOCK_STREAM
|
||||
):
|
||||
key = (family, sockaddr)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
addresses.append((family, sockaddr))
|
||||
return addresses
|
||||
|
||||
|
||||
def resolve_doh(host: str, port: int, provider: str, timeout: float) -> list[Endpoint]:
|
||||
base = DOH_PROVIDERS[provider]
|
||||
url = base + "?" + urllib.parse.urlencode({"name": host, "type": "A"})
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
headers={"Accept": "application/dns-json", "User-Agent": "burrow-proxy-ladder"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
body = json.load(response)
|
||||
endpoints: list[Endpoint] = []
|
||||
for answer in body.get("Answer") or []:
|
||||
if answer.get("type") != 1:
|
||||
continue
|
||||
address = str(answer.get("data") or "")
|
||||
try:
|
||||
ipaddress.IPv4Address(address)
|
||||
except ValueError:
|
||||
continue
|
||||
endpoint = Endpoint(address, port)
|
||||
if endpoint not in endpoints:
|
||||
endpoints.append(endpoint)
|
||||
return endpoints
|
||||
|
||||
|
||||
def resolve_doh_all(host: str, port: int, timeout: float) -> list[Endpoint]:
|
||||
endpoints: list[Endpoint] = []
|
||||
errors: list[str] = []
|
||||
for provider in DOH_PROVIDERS:
|
||||
try:
|
||||
provider_endpoints = resolve_doh(host, port, provider, timeout)
|
||||
except BaseException as exc:
|
||||
errors.append(f"{provider}={type(exc).__name__}: {exc}")
|
||||
continue
|
||||
print(
|
||||
f"{provider}_answers="
|
||||
+ ",".join(f"{endpoint.host}:{endpoint.port}" for endpoint in provider_endpoints)
|
||||
)
|
||||
for endpoint in provider_endpoints:
|
||||
if endpoint not in endpoints:
|
||||
endpoints.append(endpoint)
|
||||
if errors:
|
||||
print("doh_warnings=" + "; ".join(errors))
|
||||
return endpoints
|
||||
|
||||
|
||||
def is_fake_ip(host: str) -> bool:
|
||||
try:
|
||||
ip = ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
return False
|
||||
return ip in ipaddress.ip_network("198.18.0.0/15")
|
||||
|
||||
|
||||
def server_hostname(server: str, sni: str | None) -> str | None:
|
||||
value = sni or server
|
||||
if not value:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def tls_context(config: dict[str, Any]) -> ssl.SSLContext:
|
||||
skip_verify = bool(config.get("skip_cert_verify"))
|
||||
if skip_verify:
|
||||
context = ssl._create_unverified_context()
|
||||
else:
|
||||
context = ssl.create_default_context()
|
||||
|
||||
alpn_present = bool(config.get("alpn_present"))
|
||||
alpn = config.get("alpn") or []
|
||||
if not alpn_present:
|
||||
alpn = ["h2", "http/1.1"]
|
||||
if alpn:
|
||||
context.set_alpn_protocols([str(value) for value in alpn])
|
||||
return context
|
||||
|
||||
|
||||
def connect_tls(
|
||||
connect_endpoint: Endpoint,
|
||||
tls_server_name: str,
|
||||
node: dict[str, Any],
|
||||
timeout: float,
|
||||
) -> ssl.SSLSocket:
|
||||
config = node["config"]
|
||||
context = tls_context(config)
|
||||
sni = server_hostname(tls_server_name, config.get("sni"))
|
||||
last_error: BaseException | None = None
|
||||
for _family, sockaddr in resolved_stream_addresses(connect_endpoint):
|
||||
started = time.monotonic()
|
||||
raw_sock: socket.socket | None = None
|
||||
try:
|
||||
raw_sock = socket.create_connection(sockaddr, timeout=timeout)
|
||||
raw_sock.settimeout(timeout)
|
||||
tls_sock = context.wrap_socket(raw_sock, server_hostname=sni)
|
||||
tls_sock.settimeout(timeout)
|
||||
elapsed_ms = (time.monotonic() - started) * 1000
|
||||
peer = sockaddr[0] if isinstance(sockaddr, tuple) else str(sockaddr)
|
||||
print(
|
||||
f" tls_connect_ok address={peer}:{connect_endpoint.port} "
|
||||
f"elapsed_ms={elapsed_ms:.1f} alpn={tls_sock.selected_alpn_protocol()!r}"
|
||||
)
|
||||
return tls_sock
|
||||
except BaseException as exc:
|
||||
last_error = exc
|
||||
elapsed_ms = (time.monotonic() - started) * 1000
|
||||
peer = sockaddr[0] if isinstance(sockaddr, tuple) else str(sockaddr)
|
||||
print(
|
||||
f" tls_connect_error address={peer}:{connect_endpoint.port} "
|
||||
f"elapsed_ms={elapsed_ms:.1f} error={type(exc).__name__}: {exc}"
|
||||
)
|
||||
if raw_sock is not None:
|
||||
raw_sock.close()
|
||||
assert last_error is not None
|
||||
raise last_error
|
||||
|
||||
|
||||
def probe_trojan_tcp(
|
||||
connect_endpoint: Endpoint,
|
||||
tls_server_name: str,
|
||||
node: dict[str, Any],
|
||||
target: Endpoint,
|
||||
http_host: str,
|
||||
timeout: float,
|
||||
) -> ProbeResult:
|
||||
request = (
|
||||
f"GET / HTTP/1.1\r\n"
|
||||
f"Host: {http_host}\r\n"
|
||||
f"Connection: close\r\n"
|
||||
f"User-Agent: burrow-proxy-ladder\r\n\r\n"
|
||||
).encode()
|
||||
try:
|
||||
with connect_tls(connect_endpoint, tls_server_name, node, timeout) as sock:
|
||||
sock.sendall(trojan_header(node["config"]["password"], TROJAN_CMD_CONNECT, target))
|
||||
sock.sendall(request)
|
||||
data = sock.recv(2048)
|
||||
except BaseException as exc:
|
||||
return ProbeResult(False, f"{type(exc).__name__}: {exc}")
|
||||
if not data:
|
||||
return ProbeResult(False, "proxy returned EOF before any HTTP response bytes")
|
||||
first_line = data.splitlines()[0].decode("utf-8", errors="replace")
|
||||
return ProbeResult(True, f"received {len(data)} bytes; first_line={first_line!r}")
|
||||
|
||||
|
||||
def probe_trojan_udp_dns(
|
||||
connect_endpoint: Endpoint,
|
||||
tls_server_name: str,
|
||||
node: dict[str, Any],
|
||||
dns_server: Endpoint,
|
||||
dns_name: str,
|
||||
timeout: float,
|
||||
) -> ProbeResult:
|
||||
if node["config"].get("udp") is False:
|
||||
return ProbeResult(False, "selected Trojan node has udp=false")
|
||||
query_id, query = build_dns_query(dns_name)
|
||||
try:
|
||||
with connect_tls(connect_endpoint, tls_server_name, node, timeout) as sock:
|
||||
sock.sendall(
|
||||
trojan_header(node["config"]["password"], TROJAN_CMD_UDP_ASSOCIATE, dns_server)
|
||||
)
|
||||
sock.sendall(trojan_udp_frame(dns_server, query))
|
||||
source = read_socks_addr(sock)
|
||||
payload_len = struct.unpack("!H", read_exact(sock, 2))[0]
|
||||
delimiter = read_exact(sock, 2)
|
||||
if delimiter != b"\r\n":
|
||||
return ProbeResult(False, f"invalid Trojan UDP delimiter: {delimiter!r}")
|
||||
payload = read_exact(sock, payload_len)
|
||||
except BaseException as exc:
|
||||
return ProbeResult(False, f"{type(exc).__name__}: {exc}")
|
||||
dns_summary = parse_dns_response(query_id, payload)
|
||||
return ProbeResult(
|
||||
True,
|
||||
f"received UDP frame from {source.host}:{source.port}; DNS response {dns_summary}",
|
||||
)
|
||||
|
||||
|
||||
def print_node_summary(network_id: int, payload: dict[str, Any], node: dict[str, Any]) -> None:
|
||||
config = node.get("config") or {}
|
||||
print(f"network_id={network_id}")
|
||||
print(f"subscription={payload.get('name')!r}")
|
||||
print(f"selected_node={node.get('name')!r}")
|
||||
print(f"protocol={node.get('protocol')!r}")
|
||||
print(f"server={node.get('server')}:{node.get('port')}")
|
||||
print(f"sni={config.get('sni')!r}")
|
||||
print(f"skip_cert_verify={bool(config.get('skip_cert_verify'))}")
|
||||
print(f"alpn_present={bool(config.get('alpn_present'))} alpn={config.get('alpn') or []!r}")
|
||||
print(f"udp={config.get('udp', True)!r}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Directly probe the selected Burrow proxy subscription node without "
|
||||
"using the daemon, Network Extension, system proxy, routes, or DNS settings."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--db", type=Path, default=Path(os.environ.get("BURROW_DB", DEFAULT_DB)))
|
||||
parser.add_argument("--network-id", type=int)
|
||||
parser.add_argument("--node-name", help="Probe this subscription node instead of the selected node.")
|
||||
parser.add_argument("--node-ordinal", type=int, help="Probe this subscription ordinal instead of the selected node.")
|
||||
parser.add_argument(
|
||||
"--server-address",
|
||||
help=(
|
||||
"Override the proxy connect address, as host or host:port. "
|
||||
"This bypasses system DNS for the proxy hostname while preserving SNI."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resolve-server",
|
||||
choices=["system", "doh-google", "doh-cloudflare", "doh-all"],
|
||||
default="system",
|
||||
help="How to resolve the proxy server when --server-address is not provided.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-server-addresses",
|
||||
action="store_true",
|
||||
help="Probe every resolved proxy server address and pass if any address works.",
|
||||
)
|
||||
parser.add_argument("--target", default=DEFAULT_TARGET, help="TCP HTTP target, host:port")
|
||||
parser.add_argument("--http-host", default=DEFAULT_HTTP_HOST)
|
||||
parser.add_argument("--dns-server", default=DEFAULT_DNS_SERVER, help="UDP DNS target, host:port")
|
||||
parser.add_argument("--dns-name", default=DEFAULT_DNS_NAME)
|
||||
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT)
|
||||
parser.add_argument("--skip-udp", action="store_true")
|
||||
parser.add_argument("--require-udp", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Burrow proxy ladder: direct provider/protocol probe")
|
||||
print("bypasses=daemon,NetworkExtension,system_proxy,routes,system_dns,browser")
|
||||
print(f"db={args.db}")
|
||||
|
||||
try:
|
||||
if args.node_name is not None and args.node_ordinal is not None:
|
||||
raise ValueError("--node-name and --node-ordinal are mutually exclusive")
|
||||
network_id, payload = load_payload(args.db, args.network_id)
|
||||
node = choose_node(payload, args.node_name, args.node_ordinal)
|
||||
print_node_summary(network_id, payload, node)
|
||||
if node.get("protocol") != "trojan":
|
||||
print(f"unsupported protocol for this direct probe: {node.get('protocol')!r}")
|
||||
return 2
|
||||
server = Endpoint(str(node["server"]), int(node["port"]))
|
||||
if args.server_address:
|
||||
connect_endpoints = [parse_endpoint(args.server_address, default_port=server.port)]
|
||||
elif args.resolve_server == "system":
|
||||
system_addresses = resolved_stream_addresses(server)
|
||||
connect_endpoints = [
|
||||
Endpoint(str(sockaddr[0]), int(sockaddr[1])) for _family, sockaddr in system_addresses
|
||||
]
|
||||
if not args.all_server_addresses and connect_endpoints:
|
||||
connect_endpoints = connect_endpoints[:1]
|
||||
if not connect_endpoints:
|
||||
connect_endpoints = [server]
|
||||
elif args.resolve_server == "doh-all":
|
||||
connect_endpoints = resolve_doh_all(server.host, server.port, args.timeout)
|
||||
if not args.all_server_addresses and connect_endpoints:
|
||||
connect_endpoints = connect_endpoints[:1]
|
||||
else:
|
||||
connect_endpoints = resolve_doh(server.host, server.port, args.resolve_server, args.timeout)
|
||||
print(
|
||||
f"{args.resolve_server}_answers="
|
||||
+ ",".join(f"{endpoint.host}:{endpoint.port}" for endpoint in connect_endpoints)
|
||||
)
|
||||
if not args.all_server_addresses and connect_endpoints:
|
||||
connect_endpoints = connect_endpoints[:1]
|
||||
if not connect_endpoints:
|
||||
raise LookupError("proxy server resolution returned no usable A records")
|
||||
target = parse_endpoint(args.target)
|
||||
dns_server = parse_endpoint(args.dns_server)
|
||||
except BaseException as exc:
|
||||
print(f"setup_error={type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(
|
||||
"connect_endpoints="
|
||||
+ ",".join(f"{endpoint.host}:{endpoint.port}" for endpoint in connect_endpoints)
|
||||
)
|
||||
if any(endpoint != server for endpoint in connect_endpoints):
|
||||
print(f"tls_server_name_source={server.host!r} sni_preserved=True")
|
||||
if any(is_fake_ip(endpoint.host) for endpoint in connect_endpoints):
|
||||
print(
|
||||
"warning=proxy hostname resolved into 198.18.0.0/15; "
|
||||
"rerun with --resolve-server doh-all or --server-address REAL_IP to avoid fake-IP DNS"
|
||||
)
|
||||
|
||||
any_tcp_ok = False
|
||||
any_required_udp_ok = False
|
||||
for idx, connect_endpoint in enumerate(connect_endpoints, start=1):
|
||||
if len(connect_endpoints) > 1:
|
||||
print(f"\n=== proxy server candidate {idx}/{len(connect_endpoints)} ===")
|
||||
print(f"connect_endpoint={connect_endpoint.host}:{connect_endpoint.port}")
|
||||
|
||||
print("\n[1/2] Trojan TCP CONNECT probe")
|
||||
print(f"target={target.host}:{target.port} http_host={args.http_host!r}")
|
||||
tcp_result = probe_trojan_tcp(
|
||||
connect_endpoint, server.host, node, target, args.http_host, args.timeout
|
||||
)
|
||||
print(("ok: " if tcp_result.ok else "fail: ") + tcp_result.detail)
|
||||
any_tcp_ok = any_tcp_ok or tcp_result.ok
|
||||
|
||||
udp_result = ProbeResult(True, "skipped")
|
||||
if not args.skip_udp:
|
||||
print("\n[2/2] Trojan UDP DNS probe")
|
||||
print(f"dns_server={dns_server.host}:{dns_server.port} dns_name={args.dns_name!r}")
|
||||
udp_result = probe_trojan_udp_dns(
|
||||
connect_endpoint, server.host, node, dns_server, args.dns_name, args.timeout
|
||||
)
|
||||
print(("ok: " if udp_result.ok else "fail: ") + udp_result.detail)
|
||||
else:
|
||||
print("\n[2/2] Trojan UDP DNS probe")
|
||||
print("skipped by --skip-udp")
|
||||
any_required_udp_ok = any_required_udp_ok or udp_result.ok
|
||||
|
||||
if tcp_result.ok and (not args.require_udp or udp_result.ok):
|
||||
break
|
||||
|
||||
if not any_tcp_ok:
|
||||
return 1
|
||||
if args.require_udp and not any_required_udp_ok:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue