mirror of
https://github.com/quarkslab/proxyblob
synced 2026-06-08 16:52:59 +00:00
5b21bc7cda
Compile the agent to WebAssembly for deployment in JS runtimes (Bun, Node.js, or any runtime implementing the TCPDial/UDPListen interface). - Split network and UDP code with js/native build tags - Extract UDP relay into a platform-agnostic shared layer - Expose runtime-agnostic TCPDial and UDPListen JS hooks - Add azure-sdk-for-go fork as submodule for WASM-compatible mmf fallback - Bump aznet dependency - Document WASM build and JS interface in README
253 lines
7.0 KiB
Go
253 lines
7.0 KiB
Go
// Package proxy implements a SOCKS proxy server.
|
|
// It accepts client connections and forwards traffic through transport channels
|
|
// to remote agents. The server manages connection lifecycle and bidirectional
|
|
// data transfer.
|
|
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"proxyblob/pkg/protocol"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// ProxyServer implements a SOCKS proxy server that forwards traffic transparently.
|
|
// It accepts client connections and manages the protocol flow between clients and
|
|
// remote agents.
|
|
type ProxyServer struct {
|
|
// BaseHandler provides common protocol functionality
|
|
*protocol.BaseHandler
|
|
|
|
// Listener accepts incoming TCP connections
|
|
Listener net.Listener
|
|
}
|
|
|
|
// NewProxyServer creates a proxy server instance with the given connection.
|
|
// The connection is used for communication with remote agents.
|
|
func NewProxyServer(ctx context.Context, conn net.Conn) *ProxyServer {
|
|
server := &ProxyServer{}
|
|
server.BaseHandler = protocol.NewBaseHandler(ctx, conn)
|
|
server.PacketHandler = server
|
|
return server
|
|
}
|
|
|
|
// Start begins listening for client connections on the specified address.
|
|
// It launches background goroutines for accepting connections and processing
|
|
// protocol messages. If listening fails, the server is stopped.
|
|
func (s *ProxyServer) Start(address string) {
|
|
var err error
|
|
s.Listener, err = net.Listen("tcp", address)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("addr", address).Msg("Failed to listen on address")
|
|
s.Stop()
|
|
return
|
|
}
|
|
|
|
go s.ReceiveLoop()
|
|
go s.acceptLoop()
|
|
}
|
|
|
|
// Stop gracefully terminates the proxy server by closing all active
|
|
// connections, canceling the handler's context, and stopping the listener.
|
|
func (s *ProxyServer) Stop() {
|
|
s.CloseAllConnections()
|
|
s.Cancel()
|
|
if s.Listener != nil {
|
|
s.Listener.Close()
|
|
}
|
|
}
|
|
|
|
// OnNew handles new connection requests. The server is the only one initiating
|
|
// connections, so this always returns ErrUnexpectedPacket.
|
|
func (s *ProxyServer) OnNew(connectionID uuid.UUID, data []byte) byte {
|
|
return protocol.ErrUnexpectedPacket
|
|
}
|
|
|
|
// OnAck processes connection acknowledgments from agents.
|
|
// Returns an error code indicating success or failure.
|
|
func (s *ProxyServer) OnAck(connectionID uuid.UUID, data []byte) byte {
|
|
value, ok := s.Connections.Load(connectionID)
|
|
if !ok {
|
|
return protocol.ErrConnectionNotFound
|
|
}
|
|
conn := value.(*protocol.Connection)
|
|
|
|
// Check if connection already established (ProtocolConn should be nil for new connections)
|
|
if conn.ProtocolConn != nil {
|
|
return protocol.ErrInvalidState
|
|
}
|
|
|
|
// Create the virtual protocol connection (marks connection as established)
|
|
conn.ProtocolConn = protocol.NewProtocolConn(s.Ctx, connectionID, s.BaseHandler)
|
|
conn.StartDelivery()
|
|
conn.LastActivity = time.Now()
|
|
return protocol.ErrNone
|
|
}
|
|
|
|
// OnData processes data received from agents and forwards it to the client.
|
|
// Returns an error code indicating success or failure.
|
|
func (s *ProxyServer) OnData(connectionID uuid.UUID, data []byte) byte {
|
|
value, ok := s.Connections.Load(connectionID)
|
|
if !ok {
|
|
return protocol.ErrConnectionNotFound
|
|
}
|
|
conn := value.(*protocol.Connection)
|
|
conn.LastActivity = time.Now()
|
|
|
|
// Non-blocking delivery to per-connection goroutine
|
|
if conn.ProtocolConn != nil {
|
|
if !conn.Deliver(data) {
|
|
return protocol.ErrBufferFull
|
|
}
|
|
}
|
|
return protocol.ErrNone
|
|
}
|
|
|
|
// OnClose handles connection termination from agents. It cleans up the
|
|
// connection state.
|
|
func (s *ProxyServer) OnClose(connectionID uuid.UUID, errorCode byte) byte {
|
|
value, ok := s.Connections.Load(connectionID)
|
|
if !ok {
|
|
return protocol.ErrNone // Connection already removed, nothing to do
|
|
}
|
|
conn := value.(*protocol.Connection)
|
|
conn.Close()
|
|
s.Connections.Delete(connectionID)
|
|
return protocol.ErrNone
|
|
}
|
|
|
|
// cleanupConnection closes all connection resources and removes from connection map.
|
|
// This helper reduces code duplication in handleConnection.
|
|
func (s *ProxyServer) cleanupConnection(connID uuid.UUID, clientConn net.Conn, proxyConn *protocol.Connection) {
|
|
clientConn.Close()
|
|
if proxyConn.ProtocolConn != nil {
|
|
proxyConn.ProtocolConn.Close()
|
|
}
|
|
proxyConn.Close()
|
|
s.Connections.Delete(connID)
|
|
}
|
|
|
|
// acceptLoop accepts incoming TCP connections and spawns goroutines to handle
|
|
// each one. It continues until the context is canceled or a non-temporary
|
|
// error occurs.
|
|
func (s *ProxyServer) acceptLoop() {
|
|
for {
|
|
select {
|
|
case <-s.Ctx.Done():
|
|
return
|
|
default:
|
|
conn, err := s.Listener.Accept()
|
|
if err != nil {
|
|
if s.Ctx.Err() != nil {
|
|
return // Exit quietly on shutdown
|
|
}
|
|
|
|
if _, ok := err.(net.Error); ok {
|
|
continue // Retry on temporary network errors
|
|
}
|
|
return
|
|
}
|
|
|
|
go s.handleConnection(conn)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleConnection processes a new client connection by:
|
|
// - Generating a unique connection ID
|
|
// - Initiating connection with remote agent
|
|
// - Setting up bidirectional data forwarding
|
|
// - Managing connection lifecycle and cleanup
|
|
func (s *ProxyServer) handleConnection(clientConn net.Conn) {
|
|
defer clientConn.Close()
|
|
|
|
// Enable TCP_NODELAY to disable Nagle's algorithm for better TLS performance
|
|
if tcpConn, ok := clientConn.(*net.TCPConn); ok {
|
|
tcpConn.SetNoDelay(true)
|
|
}
|
|
|
|
connID := uuid.New()
|
|
proxyConn := protocol.NewConnection(connID)
|
|
s.Connections.Store(proxyConn.ID, proxyConn)
|
|
|
|
// 1. Initiate connection with the agent
|
|
errCode := s.SendNewConnection(connID)
|
|
if errCode != protocol.ErrNone {
|
|
s.Connections.Delete(connID)
|
|
return
|
|
}
|
|
|
|
// 2. Wait for agent acknowledgment with timeout (ProtocolConn will be created in OnAck)
|
|
timeout := time.After(30 * time.Second)
|
|
ticker := time.NewTicker(50 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-s.Ctx.Done():
|
|
s.SendClose(connID, protocol.ErrHandlerStopped)
|
|
s.Connections.Delete(connID)
|
|
return
|
|
case <-timeout:
|
|
s.SendClose(connID, protocol.ErrTransportTimeout)
|
|
s.Connections.Delete(connID)
|
|
return
|
|
case <-ticker.C:
|
|
if proxyConn.ProtocolConn != nil {
|
|
// Agent acknowledged connection
|
|
goto connected
|
|
}
|
|
}
|
|
}
|
|
|
|
connected:
|
|
// 3. Connection established, start bidirectional forwarding using io.Copy
|
|
errCh := make(chan error, 2)
|
|
|
|
// Client → Agent
|
|
go func() {
|
|
_, err := io.Copy(proxyConn.ProtocolConn, clientConn)
|
|
errCh <- err
|
|
}()
|
|
|
|
// Agent → Client
|
|
go func() {
|
|
_, err := io.Copy(clientConn, proxyConn.ProtocolConn)
|
|
errCh <- err
|
|
}()
|
|
|
|
// Wait for BOTH directions to complete before cleaning up
|
|
var err1, err2 error
|
|
for i := 0; i < 2; i++ {
|
|
select {
|
|
case <-s.Ctx.Done():
|
|
s.cleanupConnection(connID, clientConn, proxyConn)
|
|
return
|
|
case <-proxyConn.Closed:
|
|
s.cleanupConnection(connID, clientConn, proxyConn)
|
|
return
|
|
case err := <-errCh:
|
|
if err1 == nil {
|
|
err1 = err
|
|
} else {
|
|
err2 = err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Both directions finished - NOW clean up
|
|
s.cleanupConnection(connID, clientConn, proxyConn)
|
|
|
|
// Log errors if any
|
|
for _, err := range []error{err1, err2} {
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
log.Debug().Err(err).Str("conn_id", connID.String()).Msg("Connection closed with error")
|
|
}
|
|
}
|
|
}
|