mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-30 18:32:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8678c8ab1 |
No files matched your search
@@ -98,6 +98,34 @@ function runWhenProxyConnectionReady(socket, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Bytes after the proxy reply (often the SSH banner) can share the reply's
|
||||
// TCP packet. Put them back for ssh2. Pause first so the tail stays queued
|
||||
// until the SSH client resumes the socket.
|
||||
function restoreTrailingSocketBytes(socket, trailing) {
|
||||
if (!trailing || trailing.length === 0) return;
|
||||
socket.pause();
|
||||
socket.unshift(Buffer.from(trailing));
|
||||
}
|
||||
|
||||
// SOCKS5 connect reply: VER REP RSV ATYP BND.ADDR BND.PORT.
|
||||
// Returns the reply size, null when the buffer is short, or -1 when ATYP is
|
||||
// not a form we can bound.
|
||||
function socks5ConnectReplyLength(buffer) {
|
||||
if (buffer.length < 4) return null;
|
||||
const addressType = buffer[3];
|
||||
let addressLength;
|
||||
if (addressType === 0x01) addressLength = 4;
|
||||
else if (addressType === 0x04) addressLength = 16;
|
||||
else if (addressType === 0x03) {
|
||||
if (buffer.length < 5) return null;
|
||||
addressLength = 1 + buffer[4];
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
const total = 4 + addressLength + 2;
|
||||
return buffer.length < total ? null : total;
|
||||
}
|
||||
|
||||
function createProxyCommandSocket(proxy, targetHost, targetPort, options = {}) {
|
||||
const command = substituteProxyCommand(proxy.command, targetHost, targetPort, { platform: process.platform }).trim();
|
||||
if (!command) return Promise.reject(new Error("ProxyCommand is required"));
|
||||
@@ -238,16 +266,18 @@ function createProxySocket(proxy, targetHost, targetPort, options = {}) {
|
||||
const connectRequest = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${authHeader}\r\n`;
|
||||
socket.write(connectRequest);
|
||||
|
||||
let response = '';
|
||||
let pending = Buffer.alloc(0);
|
||||
const onData = (data) => {
|
||||
response += data.toString();
|
||||
if (response.includes('\r\n\r\n')) {
|
||||
socket.removeListener('data', onData);
|
||||
if (response.startsWith('HTTP/1.1 200') || response.startsWith('HTTP/1.0 200')) {
|
||||
settleResolve();
|
||||
} else {
|
||||
settleReject(new Error(`HTTP proxy error: ${response.split('\r\n')[0]}`));
|
||||
}
|
||||
pending = pending.length === 0 ? data : Buffer.concat([pending, data]);
|
||||
const headerEnd = pending.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) return;
|
||||
socket.removeListener('data', onData);
|
||||
const header = pending.toString('latin1', 0, headerEnd);
|
||||
if (header.startsWith('HTTP/1.1 200') || header.startsWith('HTTP/1.0 200')) {
|
||||
restoreTrailingSocketBytes(socket, pending.subarray(headerEnd + 4));
|
||||
settleResolve();
|
||||
} else {
|
||||
settleReject(new Error(`HTTP proxy error: ${header.split('\r\n')[0]}`));
|
||||
}
|
||||
};
|
||||
socket.on('data', onData);
|
||||
@@ -265,6 +295,7 @@ function createProxySocket(proxy, targetHost, targetPort, options = {}) {
|
||||
socket.write(Buffer.from([0x05, authMethods.length, ...authMethods]));
|
||||
|
||||
let step = 'greeting';
|
||||
let connectReply = Buffer.alloc(0);
|
||||
const onData = (data) => {
|
||||
if (step === 'greeting') {
|
||||
if (data[0] !== 0x05) {
|
||||
@@ -298,8 +329,18 @@ function createProxySocket(proxy, targetHost, targetPort, options = {}) {
|
||||
step = 'connect';
|
||||
sendConnectRequest();
|
||||
} else if (step === 'connect') {
|
||||
connectReply = connectReply.length === 0
|
||||
? data
|
||||
: Buffer.concat([connectReply, data]);
|
||||
const replyLength = socks5ConnectReplyLength(connectReply);
|
||||
if (replyLength === null) return;
|
||||
socket.removeListener('data', onData);
|
||||
if (data[1] === 0x00) {
|
||||
if (replyLength < 0) {
|
||||
settleReject(new Error('Invalid SOCKS5 connect reply'));
|
||||
return;
|
||||
}
|
||||
if (connectReply[1] === 0x00) {
|
||||
restoreTrailingSocketBytes(socket, connectReply.subarray(replyLength));
|
||||
settleResolve();
|
||||
} else {
|
||||
const errors = {
|
||||
@@ -312,7 +353,7 @@ function createProxySocket(proxy, targetHost, targetPort, options = {}) {
|
||||
0x07: 'Command not supported',
|
||||
0x08: 'Address type not supported',
|
||||
};
|
||||
settleReject(new Error(`SOCKS5 error: ${errors[data[1]] || 'Unknown'}`));
|
||||
settleReject(new Error(`SOCKS5 error: ${errors[connectReply[1]] || 'Unknown'}`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ const assert = require("node:assert/strict");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const { once } = require("node:events");
|
||||
const net = require("node:net");
|
||||
const { Client } = require("ssh2");
|
||||
|
||||
const {
|
||||
createProxySocket,
|
||||
@@ -10,6 +11,52 @@ const {
|
||||
substituteProxyCommand,
|
||||
} = require("./proxyUtils.cjs");
|
||||
|
||||
const SSH_BANNER = Buffer.from("SSH-2.0-Fixture\r\n");
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
function closeServer(server) {
|
||||
return new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
// ssh2 only records the peer banner after it parses an identification line.
|
||||
// "Connection lost before handshake" means those bytes never reached it.
|
||||
function remoteIdent(socket) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = new Client();
|
||||
let settled = false;
|
||||
const finish = (error, ident) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
setImmediate(() => {
|
||||
try { client.destroy(); } catch { /* ignore */ }
|
||||
});
|
||||
if (error) reject(error);
|
||||
else resolve(ident);
|
||||
};
|
||||
const timer = setTimeout(() => finish(new Error("Timed out waiting for remote SSH ident")), 1500);
|
||||
timer.unref();
|
||||
client.on("error", (err) => {
|
||||
if (/Connection lost before handshake/.test(err.message)) finish(err);
|
||||
});
|
||||
client.connect({
|
||||
sock: socket,
|
||||
username: "fixture",
|
||||
readyTimeout: 1500,
|
||||
debug(message) {
|
||||
const match = /Remote ident: (.*)/.exec(message);
|
||||
if (match) finish(null, match[1]);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("substituteProxyCommand replaces OpenSSH-style host and port tokens for POSIX shells", () => {
|
||||
assert.equal(
|
||||
substituteProxyCommand(
|
||||
@@ -183,3 +230,57 @@ test("createProxySocket times out stalled HTTP proxy handshakes", async (t) => {
|
||||
);
|
||||
assert.equal(socketDestroyed, true);
|
||||
});
|
||||
|
||||
test("HTTP CONNECT keeps an SSH banner coalesced with the proxy reply", async (t) => {
|
||||
const sockets = new Set();
|
||||
const proxy = net.createServer((socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("error", () => {});
|
||||
socket.once("data", () => {
|
||||
socket.write(Buffer.concat([
|
||||
Buffer.from("HTTP/1.1 200 Connection Established\r\n\r\n"),
|
||||
SSH_BANNER,
|
||||
]));
|
||||
});
|
||||
});
|
||||
t.after(async () => {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await closeServer(proxy);
|
||||
});
|
||||
const port = await listen(proxy);
|
||||
const socket = await createProxySocket(
|
||||
{ type: "http", host: "127.0.0.1", port },
|
||||
"203.0.113.10",
|
||||
22,
|
||||
);
|
||||
sockets.add(socket);
|
||||
assert.match(await remoteIdent(socket), /SSH-2\.0-Fixture/);
|
||||
});
|
||||
|
||||
test("SOCKS5 keeps an SSH banner coalesced with the connect reply", async (t) => {
|
||||
const sockets = new Set();
|
||||
const proxy = net.createServer((socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("error", () => {});
|
||||
socket.once("data", () => {
|
||||
socket.write(Buffer.from([0x05, 0x00]));
|
||||
socket.once("data", () => {
|
||||
// IPv4 bind address is 10 bytes; the banner shares this write.
|
||||
const reply = Buffer.from([0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0, 22]);
|
||||
socket.write(Buffer.concat([reply, SSH_BANNER]));
|
||||
});
|
||||
});
|
||||
});
|
||||
t.after(async () => {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await closeServer(proxy);
|
||||
});
|
||||
const port = await listen(proxy);
|
||||
const socket = await createProxySocket(
|
||||
{ type: "socks5", host: "127.0.0.1", port },
|
||||
"203.0.113.10",
|
||||
22,
|
||||
);
|
||||
sockets.add(socket);
|
||||
assert.match(await remoteIdent(socket), /SSH-2\.0-Fixture/);
|
||||
});
|
||||
Reference in new issue
Block a user