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";
const RFB_BANNER_LENGTH = 12;
const VNC_AUTH_SECURITY_TYPE = 2;
const MACOS_RFB_BANNER = Buffer.from("RFB 003.889\n", "ascii");
const STANDARD_RFB_BANNER = Buffer.from("RFB 003.008\n", "ascii");
@@ -32,17 +33,47 @@ export async function createMacosVncCompatibilityProxy({
client.pipe(upstream);
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) => {
pending = Buffer.concat([pending, chunk]);
if (pending.length < RFB_BANNER_LENGTH) return;
upstream.off("data", forwardServerBanner);
const banner = pending.subarray(0, RFB_BANNER_LENGTH);
client.write(
banner.equals(MACOS_RFB_BANNER) ? STANDARD_RFB_BANNER : banner,
);
client.write(pending.subarray(RFB_BANNER_LENGTH));
upstream.pipe(client);
const remainder = pending.subarray(RFB_BANNER_LENGTH);
if (banner.equals(MACOS_RFB_BANNER)) {
client.write(STANDARD_RFB_BANNER);
pending = Buffer.alloc(0);
upstream.on("data", forwardMacosSecurityTypes);
if (remainder.length > 0) forwardMacosSecurityTypes(remainder);
} else {
client.write(banner);
client.write(remainder);
upstream.pipe(client);
}
};
upstream.on("data", forwardServerBanner);
});
@@ -32,14 +32,20 @@ async function read(socket: net.Socket, length: number): Promise<Buffer> {
}
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 selectedSecurityType = 0;
const target = net.createServer((socket) => {
socket.write("RFB 003.");
socket.write("889\n");
socket.once("data", (data) => {
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);
@@ -58,12 +64,18 @@ describe("createMacosVncCompatibilityProxy", () => {
"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(selectedSecurityType).toBe(2);
});
it("passes standard RFB banners through unchanged", async () => {
const target = net.createServer((socket) => socket.write("RFB 003.008\n"));
it("passes standard RFB negotiation through unchanged", async () => {
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);
closers.push(() => target.close());
const proxy = await createMacosVncCompatibilityProxy({
@@ -76,5 +88,28 @@ describe("createMacosVncCompatibilityProxy", () => {
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, 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]);
});
});