Add native traffic inspection to Harbor Connect and Gateway
This commit is contained in:
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly RC_VERSION='1.14.0-rc.5'
|
||||
readonly ROLLBACK_VERSION='1.13.18'
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
SUFFIX="${TEMP_DIR##*/}"
|
||||
readonly SUFFIX="${SUFFIX//[^[:alnum:]]/}"
|
||||
readonly IMAGE="harbor-gateway-native-test:${RC_VERSION}-${SUFFIX}"
|
||||
readonly ROLLBACK_IMAGE="harbor-gateway-rollback-test:${ROLLBACK_VERSION}-${SUFFIX}"
|
||||
readonly CONTAINER="harbor-gateway-native-${SUFFIX}"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
docker image rm "$IMAGE" "$ROLLBACK_IMAGE" >/dev/null 2>&1 || true
|
||||
rm -rf "$TEMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
docker build --build-arg "SINGBOX_VERSION=${RC_VERSION}" -t "$IMAGE" -f "$ROOT/Dockerfile" "$ROOT"
|
||||
docker build --build-arg "SINGBOX_VERSION=${ROLLBACK_VERSION}" -t "$ROLLBACK_IMAGE" -f "$ROOT/Dockerfile" "$ROOT"
|
||||
|
||||
docker run --rm \
|
||||
-e APP_MODE=gateway \
|
||||
-e APP_COMPONENT=control \
|
||||
-e DATAPLANE_SOCKET=/tmp/dataplane.sock \
|
||||
-e SING_BOX_TRAFFIC_SOURCE=snapshot \
|
||||
--entrypoint node \
|
||||
"$ROLLBACK_IMAGE" --input-type=module --eval '
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { buildGatewayConfig } from "/app/dist/server/singbox.js";
|
||||
const subscription = { outbounds: [{ type: "vless", tag: "vpn", server: "vpn.example.test", server_port: 443, uuid: "00000000-0000-4000-8000-000000000000", tls: { enabled: true } }] };
|
||||
const config = buildGatewayConfig(subscription, "vpn");
|
||||
assert.equal(config.services, undefined);
|
||||
assert.deepEqual(config.dns, { independent_cache: true });
|
||||
fs.writeFileSync("/tmp/rollback.json", JSON.stringify(config));
|
||||
const checked = spawnSync("sing-box", ["check", "-c", "/tmp/rollback.json"], { encoding: "utf8" });
|
||||
assert.equal(checked.status, 0, checked.stderr || checked.stdout);
|
||||
assert.match(spawnSync("sing-box", ["version"], { encoding: "utf8" }).stdout, /^sing-box version 1\.13\.18$/m);
|
||||
console.log("PASS Gateway snapshot rollback 1.13.18");
|
||||
'
|
||||
|
||||
docker create -i --name "$CONTAINER" \
|
||||
-e APP_MODE=gateway \
|
||||
-e APP_COMPONENT=dataplane \
|
||||
-e DATAPLANE_SOCKET=/tmp/dataplane.sock \
|
||||
-e SING_BOX_TRAFFIC_SOURCE=native \
|
||||
-e DATA_DIR=/tmp/harbor-gateway-native \
|
||||
-e SING_BOX_CACHE=/tmp/harbor-gateway-native/cache.db \
|
||||
--entrypoint /bin/bash "$IMAGE" -s >/dev/null
|
||||
|
||||
docker start -a -i "$CONTAINER" <<'CONTAINER_SCRIPT'
|
||||
set -euo pipefail
|
||||
cd /app
|
||||
|
||||
node --input-type=module <<'NODE'
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import { once } from 'node:events';
|
||||
import { createClient } from '@connectrpc/connect';
|
||||
import { createGrpcTransport } from '@connectrpc/connect-node';
|
||||
import { StartedService } from '/app/dist/server/generated/daemon/started_service_pb.js';
|
||||
import { materializeGatewayNativeConfig } from '/app/dist/server/gatewayNativeRuntime.js';
|
||||
import { buildDualChannelGatewayConfig, buildGatewayConfig } from '/app/dist/server/singbox.js';
|
||||
|
||||
const directory = '/tmp/harbor-gateway-native';
|
||||
const secretPath = `${directory}/api.secret`;
|
||||
const runtimePath = `${directory}/runtime-config.json`;
|
||||
const subscription = { outbounds: [{
|
||||
type: 'vless', tag: 'vpn', server: 'vpn.example.test', server_port: 443,
|
||||
uuid: '00000000-0000-4000-8000-000000000000', tls: { enabled: true },
|
||||
}] };
|
||||
const single = buildGatewayConfig(subscription, 'vpn');
|
||||
const dual = buildDualChannelGatewayConfig({
|
||||
primary: { subscriptionConfig: subscription, selectedServerId: 'vpn' },
|
||||
reserve: { subscriptionConfig: subscription, selectedServerId: 'vpn' },
|
||||
});
|
||||
|
||||
let secret = '';
|
||||
for (const config of [single, dual]) {
|
||||
assert.equal(JSON.stringify(config).includes('secret'), false);
|
||||
const materialized = materializeGatewayNativeConfig(config, {
|
||||
apiPort: 19091, secretPath, runtimeConfigPath: runtimePath,
|
||||
});
|
||||
assert.equal(materialized.warning, null);
|
||||
secret ||= materialized.secret;
|
||||
assert.equal(materialized.secret, secret);
|
||||
assert.equal(fs.statSync(secretPath).mode & 0o777, 0o600);
|
||||
assert.equal(fs.statSync(runtimePath).mode & 0o777, 0o600);
|
||||
const checked = spawnSync('sing-box', ['check', '-c', runtimePath], { encoding: 'utf8' });
|
||||
assert.equal(checked.status, 0, checked.stderr || checked.stdout);
|
||||
}
|
||||
|
||||
materializeGatewayNativeConfig(single, { apiPort: 19091, secretPath, runtimeConfigPath: runtimePath });
|
||||
const process = spawn('sing-box', ['run', '-c', runtimePath], { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let log = '';
|
||||
process.stdout.on('data', (chunk) => { log += chunk; });
|
||||
process.stderr.on('data', (chunk) => { log += chunk; });
|
||||
const client = createClient(StartedService, createGrpcTransport({ baseUrl: 'http://127.0.0.1:19091' }));
|
||||
const controller = new AbortController();
|
||||
const options = { signal: controller.signal, headers: { authorization: `Bearer ${secret}` } };
|
||||
try {
|
||||
let version;
|
||||
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||||
try {
|
||||
version = await client.getVersion({}, { ...options, timeoutMs: 200 });
|
||||
break;
|
||||
} catch {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
assert.ok(version, `native API did not start\n${log}`);
|
||||
assert.equal(version.version, '1.14.0-rc.5');
|
||||
assert.ok((await client.getStartedAt({}, options)).startedAt > 0n);
|
||||
const connectionStream = client.subscribeConnections({ interval: 100_000_000n }, options)[Symbol.asyncIterator]();
|
||||
const statusStream = client.subscribeStatus({ interval: 100_000_000n }, options)[Symbol.asyncIterator]();
|
||||
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('native streams timed out')), 5_000));
|
||||
assert.equal((await Promise.race([connectionStream.next(), timeout])).done, false);
|
||||
assert.equal((await Promise.race([statusStream.next(), timeout])).done, false);
|
||||
console.log('PASS Gateway RC5 single/dual config, 0600 secret and authenticated lifecycle API');
|
||||
} finally {
|
||||
controller.abort();
|
||||
process.kill('SIGTERM');
|
||||
await Promise.race([once(process, 'exit'), new Promise((resolve) => setTimeout(resolve, 1_000))]);
|
||||
}
|
||||
NODE
|
||||
CONTAINER_SCRIPT
|
||||
Reference in New Issue
Block a user