Files
sing-box-extended/transport/call/tunnel/screen_tunnel.go

279 lines
5.6 KiB
Go

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()
}