rendr

package module
v0.0.0-...-1b82f78 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

README

rendr

Go Go Reference

A connection-migration framework for Go. An application holds a stable net.Conn whose underlying network path rendr can swap — TCP socket, QUIC connection, or opaque-UDP flow — without surfacing any reset, EOF, or read-zero to the application.

rendr does only this. Proxy protocols, peer discovery, configuration management, and detailed path-quality policies belong to the embedder.

go get github.com/FrankoonG/rendr

Quickstart

Server:

ln, err := rendr.ListenTCP("0.0.0.0:5555")
if err != nil { log.Fatal(err) }
for {
    c, err := ln.Accept(context.Background())
    if err != nil { return }
    go handle(c) // c implements net.Conn
}

Client:

d := &rendr.Dialer{
    Mode: rendr.ModePrime,
    Paths: []rendr.PathSpec{
        {Transport: "tcp",  Address: "h1:5555"},
        {Transport: "quic", Address: "h2:5555"},
    },
}
c, err := d.Dial(context.Background())
// c.Read / c.Write survive a path swap.
// c.FlowID() stays constant for the connection's lifetime.

Listeners: ListenTCP, ListenQUIC(addr, *tls.Config), ListenUDPFlow.

Transport adapters auto-register: "tcp", "quic", "udpflow".

Packet mode

For datagram-oriented applications, use DialPacket and ListenUDPFlowPacket. Each WriteTo becomes one wire frame; each ReadFrom returns one frame's payload. The wire format is identical to stream mode; the two are negotiated in HELLO.

ln, _ := rendr.ListenUDPFlowPacket("0.0.0.0:5555")
pc, _ := (&rendr.Dialer{
    Mode: rendr.ModePrime,
    Paths: []rendr.PathSpec{{Transport: "udpflow", Address: "h1:5555"}},
}).DialPacket(context.Background())
// pc implements net.PacketConn; boundaries preserved 1-to-1.

Modes

Mode Bandwidth Latency Use for
prime best single path best single path SSH, RDP, control channels
race best single path min across paths trading, low-jitter UDP control
bond sum of paths mid large file, video, multi-link

Set via Dialer.Mode or runtime Conn.SetMode. Legal transitions: prime ↔ race, prime ↔ bond. race ↔ bond is forbidden because race has no per-path SEQ ordering and bond requires it.

Migration is invisible

The hard contract is that a path swap does not surface as an error from the application's Read or Write. If every path dies and no new one becomes available within the migration budget (default 90 s), Read returns rendr.ErrMigrationBudgetExceeded. Clean peer teardown surfaces as io.EOF.

Transport-layer errors (TCP RST, QUIC idle timeout, UDP socket gone) are NEVER conflated with application EOF; they trigger migration silently.

Observability and control

The Conn returned by Dial / Accept also implements AdminConn. Assert when you need it:

adm := c.(rendr.AdminConn)
s := adm.Stats()
log.Printf("flow=%x state=%s mode=%s paths=%d hwm=%d",
    s.FlowID, s.State, s.Mode, len(s.Paths), s.RecvQueueHWM)

// Explicit migration:
if newID, err := adm.AddPath(rendr.PathSpec{Transport: "tcp", Address: "h3:5555"}); err == nil {
    _ = adm.Migrate(newID)
}

AdminConn surface: Migrate, ActivePath, AddPath, RemovePath, State, Mode, RecvQueueHWM, RecvDups, BondStuckSkips, MigrationCount, Stats. Packet-mode connections expose the same methods via AdminPacketConn.

Acceptance contracts

rendr is gated on five invariants — the implementation is not considered done until all five hold under chaos-scale tests:

  • G1 Large file (≥1 GiB) with forced mid-stream migrations finishes with identical SHA-256 and <10% baseline throughput regression.
  • G2 30-minute echo loop with 30+ migrations, zero loss, P99 RTT < baseline × 2.
  • G3 100k pps QUIC datagrams + 10 ConnID migrations, zero application loss, P95 RTT < baseline × 2.
  • G4 Path A force-killed (network DROP) with path B intact; app sees no error; failover ≤ 5 s; in-flight data reaches the peer via path B.
  • G5 After G4, recover path A (via AdminConn.AddPath); it re-joins the path set without spurious reorder.

Status

In active development under tag prefix v0.1.x. Public API may still shift before v1.0; modes, AdminConn surface, and wire format v0 are pinned by tests against drift.

The five acceptance contracts (G1-G5) have all been validated on the development branch:

  • G1 — 1 GiB transfer with mid-stream migrations, SHA-256 match, <10% throughput regression
  • G2 — 60s continuous echo with 15+ migrations, 0 loss, P99 RTT within 2× baseline
  • G3 — 100k pps QUIC DATAGRAM over 8-path bond with 11 migrations, 0 loss, P95 RTT 1.6ms (validated on Linux 6.8 with net.core.rmem_max raised; the QUIC DATAGRAM transport needs a larger socket receive buffer than the default 208 KiB)
  • G4 — force-killed path fails over to surviving path in <200ms on loopback with zero application-visible error
  • G5 — AdminConn.AddPath recovers a dropped path back into the active set without reorder artifacts

License

See LICENSE.

Documentation

Overview

Package rendr is a connection-migration framework. An application holds a stable net.Conn whose underlying network path rendr can swap (TCP socket, QUIC connection, opaque-UDP flow) without surfacing any reset, EOF, or read-zero to the application.

rendr does only this. It does not implement proxy protocols, peer discovery, configuration management, or path-quality probing policies beyond the built-in defaults; those belong to the embedder.

Quickstart

Server:

ln, _ := rendr.ListenTCP("0.0.0.0:5555")
for {
    c, err := ln.Accept(context.Background())
    if err != nil { return }
    go handle(c) // c implements net.Conn
}

Client:

d := &rendr.Dialer{
    Mode: rendr.ModePrime,
    Paths: []rendr.PathSpec{
        {Transport: "tcp",  Address: "h1:5555"},
        {Transport: "quic", Address: "h2:5555"},
    },
}
c, err := d.Dial(context.Background())
// c.Read / c.Write survive a path swap; c.FlowID() stays
// constant for the connection's lifetime.

Packet-boundary mode (PacketConn)

For datagram-oriented applications (WireGuard, opaque UDP echo, any protocol that owns its own framing) use DialPacket / ListenUDPFlowPacket. The wire format is identical; only the receive side changes: each rendr DATA frame becomes one packet, boundaries are preserved 1-to-1 with WriteTo / ReadFrom.

ln, _ := rendr.ListenUDPFlowPacket("0.0.0.0:5555")
d := &rendr.Dialer{Mode: rendr.ModePrime,
    Paths: []rendr.PathSpec{{Transport: "udpflow", Address: "h1:5555"}}}
pc, _ := d.DialPacket(context.Background())
_, _ = pc.WriteTo(packet, nil)  // one packet -> one frame

Packet-mode negotiation happens in HELLO via proto.CapsPacketMode; a stream-mode peer connecting to a packet listener is routed to the regular Accept channel instead. The two modes can coexist on one listener port.

Operational modes

Three modes share the same migration engine. Set on Dialer.Mode or change at runtime via Conn.SetMode (subject to the legality rules below):

  • ModePrime: pick the lowest-scoring path; migrate to a better path only when it beats the current by Hysteresis for Dwell, with Cooldown between switches. Quality is scored as rtt + jitter + 10 ms per percent loss.
  • ModeRace: every frame fans out to every attached path; the receiver dedupes by SEQ. Bandwidth equals the best single path; latency equals the minimum across paths. Tolerates loss on individual paths.
  • ModeBond: frames are round-robined across paths with N-frame pinning to bound reorder windows under RTT skew. Bandwidth approximates the sum of paths.

Legal SetMode transitions:

prime <-> race   allowed
prime <-> bond   allowed
race  <-> bond   forbidden (race has no per-path order)

Acceptance contracts

rendr is gated on five invariants (the implementation is not considered done until all five hold in chaos-scale tests):

G1 - large file (>= 1 GiB) with forced mid-stream migrations
     finishes with identical SHA-256 and < 10% baseline
     throughput regression
G2 - 30 min echo loop with 30+ migrations, zero loss, P99
     RTT < baseline x 2
G3 - 100k pps QUIC datagrams + 10 ConnID migrations, zero
     application loss, P95 RTT < baseline x 2
G4 - path A force-killed (network DROP) with path B intact;
     app sees no error; failover <= 5 s; in-flight data
     reaches the peer via path B
G5 - after G4, recover path A (via AdminConn.AddPath); it
     re-joins the path set without spurious reorder

AdminConn observability and control

The public Conn returned by Dial / Accept also implements AdminConn (assert if you need it). PacketConn returned by DialPacket / AcceptPacket implements AdminPacketConn with the same surface. Both expose:

  • Path-set management: Migrate, ActivePath, AddPath, RemovePath
  • Mode read/write: Mode, inherited SetMode
  • Lifecycle: State
  • Diagnostic counters: RecvQueueHWM (race-mode dedup window), RecvDups (race-mode dedup events), BondStuckSkips (bond stuck- path bypass count), MigrationCount (active-path changes)
  • One-call monitoring snapshot: Stats (returns ConnStats with all the above plus per-path PathInfo and FlowID)

Hard rules

  • A migration in flight NEVER surfaces an error from the application's Read or Write. Read may block during the migration budget (default 90 s); after the budget elapses with no usable path, Read returns ErrMigrationBudgetExceeded.
  • Transport errors and application EOF are NEVER conflated. A clean peer BYE surfaces as io.EOF; everything else triggers migration.
  • Default has NO active-migration trigger. Migration fires only on path death or explicit ModePrime quality-scoring decisions; quality scoring itself is OFF unless armed by setting Mode=ModePrime on the Dialer.
  • All wire formats are versioned (see package proto). Any change to the bytes on the wire requires bumping the version constant.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrMigrationBudgetExceeded = engine.ErrMigrationBudgetExceeded
	ErrZombie                  = engine.ErrZombie
	ErrPeerProtoVersion        = engine.ErrPeerProtoVersion
	ErrLastPath                = engine.ErrLastPath
	ErrPacketTooLarge          = engine.ErrPacketTooLarge

	// ErrReadDeadlineExceeded is the sentinel returned by Read /
	// ReadFrom when a SetReadDeadline-set deadline elapses before
	// payload is ready. Implements net.Error with Timeout()==true so
	// idiomatic timeout checks via errors.As work transparently.
	ErrReadDeadlineExceeded net.Error = engine.ErrReadDeadlineExceeded

	// ErrModeSwitchIllegal is returned by Conn.SetMode for transitions
	// that the engine refuses (e.g. race -> bond).
	ErrModeSwitchIllegal = errors.New("rendr: illegal mode transition")

	// ErrNotImplemented is a build-stage placeholder used by API
	// surfaces whose implementations land in later milestones.
	ErrNotImplemented = errors.New("rendr: not implemented in this milestone")
)

Sentinel errors surfaced by the rendr API. The set is small on purpose: the contract is that migrations do not surface errors at all (CLAUDE.md hard rule #1), so the only errors that ever escape the API are end-of-life errors.

The three end-of-life errors live in internal/engine so the engine can return them directly; rendr re-exports the same value so callers can errors.Is against rendr.ErrFoo and have it match what the engine returned.

Functions

This section is empty.

Types

type AdminConn

type AdminConn interface {
	Conn
	Migrate(pathID uint32) error
	ActivePath() uint32

	// AddPath dials a new path matching spec and joins it to the
	// existing engine via BRIDGE_TAG. Returns the new path id on
	// success. This is the G5 "path recovery" primitive: after a
	// path death, dial a fresh replacement (typically same spec)
	// to bring the path set back to its original cardinality.
	AddPath(spec PathSpec) (uint32, error)

	// RemovePath gracefully detaches the named path. If it is the
	// active path, the engine first failovers to another attached
	// path. Returns ErrLastPath if id is the only attached path;
	// in that case callers who want full teardown should call Close.
	// AddPath/RemovePath are the symmetric primitives for runtime
	// path-set management; the engine itself never calls RemovePath.
	RemovePath(pathID uint32) error

	// State returns the bridge lifecycle stage as a short string:
	// "init", "handshaking", "active", "migrating", "closing",
	// "dead". Production monitoring uses this for a liveness
	// check that doesn't require sending traffic.
	State() string

	// RecvQueueHWM returns the high-water mark of the reorder
	// buffer over this Conn's lifetime. A consistently-growing HWM
	// in race mode indicates "dedup window overflow" - the receiver
	// is buffering more out-of-order frames than the path RTT skew
	// should produce, suggesting a path is dropping or stuck.
	// Production dashboards consult this to spot the condition.
	RecvQueueHWM() int

	// RecvDups returns the cumulative count of frames whose SEQ
	// had already been delivered (or was already buffered in the
	// reorder window). For race mode this is the duplicate-frames-
	// reaped counter; for any mode it surfaces accidental
	// retransmits.
	RecvDups() uint64

	// BondStuckSkips returns the cumulative count of round-robin
	// slots that bond dispatch bypassed because the candidate
	// path's probe-measured RTT exceeded best_rtt *
	// BondStuckRTTMultiplier. Always zero outside bond mode.
	BondStuckSkips() uint64

	// MigrationCount returns the cumulative number of active-path
	// changes since this Conn was established (initial activation
	// is not counted). Both explicit Migrate calls and death-
	// driven failover contribute. Production dashboards use this
	// to detect churn that may need human attention.
	MigrationCount() uint64

	// OnMigrate registers fn to fire (in its own goroutine) on every
	// active-path change. cause is "explicit" for Migrate-driven
	// transitions and "death" for failover via onPathDeath. The
	// returned cancel function unsubscribes. Use this instead of
	// polling MigrationCount when you want push-based notification
	// (e.g. metrics, structured logs).
	OnMigrate(fn func(oldID, newID uint32, cause string)) (cancel func())

	// Mode returns the current operational mode (prime/race/bond).
	// Symmetric counterpart to SetMode; lets callers verify a
	// mode transition succeeded.
	Mode() Mode

	// Stats returns a coherent one-call snapshot of everything a
	// monitoring layer wants to see: flow id, mode, lifecycle
	// state, path list (with per-path counters + quality), active
	// path id, and recv-queue high-water mark. The contents are
	// also obtainable individually but Stats avoids torn reads
	// across getters.
	Stats() ConnStats
}

AdminConn extends Conn with operations that are not part of the normal application surface: explicit path migration, active-path introspection, and dynamic path attach. Tools that drive migration externally (runtime balancers, monitoring panels, integration tests) assert to this interface.

Application code should NOT depend on AdminConn; the engine reserves the right to migrate on its own and an external migrate can race with internal scheduling.

Example

ExampleAdminConn shows the monitoring / control face. After Dial, the application can assert to rendr.AdminConn to inspect path state, drive migration, or attach a fresh path post-death.

package main

import (
	"context"
	"fmt"
	"io"
	"log"

	"github.com/FrankoonG/rendr"
)

func main() {
	ln, err := rendr.ListenTCP("127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()

	srvDone := make(chan struct{})
	go func() {
		defer close(srvDone)
		c, err := ln.Accept(context.Background())
		if err != nil {
			return
		}
		defer c.Close()
		buf := make([]byte, 8)
		_, _ = io.ReadFull(c, buf)
	}()

	d := &rendr.Dialer{
		Mode:  rendr.ModePrime,
		Paths: []rendr.PathSpec{{Transport: "tcp", Address: ln.Addr().String()}},
	}
	c, err := d.Dial(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	defer c.Close()
	_, _ = c.Write(make([]byte, 8))

	if adm, ok := c.(rendr.AdminConn); ok {
		s := adm.Stats()
		fmt.Println("state:", s.State)
		fmt.Println("mode:", s.Mode)
		fmt.Println("paths:", len(s.Paths))
	}
	<-srvDone
}
Output:
state: active
mode: prime
paths: 1

type AdminPacketConn

type AdminPacketConn interface {
	PacketConn
	Migrate(pathID uint32) error
	ActivePath() uint32
	AddPath(spec PathSpec) (uint32, error)
	RemovePath(pathID uint32) error
	State() string
	RecvQueueHWM() int
	RecvDups() uint64
	BondStuckSkips() uint64
	MigrationCount() uint64
	OnMigrate(fn func(oldID, newID uint32, cause string)) (cancel func())
	Mode() Mode
	Stats() ConnStats
}

AdminPacketConn is the AdminConn analogue for packet-mode. It extends PacketConn with the same migration/observability surface stream-mode AdminConn exposes.

type Conn

type Conn interface {
	net.Conn

	// Paths returns a snapshot of the currently-attached path set.
	Paths() []PathInfo

	// SetMode atomically switches the operational mode.
	// Some transitions are illegal at runtime: race → bond is rejected
	// (race has no per-path sequencing; bond requires it). bond → prime
	// and prime ↔ race are allowed. SetMode returns nil on success or
	// an error describing the rejection cause.
	SetMode(Mode) error

	// FlowID returns the 16-byte flow identifier assigned at
	// handshake. It is invariant for the Conn's lifetime and serves
	// as the demux key on the server side across path migration.
	FlowID() [16]byte
}

Conn is a stream-oriented rendr connection. It is a net.Conn that survives underlying path changes.

Hard contract (CLAUDE.md, hard rule #1): no Read/Write/Close on this interface returns an error caused by a migration. A migration in flight may pause individual Read/Write calls but never surfaces a reset / SO_ERROR / read-zero / write-error. If the migration budget (90s) elapses without a usable path, Read/Write will then return a real error and the Conn is dead.

type ConnStats

type ConnStats struct {
	FlowID         [16]byte
	State          string
	Mode           Mode
	ActivePath     uint32
	Paths          []PathInfo
	RecvQueueHWM   int
	RecvDups       uint64
	BondStuckSkips uint64
	MigrationCount uint64
	// CreatedAt is the wall-clock time at which this connection's
	// engine was constructed. Use time.Since(s.CreatedAt) to compute
	// connection age.
	CreatedAt time.Time
}

ConnStats is the one-call snapshot returned by AdminConn.Stats. Layout is stable; fields are added to the end for forward compatibility.

type Dialer

type Dialer struct {
	// Mode is the initial operational mode.
	Mode Mode

	// Paths are the candidate paths for this connection.
	Paths []PathSpec

	// Hysteresis (prime only): the new path must beat the current by
	// this fraction of the current score before a switch is allowed.
	// Default 0.25.
	Hysteresis float64

	// Dwell (prime only): minimum time the engine must stay on a path
	// after attaching to it. Default 5s.
	Dwell time.Duration

	// Cooldown (prime only): minimum gap between two successive
	// migrations regardless of quality. Default 30s.
	Cooldown time.Duration

	// MigrationBudget caps how long the engine will hold a Conn open
	// after all paths have died. Default and maximum 90s, per
	// CLAUDE.md hard rule #4. Lower values are allowed; higher ones
	// will be clamped to 90s.
	MigrationBudget time.Duration

	// ProbeInterval is how often each attached path issues a
	// CtrlPathProbe to measure RTT. 0 = default 1s.
	ProbeInterval time.Duration

	// ZombieMaxMigrations: number of consecutive completed migrations
	// with zero application payload between them before the engine
	// declares the peer a zombie and tears down (CLAUDE.md hard rule
	// #5). Default 2; lower values trip earlier.
	ZombieMaxMigrations int

	// ZombieCooldown: how long the engine waits between zombie-
	// counter decrements. After ZombieCooldown elapsed since the
	// last migration, the counter resets to ZombieMaxMigrations.
	// Default 30s.
	ZombieCooldown time.Duration

	// BondStuckRTTMultiplier (bond only): a path whose latest probe
	// RTT exceeds best_path_rtt * Multiplier is skipped on bond
	// round-robin. Default 3.0. Set lower to be more aggressive
	// about bypassing slow paths.
	BondStuckRTTMultiplier float64
	// contains filtered or unexported fields
}

Dialer is the entry point for constructing a rendr Conn.

Dialer is intentionally minimal: the path set is fixed at dial time. The engine will not discover paths on its own; the embedder feeds candidate PathSpecs. Use AddPath / RemovePath on the returned AdminConn to mutate the path set after dial.

func (*Dialer) AddPacketPathFactory

func (d *Dialer) AddPacketPathFactory(name string, f PacketPathFactory) error

AddPacketPathFactory registers a packet factory under name. Any PathSpec in d.Paths whose Transport equals name is dialed via this factory instead of transport.Default, used only by DialPacket() (packet-mode sessions). The contract:

  • The returned net.PacketConn MUST preserve datagram boundaries (one WriteTo == one peer ReadFrom).
  • MTU MUST be sufficient for the rendr 8B flow-id header plus expected payload.
  • PathSpec.Address (or PathSpec.Opts["peer_addr"] if you need to decouple "dial target" from "datagram peer") is resolved as net.ResolveUDPAddr and used as the WriteTo destination on every outgoing datagram.
  • PathSpec.Opts["flow_id_hex"] (14 hex digits = 7 bytes) overrides the random flow_id; otherwise crypto/rand picks one.

Names are unique per Dialer across both stream and packet maps.

func (*Dialer) AddStreamPathFactory

func (d *Dialer) AddStreamPathFactory(name string, f StreamPathFactory) error

AddStreamPathFactory registers a stream factory under name. Any PathSpec in d.Paths whose Transport equals name is dialed via this factory instead of transport.Default. Names must be unique per Dialer; a duplicate (or shadowing of a packet factory name) returns an error.

Factories are consulted before transport.Default, so they can also override a registered transport (e.g. a custom "tcp" implementation for one Dialer only). To reach the global registry name, just leave it unregistered on the Dialer.

func (*Dialer) Dial

func (d *Dialer) Dial(ctx context.Context) (Conn, error)

Dial establishes a rendr Conn using d's configuration. The engine performs the HELLO handshake on the first path; subsequent paths (attached during the same Dial call or later via the migration API) send BRIDGE_TAG carrying the same flow_id.

M1 limits: prime mode only; the first path in d.Paths becomes the active path, the remainder are attached but kept idle until a migration trigger fires.

Example

ExampleDialer_Dial demonstrates the smallest useful rendr setup: one TCP path between two in-process endpoints. Real deployments supply two or more paths so migration has somewhere to go.

package main

import (
	"context"
	"fmt"
	"io"
	"log"

	"github.com/FrankoonG/rendr"
)

func main() {
	ln, err := rendr.ListenTCP("127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()

	srvDone := make(chan struct{})
	go func() {
		defer close(srvDone)
		c, err := ln.Accept(context.Background())
		if err != nil {
			return
		}
		defer c.Close()
		buf := make([]byte, 64)
		n, _ := c.Read(buf)
		fmt.Println("server got:", string(buf[:n]))
	}()

	d := &rendr.Dialer{
		Mode:  rendr.ModePrime,
		Paths: []rendr.PathSpec{{Transport: "tcp", Address: ln.Addr().String()}},
	}
	c, err := d.Dial(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	defer c.Close()
	_, _ = io.WriteString(c, "hello rendr")
	<-srvDone
}
Output:
server got: hello rendr

func (*Dialer) DialPacket

func (d *Dialer) DialPacket(ctx context.Context) (PacketConn, error)

DialPacket establishes a rendr PacketConn using d's configuration. Each application WriteTo becomes one wire frame; each ReadFrom returns the payload of one wire frame. The peer (server) is notified via proto.CapsPacketMode in the HELLO so its engine also runs the packet-boundary drainer.

All Dialer fields (Mode, Paths, prime knobs, MigrationBudget) apply identically to packet-mode connections; the underlying engine and path machinery are the same.

Example

ExampleDialer_DialPacket demonstrates packet-boundary mode over opaque UDP. Each WriteTo becomes one frame; each ReadFrom returns one frame's payload. Use this for datagram-oriented protocols (WireGuard, custom UDP echo) where boundaries must be preserved.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/FrankoonG/rendr"
)

func main() {
	ln, err := rendr.ListenUDPFlowPacket("127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()

	srvDone := make(chan struct{})
	go func() {
		defer close(srvDone)
		c, err := ln.AcceptPacket(context.Background())
		if err != nil {
			return
		}
		defer c.Close()
		buf := make([]byte, 64)
		n, _, _ := c.ReadFrom(buf)
		fmt.Println("server got:", string(buf[:n]))
	}()

	d := &rendr.Dialer{
		Mode:  rendr.ModePrime,
		Paths: []rendr.PathSpec{{Transport: "udpflow", Address: ln.Addr().String()}},
	}
	c, err := d.DialPacket(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	defer c.Close()
	_, _ = c.WriteTo([]byte("hello packets"), nil)
	<-srvDone
}
Output:
server got: hello packets

type Listener

type Listener interface {
	Accept(ctx context.Context) (Conn, error)
	Close() error
	// Addr returns the listener's local network address, useful for
	// tests that bind ":0" and need to discover the chosen port.
	Addr() net.Addr
	// FlowIDs returns the live flow_id set for diagnostics.
	FlowIDs() [][16]byte
}

Listener accepts inbound rendr Conns. The set of acceptable transports is determined by registering transport adapters on the Listener (see transport.Registry).

func ListenQUIC

func ListenQUIC(addr string, tlsCfg *tls.Config) (Listener, error)

ListenQUIC starts a QUIC-only rendr listener bound to addr.

tlsCfg may be nil during local development; production embedders MUST supply a real *tls.Config. Inbound QUIC connections are demultiplexed by their first frame (HELLO -> new flow_id / engine; BRIDGE_TAG -> attach to existing engine), exactly as ListenTCP does.

func ListenTCP

func ListenTCP(addr string) (Listener, error)

ListenTCP starts a TCP-only rendr listener bound to addr. The returned Listener accepts inbound paths, demultiplexes them by flow_id, and yields one rendr.Conn per new flow.

M1: TCP is the only transport. M2 expands this to a generic Listener with a TransportSet.

func ListenUDPFlow

func ListenUDPFlow(addr string) (Listener, error)

ListenUDPFlow starts an opaque-UDP rendr listener bound to addr. It mirrors ListenTCP / ListenQUIC: inbound flows are demuxed by their first ctrl frame (HELLO -> new engine / BRIDGE_TAG -> attach path to existing engine), and the engine sees a transport.PathConn that hides the UDP demux machinery entirely.

Migration over opaque UDP is implicit at the transport layer: a datagram arriving from a fresh 4-tuple but carrying a known flow_id updates the corresponding ServerPathConn's RemoteAddr without firing any engine-level migration. Engine-driven migration (Engine.Migrate / prime / race) layers on top of that and remains transport-agnostic.

type Mode

type Mode uint8

Mode selects how the engine uses the set of available paths.

const (
	// ModePrime picks the single best-scoring path and migrates only
	// when another path beats the current one by the hysteresis margin
	// for at least dwell, with cooldown between switches.
	ModePrime Mode = 1
	// ModeBond splits frames across paths to aggregate throughput.
	ModeBond Mode = 2
	// ModeRace duplicates every frame across all paths and dedupes on
	// receive. Bandwidth equals the single best path; latency equals
	// the minimum across paths.
	ModeRace Mode = 3
)

func (Mode) String

func (m Mode) String() string

func (Mode) Valid

func (m Mode) Valid() bool

Valid reports whether m is one of the defined modes.

type PacketConn

type PacketConn interface {
	net.PacketConn

	Paths() []PathInfo
	SetMode(Mode) error
	FlowID() [16]byte
}

PacketConn is the datagram analogue of Conn.

type PacketListener

type PacketListener interface {
	AcceptPacket(ctx context.Context) (PacketConn, error)
	Close() error
	Addr() net.Addr
	FlowIDs() [][16]byte
}

PacketListener accepts inbound rendr PacketConns. The udpflow listener implements both Listener and PacketListener: HELLO with CapsPacketMode routes to AcceptPacket, otherwise to Accept. A listener can therefore serve mixed packet- and stream-mode peers simultaneously without separate ports.

func ListenQUICDatagram

func ListenQUICDatagram(addr string, tlsCfg *tls.Config) (PacketListener, error)

ListenQUICDatagram starts a QUIC listener that accepts incoming connections in DATAGRAM mode (RFC 9221). One DATAGRAM == one rendr frame; no bidi stream is opened. Pair with Dialer.DialPacket and PathSpec.Opts["mode"]="datagram" on the client side.

The server's engine is forced into packet mode regardless of the CapsPacketMode bit on HELLO - a DATAGRAM-mode peer cannot honour stream-style byte concatenation, so accepting one here implies packet-boundary semantics on both ends.

tlsCfg may be nil during local development; production embedders MUST supply a real *tls.Config.

func ListenUDPFlowPacket

func ListenUDPFlowPacket(addr string) (PacketListener, error)

ListenUDPFlowPacket is a convenience constructor returning the same listener cast to PacketListener. Stream and packet acceptors share the same socket; HELLO caps decide which channel each connection lands on.

type PacketPathFactory

type PacketPathFactory func(ctx context.Context, addr string) (net.PacketConn, error)

PacketPathFactory mirrors StreamPathFactory for packet-mode paths. The returned net.PacketConn MUST preserve datagram boundaries (single WriteTo == single peer ReadFrom) and have MTU sufficient for the rendr 8-byte flow-id header plus expected payload.

rendr-side wraps the returned net.PacketConn with transport/udpflow.WrapFromSpec — the same flow-id framing layer that backs the built-in udpflow transport. The factory does NOT have to produce a "connected" socket: udpflow's WrapFromSpec resolves PathSpec.Address as the WriteTo peer and validates incoming flow_ids regardless of source-addr.

type PathInfo

type PathInfo = transport.PathInfo

type PathQuality

type PathQuality = transport.PathQuality

type PathSpec

type PathSpec = transport.PathSpec

type StreamPathFactory

type StreamPathFactory func(ctx context.Context, addr string) (net.Conn, error)

StreamPathFactory constructs the underlying byte-stream net.Conn for one rendr stream-mode path. The factory's contract:

  • The returned net.Conn MUST preserve byte order (Read sees Write's bytes in order, unbroken). rendr layers 2-byte length-prefix framing on top; a torn or reordered byte stream will fail HELLO and the path is rejected.
  • The returned connection MUST terminate at the same rendr peer as every other path in the Dialer (docs/plan.md §"项目定位" C1). rendr verifies this in HELLO via the shared flow_id; a mismatch causes the path to be dropped at handshake.

Typical uses:

  • Wrap an xray-core outbound chain (vless / trojan / ss / nested / reverse) so rendr migrates across xray-protected paths.
  • Plug in a custom transport (e.g. a private overlay socket) that isn't worth a full transport.Transport adapter.

addr is passed straight from PathSpec.Address. ctx is honored for dial cancellation.

Directories

Path Synopsis
internal
engine
Package engine houses the rendr migration engine: bridge table, per-Conn state machine, cleanClose discrimination, zombie protection.
Package engine houses the rendr migration engine: bridge table, per-Conn state machine, cleanClose discrimination, zombie protection.
Package mode defines the Scheduler contract between the rendr engine and a mode strategy (prime / bond / race).
Package mode defines the Scheduler contract between the rendr engine and a mode strategy (prime / bond / race).
Package proto defines the rendr control-plane wire format v0.
Package proto defines the rendr control-plane wire format v0.
Package transport defines the contract between the rendr engine and a concrete network transport (tcp, quic, udp_opaque, gvisor, ...).
Package transport defines the contract between the rendr engine and a concrete network transport (tcp, quic, udp_opaque, gvisor, ...).
quic
Package quic is the QUIC transport adapter.
Package quic is the QUIC transport adapter.
tcp
Package tcp is the TCP byte-stream transport adapter.
Package tcp is the TCP byte-stream transport adapter.
udpflow
Package udpflow is the opaque-UDP transport adapter.
Package udpflow is the opaque-UDP transport adapter.
Package xray provides the rendr-as-xray-transport binding.
Package xray provides the rendr-as-xray-transport binding.

Jump to

Keyboard shortcuts

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