From e0dd953c5a48bbbc64e4b58050bbed4b5b1db420 Mon Sep 17 00:00:00 2001 From: Alexander Anisimov Date: Sun, 10 May 2026 16:09:02 +0300 Subject: [PATCH] add http ping to mobile --- mobile/mobile.go | 253 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 246 insertions(+), 7 deletions(-) diff --git a/mobile/mobile.go b/mobile/mobile.go index f21f6ab..1ba5811 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -6,7 +6,10 @@ import ( "context" "errors" "fmt" + "io" "log" + "net/http" + "net/url" "sync" "time" @@ -39,16 +42,25 @@ var ( errNotRunning = errors.New("olcRTC is not running") errStoppedBeforeReady = errors.New("olcRTC stopped before becoming ready") errStartTimedOut = errors.New("olcRTC start timed out") + errHTTPPingTimedOut = errors.New("HTTP ping timed out") ) const ( - defaultLink = "direct" - defaultTransport = "vp8channel" - dataTransport = "datachannel" - defaultDNSServer = "1.1.1.1:53" - carrierWBStream = "wbstream" - carrierJazz = "jazz" - roomURLAny = "any" + defaultLink = "direct" + defaultTransport = "vp8channel" + dataTransport = "datachannel" + defaultDNSServer = "1.1.1.1:53" + defaultHTTPPingURL = "https://www.google.com/generate_204" + carrierWBStream = "wbstream" + carrierJazz = "jazz" + roomURLAny = "any" +) + +const ( + httpPingWarmupTimeout = 1500 * time.Millisecond + httpPingSampleTimeout = 1500 * time.Millisecond + httpPingSamples = 3 + httpPingSampleDelay = 80 * time.Millisecond ) //nolint:gochecknoglobals // Mobile bindings expose a singleton runtime controlled by the embedding app. @@ -259,6 +271,233 @@ func Check( } } +// Ping starts an isolated short-lived client, waits until its SOCKS listener is ready, +// performs HTTP requests through that SOCKS tunnel, and returns HTTP latency in milliseconds. +// +// The returned value does not include RTC startup time. It measures only HTTP request latency +// after the tunnel is ready. +func Ping( + carrierName, transportName, roomID, clientID, keyHex string, + socksPort int, + timeoutMillis int, + pingURL string, + vp8FPS int, + vp8BatchSize int, +) (int64, error) { + registerDefaults() + carrierName = normalizeCarrier(carrierName) + transportName = normalizeTransport(transportName) + + if err := validateStartArgs(carrierName, roomID, clientID, keyHex); err != nil { + return 0, err + } + + if timeoutMillis <= 0 { + timeoutMillis = 10000 + } + if pingURL == "" { + pingURL = defaultHTTPPingURL + } + + ctx, cancelFunc := context.WithTimeout( + context.Background(), + time.Duration(timeoutMillis)*time.Millisecond, + ) + defer cancelFunc() + + readyCh := make(chan struct{}) + doneCh := make(chan error, 1) + + var readyOnce sync.Once + + go func() { + doneCh <- runClientWithReady( + ctx, + defaultLink, + transportName, + carrierName, + buildRoomURL(carrierName, roomID), + keyHex, + clientID, + fmt.Sprintf("127.0.0.1:%d", socksPort), + defaultDNSServer, + "", + "", + func() { + readyOnce.Do(func() { + close(readyCh) + }) + }, + 0, + 0, + 0, + "", + "", + 0, + "", + "", + 0, + 0, + clampAtLeastOne(vp8FPS, 120), + clampAtLeastOne(vp8BatchSize, 64), + 0, + 0, + 0, + 0, + ) + }() + + select { + case <-readyCh: + elapsed, err := httpPingThroughSocks( + ctx, + fmt.Sprintf("127.0.0.1:%d", socksPort), + pingURL, + ) + + cancelFunc() + waitForCheckDone(doneCh) + + if err != nil { + return 0, err + } + + return elapsed, nil + + case err := <-doneCh: + if err != nil { + return 0, err + } + + return 0, errStoppedBeforeReady + + case <-ctx.Done(): + cancelFunc() + waitForCheckDone(doneCh) + + return 0, errStartTimedOut + } +} + +func httpPingThroughSocks( + parentCtx context.Context, + socksAddr string, + targetURL string, +) (int64, error) { + if targetURL == "" { + targetURL = defaultHTTPPingURL + } + + if _, err := url.ParseRequestURI(targetURL); err != nil { + return 0, err + } + + proxyURL := &url.URL{ + Scheme: "socks5", + Host: socksAddr, + } + + transport := &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + + DisableKeepAlives: false, + MaxIdleConns: 4, + MaxIdleConnsPerHost: 4, + IdleConnTimeout: 10 * time.Second, + + ForceAttemptHTTP2: false, + TLSHandshakeTimeout: httpPingSampleTimeout, + ResponseHeaderTimeout: httpPingSampleTimeout, + ExpectContinueTimeout: 500 * time.Millisecond, + } + defer transport.CloseIdleConnections() + + client := &http.Client{ + Transport: transport, + Timeout: httpPingSampleTimeout, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + // Warm up the SOCKS/TCP/TLS path. This request is intentionally not included + // in the returned latency. + _, _ = singleHTTPPingRequest( + parentCtx, + client, + targetURL, + httpPingWarmupTimeout, + ) + + var best int64 + var lastErr error + + for i := 0; i < httpPingSamples; i++ { + elapsed, err := singleHTTPPingRequest( + parentCtx, + client, + targetURL, + httpPingSampleTimeout, + ) + if err != nil { + lastErr = err + } else if elapsed > 0 && (best == 0 || elapsed < best) { + best = elapsed + } + + if i < httpPingSamples-1 { + time.Sleep(httpPingSampleDelay) + } + } + + if best > 0 { + return best, nil + } + + if lastErr != nil { + return 0, lastErr + } + + return 0, errHTTPPingTimedOut +} + +func singleHTTPPingRequest( + parentCtx context.Context, + client *http.Client, + targetURL string, + timeout time.Duration, +) (int64, error) { + ctx, cancel := context.WithTimeout(parentCtx, timeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) + if err != nil { + return 0, err + } + + req.Header.Set("User-Agent", "Olcbox-Android") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Cache-Control", "no-cache") + + startedAt := time.Now() + + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + elapsed := time.Since(startedAt).Milliseconds() + + _, _ = io.Copy(io.Discard, resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode > 399 { + return 0, fmt.Errorf("unexpected HTTP status: %d", resp.StatusCode) + } + + return elapsed, nil +} + func startWithConfig( carrierName, transportName, roomID, clientID, keyHex string, socksPort int,