#!/usr/bin/env bash set -euo pipefail readonly RC_VERSION='1.14.0-rc.5' readonly IMAGE="harbor-singbox-client-rc-test:${RC_VERSION}" FIXTURES="$(mktemp -d)" SUFFIX="${FIXTURES##*/}" readonly SUFFIX="${SUFFIX//[^[:alnum:]]/}" readonly NETWORK="harbor-singbox-rc-${SUFFIX}" readonly TARGET="harbor-singbox-rc-target-${SUFFIX}" readonly PROXY="harbor-singbox-rc-proxy-${SUFFIX}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" NETWORK_CREATED=false TARGET_CREATED=false PROXY_CREATED=false cleanup() { [[ "$PROXY_CREATED" == true ]] && docker rm -f "$PROXY" >/dev/null 2>&1 || true [[ "$TARGET_CREATED" == true ]] && docker rm -f "$TARGET" >/dev/null 2>&1 || true [[ "$NETWORK_CREATED" == true ]] && docker network rm "$NETWORK" >/dev/null 2>&1 || true docker image rm "$IMAGE" >/dev/null 2>&1 || true rm -rf "$FIXTURES" } trap cleanup EXIT check_config() { local name="$1" local output local unexpected if ! output="$(docker run --rm \ -v "$FIXTURES:/fixtures:ro" \ --entrypoint sing-box \ "$IMAGE" check -c "/fixtures/${name}.json" 2>&1)"; then printf '%s\n' "$output" >&2 return 1 fi unexpected="$(printf '%s\n' "$output" \ | grep -Ei 'warn|deprecated' \ | grep -Evi 'independent_cache.*DNS option is deprecated' || true)" if [[ -n "$unexpected" ]]; then printf 'unexpected warning for %s:\n%s\n' "$name" "$unexpected" >&2 return 1 fi printf 'PASS config %s\n' "$name" } docker build \ --build-arg "SINGBOX_VERSION=${RC_VERSION}" \ -t "$IMAGE" \ -f "$ROOT/Dockerfile.client" \ "$ROOT" version_output="$(docker run --rm --entrypoint sing-box "$IMAGE" version)" printf '%s\n' "$version_output" grep -Fxq "sing-box version ${RC_VERSION}" <<< "$version_output" cat > "$FIXTURES/generate-configs.mjs" <<'EOF' import fs from 'node:fs'; import { parseSubscriptionBody } from '/app/dist/server/subscription.js'; import { buildGatewayConfig } from '/app/dist/server/singbox.js'; const reality = 'vless://00000000-0000-4000-8000-000000000001@reality.example.test:443?security=reality&type=tcp&pbk=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&sid=0123456789abcdef&sni=cover.example.test&fp=chrome#Reality'; const websocket = 'vless://00000000-0000-4000-8000-000000000002@ws.example.test:8443?encryption=none&security=tls&sni=edge.example.test&fp=firefox&alpn=h2%2Chttp%2F1.1&type=ws&host=edge.example.test&path=%2Fsocket%3Fed%3D2048#TLS%20WS'; function generated(link, clientDirect = false) { const parsed = parseSubscriptionBody(Buffer.from(link).toString('base64')); return buildGatewayConfig(parsed.config, parsed.servers[0].id, { clientDirect }); } for (const [name, config] of [ ['local-vpn', generated(websocket)], ['gateway-direct', generated(websocket, true)], ['vless-reality', generated(reality)], ['vless-tls-websocket', generated(websocket)], ]) { fs.writeFileSync(`/fixtures/${name}.json`, JSON.stringify(config)); } EOF docker run --rm \ -e APP_MODE=client \ -e PROXY_PORT=18081 \ -e DIAGNOSTICS_PROXY_PORT=18082 \ -e DATA_DIR=/tmp/harbor-rc \ -e SING_BOX_CACHE=/tmp/harbor-rc-cache.db \ -v "$FIXTURES:/fixtures" \ --entrypoint node \ "$IMAGE" /fixtures/generate-configs.mjs check_config local-vpn check_config gateway-direct check_config vless-reality check_config vless-tls-websocket docker network create "$NETWORK" >/dev/null NETWORK_CREATED=true docker create --name "$TARGET" --network "$NETWORK" --entrypoint node "$IMAGE" \ -e 'require("node:http").createServer((request,response)=>response.end(request.url)).listen(18080,"0.0.0.0")' >/dev/null TARGET_CREATED=true docker start "$TARGET" >/dev/null docker create --name "$PROXY" --network "$NETWORK" \ -v "$FIXTURES:/fixtures:ro" --entrypoint sing-box "$IMAGE" run -c /fixtures/gateway-direct.json >/dev/null PROXY_CREATED=true docker start "$PROXY" >/dev/null for _ in {1..30}; do if docker run --rm --network "$NETWORK" --entrypoint curl "$IMAGE" \ --noproxy '' -fsS -x "http://${PROXY}:18081" "http://${TARGET}:18080/http" | grep -Fxq '/http'; then break fi sleep 0.2 done http_body="$(docker run --rm --network "$NETWORK" --entrypoint curl "$IMAGE" \ --noproxy '' -fsS -x "http://${PROXY}:18081" "http://${TARGET}:18080/http")" socks_body="$(docker run --rm --network "$NETWORK" --entrypoint curl "$IMAGE" \ --noproxy '' -fsS --socks5-hostname "${PROXY}:18081" "http://${TARGET}:18080/socks")" [[ "$http_body" == '/http' ]] [[ "$socks_body" == '/socks' ]] printf 'PASS mixed inbound HTTP\nPASS mixed inbound SOCKS5\n'