fix: force classic auth for macOS VNC (#1313)

This commit is contained in:
Neo
2026-08-24 05:40:45 +08:00
committed by GitHub
parent b3cc66efdf
commit c4c9b51294
2 changed files with 76 additions and 10 deletions
+36 -5
View File
@@ -1,6 +1,7 @@
import net from "net"; import net from "net";
const RFB_BANNER_LENGTH = 12; const RFB_BANNER_LENGTH = 12;
const VNC_AUTH_SECURITY_TYPE = 2;
const MACOS_RFB_BANNER = Buffer.from("RFB 003.889\n", "ascii"); const MACOS_RFB_BANNER = Buffer.from("RFB 003.889\n", "ascii");
const STANDARD_RFB_BANNER = Buffer.from("RFB 003.008\n", "ascii"); const STANDARD_RFB_BANNER = Buffer.from("RFB 003.008\n", "ascii");
@@ -32,17 +33,47 @@ export async function createMacosVncCompatibilityProxy({
client.pipe(upstream); client.pipe(upstream);
let pending = Buffer.alloc(0); let pending = Buffer.alloc(0);
const forwardMacosSecurityTypes = (chunk: Buffer) => {
pending = Buffer.concat([pending, chunk]);
if (pending.length < 1) return;
const securityTypeCount = pending[0];
if (securityTypeCount === 0) {
upstream.off("data", forwardMacosSecurityTypes);
client.write(pending);
upstream.pipe(client);
return;
}
if (pending.length < 1 + securityTypeCount) return;
upstream.off("data", forwardMacosSecurityTypes);
const securityTypes = pending.subarray(1, 1 + securityTypeCount);
if (securityTypes.includes(VNC_AUTH_SECURITY_TYPE)) {
client.write(Buffer.from([1, VNC_AUTH_SECURITY_TYPE]));
} else {
client.write(pending.subarray(0, 1 + securityTypeCount));
}
client.write(pending.subarray(1 + securityTypeCount));
upstream.pipe(client);
};
const forwardServerBanner = (chunk: Buffer) => { const forwardServerBanner = (chunk: Buffer) => {
pending = Buffer.concat([pending, chunk]); pending = Buffer.concat([pending, chunk]);
if (pending.length < RFB_BANNER_LENGTH) return; if (pending.length < RFB_BANNER_LENGTH) return;
upstream.off("data", forwardServerBanner); upstream.off("data", forwardServerBanner);
const banner = pending.subarray(0, RFB_BANNER_LENGTH); const banner = pending.subarray(0, RFB_BANNER_LENGTH);
client.write( const remainder = pending.subarray(RFB_BANNER_LENGTH);
banner.equals(MACOS_RFB_BANNER) ? STANDARD_RFB_BANNER : banner, if (banner.equals(MACOS_RFB_BANNER)) {
); client.write(STANDARD_RFB_BANNER);
client.write(pending.subarray(RFB_BANNER_LENGTH)); pending = Buffer.alloc(0);
upstream.pipe(client); upstream.on("data", forwardMacosSecurityTypes);
if (remainder.length > 0) forwardMacosSecurityTypes(remainder);
} else {
client.write(banner);
client.write(remainder);
upstream.pipe(client);
}
}; };
upstream.on("data", forwardServerBanner); upstream.on("data", forwardServerBanner);
}); });
@@ -32,14 +32,20 @@ async function read(socket: net.Socket, length: number): Promise<Buffer> {
} }
describe("createMacosVncCompatibilityProxy", () => { describe("createMacosVncCompatibilityProxy", () => {
it("normalizes Apple's private RFB banner and preserves later traffic", async () => { it("normalizes Apple's private RFB banner and forces classic VNC auth", async () => {
let clientBanner = ""; let clientBanner = "";
let selectedSecurityType = 0;
const target = net.createServer((socket) => { const target = net.createServer((socket) => {
socket.write("RFB 003."); socket.write("RFB 003.");
socket.write("889\n"); socket.write("889\n");
socket.once("data", (data) => { socket.once("data", (data) => {
clientBanner = data.toString("ascii"); clientBanner = data.toString("ascii");
socket.write("security-types"); socket.write(Buffer.from([5, 30, 33]));
socket.write(Buffer.from([36, 2, 35]));
socket.once("data", (selection) => {
selectedSecurityType = selection[0];
socket.write("desktop-data");
});
}); });
}); });
const targetPort = await listen(target); const targetPort = await listen(target);
@@ -58,12 +64,18 @@ describe("createMacosVncCompatibilityProxy", () => {
"RFB 003.008\n", "RFB 003.008\n",
); );
client.write("RFB 003.008\n"); client.write("RFB 003.008\n");
expect((await read(client, 14)).toString("ascii")).toBe("security-types"); expect([...(await read(client, 2))]).toEqual([1, 2]);
client.write(Buffer.from([2]));
expect((await read(client, 12)).toString("ascii")).toBe("desktop-data");
expect(clientBanner).toBe("RFB 003.008\n"); expect(clientBanner).toBe("RFB 003.008\n");
expect(selectedSecurityType).toBe(2);
}); });
it("passes standard RFB banners through unchanged", async () => { it("passes standard RFB negotiation through unchanged", async () => {
const target = net.createServer((socket) => socket.write("RFB 003.008\n")); const target = net.createServer((socket) => {
socket.write("RFB 003.008\n");
socket.once("data", () => socket.write(Buffer.from([2, 30, 2])));
});
const targetPort = await listen(target); const targetPort = await listen(target);
closers.push(() => target.close()); closers.push(() => target.close());
const proxy = await createMacosVncCompatibilityProxy({ const proxy = await createMacosVncCompatibilityProxy({
@@ -76,5 +88,28 @@ describe("createMacosVncCompatibilityProxy", () => {
const client = net.createConnection(proxy.port, "127.0.0.1"); const client = net.createConnection(proxy.port, "127.0.0.1");
closers.push(() => client.destroy()); closers.push(() => client.destroy());
expect((await read(client, 12)).toString("ascii")).toBe("RFB 003.008\n"); expect((await read(client, 12)).toString("ascii")).toBe("RFB 003.008\n");
client.write("RFB 003.008\n");
expect([...(await read(client, 3))]).toEqual([2, 30, 2]);
});
it("passes Apple's security types through when VNC auth is unavailable", async () => {
const target = net.createServer((socket) => {
socket.write("RFB 003.889\n");
socket.once("data", () => socket.write(Buffer.from([2, 30, 33])));
});
const targetPort = await listen(target);
closers.push(() => target.close());
const proxy = await createMacosVncCompatibilityProxy({
targetHost: "127.0.0.1",
targetPort,
bindHost: "127.0.0.1",
});
closers.push(proxy.close);
const client = net.createConnection(proxy.port, "127.0.0.1");
closers.push(() => client.destroy());
expect((await read(client, 12)).toString("ascii")).toBe("RFB 003.008\n");
client.write("RFB 003.008\n");
expect([...(await read(client, 3))]).toEqual([2, 30, 33]);
}); });
}); });