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
- Variables
- func EncodeFrame(msgID uint32, flags Flags, payload []byte) []byte
- func StripVP8Descriptor(payload []byte) ([]byte, bool)
- func StripVP9Descriptor(payload []byte) ([]byte, bool)
- type DecodedFrame
- type Dispatcher
- type Flags
- type RateController
- type Reassembler
- type ReceivedFrame
- type Receiver
- type ReleasedFrame
- type Sender
- type StreamManager
- type TunnelPacketConn
- func (c *TunnelPacketConn) Close() error
- func (c *TunnelPacketConn) Deliver(payload []byte) bool
- func (c *TunnelPacketConn) LocalAddr() net.Addr
- func (c *TunnelPacketConn) ReadFrom(p []byte) (int, net.Addr, error)
- func (c *TunnelPacketConn) SetDeadline(t time.Time) error
- func (c *TunnelPacketConn) SetReadDeadline(t time.Time) error
- func (c *TunnelPacketConn) SetWriteDeadline(t time.Time) error
- func (c *TunnelPacketConn) WriteTo(p []byte, _ net.Addr) (int, error)
Constants ¶
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 )
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 )
const VP8KeyframeHeaderLen = 10
VP8KeyframeHeaderLen is the fixed prefix length (10).
Variables ¶
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.
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 ¶
EncodeFrame builds one wire-format frame from a payload. msgID is the sender-monotonic sequence number, flags carries protocol bits.
func StripVP8Descriptor ¶
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
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 )
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 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 (*Receiver) Frames ¶
func (r *Receiver) Frames() <-chan ReceivedFrame
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:
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.
Pacing: enforce a minimum gap (PacingInterval) between two WriteSample calls so REMB/TWCC doesn't throttle us.
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 (*Sender) RateLimit ¶ added in v1.2.0
RateLimit returns the current cap in bits/sec (0 = unlimited).
func (*Sender) Send ¶
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
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.
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 ¶
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