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
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".
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.| 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.
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.
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.
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.
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_maxraised; 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.AddPathrecovers a dropped path back into the active set without reorder artifacts
See LICENSE.