Expand Shadowsocks runtime compatibility

This commit is contained in:
JettChenT 2026-06-05 10:33:41 -07:00
parent d1638726ca
commit 4d1f589280
167 changed files with 57173 additions and 1640 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,504 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: Scripts/burrow-shadowsocks-singmux-interop
Run controlled Burrow Shadowsocks top-level sing-mux interop tests against the
same github.com/metacubex/sing-mux Go module used by Mihomo. By default this
checks smux, yamux, and h2mux with and without Mihomo's padded sing-mux
preface/framing. The test does not change system proxy settings or use the
user's Burrow database.
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}"
sing_mux_dir="${SING_MUX_DIR:-/Users/jettchen/go/pkg/mod/github.com/metacubex/sing-mux@v0.3.9}"
sing_dir="${SING_DIR:-/Users/jettchen/go/pkg/mod/github.com/metacubex/sing@v0.5.7}"
if [[ -z "${BURROW_SINGMUX_INTEROP_SINGLE:-}" ]]; then
for protocol in ${BURROW_SINGMUX_PROTOCOLS:-smux yamux h2mux}; do
for padding in ${BURROW_SINGMUX_PADDING_MODES:-false true}; do
echo "=== Mihomo sing-mux interop: $protocol padding=$padding ==="
BURROW_SINGMUX_INTEROP_SINGLE=1 \
BURROW_SINGMUX_PROTOCOL="$protocol" \
BURROW_SINGMUX_PADDING="$padding" \
"$0"
done
done
exit 0
fi
protocol="${BURROW_SINGMUX_PROTOCOL:-smux}"
case "$protocol" in
smux | yamux | h2mux) ;;
*)
echo "unsupported sing-mux protocol: $protocol" >&2
exit 2
;;
esac
padding="${BURROW_SINGMUX_PADDING:-false}"
case "$padding" in
false | true) ;;
*)
echo "unsupported sing-mux padding mode: $padding" >&2
exit 2
;;
esac
node_udp="${BURROW_SINGMUX_NODE_UDP:-false}"
case "$node_udp" in
false | true) ;;
*)
echo "unsupported Shadowsocks node udp flag: $node_udp" >&2
exit 2
;;
esac
if [[ -z "${BURROW_BIN:-}" ]]; then
(cd "$repo_root" && cargo build -p burrow)
elif [[ ! -x "$burrow_bin" ]]; then
echo "BURROW_BIN is not executable: $burrow_bin" >&2
exit 2
fi
if ! command -v go >/dev/null 2>&1; then
echo "go is required for the Mihomo sing-mux interop peer" >&2
exit 2
fi
if ! command -v uv >/dev/null 2>&1; then
echo "uv is required for result assertions" >&2
exit 2
fi
if [[ ! -d "$sing_mux_dir" ]]; then
echo "sing-mux module directory not found: $sing_mux_dir" >&2
exit 2
fi
if [[ ! -d "$sing_dir" ]]; then
echo "sing module directory not found: $sing_dir" >&2
exit 2
fi
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-singmux-interop.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 sing-mux interop artifacts: $tmpdir" >&2
else
rm -rf "$tmpdir"
fi
}
trap 'status=$?; cleanup' EXIT
socket="$tmpdir/burrow.sock"
port_file="$tmpdir/singmux.port"
result_file="$tmpdir/singmux-result.json"
payload="$tmpdir/proxy-payload.json"
server_log="$tmpdir/singmux-server.log"
daemon_log="$tmpdir/daemon.log"
go_dir="$tmpdir/go-peer"
mkdir -p "$go_dir"
cat >"$go_dir/go.mod" <<EOF
module burrow-singmux-interop
go 1.23
require (
github.com/metacubex/sing v0.5.7
github.com/metacubex/sing-mux v0.3.9
)
replace github.com/metacubex/sing => $sing_dir
replace github.com/metacubex/sing-mux => $sing_mux_dir
EOF
cat >"$go_dir/main.go" <<'EOF'
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"os"
"strings"
"sync"
"time"
mux "github.com/metacubex/sing-mux"
"github.com/metacubex/sing/common/buf"
"github.com/metacubex/sing/common/bufio"
"github.com/metacubex/sing/common/logger"
M "github.com/metacubex/sing/common/metadata"
N "github.com/metacubex/sing/common/network"
)
type noopLogger struct{}
func (noopLogger) Trace(args ...any) {}
func (noopLogger) Debug(args ...any) {}
func (noopLogger) Info(args ...any) {}
func (noopLogger) Warn(args ...any) {}
func (noopLogger) Error(args ...any) {}
func (noopLogger) Fatal(args ...any) {}
func (noopLogger) Panic(args ...any) {}
func (noopLogger) TraceContext(context.Context, ...any) {}
func (noopLogger) DebugContext(context.Context, ...any) {}
func (noopLogger) InfoContext(context.Context, ...any) {}
func (noopLogger) WarnContext(context.Context, ...any) {}
func (noopLogger) ErrorContext(context.Context, ...any) {}
func (noopLogger) FatalContext(context.Context, ...any) {}
func (noopLogger) PanicContext(context.Context, ...any) {}
var _ logger.ContextLogger = noopLogger{}
type observed struct {
MuxTarget map[string]any `json:"mux_target,omitempty"`
TCPDestination map[string]any `json:"tcp_destination,omitempty"`
TCPFirstLine string `json:"tcp_first_line,omitempty"`
UDPDestination map[string]any `json:"udp_destination,omitempty"`
UDPPayload string `json:"udp_payload,omitempty"`
Protocol string `json:"protocol,omitempty"`
Padding bool `json:"padding,omitempty"`
PacketReplyCount int `json:"packet_reply_count,omitempty"`
}
type handler struct {
mu sync.Mutex
result observed
done chan struct{}
doneOnce sync.Once
}
func (h *handler) markDoneIfReady() {
if h.result.TCPFirstLine != "" && h.result.UDPPayload != "" {
h.doneOnce.Do(func() { close(h.done) })
}
}
func (h *handler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
defer conn.Close()
request, err := readHTTPHeaders(conn)
if err != nil {
return err
}
firstLine := strings.SplitN(string(request), "\r\n", 2)[0]
body := []byte("burrow sing-mux tcp ok\n")
response := fmt.Sprintf(
"HTTP/1.1 200 OK\r\nContent-Length: %d\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n%s",
len(body),
body,
)
if _, err := conn.Write([]byte(response)); err != nil {
return err
}
h.mu.Lock()
h.result.TCPDestination = socksaddrJSON(metadata.Destination)
h.result.TCPFirstLine = firstLine
h.markDoneIfReady()
h.mu.Unlock()
return nil
}
func (h *handler) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
defer conn.Close()
packet := buf.NewPacket()
defer packet.Release()
destination, err := conn.ReadPacket(packet)
if err != nil {
return err
}
payload := append([]byte(nil), packet.Bytes()...)
if _, err := bufio.WritePacketBuffer(conn, buf.As(payload), destination); err != nil {
return err
}
h.mu.Lock()
h.result.UDPDestination = socksaddrJSON(metadata.Destination)
h.result.UDPPayload = string(payload)
h.result.PacketReplyCount++
h.markDoneIfReady()
h.mu.Unlock()
return nil
}
func main() {
portFile := os.Args[1]
resultFile := os.Args[2]
protocol := os.Args[3]
padding := os.Args[4] == "true"
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
panic(err)
}
defer listener.Close()
port := listener.Addr().(*net.TCPAddr).Port
if err := os.WriteFile(portFile, []byte(fmt.Sprint(port)), 0o644); err != nil {
panic(err)
}
h := &handler{done: make(chan struct{})}
h.result.Protocol = protocol
h.result.Padding = padding
service, err := mux.NewService(mux.ServiceOptions{
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { return ctx },
Logger: noopLogger{},
Handler: h,
})
if err != nil {
panic(err)
}
raw, err := listener.Accept()
if err != nil {
panic(err)
}
raw.SetDeadline(time.Now().Add(20 * time.Second))
target, err := readSocksAddr(raw)
if err != nil {
panic(err)
}
h.mu.Lock()
h.result.MuxTarget = target
h.mu.Unlock()
go func() {
if err := service.NewConnection(context.Background(), raw, M.Metadata{}); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}()
select {
case <-h.done:
case <-time.After(20 * time.Second):
panic("timed out waiting for sing-mux TCP and UDP probes")
}
h.mu.Lock()
output, err := json.MarshalIndent(h.result, "", " ")
h.mu.Unlock()
if err != nil {
panic(err)
}
if err := os.WriteFile(resultFile, append(output, '\n'), 0o644); err != nil {
panic(err)
}
}
func readHTTPHeaders(reader io.Reader) ([]byte, error) {
data := make([]byte, 0, 512)
tmp := make([]byte, 1)
for !strings.Contains(string(data), "\r\n\r\n") {
if _, err := io.ReadFull(reader, tmp); err != nil {
return nil, err
}
data = append(data, tmp[0])
if len(data) > 65535 {
return nil, fmt.Errorf("HTTP headers exceeded 64 KiB")
}
}
return data, nil
}
func readSocksAddr(reader io.Reader) (map[string]any, error) {
destination, err := M.SocksaddrSerializer.ReadAddrPort(reader)
if err != nil {
return nil, err
}
return socksaddrJSON(destination), nil
}
func socksaddrJSON(destination M.Socksaddr) map[string]any {
if destination.IsFqdn() {
return map[string]any{
"host": destination.Fqdn,
"port": destination.Port,
}
}
return map[string]any{
"host": destination.Addr.String(),
"port": destination.Port,
}
}
func (h *handler) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata M.Metadata) error {
return fmt.Errorf("unexpected packet handler path")
}
EOF
(
cd "$go_dir"
go mod tidy
)
(
cd "$go_dir"
go run . "$port_file" "$result_file" "$protocol" "$padding"
) >"$server_log" 2>&1 &
server_pid=$!
for _ in {1..600}; do
if [[ -s "$port_file" ]]; then
break
fi
if ! kill -0 "$server_pid" >/dev/null 2>&1; then
echo "sing-mux peer 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 sing-mux peer" >&2
cat "$server_log" >&2 || true
exit 1
fi
server_port="$(cat "$port_file")"
cat >"$payload" <<EOF
{
"name": "burrow sing-mux interop",
"source": { "url": "selftest://sing-mux" },
"detected_format": "uri-list",
"selected_ordinal": 1,
"selected_name": "local shadowsocks sing-mux $protocol padding $padding",
"nodes": [
{
"ordinal": 1,
"name": "local shadowsocks sing-mux $protocol padding $padding",
"protocol": "shadowsocks",
"server": "127.0.0.1",
"port": $server_port,
"warnings": [],
"config": {
"type": "shadowsocks",
"cipher": "none",
"password": "unused",
"udp": $node_udp,
"udp_over_tcp": false,
"udp_over_tcp_version": 1,
"client_fingerprint": null,
"plugin": null,
"smux": {
"enabled": true,
"protocol": "$protocol",
"max_connections": 1,
"min_streams": 0,
"max_streams": 16,
"padding": $padding,
"statistic": false,
"only_tcp": false,
"brutal": 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
BURROW_SOCKET_PATH="$socket" "$burrow_bin" proxy-udp-echo \
--message burrow-singmux-udp-selftest \
--timeout-ms 8000 \
9.9.9.9:5353
)
wait "$server_pid"
server_pid=""
echo
echo "Mihomo sing-mux peer observed:"
cat "$result_file"
echo
uv run python - "$result_file" "$protocol" "$padding" <<'PY'
from __future__ import annotations
import json
import sys
with open(sys.argv[1], "r", encoding="utf-8") as f:
result = json.load(f)
expected_protocol = sys.argv[2]
expected_padding = sys.argv[3] == "true"
def expect(value: object, expected: object, label: str) -> None:
if value != expected:
raise AssertionError(f"{label}: expected {expected!r}, got {value!r}")
def expect_target(key: str, host: str, port: int) -> None:
target = result.get(key)
if not isinstance(target, dict):
raise AssertionError(f"{key}: missing target object")
expect(target.get("host"), host, f"{key}.host")
expect(target.get("port"), port, f"{key}.port")
expect(result.get("protocol"), expected_protocol, "sing-mux protocol")
expect(result.get("padding", False), expected_padding, "sing-mux padding")
expect_target("mux_target", "sp.mux.sing-box.arpa", 444)
expect_target("tcp_destination", "selftest.invalid", 80)
expect(result.get("tcp_first_line"), "GET / HTTP/1.1", "tcp first line")
expect_target("udp_destination", "9.9.9.9", 5353)
expect(result.get("udp_payload"), "burrow-singmux-udp-selftest", "udp payload")
expect(result.get("packet_reply_count"), 1, "packet reply count")
padding_label = "padded" if expected_padding else "unpadded"
print(f"Mihomo sing-mux {expected_protocol} {padding_label} interop assertions passed")
PY