diff --git a/go.mod b/go.mod index a243ed6..c63aba3 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/zarazaex69/gr v0.0.0-20260430043628-45b595f4fef0 golang.org/x/crypto v0.50.0 golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b + google.golang.org/genproto v0.0.0-20260209200024-4cfbd4190f57 ) require ( diff --git a/go.sum b/go.sum index 4ac3421..2e168ec 100644 --- a/go.sum +++ b/go.sum @@ -345,6 +345,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20260209200024-4cfbd4190f57 h1:uZSB/r2MjH9IsqpG2vRNSV1Juteix90oHe8oTcLW9tk= +google.golang.org/genproto v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:nGuPfp0lnDJcJD0J47StV0Skgnw3qMSQhjsLKiejq5Y= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= diff --git a/mobile/mobile.go b/mobile/mobile.go index de96987..ad395d1 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -10,11 +10,13 @@ import ( "sync" "time" + "github.com/openlibrecommunity/olcrtc/internal/app/session" "github.com/openlibrecommunity/olcrtc/internal/client" "github.com/openlibrecommunity/olcrtc/internal/logger" "github.com/openlibrecommunity/olcrtc/internal/protect" - _ "golang.org/x/mobile/bind" // ensure gomobile bind is available + _ "golang.org/x/mobile/bind" // ensure gomobile bind is available + _ "google.golang.org/genproto/protobuf/field_mask" // keep gomobile on post-split genproto modules ) // SocketProtector protects sockets from VPN routing on Android. @@ -30,6 +32,7 @@ type LogWriter interface { var ( errAlreadyRunning = errors.New("olcRTC already running") + errCarrierRequired = errors.New("carrier is required") errRoomIDRequired = errors.New("roomID is required") errKeyHexRequired = errors.New("keyHex is required") errNotRunning = errors.New("olcRTC is not running") @@ -37,15 +40,32 @@ var ( errStartTimedOut = errors.New("olcRTC start timed out") ) +const ( + defaultLink = "direct" + defaultTransport = "vp8channel" + dataTransport = "datachannel" + defaultDNSServer = "1.1.1.1:53" +) + //nolint:gochecknoglobals // Mobile bindings expose a singleton runtime controlled by the embedding app. var ( - mu sync.Mutex - cancel context.CancelFunc - done chan struct{} - ready chan struct{} - errRun error + mu sync.Mutex + defaults mobileConfig + defaultsSet sync.Once + cancel context.CancelFunc + done chan struct{} + ready chan struct{} + errRun error ) +type mobileConfig struct { + link string + transport string + dnsServer string + vp8FPS int + vp8BatchSize int +} + // SetProtector sets the Android VPN socket protector. // Must be called before Start. func SetProtector(p SocketProtector) { @@ -65,6 +85,46 @@ func SetLogWriter(w LogWriter) { } } +// SetProviders registers built-in carriers, links, and transports. +func SetProviders() { + registerDefaults() +} + +// SetTransport selects the transport used by Start. +// Supported values: vp8channel and datachannel. +func SetTransport(transport string) { + mu.Lock() + defer mu.Unlock() + ensureDefaultConfigLocked() + defaults.transport = normalizeTransport(transport) +} + +// SetLink selects the link used by Start. +// Supported value today: direct. +func SetLink(link string) { + mu.Lock() + defer mu.Unlock() + ensureDefaultConfigLocked() + defaults.link = link +} + +// SetDNS selects the DNS server used by the tunnel. +func SetDNS(dnsServer string) { + mu.Lock() + defer mu.Unlock() + ensureDefaultConfigLocked() + defaults.dnsServer = dnsServer +} + +// SetVP8Options configures vp8channel. +func SetVP8Options(fps, batchSize int) { + mu.Lock() + defer mu.Unlock() + ensureDefaultConfigLocked() + defaults.vp8FPS = clamp(fps, 1, 120) + defaults.vp8BatchSize = clamp(batchSize, 1, 32) +} + // SetDebug enables or disables verbose logging. func SetDebug(enabled bool) { logger.SetVerbose(enabled) @@ -77,24 +137,62 @@ func SetDebug(enabled bool) { } // Start launches the olcRTC client in background. -// roomID: Telemost room ID (e.g. "xxx-xxx-xxx") +// carrierName: carrier/provider name ("telemost", "jazz", "wbstream", "wb_stream") +// roomID: carrier-specific room ID // keyHex: 64-char hex encryption key // socksPort: local SOCKS5 proxy port (e.g. 10808) // socksUser/socksPass: SOCKS5 credentials (empty = no auth). -func Start(roomID, keyHex string, socksPort int, socksUser, socksPass string) error { +func Start(carrierName, roomID, keyHex string, socksPort int, socksUser, socksPass string) error { + mu.Lock() + ensureDefaultConfigLocked() + cfg := defaults + mu.Unlock() + + return startWithConfig(carrierName, cfg.transport, roomID, keyHex, socksPort, socksUser, socksPass, cfg) +} + +// StartWithTransport launches the client with an explicit transport for this start. +func StartWithTransport( + carrierName, transportName, roomID, keyHex string, + socksPort int, + socksUser, socksPass string, +) error { + mu.Lock() + ensureDefaultConfigLocked() + cfg := defaults + cfg.transport = transportName + mu.Unlock() + + return startWithConfig(carrierName, transportName, roomID, keyHex, socksPort, socksUser, socksPass, cfg) +} + +func startWithConfig( + carrierName, transportName, roomID, keyHex string, + socksPort int, + socksUser, socksPass string, + cfg mobileConfig, +) error { mu.Lock() defer mu.Unlock() + registerDefaults() + carrierName = normalizeCarrier(carrierName) + if transportName != "" { + cfg.transport = normalizeTransport(transportName) + } + switch { case cancel != nil: return errAlreadyRunning - case roomID == "": + case carrierName == "": + return errCarrierRequired + case roomID == "" && carrierName != "jazz": return errRoomIDRequired case keyHex == "": return errKeyHexRequired } - roomURL := "https://telemost.yandex.ru/j/" + roomID + roomURL := buildRoomURL(carrierName, roomID) ctx, cancelFunc := context.WithCancel(context.Background()) cancel = cancelFunc @@ -109,13 +207,13 @@ func Start(roomID, keyHex string, socksPort int, socksUser, socksPass string) er err := client.RunWithReady( ctx, - "direct", - "datachannel", - "telemost", + cfg.link, + cfg.transport, + carrierName, roomURL, keyHex, fmt.Sprintf("127.0.0.1:%d", socksPort), - "", + cfg.dnsServer, socksUser, socksPass, func() { @@ -123,7 +221,18 @@ func Start(roomID, keyHex string, socksPort int, socksUser, socksPass string) er close(localReady) }) }, - 0, 0, 0, "", "", 0, "", "", 0, 0, 0, 0, + 0, + 0, + 0, + "", + "", + 0, + "", + "", + 0, + 0, + cfg.vp8FPS, + cfg.vp8BatchSize, ) mu.Lock() @@ -136,7 +245,7 @@ func Start(roomID, keyHex string, socksPort int, socksUser, socksPass string) er return nil } -// WaitReady blocks until the Telemost peers are connected and the local SOCKS5 listener is ready. +// WaitReady blocks until the selected transport is connected and the local SOCKS5 listener is ready. // //nolint:cyclop // The control flow is intentionally linear so mobile callers can observe each startup state clearly. func WaitReady(timeoutMillis int) error { @@ -214,7 +323,67 @@ func IsRunning() bool { return cancel != nil } -// logBridge adapts LogWriter to io.Writer for log package. +func registerDefaults() { + session.RegisterDefaults() +} + +func ensureDefaultConfigLocked() { + defaultsSet.Do(func() { + defaults = mobileConfig{ + link: defaultLink, + transport: defaultTransport, + dnsServer: defaultDNSServer, + vp8FPS: 60, + vp8BatchSize: 8, + } + }) +} + +func normalizeTransport(value string) string { + switch value { + case dataTransport, "data", "dc": + return dataTransport + case defaultTransport, "vp8": + return defaultTransport + default: + return defaultTransport + } +} + +func normalizeCarrier(carrierName string) string { + if carrierName == "wb_stream" { + return "wbstream" + } + return carrierName +} + +func buildRoomURL(carrierName, roomID string) string { + switch carrierName { + case "telemost": + return "https://telemost.yandex.ru/j/" + roomID + case "jazz": + if roomID == "" { + return "any" + } + return roomID + case "wbstream": + return roomID + default: + return roomID + } +} + +func clamp(value, minValue, maxValue int) int { + if value < minValue { + return minValue + } + if value > maxValue { + return maxValue + } + return value +} + +// logBridge adapts LogWriter to io.Writer. type logBridge struct { w LogWriter }