mirror of
https://github.com/quarkslab/proxyblob
synced 2026-06-08 16:52:59 +00:00
Add WASM agent support for JavaScript runtimes
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
This commit is contained in:
Submodule
+1
Submodule pkg/azure-sdk added at 2bbb11c897
+62
-24
@@ -41,6 +41,9 @@ type BaseHandler struct {
|
||||
// conn handles underlying packet transmission (direct net.Conn)
|
||||
conn net.Conn
|
||||
|
||||
// writeCh buffers encoded packets for the writeLoop goroutine
|
||||
writeCh chan []byte
|
||||
|
||||
// Connections maps UUIDs to active Connection objects
|
||||
Connections sync.Map
|
||||
|
||||
@@ -64,11 +67,14 @@ func NewBaseHandler(parentCtx context.Context, conn net.Conn) *BaseHandler {
|
||||
parentCtx = context.Background()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
return &BaseHandler{
|
||||
conn: conn,
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
h := &BaseHandler{
|
||||
conn: conn,
|
||||
writeCh: make(chan []byte, 1024),
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
}
|
||||
go h.writeLoop()
|
||||
return h
|
||||
}
|
||||
|
||||
// ReceiveLoop processes incoming packets until EOF or context cancellation.
|
||||
@@ -127,18 +133,21 @@ func (h *BaseHandler) ReceiveLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
packet := Decode(data)
|
||||
if packet == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errCode := h.handlePacket(packet)
|
||||
if errCode != ErrNone {
|
||||
if h.Ctx.Err() != nil {
|
||||
continue
|
||||
// Decode all packets in the message (write coalescing may batch multiple)
|
||||
for len(data) >= HeaderSize {
|
||||
packet, rest := DecodeNext(data)
|
||||
if packet == nil {
|
||||
break
|
||||
}
|
||||
// Async close dispatch to avoid blocking ReceiveLoop on aznet writes
|
||||
go h.SendClose(packet.ConnectionID, errCode)
|
||||
errCode := h.handlePacket(packet)
|
||||
if errCode != ErrNone {
|
||||
if h.Ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
// Async close dispatch to avoid blocking ReceiveLoop on aznet writes
|
||||
go h.SendClose(packet.ConnectionID, errCode)
|
||||
}
|
||||
data = rest
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,8 +205,8 @@ func (h *BaseHandler) SendClose(connectionID uuid.UUID, errCode byte) byte {
|
||||
return h.sendPacket(CmdClose, connectionID, []byte{errCode})
|
||||
}
|
||||
|
||||
// sendPacket is the internal method that encodes and sends all packet types.
|
||||
// It handles error checking and context checking.
|
||||
// sendPacket encodes a packet and submits it to the write coalescing channel.
|
||||
// The actual aznet write happens asynchronously in writeLoop.
|
||||
func (h *BaseHandler) sendPacket(cmd byte, connectionID uuid.UUID, data []byte) byte {
|
||||
if h.Ctx.Err() != nil {
|
||||
return ErrHandlerStopped
|
||||
@@ -213,15 +222,44 @@ func (h *BaseHandler) sendPacket(cmd byte, connectionID uuid.UUID, data []byte)
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
|
||||
_, err := h.conn.Write(encoded)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return ErrTransportClosed
|
||||
}
|
||||
return ErrTransportError
|
||||
select {
|
||||
case h.writeCh <- encoded:
|
||||
return ErrNone
|
||||
case <-h.Ctx.Done():
|
||||
return ErrHandlerStopped
|
||||
}
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
// writeLoop coalesces queued packets and writes them in batches to aznet.
|
||||
// Only this goroutine calls conn.Write, eliminating fmu contention.
|
||||
func (h *BaseHandler) writeLoop() {
|
||||
for {
|
||||
var buf []byte
|
||||
|
||||
// Wait for the first packet
|
||||
select {
|
||||
case first := <-h.writeCh:
|
||||
buf = append(buf, first...)
|
||||
case <-h.Ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
// Drain all queued packets (non-blocking)
|
||||
for {
|
||||
select {
|
||||
case more := <-h.writeCh:
|
||||
buf = append(buf, more...)
|
||||
default:
|
||||
goto flush
|
||||
}
|
||||
}
|
||||
|
||||
flush:
|
||||
if _, err := h.conn.Write(buf); err != nil {
|
||||
h.Cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) CloseAllConnections() {
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewConnection(id uuid.UUID) *Connection {
|
||||
return &Connection{
|
||||
ID: id,
|
||||
Closed: make(chan struct{}),
|
||||
deliverCh: make(chan []byte, 256),
|
||||
deliverCh: make(chan []byte, 1024),
|
||||
CreatedAt: time.Now(),
|
||||
LastActivity: time.Now(),
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewProtocolConn(ctx context.Context, id uuid.UUID, handler *BaseHandler) *P
|
||||
return &ProtocolConn{
|
||||
id: id,
|
||||
handler: handler,
|
||||
readBuffer: make(chan []byte, 100), // Buffered channel for received data
|
||||
readBuffer: make(chan []byte, 1024), // Buffered channel for received data
|
||||
closed: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
@@ -112,3 +112,35 @@ func Decode(data []byte) *Packet {
|
||||
|
||||
return NewPacket(command, id, packetData)
|
||||
}
|
||||
|
||||
// DecodeNext parses the first packet from a buffer that may contain multiple
|
||||
// concatenated packets (from write coalescing). Returns the decoded packet and
|
||||
// the remaining unconsumed bytes. Returns nil packet if data is incomplete or invalid.
|
||||
func DecodeNext(data []byte) (*Packet, []byte) {
|
||||
if len(data) < HeaderSize {
|
||||
return nil, data
|
||||
}
|
||||
|
||||
command := data[0]
|
||||
if command < CmdNew || command > CmdClose {
|
||||
return nil, data
|
||||
}
|
||||
|
||||
var id uuid.UUID
|
||||
copy(id[:], data[CommandSize:CommandSize+UUIDSize])
|
||||
|
||||
dataLength := binary.BigEndian.Uint32(data[CommandSize+UUIDSize : HeaderSize])
|
||||
|
||||
totalSize := HeaderSize + int(dataLength)
|
||||
if len(data) < totalSize {
|
||||
return nil, data
|
||||
}
|
||||
|
||||
var packetData []byte
|
||||
if dataLength > 0 {
|
||||
packetData = make([]byte, dataLength)
|
||||
copy(packetData, data[HeaderSize:totalSize])
|
||||
}
|
||||
|
||||
return NewPacket(command, id, packetData), data[totalSize:]
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ func (s *ProxyServer) handleConnection(clientConn net.Conn) {
|
||||
}
|
||||
|
||||
// 2. Wait for agent acknowledgment with timeout (ProtocolConn will be created in OnAck)
|
||||
timeout := time.After(5 * time.Second)
|
||||
timeout := time.After(30 * time.Second)
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"proxyblob/pkg/protocol"
|
||||
)
|
||||
@@ -40,8 +39,8 @@ func (h *SocksHandler) handleConnect(conn *protocol.Connection, cmdData []byte)
|
||||
|
||||
fmt.Printf("[CONNECT] %s\n", target)
|
||||
|
||||
// Establish TCP connection to target
|
||||
targetConn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
||||
// Establish TCP connection to target (uses Bun bridge on WASM, native net on other platforms)
|
||||
targetConn, err := dialTCP(target)
|
||||
if err != nil {
|
||||
// Map network error to appropriate protocol error code
|
||||
errCode = protocol.MapNetError(err)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
//go:build js
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"syscall/js"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jsConn struct {
|
||||
socket js.Value
|
||||
pr *io.PipeReader
|
||||
pw *io.PipeWriter
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func dialTCP(target string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
conn := &jsConn{
|
||||
pr: pr,
|
||||
pw: pw,
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
|
||||
onConnect := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
conn.socket = args[0]
|
||||
select {
|
||||
case done <- nil:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
onData := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
jsData := args[1]
|
||||
data := make([]byte, jsData.Length())
|
||||
js.CopyBytesToGo(data, jsData)
|
||||
go pw.Write(data) // don't block the JS callback
|
||||
return nil
|
||||
})
|
||||
|
||||
onClose := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
pw.CloseWithError(io.EOF)
|
||||
select {
|
||||
case <-conn.closed:
|
||||
default:
|
||||
close(conn.closed)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
onError := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
msg := "connection error"
|
||||
if len(args) > 0 && args[0].Type() == js.TypeString {
|
||||
msg = args[0].String()
|
||||
}
|
||||
e := fmt.Errorf("%s", msg)
|
||||
pw.CloseWithError(e)
|
||||
select {
|
||||
case done <- e:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
js.Global().Call("TCPDial", host, port, onConnect, onData, onClose, onError)
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *jsConn) Read(b []byte) (int, error) { return c.pr.Read(b) }
|
||||
|
||||
func (c *jsConn) Write(b []byte) (int, error) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, net.ErrClosed
|
||||
default:
|
||||
}
|
||||
jsData := js.Global().Get("Uint8Array").New(len(b))
|
||||
js.CopyBytesToJS(jsData, b)
|
||||
c.socket.Call("write", jsData)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *jsConn) Close() error {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return nil
|
||||
default:
|
||||
close(c.closed)
|
||||
c.pr.CloseWithError(net.ErrClosed)
|
||||
c.pw.CloseWithError(net.ErrClosed)
|
||||
c.socket.Call("end")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *jsConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
|
||||
func (c *jsConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
|
||||
func (c *jsConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c *jsConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c *jsConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !js
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func dialTCP(target string) (net.Conn, error) {
|
||||
return net.DialTimeout("tcp", target, 10*time.Second)
|
||||
}
|
||||
@@ -4,310 +4,153 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"proxyblob/pkg/protocol"
|
||||
)
|
||||
|
||||
// handleUDPAssociate processes the SOCKS5 UDP ASSOCIATE command.
|
||||
// It creates a UDP relay that allows clients to send and receive
|
||||
// UDP datagrams through the SOCKS server.
|
||||
//
|
||||
// The process involves:
|
||||
// 1. Creating a UDP socket for client communication
|
||||
// 2. Sending the socket address back to the client
|
||||
// 3. Maintaining the TCP control connection
|
||||
// 4. Relaying UDP datagrams between client and targets
|
||||
//
|
||||
// The command format follows RFC 1928 Section 4.
|
||||
// It creates a single UDP relay socket, tells the client which port to use,
|
||||
// then dispatches all relay logic to handleUDPPackets.
|
||||
func (h *SocksHandler) handleUDPAssociate(conn *protocol.Connection) byte {
|
||||
// Create UDP socket
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0")
|
||||
udpConn, err := listenUDP()
|
||||
if err != nil {
|
||||
// Send network unreachable error
|
||||
h.SendError(conn, protocol.ErrNetworkUnreachable)
|
||||
return protocol.ErrNetworkUnreachable
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
// Map network error to appropriate protocol error code
|
||||
errCode := protocol.MapNetError(err)
|
||||
h.SendError(conn, errCode)
|
||||
return errCode
|
||||
}
|
||||
port := udpConn.LocalPort()
|
||||
|
||||
// Get the allocated port
|
||||
localAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||
port := localAddr.Port
|
||||
|
||||
// Create response
|
||||
// Format: |VER|REP|RSV|ATYP|BND.ADDR|BND.PORT|
|
||||
// Send success response: VER REP RSV ATYP BND.ADDR(4) BND.PORT(2)
|
||||
response := []byte{
|
||||
Version5, // VER
|
||||
Succeeded, // REP - success
|
||||
0, // RSV - reserved, must be 0
|
||||
IPv4, // ATYP - IPv4
|
||||
0, 0, 0, 0, // BND.ADDR - 0.0.0.0 (any address)
|
||||
byte(port >> 8), // BND.PORT - high byte
|
||||
byte(port & 0xff), // BND.PORT - low byte
|
||||
Version5, Succeeded, 0, IPv4,
|
||||
0, 0, 0, 0,
|
||||
byte(port >> 8), byte(port & 0xff),
|
||||
}
|
||||
|
||||
errCode := h.SendData(conn.ID, response)
|
||||
if errCode != protocol.ErrNone {
|
||||
if errCode := h.SendData(conn.ID, response); errCode != protocol.ErrNone {
|
||||
udpConn.Close()
|
||||
return protocol.ErrPacketSendFailed
|
||||
}
|
||||
|
||||
// Store UDP connection and start handling packets (state is already established via ProtocolConn)
|
||||
conn.Conn = udpConn
|
||||
go h.handleUDPPackets(conn, udpConn)
|
||||
|
||||
go h.handleUDPPackets(conn)
|
||||
|
||||
// Keep the control connection open until it's closed elsewhere
|
||||
// Hold the control connection open; close the socket when context dies.
|
||||
select {
|
||||
case <-conn.Closed:
|
||||
// Control connection closed, UDP associate terminated
|
||||
case <-h.Ctx.Done():
|
||||
udpConn.Close()
|
||||
}
|
||||
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
// handleUDPPackets manages the UDP relay for a client.
|
||||
// It:
|
||||
// - Receives UDP packets from the client
|
||||
// - Extracts target addresses from SOCKS headers
|
||||
// - Forwards packets to targets
|
||||
// - Receives responses from targets
|
||||
// - Wraps responses in SOCKS headers
|
||||
// - Returns them to the client
|
||||
// handleUDPPackets relays UDP datagrams between the SOCKS client and internet targets.
|
||||
//
|
||||
// The relay operates until the control connection closes or
|
||||
// the context is canceled.
|
||||
func (h *SocksHandler) handleUDPPackets(conn *protocol.Connection) {
|
||||
udpConn := conn.Conn.(*net.UDPConn)
|
||||
buffer := make([]byte, 16*1024) // Reduced from 64KB to 16KB
|
||||
// A single socket handles both directions:
|
||||
// - Packets from clientAddr → strip SOCKS5 header → forward to target
|
||||
// - Packets from a known target → wrap in SOCKS5 header → forward to clientAddr
|
||||
func (h *SocksHandler) handleUDPPackets(conn *protocol.Connection, udpConn UDPRelayConn) {
|
||||
defer udpConn.Close()
|
||||
|
||||
buf := make([]byte, 16*1024)
|
||||
var clientAddr *net.UDPAddr
|
||||
|
||||
// Create a UDP connection for sending to targets
|
||||
targetConn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
h.SendClose(conn.ID, protocol.ErrNetworkUnreachable)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure connections are properly closed when this handler exits
|
||||
defer targetConn.Close()
|
||||
|
||||
// Map to track target addresses and their corresponding responses
|
||||
type targetInfo struct {
|
||||
addr *net.UDPAddr
|
||||
lastActive time.Time
|
||||
}
|
||||
targets := make(map[string]*targetInfo)
|
||||
|
||||
// Channel for receiving UDP responses
|
||||
responses := make(chan struct {
|
||||
data []byte
|
||||
addr *net.UDPAddr
|
||||
}, 100)
|
||||
|
||||
// Start a goroutine to handle responses
|
||||
go func() {
|
||||
respBuf := make([]byte, 16*1024) // Reduced from 128KB to 16KB
|
||||
for {
|
||||
// Check for cancellation
|
||||
select {
|
||||
case <-h.Ctx.Done():
|
||||
return
|
||||
case <-conn.Closed:
|
||||
return
|
||||
default:
|
||||
// Continue processing
|
||||
}
|
||||
|
||||
// Set read deadline to 100ms for faster response (reduced from 300ms)
|
||||
if err := targetConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue // Non-fatal, just retry
|
||||
}
|
||||
|
||||
n, addr, err := targetConn.ReadFromUDP(respBuf)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
// Make a copy of the data since respBuf will be reused
|
||||
data := make([]byte, n)
|
||||
copy(data, respBuf[:n])
|
||||
|
||||
// Send response through channel
|
||||
select {
|
||||
case responses <- struct {
|
||||
data []byte
|
||||
addr *net.UDPAddr
|
||||
}{data, addr}:
|
||||
// Successfully sent
|
||||
default:
|
||||
// Channel full, log and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Timeout to clean up inactive targets
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
cleanup := time.NewTicker(30 * time.Second)
|
||||
defer cleanup.Stop()
|
||||
|
||||
for {
|
||||
// Check for shutdown before blocking on I/O.
|
||||
select {
|
||||
case <-h.Ctx.Done():
|
||||
return
|
||||
case <-conn.Closed:
|
||||
return
|
||||
case resp := <-responses:
|
||||
// Find the target that matches this response
|
||||
var found bool
|
||||
|
||||
for _, target := range targets {
|
||||
if target.addr.IP.Equal(resp.addr.IP) && target.addr.Port == resp.addr.Port {
|
||||
// Update last active time
|
||||
target.lastActive = time.Now()
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// Skip unknown targets
|
||||
continue
|
||||
}
|
||||
|
||||
if clientAddr == nil {
|
||||
// Skip client address not set
|
||||
continue
|
||||
}
|
||||
|
||||
// Create response packet with appropriate header
|
||||
// For simplicity, we'll just use a minimal header with the correct address type
|
||||
var respHeader []byte
|
||||
|
||||
// Determine address type
|
||||
var addrType byte
|
||||
if resp.addr.IP.To4() != nil {
|
||||
addrType = IPv4
|
||||
} else {
|
||||
addrType = IPv6
|
||||
}
|
||||
|
||||
// header: RSV(2) + FRAG(0) + ATYP(1) + ADDR + PORT(2)
|
||||
respHeader = append(respHeader, 0, 0, 0, addrType)
|
||||
|
||||
if addrType == IPv4 {
|
||||
respHeader = append(respHeader, resp.addr.IP.To4()...)
|
||||
} else {
|
||||
respHeader = append(respHeader, resp.addr.IP.To16()...)
|
||||
}
|
||||
|
||||
// Add port
|
||||
portBytes := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(portBytes, uint16(resp.addr.Port))
|
||||
respHeader = append(respHeader, portBytes...)
|
||||
|
||||
// Combine header and payload
|
||||
respPacket := append(respHeader, resp.data...)
|
||||
|
||||
_, err = udpConn.WriteToUDP(respPacket, clientAddr)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
// Clean up inactive targets
|
||||
case <-cleanup.C:
|
||||
now := time.Now()
|
||||
for targetKey, targetInfo := range targets {
|
||||
if now.Sub(targetInfo.lastActive) > 1*time.Minute {
|
||||
delete(targets, targetKey)
|
||||
for k, t := range targets {
|
||||
if now.Sub(t.lastActive) > time.Minute {
|
||||
delete(targets, k)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Set read deadline to 100ms for faster response (reduced from 300ms)
|
||||
if err := udpConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue // Non-fatal
|
||||
}
|
||||
|
||||
udpConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
n, addr, err := udpConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if isUDPTimeout(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
n, remoteAddr, err := udpConn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
// Handle various error conditions
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
if netErr, ok := err.(net.Error); ok {
|
||||
if netErr.Timeout() {
|
||||
// This is expected due to deadline, don't log
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
h.SendClose(conn.ID, protocol.ErrNetworkUnreachable)
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
h.SendClose(conn.ID, protocol.ErrNetworkUnreachable)
|
||||
return
|
||||
}
|
||||
|
||||
// Store client address from first packet
|
||||
if clientAddr == nil {
|
||||
clientAddr = remoteAddr
|
||||
}
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only accept packets from original client
|
||||
if !remoteAddr.IP.Equal(clientAddr.IP) {
|
||||
if clientAddr == nil {
|
||||
// First valid SOCKS5 UDP datagram (RSV=0x0000, FRAG=0) sets the client.
|
||||
if n > 3 && buf[0] == 0 && buf[1] == 0 && buf[2] == 0 {
|
||||
clientAddr = addr
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Handle UDP packet
|
||||
if n > 3 {
|
||||
// Extract target address from UDP header
|
||||
targetAddr, headerLen, errCode := ExtractUDPHeader(buffer[:n])
|
||||
if errCode != protocol.ErrNone {
|
||||
if addr.IP.Equal(clientAddr.IP) && addr.Port == clientAddr.Port {
|
||||
// Client → target
|
||||
if n <= 3 {
|
||||
continue
|
||||
}
|
||||
targetAddr, headerLen, errCode := ExtractUDPHeader(buf[:n])
|
||||
if errCode != protocol.ErrNone {
|
||||
continue
|
||||
}
|
||||
targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
targets[targetAddr] = &targetInfo{addr: targetUDPAddr, lastActive: time.Now()}
|
||||
udpConn.WriteTo(buf[headerLen:n], targetUDPAddr)
|
||||
} else {
|
||||
// Target → client: find the matching target entry and wrap with SOCKS5 header.
|
||||
for _, t := range targets {
|
||||
if !t.addr.IP.Equal(addr.IP) || t.addr.Port != addr.Port {
|
||||
continue
|
||||
}
|
||||
t.lastActive = time.Now()
|
||||
|
||||
// Resolve target address
|
||||
targetUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
|
||||
if err != nil {
|
||||
continue
|
||||
var header []byte
|
||||
if ip4 := addr.IP.To4(); ip4 != nil {
|
||||
header = append([]byte{0, 0, 0, IPv4}, ip4...)
|
||||
} else {
|
||||
header = append([]byte{0, 0, 0, IPv6}, addr.IP.To16()...)
|
||||
}
|
||||
var portBuf [2]byte
|
||||
binary.BigEndian.PutUint16(portBuf[:], uint16(addr.Port))
|
||||
header = append(header, portBuf[:]...)
|
||||
|
||||
// Store target information
|
||||
targetKey := targetAddr
|
||||
targets[targetKey] = &targetInfo{
|
||||
addr: targetUDPAddr,
|
||||
lastActive: time.Now(),
|
||||
}
|
||||
|
||||
// Send payload to target
|
||||
_, err = targetConn.WriteToUDP(buffer[headerLen:n], targetUDPAddr)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
udpConn.WriteTo(append(header, buf[:n]...), clientAddr)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isUDPTimeout reports whether err is a read-deadline / timeout error.
|
||||
func isUDPTimeout(err error) bool {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
return errors.Is(err, os.ErrDeadlineExceeded)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//go:build js
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jsUDPPacket struct {
|
||||
data []byte
|
||||
addr *net.UDPAddr
|
||||
}
|
||||
|
||||
type jsUDPConn struct {
|
||||
socket js.Value
|
||||
port int
|
||||
packets chan jsUDPPacket
|
||||
closed chan struct{}
|
||||
once sync.Once
|
||||
mu sync.Mutex
|
||||
dl time.Time // read deadline
|
||||
}
|
||||
|
||||
func listenUDP() (UDPRelayConn, error) {
|
||||
c := &jsUDPConn{
|
||||
packets: make(chan jsUDPPacket, 256),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
|
||||
onBind := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
c.socket = args[0]
|
||||
c.port = args[1].Int()
|
||||
select {
|
||||
case done <- nil:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
onData := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
// args: socket, Uint8Array, port, address
|
||||
jsData := args[1]
|
||||
port := args[2].Int()
|
||||
host := args[3].String()
|
||||
|
||||
data := make([]byte, jsData.Length())
|
||||
js.CopyBytesToGo(data, jsData)
|
||||
|
||||
addr := &net.UDPAddr{IP: net.ParseIP(host), Port: port}
|
||||
select {
|
||||
case c.packets <- jsUDPPacket{data, addr}:
|
||||
case <-c.closed:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
onError := js.FuncOf(func(_ js.Value, args []js.Value) any {
|
||||
msg := "udp socket error"
|
||||
if len(args) > 0 {
|
||||
msg = args[0].String()
|
||||
}
|
||||
select {
|
||||
case done <- fmt.Errorf("%s", msg):
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
js.Global().Call("UDPListen", onBind, onData, onError)
|
||||
|
||||
if err := <-done; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *jsUDPConn) LocalPort() int { return c.port }
|
||||
|
||||
func (c *jsUDPConn) ReadFrom(b []byte) (int, *net.UDPAddr, error) {
|
||||
c.mu.Lock()
|
||||
dl := c.dl
|
||||
c.mu.Unlock()
|
||||
|
||||
var timer <-chan time.Time
|
||||
if !dl.IsZero() {
|
||||
d := time.Until(dl)
|
||||
if d <= 0 {
|
||||
return 0, nil, &net.OpError{Op: "read", Net: "udp", Err: os.ErrDeadlineExceeded}
|
||||
}
|
||||
timer = time.After(d)
|
||||
}
|
||||
|
||||
select {
|
||||
case pkt := <-c.packets:
|
||||
n := copy(b, pkt.data)
|
||||
return n, pkt.addr, nil
|
||||
case <-c.closed:
|
||||
return 0, nil, net.ErrClosed
|
||||
case <-timer:
|
||||
return 0, nil, &net.OpError{Op: "read", Net: "udp", Err: os.ErrDeadlineExceeded}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *jsUDPConn) WriteTo(b []byte, addr *net.UDPAddr) error {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return net.ErrClosed
|
||||
default:
|
||||
}
|
||||
jsData := js.Global().Get("Uint8Array").New(len(b))
|
||||
js.CopyBytesToJS(jsData, b)
|
||||
c.socket.Call("send", jsData, addr.Port, addr.IP.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *jsUDPConn) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.dl = t
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *jsUDPConn) Close() error {
|
||||
c.once.Do(func() {
|
||||
close(c.closed)
|
||||
c.socket.Call("close")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//go:build !js
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type nativeUDPConn struct {
|
||||
conn *net.UDPConn
|
||||
}
|
||||
|
||||
func listenUDP() (UDPRelayConn, error) {
|
||||
c, err := net.ListenUDP("udp", &net.UDPAddr{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &nativeUDPConn{c}, nil
|
||||
}
|
||||
|
||||
func (c *nativeUDPConn) LocalPort() int {
|
||||
return c.conn.LocalAddr().(*net.UDPAddr).Port
|
||||
}
|
||||
|
||||
func (c *nativeUDPConn) ReadFrom(b []byte) (int, *net.UDPAddr, error) {
|
||||
return c.conn.ReadFromUDP(b)
|
||||
}
|
||||
|
||||
func (c *nativeUDPConn) WriteTo(b []byte, addr *net.UDPAddr) error {
|
||||
_, err := c.conn.WriteToUDP(b, addr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *nativeUDPConn) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *nativeUDPConn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UDPRelayConn is a UDP socket that can send and receive datagrams to/from
|
||||
// arbitrary addresses. Used by the SOCKS5 UDP ASSOCIATE handler.
|
||||
// Implementations must be safe for concurrent use.
|
||||
type UDPRelayConn interface {
|
||||
// LocalPort returns the port this socket is bound to.
|
||||
LocalPort() int
|
||||
|
||||
// ReadFrom reads a datagram into b, returning the sender's address.
|
||||
// Blocks until data arrives, the socket is closed, or the read deadline fires.
|
||||
ReadFrom(b []byte) (int, *net.UDPAddr, error)
|
||||
|
||||
// WriteTo sends a datagram to addr.
|
||||
WriteTo(b []byte, addr *net.UDPAddr) error
|
||||
|
||||
// SetReadDeadline sets the deadline for future ReadFrom calls.
|
||||
// A zero value disables the deadline.
|
||||
SetReadDeadline(t time.Time) error
|
||||
|
||||
// Close closes the socket.
|
||||
Close() error
|
||||
}
|
||||
Reference in New Issue
Block a user