trunk

package
v0.0.0-...-de2e28c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 25, 2026 License: GPL-2.0 Imports: 15 Imported by: 0

README

Trunk

Per-client browser↔UDP tunnel library. One Conn per connected client: a pluggable Transport (WebSocket today) carries binary frames with a 2-byte big-endian port header, which the Conn demultiplexes to UDP datagrams aimed at a localhost backend. Each client is assigned a deterministic 127.x.x.x VirtualIP via VirtualIPAllocator so the backend sees distinct source addresses.

Import path: github.com/0xBrsm/NexQuake/nexus/trunk Module: github.com/0xBrsm/NexQuake/nexus (src/nexus/go.mod) Versioned with: NexQuake commits/tags (not a separate module)

Full API documentation is in the Go source — run go doc ./... or visit pkg.go.dev.

Vendoring checklist

  1. Frame format is fixed: 2-byte big-endian port header + payload. Keep client and relay in sync.
  2. Port 0 is the control channel. Define your own payload semantics in HandleControlFrame.
  3. Override Upgrader.CheckOrigin for production deployments.
  4. Set IsAllowedPort to restrict clients to specific UDP destinations.
  5. Keep sourceKey stable across reconnects if deterministic VirtualIP identity matters.

Documentation

Overview

Package trunk implements a per-client browser↔UDP tunnel: it bridges a binary-frame transport (WebSocket today, WebTransport in the future, or any user-provided Transport) to UDP datagrams aimed at a localhost backend. One Conn per connected client, each owning a UDP socket bound to a unique virtual loopback IP allocated by VirtualIPAllocator.

Wire format

Every tunnel message is a binary frame with a 2-byte big-endian port header followed by the payload:

byte 0    byte 1    byte 2 …
+---------+---------+----------+
| port (uint16, BE) | payload  |
+---------+---------+----------+

Port 0 is the control channel; non-zero values are UDP destination ports on the backend. Control frames are handed to [FrameDispatch.HandleControlFrame] instead of being forwarded over UDP. On connect, the Conn sends an identity frame on the control channel containing the 4-byte magic "NQIP" followed by the 4-byte virtual IPv4 address assigned to the client.

Usage

alloc := trunk.NewVirtualIPAllocator(net.ParseIP(trunk.DefaultServerIP))

http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
	ws, err := trunk.Upgrader.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	conn, err := trunk.NewConn(trunk.WebSocket(ws), alloc, sourceKey,
		trunk.WithDispatch(trunk.FrameDispatch{
			IsAllowedPort: func(port int) bool { return port == 26000 },
		}),
	)
	if err != nil {
		_ = ws.Close()
		return
	}
	conn.Run() // blocks until the connection closes
})

Index

Constants

View Source
const DefaultServerIP = "127.0.0.1"

DefaultServerIP is the loopback address of the NQ dedicated server that the relay forwards UDP traffic to. Override by passing a different IP to NewVirtualIPAllocator.

Variables

View Source
var Upgrader = websocket.Upgrader{
	HandshakeTimeout:  10 * time.Second,
	ReadBufferSize:    4096,
	WriteBufferSize:   4096,
	Subprotocols:      []string{"binary"},
	CheckOrigin:       func(r *http.Request) bool { return true },
	EnableCompression: false,
}

Upgrader is a default WebSocket upgrader wired for binary framing with compression disabled. Exposed as a convenience — callers can build their own upgrader and still use WebSocket to wrap the resulting conn.

Functions

This section is empty.

Types

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is a single client's tunnel↔UDP bridge. It owns one Transport and one UDP socket bound to a unique virtual loopback IP. Three goroutines run concurrently while Conn.Run is active: readLoop, writeLoop, and udpReadLoop; all exit when the connection closes.

func NewConn

func NewConn(t Transport, alloc *VirtualIPAllocator, sourceKey string, opts ...Option) (*Conn, error)

NewConn builds a tunnel↔UDP Conn for a single client.

sourceKey is the stable identity token that determines the client's VirtualIP — the same sourceKey always hashes to the same 127.x.x.x candidate, subject to collision avoidance. alloc must be non-nil and is shared across all active Conns in the process.

Dispatch callbacks and loggers flow in through functional options. Zero options is fine: the defaults are a no-op logger and a zero-value FrameDispatch. The identity-frame magic is always the 4-byte string [defaultIdentityMagic] ("NQIP") — the token NexQuake's WASM client recognizes.

Application-layer identity (source IP for logging, user ID, admin flag) is the caller's concern; trunk does not track it.

Call Conn.Run to start the I/O loops.

func (*Conn) ActiveServerPort

func (c *Conn) ActiveServerPort() int

ActiveServerPort returns the last-routed server port.

func (*Conn) Close

func (c *Conn) Close()

Close tears down the Conn: invokes HandleClose, cancels the context, closes the UDP and tunnel connections, and releases the VirtualIP. Safe to call more than once; all work happens at most once. Callers that track the Conn externally are responsible for dropping their reference.

func (*Conn) Run

func (c *Conn) Run()

Run sends the identity frame, starts the UDP and tunnel I/O goroutines, and blocks until the connection closes. It calls Close before returning.

func (*Conn) SourceKey

func (c *Conn) SourceKey() string

SourceKey returns the stable identity token passed to NewConn.

func (*Conn) VirtualIP

func (c *Conn) VirtualIP() string

VirtualIP returns the Conn's 127.x.x.x VirtualIP as a dotted string. Returns an empty string after the Conn has been closed.

func (*Conn) VirtualIPBytes

func (c *Conn) VirtualIPBytes() [4]byte

VirtualIPBytes returns the raw 4-byte VirtualIP.

type FrameDispatch

type FrameDispatch struct {
	// HandleControlFrame is called for every incoming control-channel frame
	// (port 0). Return a non-nil slice to send a reply on the control channel;
	// return nil to send nothing. The payload slice is only valid for the
	// duration of the call — copy it if you need to retain it.
	HandleControlFrame func(conn *Conn, payload []byte) []byte

	// HandleClose is called once when the connection is closing, before the
	// tunnel and UDP sockets are torn down.
	HandleClose func(conn *Conn)

	// IsAllowedPort, if non-nil, is called for every incoming non-control
	// frame before the payload is forwarded over UDP. Return true to allow,
	// false to drop. When nil, all destination ports are allowed.
	IsAllowedPort func(port int) bool
}

FrameDispatch holds application-defined callbacks that Conn invokes for frames it cannot handle internally. All fields are optional (nil is safe).

type Option

type Option func(*connOptions)

Option configures a Conn at construction time. See NewConn.

func WithDispatch

func WithDispatch(d FrameDispatch) Option

WithDispatch wires application-level callbacks for control-channel frames, connection close, and per-frame port gating.

func WithLogger

func WithLogger(warnf, debugf logf) Option

WithLogger plumbs printf-style loggers for warnings (recoverable issues worth surfacing) and debug output (chatty, protocol-tracing level). Passing nil for either yields a no-op.

type Transport

type Transport interface {
	// ReadFrame blocks until a binary frame is received. On connection close
	// or error it returns a non-nil error; the Conn tears down. Non-binary
	// messages (e.g. WS control frames) are skipped internally.
	ReadFrame() ([]byte, error)
	// WriteFrame sends a binary frame with a short write deadline.
	WriteFrame([]byte) error
	// Ping sends an application-level keepalive. Transports with native
	// connection-level keepalive (e.g. QUIC) may return nil.
	Ping() error
	// Close tears down the underlying connection.
	Close() error
}

Transport is a bidirectional binary-frame tunnel between nexus and a single browser client. Adapters wrap the concrete connection type (WebSocket today, [WebTransport] in the future, or a custom implementation); the Conn's read/write loops are transport-agnostic.

func WebSocket

func WebSocket(ws *websocket.Conn) Transport

WebSocket wraps an already-upgraded *websocket.Conn as a trunk Transport. Non-binary messages received on the underlying conn are skipped silently.

type VirtualIPAllocator

type VirtualIPAllocator struct {
	// contains filtered or unexported fields
}

VirtualIPAllocator deterministically maps client source keys to unique 127.x.x.x VirtualIP addresses. The mapping is stable for a given sourceKey within a process lifetime: the same key always hashes to the same candidate, with linear probing on collision. Safe for concurrent use.

func NewVirtualIPAllocator

func NewVirtualIPAllocator(serverIP net.IP) *VirtualIPAllocator

NewVirtualIPAllocator creates an allocator. serverIP must be a 127.x.x.x address (typically net.ParseIP(DefaultServerIP)); that address is excluded from the allocation pool to avoid the relay colliding with the game server itself.

func (*VirtualIPAllocator) IsBlocked

func (a *VirtualIPAllocator) IsBlocked(sourceKey string) bool

IsBlocked reports whether sourceKey has been permanently blocked via VirtualIPAllocator.ReserveAndBlock. Callers can use this to reject reconnects before attempting to construct a new relay.

func (*VirtualIPAllocator) ReserveAndBlock

func (a *VirtualIPAllocator) ReserveAndBlock(ip4 [4]byte, sourceKey string)

ReserveAndBlock permanently reserves ip4 so it is never re-allocated, and blocks sourceKey from receiving any future allocation. Intended for banning: call after closing a relay to ensure the banned VirtualIP is not recycled and the banned key cannot reconnect with a different VirtualIP.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL