tunnel

package
v1.1.4 Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: Apache-2.0 Imports: 15 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 FrameAssembler

type FrameAssembler struct {

	// PartialDrops counts how many times we threw away a partial frame
	// because a new timestamp arrived before the marker. Useful for telemetry.
	PartialDrops uint64
	// contains filtered or unexported fields
}

FrameAssembler reassembles one logical frame from the stream of per-RTP-packet payloads that share a single RTP timestamp. The last RTP packet of a frame has its marker bit set; that's our signal to flush.

Behaviour summary:

  • First call sets currentTS and arms.
  • Subsequent calls with the same timestamp append.
  • A different timestamp before marker means the previous frame was lost mid-flight; we drop the partial buffer and start fresh on the new TS.
  • A call with marker=true returns the concatenated payload and disarms.

Not safe for concurrent use; expected to be called from a single per-track read loop.

func (*FrameAssembler) Add

func (a *FrameAssembler) Add(timestamp uint32, payload []byte, marker bool) ([]byte, bool)

Add ingests one RTP packet's stripped payload. Returns (frame, true) when a complete frame is ready, otherwise (nil, false).

The caller must pass an already-VP8-descriptor-stripped payload slice. The slice is copied internally because Pion reuses the underlying buffer across ReadRTP calls.

func (*FrameAssembler) Reset

func (a *FrameAssembler) Reset()

Reset clears any partial state. Call when the underlying RTP stream resets (e.g. SSRC change or after teardown).

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
	// 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 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
	// 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 the call-gate measurements: 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) 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) 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