mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-09 11:57:13 +03:00
Add call protocol, Rmux. Update AmneziaWG. Fixes and improvements
This commit is contained in:
69
transport/call/tunnel/config_ack.go
Normal file
69
transport/call/tunnel/config_ack.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const configResendPeriod = 3 * time.Second
|
||||
|
||||
type ConfigAckTracker struct {
|
||||
mu sync.Mutex
|
||||
acked chan struct{}
|
||||
cancel chan struct{}
|
||||
confirmed bool
|
||||
}
|
||||
|
||||
func (t *ConfigAckTracker) Acknowledged() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.confirmed
|
||||
}
|
||||
|
||||
func (t *ConfigAckTracker) Arm() (acked, cancel chan struct{}) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.cancel != nil {
|
||||
close(t.cancel)
|
||||
}
|
||||
t.acked = make(chan struct{})
|
||||
t.cancel = make(chan struct{})
|
||||
return t.acked, t.cancel
|
||||
}
|
||||
|
||||
func (t *ConfigAckTracker) Mark() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.confirmed = true
|
||||
if t.acked == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-t.acked:
|
||||
default:
|
||||
close(t.acked)
|
||||
}
|
||||
}
|
||||
|
||||
func SendVP8ConfigUntilAcked(acked, cancel <-chan struct{}, stopCh <-chan struct{}, tun DataTunnel, fps, batch, trackCount int, logger logger.ContextLogger, logPrefix string) {
|
||||
tun.SendData(EncodeVP8Config(fps, batch, trackCount))
|
||||
ticker := time.NewTicker(configResendPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-acked:
|
||||
return
|
||||
case <-cancel:
|
||||
return
|
||||
case <-stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
logger.Debug(fmt.Sprintf("%s: resending vp8 config fps=%d batch=%d trackCount=%d, no ack yet",
|
||||
logPrefix, fps, batch, trackCount))
|
||||
tun.SendData(EncodeVP8Config(fps, batch, trackCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
230
transport/call/tunnel/dctunnel.go
Normal file
230
transport/call/tunnel/dctunnel.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/pion/datachannel"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const chunkSize = 994
|
||||
|
||||
type chunkBuf struct {
|
||||
chunks [][]byte
|
||||
count int
|
||||
size int
|
||||
}
|
||||
|
||||
type DCTunnel struct {
|
||||
dc *webrtc.DataChannel
|
||||
raw datachannel.ReadWriteCloser
|
||||
writeRaw datachannel.ReadWriteCloser
|
||||
logger logger.ContextLogger
|
||||
onData func([]byte)
|
||||
onClose func()
|
||||
obf *TunnelObfuscator
|
||||
chunked bool
|
||||
readBuf int
|
||||
|
||||
recvBufs sync.Map
|
||||
sendMsgID uint32
|
||||
}
|
||||
|
||||
func NewDCTunnel(dc *webrtc.DataChannel, obf *TunnelObfuscator, readBuf int, logger logger.ContextLogger) *DCTunnel {
|
||||
t := &DCTunnel{dc: dc, obf: obf, readBuf: readBuf, logger: logger}
|
||||
raw, err := dc.Detach()
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("dctunnel: detach failed, using callback mode: %v", err))
|
||||
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
t.deliverMessage(msg.Data)
|
||||
})
|
||||
dc.OnClose(func() {
|
||||
if t.onClose != nil {
|
||||
t.onClose()
|
||||
}
|
||||
})
|
||||
return t
|
||||
}
|
||||
t.raw = raw
|
||||
go t.readLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
func NewDCTunnelFromRaw(dc *webrtc.DataChannel, raw datachannel.ReadWriteCloser, obf *TunnelObfuscator, readBuf int, logger logger.ContextLogger) *DCTunnel {
|
||||
t := &DCTunnel{dc: dc, raw: raw, obf: obf, readBuf: readBuf, logger: logger}
|
||||
go t.readLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
func NewChunkedDCTunnel(readRaw datachannel.ReadWriteCloser, writeDC *webrtc.DataChannel, obf *TunnelObfuscator, readBuf int, logger logger.ContextLogger) *DCTunnel {
|
||||
writeRaw, err := writeDC.Detach()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("dctunnel: write DC detach failed: %v", err))
|
||||
return nil
|
||||
}
|
||||
t := &DCTunnel{raw: readRaw, writeRaw: writeRaw, obf: obf, readBuf: readBuf, logger: logger, chunked: true}
|
||||
go t.readLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
func NewChunkedDCTunnelFromRaw(readRaw, writeRaw datachannel.ReadWriteCloser, obf *TunnelObfuscator, readBuf int, logger logger.ContextLogger) *DCTunnel {
|
||||
t := &DCTunnel{raw: readRaw, writeRaw: writeRaw, obf: obf, readBuf: readBuf, logger: logger, chunked: true}
|
||||
go t.readLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *DCTunnel) SendData(data []byte) {
|
||||
for len(data) >= 4 {
|
||||
frameLen := int(binary.BigEndian.Uint32(data[0:4]))
|
||||
if frameLen < 5 || 4+frameLen > len(data) {
|
||||
return
|
||||
}
|
||||
body := data[4 : 4+frameLen]
|
||||
wire := body
|
||||
if t.obf != nil {
|
||||
wire = t.obf.EncryptPayload(body)
|
||||
if wire == nil {
|
||||
data = data[4+frameLen:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
if t.chunked {
|
||||
t.sendChunked(wire)
|
||||
} else {
|
||||
t.sendRaw(wire)
|
||||
}
|
||||
data = data[4+frameLen:]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DCTunnel) SetOnData(fn func([]byte)) { t.onData = fn }
|
||||
func (t *DCTunnel) OnData() func([]byte) { return t.onData }
|
||||
func (t *DCTunnel) SetOnClose(fn func()) { t.onClose = fn }
|
||||
func (t *DCTunnel) Reconfigure(fps, batch int) {}
|
||||
|
||||
func (t *DCTunnel) readLoop() {
|
||||
buf := make([]byte, t.readBuf)
|
||||
for {
|
||||
n, isString, err := t.raw.ReadDataChannel(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
t.logger.Warn(fmt.Sprintf("dctunnel: read error: %v", err))
|
||||
}
|
||||
if t.onClose != nil {
|
||||
t.onClose()
|
||||
}
|
||||
return
|
||||
}
|
||||
if isString {
|
||||
continue
|
||||
}
|
||||
if t.chunked && n >= 6 {
|
||||
t.handleChunk(buf[:n])
|
||||
} else if n > 0 {
|
||||
t.deliverMessage(buf[:n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DCTunnel) handleChunk(data []byte) {
|
||||
id := uint16(data[0])<<8 | uint16(data[1])
|
||||
idx := int(uint16(data[2])<<8 | uint16(data[3]))
|
||||
total := int(uint16(data[4])<<8 | uint16(data[5]))
|
||||
payload := data[6:]
|
||||
if total == 1 {
|
||||
cp := make([]byte, len(payload))
|
||||
copy(cp, payload)
|
||||
t.deliverMessage(cp)
|
||||
return
|
||||
}
|
||||
val, _ := t.recvBufs.LoadOrStore(id, &chunkBuf{chunks: make([][]byte, total)})
|
||||
cb := val.(*chunkBuf)
|
||||
if idx < len(cb.chunks) && cb.chunks[idx] == nil {
|
||||
cp := make([]byte, len(payload))
|
||||
copy(cp, payload)
|
||||
cb.chunks[idx] = cp
|
||||
cb.count++
|
||||
cb.size += len(cp)
|
||||
}
|
||||
if cb.count == total {
|
||||
t.recvBufs.Delete(id)
|
||||
out := make([]byte, 0, cb.size)
|
||||
for _, c := range cb.chunks {
|
||||
out = append(out, c...)
|
||||
}
|
||||
t.deliverMessage(out)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DCTunnel) deliverMessage(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
if t.obf != nil {
|
||||
pt, ok := t.obf.DecryptPayload(data)
|
||||
if !ok {
|
||||
t.logger.Debug(fmt.Sprintf("dctunnel: decrypt failed, dropping %d bytes", len(data)))
|
||||
return
|
||||
}
|
||||
data = pt
|
||||
}
|
||||
if t.onData != nil && len(data) > 0 {
|
||||
frame := make([]byte, 4+len(data))
|
||||
binary.BigEndian.PutUint32(frame[0:4], uint32(len(data)))
|
||||
copy(frame[4:], data)
|
||||
t.onData(frame)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DCTunnel) sendChunked(data []byte) {
|
||||
w := t.writeRaw
|
||||
if w == nil {
|
||||
w = t.raw
|
||||
}
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
total := int(math.Ceil(float64(len(data)) / float64(chunkSize)))
|
||||
if total == 0 {
|
||||
total = 1
|
||||
}
|
||||
id := uint16(atomic.AddUint32(&t.sendMsgID, 1)) & 0xFFFF
|
||||
for i := 0; i < total; i++ {
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
p := data[start:end]
|
||||
f := make([]byte, 6+len(p))
|
||||
f[0] = byte(id >> 8)
|
||||
f[1] = byte(id & 0xFF)
|
||||
f[2] = byte(i >> 8)
|
||||
f[3] = byte(i & 0xFF)
|
||||
f[4] = byte(total >> 8)
|
||||
f[5] = byte(total & 0xFF)
|
||||
copy(f[6:], p)
|
||||
w.Write(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DCTunnel) sendRaw(data []byte) {
|
||||
w := t.writeRaw
|
||||
if w == nil {
|
||||
w = t.raw
|
||||
}
|
||||
if w != nil {
|
||||
w.Write(data)
|
||||
return
|
||||
}
|
||||
if t.dc == nil || t.dc.ReadyState() != webrtc.DataChannelStateOpen {
|
||||
return
|
||||
}
|
||||
t.dc.Send(data)
|
||||
}
|
||||
69
transport/call/tunnel/dctunnel_bench_test.go
Normal file
69
transport/call/tunnel/dctunnel_bench_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type discardRawConn struct{}
|
||||
|
||||
func (discardRawConn) Read(p []byte) (int, error) { return 0, io.EOF }
|
||||
func (discardRawConn) ReadDataChannel(p []byte) (int, bool, error) { return 0, false, io.EOF }
|
||||
func (discardRawConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||
func (discardRawConn) WriteDataChannel(p []byte, isString bool) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
func (discardRawConn) Close() error { return nil }
|
||||
|
||||
type benchLogger struct{}
|
||||
|
||||
func (benchLogger) Trace(args ...any) {}
|
||||
func (benchLogger) Debug(args ...any) {}
|
||||
func (benchLogger) Info(args ...any) {}
|
||||
func (benchLogger) Notice(args ...any) {}
|
||||
func (benchLogger) Warn(args ...any) {}
|
||||
func (benchLogger) Error(args ...any) {}
|
||||
func (benchLogger) Fatal(args ...any) {}
|
||||
func (benchLogger) Panic(args ...any) {}
|
||||
func (benchLogger) TraceContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) DebugContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) InfoContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) NoticeContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) WarnContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) ErrorContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) FatalContext(ctx context.Context, args ...any) {}
|
||||
func (benchLogger) PanicContext(ctx context.Context, args ...any) {}
|
||||
|
||||
func newBenchDCTunnel() *DCTunnel {
|
||||
return &DCTunnel{raw: discardRawConn{}, logger: benchLogger{}, readBuf: 4096}
|
||||
}
|
||||
|
||||
func BenchmarkDCTunnelSendData(b *testing.B) {
|
||||
sizes := []int{64, 512, 4096}
|
||||
for _, size := range sizes {
|
||||
payload := make([]byte, size)
|
||||
frame := EncodeFrame(42, MsgData, payload)
|
||||
b.Run(sizeLabel(size), func(b *testing.B) {
|
||||
t := newBenchDCTunnel()
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(frame)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
t.SendData(frame)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sizeLabel(n int) string {
|
||||
switch n {
|
||||
case 64:
|
||||
return "64B"
|
||||
case 512:
|
||||
return "512B"
|
||||
case 4096:
|
||||
return "4KB"
|
||||
default:
|
||||
return "custom"
|
||||
}
|
||||
}
|
||||
400
transport/call/tunnel/multi_track_kcp_tunnel.go
Normal file
400
transport/call/tunnel/multi_track_kcp_tunnel.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
kcp "github.com/xtaci/kcp-go/v5"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
kcpConvBase = 0x77627374
|
||||
kcpUpdateInterval = 10 * time.Millisecond
|
||||
// One KCP segment must ride in a single RTP packet so a dropped packet
|
||||
// loses only its own frame, not a two-packet frame that readVP8Track
|
||||
// would discard whole. 1200 RTP budget - 1 VP8 descriptor - interframe
|
||||
// header - 24 XChaCha20 nonce - 16 Poly1305 tag - 1 channel tag.
|
||||
kcpSegmentMTU = 1200 - 1 - interframeHdrLen - 24 - 16 - 1
|
||||
kcpReceiveBufSize = 128 * 1024
|
||||
kcpStatsEvery = 500
|
||||
|
||||
kcpWindowFloor = 64
|
||||
kcpWindowCeiling = 512
|
||||
kcpCarrierRTT = 250 * time.Millisecond
|
||||
kcpWaitSndFactor = 2
|
||||
kcpBackpressurePoll = 2 * time.Millisecond
|
||||
|
||||
kcpChannelReliable byte = 0x00
|
||||
kcpChannelRaw byte = 0x01
|
||||
|
||||
KCPCarrierQueueDepth = kcpWaitSndFactor * kcpWindowCeiling
|
||||
)
|
||||
|
||||
func computeKCPWindow(fps, batch int) int {
|
||||
rate := fps * batch
|
||||
if rate < 1 {
|
||||
rate = defaultVP8FPS * defaultVP8Batch
|
||||
}
|
||||
window := int(float64(rate) * kcpCarrierRTT.Seconds())
|
||||
if window < kcpWindowFloor {
|
||||
return kcpWindowFloor
|
||||
}
|
||||
if window > kcpWindowCeiling {
|
||||
return kcpWindowCeiling
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
type trackKCPSession struct {
|
||||
conv uint32
|
||||
vp8 *VP8DataTunnel
|
||||
parent *MultiTrackKCPTunnel
|
||||
kcpMu sync.Mutex
|
||||
kcp *kcp.KCP
|
||||
recvBuf []byte
|
||||
}
|
||||
|
||||
func newTrackKCPSession(parent *MultiTrackKCPTunnel, vp8 *VP8DataTunnel, conv uint32, window int) *trackKCPSession {
|
||||
session := &trackKCPSession{
|
||||
conv: conv,
|
||||
vp8: vp8,
|
||||
parent: parent,
|
||||
recvBuf: make([]byte, kcpReceiveBufSize),
|
||||
}
|
||||
session.kcp = kcp.NewKCP(conv, func(buf []byte, size int) {
|
||||
if size <= 0 {
|
||||
return
|
||||
}
|
||||
segment := make([]byte, size+1)
|
||||
segment[0] = kcpChannelReliable
|
||||
copy(segment[1:], buf[:size])
|
||||
parent.outputSegments.Add(1)
|
||||
if !session.vp8.TrySendData(segment) {
|
||||
parent.droppedSegments.Add(1)
|
||||
}
|
||||
})
|
||||
session.kcp.NoDelay(1, 10, 2, 1)
|
||||
session.kcp.WndSize(window, window)
|
||||
session.kcp.SetMtu(kcpSegmentMTU)
|
||||
return session
|
||||
}
|
||||
|
||||
func (s *trackKCPSession) setWindow(window int) {
|
||||
s.kcpMu.Lock()
|
||||
s.kcp.WndSize(window, window)
|
||||
s.kcpMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *trackKCPSession) send(frame []byte) {
|
||||
s.kcpMu.Lock()
|
||||
s.kcp.Send(frame)
|
||||
s.kcp.Update()
|
||||
s.kcpMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *trackKCPSession) input(segment []byte) [][]byte {
|
||||
s.kcpMu.Lock()
|
||||
s.kcp.Input(segment, kcp.IKCP_PACKET_REGULAR, true)
|
||||
var messages [][]byte
|
||||
for {
|
||||
size := s.kcp.PeekSize()
|
||||
if size <= 0 {
|
||||
break
|
||||
}
|
||||
if size > len(s.recvBuf) {
|
||||
s.recvBuf = make([]byte, size)
|
||||
}
|
||||
n := s.kcp.Recv(s.recvBuf)
|
||||
if n <= 0 {
|
||||
break
|
||||
}
|
||||
message := make([]byte, n)
|
||||
copy(message, s.recvBuf[:n])
|
||||
messages = append(messages, message)
|
||||
}
|
||||
s.kcpMu.Unlock()
|
||||
return messages
|
||||
}
|
||||
|
||||
func (s *trackKCPSession) update() {
|
||||
s.kcpMu.Lock()
|
||||
s.kcp.Update()
|
||||
s.kcpMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *trackKCPSession) waitSnd() int {
|
||||
s.kcpMu.Lock()
|
||||
pending := s.kcp.WaitSnd()
|
||||
s.kcpMu.Unlock()
|
||||
return pending
|
||||
}
|
||||
|
||||
type MultiTrackKCPTunnel struct {
|
||||
mt *MultiTrackTunnel
|
||||
logger logger.ContextLogger
|
||||
|
||||
mu sync.Mutex
|
||||
sessions []*trackKCPSession
|
||||
convMap map[uint32]*trackKCPSession
|
||||
connPin map[uint32]int
|
||||
onData func([]byte)
|
||||
onClose func()
|
||||
|
||||
stopCh chan struct{}
|
||||
stopOnce sync.Once
|
||||
|
||||
currentWindow atomic.Int32
|
||||
|
||||
sentMessages atomic.Uint64
|
||||
deliveredMessages atomic.Uint64
|
||||
outputSegments atomic.Uint64
|
||||
inputSegments atomic.Uint64
|
||||
rawSent atomic.Uint64
|
||||
rawReceived atomic.Uint64
|
||||
droppedSegments atomic.Uint64
|
||||
}
|
||||
|
||||
func NewMultiTrackKCPTunnel(mt *MultiTrackTunnel, logger logger.ContextLogger) *MultiTrackKCPTunnel {
|
||||
t := &MultiTrackKCPTunnel{
|
||||
mt: mt,
|
||||
logger: logger,
|
||||
convMap: make(map[uint32]*trackKCPSession),
|
||||
connPin: make(map[uint32]int),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
subs := mt.SubTunnels()
|
||||
window := kcpWindowFloor
|
||||
if len(subs) > 0 {
|
||||
window = computeKCPWindow(subs[0].FPS(), subs[0].Batch())
|
||||
}
|
||||
t.currentWindow.Store(int32(window))
|
||||
for i, sub := range subs {
|
||||
conv := uint32(kcpConvBase + i)
|
||||
session := newTrackKCPSession(t, sub, conv, window)
|
||||
t.sessions = append(t.sessions, session)
|
||||
t.convMap[conv] = session
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Debug(fmt.Sprintf("kcptunnel: init tracks=%d window=%d queue=%d", len(subs), window, KCPCarrierQueueDepth))
|
||||
}
|
||||
mt.SetOnData(t.handleDecodedSegment)
|
||||
mt.SetOnClose(t.handleInnerClose)
|
||||
go t.updateLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) SendData(frame []byte) {
|
||||
if len(frame) < 9 {
|
||||
return
|
||||
}
|
||||
connID := binary.BigEndian.Uint32(frame[4:8])
|
||||
msgType := frame[8]
|
||||
|
||||
if msgType == MsgUDP || msgType == MsgUDPReply {
|
||||
t.sendRaw(connID, frame)
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
if len(t.sessions) == 0 {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
index, pinned := t.connPin[connID]
|
||||
if !pinned || index >= len(t.sessions) {
|
||||
index = int(connID % uint32(len(t.sessions)))
|
||||
t.connPin[connID] = index
|
||||
}
|
||||
session := t.sessions[index]
|
||||
t.mu.Unlock()
|
||||
|
||||
if msgType == MsgData {
|
||||
sndCap := int(t.currentWindow.Load()) * kcpWaitSndFactor
|
||||
for session.waitSnd() >= sndCap {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
return
|
||||
case <-time.After(kcpBackpressurePoll):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.sentMessages.Add(1)
|
||||
session.send(frame)
|
||||
|
||||
if msgType == MsgClose {
|
||||
t.mu.Lock()
|
||||
delete(t.connPin, connID)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) sendRaw(connID uint32, frame []byte) {
|
||||
t.mu.Lock()
|
||||
if len(t.sessions) == 0 {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
index := int(connID % uint32(len(t.sessions)))
|
||||
session := t.sessions[index]
|
||||
t.mu.Unlock()
|
||||
|
||||
segment := make([]byte, len(frame)+1)
|
||||
segment[0] = kcpChannelRaw
|
||||
copy(segment[1:], frame)
|
||||
t.rawSent.Add(1)
|
||||
session.vp8.TrySendData(segment)
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) InjectSegment(payload []byte) {
|
||||
t.handleDecodedSegment(payload)
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) handleDecodedSegment(payload []byte) {
|
||||
if len(payload) < 1 {
|
||||
return
|
||||
}
|
||||
channel := payload[0]
|
||||
body := payload[1:]
|
||||
|
||||
if channel == kcpChannelRaw {
|
||||
t.mu.Lock()
|
||||
callback := t.onData
|
||||
t.mu.Unlock()
|
||||
if callback == nil {
|
||||
return
|
||||
}
|
||||
t.rawReceived.Add(1)
|
||||
callback(body)
|
||||
return
|
||||
}
|
||||
|
||||
if len(body) < 4 {
|
||||
return
|
||||
}
|
||||
conv := binary.LittleEndian.Uint32(body[0:4])
|
||||
t.mu.Lock()
|
||||
session := t.convMap[conv]
|
||||
callback := t.onData
|
||||
t.mu.Unlock()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
t.inputSegments.Add(1)
|
||||
messages := session.input(body)
|
||||
if callback == nil {
|
||||
return
|
||||
}
|
||||
for _, message := range messages {
|
||||
t.deliveredMessages.Add(1)
|
||||
callback(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) SetOnData(fn func([]byte)) {
|
||||
t.mu.Lock()
|
||||
t.onData = fn
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) SetOnClose(fn func()) {
|
||||
t.mu.Lock()
|
||||
t.onClose = fn
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) Reconfigure(fps, batch int) {
|
||||
t.mt.Reconfigure(fps, batch)
|
||||
window := computeKCPWindow(fps, batch)
|
||||
t.applyWindow(window)
|
||||
if t.logger != nil {
|
||||
t.logger.Debug(fmt.Sprintf("kcptunnel: reconfigure fps=%d batch=%d -> window=%d", fps, batch, window))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) applyWindow(window int) {
|
||||
t.currentWindow.Store(int32(window))
|
||||
t.mu.Lock()
|
||||
sessions := make([]*trackKCPSession, len(t.sessions))
|
||||
copy(sessions, t.sessions)
|
||||
t.mu.Unlock()
|
||||
for _, session := range sessions {
|
||||
session.setWindow(window)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) AddSession(sub *VP8DataTunnel) {
|
||||
window := int(t.currentWindow.Load())
|
||||
t.mu.Lock()
|
||||
conv := uint32(kcpConvBase + len(t.sessions))
|
||||
session := newTrackKCPSession(t, sub, conv, window)
|
||||
t.sessions = append(t.sessions, session)
|
||||
t.convMap[conv] = session
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) RemoveLastSession() {
|
||||
t.mu.Lock()
|
||||
if len(t.sessions) <= 1 {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
last := t.sessions[len(t.sessions)-1]
|
||||
t.sessions = t.sessions[:len(t.sessions)-1]
|
||||
delete(t.convMap, last.conv)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) Stop() {
|
||||
t.stopOnce.Do(func() { close(t.stopCh) })
|
||||
t.mt.Stop()
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) StopLayer() {
|
||||
t.stopOnce.Do(func() { close(t.stopCh) })
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) handleInnerClose() {
|
||||
t.stopOnce.Do(func() { close(t.stopCh) })
|
||||
t.mu.Lock()
|
||||
callback := t.onClose
|
||||
t.mu.Unlock()
|
||||
if callback != nil {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MultiTrackKCPTunnel) updateLoop() {
|
||||
ticker := time.NewTicker(kcpUpdateInterval)
|
||||
defer ticker.Stop()
|
||||
ticks := 0
|
||||
for {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
t.mu.Lock()
|
||||
sessions := make([]*trackKCPSession, len(t.sessions))
|
||||
copy(sessions, t.sessions)
|
||||
t.mu.Unlock()
|
||||
for _, session := range sessions {
|
||||
session.update()
|
||||
}
|
||||
ticks++
|
||||
if ticks%kcpStatsEvery == 0 && t.logger != nil {
|
||||
snmp := kcp.DefaultSnmp.Copy()
|
||||
t.logger.Debug(fmt.Sprintf("kcptunnel: sessions=%d window=%d sent=%d delivered=%d out_segs=%d in_segs=%d raw_out=%d raw_in=%d dropped=%d",
|
||||
len(sessions), t.currentWindow.Load(), t.sentMessages.Load(), t.deliveredMessages.Load(),
|
||||
t.outputSegments.Load(), t.inputSegments.Load(),
|
||||
t.rawSent.Load(), t.rawReceived.Load(), t.droppedSegments.Load()))
|
||||
t.logger.Debug(fmt.Sprintf("kcptunnel: kcp_out=%d kcp_in=%d retrans=%d fastretrans=%d lost=%d repeat=%d",
|
||||
snmp.OutSegs, snmp.InSegs, snmp.RetransSegs, snmp.FastRetransSegs, snmp.LostSegs, snmp.RepeatSegs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
transport/call/tunnel/multi_track_tunnel.go
Normal file
199
transport/call/tunnel/multi_track_tunnel.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MultiTrackTunnel struct {
|
||||
tunnels []*VP8DataTunnel
|
||||
|
||||
mu sync.Mutex
|
||||
onData func([]byte)
|
||||
onClose func()
|
||||
onPeerRestart func()
|
||||
isClosed bool
|
||||
fps int
|
||||
batch int
|
||||
}
|
||||
|
||||
func NewMultiTrackTunnel(tunnels []*VP8DataTunnel) *MultiTrackTunnel {
|
||||
m := &MultiTrackTunnel{tunnels: tunnels}
|
||||
for i, tun := range tunnels {
|
||||
m.wireSubTunnel(tun, i == 0)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) AddSubTunnel(tun *VP8DataTunnel) {
|
||||
m.mu.Lock()
|
||||
if m.isClosed {
|
||||
m.mu.Unlock()
|
||||
tun.Stop()
|
||||
return
|
||||
}
|
||||
m.tunnels = append(m.tunnels, tun)
|
||||
fps := m.fps
|
||||
batch := m.batch
|
||||
m.mu.Unlock()
|
||||
m.wireSubTunnel(tun, false)
|
||||
if fps > 0 && batch > 0 {
|
||||
tun.Start(fps, batch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) RemoveLastSubTunnel() *VP8DataTunnel {
|
||||
m.mu.Lock()
|
||||
if len(m.tunnels) <= 1 {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
last := m.tunnels[len(m.tunnels)-1]
|
||||
m.tunnels = m.tunnels[:len(m.tunnels)-1]
|
||||
m.mu.Unlock()
|
||||
last.Stop()
|
||||
return last
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) SubTunnelCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.tunnels)
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) SendData(data []byte) {
|
||||
m.mu.Lock()
|
||||
tunnels := m.tunnels
|
||||
m.mu.Unlock()
|
||||
if len(tunnels) == 0 {
|
||||
return
|
||||
}
|
||||
var connID uint32
|
||||
if len(data) >= 8 {
|
||||
connID = binary.BigEndian.Uint32(data[4:8])
|
||||
}
|
||||
idx := connID % uint32(len(tunnels))
|
||||
tunnels[idx].SendData(data)
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) DeliverData(data []byte) {
|
||||
m.mu.Lock()
|
||||
handler := m.onData
|
||||
m.mu.Unlock()
|
||||
if handler != nil {
|
||||
handler(data)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) SubTunnels() []*VP8DataTunnel {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
subs := make([]*VP8DataTunnel, len(m.tunnels))
|
||||
copy(subs, m.tunnels)
|
||||
return subs
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) SetOnData(fn func([]byte)) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.onData = fn
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) SetOnClose(fn func()) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.onClose = fn
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) SetOnPeerRestart(fn func()) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.onPeerRestart = fn
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) Reconfigure(fps, batch int) {
|
||||
m.mu.Lock()
|
||||
m.fps = fps
|
||||
m.batch = batch
|
||||
tunnels := m.tunnels
|
||||
m.mu.Unlock()
|
||||
for _, tun := range tunnels {
|
||||
tun.Reconfigure(fps, batch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) Start(fps, batch int) {
|
||||
m.mu.Lock()
|
||||
m.fps = fps
|
||||
m.batch = batch
|
||||
tunnels := m.tunnels
|
||||
m.mu.Unlock()
|
||||
for _, tun := range tunnels {
|
||||
tun.Start(fps, batch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) Stop() {
|
||||
m.mu.Lock()
|
||||
if m.isClosed {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.isClosed = true
|
||||
tunnels := m.tunnels
|
||||
m.mu.Unlock()
|
||||
for _, tun := range tunnels {
|
||||
tun.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) HandleFrame(frame []byte) {
|
||||
m.mu.Lock()
|
||||
var first *VP8DataTunnel
|
||||
if len(m.tunnels) > 0 {
|
||||
first = m.tunnels[0]
|
||||
}
|
||||
m.mu.Unlock()
|
||||
if first != nil {
|
||||
first.HandleFrame(frame)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiTrackTunnel) wireSubTunnel(tun *VP8DataTunnel, isCamera bool) {
|
||||
tun.SetOnData(func(data []byte) {
|
||||
m.mu.Lock()
|
||||
handler := m.onData
|
||||
m.mu.Unlock()
|
||||
if handler != nil {
|
||||
handler(data)
|
||||
}
|
||||
})
|
||||
if !isCamera {
|
||||
return
|
||||
}
|
||||
tun.SetOnPeerRestart(func() {
|
||||
m.mu.Lock()
|
||||
handler := m.onPeerRestart
|
||||
m.mu.Unlock()
|
||||
if handler != nil {
|
||||
handler()
|
||||
}
|
||||
})
|
||||
tun.SetOnClose(func() {
|
||||
m.mu.Lock()
|
||||
if m.isClosed {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.isClosed = true
|
||||
closeHandler := m.onClose
|
||||
subTunnels := m.tunnels
|
||||
m.mu.Unlock()
|
||||
for _, t := range subTunnels {
|
||||
t.Stop()
|
||||
}
|
||||
if closeHandler != nil {
|
||||
closeHandler()
|
||||
}
|
||||
})
|
||||
}
|
||||
221
transport/call/tunnel/obfuscator.go
Normal file
221
transport/call/tunnel/obfuscator.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
var vp8Keepalive = []byte{
|
||||
0x30, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x10, 0x00,
|
||||
0x10, 0x00, 0x00, 0x47, 0x08, 0x85, 0x85, 0x88,
|
||||
0x99, 0x84, 0x88, 0xfc,
|
||||
}
|
||||
|
||||
var vp8Interframe = []byte{
|
||||
0xb1, 0x01, 0x00, 0x08, 0x11, 0x18, 0x00, 0x18,
|
||||
0x00, 0x18, 0x58, 0x2f, 0xf4, 0x00, 0x08, 0x00,
|
||||
0x00,
|
||||
}
|
||||
|
||||
const (
|
||||
vp8KeepaliveLen = 20
|
||||
vp8InterframeLen = 17
|
||||
epochFieldLen = 4
|
||||
keepaliveHdrLen = vp8KeepaliveLen + epochFieldLen
|
||||
interframeHdrLen = vp8InterframeLen + epochFieldLen
|
||||
)
|
||||
|
||||
var ErrEmptySecret = errors.New("tunnel: obfuscator requires a non-empty secret")
|
||||
|
||||
type DecodeResult struct {
|
||||
HasFrame bool
|
||||
Keepalive bool
|
||||
SelfEcho bool
|
||||
PeerRestart bool
|
||||
Payload []byte
|
||||
PeerEpoch uint32
|
||||
}
|
||||
|
||||
type TunnelObfuscator struct {
|
||||
aead cipher.AEAD
|
||||
localEpoch uint32
|
||||
|
||||
mu sync.Mutex
|
||||
peerEpoch uint32
|
||||
hasPeer bool
|
||||
}
|
||||
|
||||
func DeriveSecretFromJoinLink(joinLink string) []byte {
|
||||
token := extractJoinToken(joinLink)
|
||||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
return []byte(token)
|
||||
}
|
||||
|
||||
func NewTunnelObfuscator(secret []byte) (*TunnelObfuscator, error) {
|
||||
if len(secret) == 0 {
|
||||
return nil, ErrEmptySecret
|
||||
}
|
||||
keyHash := sha256.Sum256(secret)
|
||||
aead, err := chacha20poly1305.NewX(keyHash[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var epochBytes [4]byte
|
||||
if _, err := rand.Read(epochBytes[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
epoch := binary.BigEndian.Uint32(epochBytes[:])
|
||||
if epoch == 0 {
|
||||
epoch = 1
|
||||
}
|
||||
return &TunnelObfuscator{aead: aead, localEpoch: epoch}, nil
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) LocalEpoch() uint32 { return o.localEpoch }
|
||||
|
||||
func (o *TunnelObfuscator) EncodeKeepalive(padLen int) []byte {
|
||||
hdr := o.keepaliveHeader()
|
||||
if padLen <= 0 {
|
||||
return hdr
|
||||
}
|
||||
out := make([]byte, keepaliveHdrLen+padLen)
|
||||
copy(out, hdr)
|
||||
if _, err := rand.Read(out[keepaliveHdrLen:]); err != nil {
|
||||
return hdr
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) EncodeData(payload []byte) []byte {
|
||||
hdr := o.dataHeader()
|
||||
nonce := make([]byte, o.aead.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]byte, 0, len(hdr)+len(nonce)+len(payload)+o.aead.Overhead())
|
||||
out = append(out, hdr...)
|
||||
out = append(out, nonce...)
|
||||
out = o.aead.Seal(out, nonce, payload, nil)
|
||||
return out
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) EncryptPayload(plaintext []byte) []byte {
|
||||
if o == nil {
|
||||
return plaintext
|
||||
}
|
||||
nonce := make([]byte, o.aead.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]byte, 0, len(nonce)+len(plaintext)+o.aead.Overhead())
|
||||
out = append(out, nonce...)
|
||||
return o.aead.Seal(out, nonce, plaintext, nil)
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) DecryptPayload(data []byte) ([]byte, bool) {
|
||||
if o == nil {
|
||||
return data, true
|
||||
}
|
||||
nonceSize := o.aead.NonceSize()
|
||||
if len(data) < nonceSize+o.aead.Overhead() {
|
||||
return nil, false
|
||||
}
|
||||
nonce := data[:nonceSize]
|
||||
ciphertext := data[nonceSize:]
|
||||
plaintext, err := o.aead.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return plaintext, true
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) Decode(frame []byte) DecodeResult {
|
||||
if len(frame) < 1 {
|
||||
return DecodeResult{}
|
||||
}
|
||||
var hdrLen, epochOff int
|
||||
isKeepaliveFrame := false
|
||||
switch frame[0] {
|
||||
case vp8Keepalive[0]:
|
||||
hdrLen = keepaliveHdrLen
|
||||
epochOff = vp8KeepaliveLen
|
||||
isKeepaliveFrame = true
|
||||
case vp8Interframe[0]:
|
||||
hdrLen = interframeHdrLen
|
||||
epochOff = vp8InterframeLen
|
||||
default:
|
||||
return DecodeResult{}
|
||||
}
|
||||
if len(frame) < hdrLen {
|
||||
return DecodeResult{}
|
||||
}
|
||||
peerEpoch := binary.BigEndian.Uint32(frame[epochOff : epochOff+epochFieldLen])
|
||||
if peerEpoch == o.localEpoch {
|
||||
return DecodeResult{HasFrame: true, SelfEcho: true, PeerEpoch: peerEpoch}
|
||||
}
|
||||
res := DecodeResult{HasFrame: true, PeerEpoch: peerEpoch}
|
||||
o.mu.Lock()
|
||||
if !o.hasPeer {
|
||||
o.peerEpoch = peerEpoch
|
||||
o.hasPeer = true
|
||||
} else if o.peerEpoch != peerEpoch {
|
||||
o.peerEpoch = peerEpoch
|
||||
res.PeerRestart = true
|
||||
}
|
||||
o.mu.Unlock()
|
||||
if isKeepaliveFrame || len(frame) == hdrLen {
|
||||
res.Keepalive = true
|
||||
return res
|
||||
}
|
||||
body := frame[hdrLen:]
|
||||
nonceSize := o.aead.NonceSize()
|
||||
if len(body) < nonceSize+o.aead.Overhead() {
|
||||
return DecodeResult{}
|
||||
}
|
||||
nonce := body[:nonceSize]
|
||||
ciphertext := body[nonceSize:]
|
||||
plaintext, err := o.aead.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return DecodeResult{}
|
||||
}
|
||||
res.Payload = plaintext
|
||||
return res
|
||||
}
|
||||
|
||||
func extractJoinToken(joinLink string) string {
|
||||
s := strings.TrimSpace(joinLink)
|
||||
s = strings.TrimRight(s, "/")
|
||||
if i := strings.IndexByte(s, '?'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
if i := strings.LastIndexByte(s, '/'); i >= 0 {
|
||||
s = s[i+1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) keepaliveHeader() []byte {
|
||||
hdr := make([]byte, keepaliveHdrLen)
|
||||
copy(hdr, vp8Keepalive)
|
||||
binary.BigEndian.PutUint32(hdr[vp8KeepaliveLen:], o.localEpoch)
|
||||
return hdr
|
||||
}
|
||||
|
||||
func (o *TunnelObfuscator) dataHeader() []byte {
|
||||
hdr := make([]byte, interframeHdrLen)
|
||||
copy(hdr, vp8Interframe)
|
||||
binary.BigEndian.PutUint32(hdr[vp8InterframeLen:], o.localEpoch)
|
||||
return hdr
|
||||
}
|
||||
94
transport/call/tunnel/protocol.go
Normal file
94
transport/call/tunnel/protocol.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package tunnel
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
const (
|
||||
MsgConnect byte = 0x01
|
||||
MsgConnectOK byte = 0x02
|
||||
MsgConnectErr byte = 0x03
|
||||
MsgData byte = 0x04
|
||||
MsgClose byte = 0x05
|
||||
MsgUDP byte = 0x06
|
||||
MsgUDPReply byte = 0x07
|
||||
MsgConfig byte = 0x08
|
||||
MsgConfigAck byte = 0x09
|
||||
)
|
||||
|
||||
const ControlConnID uint32 = 0
|
||||
|
||||
type DataTunnel interface {
|
||||
SendData(data []byte)
|
||||
SetOnData(fn func([]byte))
|
||||
SetOnClose(fn func())
|
||||
Reconfigure(fps, batch int)
|
||||
}
|
||||
|
||||
func EncodeVP8Config(fps, batch, trackCount int) []byte {
|
||||
if fps < 1 {
|
||||
fps = 1
|
||||
}
|
||||
if batch < 1 {
|
||||
batch = 1
|
||||
}
|
||||
if trackCount < 1 {
|
||||
trackCount = 1
|
||||
}
|
||||
if fps > 0xFFFF {
|
||||
fps = 0xFFFF
|
||||
}
|
||||
if batch > 0xFFFF {
|
||||
batch = 0xFFFF
|
||||
}
|
||||
if trackCount > 0xFFFF {
|
||||
trackCount = 0xFFFF
|
||||
}
|
||||
var payload [6]byte
|
||||
binary.BigEndian.PutUint16(payload[0:2], uint16(fps))
|
||||
binary.BigEndian.PutUint16(payload[2:4], uint16(batch))
|
||||
binary.BigEndian.PutUint16(payload[4:6], uint16(trackCount))
|
||||
return EncodeFrame(ControlConnID, MsgConfig, payload[:])
|
||||
}
|
||||
|
||||
func DecodeVP8Config(payload []byte) (fps, batch, trackCount int, ok bool) {
|
||||
if len(payload) < 4 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
fps = int(binary.BigEndian.Uint16(payload[0:2]))
|
||||
batch = int(binary.BigEndian.Uint16(payload[2:4]))
|
||||
trackCount = 1
|
||||
if len(payload) >= 6 {
|
||||
trackCount = int(binary.BigEndian.Uint16(payload[4:6]))
|
||||
}
|
||||
return fps, batch, trackCount, true
|
||||
}
|
||||
|
||||
func EncodeFrame(connID uint32, msgType byte, payload []byte) []byte {
|
||||
buf := make([]byte, 4+5+len(payload))
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(5+len(payload)))
|
||||
binary.BigEndian.PutUint32(buf[4:8], connID)
|
||||
buf[8] = msgType
|
||||
copy(buf[9:], payload)
|
||||
return buf
|
||||
}
|
||||
|
||||
func LooksLikeRelayFrame(payload []byte) bool {
|
||||
if len(payload) < 9 {
|
||||
return false
|
||||
}
|
||||
frameLen := binary.BigEndian.Uint32(payload[0:4])
|
||||
return frameLen >= 5 && int(frameLen)+4 <= len(payload)
|
||||
}
|
||||
|
||||
func DecodeFrames(data []byte, cb func(connID uint32, msgType byte, payload []byte)) {
|
||||
for len(data) >= 4 {
|
||||
frameLen := int(binary.BigEndian.Uint32(data[0:4]))
|
||||
if frameLen < 5 || 4+frameLen > len(data) {
|
||||
return
|
||||
}
|
||||
connID := binary.BigEndian.Uint32(data[4:8])
|
||||
msgType := data[8]
|
||||
payload := data[9 : 4+frameLen]
|
||||
cb(connID, msgType, payload)
|
||||
data = data[4+frameLen:]
|
||||
}
|
||||
}
|
||||
662
transport/call/tunnel/relay_bridge.go
Normal file
662
transport/call/tunnel/relay_bridge.go
Normal file
@@ -0,0 +1,662 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/transport/call/common"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type udpClient struct {
|
||||
pending chan []byte
|
||||
closed atomic.Bool
|
||||
addr string
|
||||
}
|
||||
|
||||
type RelayBridge struct {
|
||||
tunnelMu sync.RWMutex
|
||||
tunnel DataTunnel
|
||||
conns sync.Map
|
||||
udpClients sync.Map
|
||||
nextID atomic.Uint32
|
||||
logger logger.ContextLogger
|
||||
mode string
|
||||
readBuf int
|
||||
ready chan struct{}
|
||||
once sync.Once
|
||||
closed atomic.Bool
|
||||
dialer N.Dialer
|
||||
|
||||
acceptHandlerMu sync.Mutex
|
||||
acceptHandler func(conn net.Conn, destination string)
|
||||
|
||||
udpAcceptHandlerMu sync.Mutex
|
||||
udpAcceptHandler func(conn net.Conn, destination string)
|
||||
|
||||
onPeerConfigMu sync.Mutex
|
||||
onPeerConfig func(fps, batch, trackCount int)
|
||||
}
|
||||
|
||||
func NewRelayBridge(tunnel DataTunnel, mode string, readBuf int, dialer N.Dialer, logger logger.ContextLogger) *RelayBridge {
|
||||
rb := &RelayBridge{
|
||||
tunnel: tunnel,
|
||||
logger: logger,
|
||||
mode: mode,
|
||||
readBuf: readBuf,
|
||||
dialer: dialer,
|
||||
ready: make(chan struct{}),
|
||||
}
|
||||
tunnel.SetOnData(rb.handleTunnelData)
|
||||
tunnel.SetOnClose(rb.handleTunnelClose)
|
||||
return rb
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) SetAcceptHandler(fn func(conn net.Conn, destination string)) {
|
||||
rb.acceptHandlerMu.Lock()
|
||||
rb.acceptHandler = fn
|
||||
rb.acceptHandlerMu.Unlock()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) SetUDPAcceptHandler(fn func(conn net.Conn, destination string)) {
|
||||
rb.udpAcceptHandlerMu.Lock()
|
||||
rb.udpAcceptHandler = fn
|
||||
rb.udpAcceptHandlerMu.Unlock()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) SetOnPeerConfig(fn func(fps, batch, trackCount int)) {
|
||||
rb.onPeerConfigMu.Lock()
|
||||
rb.onPeerConfig = fn
|
||||
rb.onPeerConfigMu.Unlock()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) DialContext(ctx context.Context, destination string) (net.Conn, error) {
|
||||
if rb.closed.Load() {
|
||||
return nil, fmt.Errorf("relay: bridge already closed")
|
||||
}
|
||||
if M.ParseSocksaddr(destination).IsIPv6() {
|
||||
return nil, fmt.Errorf("relay: network unreachable (ipv6): %s", common.MaskAddr(destination))
|
||||
}
|
||||
select {
|
||||
case <-rb.ready:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
id := rb.nextID.Add(1)
|
||||
tc := newTunnelConn(id, rb)
|
||||
rb.conns.Store(id, tc)
|
||||
rb.logger.Debug(fmt.Sprintf("relay: DIAL %d -> %s", id, common.MaskAddr(destination)))
|
||||
rb.send(id, MsgConnect, []byte(destination))
|
||||
select {
|
||||
case err := <-tc.rdy:
|
||||
if err != nil {
|
||||
rb.conns.Delete(id)
|
||||
return nil, err
|
||||
}
|
||||
return tc, nil
|
||||
case <-ctx.Done():
|
||||
rb.conns.Delete(id)
|
||||
rb.send(id, MsgClose, nil)
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) ListenPacket(ctx context.Context, destination string) (net.Conn, error) {
|
||||
if rb.closed.Load() {
|
||||
return nil, fmt.Errorf("relay: bridge already closed")
|
||||
}
|
||||
if M.ParseSocksaddr(destination).IsIPv6() {
|
||||
return nil, fmt.Errorf("relay: network unreachable (ipv6): %s", common.MaskAddr(destination))
|
||||
}
|
||||
select {
|
||||
case <-rb.ready:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
id := rb.nextID.Add(1)
|
||||
uc := &udpClient{pending: make(chan []byte, 64), addr: destination}
|
||||
rb.udpClients.Store(id, uc)
|
||||
return &tunnelPacketConn{id: id, rb: rb, uc: uc, destStr: destination}, nil
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) Reset() {
|
||||
rb.closeAll()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) Close() {
|
||||
if !rb.closed.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
rb.closeAll()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) MarkReady() {
|
||||
rb.once.Do(func() { close(rb.ready) })
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) currentTunnel() DataTunnel {
|
||||
rb.tunnelMu.RLock()
|
||||
defer rb.tunnelMu.RUnlock()
|
||||
return rb.tunnel
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) SwapTunnel(newTunnel DataTunnel) {
|
||||
rb.tunnelMu.Lock()
|
||||
rb.tunnel = newTunnel
|
||||
rb.tunnelMu.Unlock()
|
||||
newTunnel.SetOnData(rb.handleTunnelData)
|
||||
newTunnel.SetOnClose(rb.handleTunnelClose)
|
||||
rb.closeAll()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) IsClosed() bool {
|
||||
return rb.closed.Load()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) handleTunnelClose() {
|
||||
rb.closeAll()
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) closeAll() {
|
||||
var ids []uint32
|
||||
rb.conns.Range(func(key, value any) bool {
|
||||
if id, ok := key.(uint32); ok {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if c, ok := value.(net.Conn); ok {
|
||||
c.Close()
|
||||
}
|
||||
rb.conns.Delete(key)
|
||||
return true
|
||||
})
|
||||
udpCount := 0
|
||||
rb.udpClients.Range(func(key, value any) bool {
|
||||
udpCount++
|
||||
if uc, ok := value.(*udpClient); ok {
|
||||
uc.closed.Store(true)
|
||||
close(uc.pending)
|
||||
}
|
||||
rb.udpClients.Delete(key)
|
||||
return true
|
||||
})
|
||||
rb.logger.Debug(fmt.Sprintf("relay: closeAll mode=%s tcp=%d udp=%d ids=%v nextID=%d", rb.mode, len(ids), udpCount, ids, rb.nextID.Load()))
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) send(connID uint32, msgType byte, payload []byte) {
|
||||
frame := EncodeFrame(connID, msgType, payload)
|
||||
rb.currentTunnel().SendData(frame)
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) handleTunnelData(data []byte) {
|
||||
DecodeFrames(data, func(connID uint32, msgType byte, payload []byte) {
|
||||
if connID == ControlConnID && msgType == MsgConfig {
|
||||
fps, batch, trackCount, ok := DecodeVP8Config(payload)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if rb.mode == "creator" {
|
||||
rb.logger.Debug(fmt.Sprintf("relay: peer requested vp8 pacing fps=%d batch=%d trackCount=%d", fps, batch, trackCount))
|
||||
rb.currentTunnel().Reconfigure(fps, batch)
|
||||
rb.send(ControlConnID, MsgConfigAck, nil)
|
||||
rb.onPeerConfigMu.Lock()
|
||||
cb := rb.onPeerConfig
|
||||
rb.onPeerConfigMu.Unlock()
|
||||
if cb != nil {
|
||||
cb(fps, batch, trackCount)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if connID == ControlConnID && msgType == MsgConfigAck {
|
||||
return
|
||||
}
|
||||
switch rb.mode {
|
||||
case "joiner":
|
||||
rb.handleJoinerMessage(connID, msgType, payload)
|
||||
case "creator":
|
||||
rb.handleCreatorMessage(connID, msgType, payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) handleJoinerMessage(connID uint32, msgType byte, payload []byte) {
|
||||
if msgType == MsgUDPReply {
|
||||
uval, ok := rb.udpClients.Load(connID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
uc := uval.(*udpClient)
|
||||
if uc.closed.Load() {
|
||||
return
|
||||
}
|
||||
cp := make([]byte, len(payload))
|
||||
copy(cp, payload)
|
||||
select {
|
||||
case uc.pending <- cp:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
val, ok := rb.conns.Load(connID)
|
||||
if !ok {
|
||||
if msgType != MsgClose {
|
||||
rb.logger.Debug(fmt.Sprintf("relay[joiner]: drop msgType=%d for unknown conn %d (payload=%dB)", msgType, connID, len(payload)))
|
||||
}
|
||||
return
|
||||
}
|
||||
tc := val.(*tunnelConn)
|
||||
switch msgType {
|
||||
case MsgConnectOK:
|
||||
select {
|
||||
case tc.rdy <- nil:
|
||||
default:
|
||||
}
|
||||
case MsgConnectErr:
|
||||
select {
|
||||
case tc.rdy <- fmt.Errorf("%s", payload):
|
||||
default:
|
||||
}
|
||||
case MsgData:
|
||||
tc.deliver(payload)
|
||||
case MsgClose:
|
||||
tc.remoteClosed()
|
||||
rb.conns.Delete(connID)
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) handleCreatorMessage(connID uint32, msgType byte, payload []byte) {
|
||||
switch msgType {
|
||||
case MsgConnect:
|
||||
rb.acceptHandlerMu.Lock()
|
||||
handler := rb.acceptHandler
|
||||
rb.acceptHandlerMu.Unlock()
|
||||
if handler != nil {
|
||||
destination := string(payload)
|
||||
tc := newTunnelConn(connID, rb)
|
||||
rb.conns.Store(connID, tc)
|
||||
rb.send(connID, MsgConnectOK, nil)
|
||||
go handler(tc, destination)
|
||||
return
|
||||
}
|
||||
go rb.connectTCP(connID, string(payload))
|
||||
case MsgUDP:
|
||||
payloadCopy := make([]byte, len(payload))
|
||||
copy(payloadCopy, payload)
|
||||
go rb.handleUDP(connID, payloadCopy)
|
||||
case MsgData:
|
||||
val, ok := rb.conns.Load(connID)
|
||||
if !ok {
|
||||
rb.logger.Debug(fmt.Sprintf("relay[creator]: drop MsgData for unknown conn %d (payload=%dB)", connID, len(payload)))
|
||||
rb.send(connID, MsgClose, nil)
|
||||
return
|
||||
}
|
||||
switch c := val.(type) {
|
||||
case *tunnelConn:
|
||||
c.deliver(payload)
|
||||
case net.Conn:
|
||||
if _, err := c.Write(payload); err != nil {
|
||||
rb.logger.Debug(fmt.Sprintf("relay[creator]: write to target %d failed: %s", connID, common.MaskError(err)))
|
||||
}
|
||||
}
|
||||
case MsgClose:
|
||||
found := false
|
||||
if val, ok := rb.conns.LoadAndDelete(connID); ok {
|
||||
found = true
|
||||
switch c := val.(type) {
|
||||
case *tunnelConn:
|
||||
c.remoteClosed()
|
||||
case net.Conn:
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
if uval, ok := rb.udpClients.LoadAndDelete(connID); ok {
|
||||
found = true
|
||||
switch uc := uval.(type) {
|
||||
case *creatorUDPConn:
|
||||
uc.remoteClosed()
|
||||
case net.Conn:
|
||||
uc.Close()
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
rb.logger.Debug(fmt.Sprintf("relay[creator]: drop MsgClose for unknown conn %d", connID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) handleUDP(connID uint32, payload []byte) {
|
||||
if len(payload) < 2 {
|
||||
return
|
||||
}
|
||||
addrLen := int(payload[0])
|
||||
if addrLen == 0 || len(payload) < 1+addrLen {
|
||||
return
|
||||
}
|
||||
if bytes.IndexByte(payload[1:1+addrLen], 0) != -1 {
|
||||
return
|
||||
}
|
||||
addr := string(payload[1 : 1+addrLen])
|
||||
data := payload[1+addrLen:]
|
||||
rb.udpAcceptHandlerMu.Lock()
|
||||
handler := rb.udpAcceptHandler
|
||||
rb.udpAcceptHandlerMu.Unlock()
|
||||
if handler != nil {
|
||||
var cuc *creatorUDPConn
|
||||
if val, ok := rb.udpClients.Load(connID); ok {
|
||||
existing, ok := val.(*creatorUDPConn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cuc = existing
|
||||
} else {
|
||||
created := newCreatorUDPConn(connID, rb, addr)
|
||||
if actual, loaded := rb.udpClients.LoadOrStore(connID, created); loaded {
|
||||
existing, ok := actual.(*creatorUDPConn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cuc = existing
|
||||
} else {
|
||||
cuc = created
|
||||
go handler(cuc, addr)
|
||||
}
|
||||
}
|
||||
cuc.deliver(data)
|
||||
return
|
||||
}
|
||||
var egress net.Conn
|
||||
if val, ok := rb.udpClients.Load(connID); ok {
|
||||
existing, ok := val.(net.Conn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
egress = existing
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
created, err := rb.dialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
|
||||
cancel()
|
||||
if err != nil {
|
||||
rb.logger.Warn(fmt.Sprintf("relay[creator]: UDP %d open %s failed: %v", connID, common.MaskAddr(addr), err))
|
||||
return
|
||||
}
|
||||
if actual, loaded := rb.udpClients.LoadOrStore(connID, created); loaded {
|
||||
created.Close()
|
||||
existing, ok := actual.(net.Conn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
egress = existing
|
||||
} else {
|
||||
egress = created
|
||||
go func(conn net.Conn, id uint32, target string) {
|
||||
defer conn.Close()
|
||||
defer rb.udpClients.Delete(id)
|
||||
defer rb.send(id, MsgClose, nil)
|
||||
buf := make([]byte, common.UDPBufSize)
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rb.send(id, MsgUDPReply, buf[:n])
|
||||
}
|
||||
}(egress, connID, addr)
|
||||
}
|
||||
}
|
||||
egress.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := egress.Write(data); err != nil {
|
||||
rb.logger.Debug(fmt.Sprintf("relay[creator]: UDP %d write %s failed: %v", connID, common.MaskAddr(addr), err))
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RelayBridge) connectTCP(connID uint32, addr string) {
|
||||
rb.logger.Debug(fmt.Sprintf("relay: CONNECT %d -> %s", connID, common.MaskAddr(addr)))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
conn, err := rb.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(addr))
|
||||
cancel()
|
||||
if err != nil {
|
||||
rb.logger.Warn(fmt.Sprintf("relay: CONNECT %d failed: %s", connID, common.MaskError(err)))
|
||||
rb.send(connID, MsgConnectErr, []byte(common.MaskError(err)))
|
||||
return
|
||||
}
|
||||
rb.conns.Store(connID, conn)
|
||||
rb.send(connID, MsgConnectOK, nil)
|
||||
rb.logger.Debug(fmt.Sprintf("relay: CONNECTED %d -> %s", connID, common.MaskAddr(addr)))
|
||||
buf := make([]byte, rb.readBuf)
|
||||
var totalRead int64
|
||||
var reads int
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
rb.send(connID, MsgData, buf[:n])
|
||||
totalRead += int64(n)
|
||||
reads++
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
rb.logger.Warn(fmt.Sprintf("relay: conn %d read error: %s (read %d times, %dB)", connID, common.MaskError(err), reads, totalRead))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
rb.send(connID, MsgClose, nil)
|
||||
rb.conns.Delete(connID)
|
||||
}
|
||||
|
||||
type tunnelAddr struct{}
|
||||
|
||||
func (tunnelAddr) Network() string { return "call" }
|
||||
func (tunnelAddr) String() string { return "call" }
|
||||
|
||||
type tunnelConn struct {
|
||||
id uint32
|
||||
rb *RelayBridge
|
||||
rdy chan error
|
||||
readBuf bytes.Buffer
|
||||
readMu sync.Mutex
|
||||
readCond chan struct{}
|
||||
closed atomic.Bool
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func newTunnelConn(id uint32, rb *RelayBridge) *tunnelConn {
|
||||
return &tunnelConn{
|
||||
id: id,
|
||||
rb: rb,
|
||||
rdy: make(chan error, 1),
|
||||
readCond: make(chan struct{}, 1),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *tunnelConn) Read(b []byte) (int, error) {
|
||||
for {
|
||||
tc.readMu.Lock()
|
||||
if tc.readBuf.Len() > 0 {
|
||||
n, _ := tc.readBuf.Read(b)
|
||||
tc.readMu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
tc.readMu.Unlock()
|
||||
select {
|
||||
case <-tc.closeCh:
|
||||
tc.readMu.Lock()
|
||||
if tc.readBuf.Len() > 0 {
|
||||
n, _ := tc.readBuf.Read(b)
|
||||
tc.readMu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
tc.readMu.Unlock()
|
||||
return 0, io.EOF
|
||||
case <-tc.readCond:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *tunnelConn) Write(b []byte) (int, error) {
|
||||
if tc.closed.Load() {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
tc.rb.send(tc.id, MsgData, b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (tc *tunnelConn) Close() error {
|
||||
if tc.closed.CompareAndSwap(false, true) {
|
||||
close(tc.closeCh)
|
||||
tc.rb.send(tc.id, MsgClose, nil)
|
||||
tc.rb.conns.Delete(tc.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *tunnelConn) LocalAddr() net.Addr { return tunnelAddr{} }
|
||||
func (tc *tunnelConn) RemoteAddr() net.Addr { return tunnelAddr{} }
|
||||
func (tc *tunnelConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (tc *tunnelConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (tc *tunnelConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (tc *tunnelConn) deliver(payload []byte) {
|
||||
tc.readMu.Lock()
|
||||
tc.readBuf.Write(payload)
|
||||
tc.readMu.Unlock()
|
||||
select {
|
||||
case tc.readCond <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *tunnelConn) remoteClosed() {
|
||||
if tc.closed.CompareAndSwap(false, true) {
|
||||
close(tc.closeCh)
|
||||
}
|
||||
}
|
||||
|
||||
type tunnelPacketConn struct {
|
||||
id uint32
|
||||
rb *RelayBridge
|
||||
uc *udpClient
|
||||
destStr string
|
||||
}
|
||||
|
||||
func (pc *tunnelPacketConn) Read(b []byte) (int, error) {
|
||||
data, ok := <-pc.uc.pending
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(b, data)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (pc *tunnelPacketConn) Write(b []byte) (int, error) {
|
||||
if pc.uc.closed.Load() {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
payload := make([]byte, 1+len(pc.destStr)+len(b))
|
||||
payload[0] = byte(len(pc.destStr))
|
||||
copy(payload[1:], pc.destStr)
|
||||
copy(payload[1+len(pc.destStr):], b)
|
||||
pc.rb.send(pc.id, MsgUDP, payload)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (pc *tunnelPacketConn) Close() error {
|
||||
if pc.uc.closed.CompareAndSwap(false, true) {
|
||||
close(pc.uc.pending)
|
||||
pc.rb.udpClients.Delete(pc.id)
|
||||
pc.rb.send(pc.id, MsgClose, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *tunnelPacketConn) LocalAddr() net.Addr { return tunnelAddr{} }
|
||||
func (pc *tunnelPacketConn) RemoteAddr() net.Addr { return tunnelAddr{} }
|
||||
func (pc *tunnelPacketConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (pc *tunnelPacketConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (pc *tunnelPacketConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
type creatorUDPConn struct {
|
||||
id uint32
|
||||
rb *RelayBridge
|
||||
addr string
|
||||
readBuf bytes.Buffer
|
||||
readMu sync.Mutex
|
||||
readCond chan struct{}
|
||||
closed atomic.Bool
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func newCreatorUDPConn(id uint32, rb *RelayBridge, addr string) *creatorUDPConn {
|
||||
return &creatorUDPConn{
|
||||
id: id,
|
||||
rb: rb,
|
||||
addr: addr,
|
||||
readCond: make(chan struct{}, 1),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *creatorUDPConn) Read(b []byte) (int, error) {
|
||||
for {
|
||||
uc.readMu.Lock()
|
||||
if uc.readBuf.Len() > 0 {
|
||||
n, _ := uc.readBuf.Read(b)
|
||||
uc.readMu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
uc.readMu.Unlock()
|
||||
select {
|
||||
case <-uc.closeCh:
|
||||
return 0, io.EOF
|
||||
case <-uc.readCond:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *creatorUDPConn) Write(b []byte) (int, error) {
|
||||
if uc.closed.Load() {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
uc.rb.send(uc.id, MsgUDPReply, b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (uc *creatorUDPConn) Close() error {
|
||||
if uc.closed.CompareAndSwap(false, true) {
|
||||
close(uc.closeCh)
|
||||
uc.rb.send(uc.id, MsgClose, nil)
|
||||
uc.rb.udpClients.Delete(uc.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *creatorUDPConn) LocalAddr() net.Addr { return tunnelAddr{} }
|
||||
func (uc *creatorUDPConn) RemoteAddr() net.Addr { return tunnelAddr{} }
|
||||
func (uc *creatorUDPConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (uc *creatorUDPConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (uc *creatorUDPConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (uc *creatorUDPConn) deliver(payload []byte) {
|
||||
uc.readMu.Lock()
|
||||
uc.readBuf.Write(payload)
|
||||
uc.readMu.Unlock()
|
||||
select {
|
||||
case uc.readCond <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *creatorUDPConn) remoteClosed() {
|
||||
if uc.closed.CompareAndSwap(false, true) {
|
||||
close(uc.closeCh)
|
||||
}
|
||||
}
|
||||
278
transport/call/tunnel/screen_tunnel.go
Normal file
278
transport/call/tunnel/screen_tunnel.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/transport/call/common"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
screenWriterFPS = 24
|
||||
screenWriterBatch = 30
|
||||
screenWriterMaxBytes = 60000
|
||||
screenWriterQueue = 256
|
||||
screenKeepalivePadMax = 48
|
||||
)
|
||||
|
||||
type ScreenWriter struct {
|
||||
obf *TunnelObfuscator
|
||||
logger logger.ContextLogger
|
||||
label string
|
||||
|
||||
sendMu sync.Mutex
|
||||
send func([]byte) error
|
||||
|
||||
stopCh chan struct{}
|
||||
sendQueue chan []byte
|
||||
cfgChan chan struct{}
|
||||
stopOnce sync.Once
|
||||
running atomic.Bool
|
||||
|
||||
cfgMu sync.Mutex
|
||||
fps int
|
||||
batch int
|
||||
sent atomic.Uint64
|
||||
}
|
||||
|
||||
func NewScreenWriter(obf *TunnelObfuscator, label string, logger logger.ContextLogger) *ScreenWriter {
|
||||
return &ScreenWriter{
|
||||
obf: obf,
|
||||
logger: logger,
|
||||
label: label,
|
||||
stopCh: make(chan struct{}),
|
||||
sendQueue: make(chan []byte, screenWriterQueue),
|
||||
cfgChan: make(chan struct{}, 1),
|
||||
fps: screenWriterFPS,
|
||||
batch: screenWriterBatch,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) SetSend(fn func([]byte) error) {
|
||||
w.sendMu.Lock()
|
||||
w.send = fn
|
||||
w.sendMu.Unlock()
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) SendData(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case w.sendQueue <- data:
|
||||
case <-w.stopCh:
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) Reconfigure(fps, batch int) {
|
||||
if fps <= 0 && batch <= 0 {
|
||||
return
|
||||
}
|
||||
w.cfgMu.Lock()
|
||||
changed := false
|
||||
if fps > 0 && w.fps != fps {
|
||||
w.fps = fps
|
||||
changed = true
|
||||
}
|
||||
if batch > 0 && w.batch != batch {
|
||||
w.batch = batch
|
||||
changed = true
|
||||
}
|
||||
w.cfgMu.Unlock()
|
||||
if changed {
|
||||
select {
|
||||
case w.cfgChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) Start() {
|
||||
if !w.running.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
go w.writerLoop()
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) Stop() {
|
||||
if !w.running.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
w.stopOnce.Do(func() { close(w.stopCh) })
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) interval() time.Duration {
|
||||
w.cfgMu.Lock()
|
||||
fps, batch := w.fps, w.batch
|
||||
w.cfgMu.Unlock()
|
||||
frame := time.Second / time.Duration(fps)
|
||||
sample := frame
|
||||
if batch > 1 {
|
||||
sample = frame / time.Duration(batch)
|
||||
}
|
||||
if sample <= 0 {
|
||||
sample = time.Millisecond
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) nextKeepalive(sample time.Duration) (ticks, padLen int) {
|
||||
ticks = int(common.DurationInRange(keepaliveIdleMin, keepaliveIdleMax) / sample)
|
||||
if ticks < 1 {
|
||||
ticks = 1
|
||||
}
|
||||
return ticks, common.IntInRange(0, screenKeepalivePadMax)
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) emit(msg []byte) {
|
||||
if msg == nil || len(msg) > screenWriterMaxBytes {
|
||||
return
|
||||
}
|
||||
w.sendMu.Lock()
|
||||
send := w.send
|
||||
w.sendMu.Unlock()
|
||||
if send == nil {
|
||||
return
|
||||
}
|
||||
if err := send(msg); err != nil {
|
||||
return
|
||||
}
|
||||
n := w.sent.Add(1)
|
||||
if n <= 5 || n%500 == 0 {
|
||||
w.logger.Debug(fmt.Sprintf("[%s] sent frame #%d size=%d", w.label, n, len(msg)))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ScreenWriter) writerLoop() {
|
||||
for {
|
||||
sample := w.interval()
|
||||
keepaliveEvery, keepalivePad := w.nextKeepalive(sample)
|
||||
ticker := time.NewTicker(sample)
|
||||
idle := 0
|
||||
reconfigure := false
|
||||
for !reconfigure {
|
||||
select {
|
||||
case <-w.stopCh:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-w.cfgChan:
|
||||
reconfigure = true
|
||||
case <-ticker.C:
|
||||
select {
|
||||
case data := <-w.sendQueue:
|
||||
w.emit(w.obf.EncodeData(data))
|
||||
idle = 0
|
||||
default:
|
||||
idle++
|
||||
if idle < keepaliveEvery {
|
||||
continue
|
||||
}
|
||||
idle = 0
|
||||
w.emit(w.obf.EncodeKeepalive(keepalivePad))
|
||||
keepaliveEvery, keepalivePad = w.nextKeepalive(sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
ticker.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
type SymmetricScreenTunnel struct {
|
||||
cam *VP8DataTunnel
|
||||
screen *ScreenWriter
|
||||
obf *TunnelObfuscator
|
||||
logger logger.ContextLogger
|
||||
screenReady func() bool
|
||||
|
||||
onDataMu sync.Mutex
|
||||
onData func([]byte)
|
||||
recv atomic.Uint64
|
||||
trackCount atomic.Int32
|
||||
}
|
||||
|
||||
func NewSymmetricScreenTunnel(cam *VP8DataTunnel, screen *ScreenWriter, obf *TunnelObfuscator, screenReady func() bool, logger logger.ContextLogger) *SymmetricScreenTunnel {
|
||||
return &SymmetricScreenTunnel{cam: cam, screen: screen, obf: obf, screenReady: screenReady, logger: logger}
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) SetTrackCount(n int) {
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
if n > 2 {
|
||||
n = 2
|
||||
}
|
||||
old := s.trackCount.Swap(int32(n))
|
||||
if int(old) != n {
|
||||
s.logger.Debug(fmt.Sprintf("screen tunnel track count %d -> %d", old, n))
|
||||
}
|
||||
if n >= 2 {
|
||||
s.screen.Start()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) SendData(data []byte) {
|
||||
var connID uint32
|
||||
if len(data) >= 8 {
|
||||
connID = binary.BigEndian.Uint32(data[4:8])
|
||||
}
|
||||
if connID == ControlConnID {
|
||||
s.cam.SendData(data)
|
||||
return
|
||||
}
|
||||
tc := uint32(s.trackCount.Load())
|
||||
if tc < 1 {
|
||||
tc = 1
|
||||
}
|
||||
if connID%tc == 1 && s.screenUp() {
|
||||
s.screen.SendData(data)
|
||||
return
|
||||
}
|
||||
s.cam.SendData(data)
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) SetOnData(fn func([]byte)) {
|
||||
s.onDataMu.Lock()
|
||||
s.onData = fn
|
||||
s.onDataMu.Unlock()
|
||||
s.cam.SetOnData(fn)
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) SetOnClose(fn func()) { s.cam.SetOnClose(fn) }
|
||||
|
||||
func (s *SymmetricScreenTunnel) Reconfigure(fps, batch int) {
|
||||
s.cam.Reconfigure(fps, batch)
|
||||
s.screen.Reconfigure(fps, batch)
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) Stop() {
|
||||
s.screen.Stop()
|
||||
s.cam.Stop()
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) HandleScreenFrame(frame []byte) {
|
||||
res := s.obf.Decode(frame)
|
||||
n := s.recv.Add(1)
|
||||
if n <= 10 || n%500 == 0 {
|
||||
s.logger.Debug(fmt.Sprintf("screen recv frame #%d in=%d hasFrame=%v keepalive=%v payload=%d", n, len(frame), res.HasFrame, res.Keepalive, len(res.Payload)))
|
||||
}
|
||||
if !res.HasFrame || res.SelfEcho || res.Keepalive || len(res.Payload) == 0 {
|
||||
return
|
||||
}
|
||||
s.onDataMu.Lock()
|
||||
handler := s.onData
|
||||
s.onDataMu.Unlock()
|
||||
if handler != nil {
|
||||
handler(res.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SymmetricScreenTunnel) screenUp() bool {
|
||||
if s.screenReady == nil {
|
||||
return true
|
||||
}
|
||||
return s.screenReady()
|
||||
}
|
||||
319
transport/call/tunnel/vp8tunnel.go
Normal file
319
transport/call/tunnel/vp8tunnel.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
"github.com/sagernet/sing-box/transport/call/common"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVP8FPS = 24
|
||||
defaultVP8Batch = 30
|
||||
keepaliveIdleMin = 60 * time.Millisecond
|
||||
keepaliveIdleMax = 200 * time.Millisecond
|
||||
keepalivePadMax = 176
|
||||
sendQueueDepth = 128
|
||||
|
||||
paceBatchFloorPercent = 80
|
||||
paceDriftMin = 5 * time.Second
|
||||
paceDriftMax = 20 * time.Second
|
||||
)
|
||||
|
||||
type VP8DataTunnel struct {
|
||||
track *webrtc.TrackLocalStaticSample
|
||||
logger logger.ContextLogger
|
||||
obf *TunnelObfuscator
|
||||
stopCh chan struct{}
|
||||
sendQueue chan []byte
|
||||
cfgChan chan struct{}
|
||||
|
||||
stopOnce sync.Once
|
||||
running atomic.Bool
|
||||
|
||||
cfgMu sync.Mutex
|
||||
fps int
|
||||
batch int
|
||||
keepaliveMin time.Duration
|
||||
keepaliveMax time.Duration
|
||||
keepalivePadMax int
|
||||
|
||||
sentFrames atomic.Uint64
|
||||
recvFrames atomic.Uint64
|
||||
keepaliveFrames atomic.Uint64
|
||||
|
||||
OnData func([]byte)
|
||||
OnClose func()
|
||||
OnPeerRestart func()
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) SetOnData(fn func([]byte)) { t.OnData = fn }
|
||||
func (t *VP8DataTunnel) SetOnClose(fn func()) { t.OnClose = fn }
|
||||
func (t *VP8DataTunnel) SetOnPeerRestart(fn func()) { t.OnPeerRestart = fn }
|
||||
|
||||
func NewVP8DataTunnel(track *webrtc.TrackLocalStaticSample, obf *TunnelObfuscator, logger logger.ContextLogger) *VP8DataTunnel {
|
||||
return NewVP8DataTunnelWithQueue(track, obf, logger, sendQueueDepth)
|
||||
}
|
||||
|
||||
func NewVP8DataTunnelWithQueue(track *webrtc.TrackLocalStaticSample, obf *TunnelObfuscator, logger logger.ContextLogger, queueDepth int) *VP8DataTunnel {
|
||||
if queueDepth < sendQueueDepth {
|
||||
queueDepth = sendQueueDepth
|
||||
}
|
||||
return &VP8DataTunnel{
|
||||
track: track,
|
||||
obf: obf,
|
||||
logger: logger,
|
||||
stopCh: make(chan struct{}),
|
||||
sendQueue: make(chan []byte, queueDepth),
|
||||
cfgChan: make(chan struct{}, 1),
|
||||
fps: defaultVP8FPS,
|
||||
batch: defaultVP8Batch,
|
||||
keepaliveMin: keepaliveIdleMin,
|
||||
keepaliveMax: keepaliveIdleMax,
|
||||
keepalivePadMax: keepalivePadMax,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) SetKeepaliveShape(minPeriod, maxPeriod time.Duration, padMax int) {
|
||||
t.cfgMu.Lock()
|
||||
if minPeriod > 0 {
|
||||
t.keepaliveMin = minPeriod
|
||||
}
|
||||
if maxPeriod >= t.keepaliveMin {
|
||||
t.keepaliveMax = maxPeriod
|
||||
}
|
||||
if padMax >= 0 {
|
||||
t.keepalivePadMax = padMax
|
||||
}
|
||||
newMin, newMax, newPad := t.keepaliveMin, t.keepaliveMax, t.keepalivePadMax
|
||||
t.cfgMu.Unlock()
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: keepalive shape min=%s max=%s padMax=%d", newMin, newMax, newPad))
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) nextKeepalive(sampleInterval time.Duration) (ticks, padLen int) {
|
||||
t.cfgMu.Lock()
|
||||
minPeriod, maxPeriod, padMax := t.keepaliveMin, t.keepaliveMax, t.keepalivePadMax
|
||||
t.cfgMu.Unlock()
|
||||
ticks = int(common.DurationInRange(minPeriod, maxPeriod) / sampleInterval)
|
||||
if ticks < 1 {
|
||||
ticks = 1
|
||||
}
|
||||
return ticks, common.IntInRange(0, padMax)
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) Reconfigure(fps, batch int) {
|
||||
if fps <= 0 && batch <= 0 {
|
||||
return
|
||||
}
|
||||
t.cfgMu.Lock()
|
||||
changed := false
|
||||
if fps > 0 && t.fps != fps {
|
||||
t.fps = fps
|
||||
changed = true
|
||||
}
|
||||
if batch > 0 && t.batch != batch {
|
||||
t.batch = batch
|
||||
changed = true
|
||||
}
|
||||
newFPS, newBatch := t.fps, t.batch
|
||||
t.cfgMu.Unlock()
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: reconfigure fps=%d batch=%d", newFPS, newBatch))
|
||||
select {
|
||||
case t.cfgChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) FPS() int {
|
||||
t.cfgMu.Lock()
|
||||
defer t.cfgMu.Unlock()
|
||||
return t.fps
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) Batch() int {
|
||||
t.cfgMu.Lock()
|
||||
defer t.cfgMu.Unlock()
|
||||
return t.batch
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) SendData(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case t.sendQueue <- data:
|
||||
case <-t.stopCh:
|
||||
}
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) TrySendData(data []byte) bool {
|
||||
if len(data) == 0 {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case t.sendQueue <- data:
|
||||
return true
|
||||
case <-t.stopCh:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) Start(fps, batch int) {
|
||||
t.cfgMu.Lock()
|
||||
if fps > 0 {
|
||||
t.fps = fps
|
||||
}
|
||||
if batch > 0 {
|
||||
t.batch = batch
|
||||
}
|
||||
t.cfgMu.Unlock()
|
||||
if !t.running.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
go t.writerLoop()
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) Stop() {
|
||||
if !t.running.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
t.stopOnce.Do(func() { close(t.stopCh) })
|
||||
if t.OnClose != nil {
|
||||
t.OnClose()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) HandleFrame(frame []byte) {
|
||||
res := t.obf.Decode(frame)
|
||||
if !res.HasFrame {
|
||||
return
|
||||
}
|
||||
if res.SelfEcho {
|
||||
return
|
||||
}
|
||||
if res.PeerRestart {
|
||||
t.logger.Info(fmt.Sprintf("vp8tunnel: peer restart detected, new epoch=0x%08x", res.PeerEpoch))
|
||||
if t.OnPeerRestart != nil {
|
||||
t.OnPeerRestart()
|
||||
}
|
||||
}
|
||||
if res.Keepalive || len(res.Payload) == 0 {
|
||||
return
|
||||
}
|
||||
n := t.recvFrames.Add(1)
|
||||
if n <= 5 || n%500 == 0 {
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: recv frame #%d size=%d", n, len(res.Payload)))
|
||||
}
|
||||
if t.OnData != nil {
|
||||
t.OnData(res.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) currentRate() (fps, batch int) {
|
||||
t.cfgMu.Lock()
|
||||
defer t.cfgMu.Unlock()
|
||||
return t.fps, t.batch
|
||||
}
|
||||
|
||||
func sampleIntervalFor(fps, batch int) time.Duration {
|
||||
if fps < 1 {
|
||||
fps = 1
|
||||
}
|
||||
frameInterval := time.Second / time.Duration(fps)
|
||||
interval := frameInterval
|
||||
if batch > 1 {
|
||||
interval = frameInterval / time.Duration(batch)
|
||||
}
|
||||
if interval <= 0 {
|
||||
interval = time.Millisecond
|
||||
}
|
||||
return interval
|
||||
}
|
||||
|
||||
func pacedBatchFor(batch int) int {
|
||||
if batch <= 1 {
|
||||
return batch
|
||||
}
|
||||
floor := batch * paceBatchFloorPercent / 100
|
||||
if floor < 1 {
|
||||
floor = 1
|
||||
}
|
||||
return common.IntInRange(floor, batch)
|
||||
}
|
||||
|
||||
func (t *VP8DataTunnel) writerLoop() {
|
||||
for {
|
||||
fps, batch := t.currentRate()
|
||||
pacedBatch := pacedBatchFor(batch)
|
||||
sampleInterval := sampleIntervalFor(fps, pacedBatch)
|
||||
keepaliveEvery, keepalivePad := t.nextKeepalive(sampleInterval)
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: writer (re)started fps=%d batch=%d pacedBatch=%d sampleInterval=%s keepaliveEvery=%d",
|
||||
fps, batch, pacedBatch, sampleInterval, keepaliveEvery))
|
||||
|
||||
ticker := time.NewTicker(sampleInterval)
|
||||
drift := time.NewTimer(common.DurationInRange(paceDriftMin, paceDriftMax))
|
||||
idleTicks := 0
|
||||
reconfigure := false
|
||||
for !reconfigure {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
ticker.Stop()
|
||||
drift.Stop()
|
||||
return
|
||||
case <-t.cfgChan:
|
||||
reconfigure = true
|
||||
case <-drift.C:
|
||||
pacedBatch = pacedBatchFor(batch)
|
||||
sampleInterval = sampleIntervalFor(fps, pacedBatch)
|
||||
ticker.Reset(sampleInterval)
|
||||
keepaliveEvery, keepalivePad = t.nextKeepalive(sampleInterval)
|
||||
drift.Reset(common.DurationInRange(paceDriftMin, paceDriftMax))
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: pace drift pacedBatch=%d/%d sampleInterval=%s", pacedBatch, batch, sampleInterval))
|
||||
case <-ticker.C:
|
||||
var sample []byte
|
||||
isKeepalive := false
|
||||
select {
|
||||
case data := <-t.sendQueue:
|
||||
sample = t.obf.EncodeData(data)
|
||||
idleTicks = 0
|
||||
default:
|
||||
idleTicks++
|
||||
if idleTicks < keepaliveEvery {
|
||||
continue
|
||||
}
|
||||
idleTicks = 0
|
||||
sample = t.obf.EncodeKeepalive(keepalivePad)
|
||||
keepaliveEvery, keepalivePad = t.nextKeepalive(sampleInterval)
|
||||
isKeepalive = true
|
||||
}
|
||||
if sample == nil {
|
||||
continue
|
||||
}
|
||||
if err := t.track.WriteSample(media.Sample{Data: sample, Duration: sampleInterval}); err != nil {
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: WriteSample error: %v", err))
|
||||
continue
|
||||
}
|
||||
n := t.sentFrames.Add(1)
|
||||
if isKeepalive {
|
||||
t.keepaliveFrames.Add(1)
|
||||
}
|
||||
if n <= 5 || n%500 == 0 {
|
||||
keepalives := t.keepaliveFrames.Load()
|
||||
t.logger.Debug(fmt.Sprintf("vp8tunnel: sent frame #%d size=%d data=%d keepalive=%d", n, len(sample), n-keepalives, keepalives))
|
||||
}
|
||||
}
|
||||
}
|
||||
ticker.Stop()
|
||||
drift.Stop()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user