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:
JettChenT 2026-06-01 15:23:17 +08:00
parent 97c569fb35
commit d1638726ca
46 changed files with 15079 additions and 456 deletions

272
Scripts/burrow-proxy-selftest Executable file
View file

@ -0,0 +1,272 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: Scripts/burrow-proxy-selftest
Run a controlled Burrow proxy packet-runtime test without changing system proxy
settings or using the user's Burrow database. The script starts a local Trojan
TLS server, starts a Burrow daemon against a temporary database/socket, imports a
temporary ProxySubscription pointed at that server, then runs the packet-level
proxy-tcp-probe through daemon DNS and TCP packet streaming.
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
burrow_bin="${BURROW_BIN:-$repo_root/target/debug/burrow}"
if [[ ! -x "$burrow_bin" ]]; then
(cd "$repo_root" && cargo build -p burrow)
fi
if ! command -v openssl >/dev/null 2>&1; then
echo "openssl is required for the local TLS server certificate" >&2
exit 2
fi
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-proxy-selftest.XXXXXX")"
daemon_pid=""
server_pid=""
status=0
cleanup() {
if [[ -n "$server_pid" ]]; then
kill "$server_pid" >/dev/null 2>&1 || true
fi
if [[ -n "$daemon_pid" ]]; then
kill "$daemon_pid" >/dev/null 2>&1 || true
fi
if [[ "${BURROW_SELFTEST_KEEP:-}" == "1" || "$status" -ne 0 ]]; then
echo "preserving self-test artifacts: $tmpdir" >&2
else
rm -rf "$tmpdir"
fi
}
trap 'status=$?; cleanup' EXIT
cert="$tmpdir/cert.pem"
key="$tmpdir/key.pem"
socket="$tmpdir/burrow.sock"
port_file="$tmpdir/server.port"
server_result="$tmpdir/server-result.json"
payload="$tmpdir/proxy-payload.json"
daemon_log="$tmpdir/daemon.log"
server_log="$tmpdir/server.log"
openssl req \
-x509 \
-newkey rsa:2048 \
-nodes \
-subj /CN=localhost \
-keyout "$key" \
-out "$cert" \
-days 1 >/dev/null 2>&1
python3 - "$port_file" "$server_result" "$cert" "$key" >"$server_log" 2>&1 <<'PY' &
from __future__ import annotations
import hashlib
import json
import socket
import ssl
import struct
import sys
port_file, result_file, cert_file, key_file = sys.argv[1:]
password = "secret"
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) -> dict[str, object]:
atyp = read_exact(sock, 1)[0]
if atyp == 1:
host = socket.inet_ntop(socket.AF_INET, read_exact(sock, 4))
elif atyp == 4:
host = socket.inet_ntop(socket.AF_INET6, 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 {atyp}")
port = struct.unpack("!H", read_exact(sock, 2))[0]
return {"atyp": atyp, "host": host, "port": port}
def read_http_headers(sock: ssl.SSLSocket) -> bytes:
data = bytearray()
while b"\r\n\r\n" not in data:
chunk = sock.recv(4096)
if not chunk:
break
data.extend(chunk)
if len(data) > 65535:
raise ValueError("HTTP request headers exceeded 64 KiB")
return bytes(data)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert_file, key_file)
ctx.set_alpn_protocols(["h2", "http/1.1"])
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
port = listener.getsockname()[1]
with open(port_file, "w", encoding="utf-8") as f:
f.write(str(port))
raw, peer = listener.accept()
raw.settimeout(10)
result: dict[str, object] = {"peer": str(peer)}
try:
with ctx.wrap_socket(raw, server_side=True) as tls:
tls.settimeout(10)
result["alpn"] = tls.selected_alpn_protocol()
got_hash = read_exact(tls, 56)
expected_hash = hashlib.sha224(password.encode()).hexdigest().encode()
result["password_hash_ok"] = got_hash == expected_hash
if not result["password_hash_ok"]:
raise ValueError("unexpected Trojan password hash")
if read_exact(tls, 2) != b"\r\n":
raise ValueError("missing Trojan password delimiter")
command = read_exact(tls, 1)[0]
result["command"] = command
target = read_socks_addr(tls)
result["target"] = target
if read_exact(tls, 2) != b"\r\n":
raise ValueError("missing Trojan header delimiter")
request = read_http_headers(tls)
result["request_first_line"] = request.splitlines()[0].decode("utf-8", "replace")
body = json.dumps(
{
"ok": True,
"target": target,
"request_first_line": result["request_first_line"],
},
sort_keys=True,
).encode()
tls.sendall(
b"HTTP/1.1 200 OK\r\n"
+ f"Content-Length: {len(body)}\r\n".encode()
+ b"Connection: close\r\n"
+ b"Content-Type: application/json\r\n\r\n"
+ body
)
finally:
with open(result_file, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2, sort_keys=True)
listener.close()
PY
server_pid=$!
for _ in {1..100}; do
if [[ -s "$port_file" ]]; then
break
fi
if ! kill -0 "$server_pid" >/dev/null 2>&1; then
echo "local Trojan server exited before listening" >&2
cat "$server_log" >&2 || true
exit 1
fi
sleep 0.05
done
if [[ ! -s "$port_file" ]]; then
echo "timed out waiting for local Trojan server" >&2
cat "$server_log" >&2 || true
exit 1
fi
server_port="$(cat "$port_file")"
cat >"$payload" <<EOF
{
"name": "burrow proxy selftest",
"source": { "url": "selftest://local-trojan" },
"detected_format": "uri-list",
"selected_ordinal": 1,
"selected_name": "local trojan",
"nodes": [
{
"ordinal": 1,
"name": "local trojan",
"protocol": "trojan",
"server": "127.0.0.1",
"port": $server_port,
"warnings": [],
"config": {
"type": "trojan",
"password": "secret",
"sni": "localhost",
"alpn": [],
"alpn_present": true,
"skip_cert_verify": true,
"network": "tcp",
"udp": true,
"client_fingerprint": null,
"fingerprint": null
}
}
],
"warnings": []
}
EOF
(
cd "$tmpdir"
BURROW_SOCKET_PATH="$socket" "$burrow_bin" daemon >"$daemon_log" 2>&1
) &
daemon_pid=$!
for _ in {1..100}; do
if [[ -S "$socket" ]]; then
break
fi
if ! kill -0 "$daemon_pid" >/dev/null 2>&1; then
echo "Burrow daemon exited before creating socket" >&2
cat "$daemon_log" >&2 || true
exit 1
fi
sleep 0.05
done
if [[ ! -S "$socket" ]]; then
echo "timed out waiting for Burrow daemon socket" >&2
cat "$daemon_log" >&2 || true
exit 1
fi
(
cd "$tmpdir"
BURROW_SOCKET_PATH="$socket" "$burrow_bin" network-add 1 3 "$payload"
BURROW_SOCKET_PATH="$socket" "$burrow_bin" tunnel-config
BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-tcp-probe \
--dns-name selftest.invalid \
--remote 1.1.1.1:80 \
--timeout-ms 8000
)
wait "$server_pid"
server_pid=""
echo
echo "Local Trojan server observed:"
cat "$server_result"
echo