feat: expose mobile liveness options

This commit is contained in:
cyber-debug
2026-05-16 00:30:04 +03:00
parent b0fc3bd0f1
commit d16cd0686a
2 changed files with 135 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ import (
"github.com/openlibrecommunity/olcrtc/internal/app/session"
"github.com/openlibrecommunity/olcrtc/internal/client"
"github.com/openlibrecommunity/olcrtc/internal/control"
"github.com/openlibrecommunity/olcrtc/internal/logger"
"github.com/openlibrecommunity/olcrtc/internal/protect"
@@ -65,23 +66,26 @@ const (
)
var (
mu sync.Mutex //nolint:gochecknoglobals // package-level state intentional
defaults mobileConfig //nolint:gochecknoglobals // package-level state intentional
defaultsSet sync.Once //nolint:gochecknoglobals // package-level state intentional
registerSet sync.Once //nolint:gochecknoglobals // package-level state intentional
mu sync.Mutex //nolint:gochecknoglobals // package-level state intentional
defaults mobileConfig //nolint:gochecknoglobals // package-level state intentional
defaultsSet sync.Once //nolint:gochecknoglobals // package-level state intentional
registerSet sync.Once //nolint:gochecknoglobals // package-level state intentional
runClientWithReady = client.RunWithReady //nolint:gochecknoglobals // package-level state intentional
cancel context.CancelFunc //nolint:gochecknoglobals // package-level state intentional
done chan struct{} //nolint:gochecknoglobals // package-level state intentional
ready chan struct{} //nolint:gochecknoglobals // package-level state intentional
cancel context.CancelFunc //nolint:gochecknoglobals // package-level state intentional
done chan struct{} //nolint:gochecknoglobals // package-level state intentional
ready chan struct{} //nolint:gochecknoglobals // package-level state intentional
errRun error
)
type mobileConfig struct {
link string
transport string
dnsServer string
vp8FPS int
vp8BatchSize int
link string
transport string
dnsServer string
vp8FPS int
vp8BatchSize int
livenessInterval time.Duration
livenessTimeout time.Duration
livenessFailures int
}
// SetProtector sets the Android VPN socket protector.
@@ -143,6 +147,21 @@ func SetVP8Options(fps, batchSize int) {
defaults.vp8BatchSize = clampAtLeastOne(batchSize, 64)
}
// SetLivenessOptions configures control-stream ping/pong checks.
// Values <= 0 reset that field to its default. Durations are milliseconds.
func SetLivenessOptions(intervalMillis, timeoutMillis, failures int) {
mu.Lock()
defer mu.Unlock()
ensureDefaultConfigLocked()
defaults.livenessInterval = durationFromMillisOrDefault(intervalMillis, control.DefaultInterval)
defaults.livenessTimeout = durationFromMillisOrDefault(timeoutMillis, control.DefaultTimeout)
if failures <= 0 {
defaults.livenessFailures = control.DefaultFailures
return
}
defaults.livenessFailures = failures
}
// SetDebug enables or disables verbose logging.
func SetDebug(enabled bool) {
logger.SetVerbose(enabled)
@@ -195,6 +214,11 @@ func Check(
vp8BatchSize int,
) (int64, error) {
registerDefaults()
mu.Lock()
ensureDefaultConfigLocked()
cfg := defaults
mu.Unlock()
carrierName = normalizeCarrier(carrierName)
transportName = normalizeTransport(transportName)
if err := validateStartArgs(carrierName, roomID, clientID, keyHex); err != nil {
@@ -227,6 +251,7 @@ func Check(
DNSServer: defaultDNSServer,
VP8FPS: clampAtLeastOne(vp8FPS, 120),
VP8BatchSize: clampAtLeastOne(vp8BatchSize, 64),
Liveness: livenessConfig(cfg),
},
func() {
readyOnce.Do(func() {
@@ -271,6 +296,11 @@ func Ping(
vp8BatchSize int,
) (int64, error) {
registerDefaults()
mu.Lock()
ensureDefaultConfigLocked()
cfg := defaults
mu.Unlock()
carrierName = normalizeCarrier(carrierName)
transportName = normalizeTransport(transportName)
@@ -310,6 +340,7 @@ func Ping(
DNSServer: defaultDNSServer,
VP8FPS: clampAtLeastOne(vp8FPS, 120),
VP8BatchSize: clampAtLeastOne(vp8BatchSize, 64),
Liveness: livenessConfig(cfg),
},
func() {
readyOnce.Do(func() {
@@ -557,6 +588,7 @@ func startWithConfig(
SOCKSPass: socksPass,
VP8FPS: cfg.vp8FPS,
VP8BatchSize: cfg.vp8BatchSize,
Liveness: livenessConfig(cfg),
},
func() {
readyOnce.Do(func() {
@@ -576,6 +608,7 @@ func startWithConfig(
}
// WaitReady blocks until the selected transport is connected and the local SOCKS5 listener is ready.
//
//nolint:cyclop // straightforward state-machine waits with multiple terminal conditions
func WaitReady(timeoutMillis int) error {
mu.Lock()
@@ -666,15 +699,38 @@ func waitForCheckDone(doneCh <-chan error) {
func ensureDefaultConfigLocked() {
defaultsSet.Do(func() {
defaults = mobileConfig{
link: defaultLink,
transport: defaultTransport,
dnsServer: defaultDNSServer,
vp8FPS: 60,
vp8BatchSize: 8,
link: defaultLink,
transport: defaultTransport,
dnsServer: defaultDNSServer,
vp8FPS: 60,
vp8BatchSize: 8,
livenessInterval: control.DefaultInterval,
livenessTimeout: control.DefaultTimeout,
livenessFailures: control.DefaultFailures,
}
})
}
func livenessConfig(cfg mobileConfig) control.Config {
interval := cfg.livenessInterval
if interval <= 0 {
interval = control.DefaultInterval
}
timeout := cfg.livenessTimeout
if timeout <= 0 {
timeout = control.DefaultTimeout
}
failures := cfg.livenessFailures
if failures <= 0 {
failures = control.DefaultFailures
}
return control.Config{
Interval: interval,
Timeout: timeout,
Failures: failures,
}
}
func normalizeTransport(value string) string {
switch value {
case dataTransport, "data", "dc":
@@ -734,6 +790,17 @@ func clampAtLeastOne(value, maxValue int) int {
return value
}
func durationFromMillisOrDefault(value int, def time.Duration) time.Duration {
if value <= 0 {
return def
}
d := time.Duration(value) * time.Millisecond
if d <= 0 {
return def
}
return d
}
// logBridge adapts LogWriter to io.Writer.
type logBridge struct {
w LogWriter

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/openlibrecommunity/olcrtc/internal/client"
"github.com/openlibrecommunity/olcrtc/internal/control"
"github.com/openlibrecommunity/olcrtc/internal/logger"
"github.com/openlibrecommunity/olcrtc/internal/protect"
)
@@ -83,12 +84,15 @@ func TestDefaultsAndSetters(t *testing.T) {
SetLink("direct")
SetDNS("9.9.9.9:53")
SetVP8Options(-1, 999)
SetLivenessOptions(2500, 750, -1)
mu.Lock()
got := defaults
mu.Unlock()
if got.transport != dataTransport || got.link != defaultLink || got.dnsServer != "9.9.9.9:53" ||
got.vp8FPS != 1 || got.vp8BatchSize != 64 {
got.vp8FPS != 1 || got.vp8BatchSize != 64 ||
got.livenessInterval != 2500*time.Millisecond || got.livenessTimeout != 750*time.Millisecond ||
got.livenessFailures != control.DefaultFailures {
t.Fatalf("defaults = %+v", got)
}
@@ -168,15 +172,19 @@ func TestStartWithInjectedRunnerLifecycle(t *testing.T) {
t.Cleanup(func() {
resetMobileGlobals(t)
})
SetLivenessOptions(2500, 750, 4)
runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error {
if cfg.Link != defaultLink || cfg.Transport != dataTransport || cfg.Carrier != carrierJazz ||
cfg.RoomURL != "any" || cfg.DeviceID != "client" || cfg.LocalAddr != "127.0.0.1:1080" ||
cfg.DNSServer != defaultDNSServer || cfg.VP8FPS != 60 || cfg.VP8BatchSize != 8 {
cfg.DNSServer != defaultDNSServer || cfg.VP8FPS != 60 || cfg.VP8BatchSize != 8 ||
cfg.Liveness.Interval != 2500*time.Millisecond ||
cfg.Liveness.Timeout != 750*time.Millisecond ||
cfg.Liveness.Failures != 4 {
t.Fatalf(
"RunWithReady args mismatch: link=%q transport=%q carrier=%q room=%q client=%q local=%q dns=%q vp8=%d/%d",
"RunWithReady args mismatch: link=%q transport=%q carrier=%q room=%q client=%q local=%q dns=%q vp8=%d/%d liveness=%+v",
cfg.Link, cfg.Transport, cfg.Carrier, cfg.RoomURL, cfg.DeviceID,
cfg.LocalAddr, cfg.DNSServer, cfg.VP8FPS, cfg.VP8BatchSize,
cfg.LocalAddr, cfg.DNSServer, cfg.VP8FPS, cfg.VP8BatchSize, cfg.Liveness,
)
}
onReady()
@@ -208,9 +216,12 @@ func TestStartUsesDefaultsAndCheckWithInjectedRunner(t *testing.T) {
runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error {
if cfg.Transport != defaultTransport || cfg.RoomURL != "https://telemost.yandex.ru/j/room" ||
cfg.LocalAddr != "127.0.0.1:1081" || cfg.SOCKSUser != "u" || cfg.SOCKSPass != "p" {
t.Fatalf("Start args mismatch: transport=%q room=%q local=%q user/pass=%q/%q",
cfg.Transport, cfg.RoomURL, cfg.LocalAddr, cfg.SOCKSUser, cfg.SOCKSPass)
cfg.LocalAddr != "127.0.0.1:1081" || cfg.SOCKSUser != "u" || cfg.SOCKSPass != "p" ||
cfg.Liveness.Interval != control.DefaultInterval ||
cfg.Liveness.Timeout != control.DefaultTimeout ||
cfg.Liveness.Failures != control.DefaultFailures {
t.Fatalf("Start args mismatch: transport=%q room=%q local=%q user/pass=%q/%q liveness=%+v",
cfg.Transport, cfg.RoomURL, cfg.LocalAddr, cfg.SOCKSUser, cfg.SOCKSPass, cfg.Liveness)
}
onReady()
<-ctx.Done()
@@ -225,9 +236,14 @@ func TestStartUsesDefaultsAndCheckWithInjectedRunner(t *testing.T) {
}
Stop()
SetLivenessOptions(3000, 1000, 5)
runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error {
if cfg.Transport != dataTransport || cfg.VP8FPS != 1 || cfg.VP8BatchSize != 64 {
t.Fatalf("Check args mismatch: transport=%q vp8=%d/%d", cfg.Transport, cfg.VP8FPS, cfg.VP8BatchSize)
if cfg.Transport != dataTransport || cfg.VP8FPS != 1 || cfg.VP8BatchSize != 64 ||
cfg.Liveness.Interval != 3000*time.Millisecond ||
cfg.Liveness.Timeout != time.Second ||
cfg.Liveness.Failures != 5 {
t.Fatalf("Check args mismatch: transport=%q vp8=%d/%d liveness=%+v",
cfg.Transport, cfg.VP8FPS, cfg.VP8BatchSize, cfg.Liveness)
}
onReady()
<-ctx.Done()
@@ -242,6 +258,32 @@ func TestStartUsesDefaultsAndCheckWithInjectedRunner(t *testing.T) {
}
}
func TestPingPassesLiveness(t *testing.T) {
resetMobileGlobals(t)
t.Cleanup(func() {
resetMobileGlobals(t)
})
SetLivenessOptions(4000, 1500, 6)
seen := make(chan control.Config, 1)
runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error {
seen <- cfg.Liveness
onReady()
<-ctx.Done()
return nil
}
_, _ = Ping("jazz", "dc", "", "client", "key", 1085, 100, "http://127.0.0.1/", 30, 1)
select {
case got := <-seen:
if got.Interval != 4000*time.Millisecond || got.Timeout != 1500*time.Millisecond || got.Failures != 6 {
t.Fatalf("Ping liveness = %+v", got)
}
default:
t.Fatal("Ping did not start client")
}
}
func TestCheckTimeoutAndRunError(t *testing.T) {
resetMobileGlobals(t)
t.Cleanup(func() {