Files
Atsika 5b21bc7cda 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
2026-03-21 19:45:24 +01:00

147 lines
4.1 KiB
Go

// Package protocol implements the communication protocol between proxy server and agent.
// It provides packet encoding/decoding and connection management.
//
// The protocol uses a binary packet format with fixed-size headers and variable-length
// payloads. Each packet contains a command type, connection ID, and optional data.
package protocol
import (
"bytes"
"encoding/binary"
"github.com/google/uuid"
)
// Command types for protocol operations.
const (
CmdNew byte = iota + 1 // Request new connection
CmdAck // Acknowledge connection
CmdData // Transfer data
CmdClose // Terminate connection
)
// Protocol packet field sizes in bytes.
const (
CommandSize = 1 // Command field
UUIDSize = 16 // Connection ID field
DataLengthSize = 4 // Payload length field
HeaderSize = CommandSize + UUIDSize + DataLengthSize
)
// Packet represents a protocol message with the following binary format:
//
// +---------+----------------+--------------+---------+
// | Command | Connection ID | Data Length | Payload |
// +---------+----------------+--------------+---------+
// | 1B | 16B | 4B | var |
type Packet struct {
Command byte // Operation type (CmdNew, CmdAck, etc.)
ConnectionID uuid.UUID // Unique connection identifier
Data []byte // Optional payload data
}
// NewPacket creates a protocol packet with the given parameters.
// The data parameter is optional and may be nil.
func NewPacket(command byte, connectionID uuid.UUID, data []byte) *Packet {
return &Packet{
Command: command,
ConnectionID: connectionID,
Data: data,
}
}
// EncodedSize returns the total size of the encoded packet in bytes.
func (p *Packet) EncodedSize() int {
return HeaderSize + len(p.Data)
}
// Encode serializes the packet into a byte slice following the protocol format.
// Returns nil if any encoding operation fails.
func (p *Packet) Encode() []byte {
buf := bytes.NewBuffer(make([]byte, 0, HeaderSize+len(p.Data)))
if err := buf.WriteByte(p.Command); err != nil {
return nil
}
if _, err := buf.Write(p.ConnectionID[:]); err != nil {
return nil
}
if err := binary.Write(buf, binary.BigEndian, uint32(len(p.Data))); err != nil {
return nil
}
if len(p.Data) > 0 {
if _, err := buf.Write(p.Data); err != nil {
return nil
}
}
return buf.Bytes()
}
// Decode deserializes a byte slice into a protocol packet.
// Returns nil if the data is malformed or contains an invalid command.
// The input must be EXACTLY one complete packet (aznet ensures this).
func Decode(data []byte) *Packet {
if len(data) < HeaderSize {
return nil
}
command := data[0]
if command < CmdNew || command > CmdClose {
return nil
}
var id uuid.UUID
copy(id[:], data[CommandSize:CommandSize+UUIDSize])
dataLength := binary.BigEndian.Uint32(data[CommandSize+UUIDSize : HeaderSize])
// Check exact size - aznet returns complete messages, so size must match exactly
if uint32(len(data)) != uint32(HeaderSize)+dataLength {
return nil
}
var packetData []byte
if dataLength > 0 {
packetData = make([]byte, dataLength)
copy(packetData, data[HeaderSize:])
}
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:]
}