From 0ce1edb224c0566a675f6a5f48f9a137eac09b0d Mon Sep 17 00:00:00 2001 From: Ilya Yakelzon Date: Fri, 22 May 2026 15:53:51 +0200 Subject: [PATCH] banditcallback: shorten default TTL to 60s for absence-detection heartbeat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lantern-cloud API now interprets absence of arm callbacks as a negative reward signal for the EXP3 bandit. That's the only signal the API can synthesize for censorship blocking: when a proxy is IP-blocked, SNI-blocked, or DPI-dropped at the network layer, the proxy itself sees nothing — the client never reaches it. Only the silence of expected heartbeats reveals the event. For absence to be detected promptly, the emitter must re-fire callbacks on a short cadence while a device is actively using the proxy. Drop the default TTL from 10 min to 60 s; the API-side heartbeat window is 90 s, so a healthy 60s-cadence device always lands before the absence-reaper deadline with ~30 s of jitter slack. Operators who pinned --banditcallbackttl explicitly are unaffected. Co-Authored-By: Claude Opus 4.7 --- banditcallback/banditcallback.go | 12 +++++++++--- http-proxy/main.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/banditcallback/banditcallback.go b/banditcallback/banditcallback.go index 1b2d9bfd..13d0006d 100644 --- a/banditcallback/banditcallback.go +++ b/banditcallback/banditcallback.go @@ -51,11 +51,17 @@ type Emitter struct { // New returns an emitter. token and callbackURL come from the daemon's // INI (banditcallbacktoken / banditcallbackurl). ttl is the per-device -// dedup window — typically matches the API-side ProbeTTLForPollInterval -// at the daemon's expected poll. Zero ttl uses 10m as a sensible default. +// dedup window — also the heartbeat cadence: while a device keeps +// using the proxy, it triggers a callback at most once per ttl. The +// API side uses absence of those heartbeats as its only available +// signal for censorship blocking (the proxy itself can't observe a +// blocked connection because the client never reaches it), so ttl +// must stay short enough that an absence is detected before users +// suffer. Zero ttl uses 60s as a sensible default. Keep this in lock- +// step with bandit.ArmCallbackHeartbeatWindow on the API side. func New(token, callbackURL string, ttl time.Duration) *Emitter { if ttl <= 0 { - ttl = 10 * time.Minute + ttl = 60 * time.Second } return &Emitter{ token: token, diff --git a/http-proxy/main.go b/http-proxy/main.go index 9c82b633..d3baacce 100644 --- a/http-proxy/main.go +++ b/http-proxy/main.go @@ -95,7 +95,7 @@ var ( // the same binary without firing any callbacks. banditCallbackToken = flag.String("banditcallbacktoken", "", "Per-arm bandit callback token plumbed by the provisioner") banditCallbackURL = flag.String("banditcallbackurl", "", "Full URL of the /v1/bandit/callback endpoint") - banditCallbackTTL = flag.Duration("banditcallbackttl", 10*time.Minute, "Per-device dedup window for bandit callback emission") + banditCallbackTTL = flag.Duration("banditcallbackttl", 60*time.Second, "Per-device dedup window and heartbeat cadence for bandit callback emission. The API's absence-reaper expects a callback within ArmCallbackHeartbeatWindow (~90s) of the previous one; values much smaller than that just amplify callback traffic, values larger risk false-positive negative rewards on idle clients.") throttleRefreshInterval = flag.Duration("throttlerefresh", throttle.DefaultRefreshInterval, "Specifies how frequently to refresh throttling configuration from redis. Defaults to 5 minutes.")