139 lines
15 KiB
TypeScript
139 lines
15 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { createServer } from "node:http"
|
|
import { randomUUID } from "node:crypto"
|
|
import type { FastifyInstance } from "fastify"
|
|
import { SignJWT } from "jose"
|
|
import { env } from "../../config.js"
|
|
import { setTimeout as delay } from "node:timers/promises"
|
|
import { mcpConnectionCreateSchema } from "@mmapp/contracts/mcp"
|
|
import { pool } from "../../db/index.js"
|
|
import type { AuthUser } from "../../lib/permissions.js"
|
|
import { createMcpConnection, authenticateMcpToken, setConnectionStatus } from "./store.js"
|
|
import { invokeMcpTool, approvePlan, applyPlan, loadPlan, publicPlan } from "./runtime.js"
|
|
import { humanWireguardExportMetadata, readHumanWireguardExportArtifact, readWireguardExportSource, verifyWireguardExportArtifact } from "./wireguard-export-artifact.js"
|
|
|
|
/** Synthetic SQL/loopback fixture and authenticated owner portal downloads. */
|
|
export async function acceptWireguardExportMcp(actor: AuthUser, app: FastifyInstance) {
|
|
const serverId=143, owner={...actor,id:"wireguard-export-owner",permissions:[...new Set([...actor.permissions,"mm:network:write"])]}
|
|
const serverPrivate=Buffer.alloc(32,71).toString("base64"), serverPublic=Buffer.alloc(32,72).toString("base64"), peerPrivate=Buffer.alloc(32,73).toString("base64"), peerPublic=Buffer.alloc(32,74).toString("base64")
|
|
let writes=0,reads=0,peerDisabled=false,keepalive="0",comment="synthetic export",oversized=false,malformed=false,afterRead:(()=>Promise<unknown>)|undefined
|
|
const rows=(path:string):Record<string,string>[]=>({
|
|
"/interface/wireguard":[{".id":"*wg6",name:"wg-export","listen-port":"13231",mtu:"1420",disabled:"false","private-key":serverPrivate,"public-key":serverPublic,comment}],
|
|
"/interface":[{".id":"*if3",name:"wg-export",type:"wg",disabled:"false",running:"true"}],
|
|
"/interface/wireguard/peers":[{".id":"*p1",interface:"*if3","public-key":peerPublic,"private-key":peerPrivate,"allowed-address":"192.0.2.2/32","persistent-keepalive":keepalive,disabled:String(peerDisabled),"client-address":"192.0.2.2/32","client-dns":"1.1.1.1"}],
|
|
"/ip/address":[{".id":"*a1",interface:"*if3",address:"192.0.2.1/32",disabled:"false",dynamic:"false"}],"/ipv6/address":[],
|
|
} as Record<string,Record<string,string>[]>)[path]??[]
|
|
const router=createServer(async(req,res)=>{
|
|
res.setHeader("content-type","application/json");reads++
|
|
if(afterRead){const run=afterRead;afterRead=undefined;await run()}
|
|
if(req.method!=="GET"){writes++;res.statusCode=405;res.end("{}");return}
|
|
if(oversized){res.end(JSON.stringify("я".repeat(600000)));return}
|
|
if(malformed){res.end("{NEVER_PROVIDER_BODY");return}
|
|
res.end(JSON.stringify(rows((req.url??"").replace(/^\/rest/,""))))
|
|
})
|
|
await new Promise<void>(resolve=>router.listen(0,"127.0.0.1",resolve))
|
|
let connectionId:string|undefined
|
|
try{
|
|
await pool.query("INSERT INTO servers(id,name,host,port,use_ssl,password) VALUES($1,'wg-export-fixture','127.0.0.1',$2,false,'NEVER_ROUTER_PASSWORD')",[serverId,(router.address() as {port:number}).port])
|
|
const access=await createMcpConnection(mcpConnectionCreateSchema.parse({name:"WG export fixture",mode:"autonomous",expiresAt:new Date(Date.now()+1800000).toISOString(),permissions:["mm:network:write"],scope:{serverIds:[serverId],operationIds:["network.wireguard.export"],allowGlobal:false},limits:{callsPerMinute:120,mutationsPerHour:100}}),owner)
|
|
connectionId=access.connection.id
|
|
const principal=await authenticateMcpToken(access.token),request={serverId,interfaceName:"wg-export",format:"conf" as "conf"|"rsc"|"peer-conf",includePrivateKey:true}
|
|
const prepare=(body=request)=>invokeMcpTool(principal,"mm_wireguard_export_plan",body) as Promise<{planId:string;state:string}>
|
|
const wait=async(id:string)=>{for(let n=0;n<600;n++){const row=(await pool.query("SELECT state,result,error_code FROM mcp_operations WHERE id=$1",[id])).rows[0];if(!["pending","running"].includes(row.state))return row;await delay(25)}throw Error("Synthetic export remains active; do not replay")}
|
|
const hidden=(value:unknown)=>{const serialized=JSON.stringify(value);for(const marker of [serverPrivate,peerPrivate,"NEVER_ROUTER_PASSWORD","NEVER_PROVIDER_BODY"])assert.ok(!serialized.includes(marker),"private material must not appear in model/audit data")}
|
|
const plan=await prepare();assert.equal(plan.state,"awaiting-approval");hidden(plan);hidden(await publicPlan(await loadPlan(plan.planId)))
|
|
await assert.rejects(applyPlan(principal,plan.planId,randomUUID()))
|
|
await approvePlan(plan.planId,owner,true)
|
|
const key=randomUUID(),operation=await applyPlan(principal,plan.planId,key),done=await wait(operation.id)
|
|
assert.equal(done.state,"applied",JSON.stringify(done));hidden(done)
|
|
const receipt=(await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[operation.id])).rows[0].result
|
|
assert.equal(receipt.humanDownloadOnly,true);assert.equal(receipt.wireguardContentReturned,false)
|
|
const stored=(await pool.query("SELECT * FROM mcp_artifacts WHERE id=$1",[receipt.artifactId])).rows[0]
|
|
hidden(stored);assert.equal(stored.access_scope.kind,"wireguard-export");assert.ok(stored.expires_at.getTime()<=Date.now()+900000)
|
|
const authority=stored.access_scope.authority,proof=stored.access_scope.sourceProof
|
|
assert.equal(await verifyWireguardExportArtifact(receipt.artifactId,authority,proof),true)
|
|
const token=async(id=owner.id,permissions=owner.permissions)=>new SignJWT({sub:id,apps:["mm"],permissions}).setProtectedHeader({alg:"HS256"}).setIssuer(env.authIssuer).setExpirationTime("1h").sign(new TextEncoder().encode(env.jwtSecret))
|
|
const headers={authorization:"Bearer "+await token()}
|
|
const beforeInvalid=reads
|
|
for(const invalid of [{serverId:"abc"},{serverId:0},{serverId:1.5},{interfaceName:"wg\nunsafe"},{interfaceName:"x".repeat(201)},{peerId:""},{includePrivateKey:"true"},{unknown:"value"}]){
|
|
const rejected=await app.inject({method:"POST",url:"/api/wireguard/export",headers,payload:{...request,...invalid}})
|
|
assert.equal(rejected.statusCode,400,rejected.body);hidden(rejected.body)
|
|
}
|
|
assert.equal(reads,beforeInvalid,"invalid REST inputs fail at the shared boundary before RouterOS reads")
|
|
const download=()=>app.inject({url:receipt.downloadPath,headers})
|
|
const response=await download();assert.equal(response.statusCode,200,response.body)
|
|
assert.equal(response.headers["cache-control"],"private, no-store")
|
|
assert.equal(response.headers["content-disposition"],'attachment; filename="wg-export.conf"')
|
|
assert.ok(response.body.includes(serverPrivate));assert.ok(!response.body.includes(peerPrivate))
|
|
assert.deepEqual(response.rawPayload,(await readWireguardExportSource(authority)).content)
|
|
const listing=await app.inject({url:`/api/mcp/operations?connectionId=${connectionId}`,headers})
|
|
assert.equal(listing.statusCode,200,listing.body);hidden(listing.json())
|
|
assert.equal(listing.json().items.find((item:{id:string})=>item.id===operation.id).artifactReceipts[0].artifactId,receipt.artifactId)
|
|
const beforeDenied=reads
|
|
for(const deniedHeaders of [{},{authorization:"Bearer "+access.token},{authorization:"Bearer "+await token("foreign-admin")},{authorization:"Bearer "+await token(owner.id,["mm:mcp:read"])}]){
|
|
const denied=await app.inject({url:receipt.downloadPath,headers:deniedHeaders});assert.ok([401,403].includes(denied.statusCode));hidden(denied.body)
|
|
}
|
|
assert.equal(reads,beforeDenied,"unauthorized downloads never read native source")
|
|
for(const [field,value] of [["filename","tampered.conf"],["ciphertext",stored.ciphertext+"x"],["access_scope",{...stored.access_scope,kind:"logs"}]] as const){
|
|
await pool.query(`UPDATE mcp_artifacts SET ${field}=$2${field==="access_scope"?"::jsonb":""} WHERE id=$1`,[receipt.artifactId,field==="access_scope"?JSON.stringify(value):value])
|
|
const denied=await download();assert.equal(denied.statusCode,409);hidden(denied.body)
|
|
await pool.query("UPDATE mcp_artifacts SET filename=$2,ciphertext=$3,access_scope=$4::jsonb WHERE id=$1",[receipt.artifactId,stored.filename,stored.ciphertext,JSON.stringify(stored.access_scope)])
|
|
}
|
|
await pool.query("UPDATE mcp_artifacts SET expires_at=now()-interval '1 second' WHERE id=$1",[receipt.artifactId]);assert.equal((await download()).statusCode,404)
|
|
await pool.query("UPDATE mcp_artifacts SET expires_at=$2 WHERE id=$1",[receipt.artifactId,stored.expires_at])
|
|
await assert.rejects(humanWireguardExportMetadata(receipt.artifactId,{...owner,id:"foreign-admin"}))
|
|
await assert.rejects(humanWireguardExportMetadata(receipt.artifactId,{...owner,permissions:[]}))
|
|
await pool.query("UPDATE mcp_operation_steps SET state='applied' WHERE operation_id=$1",[operation.id])
|
|
await assert.rejects(humanWireguardExportMetadata(receipt.artifactId,owner),"metadata requires exact generic VERIFIED checkpoint")
|
|
assert.equal((await download()).statusCode,409)
|
|
await pool.query("UPDATE mcp_operation_steps SET state='verified' WHERE operation_id=$1",[operation.id])
|
|
const beforeList=reads;await humanWireguardExportMetadata(receipt.artifactId,owner);assert.equal(reads,beforeList,"receipt metadata never reads source or ciphertext")
|
|
await applyPlan(principal,plan.planId,key)
|
|
assert.equal((await pool.query("SELECT count(*)::int AS count FROM mcp_artifacts WHERE connection_id=$1",[connectionId])).rows[0].count,1)
|
|
for(const format of ["conf","rsc","peer-conf"] as const)for(const includePrivateKey of [false,true]){
|
|
keepalive=format==="conf"&&!includePrivateKey?"00:00:25":"0"
|
|
const next=await prepare({...request,format,includePrivateKey});await approvePlan(next.planId,owner,true)
|
|
const op=await applyPlan(principal,next.planId,randomUUID());assert.equal((await wait(op.id)).state,"applied")
|
|
const fileReceipt=(await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[op.id])).rows[0].result
|
|
const file=await app.inject({url:fileReceipt.downloadPath,headers});assert.equal(file.statusCode,200,file.body)
|
|
assert.equal(file.body.includes(serverPrivate),includePrivateKey&&format!=="peer-conf");assert.ok(!file.body.includes(peerPrivate))
|
|
if(keepalive!=="0")assert.ok(file.body.includes("PersistentKeepalive = 25"))
|
|
const rest=await app.inject({method:"POST",url:"/api/wireguard/export",headers,payload:{...request,serverId:String(serverId),format,includePrivateKey,...(format==="peer-conf"?{peerId:`${serverId}:*p1`}:{})}})
|
|
assert.equal(rest.statusCode,200,rest.body);assert.equal(rest.headers["cache-control"],"private, no-store")
|
|
assert.equal(rest.json().content,file.body);assert.equal(rest.json().filename,format==="rsc"?"wg-export.rsc":format==="conf"?"wg-export.conf":"wg-export-peer.conf")
|
|
}
|
|
keepalive="0"
|
|
const omitted=await app.inject({method:"POST",url:"/api/wireguard/export",headers,payload:{serverId:String(serverId),interfaceName:"wg-export",format:"rsc"}})
|
|
assert.equal(omitted.statusCode,200,omitted.body);assert.ok(!omitted.json().content.includes(serverPrivate))
|
|
for(const duration of ["25s","00:00:25"]){
|
|
keepalive=duration
|
|
for(const format of ["rsc","conf","peer-conf"] as const){const rest=await app.inject({method:"POST",url:"/api/wireguard/export",headers,payload:{...request,format,includePrivateKey:false}});assert.equal(rest.statusCode,200,rest.body);assert.ok(rest.json().content.includes(format==="rsc"?"persistent-keepalive=25":"PersistentKeepalive = 25"))}
|
|
}
|
|
keepalive="0"
|
|
console.log("✓ WG export contract delta: malformed REST inputs return 400 before native reads; numeric/unit/clock keepalives share peer codec, all REST formats and approved MCP owner download preserve 25 seconds")
|
|
const cancelledDownload=new AbortController();cancelledDownload.abort();await assert.rejects(readHumanWireguardExportArtifact(receipt.artifactId,owner,undefined,cancelledDownload.signal))
|
|
comment="changed source";assert.equal(await verifyWireguardExportArtifact(receipt.artifactId,authority,proof),false);comment="synthetic export"
|
|
await pool.query("UPDATE mcp_artifacts SET ciphertext=ciphertext || 'x' WHERE id=$1",[receipt.artifactId]);assert.equal(await verifyWireguardExportArtifact(receipt.artifactId,authority,proof),false);await pool.query("UPDATE mcp_artifacts SET ciphertext=$2 WHERE id=$1",[receipt.artifactId,stored.ciphertext])
|
|
const protection=randomUUID();await pool.query("INSERT INTO mcp_protections(id,object_ref,created_by) VALUES($1,$2::jsonb,$3)",[protection,JSON.stringify({kind:"wireguard-export",id:"wg-export",serverId,protection:"deny"}),owner.id]);await assert.rejects(humanWireguardExportMetadata(receipt.artifactId,owner));await pool.query("DELETE FROM mcp_protections WHERE id=$1",[protection])
|
|
peerDisabled=true;await assert.rejects(prepare(),"conf cannot silently enable disabled peers");hidden(await prepare({...request,format:"rsc"}));peerDisabled=false
|
|
oversized=true;await assert.rejects(prepare());oversized=false;malformed=true;await assert.rejects(prepare());malformed=false
|
|
const controller=new AbortController();controller.abort();await assert.rejects(readWireguardExportSource(authority,async()=>{},controller.signal))
|
|
afterRead=()=>pool.query("UPDATE servers SET password='changed fixture endpoint' WHERE id=$1",[serverId]);await assert.rejects(prepare(),(error:{code?:string})=>error.code==="STATE_CONFLICT")
|
|
await pool.query("UPDATE servers SET password='NEVER_ROUTER_PASSWORD' WHERE id=$1",[serverId])
|
|
assert.equal(await verifyWireguardExportArtifact(receipt.artifactId,authority,proof),false,"endpoint ABA invalidates source epoch")
|
|
const fresh=await prepare();await approvePlan(fresh.planId,owner,true)
|
|
const freshOp=await applyPlan(principal,fresh.planId,randomUUID());assert.equal((await wait(freshOp.id)).state,"applied")
|
|
const freshReceipt=(await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1",[freshOp.id])).rows[0].result
|
|
const originalQuery=pool.query.bind(pool);let lateRevocation=false
|
|
pool.query=((...args:unknown[])=>{const pending=(originalQuery as (...args:unknown[])=>Promise<unknown>)(...args);if(!lateRevocation&&String(args[0]).includes("MCP WireGuard artifact payload")){lateRevocation=true;return pending.then(async row=>{await setConnectionStatus(connectionId!,"paused",owner);return row})}return pending}) as typeof pool.query
|
|
try{await assert.rejects(readHumanWireguardExportArtifact(freshReceipt.artifactId,owner));assert.equal(lateRevocation,true)}finally{pool.query=originalQuery as typeof pool.query}
|
|
await setConnectionStatus(connectionId,"active",owner)
|
|
assert.equal((await app.inject({url:freshReceipt.downloadPath,headers})).statusCode,409,"policy ABA invalidates old packages")
|
|
hidden((await pool.query("SELECT payload FROM mcp_audit WHERE connection_id=$1",[connectionId])).rows);assert.equal(writes,0)
|
|
console.log("✓ Synthetic WG export portal: explicit private-key opt-in for conf/rsc only, human approval, encrypted 15-minute package, owner JWT download/no model or audit keys, no-store, anonymous/MCP/foreign/permission denial before native reads, exact VERIFIED metadata, tamper/expiry/cancel/late revoke/source/protection/ABA; no native writes")
|
|
}finally{
|
|
if(connectionId){await setConnectionStatus(connectionId,"paused",owner);await pool.query("DELETE FROM mcp_artifacts WHERE connection_id=$1",[connectionId])}
|
|
await pool.query("DELETE FROM servers WHERE id=$1",[serverId]);router.closeAllConnections();await new Promise<void>((resolve,reject)=>router.close(error=>error?reject(error):resolve()))
|
|
}
|
|
}
|