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
272
Scripts/burrow-proxy-selftest
Executable file
272
Scripts/burrow-proxy-selftest
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue