mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 22:35:16 +03:00
218 lines
6.1 KiB
Go
218 lines
6.1 KiB
Go
package wbstream
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
"github.com/sagernet/sing-box/transport/call/tunnel"
|
|
"github.com/sagernet/sing/common/logger"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
const (
|
|
reconnectInitialDelay = time.Second
|
|
reconnectMaxDelay = 16 * time.Second
|
|
)
|
|
|
|
type WBStreamJoiner struct {
|
|
logger logger.ContextLogger
|
|
OnConnected func(tunnel.DataTunnel)
|
|
dialer N.Dialer
|
|
dnsRouter adapter.DNSRouter
|
|
PCConfig common.PeerConnectionConfigurer
|
|
|
|
mu sync.Mutex
|
|
session *Session
|
|
closed bool
|
|
stopCh chan struct{}
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
func NewWBStreamJoiner(logger logger.ContextLogger, dialer N.Dialer, dnsRouter adapter.DNSRouter, pcConfig common.PeerConnectionConfigurer) *WBStreamJoiner {
|
|
return &WBStreamJoiner{
|
|
logger: logger,
|
|
dialer: dialer,
|
|
dnsRouter: dnsRouter,
|
|
PCConfig: pcConfig,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (j *WBStreamJoiner) RunWithParams(jsonParams string) {
|
|
var params struct {
|
|
RoomID string `json:"roomId"`
|
|
DisplayName string `json:"displayName"`
|
|
TunnelMode string `json:"tunnelMode"`
|
|
VP8FPS int `json:"vp8Fps"`
|
|
VP8Batch int `json:"vp8Batch"`
|
|
DualTrack bool `json:"dualTrack"`
|
|
Reliable *bool `json:"reliable"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonParams), ¶ms); err != nil {
|
|
j.logger.Error(fmt.Sprintf("wbstream-joiner: failed to parse params: %v", err))
|
|
return
|
|
}
|
|
if params.RoomID == "" {
|
|
j.logger.Error("wbstream-joiner: missing roomId")
|
|
return
|
|
}
|
|
if params.DisplayName == "" {
|
|
params.DisplayName = "Joiner"
|
|
}
|
|
reliable := params.Reliable != nil && *params.Reliable
|
|
httpClient := j.makeHTTPClient()
|
|
j.logger.Info(fmt.Sprintf("wbstream-joiner: room=%s name=%s vp8Fps=%d vp8Batch=%d dualTrack=%v", params.RoomID, params.DisplayName, params.VP8FPS, params.VP8Batch, params.DualTrack))
|
|
obf, err := tunnel.NewTunnelObfuscator(tunnel.DeriveSecretFromJoinLink(params.RoomID))
|
|
if err != nil {
|
|
j.logger.Error(fmt.Sprintf("wbstream-joiner: obfuscator init failed: %v", err))
|
|
return
|
|
}
|
|
j.logger.Debug(fmt.Sprintf("wbstream-joiner: obf key-source=%q localEpoch=0x%08x", params.RoomID, obf.LocalEpoch()))
|
|
var settingEngine *webrtc.SettingEngine
|
|
if j.PCConfig != nil {
|
|
se := webrtc.SettingEngine{}
|
|
j.PCConfig.ConfigureSettingEngine(&se)
|
|
settingEngine = &se
|
|
}
|
|
var attempt atomic.Int32
|
|
j.logger.Info("wbstream-joiner: connecting")
|
|
if err := j.runOnce(httpClient, params.RoomID, params.DisplayName, params.TunnelMode, obf, settingEngine, params.VP8FPS, params.VP8Batch, params.DualTrack, reliable, &attempt); err != nil {
|
|
j.logger.Error(fmt.Sprintf("wbstream-joiner: %v", err))
|
|
return
|
|
}
|
|
for {
|
|
if j.isClosed() {
|
|
j.logger.Info("wbstream-joiner: stopped")
|
|
return
|
|
}
|
|
j.logger.Info("wbstream-joiner: tunnel lost")
|
|
if !j.waitBeforeRetry(int(attempt.Load())) {
|
|
return
|
|
}
|
|
attempt.Add(1)
|
|
if j.isClosed() {
|
|
return
|
|
}
|
|
j.logger.Info(fmt.Sprintf("wbstream-joiner: reconnect attempt #%d", attempt.Load()))
|
|
if err := j.runOnce(httpClient, params.RoomID, params.DisplayName, params.TunnelMode, obf, settingEngine, params.VP8FPS, params.VP8Batch, params.DualTrack, reliable, &attempt); err != nil {
|
|
j.logger.Warn(fmt.Sprintf("wbstream-joiner: %v, will retry", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (j *WBStreamJoiner) MarkConfigAcked() {
|
|
j.mu.Lock()
|
|
sess := j.session
|
|
j.mu.Unlock()
|
|
if sess != nil {
|
|
sess.MarkConfigAcked()
|
|
}
|
|
}
|
|
|
|
func (j *WBStreamJoiner) Close() {
|
|
j.stopOnce.Do(func() { close(j.stopCh) })
|
|
j.mu.Lock()
|
|
j.closed = true
|
|
sess := j.session
|
|
j.session = nil
|
|
j.mu.Unlock()
|
|
if sess != nil {
|
|
sess.Close()
|
|
}
|
|
}
|
|
|
|
func (j *WBStreamJoiner) runOnce(httpClient *http.Client, roomID, displayName, tunnelMode string, obf *tunnel.TunnelObfuscator, settingEngine *webrtc.SettingEngine, vp8FPS, vp8Batch int, dualTrack, reliable bool, attempt *atomic.Int32) error {
|
|
_, roomToken, _, serverURL, authErr := AuthAndGetToken(httpClient, roomID, displayName)
|
|
if authErr != nil {
|
|
return fmt.Errorf("auth: %w", authErr)
|
|
}
|
|
j.logger.Debug(fmt.Sprintf("wbstream-joiner: server=%s", serverURL))
|
|
sess := NewSession(SessionConfig{
|
|
RoomToken: roomToken,
|
|
ServerURL: serverURL,
|
|
DisplayName: displayName,
|
|
TunnelMode: tunnelMode,
|
|
Obfuscator: obf,
|
|
Logger: j.logger,
|
|
SettingEngine: settingEngine,
|
|
Dialer: j.dialer,
|
|
DNSRouter: j.dnsRouter,
|
|
VP8FPS: vp8FPS,
|
|
VP8Batch: vp8Batch,
|
|
ScreenShare: dualTrack,
|
|
IsJoiner: true,
|
|
Reliable: reliable,
|
|
})
|
|
sess.OnConnected = func(tun tunnel.DataTunnel) {
|
|
attempt.Store(0)
|
|
j.logger.Info("wbstream-joiner: === TUNNEL CONNECTED ===")
|
|
if j.OnConnected != nil {
|
|
j.OnConnected(tun)
|
|
}
|
|
}
|
|
j.mu.Lock()
|
|
if j.closed {
|
|
j.mu.Unlock()
|
|
sess.Close()
|
|
return nil
|
|
}
|
|
j.session = sess
|
|
j.mu.Unlock()
|
|
if err := sess.Start(); err != nil {
|
|
j.clearSession(sess)
|
|
return fmt.Errorf("session: %w", err)
|
|
}
|
|
<-sess.Done()
|
|
sess.Close()
|
|
j.clearSession(sess)
|
|
return nil
|
|
}
|
|
|
|
func (j *WBStreamJoiner) waitBeforeRetry(attempt int) bool {
|
|
delay := common.BackoffWithJitter(attempt, reconnectInitialDelay, reconnectMaxDelay)
|
|
j.logger.Debug(fmt.Sprintf("wbstream-joiner: waiting %s before reconnect", delay))
|
|
timer := time.NewTimer(delay)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-timer.C:
|
|
return !j.isClosed()
|
|
case <-j.stopCh:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (j *WBStreamJoiner) clearSession(sess *Session) {
|
|
j.mu.Lock()
|
|
if j.session == sess {
|
|
j.session = nil
|
|
}
|
|
j.mu.Unlock()
|
|
}
|
|
|
|
func (j *WBStreamJoiner) isClosed() bool {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
return j.closed
|
|
}
|
|
|
|
func (j *WBStreamJoiner) makeDialContext() func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return j.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
}
|
|
}
|
|
|
|
func (j *WBStreamJoiner) makeHTTPClient() *http.Client {
|
|
transport := &http.Transport{DialContext: j.makeDialContext()}
|
|
return &http.Client{Timeout: 60 * time.Second, Transport: transport}
|
|
}
|