tunnel

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package tunnel implements PoC #2A's framing layer: arbitrary []byte payloads ride inside what the SFU sees as VP8 video frames. One logical message == one media.Sample == one VP8 "frame" (1+ RTP packets sharing a timestamp, last packet has marker=1).

On the wire (after Pion's VP8 payload descriptor):

+--+--+--+--+--+--+--+--+--+--+--+--+--... ----+
|magic 'G' 'T'|ver |flg|     msgID         |  ...payload (length bytes)
|0x47   0x54  |0x01|   |  BE uint32        |     |
+--+--+--+--+--+--+--+--+--+--+--+--+--... ----+
     0    1    2    3   4   5   6   7   8 ... 11+length

Index

Constants

View Source
const (
	// MagicByte0 / MagicByte1 spell "GT" — they distinguish our frames from
	// genuine VP8 video that other (browser) participants in the same room
	// may publish. Receivers drop anything without this prefix silently.
	MagicByte0 byte = 0x47 // 'G'
	MagicByte1 byte = 0x54 // 'T'

	// Version is bumped on any wire-incompatible change to the header.
	Version byte = 0x01

	// HeaderSize is the fixed-length header before payload bytes.
	HeaderSize = 12

	// MaxPayloadSize caps a single frame to 10 MiB. Realistically we send
	// up to ~100 KB during PoC #2A T3, but the cap stops a malformed length
	// field from triggering huge allocations on the receive side.
	MaxPayloadSize = 10 * 1024 * 1024
)
View Source
const (
	// 2026-05-28 — frame cadence reworked to mimic real video. Was
	// BatchSize=6144 / BatchInterval=2ms which, under speedtest
	// saturation, shipped ~600-2000 samples/sec. A DPI classifier
	// counting RTP marker-bit "frames per second" saw 600-2000 fps —
	// impossible for video (real is 24-60 fps). Now BatchSize=64KB +
	// BatchInterval=20ms caps the cadence near ~50 fps while letting
	// frame *size* scale with bitrate, exactly like a constant-fps
	// variable-bitrate video encoder. Combined with computeSampleDuration
	// (timestamps track wall-clock) the flow's temporal fingerprint now
	// matches a genuine 50 fps source.
	DefaultBatchSize             = 64 * 1024
	DefaultBatchInterval         = 20 * time.Millisecond // ~50 fps cadence ceiling
	DefaultPacingInterval        = 500 * time.Microsecond
	DefaultKeepaliveInterval     = 40 * time.Millisecond   // ~25fps when active
	DefaultIdleKeepaliveInterval = 1000 * time.Millisecond // 1fps when idle
	DefaultIdleAfter             = 10 * time.Second        // switch to idle after this much silence

	// Mask randomization defaults — applied on top of the legacy batch
	// pacing constants above. Average bitrate is preserved (jitters are
	// symmetric); only the size/timing *distribution* changes, which is
	// what classifiers fingerprint on. Added 2026-05-27 in response to
	// Yandex Telemost shaping plain DTLS+WG to ~10 kbit/s per track —
	// see [internal/wgrelay/datatunnel.go] for the long story.
	DefaultBatchSizeJitter   = 0.4 // batch threshold in [38KB, 90KB] for BatchSize=64KB
	DefaultPacingJitter      = 200 * time.Microsecond
	DefaultKeyframeEvery     = 50         // ~1 in 50 batches is keyframe-sized (~3% of ships)
	DefaultKeyframeBatchSize = 128 * 1024 // 128 KB — realistic 4K VP9 I-frame spike

	// DefaultKeyframePeriod paints ~1.6% of frames as keyframes which
	// matches real VP9 video streams at ~25 fps with a 2-3s keyframe
	// interval (Chrome / Telemost / Yandex SDK typical config).
	DefaultKeyframePeriod = 60
)
View Source
const VP8KeyframeHeaderLen = 10

VP8KeyframeHeaderLen is the fixed prefix length (10).

Variables

View Source
var (
	ErrShortFrame     = errors.New("tunnel: frame shorter than header")
	ErrBadMagic       = errors.New("tunnel: magic bytes mismatch (likely real video)")
	ErrBadVersion     = errors.New("tunnel: unknown frame version")
	ErrLengthMismatch = errors.New("tunnel: declared length != actual payload size")
	ErrPayloadTooBig  = errors.New("tunnel: declared length exceeds MaxPayloadSize")
)

ErrShortFrame, ErrBadMagic, ErrBadVersion, ErrLengthMismatch are all non-fatal: receivers should drop the offending frame and keep going.

View Source
var VP8KeyframeHeader = []byte{
	0x10, 0x02, 0x00,
	0x9d, 0x01, 0x2a,
	0x40, 0x01,
	0xf0, 0x00,
}

VP8KeyframeHeader is a 10-byte fake VP8 frame header prepended to every outbound tunnel frame so the SFU's keyframe-gated forwarder treats each Sample as a key frame and forwards it. Pion's VP8Payloader doesn't peek inside the payload, and goloom's SFU only inspects the first few bytes to classify frames.

Layout (RFC 6386 §9.1):

byte 0:  0x10  — frame tag bits: key_frame=0 (LSB), version=0,
                 show_frame=1, first_part_size_low=0
byte 1:  0x02  — first_part_size_mid
byte 2:  0x00  — first_part_size_high
byte 3-5: 0x9d 0x01 0x2a — start code (mandatory for keyframes)
byte 6-7: 0x40 0x01     — width = 0x140 = 320
byte 8-9: 0xf0 0x00     — height = 0x0f0 = 240

Receivers MUST skip these 10 bytes (after stripping Pion's RTP payload descriptor) before parsing the tunnel header.

Functions

func EncodeFrame

func EncodeFrame(msgID uint32, flags Flags, payload []byte) []byte

EncodeFrame builds one wire-format frame from a payload. msgID is the sender-monotonic sequence number, flags carries protocol bits.

func StripVP8Descriptor

func StripVP8Descriptor(payload []byte) ([]byte, bool)

StripVP8Descriptor removes the VP8 RTP payload descriptor (RFC 7741 §4.2) from the front of an RTP packet payload and returns the remaining VP8 payload bytes. ok=false means the input is malformed (truncated descriptor).

Descriptor layout:

byte 0: |X|R|N|S|R| PID(3) |          mandatory
byte 1: |I|L|T|K| RSV(4)   |          present iff X=1
byte 2..: PictureID         present iff I=1; 1 byte (M=0) or 2 bytes (M=1, MSB has M flag set)
+1:      TL0PICIDX          present iff L=1
+1:      TID/Y/KEYIDX       present iff T=1 OR K=1 (single byte combines both)

We don't validate the inner VP8 payload (Pion's Sample Builder would, but we never feed our payload to a VP8 decoder — the SFU forwards bytes verbatim). All we care about is computing the correct prefix length to strip so the caller sees pure VP8 video bytes (which in our case are our tunnel frame bytes).

func StripVP9Descriptor added in v1.1.4

func StripVP9Descriptor(payload []byte) ([]byte, bool)

StripVP9Descriptor removes the VP9 RTP payload descriptor (RFC 8741 §4.2) from the front of an RTP packet payload and returns the remaining VP9 payload bytes. ok=false means the input is malformed (truncated descriptor).

Descriptor layout for pion's github.com/pion/rtp/codecs.VP9Payloader in non-flexible mode with PictureID and (for keyframes) Scalability Structure:

byte 0: |I|P|L|F|B|E|V|Z|     mandatory flags
I=1   : |M| PictureID(7)|     1-byte (M=0) or
         | EXT_PID(8)   |     2-byte (M=1) — pion always uses 2-byte
L=1   : |TID|U|SID|D|        1 byte
L=1,F=0: TL0PICIDX            1 byte (non-flexible only)
V=1   : SS structure          variable (set on keyframes)

pion in non-flexible mode + keyframes emits 0x80|0x01 | 0x08(B) | 0x04(E) | 0x02(V) = 0x8F header byte, followed by 2-byte PictureID, then a fixed 8-byte SS (N_S=0, Y=1, G=1, 1 spatial layer 4-byte WxH, N_G=1, 1 PG entry). Total 11-byte descriptor.

Non-keyframes have 0x80|0x40(P)|0x08(B)|0x04(E)|0x01(Z) = 0xCD = 3 bytes.

We don't validate inner VP9 bitstream — the SFU forwards bytes verbatim and we use those bytes as a tunnel for WireGuard datagrams.

2026-05-27 — added during VP8 → VP9 migration to bypass Telemost VP8 shaping. Mirrors StripVP8Descriptor semantics.

Types

type DecodedFrame

type DecodedFrame struct {
	MsgID   uint32
	Flags   Flags
	Payload []byte // alias into the input buffer; copy if you need to outlive it
}

DecodedFrame is what DecodeFrame returns on success.

func DecodeFrame

func DecodeFrame(buf []byte) (DecodedFrame, error)

DecodeFrame validates a complete frame and returns the parsed view. The returned Payload aliases the input slice — callers that buffer it must copy.

type Dispatcher

type Dispatcher struct {
	KCPConn   *TunnelPacketConn
	Handshake chan<- ReceivedFrame
	Legacy    chan<- ReceivedFrame // ping/pong/test frames

	DroppedKCP   uint64
	DroppedOther uint64
}

Dispatcher reads from a merged ReceivedFrame channel and routes frames to the appropriate consumer based on flags:

  • FlagKCP → TunnelPacketConn.Deliver (KCP transport datagrams)
  • FlagHandshake/FlagHandshakeAck → handshake channel
  • FlagPing/FlagPong → legacy test channel

Run blocks until ctx is done or the input channel closes.

func (*Dispatcher) Run

func (d *Dispatcher) Run(ctx context.Context, in <-chan ReceivedFrame, lg *log.Logger)

type Flags

type Flags uint8

Flags occupies one byte. Multiple flags can be combined.

const (
	FlagTest         Flags = 1 << 0 // test-phase traffic (vs WG in future)
	FlagHandshake    Flags = 1 << 1 // HELLO frame, payload = our participantId
	FlagHandshakeAck Flags = 1 << 2 // HELLO_ACK frame, payload = our participantId
	FlagPing         Flags = 1 << 3 // initiator → echoer; payload[:4]=ping msgID, then data
	FlagPong         Flags = 1 << 4 // echoer → initiator; payload echoed verbatim
	FlagKCP          Flags = 1 << 5 // KCP reliable transport datagram

	// FlagFromServer marks frames published by the server-side endpoint
	// (or any of its N pool participants). The receiver side filters by
	// this flag to avoid bot-to-bot cross-talk loops when multiple server
	// participants share a Telemost room: server bot J receives server bot
	// K's track via the SFU broadcast, but must NOT process those frames
	// as if they came from the client. Same logic mirrored on the client
	// pool side — client bots drop frames WITHOUT this flag (own side).
	//
	// 2026-05-27 — added for the SFU pool architecture; backward-compatible
	// with single-instance peers since 0 has been the legacy default and
	// both Sender.SideFlag and Receiver.DropFlag default to 0 (no filtering).
	FlagFromServer Flags = 1 << 6
)

func (Flags) Has

func (f Flags) Has(other Flags) bool

Has reports whether all the bits set in other are also set in f.

type RateController added in v1.2.0

type RateController struct {

	// Tunables (exported for tests / call-site override).
	MinBps      float64
	MaxBps      float64
	LowLoss     float64 // below this smoothed loss we ramp up
	HighLoss    float64 // above this we back off
	IncreaseMul float64 // multiplicative increase per feedback when clean
	EMAWeight   float64 // weight of the newest sample in the loss EMA
	// contains filtered or unexported fields
}

RateController turns the SFU's TWCC transport-feedback into a target send bitrate for the Sender. It exists because Telemost's SFU demotes a publisher whose stream it can't receive cleanly: our open-loop sender overshoots the path, self-induces packet loss, the SFU NACKs relentlessly, we have no RTX to answer with, and after ~10-120 s (faster the higher the overshoot) it unbinds our video slot (mid= emptied) and forwarding stops.

The fix is closed-loop pacing: keep the send rate just under where loss appears, so the SFU sees a clean, gap-free stream and never NACKs. Excess tunnel data then drops LOCALLY in the Sender's queue (before it is assigned an RTP sequence number), which — unlike a sent-then-lost packet — creates NO wire-level sequence gap and therefore NO NACK. The inner TCP-over-WireGuard observes the backpressure and backs off on its own.

The control law is a simple loss-based AIMD, the delay-based half of Google's GCC deliberately omitted (we cannot install pion's gcc pacer without putting a LeakyBucketPacer in the RTP path, which re-clocks the HELLO frames and breaks pairing — see the throughput memo). Feedback arrives only a few times per second, so loss is smoothed with an EMA before acting.

func NewRateController added in v1.2.0

func NewRateController(initialBps float64) *RateController

NewRateController returns a controller seeded at initialBps.

func (*RateController) LossEMA added in v1.2.0

func (rc *RateController) LossEMA() float64

LossEMA returns the current smoothed loss fraction (for diagnostics).

func (*RateController) Observe added in v1.2.0

func (rc *RateController) Observe(delivered, lost int) float64

Observe folds one TWCC feedback window (packets delivered vs lost) into the estimate and returns the updated target bitrate in bits/sec. delivered and lost come straight from github.com/pion/rtcp.TransportLayerCC: delivered = packets with a recv-delta, lost = status_count − delivered.

func (*RateController) Target added in v1.2.0

func (rc *RateController) Target() float64

Target returns the current target bitrate in bits/sec.

type Reassembler added in v1.2.0

type Reassembler struct {
	// MaxWait is how long a frame waits after its LAST received packet before
	// it is released incomplete. It must be measured from the last packet, not
	// from the first: a 64 KB sample spread over a ~1.5 Mbit/s mobile leg takes
	// ~350 ms to arrive in full, so an age-based deadline would cut healthy,
	// loss-free frames in half.
	MaxWait time.Duration

	// MaxAge bounds how long a frame may be held in total, so a source that
	// keeps dribbling packets into one timestamp cannot pin memory forever.
	MaxAge time.Duration
	// MaxPending caps the frames held at once; the oldest is released early.
	MaxPending int

	Complete uint64 // frames released with every packet
	Partial  uint64 // frames released with holes (timeout or eviction)
	Late     uint64 // packets for frames already released
	Dups     uint64 // duplicate packets within a pending frame
	// contains filtered or unexported fields
}

Reassembler rebuilds tunnel samples from RTP packets that may arrive out of order, duplicated, late, or not at all.

Every sample the Sender writes gets its own RTP timestamp (pion advances it by the sample Duration, which is always ≥1 ms), so packets are grouped by timestamp. A frame is complete once its first packet, its marker packet and every sequence number in between have arrived. It is released at once, even while an older frame is still incomplete, so one hole never delays the frames behind it. The first packet is recognised by the VP9 begin-of-frame bit, or by following the marker packet of another frame.

An incomplete frame waits MaxWait for NACK retransmissions and is then released as its contiguous runs (Segments), so the caller can salvage the tunnel frames that did arrive instead of losing the whole ~64 KB sample. Packets for a frame that was already released are dropped as Late.

2026-09-26 — replaces the timestamp+marker FrameAssembler, which discarded the in-progress frame whenever a packet of another frame arrived before the marker. That turned every reordered or retransmitted packet into a lost frame (and was why NACK retransmission used to "poison" the handshake).

Not safe for concurrent use; call from the single per-track read loop.

func NewReassembler added in v1.2.0

func NewReassembler() *Reassembler

NewReassembler returns a Reassembler with the default wait and limits.

func (*Reassembler) Add added in v1.2.0

func (r *Reassembler) Add(out []ReleasedFrame, seq uint16, ts uint32, begin, marker bool, payload []byte) []ReleasedFrame

Add ingests one RTP packet's descriptor-stripped payload and appends every frame that became ready (complete, timed out or evicted) to out. begin is the VP9 B bit, marker the RTP marker bit. payload is copied.

func (*Reassembler) Pending added in v1.2.0

func (r *Reassembler) Pending() int

Pending returns the number of frames currently held.

type ReceivedFrame

type ReceivedFrame struct {
	MsgID   uint32
	Flags   Flags
	Payload []byte
}

type Receiver

type Receiver struct {

	// DropFlag, when non-zero, causes [Receiver.walkFrames] to silently
	// discard any frame whose Flags has DropFlag set. Used by the SFU pool
	// architecture: server-side pool members set DropFlag=FlagFromServer
	// so they drop frames produced by other server-side pool members
	// (which the SFU broadcasts to everyone in the room). 0 = no filtering
	// (legacy single-instance behaviour).
	DropFlag Flags

	RTPPackets     atomic.Uint64
	StripErrs      atomic.Uint64
	BadMagic       atomic.Uint64
	DecodeErrs     atomic.Uint64
	FramesPushed   atomic.Uint64
	HeaderTooShort atomic.Uint64
	HeaderBadStart atomic.Uint64
	SideFiltered   atomic.Uint64 // count of frames dropped via DropFlag
	PartialFrames  atomic.Uint64 // samples released with holes after the reassembly wait
	ResyncFrames   atomic.Uint64 // tunnel frames salvaged from mid-sample fragments
	LatePackets    atomic.Uint64 // packets for samples already released
	// contains filtered or unexported fields
}

func NewReceiver

func NewReceiver(bufSize int) *Receiver

func (*Receiver) Frames

func (r *Receiver) Frames() <-chan ReceivedFrame

func (*Receiver) Run

func (r *Receiver) Run(ctx context.Context, track *webrtc.TrackRemote, lg *log.Logger)

type ReleasedFrame added in v1.2.0

type ReleasedFrame struct {
	// Segments holds the contiguous runs of the frame in sequence order. A
	// complete frame has exactly one segment.
	Segments [][]byte
	Complete bool
	// AtBegin reports that Segments[0] starts at the frame's first packet,
	// i.e. on the codec prefix rather than somewhere mid-frame.
	AtBegin bool
}

ReleasedFrame is one sample handed back by the Reassembler.

type Sender

type Sender struct {
	FrameDuration time.Duration

	VP8Prefix []byte
	VP8Wrap   bool

	BatchSize         int
	BatchInterval     time.Duration
	PacingInterval    time.Duration
	KeepaliveInterval time.Duration

	// IdleKeepaliveInterval is the keepalive cadence used when no real
	// tunnel data has been sent for IdleAfter. Defaults to ~1fps —
	// enough to keep the SFU's track-active heuristics happy while
	// dropping our battery footprint by ~25× compared to the active
	// 25fps cadence. Set to 0 to disable idle-mode entirely.
	IdleKeepaliveInterval time.Duration
	IdleAfter             time.Duration

	// BatchSizeJitter randomizes per-batch flush threshold to
	// BatchSize × (1 ± BatchSizeJitter). 0.0 means constant BatchSize
	// (legacy behaviour); 0.4 means each batch flushes at a size drawn
	// uniformly from [0.6·BatchSize, 1.4·BatchSize]. Mimics the size
	// distribution of real VP8 interframes which varies with motion —
	// uniform 6KB samples every flush is a giveaway to classifiers.
	// See [Sender.pickBatchTarget].
	BatchSizeJitter float64

	// PacingJitter is the maximum ±delay applied on top of PacingInterval
	// per ship. 0 = constant pacing (legacy). Non-zero breaks the
	// periodic-write fingerprint that REMB/TWCC heuristics latch onto.
	PacingJitter time.Duration

	// KeyframeEvery, when >0 and combined with KeyframeBatchSize, makes
	// ~1-in-N batches use KeyframeBatchSize as the flush threshold instead
	// of BatchSize±jitter. Imitates the size spike of a real VP8 keyframe
	// (5-10× larger than interframes). 0 disables the peak entirely.
	KeyframeEvery     int
	KeyframeBatchSize int

	// SideFlag is OR'd into every outgoing frame's Flags byte. Used by the
	// SFU pool architecture to stamp each side's frames so the receiver
	// can drop same-side cross-talk (e.g. server bot J seeing server bot
	// K's frames via SFU broadcast). 0 = legacy single-instance behaviour.
	// See [tunnel.FlagFromServer].
	SideFlag Flags

	// InterframePrefix is the bytes prepended to outbound samples that
	// are NOT periodic keyframes — i.e. ~98% of frames in a realistic
	// VP9 stream. When non-empty and VP8Wrap is true, the sendLoop
	// alternates between [Sender.VP8Prefix] (keyframe header) for every
	// KeyframePeriod-th frame and InterframePrefix for all the others.
	//
	// pion's VP9 packetizer reads the uncompressed header inside the
	// prefix to decide keyframe vs interframe — keyframes emit an 11-byte
	// RTP descriptor (with Scalability Structure), interframes emit a
	// 3-byte descriptor. Real video sources alternate the same way, so
	// matching this ratio is critical for any DPI classifier looking at
	// keyframe-rate fingerprints.
	//
	// Empty (default) preserves the legacy single-prefix behaviour
	// (every frame uses VP8Prefix and is therefore tagged as keyframe).
	// 2026-05-28 — added to fix the "100% keyframes" DPI fingerprint.
	InterframePrefix []byte

	// KeyframePeriod controls how often the data path emits a real
	// keyframe (using VP8Prefix). Every KeyframePeriod-th frame counter
	// uses keyframe prefix; all others use InterframePrefix. 0 disables
	// alternation — every frame is a keyframe (legacy behaviour). Realistic
	// values: 60-90 (matches real WebRTC video sources at ~25-30 fps).
	KeyframePeriod int

	TxSamples atomic.Uint64
	TxBytes   atomic.Uint64
	TxBatches atomic.Uint64
	TxDrops   atomic.Uint64 // batches dropped on full queue (backpressure) — diagnostic

	// WriteMu, when non-nil, serializes track.WriteSample across ALL goroutines
	// that write the same video track (this Sender's data path plus the keyframe
	// refresh / PLI-keyframe helpers). pion's TrackLocalStaticSample assigns
	// seq/ts/pictureID under its own lock but RELEASES it before the per-packet
	// WriteRTP loop, so concurrent writers interleave packets ON THE WIRE — a
	// big data frame's packets get split by an injected keyframe packet, the SFU
	// sheds the out-of-order tail (gappy forward while TWCC stays 0% loss), and
	// with no RTX the gaps are never repaired → slot unbind. Holding one shared
	// mutex around the whole WriteSample makes each frame flush completely before
	// the next starts. Mutates zero wire bytes — only ordering. 2026-07-22.
	WriteMu *sync.Mutex

	// Logger, when non-nil, receives throttled backpressure-drop diagnostics.
	// Optional / nil-safe. Set by the server telemost path (2026-06-25) to
	// tell local queue-overflow stalls apart from network loss.
	Logger *log.Logger
	// contains filtered or unexported fields
}

Sender turns Send([]byte) calls into VP8-shaped media samples.

Three throughput optimizations over a naive one-Send-per-WriteSample:

  1. Batching: concatenate up to BatchSize bytes (or up to BatchInterval time) of frames into a single VP8 sample. Cuts per-RTP-packet overhead and gets us under the SFU's per-packet rate cap.

  2. Pacing: enforce a minimum gap (PacingInterval) between two WriteSample calls so REMB/TWCC doesn't throttle us.

  3. Keepalive: when the batch is empty for KeepaliveInterval, send a real VP8 interframe to keep the SFU's track-active timers happy (without it the SFU eventually stops forwarding the slot).

Tuning is informed by a sibling project: pacing 500µs + batching 6KB/2ms got their tunnel from 6 Mbit/s to ~50 Mbit/s sustained, which appears to be Telemost's per-track bandwidth cap.

func NewSender

func NewSender(track *webrtc.TrackLocalStaticSample) *Sender

func (*Sender) Close

func (s *Sender) Close()

Close stops the background goroutines, flushing whatever is buffered. Idempotent.

func (*Sender) PeekNextID

func (s *Sender) PeekNextID() uint32

func (*Sender) RateLimit added in v1.2.0

func (s *Sender) RateLimit() uint64

RateLimit returns the current cap in bits/sec (0 = unlimited).

func (*Sender) Send

func (s *Sender) Send(flags Flags, payload []byte) (uint32, error)

Send queues a logical message for batched transmission. Returns the assigned msgID. Returns immediately — actual WriteSample happens on the background goroutine after batching/pacing.

SideFlag is OR'd into the supplied flags; for pool-deployed senders this marks the originator side so receivers can drop same-side cross-talk via Receiver.DropFlag.

func (*Sender) SetRateLimit added in v1.2.0

func (s *Sender) SetRateLimit(bps uint64)

SetRateLimit sets the average wire output cap in bits/sec (0 = unlimited). Called by the congestion controller as its TWCC estimate moves. Safe for concurrent use.

func (*Sender) Start

func (s *Sender) Start()

Start launches the background send and batch-flush goroutines. Idempotent. Send() works without Start() but data won't actually be transmitted until Start is called.

type StreamManager

type StreamManager struct {
	PacketConn *TunnelPacketConn
	KCPConn    net.Conn // the single KCP session (as net.Conn)
	KCPSession *kcp.UDPSession
	Mux        *yamux.Session
	Logger     *log.Logger
	IsServer   bool
}

StreamManager holds the full reliable-stream stack:

TunnelPacketConn → KCP (reliable, ordered bytes) → yamux (multiplexed streams)

The initiator calls Listen/Accept; the echoer calls Dial/Open.

func NewStreamManager

func NewStreamManager(pc *TunnelPacketConn, isServer bool, lg *log.Logger) (*StreamManager, error)

NewStreamManager creates the full KCP+yamux stack over the given TunnelPacketConn. isServer should be true for the initiator (screen-sharer) who accepts yamux streams, false for the echoer who opens them.

func (*StreamManager) AcceptStream

func (sm *StreamManager) AcceptStream() (net.Conn, error)

AcceptStream blocks until a peer opens a new yamux stream. Server-side only.

func (*StreamManager) Close

func (sm *StreamManager) Close() error

Close tears down yamux, KCP, and the underlying PacketConn.

func (*StreamManager) OpenStream

func (sm *StreamManager) OpenStream() (net.Conn, error)

OpenStream opens a new yamux stream to the peer. Client-side.

type TunnelPacketConn

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

TunnelPacketConn adapts a Sender + incoming frame channel into a net.PacketConn that KCP can read/write through. KCP sees this as a UDP-like socket; under the hood every WriteTo becomes a tunnel frame with FlagKCP, and every ReadFrom pulls from the dispatcher's channel.

func NewPacketConn

func NewPacketConn(sender *Sender, bufSize int) *TunnelPacketConn

NewPacketConn returns a PacketConn backed by sender for writes and in for reads. The caller must feed KCP-flagged frame payloads into in (see Dispatcher). bufSize controls backpressure on the read side.

func (*TunnelPacketConn) Close

func (c *TunnelPacketConn) Close() error

func (*TunnelPacketConn) Deliver

func (c *TunnelPacketConn) Deliver(payload []byte) bool

Deliver pushes a raw KCP payload (already stripped of the tunnel header) into the read buffer. Called by the dispatcher. Returns false if closed.

func (*TunnelPacketConn) LocalAddr

func (c *TunnelPacketConn) LocalAddr() net.Addr

func (*TunnelPacketConn) ReadFrom

func (c *TunnelPacketConn) ReadFrom(p []byte) (int, net.Addr, error)

ReadFrom blocks until a KCP datagram arrives or the deadline expires. The addr is a fixed dummy — KCP doesn't use it for anything meaningful since we have exactly one peer per PacketConn.

func (*TunnelPacketConn) SetDeadline

func (c *TunnelPacketConn) SetDeadline(t time.Time) error

func (*TunnelPacketConn) SetReadDeadline

func (c *TunnelPacketConn) SetReadDeadline(t time.Time) error

func (*TunnelPacketConn) SetWriteDeadline

func (c *TunnelPacketConn) SetWriteDeadline(t time.Time) error

func (*TunnelPacketConn) WriteTo

func (c *TunnelPacketConn) WriteTo(p []byte, _ net.Addr) (int, error)

WriteTo sends a KCP datagram as a tunnel frame with FlagKCP. The addr is ignored — we always send to the single tunnel peer.

Jump to

Keyboard shortcuts

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