Files
forgetting/tools/golden/sen.ts
T
maeneko 70bb587216 Port awg-ui to Go: awg-control ui
internal/ui reimplements the awg-ui backend with the same HTTP surface; the
React frontend is unchanged and served from awg-ui/dist.

- web.go: Express-like router (express.json semantics, mounts, :params,
  HTML 400/404/413 pages, SPA fallback)
- server.go: JWT login with rate limit, API keys and their server sets,
  awg-ctrl client and proxy, /api/v1/users, delete hooks
- nodes.go: WSS hub for agents (pin, challenge-response, join strings),
  RPC with timeouts, health, /ui/nodes
- sub.go, audit.go, sub_admin.go: sen:// master keys and devices,
  per-server reconcile with tombstones and /api/peers/sync, signed /sub/v1
  listener, audit with two-pass confirmation and cleaning, panel and
  /api/v1/masterkeys routers
- metrics.go, monitor.go: /metrics exposition with tokens, /ui/monitor
- sen.go: link codec and protocol signatures, tested byte for byte against
  fixtures produced by awg-ui/sen.ts (tools/golden/sen.ts)

Differential bench (tools/diffbench/local-ui.sh, scenario-ui.ts,
compare-ui.mjs): 158 steps over every route group, including signed device
requests and a Go agent joining as a node, plus the resulting ui.db and
awg-ctrl state. TS and Go are identical after masking random values, across
repeated runs and with the race detector on.
2026-10-14 13:27:24 +00:00

64 lines
4.1 KiB
TypeScript

// Эталон для internal/ui (sen.go): ссылки sen:// и подписи протокола, как их делает awg-ui/sen.ts.
// Запуск: awg-ui/node_modules/.bin/tsx tools/golden/sen.ts > testdata/golden/sen.json
import crypto from "crypto";
import {
encodeSenLink, decodeSenLink, crc32, requestSigString, signResponse, signRequest, rawPublicKey,
type SenLink,
} from "../../awg-ui/sen";
const seed = (s: string) => crypto.createHash("sha256").update(s).digest();
// Ed25519 из фиксированного seed: PKCS#8 = префикс + 32 байта seed.
const keyFromSeed = (s: Buffer) => crypto.createPrivateKey({
key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), s]), format: "der", type: "pkcs8",
});
const signKey = keyFromSeed(seed("sign"));
const signPub = rawPublicKey(crypto.createPublicKey(signKey));
const hex = (b?: Buffer) => b?.toString("hex");
const links: SenLink[] = [
{ tls: false, addrs: [{ host: "203.0.113.7", port: 8443 }], secret: seed("s1").subarray(0, 16), signPub, name: "Семья" },
{ tls: true, addrs: [{ host: "vpn.example.com", port: 443 }], secret: seed("s2").subarray(0, 16), signPub, tlsPin: seed("pin"), name: "" },
{ tls: false, addrs: [{ host: "2001:db8::1", port: 1 }, { host: "::", port: 65535 }, { host: "fe80:0:0:1:0:0:0:1", port: 80 }],
secret: seed("s3").subarray(0, 16), signPub, name: "x".repeat(64) },
{ tls: false, addrs: [{ host: "1:2:3:4:5:6:7:8", port: 2 }], secret: seed("s4").subarray(0, 16), signPub, name: "emoji 😀 & <>" },
];
const badLinks = [
{ tls: false, addrs: [], secret: seed("a").subarray(0, 16), signPub, name: "" },
{ tls: false, addrs: [{ host: "256.1.1.1", port: 1 }], secret: seed("a").subarray(0, 16), signPub, name: "" },
{ tls: false, addrs: [{ host: "1::2::3", port: 1 }], secret: seed("a").subarray(0, 16), signPub, name: "" },
{ tls: false, addrs: [{ host: "bad_host", port: 1 }], secret: seed("a").subarray(0, 16), signPub, name: "" },
{ tls: false, addrs: [{ host: "a.b", port: 0 }], secret: seed("a").subarray(0, 16), signPub, name: "" },
{ tls: false, addrs: [{ host: "a.b", port: 1 }], secret: seed("a").subarray(0, 16), signPub, name: "я".repeat(33) },
{ tls: true, addrs: [{ host: "a.b", port: 1 }], secret: seed("a").subarray(0, 16), signPub, name: "" },
];
const good = links.map(encodeSenLink);
const decodeCases = [
...good, " " + good[0].toUpperCase().slice(0, 6) + good[0].slice(6) + " \n", good[1].slice(0, 20) + "\n" + good[1].slice(20),
"http://x", "sen://", "sen://AAAA", "sen://a+b", good[0].slice(0, -2), good[0].slice(0, -1) + (good[0].endsWith("A") ? "B" : "A"),
];
const err = (f: () => unknown) => { try { return { ok: f() }; } catch (e) { return { error: (e as Error).message }; } };
process.stdout.write(JSON.stringify({
sign_seed: hex(seed("sign")),
sign_pub: hex(signPub),
crc32: ["", "a", "hello", "кириллица"].map(s => ({ in: s, out: crc32(Buffer.from(s)) })),
encode: links.map((l, i) => ({
tls: l.tls, addrs: l.addrs, secret: hex(l.secret), tlsPin: hex(l.tlsPin), name: l.name, out: good[i],
})),
encode_errors: badLinks.map(l => ({
tls: l.tls, addrs: l.addrs, secret: hex(l.secret), name: l.name, error: err(() => encodeSenLink(l as SenLink)).error,
})),
decode: decodeCases.map(s => {
const r = err(() => decodeSenLink(s));
if ("error" in r) return { in: s, error: r.error };
const d = r.ok as SenLink;
return { in: s, tls: d.tls, addrs: d.addrs, secret: hex(d.secret), signPub: hex(d.signPub), tlsPin: hex(d.tlsPin) ?? null, name: d.name };
}),
sig_string: [
{ method: "post", path: "/sub/v1/register", ts: 1700000000, body: '{"a":1}' },
{ method: "GET", path: "/sub/v1/config", ts: 1, body: "" },
].map(c => ({ ...c, out: requestSigString(c.method, c.path, c.ts, c.body),
sig: signRequest(c.method, c.path, c.ts, c.body, signKey) })),
sign_response: [{ config: { rev: "ab", servers: [] }, device: 3, ts: 5 }, { error: "revoked" }].map(p => ({ in: p, out: signResponse(p, signKey) })),
}, null, 1));