Files
MikrotikManager/backend/src/services/wireguard-interface-update.test.ts
T

127 lines
12 KiB
TypeScript

import assert from "node:assert/strict"
import { fileURLToPath } from "node:url"
import { wireguardInterfaceDependencyPaths, wireguardInterfaceDeleteProof, type WireguardInterfaceDeleteState } from "./wireguard-interface-delete.js"
import { wireguardPublicKey } from "./wireguard-interface-create.js"
import { planWireguardInterfaceUpdate, projectWireguardInterfaceUpdate, wireguardInterfaceUpdateSchema, wireguardInterfaceUpdateProof } from "./wireguard-interface-update.js"
import { wireguardInterfaceUpdatePlanSchema } from "../modules/mcp/wireguard-interface-update-actions.js"
import { sameWireguardEndpoint } from "./wireguard-source.js"
import type { ServerRow } from "../modules/servers/repository/servers-repository.js"
export async function acceptWireguardInterfaceUpdateModel() {
const endpoint = { id: 136, enabled: true, host: "127.0.0.1", port: 443, username: "admin", password: "NEVER_ENDPOINT_PASSWORD", useSsl: true, verifySsl: true, routingEndpointEpoch: "35adfb60-9da1-44fa-b699-9ce1a32371bf", name: "before", updatedAt: new Date(0) } as ServerRow
assert.equal(sameWireguardEndpoint({ ...endpoint, name: "after", comment: "metadata", updatedAt: new Date() }, endpoint), true)
assert.equal(sameWireguardEndpoint({ ...endpoint, routingEndpointEpoch: "ae5e2528-fe50-483b-a128-dc8ad90de36f" }, endpoint), false)
assert.equal(sameWireguardEndpoint({ ...endpoint, password: "changed" }, endpoint), false)
const privateKey = Buffer.alloc(32, 61).toString("base64"), nextKey = Buffer.alloc(32, 62).toString("base64")
const state: WireguardInterfaceDeleteState = { base: { interfaces: [{ ".id": "*a", name: "wg-owned", disabled: "false", mtu: "1420", "listen-port": "13231", "private-key": privateKey, "public-key": wireguardPublicKey(privateKey), opaque: "NEVER_OPAQUE" }], peers: [{ ".id": "*b", interface: "wg-owned", "public-key": wireguardPublicKey(nextKey), disabled: "false", "allowed-address": "192.0.2.2/32" }], bindings: [], flow: { hostPublicKey: "", registered: false }, serverProof: "source", proof: "base" }, ipv4: [{ ".id": "*c", interface: "wg-owned", address: "192.0.2.1/24", disabled: "false", dynamic: "false" }, { ".id": "*d", interface: "wg-owned", address: "198.51.100.1/24", disabled: "false", dynamic: "false", comment: "secondary" }], ipv6: [], dependencies: Object.fromEntries(wireguardInterfaceDependencyPaths.map(path => [path, []])), filters: [], proof: "" }
state.dependencies["/interface"].push({ ".id": "*a", name: "wg-owned", type: "wg" })
state.proof = await wireguardInterfaceDeleteProof(state)
const beforeProofNormalization = structuredClone(state), scalarProof = await wireguardInterfaceUpdateProof(state)
const originalBytes = Buffer.from(privateKey, "base64"), equivalentKeys: string[] = []
for (let ignored = 0; ignored < 32; ignored++) {
const bytes = Buffer.from(originalBytes)
bytes[0] = (bytes[0] & 248) | (ignored & 7)
bytes[31] = (bytes[31] & 63) | ((ignored >> 3) << 6)
const equivalent = bytes.toString("base64"); equivalentKeys.push(equivalent)
assert.equal(wireguardPublicKey(equivalent), state.base.interfaces[0]["public-key"])
const changed = structuredClone(state); changed.base.interfaces[0]["private-key"] = equivalent
assert.equal(await wireguardInterfaceUpdateProof(changed), scalarProof, "All 32 RFC 7748 ignored-bit combinations prove one observed identity")
changed.dependencies["/interface"][0]["private-key"] = privateKey
assert.equal(await wireguardInterfaceUpdateProof(changed), scalarProof, "Corroborated generic key mirrors may differ only in ignored scalar bits")
changed.dependencies["/interface"][0]["private-key"] = equivalent
changed.base.interfaces[0]["private-key"] = privateKey
assert.equal(await wireguardInterfaceUpdateProof(changed), scalarProof)
}
assert.deepEqual(state, beforeProofNormalization, "Proof normalization must not rewrite captured or native secret material")
const meaningfulBytes = Buffer.from(originalBytes); meaningfulBytes[1] ^= 1
const meaningfulKey = meaningfulBytes.toString("base64"), inconsistent = structuredClone(state)
inconsistent.base.interfaces[0]["private-key"] = meaningfulKey
assert.notEqual(await wireguardInterfaceUpdateProof(inconsistent), scalarProof)
const rotated = structuredClone(inconsistent); rotated.base.interfaces[0]["public-key"] = wireguardPublicKey(meaningfulKey)
assert.notEqual(await wireguardInterfaceUpdateProof(rotated), scalarProof, "An actual key rotation remains visible")
for (const publicKey of [undefined, "invalid", wireguardPublicKey(nextKey)]) {
const first = structuredClone(state), second = structuredClone(state)
if (publicKey === undefined) { delete first.base.interfaces[0]["public-key"]; delete second.base.interfaces[0]["public-key"] }
else { first.base.interfaces[0]["public-key"] = publicKey; second.base.interfaces[0]["public-key"] = publicKey }
first.base.interfaces[0]["private-key"] = equivalentKeys[0]; second.base.interfaces[0]["private-key"] = equivalentKeys[1]
assert.notEqual(await wireguardInterfaceUpdateProof(first), await wireguardInterfaceUpdateProof(second), "A missing or inconsistent public identity cannot authorize key normalization")
second.dependencies["/interface"][0]["private-key"] = equivalentKeys[0]
await assert.rejects(wireguardInterfaceUpdateProof(second), "An inconsistent generic private mirror cannot be normalized")
}
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", noncanonical = privateKey.slice(0, -2) + alphabet[alphabet.indexOf(privateKey.at(-2)!) ^ 1] + "="
assert.deepEqual(Buffer.from(noncanonical, "base64"), originalBytes)
const opaqueKeys: Array<string | undefined> = [undefined, "", "********", "(hidden)", "invalid-key", noncanonical, Buffer.alloc(32).toString("base64")]
const opaqueProofs: string[] = []
for (const opaque of opaqueKeys) {
const changed = structuredClone(state)
if (opaque === undefined) delete changed.base.interfaces[0]["private-key"]; else changed.base.interfaces[0]["private-key"] = opaque
const unchanged = structuredClone(changed), proof = await wireguardInterfaceUpdateProof(changed)
assert.equal(await wireguardInterfaceUpdateProof(changed), proof); assert.deepEqual(changed, unchanged)
opaqueProofs.push(proof)
}
assert.equal(new Set(opaqueProofs).size, opaqueKeys.length, "Missing, invalid and unknown key representations remain exact opaque state")
assert.ok(opaqueProofs.every(proof => proof !== scalarProof))
const wrongGeneric = structuredClone(state); wrongGeneric.dependencies["/interface"][0]["private-key"] = meaningfulKey
await assert.rejects(wireguardInterfaceUpdateProof(wrongGeneric))
const opaqueGeneric = structuredClone(state); opaqueGeneric.base.interfaces[0]["private-key"] = "(hidden)"; opaqueGeneric.dependencies["/interface"][0]["private-key"] = "(hidden)"
await wireguardInterfaceUpdateProof(opaqueGeneric)
opaqueGeneric.dependencies["/interface"][0]["private-key"] = "********"
await assert.rejects(wireguardInterfaceUpdateProof(opaqueGeneric))
const changedOpaque = structuredClone(state); changedOpaque.base.interfaces[0].opaque = equivalentKeys[0]
assert.notEqual(await wireguardInterfaceUpdateProof(changedOpaque), scalarProof)
const all = { name: "renamed", mtu: 1380, listenPort: 23456, comment: "updated", disabled: true, privateKey: nextKey }
const commands = planWireguardInterfaceUpdate(state, "*a", all)
assert.equal(commands.length, 1)
const next = projectWireguardInterfaceUpdate(state, commands[0])
const beforeProjectedProof = structuredClone(next)
await wireguardInterfaceUpdateProof(next)
assert.deepEqual(next, beforeProjectedProof); assert.equal(commands[0].body?.["private-key"], nextKey); assert.equal(next.base.interfaces[0]["private-key"], nextKey)
assert.equal(next.base.interfaces[0]["public-key"], wireguardPublicKey(nextKey))
assert.equal(next.base.interfaces[0].opaque, "NEVER_OPAQUE")
assert.equal(next.base.peers[0].interface, "renamed")
assert.equal(next.ipv4[0].interface, "renamed")
assert.equal(next.dependencies["/interface"][0].name, "renamed")
assert.notEqual(await wireguardInterfaceDeleteProof(next), state.proof)
const generic = structuredClone(state)
generic.base.interfaces.push({ ".id": "*f", name: "wg-foreign", disabled: "true", mtu: "1420" })
generic.dependencies["/interface"].push({ ".id": "*f", name: "wg-foreign", type: "wg", mtu: "1420", disabled: "yes", opaque: "NEVER_FOREIGN_GENERIC" })
const genericProof = await wireguardInterfaceUpdateProof(generic)
const foreignKey = structuredClone(generic)
foreignKey.base.interfaces[1]["private-key"] = privateKey; foreignKey.base.interfaces[1]["public-key"] = wireguardPublicKey(privateKey)
const foreignProof = await wireguardInterfaceUpdateProof(foreignKey)
foreignKey.base.interfaces[1]["private-key"] = meaningfulKey
assert.notEqual(await wireguardInterfaceUpdateProof(foreignKey), foreignProof, "Foreign private material cannot drift under an unchanged public identity")
foreignKey.base.interfaces[1]["public-key"] = wireguardPublicKey(meaningfulKey)
assert.notEqual(await wireguardInterfaceUpdateProof(foreignKey), foreignProof, "Foreign public identity and real key rotation remain sealed")
generic.dependencies["/interface"][1]["rx-byte"] = "1234"; generic.dependencies["/interface"][1]["fp-rx-byte"] = "5678"
assert.equal(await wireguardInterfaceUpdateProof(generic), genericProof)
generic.dependencies["/interface"][1].opaque = "NEVER_CHANGED_FOREIGN"
assert.notEqual(await wireguardInterfaceUpdateProof(generic), genericProof)
generic.dependencies["/interface"][1].mtu = "1380"
await assert.rejects(wireguardInterfaceUpdateProof(generic))
const mirrors = structuredClone(state); Object.assign(mirrors.dependencies["/interface"][0], { mtu: "1420", disabled: "no" })
const projectedMirrors = projectWireguardInterfaceUpdate(mirrors, commands[0])
assert.equal(projectedMirrors.dependencies["/interface"][0].mtu, "1380"); assert.equal(projectedMirrors.dependencies["/interface"][0].disabled, "true")
await wireguardInterfaceUpdateProof(projectedMirrors)
const address = planWireguardInterfaceUpdate(state, "*a", { address: "2001:db8::1/64" })
assert.deepEqual(address.map(command => command.method), ["DELETE", "PUT"])
assert.equal(address[0].id, "*c"); assert.equal(address[1].body?.advertise, "false")
const removed = projectWireguardInterfaceUpdate(state, address[0]), added = projectWireguardInterfaceUpdate(removed, address[1], "*e")
assert.equal(added.ipv4.length, 1); assert.equal(added.ipv4[0].comment, "secondary"); assert.equal(added.ipv6[0].address, "2001:db8::1/64")
assert.deepEqual(planWireguardInterfaceUpdate(state, "*a", { address: "" }).map(command => command.id), ["*c"])
assert.deepEqual(planWireguardInterfaceUpdate(state, "*a", { address: "198.51.100.1/24" }).map(command => command.id), ["*c"])
assert.equal(planWireguardInterfaceUpdate(state, "*a", { address: "192.0.2.1/24" }).length, 0)
for (const bad of [{}, { name: "\u0000bad" }, { listenPort: 65536 }, { mtu: 0 }, { privateKey: "bad" }, { unknown: true }]) assert.equal(wireguardInterfaceUpdateSchema.safeParse(bad).success, false)
assert.equal(wireguardInterfaceUpdatePlanSchema.safeParse({ serverId: 136, interfaceId: "*a", patch: { privateKey: nextKey } }).success, false)
assert.equal(wireguardInterfaceUpdatePlanSchema.safeParse({ serverId: 136, interfaceId: "*a", patch: { privateKeyRef: "b9ae94ed-5d23-4edb-b71b-60207a110c47" } }).success, true)
assert.throws(() => planWireguardInterfaceUpdate(state, "*a", { name: "renamed", address: "192.0.2.3/24" }))
for (const mutate of [
(value: typeof state) => { value.dependencies["/interface/vlan"].push({ ".id": "*f", interface: "wg-owned" }) },
(value: typeof state) => { value.base.interfaces[0].comment = "MikrotikManager traffic-flow overlay" },
(value: typeof state) => { value.dependencies["/interface"][0][".id"] = "*f" },
(value: typeof state) => { value.ipv4[0].dynamic = "true" },
]) { const changed = structuredClone(state); mutate(changed); assert.throws(() => planWireguardInterfaceUpdate(changed, "*a", { disabled: true })) }
console.log("WireGuard interface update model: all 32 corroborated RFC7748 scalar encodings, exact opaque/invalid keys, generic mirrors, real/foreign rotation, immutable native material; full fields, rename children, addresses and dependency protections checked")
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) await acceptWireguardInterfaceUpdateModel()