Start the Go rewrite: jsjson, awg-ctrl port, differential bench
ci / check (push) Successful in 6m6s
ci / check (push) Successful in 6m6s
- internal/jsjson: JSON.stringify-compatible encoder and order-preserving decoder, tested against fixtures produced by Node itself - internal/ctrl: awg-ctrl in Go (params, vpn:// keys, users, peers, sync, 3.1 upgrade, CAKE shaping, metrics, monitor job, Express-compatible HTTP layer) - internal/iauth, internal/logx: internal Ed25519 tokens and logging - tools/diffbench: Docker bench with stubbed awg/ip/tc/nft that runs one scenario against the TS and Go implementations for comparison Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Дифференциальный стенд: TS- и Go-версии awg-ctrl против одних и тех же заглушек.
|
||||
FROM node:20-bookworm
|
||||
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends openssl sqlite3 >/dev/null && rm -rf /var/lib/apt/lists/*
|
||||
COPY tools/diffbench/stubs/stub.mjs /opt/stub/stub.mjs
|
||||
RUN for c in awg awg-quick ip tc nft modinfo; do printf '#!/bin/sh\nexec node /opt/stub/stub.mjs %s "$@"\n' "$c" > /usr/local/bin/$c; chmod +x /usr/local/bin/$c; done
|
||||
# TS-эталон: тот же код, что в репозитории, со своими node_modules под linux
|
||||
COPY awg-ctrl/package.json /ts/awg-ctrl/package.json
|
||||
RUN cd /ts/awg-ctrl && npm install --no-audit --no-fund --loglevel=error
|
||||
COPY awg-ctrl/index.ts awg-ctrl/shaping.ts awg-ctrl/tsconfig.json /ts/awg-ctrl/
|
||||
COPY tools/diffbench/setup.sh tools/diffbench/run.sh tools/diffbench/scenario-ctrl.mjs tools/diffbench/collect.mjs /bench/
|
||||
WORKDIR /bench
|
||||
@@ -0,0 +1,20 @@
|
||||
// Состояние после сценария: conf, файлы каталога, строки баз, журнал и состояние заглушек.
|
||||
import fs from "fs";
|
||||
import { execSync } from "child_process";
|
||||
const out = process.argv[2];
|
||||
const t = JSON.parse(fs.readFileSync(out, "utf8"));
|
||||
const D = "/etc/amnezia/amneziawg";
|
||||
const q = sql => JSON.parse(execSync(`sqlite3 -json ${D}/users.db "${sql}"`).toString() || "[]");
|
||||
t.state = {
|
||||
conf: fs.readFileSync(`${D}/awg1.conf`, "utf8"),
|
||||
confMode: (fs.statSync(`${D}/awg1.conf`).mode & 0o777).toString(8),
|
||||
files: fs.readdirSync(D).map(f => f.replace(/\d{4}-\d\d-\d\dT[\d-]+Z/, "<ts>")).sort(),
|
||||
users: q("select * from users order by rowid"),
|
||||
peers: q("select * from peers order by rowid"),
|
||||
config: q("select * from config order by key"),
|
||||
schema: execSync(`sqlite3 ${D}/users.db .schema`).toString(),
|
||||
stubLog: fs.readFileSync("/var/lib/stub/log.jsonl", "utf8").split("\n").filter(Boolean).map(l => JSON.parse(l)),
|
||||
stubState: JSON.parse(fs.readFileSync("/var/lib/stub/state.json", "utf8")),
|
||||
};
|
||||
fs.writeFileSync(out, JSON.stringify(t, null, 1));
|
||||
console.log(`collect: done`);
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# run.sh <ts|go> <имя-прогона>; окружение: GEN, WITH_I1, STUB_MODULE
|
||||
set -e
|
||||
IMPL="$1"; NAME="$2"
|
||||
./setup.sh
|
||||
export PORT=3005 SERVER_IP=203.0.113.7 SERVER_PORT=47619 SERVER_NAME=VPN \
|
||||
INTERNAL_AUTH_PUB_FILE=/etc/amnezia/amneziawg/internal_auth_public.key \
|
||||
SHAPE_SAMPLE_S=3600 AWG_CLI=/nonexistent/awg-ctrl
|
||||
cd /ts/awg-ctrl
|
||||
if [ "$IMPL" = "ts" ]; then
|
||||
node node_modules/.bin/tsx index.ts > /run/bench/ctrl.log 2>&1 &
|
||||
else
|
||||
/go/awg-control ctrl > /run/bench/ctrl.log 2>&1 &
|
||||
fi
|
||||
PID=$!
|
||||
i=0; until curl -s -o /dev/null http://127.0.0.1:3005/health; do i=$((i+1)); [ $i -gt 100 ] && { cat /run/bench/ctrl.log; exit 1; }; sleep 0.1; done
|
||||
sleep 1 # стартовый apply шейпинга асинхронный — даём ему отработать до сценария
|
||||
cd /bench
|
||||
node scenario-ctrl.mjs "/out/$NAME.json"
|
||||
kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true
|
||||
node collect.mjs "/out/$NAME.json"
|
||||
cp /run/bench/ctrl.log "/out/$NAME.log"
|
||||
@@ -0,0 +1,112 @@
|
||||
// Сценарий для awg-ctrl: одинаковая последовательность запросов к любой реализации.
|
||||
// Пишет транскрипт (запрос → статус, тип, тело) в JSON; collect.mjs дописывает состояние.
|
||||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const BASE = "http://127.0.0.1:3005";
|
||||
const KEY = crypto.createPrivateKey(fs.readFileSync("/etc/amnezia/amneziawg/internal_auth_private.key"));
|
||||
const out = process.argv[2];
|
||||
|
||||
function token(iss = "bench") {
|
||||
const h = Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT" })).toString("base64url");
|
||||
const t = Math.floor(Date.now() / 1000);
|
||||
const p = Buffer.from(JSON.stringify({ iss, iat: t, exp: t + 60 })).toString("base64url");
|
||||
return `${h}.${p}.${crypto.sign(null, Buffer.from(`${h}.${p}`), KEY).toString("base64url")}`;
|
||||
}
|
||||
const key = s => crypto.createHash("sha256").update(s).digest("base64");
|
||||
const url64 = k => Buffer.from(k, "base64").toString("base64url");
|
||||
const P1 = key("dev1"), P2 = key("dev2"), P3 = key("dev3"), GHOST = key("ghost");
|
||||
|
||||
const steps = [];
|
||||
async function call(name, method, path, body, { auth = true, raw, headers = {} } = {}) {
|
||||
const h = { ...headers };
|
||||
if (auth) h.authorization = `Bearer ${typeof auth === "string" ? auth : token()}`;
|
||||
let data;
|
||||
if (raw !== undefined) { h["content-type"] = "application/json"; data = raw; }
|
||||
else if (body !== undefined) { h["content-type"] = "application/json"; data = JSON.stringify(body); }
|
||||
const r = await fetch(BASE + path, { method, headers: h, body: data });
|
||||
const ctype = (r.headers.get("content-type") ?? "").split(";")[0];
|
||||
const text = await r.text();
|
||||
let parsed = text;
|
||||
if (ctype === "application/json") { try { parsed = JSON.parse(text); } catch {} }
|
||||
steps.push({ name, method, path, status: r.status, ctype, body: parsed, text: ctype === "application/json" ? text : undefined });
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const gen = process.env.GEN ?? "2";
|
||||
|
||||
await call("health", "GET", "/health", undefined, { auth: false });
|
||||
await call("no token", "GET", "/api/users", undefined, { auth: false });
|
||||
await call("bad token", "GET", "/api/users", undefined, { auth: "a.b.c" });
|
||||
await call("expired token", "GET", "/api/users", undefined, { auth: (() => {
|
||||
const h = Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT" })).toString("base64url");
|
||||
const p = Buffer.from(JSON.stringify({ iss: "x", iat: 1, exp: 2 })).toString("base64url");
|
||||
return `${h}.${p}.${crypto.sign(null, Buffer.from(`${h}.${p}`), KEY).toString("base64url")}`;
|
||||
})() });
|
||||
await call("users empty", "GET", "/api/users");
|
||||
await call("add alice", "POST", "/api/users", { name: "alice" });
|
||||
await call("add bob", "POST", "/api/users", { name: "bob" });
|
||||
await call("add alice again", "POST", "/api/users", { name: "alice" });
|
||||
await call("add bad name", "POST", "/api/users", { name: "a b" });
|
||||
await call("add long name", "POST", "/api/users", { name: "x".repeat(33) });
|
||||
await call("add no body", "POST", "/api/users");
|
||||
await call("add too big", "POST", "/api/users", undefined, { raw: JSON.stringify({ name: "y", pad: "z".repeat(2000) }) });
|
||||
await call("add broken json", "POST", "/api/users", undefined, { raw: "{\"name\":" });
|
||||
await call("users", "GET", "/api/users");
|
||||
await call("stats", "GET", "/api/users/stats");
|
||||
await call("fetch alice", "POST", "/api/users/alice");
|
||||
await call("fetch nobody", "POST", "/api/users/nobody");
|
||||
await call("fetch bad name", "POST", "/api/users/a%20b");
|
||||
await call("add peer", "POST", "/api/peers", { pub_key: P1, owner: "abcdef123456/m1/d1" });
|
||||
await call("add peer dup", "POST", "/api/peers", { pub_key: P1, owner: "abcdef123456/m1/d1" });
|
||||
await call("add peer bad owner", "POST", "/api/peers", { pub_key: P2, owner: "bad owner!" });
|
||||
await call("add peer bad key", "POST", "/api/peers", { pub_key: "nope", owner: "abcdef123456/m1/d2" });
|
||||
await call("add peer legacy", "POST", "/api/peers", { pub_key: P3, owner: "m7/d9" });
|
||||
await call("peers", "GET", "/api/peers");
|
||||
await call("rekey peer", "PUT", `/api/peers/${url64(P1)}`, { pub_key: P2 });
|
||||
await call("rekey missing", "PUT", `/api/peers/${url64(key("missing"))}`, { pub_key: key("other") });
|
||||
await call("rekey to taken", "PUT", `/api/peers/${url64(P2)}`, { pub_key: P3 });
|
||||
await call("rotate psk", "POST", `/api/peers/${url64(P2)}/psk`);
|
||||
await call("rotate psk missing", "POST", `/api/peers/${url64(key("missing"))}/psk`);
|
||||
await call("profile", "GET", "/api/profile");
|
||||
await call("add peer 4", "POST", "/api/peers", { pub_key: key("dev4"), owner: "abcdef123456/m1/d4" });
|
||||
await call("sync bad owner", "PUT", "/api/peers/sync", { owner: "BAD", keep: [] });
|
||||
await call("sync bad keep", "PUT", "/api/peers/sync", { owner: "abcdef123456", keep: "x" });
|
||||
await call("sync keep P2 + legacy P3", "PUT", "/api/peers/sync", { owner: "abcdef123456", keep: [P2, P3] });
|
||||
await call("peers after sync", "GET", "/api/peers");
|
||||
execSync(`awg set awg1 peer ${GHOST} allowed-ips 10.9.9.9/32`);
|
||||
await call("audit", "GET", "/api/audit");
|
||||
await call("audit clean bad", "POST", "/api/audit/clean", { pub_keys: "x" });
|
||||
await call("audit clean", "POST", "/api/audit/clean", { pub_keys: [GHOST, P2] });
|
||||
await call("delete peer", "DELETE", `/api/peers/${url64(P2)}`);
|
||||
await call("delete peer missing", "DELETE", `/api/peers/${url64(P2)}`);
|
||||
await call("delete bob", "DELETE", "/api/users/bob");
|
||||
await call("delete nobody", "DELETE", "/api/users/nobody");
|
||||
await call("shaping", "GET", "/api/shaping");
|
||||
await call("shaping bad", "PUT", "/api/shaping", { enabled: "yes", auto: true });
|
||||
await call("shaping bad mbit", "PUT", "/api/shaping", { enabled: true, auto: true, downMbit: 1.5, upMbit: 10 });
|
||||
await call("shaping missing width", "PUT", "/api/shaping", { enabled: true, auto: true, downMbit: 0, upMbit: 10 });
|
||||
await call("shaping on", "PUT", "/api/shaping", { enabled: true, auto: false, downMbit: 285, upMbit: 280 });
|
||||
await call("shaping get on", "GET", "/api/shaping");
|
||||
await call("stats priority", "GET", "/api/users/stats");
|
||||
await call("metrics", "GET", "/api/metrics");
|
||||
await call("shaping off", "PUT", "/api/shaping", { enabled: false, auto: true, downMbit: 0, upMbit: 0 });
|
||||
await call("monitor job", "GET", "/api/monitor/job");
|
||||
await call("monitor install (no cli)", "POST", "/api/monitor/install");
|
||||
await call("awg status", "GET", "/awg/status");
|
||||
await call("awg start (already up)", "POST", "/awg/start");
|
||||
await call("reissue", "POST", "/api/users/reissue");
|
||||
await call("users after reissue", "GET", "/api/users");
|
||||
await call("awg restart", "POST", "/awg/restart");
|
||||
await call("health after restart", "GET", "/health", undefined, { auth: false });
|
||||
await call("upgrade", "POST", "/awg/upgrade");
|
||||
await call("profile after upgrade", "GET", "/api/profile");
|
||||
await call("users after upgrade", "GET", "/api/users");
|
||||
await call("fetch alice after upgrade", "POST", "/api/users/alice");
|
||||
await call("health final", "GET", "/health", undefined, { auth: false });
|
||||
await call("unknown route", "GET", "/api/nope");
|
||||
await call("wrong method", "PATCH", "/api/users");
|
||||
|
||||
fs.writeFileSync(out, JSON.stringify({ gen, steps }, null, 1));
|
||||
console.log(`scenario: ${steps.length} steps → ${out}`);
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
# Чистое состояние «сервера»: awg1.conf нужного поколения, ключи, пустые базы, заглушки.
|
||||
# GEN=2|3.1, WITH_I1=1 — добавить I1 в conf (install.sh его не пишет).
|
||||
set -e
|
||||
GEN="${GEN:-2}"
|
||||
D=/etc/amnezia/amneziawg
|
||||
rm -rf /etc/amnezia /var/lib/stub /run/bench
|
||||
mkdir -p "$D" /var/lib/stub /run/bench
|
||||
node -e 'const c=require("crypto");process.stdout.write(c.createHash("sha256").update("server").digest("base64"))' > /etc/amnezia/server_private.key
|
||||
chmod 600 /etc/amnezia/server_private.key
|
||||
node -e 'const c=require("crypto");const p=require("fs").readFileSync("/etc/amnezia/server_private.key","utf8").trim();process.stdout.write(c.createHash("sha256").update("pub:"+p).digest("base64")+"\n")' > "$D/server_public.key"
|
||||
openssl genpkey -algorithm ed25519 -out "$D/internal_auth_private.key" 2>/dev/null
|
||||
openssl pkey -in "$D/internal_auth_private.key" -pubout -out "$D/internal_auth_public.key"
|
||||
PRIV=$(cat /etc/amnezia/server_private.key)
|
||||
cat > "$D/awg1.conf" <<CONF
|
||||
[Interface]
|
||||
Address = 10.9.0.1/16
|
||||
MTU = 1376
|
||||
PostUp = awg set awg1 private-key /etc/amnezia/server_private.key; iptables -A FORWARD -i awg1 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PreDown = iptables -D FORWARD -i awg1 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
ListenPort = 47619
|
||||
PrivateKey = ${PRIV}
|
||||
Jc = 6
|
||||
Jmin = 10
|
||||
Jmax = 50
|
||||
S1 = 90
|
||||
S2 = 45
|
||||
S3 = 37
|
||||
S4 = ${S4:-14}
|
||||
H1 = 1224800044-2116730834
|
||||
H2 = 2122053282-2133204808
|
||||
H3 = 2133604274-2140756116
|
||||
H4 = 2143656228-2147444225
|
||||
CONF
|
||||
if [ "${WITH_I1:-0}" = "1" ]; then
|
||||
echo 'I1 = <r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>' >> "$D/awg1.conf"
|
||||
fi
|
||||
if [ "$GEN" = "3.1" ]; then
|
||||
cat >> "$D/awg1.conf" <<CONF
|
||||
HeaderProtectionKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
|
||||
ContentPaddingAddition = 10-100
|
||||
RekeyAfterTime = 100-120
|
||||
RekeyTimeout = 3-7
|
||||
RejectAfterTime = 150-180
|
||||
KeepaliveTimeout = 5-15
|
||||
MaxHandshakeAttempts = 15-20
|
||||
CONF
|
||||
fi
|
||||
chmod 600 "$D/awg1.conf"
|
||||
echo '{"up":false,"n":0,"peers":{}}' > /var/lib/stub/state.json
|
||||
: > /var/lib/stub/log.jsonl
|
||||
awg-quick up awg1
|
||||
: > /var/lib/stub/log.jsonl
|
||||
@@ -0,0 +1,143 @@
|
||||
// Заглушки системных команд для дифференциального стенда: awg, awg-quick, ip, tc, nft, modinfo.
|
||||
// Детерминированные ключи (счётчик), состояние интерфейса в JSON, журнал каждого вызова.
|
||||
// Одинаково ведут себя для TS- и Go-версии — сравнивается то, что версии с ними делают.
|
||||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
|
||||
const DIR = "/var/lib/stub";
|
||||
const STATE = `${DIR}/state.json`;
|
||||
const LOG = `${DIR}/log.jsonl`;
|
||||
const [, , cmd, ...args] = process.argv;
|
||||
|
||||
const load = () => { try { return JSON.parse(fs.readFileSync(STATE, "utf8")); } catch { return { up: false, n: 0, peers: {} }; } };
|
||||
const save = s => fs.writeFileSync(STATE, JSON.stringify(s, null, 1));
|
||||
const b64 = s => crypto.createHash("sha256").update(s).digest("base64");
|
||||
const pubOf = priv => b64("pub:" + priv.trim());
|
||||
const readStdin = () => { try { return fs.readFileSync(0, "utf8"); } catch { return ""; } };
|
||||
|
||||
let stdin = "";
|
||||
const needsStdin = (cmd === "awg" && args[0] === "pubkey") || (cmd === "nft" && args.includes("-"));
|
||||
if (needsStdin) stdin = readStdin();
|
||||
|
||||
const entry = { cmd, args };
|
||||
if (stdin) entry.stdin = stdin;
|
||||
// файлы, которые передают по пути (PSK, syncconf), пишем в журнал содержимым: имя у них со временем
|
||||
const fileArgs = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (["preshared-key", "private-key"].includes(args[i]) || (cmd === "awg" && args[0] === "syncconf" && i === 2)) {
|
||||
try { fileArgs.push(fs.readFileSync(args[i + (args[0] === "syncconf" ? 0 : 1)], "utf8")); } catch { fileArgs.push("<unreadable>"); }
|
||||
}
|
||||
}
|
||||
if (fileArgs.length) entry.files = fileArgs;
|
||||
fs.appendFileSync(LOG, JSON.stringify(entry) + "\n");
|
||||
|
||||
const out = s => process.stdout.write(s);
|
||||
const st = load();
|
||||
|
||||
function parsePeers(text) {
|
||||
const peers = {};
|
||||
for (const block of text.split(/^\[Peer\]/m).slice(1)) {
|
||||
const g = k => block.match(new RegExp(`^\\s*${k}\\s*=\\s*(.*)$`, "m"))?.[1]?.trim();
|
||||
const pub = g("PublicKey");
|
||||
if (pub) peers[pub] = { psk: g("PresharedKey") ?? "", allowed: g("AllowedIPs") ?? "", ka: g("PersistentKeepalive") ?? "" };
|
||||
}
|
||||
return peers;
|
||||
}
|
||||
|
||||
function serverPub() {
|
||||
try { return pubOf(fs.readFileSync("/etc/amnezia/server_private.key", "utf8")); } catch { return "(none)"; }
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "awg": {
|
||||
const [sub, iface, ...rest] = args;
|
||||
if (sub === "--version") { out("awg-tools v1.0.20210914 - https://amnezia.org\n"); break; }
|
||||
if (sub === "genkey") { st.n++; save(st); out(b64("priv" + st.n) + "\n"); break; }
|
||||
if (sub === "genpsk") { st.n++; save(st); out(b64("psk" + st.n) + "\n"); break; }
|
||||
if (sub === "pubkey") { out(pubOf(stdin) + "\n"); break; }
|
||||
if (sub === "show") {
|
||||
if (!st.up) { process.stderr.write(`Unable to access interface: No such device\n`); process.exit(1); }
|
||||
const pubs = Object.keys(st.peers);
|
||||
if (rest[0] === "dump") {
|
||||
out(`(hidden)\t${serverPub()}\t47619\toff\n`);
|
||||
for (const p of pubs) out(`${p}\t(hidden)\t(none)\t${st.peers[p].allowed}\t0\t0\t0\t${st.peers[p].ka || "off"}\n`);
|
||||
} else if (rest[0] === "peers") {
|
||||
for (const p of pubs) out(p + "\n");
|
||||
} else {
|
||||
out(`interface: ${iface}\n public key: ${serverPub()}\n private key: (hidden)\n listening port: 47619\n`);
|
||||
for (const p of pubs) out(`\npeer: ${p}\n preshared key: (hidden)\n allowed ips: ${st.peers[p].allowed}\n`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (sub === "set") {
|
||||
if (!st.up) { process.stderr.write("Unable to modify interface: No such device\n"); process.exit(1); }
|
||||
const i = rest.indexOf("peer");
|
||||
if (i >= 0) {
|
||||
const pub = rest[i + 1];
|
||||
if (rest.includes("remove")) { delete st.peers[pub]; }
|
||||
else {
|
||||
const p = st.peers[pub] ?? { psk: "", allowed: "", ka: "" };
|
||||
const k = rest.indexOf("allowed-ips"); if (k >= 0) p.allowed = rest[k + 1];
|
||||
const a = rest.indexOf("persistent-keepalive"); if (a >= 0) p.ka = rest[a + 1];
|
||||
const s = rest.indexOf("preshared-key"); if (s >= 0) { try { p.psk = fs.readFileSync(rest[s + 1], "utf8").trim(); } catch {} }
|
||||
st.peers[pub] = p;
|
||||
}
|
||||
}
|
||||
save(st);
|
||||
break;
|
||||
}
|
||||
if (sub === "syncconf") {
|
||||
if (!st.up) { process.stderr.write("Unable to modify interface: No such device\n"); process.exit(1); }
|
||||
st.peers = parsePeers(fs.readFileSync(rest[0], "utf8"));
|
||||
save(st);
|
||||
break;
|
||||
}
|
||||
process.stderr.write(`stub awg: unsupported ${args.join(" ")}\n`); process.exit(1);
|
||||
}
|
||||
case "awg-quick": {
|
||||
const [sub] = args;
|
||||
if (sub === "up") {
|
||||
if (st.up) { process.stderr.write("awg-quick: `awg1' already exists\n"); process.exit(1); }
|
||||
if (process.env.STUB_UP_FAIL === "1") { process.stderr.write("RTNETLINK answers: Invalid argument\n"); process.exit(1); }
|
||||
st.up = true;
|
||||
try { st.peers = parsePeers(fs.readFileSync("/etc/amnezia/amneziawg/awg1.conf", "utf8")); } catch { st.peers = {}; }
|
||||
} else if (sub === "down") {
|
||||
if (!st.up) { process.stderr.write("awg-quick: `awg1' is not a WireGuard interface\n"); process.exit(1); }
|
||||
st.up = false; st.peers = {};
|
||||
}
|
||||
save(st);
|
||||
break;
|
||||
}
|
||||
case "ip": {
|
||||
const a = args.join(" ");
|
||||
if (a === "route show default") out("default via 203.0.113.1 dev eth0 proto static\n");
|
||||
else if (a.startsWith("addr show")) out("2: eth0: <BROADCAST,MULTICAST,UP> mtu 1500\n inet 203.0.113.7/24 brd 203.0.113.255 scope global eth0\n");
|
||||
else if (a === "link show ifb-awg") { if (!st.ifb) process.exit(1); out("5: ifb-awg: <BROADCAST,NOARP,UP> mtu 1500\n"); }
|
||||
else if (a.startsWith("link add ifb-awg")) { st.ifb = true; save(st); }
|
||||
else if (a.startsWith("link del ifb-awg")) { if (!st.ifb) process.exit(1); st.ifb = false; save(st); }
|
||||
break;
|
||||
}
|
||||
case "tc": {
|
||||
const a = args.join(" ");
|
||||
st.tc ??= {};
|
||||
if (a.startsWith("qdisc replace dev")) { st.tc[args[3] + ":root"] = a; save(st); }
|
||||
else if (a.startsWith("qdisc add dev") && a.includes("ingress")) { st.tc[args[3] + ":ingress"] = a; save(st); }
|
||||
else if (a.startsWith("filter add dev")) { st.tc[args[3] + ":filter"] = a; save(st); }
|
||||
else if (a.startsWith("qdisc del dev")) {
|
||||
const key = args[3] + ":" + (args[4] === "root" ? "root" : "ingress");
|
||||
if (!st.tc[key]) { process.stderr.write("Error: Cannot delete qdisc with handle of zero.\n"); process.exit(2); }
|
||||
delete st.tc[key]; if (args[4] === "ingress") delete st.tc[args[3] + ":filter"]; save(st);
|
||||
}
|
||||
else if (a.startsWith("filter show dev")) { if (st.tc[args[3] + ":filter"]) out("filter parent ffff: protocol all pref 1 matchall\n\taction order 1: mirred (Egress Redirect to device ifb-awg) stolen\n"); }
|
||||
else if (args[0] === "-s" && args[1] === "-j") { const dev = args[5]; out(st.tc[dev + ":root"] ? `[{"kind":"cake","handle":"8001:","root":true,"bytes":1000,"packets":10,"drops":2,"overlimits":0,"requeues":0,"backlog":0,"qlen":0}]\n` : "[]\n"); }
|
||||
else if (a.startsWith("qdisc show dev")) { if (st.tc[args[3] + ":root"]) out(`qdisc cake 8001: root refcnt 2 bandwidth 285Mbit\n`); }
|
||||
break;
|
||||
}
|
||||
case "nft": {
|
||||
if (args[0] === "delete") { if (!st.nft) { process.stderr.write("Error: No such file or directory\n"); process.exit(1); } st.nft = false; save(st); }
|
||||
else if (args.includes("-")) { st.nft = true; save(st); }
|
||||
break;
|
||||
}
|
||||
case "modinfo": out((process.env.STUB_MODULE ?? "1.0.0") + "\n"); break;
|
||||
default: process.stderr.write(`stub: unknown ${cmd}\n`); process.exit(127);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Эталон для internal/jsjson: вывод JSON.stringify самого Node на каверзных значениях.
|
||||
// Запуск: node tools/golden/jsjson.mjs > testdata/golden/jsjson.json
|
||||
const strings = [
|
||||
"", "plain", "кириллица", "emoji 😀", "<r 2><b 0x85>", "a&b<c>d", "slash/и\\обратный",
|
||||
"quote\"q", "line\nbreak", "tab\tcr\r", "\b\f", "\u0000\u0001\u001f\u007f", "
",
|
||||
"I2 = ", " spaces ", "ü", "日本語",
|
||||
];
|
||||
const numbers = [0, -0, 1, -1, 1376, 47619, 0.1, 0.5, 1.5, 2.5, 100.25, 1e21, 1e20, 123456789012345680000,
|
||||
1e-7, 1e-6, 0.000001234, 1.7976931348623157e308, 5e-324, 3.14159, -273.15, 2 ** 53, 2 ** 53 + 2, 0.1 + 0.2, 250.5,
|
||||
NaN, Infinity, -Infinity];
|
||||
const objects = [
|
||||
{ a: 1, b: "x", c: [1, 2, { d: null }], e: true, f: false },
|
||||
{ rekey: false, endpoints: ["1.2.3.4:443"], servers: [{ id: 0, name: "VPN & <co>
", awg: { Jc: "6", I1: "<r 2>" } }] },
|
||||
{ empty: {}, arr: [], nested: { x: [] } },
|
||||
[],
|
||||
{},
|
||||
];
|
||||
const out = {
|
||||
strings: strings.map(s => ({ in: s, out: JSON.stringify(s) })),
|
||||
numbers: numbers.map(n => ({ in: Number.isFinite(n) ? n : String(n), out: JSON.stringify(n), str: String(n) })),
|
||||
objects: objects.map(o => ({ in: o, compact: JSON.stringify(o), indent2: JSON.stringify(o, null, 2) })),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out, null, 1));
|
||||
Reference in New Issue
Block a user