diff --git a/binaries/vlmcs-darwin-arm64 b/binaries/vlmcs-darwin-arm64 deleted file mode 100755 index 604724d4..00000000 Binary files a/binaries/vlmcs-darwin-arm64 and /dev/null differ diff --git a/binaries/vlmcs-darwin-x64 b/binaries/vlmcs-darwin-x64 deleted file mode 100755 index 414b4fc0..00000000 Binary files a/binaries/vlmcs-darwin-x64 and /dev/null differ diff --git a/binaries/vlmcs-linux-arm64 b/binaries/vlmcs-linux-arm64 deleted file mode 100755 index ebcc6033..00000000 Binary files a/binaries/vlmcs-linux-arm64 and /dev/null differ diff --git a/binaries/vlmcs-linux-x64 b/binaries/vlmcs-linux-x64 deleted file mode 100755 index 9a08a9a3..00000000 Binary files a/binaries/vlmcs-linux-x64 and /dev/null differ diff --git a/node-vlmcs/package.json b/node-vlmcs/package.json index 82e2021f..655ef829 100644 --- a/node-vlmcs/package.json +++ b/node-vlmcs/package.json @@ -2,7 +2,16 @@ "name": "node-vlmcs", "version": "1.0.0", "description": "Node.js implementation of vlmcs KMS client", - "main": "dist/cli.js", + "main": "dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + }, + "./cli": "./dist/cli.js" + }, + "types": "dist/index.d.ts", "bin": { "vlmcs": "dist/cli.js" }, diff --git a/node-vlmcs/src/index.ts b/node-vlmcs/src/index.ts new file mode 100644 index 00000000..94585089 --- /dev/null +++ b/node-vlmcs/src/index.ts @@ -0,0 +1,377 @@ +import * as net from 'net' +import { KmsData } from './data' +import { + guidToBuffer, + stringToGuidLE, + utf8ToUcs2, + ucs2ToUtf8, + unixTimeToFileTime, + PID_BUFFER_SIZE, + WORKSTATION_NAME_BUFFER, +} from './types' +import { + createRequestV4, + createRequestV6, + decryptResponseV4, + decryptResponseV6, +} from './kms' +import { rpcBindClient, rpcSendRequest } from './rpc' +import { get16RandomBytes } from './crypto' +import { connectToAddress } from './network' +import { logRequestVerbose, logResponseVerbose } from './output' + +const DEFAULT_TIMEOUT_MS = 5 * 1000 + +export interface VlmcsCheckParams { + host: string + port?: number + protocol?: number + edition?: number + timeout?: number + verbose?: boolean +} + +export interface VlmcsCheckResult { + host: string + content: string + delay: number + status: boolean +} + +function connectSocket( + host: string, + port: number, + timeout: number, +): Promise { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host, port }) + + const cleanup = () => { + socket.removeAllListeners('connect') + socket.removeAllListeners('error') + socket.removeAllListeners('timeout') + } + + socket.once('connect', () => { + cleanup() + socket.pause() + resolve(socket) + }) + + socket.once('error', err => { + cleanup() + socket.destroy() + reject(err) + }) + + socket.setTimeout(timeout, () => { + cleanup() + socket.destroy() + reject(new Error(`Connection timeout after ${timeout}ms`)) + }) + }) +} + +function normalizeEdition(edition: number): number { + const raw = Number(edition) + if (!Number.isFinite(raw)) return 1 + const normalized = Math.floor(raw) + if (normalized < 1 || normalized > KmsData.skuItems.length) return 1 + return normalized +} + +function normalizeProtocol( + protocol: number | undefined, + fallback: number, +): number { + const raw = Number(protocol) + if (!Number.isFinite(raw)) return fallback + const normalized = Math.floor(raw) + if (normalized < 4 || normalized > 6) return fallback + return normalized +} + +function randomWorkstationName(): string { + const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + const size = Math.floor(Math.random() * 12) + 6 + let name = 'KMS-' + for (let i = 0; i < size; i++) { + name += alphabet[Math.floor(Math.random() * alphabet.length)] + } + return name.slice(0, 63) +} + +function buildRequest({ + protocol, + edition, +}: { + protocol: number + edition: number +}): Buffer { + const sku = KmsData.skuItems[edition - 1] + const kms = KmsData.kmsItems[sku.kmsIndex] + const app = KmsData.appItems[sku.appIndex] + + const request = Buffer.alloc(236) + request.writeUInt16LE(0, 0) + request.writeUInt16LE(protocol, 2) + request.writeUInt32LE(0, 4) + request.writeUInt32LE(0x02, 8) + request.writeUInt32LE(43200, 12) + + guidToBuffer(stringToGuidLE(app.guid)!).copy(request, 16) + guidToBuffer(stringToGuidLE(sku.guid)!).copy(request, 32) + guidToBuffer(stringToGuidLE(kms.guid)!).copy(request, 48) + + const cmid = get16RandomBytes() + cmid[8] = (cmid[8] & 0x3f) | 0x80 + const d3 = cmid.readUInt16LE(6) + cmid.writeUInt16LE((d3 & 0x0fff) | 0x4000, 6) + cmid.copy(request, 64) + + request.writeUInt32LE(sku.nCountPolicy, 80) + unixTimeToFileTime().copy(request, 84) + request.fill(0, 92, 108) + + utf8ToUcs2(randomWorkstationName(), WORKSTATION_NAME_BUFFER).copy( + request, + 108, + ) + return request +} + +interface ParsedKmsResult { + status: boolean + summary: string + ePID: string + hwid: Buffer + response: any +} + +function parseKmsResponse( + rawResponse: Buffer, + rawRequest: Buffer, +): ParsedKmsResult { + const responseMajorVersion = rawResponse.readUInt16LE(2) + + if (responseMajorVersion === 4) { + const { response, result } = decryptResponseV4(rawResponse, rawRequest) + if (!result.decryptSuccess || !result.hashOK || !result.versionOK) { + return { + summary: 'Invalid KMS V4 response', + status: false, + ePID: '', + hwid: Buffer.alloc(8), + response, + } + } + const ePID = ucs2ToUtf8(response.kmsPID, PID_BUFFER_SIZE) + return { + summary: ePID, + status: true, + ePID, + hwid: Buffer.alloc(8), + response, + } + } + + const { response, result, hwid } = decryptResponseV6(rawResponse, rawRequest) + if (!result.decryptSuccess || !result.hashOK || !result.versionOK) { + return { + summary: 'Invalid KMS V5/V6 response', + status: false, + ePID: '', + hwid, + response, + } + } + + const ePID = ucs2ToUtf8(response.kmsPID, PID_BUFFER_SIZE) + if (response.majorVer > 5) { + const hwidHex = Array.from(hwid.subarray(0, 8)) + .map(b => b.toString(16).toUpperCase().padStart(2, '0')) + .join('') + return { + summary: `${ePID} (${hwidHex})`, + status: true, + ePID, + hwid, + response, + } + } + + return { + summary: ePID, + status: true, + ePID, + hwid, + response, + } +} + +function chunkToString(chunk: unknown): string { + if (typeof chunk === 'string') return chunk + if (Buffer.isBuffer(chunk)) return chunk.toString('utf8') + return String(chunk) +} + +async function captureVerboseOutput(fn: () => Promise): Promise<{ + result: T + output: string +}> { + let output = '' + + const originalStdoutWrite = process.stdout.write + const originalStderrWrite = process.stderr.write + const originalConsoleLog = console.log + const originalConsoleError = console.error + + ;(process.stdout.write as any) = ((chunk: unknown, ...args: unknown[]) => { + output += chunkToString(chunk) + const callback = args.find(arg => typeof arg === 'function') as + | ((error?: Error | null) => void) + | undefined + callback?.(null) + return true + }) as typeof process.stdout.write + ;(process.stderr.write as any) = ((chunk: unknown, ...args: unknown[]) => { + output += chunkToString(chunk) + const callback = args.find(arg => typeof arg === 'function') as + | ((error?: Error | null) => void) + | undefined + callback?.(null) + return true + }) as typeof process.stderr.write + + console.log = (...args: unknown[]) => { + output += `${args.map(chunkToString).join(' ')}\n` + } + + console.error = (...args: unknown[]) => { + output += `${args.map(chunkToString).join(' ')}\n` + } + + try { + const result = await fn() + return { result, output: output.trimEnd() } + } finally { + process.stdout.write = originalStdoutWrite + process.stderr.write = originalStderrWrite + console.log = originalConsoleLog + console.error = originalConsoleError + } +} + +export async function runVlmcs( + params: VlmcsCheckParams, +): Promise { + const { host, timeout = DEFAULT_TIMEOUT_MS, verbose = false } = params + const portRaw = Number(params.port ?? 1688) + const port = Number.isFinite(portRaw) ? Math.floor(portRaw) : 1688 + const edition = normalizeEdition(Number(params.edition ?? 1)) + + const sku = KmsData.skuItems[edition - 1] + const protocol = normalizeProtocol(params.protocol, sku.protocolVersion) + + const startedAt = Date.now() + let socket: net.Socket | null = null + + const executeCheck = async (): Promise => { + try { + const requestBase = buildRequest({ protocol, edition }) + + if (verbose) { + logRequestVerbose(requestBase) + } + + socket = verbose + ? await connectToAddress(`${host}:${port}`, 0, false) + : await connectSocket(host, port, timeout) + + if (verbose) { + process.stdout.write('\nPerforming RPC bind ...\n') + } + + const bind = await rpcBindClient(socket, verbose, true, true, true) + if (bind.status !== 0) { + return { + host, + status: false, + delay: -1, + content: `RPC bind failed: ${bind.status}`, + } + } + + if (verbose) { + process.stdout.write('... successful\n') + } + + const request = + protocol < 5 + ? createRequestV4(requestBase) + : createRequestV6(requestBase) + + if (verbose) { + process.stdout.write( + `Sending activation request (KMS V${protocol}) 1 of 1 `, + ) + } + + const rpcResult = await rpcSendRequest( + socket, + request, + bind.rpcFlags, + true, + false, + ) + + if (rpcResult.status !== 0 || !rpcResult.kmsResponse) { + return { + host, + status: false, + delay: -1, + content: `KMS request failed: 0x${(rpcResult.status >>> 0).toString(16).toUpperCase().padStart(8, '0')}`, + } + } + + const parsed = parseKmsResponse(rpcResult.kmsResponse, request) + + if (verbose && parsed.status) { + logResponseVerbose( + parsed.ePID, + parsed.hwid, + parsed.response, + rpcResult.kmsResponse.length, + ) + } + + return { + host, + status: parsed.status, + delay: parsed.status ? Date.now() - startedAt : -1, + content: parsed.summary, + } + } catch (error) { + return { + host, + status: false, + delay: -1, + content: error instanceof Error ? error.message : String(error), + } + } finally { + if (socket) { + socket.destroy() + } + } + } + + if (!verbose) { + return executeCheck() + } + + const { result, output } = await captureVerboseOutput(executeCheck) + return { + ...result, + content: output || result.content, + } +} diff --git a/node-vlmcs/src/rpc.ts b/node-vlmcs/src/rpc.ts index ed7ac8e1..d42e7aa6 100644 --- a/node-vlmcs/src/rpc.ts +++ b/node-vlmcs/src/rpc.ts @@ -6,82 +6,82 @@ * - src/rpc.h (RPC_HEADER, RPC_BIND_REQUEST, RPC_RESPONSE 等结构定义) */ -import * as net from 'net'; -import { sendData, recvData } from './network'; +import * as net from 'net' +import { sendData, recvData } from './network' // ─── 数据包类型 (参考 src/rpc.h 中的 RPC_PT_* 常量) ──────────────────────── -const RPC_PT_REQUEST = 0; // 请求 -const RPC_PT_RESPONSE = 2; // 响应 -const RPC_PT_FAULT = 3; // 错误 -const RPC_PT_BIND_REQ = 11; // 绑定请求 -const RPC_PT_BIND_ACK = 12; // 绑定确认 -const RPC_PT_ALTERCONTEXT_REQ = 14; // 修改上下文请求 -const RPC_PT_ALTERCONTEXT_ACK = 15; // 修改上下文确认 +const RPC_PT_REQUEST = 0 // 请求 +const RPC_PT_RESPONSE = 2 // 响应 +const RPC_PT_FAULT = 3 // 错误 +const RPC_PT_BIND_REQ = 11 // 绑定请求 +const RPC_PT_BIND_ACK = 12 // 绑定确认 +const RPC_PT_ALTERCONTEXT_REQ = 14 // 修改上下文请求 +const RPC_PT_ALTERCONTEXT_ACK = 15 // 修改上下文确认 // ─── 数据包标志 ───────────────────────────────────────────────────────────── -const RPC_PF_FIRST = 1; // 分片的第一个包 -const RPC_PF_LAST = 2; // 分片的最后一个包 -const RPC_PF_MULTIPLEX = 16; // 多路复用 +const RPC_PF_FIRST = 1 // 分片的第一个包 +const RPC_PF_LAST = 2 // 分片的最后一个包 +const RPC_PF_MULTIPLEX = 16 // 多路复用 // ─── 绑定确认结果码 (参考 src/rpc.h) ─────────────────────────────────────── -const RPC_BIND_ACCEPT = 0; // 接受 -const RPC_BIND_NACK = 2; // 拒绝 -const RPC_BIND_ACK = 3; // 确认(用于 BTFN 绑定时间特性协商) +const RPC_BIND_ACCEPT = 0 // 接受 +const RPC_BIND_NACK = 2 // 拒绝 +const RPC_BIND_ACK = 3 // 确认(用于 BTFN 绑定时间特性协商) // ─── RPC 头部大小 ─────────────────────────────────────────────────────────── /** RPC 头部固定大小: 16 字节 */ -const RPC_HEADER_SIZE = 16; +const RPC_HEADER_SIZE = 16 // ─── GUID 常量 (原始线路字节,无字节序转换) ───────────────────────────────── /** NDR32 传输语法 GUID */ const TransferSyntaxNDR32 = Buffer.from([ - 0x04, 0x5D, 0x88, 0x8A, 0xEB, 0x1C, 0xC9, 0x11, - 0x9F, 0xE8, 0x08, 0x00, 0x2B, 0x10, 0x48, 0x60, -]); + 0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, + 0x10, 0x48, 0x60, +]) /** KMS 接口 UUID */ const InterfaceUuid = Buffer.from([ - 0x75, 0x21, 0xC8, 0x51, 0x4E, 0x84, 0x50, 0x47, - 0xB0, 0xD8, 0xEC, 0x25, 0x55, 0x55, 0xBC, 0x06, -]); + 0x75, 0x21, 0xc8, 0x51, 0x4e, 0x84, 0x50, 0x47, 0xb0, 0xd8, 0xec, 0x25, 0x55, + 0x55, 0xbc, 0x06, +]) /** NDR64 传输语法 GUID */ const TransferSyntaxNDR64 = Buffer.from([ - 0x33, 0x05, 0x71, 0x71, 0xBA, 0xBE, 0x37, 0x49, - 0x83, 0x19, 0xB5, 0xDB, 0xEF, 0x9C, 0xCC, 0x36, -]); + 0x33, 0x05, 0x71, 0x71, 0xba, 0xbe, 0x37, 0x49, 0x83, 0x19, 0xb5, 0xdb, 0xef, + 0x9c, 0xcc, 0x36, +]) /** 绑定时间特性协商 GUID (BTFN) */ const BindTimeFeatureNegotiation = Buffer.from([ - 0x2C, 0x1C, 0xB7, 0x6C, 0x12, 0x98, 0x40, 0x45, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -]); + 0x2c, 0x1c, 0xb7, 0x6c, 0x12, 0x98, 0x40, 0x45, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, +]) // ─── 导出接口 ─────────────────────────────────────────────────────────────── /** RPC 已协商的能力标志 */ export interface RpcFlags { - hasNDR32: boolean; // 是否支持 NDR32 传输语法 - hasNDR64: boolean; // 是否支持 NDR64 传输语法 - hasBTFN: boolean; // 是否支持绑定时间特性协商 + hasNDR32: boolean // 是否支持 NDR32 传输语法 + hasNDR64: boolean // 是否支持 NDR64 传输语法 + hasBTFN: boolean // 是否支持绑定时间特性协商 } /** RPC 诊断信息 */ export interface RpcDiag { - hasRpcDiag: boolean; // 是否有 RPC 诊断信息 - hasBTFN: boolean; // 服务器是否支持 BTFN - hasNDR64: boolean; // 服务器是否支持 NDR64 + hasRpcDiag: boolean // 是否有 RPC 诊断信息 + hasBTFN: boolean // 服务器是否支持 BTFN + hasNDR64: boolean // 服务器是否支持 NDR64 } // ─── 状态 ─────────────────────────────────────────────────────────────────── /** 调用 ID,从 2 开始(与微软实现一致) */ -let callId = 2; +let callId = 2 // ─── 工具函数: 写入 RPC 头部 ─────────────────────────────────────────────── @@ -94,29 +94,29 @@ function writeRpcHeader( packetType: number, packetFlags: number, fragLength: number, - currentCallId: number + currentCallId: number, ): void { - buf.writeUInt8(5, 0); // VersionMajor = 5 - buf.writeUInt8(0, 1); // VersionMinor = 0 - buf.writeUInt8(packetType, 2); // PacketType - buf.writeUInt8(packetFlags, 3); // PacketFlags - buf.writeUInt32LE(0x00000010, 4); // DataRepresentation: LE, ASCII, IEEE - buf.writeUInt16LE(fragLength, 8); // FragLength - buf.writeUInt16LE(0, 10); // AuthLength - buf.writeUInt32LE(currentCallId, 12); // CallId + buf.writeUInt8(5, 0) // VersionMajor = 5 + buf.writeUInt8(0, 1) // VersionMinor = 0 + buf.writeUInt8(packetType, 2) // PacketType + buf.writeUInt8(packetFlags, 3) // PacketFlags + buf.writeUInt32LE(0x00000010, 4) // DataRepresentation: LE, ASCII, IEEE + buf.writeUInt16LE(fragLength, 8) // FragLength + buf.writeUInt16LE(0, 10) // AuthLength + buf.writeUInt32LE(currentCallId, 12) // CallId } // ─── 工具函数: 解析 RPC 头部 ─────────────────────────────────────────────── interface RpcHeader { - versionMajor: number; - versionMinor: number; - packetType: number; - packetFlags: number; - dataRepresentation: number; - fragLength: number; - authLength: number; - callId: number; + versionMajor: number + versionMinor: number + packetType: number + packetFlags: number + dataRepresentation: number + fragLength: number + authLength: number + callId: number } /** 从 Buffer 解析 RPC 头部 */ @@ -130,28 +130,36 @@ function parseRpcHeader(buf: Buffer): RpcHeader { fragLength: buf.readUInt16LE(8), authLength: buf.readUInt16LE(10), callId: buf.readUInt32LE(12), - }; + } } /** 获取数据包类型的可读名称(用于调试输出) */ function packetTypeName(type: number): string { switch (type) { - case RPC_PT_REQUEST: return 'Request'; - case RPC_PT_RESPONSE: return 'Response'; - case RPC_PT_FAULT: return 'Fault'; - case RPC_PT_BIND_REQ: return 'Bind'; - case RPC_PT_BIND_ACK: return 'Bind Ack'; - case RPC_PT_ALTERCONTEXT_REQ: return 'Alter Context'; - case RPC_PT_ALTERCONTEXT_ACK: return 'Alter Context Ack'; - default: return `Unknown(${type})`; + case RPC_PT_REQUEST: + return 'Request' + case RPC_PT_RESPONSE: + return 'Response' + case RPC_PT_FAULT: + return 'Fault' + case RPC_PT_BIND_REQ: + return 'Bind' + case RPC_PT_BIND_ACK: + return 'Bind Ack' + case RPC_PT_ALTERCONTEXT_REQ: + return 'Alter Context' + case RPC_PT_ALTERCONTEXT_ACK: + return 'Alter Context Ack' + default: + return `Unknown(${type})` } } // ─── 构建 Bind/AlterContext 数据包 ────────────────────────────────────────── interface CtxItem { - transferSyntax: Buffer; // 传输语法 GUID - syntaxVersion: number; // 语法版本 + transferSyntax: Buffer // 传输语法 GUID + syntaxVersion: number // 语法版本 } /** @@ -165,44 +173,44 @@ function buildBindPacket( packetType: number, packetFlags: number, currentCallId: number, - ctxItems: CtxItem[] + ctxItems: CtxItem[], ): Buffer { - const ctxItemSize = 44; + const ctxItemSize = 44 // 绑定体: MaxXmitFrag(2) + MaxRecvFrag(2) + AssocGroup(4) + NumCtxItems(4) + 上下文项数组 - const bindBodySize = 2 + 2 + 4 + 4 + ctxItems.length * ctxItemSize; - const totalSize = RPC_HEADER_SIZE + bindBodySize; - const buf = Buffer.alloc(totalSize); + const bindBodySize = 2 + 2 + 4 + 4 + ctxItems.length * ctxItemSize + const totalSize = RPC_HEADER_SIZE + bindBodySize + const buf = Buffer.alloc(totalSize) - writeRpcHeader(buf, packetType, packetFlags, totalSize, currentCallId); + writeRpcHeader(buf, packetType, packetFlags, totalSize, currentCallId) - let offset = RPC_HEADER_SIZE; - buf.writeUInt16LE(5840, offset); // MaxXmitFrag - buf.writeUInt16LE(5840, offset + 2); // MaxRecvFrag - buf.writeUInt32LE(0, offset + 4); // AssocGroup - buf.writeUInt32LE(ctxItems.length, offset + 8); // NumCtxItems - offset += 12; + let offset = RPC_HEADER_SIZE + buf.writeUInt16LE(5840, offset) // MaxXmitFrag + buf.writeUInt16LE(5840, offset + 2) // MaxRecvFrag + buf.writeUInt32LE(0, offset + 4) // AssocGroup + buf.writeUInt32LE(ctxItems.length, offset + 8) // NumCtxItems + offset += 12 for (let i = 0; i < ctxItems.length; i++) { - buf.writeUInt16LE(i, offset); // ContextId - buf.writeUInt16LE(1, offset + 2); // NumTransItems - InterfaceUuid.copy(buf, offset + 4); // InterfaceUUID - buf.writeUInt16LE(1, offset + 20); // InterfaceVerMajor - buf.writeUInt16LE(0, offset + 22); // InterfaceVerMinor - ctxItems[i].transferSyntax.copy(buf, offset + 24); // TransferSyntax - buf.writeUInt32LE(ctxItems[i].syntaxVersion, offset + 40); // SyntaxVersion - offset += ctxItemSize; + buf.writeUInt16LE(i, offset) // ContextId + buf.writeUInt16LE(1, offset + 2) // NumTransItems + InterfaceUuid.copy(buf, offset + 4) // InterfaceUUID + buf.writeUInt16LE(1, offset + 20) // InterfaceVerMajor + buf.writeUInt16LE(0, offset + 22) // InterfaceVerMinor + ctxItems[i].transferSyntax.copy(buf, offset + 24) // TransferSyntax + buf.writeUInt32LE(ctxItems[i].syntaxVersion, offset + 40) // SyntaxVersion + offset += ctxItemSize } - return buf; + return buf } // ─── 解析绑定响应 ─────────────────────────────────────────────────────────── interface BindResult { - ackResult: number; // 确认结果码 - ackReason: number; // 确认原因 - transferSyntax: Buffer; // 传输语法 - syntaxVersion: number; // 语法版本 + ackResult: number // 确认结果码 + ackReason: number // 确认原因 + transferSyntax: Buffer // 传输语法 + syntaxVersion: number // 语法版本 } /** @@ -211,54 +219,63 @@ interface BindResult { */ function parseBindResponse( body: Buffer, - verbose: boolean + verbose: boolean, ): { results: BindResult[]; maxRecvFrag: number } { - let offset = 0; + let offset = 0 - const maxXmitFrag = body.readUInt16LE(offset); - const maxRecvFrag = body.readUInt16LE(offset + 2); - const assocGroup = body.readUInt32LE(offset + 4); - offset += 8; + const maxXmitFrag = body.readUInt16LE(offset) + const maxRecvFrag = body.readUInt16LE(offset + 2) + const assocGroup = body.readUInt32LE(offset + 4) + offset += 8 - const secondaryAddressLength = body.readUInt16LE(offset); - offset += 2; + const secondaryAddressLength = body.readUInt16LE(offset) + offset += 2 if (verbose) { - console.log(` Max Xmit/Recv Frag: ${maxXmitFrag}/${maxRecvFrag}, AssocGroup: 0x${assocGroup.toString(16).padStart(8, '0')}`); - console.log(` Secondary Address Length: ${secondaryAddressLength}`); + console.log( + ` Max Xmit/Recv Frag: ${maxXmitFrag}/${maxRecvFrag}, AssocGroup: 0x${assocGroup.toString(16).padStart(8, '0')}`, + ) + console.log(` Secondary Address Length: ${secondaryAddressLength}`) } // 跳过二级地址和对齐填充到 4 字节边界 - offset += secondaryAddressLength; - const totalOff = offset; - offset += ((4 - (totalOff % 4)) % 4); + offset += secondaryAddressLength + const totalOff = offset + offset += (4 - (totalOff % 4)) % 4 - const numResults = body.readUInt32LE(offset); - offset += 4; + const numResults = body.readUInt32LE(offset) + offset += 4 if (verbose) { - console.log(` Num Results: ${numResults}`); + console.log(` Num Results: ${numResults}`) } - const results: BindResult[] = []; + const results: BindResult[] = [] for (let i = 0; i < numResults; i++) { - const ackResult = body.readUInt16LE(offset); - const ackReason = body.readUInt16LE(offset + 2); - const transferSyntax = Buffer.from(body.subarray(offset + 4, offset + 20)); - const syntaxVersion = body.readUInt32LE(offset + 20); - offset += 24; + const ackResult = body.readUInt16LE(offset) + const ackReason = body.readUInt16LE(offset + 2) + const transferSyntax = Buffer.from(body.subarray(offset + 4, offset + 20)) + const syntaxVersion = body.readUInt32LE(offset + 20) + offset += 24 if (verbose) { - const resultStr = ackResult === RPC_BIND_ACCEPT ? 'Accept' : - ackResult === RPC_BIND_NACK ? 'Nack' : - ackResult === RPC_BIND_ACK ? 'Ack' : `Unknown(0x${ackResult.toString(16)})`; - console.log(` Result[${i}]: ${resultStr} (reason: 0x${ackReason.toString(16)})`); + const resultStr = + ackResult === RPC_BIND_ACCEPT + ? 'Accept' + : ackResult === RPC_BIND_NACK + ? 'Nack' + : ackResult === RPC_BIND_ACK + ? 'Ack' + : `Unknown(0x${ackResult.toString(16)})` + console.log( + ` Result[${i}]: ${resultStr} (reason: 0x${ackReason.toString(16)})`, + ) } - results.push({ ackResult, ackReason, transferSyntax, syntaxVersion }); + results.push({ ackResult, ackReason, transferSyntax, syntaxVersion }) } - return { results, maxRecvFrag }; + return { results, maxRecvFrag } } // ─── 绑定/修改上下文 ─────────────────────────────────────────────────────── @@ -279,82 +296,96 @@ async function rpcBindOrAlterContext( rpcDiag: RpcDiag, ): Promise { // AlterContext 只发送 NDR32;Bind 发送 NDR32 + 可选 NDR64 + BTFN - const isBind = packetType === RPC_PT_BIND_REQ; + const isBind = packetType === RPC_PT_BIND_REQ const ctxItems: CtxItem[] = [ { transferSyntax: TransferSyntaxNDR32, syntaxVersion: 2 }, - ]; + ] - let ctxNDR64 = -1; - let ctxBTFN = -1; + let ctxNDR64 = -1 + let ctxBTFN = -1 if (isBind && useClientRpcNDR64) { - ctxNDR64 = ctxItems.length; - ctxItems.push({ transferSyntax: TransferSyntaxNDR64, syntaxVersion: 1 }); + ctxNDR64 = ctxItems.length + ctxItems.push({ transferSyntax: TransferSyntaxNDR64, syntaxVersion: 1 }) } if (isBind && useClientRpcBTFN) { - ctxBTFN = ctxItems.length; - ctxItems.push({ transferSyntax: BindTimeFeatureNegotiation, syntaxVersion: 1 }); + ctxBTFN = ctxItems.length + ctxItems.push({ + transferSyntax: BindTimeFeatureNegotiation, + syntaxVersion: 1, + }) } - const packetFlags = RPC_PF_FIRST | RPC_PF_LAST | (useMultiplexedRpc ? RPC_PF_MULTIPLEX : 0); - const currentCallId = callId++; - const bindPacket = buildBindPacket(packetType, packetFlags, currentCallId, ctxItems); + const packetFlags = + RPC_PF_FIRST | RPC_PF_LAST | (useMultiplexedRpc ? RPC_PF_MULTIPLEX : 0) + const currentCallId = callId++ + const bindPacket = buildBindPacket( + packetType, + packetFlags, + currentCallId, + ctxItems, + ) - await sendData(sock, bindPacket); + await sendData(sock, bindPacket) // 接收响应头 - const headerBuf = await recvData(sock, RPC_HEADER_SIZE); - const header = parseRpcHeader(headerBuf); + const headerBuf = await recvData(sock, RPC_HEADER_SIZE) + const header = parseRpcHeader(headerBuf) if (verbose) { - console.log(`Received RPC ${packetTypeName(header.packetType)} (FragLength=${header.fragLength}, CallId=${header.callId})`); + console.log( + `Received RPC ${packetTypeName(header.packetType)} (FragLength=${header.fragLength}, CallId=${header.callId})`, + ) } - const expectedAckType = isBind ? RPC_PT_BIND_ACK : RPC_PT_ALTERCONTEXT_ACK; + const expectedAckType = isBind ? RPC_PT_BIND_ACK : RPC_PT_ALTERCONTEXT_ACK - if (header.packetType === RPC_PT_FAULT || header.packetType !== expectedAckType) { - const bodySize = header.fragLength - RPC_HEADER_SIZE; - if (bodySize > 0) await recvData(sock, bodySize); - return 1; + if ( + header.packetType === RPC_PT_FAULT || + header.packetType !== expectedAckType + ) { + const bodySize = header.fragLength - RPC_HEADER_SIZE + if (bodySize > 0) await recvData(sock, bodySize) + return 1 } // 读取响应体 - const bodySize = header.fragLength - RPC_HEADER_SIZE; - const body = await recvData(sock, bodySize); - const { results } = parseBindResponse(body, verbose); + const bodySize = header.fragLength - RPC_HEADER_SIZE + const body = await recvData(sock, bodySize) + const { results } = parseBindResponse(body, verbose) // 处理每个上下文项的结果 for (let i = 0; i < results.length; i++) { - const result = results[i]; + const result = results[i] if (i === ctxBTFN) { // BTFN 上下文:结果码为 RPC_BIND_ACK(3) 表示支持 if (result.ackResult === RPC_BIND_ACK) { - rpcFlags.hasBTFN = true; - rpcDiag.hasBTFN = true; - if (verbose) process.stdout.write('... BTFN '); + rpcFlags.hasBTFN = true + rpcDiag.hasBTFN = true + if (verbose) process.stdout.write('... BTFN ') } - continue; + continue } if (result.ackResult === RPC_BIND_NACK) { - continue; // 被拒绝,跳过 + continue // 被拒绝,跳过 } if (result.ackResult === RPC_BIND_ACCEPT) { if (i === ctxNDR64) { - rpcFlags.hasNDR64 = true; - rpcDiag.hasNDR64 = true; - if (verbose) process.stdout.write('... NDR64 '); + rpcFlags.hasNDR64 = true + rpcDiag.hasNDR64 = true + if (verbose) process.stdout.write('... NDR64 ') } else if (i === 0) { - rpcFlags.hasNDR32 = true; - if (verbose) process.stdout.write('... NDR32 '); + rpcFlags.hasNDR32 = true + if (verbose) process.stdout.write('... NDR32 ') } } } - return 0; + return 0 } /** @@ -370,42 +401,62 @@ export async function rpcBindClient( verbose: boolean, useClientRpcNDR64: boolean, useClientRpcBTFN: boolean, - useMultiplexedRpc: boolean + useMultiplexedRpc: boolean, ): Promise<{ status: number; rpcDiag: RpcDiag; rpcFlags: RpcFlags }> { - const rpcFlags: RpcFlags = { hasNDR32: false, hasNDR64: false, hasBTFN: false }; - const rpcDiag: RpcDiag = { hasRpcDiag: false, hasBTFN: false, hasNDR64: false }; + const rpcFlags: RpcFlags = { + hasNDR32: false, + hasNDR64: false, + hasBTFN: false, + } + const rpcDiag: RpcDiag = { + hasRpcDiag: false, + hasBTFN: false, + hasNDR64: false, + } // 第一步: 发送 Bind 请求 let status = await rpcBindOrAlterContext( - sock, RPC_PT_BIND_REQ, verbose, - useClientRpcNDR64, useClientRpcBTFN, useMultiplexedRpc, - rpcFlags, rpcDiag - ); - - if (status) return { status, rpcDiag, rpcFlags }; + sock, + RPC_PT_BIND_REQ, + verbose, + useClientRpcNDR64, + useClientRpcBTFN, + useMultiplexedRpc, + rpcFlags, + rpcDiag, + ) + + if (status) return { status, rpcDiag, rpcFlags } // 第二步: 如果 NDR32 未被接受,发送 AlterContext 单独协商 if (!rpcFlags.hasNDR32) { status = await rpcBindOrAlterContext( - sock, RPC_PT_ALTERCONTEXT_REQ, verbose, - false, false, useMultiplexedRpc, - rpcFlags, rpcDiag - ); - if (status) return { status, rpcDiag, rpcFlags }; + sock, + RPC_PT_ALTERCONTEXT_REQ, + verbose, + false, + false, + useMultiplexedRpc, + rpcFlags, + rpcDiag, + ) + if (status) return { status, rpcDiag, rpcFlags } } if (!rpcFlags.hasNDR32 && !rpcFlags.hasNDR64) { - process.stderr.write('\nFatal: Could neither negotiate NDR32 nor NDR64 with the RPC server\n'); - return { status: 1, rpcDiag, rpcFlags }; + process.stderr.write( + '\nFatal: Could neither negotiate NDR32 nor NDR64 with the RPC server\n', + ) + return { status: 1, rpcDiag, rpcFlags } } - rpcDiag.hasRpcDiag = true; + rpcDiag.hasRpcDiag = true if (verbose) { - process.stdout.write('\n'); + process.stdout.write('\n') } - return { status: 0, rpcDiag, rpcFlags }; + return { status: 0, rpcDiag, rpcFlags } } // ─── 发送 RPC 请求 (参考 src/rpc.c 中的 rpcSendRequest) ──────────────────── @@ -422,150 +473,154 @@ export async function rpcSendRequest( kmsRequest: Buffer, rpcFlags: RpcFlags, useClientRpcNDR64: boolean, - firstPacketSent: boolean -): Promise<{ status: number; kmsResponse: Buffer | null; responseSize: number }> { - const requestSize = kmsRequest.length; - const useNDR64 = useClientRpcNDR64 && rpcFlags.hasNDR64 && firstPacketSent; - - let requestBody: Buffer; + firstPacketSent: boolean, +): Promise<{ + status: number + kmsResponse: Buffer | null + responseSize: number +}> { + const requestSize = kmsRequest.length + const useNDR64 = useClientRpcNDR64 && rpcFlags.hasNDR64 && firstPacketSent + + let requestBody: Buffer if (useNDR64) { // NDR64 格式: AllocHint(4) + ContextId(2) + Opnum(2) + DataLength(8) + DataSizeIs(8) + Data - const bodySize = 4 + 2 + 2 + 8 + 8 + requestSize; - requestBody = Buffer.alloc(bodySize); - let offset = 0; - requestBody.writeUInt32LE(requestSize + 16, offset); // AllocHint - offset += 4; - requestBody.writeUInt16LE(1, offset); // ContextId (NDR64 = 1) - offset += 2; - requestBody.writeUInt16LE(0, offset); // Opnum - offset += 2; - requestBody.writeBigUInt64LE(BigInt(requestSize), offset); // Ndr64.DataLength - offset += 8; - requestBody.writeBigUInt64LE(BigInt(requestSize), offset); // Ndr64.DataSizeIs - offset += 8; - kmsRequest.copy(requestBody, offset); + const bodySize = 4 + 2 + 2 + 8 + 8 + requestSize + requestBody = Buffer.alloc(bodySize) + let offset = 0 + requestBody.writeUInt32LE(requestSize + 16, offset) // AllocHint + offset += 4 + requestBody.writeUInt16LE(1, offset) // ContextId (NDR64 = 1) + offset += 2 + requestBody.writeUInt16LE(0, offset) // Opnum + offset += 2 + requestBody.writeBigUInt64LE(BigInt(requestSize), offset) // Ndr64.DataLength + offset += 8 + requestBody.writeBigUInt64LE(BigInt(requestSize), offset) // Ndr64.DataSizeIs + offset += 8 + kmsRequest.copy(requestBody, offset) } else { // NDR32 格式: AllocHint(4) + ContextId(2) + Opnum(2) + DataLength(4) + DataSizeIs(4) + Data - const bodySize = 4 + 2 + 2 + 4 + 4 + requestSize; - requestBody = Buffer.alloc(bodySize); - let offset = 0; - requestBody.writeUInt32LE(requestSize + 8, offset); // AllocHint - offset += 4; - requestBody.writeUInt16LE(0, offset); // ContextId (NDR32 = 0) - offset += 2; - requestBody.writeUInt16LE(0, offset); // Opnum - offset += 2; - requestBody.writeUInt32LE(requestSize, offset); // Ndr.DataLength - offset += 4; - requestBody.writeUInt32LE(requestSize, offset); // Ndr.DataSizeIs - offset += 4; - kmsRequest.copy(requestBody, offset); + const bodySize = 4 + 2 + 2 + 4 + 4 + requestSize + requestBody = Buffer.alloc(bodySize) + let offset = 0 + requestBody.writeUInt32LE(requestSize + 8, offset) // AllocHint + offset += 4 + requestBody.writeUInt16LE(0, offset) // ContextId (NDR32 = 0) + offset += 2 + requestBody.writeUInt16LE(0, offset) // Opnum + offset += 2 + requestBody.writeUInt32LE(requestSize, offset) // Ndr.DataLength + offset += 4 + requestBody.writeUInt32LE(requestSize, offset) // Ndr.DataSizeIs + offset += 4 + kmsRequest.copy(requestBody, offset) } - const totalSize = RPC_HEADER_SIZE + requestBody.length; - const packet = Buffer.alloc(totalSize); - const currentCallId = callId++; + const totalSize = RPC_HEADER_SIZE + requestBody.length + const packet = Buffer.alloc(totalSize) + const currentCallId = callId++ writeRpcHeader( packet, RPC_PT_REQUEST, RPC_PF_FIRST | RPC_PF_LAST, totalSize, - currentCallId - ); - requestBody.copy(packet, RPC_HEADER_SIZE); + currentCallId, + ) + requestBody.copy(packet, RPC_HEADER_SIZE) - await sendData(sock, packet); + await sendData(sock, packet) // ── 接收响应 ────────────────────────────────────────────────────────── - const headerBuf = await recvData(sock, RPC_HEADER_SIZE); - const header = parseRpcHeader(headerBuf); + const headerBuf = await recvData(sock, RPC_HEADER_SIZE) + const header = parseRpcHeader(headerBuf) // 处理 Fault 响应 if (header.packetType === RPC_PT_FAULT) { - const bodySize = header.fragLength - RPC_HEADER_SIZE; + const bodySize = header.fragLength - RPC_HEADER_SIZE if (bodySize >= 4) { - const faultBody = await recvData(sock, bodySize); - const faultStatus = faultBody.readUInt32LE(0); - return { status: faultStatus || 1, kmsResponse: null, responseSize: 0 }; + const faultBody = await recvData(sock, bodySize) + const faultStatus = faultBody.readUInt32LE(0) + return { status: faultStatus || 1, kmsResponse: null, responseSize: 0 } } - if (bodySize > 0) await recvData(sock, bodySize); - return { status: 1, kmsResponse: null, responseSize: 0 }; + if (bodySize > 0) await recvData(sock, bodySize) + return { status: 1, kmsResponse: null, responseSize: 0 } } // 验证响应类型 if (header.packetType !== RPC_PT_RESPONSE) { - const bodySize = header.fragLength - RPC_HEADER_SIZE; - if (bodySize > 0) await recvData(sock, bodySize); - return { status: 1, kmsResponse: null, responseSize: 0 }; + const bodySize = header.fragLength - RPC_HEADER_SIZE + if (bodySize > 0) await recvData(sock, bodySize) + return { status: 1, kmsResponse: null, responseSize: 0 } } // 读取完整响应体 - const bodySize = header.fragLength - RPC_HEADER_SIZE; - const body = await recvData(sock, bodySize); + const bodySize = header.fragLength - RPC_HEADER_SIZE + const body = await recvData(sock, bodySize) // 解析 NDR 响应头 // 参考: src/rpc.h 中的 RPC_RESPONSE / RPC_RESPONSE64 结构 - let offset = 0; - const allocHint = body.readUInt32LE(offset); // AllocHint - offset += 4; - const contextId = body.readUInt16LE(offset); // ContextId - offset += 2; - const cancelCount = body.readUInt8(offset); // CancelCount - offset += 1; - offset += 1; // Pad1 + let offset = 0 + const allocHint = body.readUInt32LE(offset) // AllocHint + offset += 4 + const contextId = body.readUInt16LE(offset) // ContextId + offset += 2 + const cancelCount = body.readUInt8(offset) // CancelCount + offset += 1 + offset += 1 // Pad1 - const responseUsesNDR64 = contextId === 1; + const responseUsesNDR64 = contextId === 1 - let dataLength: number; - let dataSizeMax: bigint | number; - let dataSizeIs: bigint | number; + let dataLength: number + let dataSizeMax: bigint | number + let dataSizeIs: bigint | number if (responseUsesNDR64) { // NDR64 响应: DataLength(8) + DataSizeMax(8) + DataSizeIs(8) - dataLength = Number(body.readBigUInt64LE(offset)); - offset += 8; - dataSizeMax = body.readBigUInt64LE(offset); - offset += 8; - dataSizeIs = body.readBigUInt64LE(offset); - offset += 8; + dataLength = Number(body.readBigUInt64LE(offset)) + offset += 8 + dataSizeMax = body.readBigUInt64LE(offset) + offset += 8 + dataSizeIs = body.readBigUInt64LE(offset) + offset += 8 } else { // NDR32 响应: DataLength(4) + DataSizeMax(4) + DataSizeIs(4) - dataLength = body.readUInt32LE(offset); - offset += 4; - dataSizeMax = body.readUInt32LE(offset); - offset += 4; - dataSizeIs = body.readUInt32LE(offset); - offset += 4; + dataLength = body.readUInt32LE(offset) + offset += 4 + dataSizeMax = body.readUInt32LE(offset) + offset += 4 + dataSizeIs = body.readUInt32LE(offset) + offset += 4 } // 如果 dataSizeMax 为 0,说明 RPC 调用返回了错误状态 if (dataSizeMax === 0 || dataSizeMax === 0n) { - const errorStatus = Number(dataSizeIs); - return { status: errorStatus || 1, kmsResponse: null, responseSize: 0 }; + const errorStatus = Number(dataSizeIs) + return { status: errorStatus || 1, kmsResponse: null, responseSize: 0 } } if (dataLength <= 0 || offset + dataLength > body.length) { - return { status: 1, kmsResponse: null, responseSize: 0 }; + return { status: 1, kmsResponse: null, responseSize: 0 } } // 提取 KMS 响应数据 - const kmsResponse = Buffer.from(body.subarray(offset, offset + dataLength)); - offset += dataLength; + const kmsResponse = Buffer.from(body.subarray(offset, offset + dataLength)) + offset += dataLength // 4 字节对齐填充 - const padBytes = (4 - (dataLength % 4)) % 4; - offset += padBytes; + const padBytes = (4 - (dataLength % 4)) % 4 + offset += padBytes // 读取 ReturnCode (HRESULT) - let returnCode = 0; + let returnCode = 0 if (offset + 4 <= body.length) { - returnCode = body.readUInt32LE(offset); + returnCode = body.readUInt32LE(offset) } if (returnCode !== 0) { - return { status: returnCode, kmsResponse, responseSize: dataLength }; + return { status: returnCode, kmsResponse, responseSize: dataLength } } - return { status: 0, kmsResponse, responseSize: dataLength }; + return { status: 0, kmsResponse, responseSize: dataLength } } diff --git a/package.json b/package.json index a0432d99..e8591df6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare" + "postinstall": "pnpm -C node-vlmcs build && nuxt prepare" }, "dependencies": { "@iconify-json/flag": "^1.2.11", @@ -21,6 +21,7 @@ "arco-design-nuxt-module": "^0.2.1", "echarts": "^6.0.0", "motion-v": "^2.0.1", + "node-vlmcs": "workspace:*", "nuxt": "^4.4.2", "nuxt-echarts": "^1.0.1", "prettier": "^3.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 841e4f66..6d53547d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: motion-v: specifier: ^2.0.1 version: 2.0.1(@vueuse/core@14.2.1(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3)) + node-vlmcs: + specifier: workspace:* + version: link:node-vlmcs nuxt: specifier: ^4.4.2 version: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.6)(@types/node@22.15.19)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.10.0)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@6.0.11(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.19.2)(typescript@5.9.3)(vite@7.3.1(@types/node@22.15.19)(jiti@2.6.1)(terser@5.46.0)(tsx@4.19.2)(yaml@2.8.2))(yaml@2.8.2) diff --git a/server/api/check.ts b/server/api/check.ts index 9f94b913..edd9986d 100644 --- a/server/api/check.ts +++ b/server/api/check.ts @@ -11,6 +11,7 @@ export default defineEventHandler(async event => { port, protocol, edition, + verbose: true, }) return result diff --git a/server/utils/kms.ts b/server/utils/kms.ts index 8ee3acdb..6c9f4ece 100644 --- a/server/utils/kms.ts +++ b/server/utils/kms.ts @@ -1,5 +1,4 @@ -import { execFile } from 'child_process' -import { arch, platform } from 'os' +import { runVlmcs as runNodeVlmcs } from 'node-vlmcs' const defaultMonitorList = [ 'kms.8b5.cn', @@ -27,8 +26,7 @@ export const getMonitorList = (() => { return () => { if (cached) return cached const config = useRuntimeConfig() - const listStr = - (config.monitorList as string) || process.env.MONITOR_LIST + const listStr = (config.monitorList as string) || process.env.MONITOR_LIST cached = listStr?.split(',').filter(Boolean) || defaultMonitorList return cached } @@ -39,30 +37,13 @@ export const runVlmcs = ({ port = 1688, protocol = 6, edition = 26, + verbose = false, }: RunVlmcsParams) => { - return new Promise((resolve, reject) => { - const before = Date.now() - const vlmcs = execFile( - `./binaries/vlmcs-${platform()}-${arch()}`, - [`${host}:${port}`, `-${protocol}`, `-l ${edition}`], - { timeout: 5 * 1000 }, - (err, stdout) => { - resolve({ - host, - delay: err ? -1 : Date.now() - before, - content: stdout.trim(), - status: err ? false : true, - }) - }, - ) - - vlmcs.on('error', err => { - reject(err) - }) - - vlmcs.on('close', () => { - vlmcs.removeAllListeners() - vlmcs.kill() - }) - }) + return runNodeVlmcs({ + host, + port: Number(port), + protocol: Number(protocol), + edition: Number(edition), + verbose, + }) as Promise } diff --git a/shared/types/kms.ts b/shared/types/kms.ts index 57739fe2..723e92e9 100644 --- a/shared/types/kms.ts +++ b/shared/types/kms.ts @@ -3,6 +3,7 @@ export interface RunVlmcsParams { port?: number protocol?: number edition?: number + verbose?: boolean } export interface RunVlmcsResult {