Skip to content

Commit 2cbdf55

Browse files
Adam Fiskclaude
andcommitted
qa-bandit: drop UDP-only protocols + retry probe while URLTest converges
Two related improvements that make the QA path complete end-to-end on TCP outbounds without the bridge needing UDP ASSOCIATE. config/fetcher: when RADIANCE_OUTBOUND_SOCKS_ADDRESS is set, filter hysteria/hysteria2/wireguard/tuic/amnezia out of the request's supportedProtocols list. The bandit then doesn't assign UDP-only tracks the bridge can't relay, and URLTest converges immediately on a working TCP outbound (samizdat / reflex / vmess / vless / trojan / shadowsocks / etc.). Hysteria-class protocols don't work in Russia today anyway, so this is a fine match for the test scope. cmd/qa-bandit: after ConnectVPN, retry the egress probe for up to 30s (every 3s) while URLTest is settling. With UDP outbounds gone this is mostly a safety net — most probes will succeed on attempt 1 — but it also handles transient residential-proxy hiccups (PacketStream occasionally returns "general SOCKS server failure" on the first dial of a fresh session). Default --timeout bumped to 180s to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b9ed811 commit 2cbdf55

2 files changed

Lines changed: 59 additions & 8 deletions

File tree

cmd/qa-bandit/main.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func main() {
6565
// Russia. See cmd/api/maxmind.go LookupCountryASNState.
6666
tz = flag.String("tz", "Europe/Moscow", "TZ env var sent as the request's X-Lantern-Time-Zone")
6767
locale = flag.String("locale", "ru_RU", "locale to pass to the radiance backend (X-Lantern-Locale)")
68-
timeout = flag.Duration("timeout", 90*time.Second, "overall timeout")
68+
timeout = flag.Duration("timeout", 180*time.Second, "overall timeout (covers config fetch + URLTest convergence + probe retries)")
6969
)
7070
flag.Parse()
7171

@@ -157,13 +157,30 @@ func main() {
157157
}
158158
defer be.DisconnectVPN()
159159

160-
fmt.Printf("[qa-bandit] VPN connected; probing %s through %s...\n", *probeURL, *socksIn)
161-
body, dur, err := probeViaSocks(ctx, *socksIn, *probeURL)
162-
if err != nil {
163-
fmt.Printf("[qa-bandit] probe FAILED: %v (%.2fs)\n", err, dur.Seconds())
164-
os.Exit(1)
160+
// URLTest needs a few seconds to converge on a working outbound. UDP
161+
// outbounds (hysteria2/wireguard/tuic) fail immediately through our
162+
// bridge — it only does TCP CONNECT. After URLTest marks them dead,
163+
// AutoSelect prefers TCP-based ones (samizdat/reflex/vmess/etc.).
164+
fmt.Printf("[qa-bandit] VPN connected; waiting up to 30s for URLTest to converge, then probing %s through %s...\n", *probeURL, *socksIn)
165+
var (
166+
body string
167+
dur time.Duration
168+
err2 error
169+
)
170+
deadline := time.Now().Add(30 * time.Second)
171+
for attempt := 1; ; attempt++ {
172+
body, dur, err2 = probeViaSocks(ctx, *socksIn, *probeURL)
173+
if err2 == nil {
174+
fmt.Printf("[qa-bandit] probe OK in %.2fs (attempt %d) — egress IP: %s\n", dur.Seconds(), attempt, body)
175+
return
176+
}
177+
if time.Now().After(deadline) || ctx.Err() != nil {
178+
fmt.Printf("[qa-bandit] probe FAILED after %d attempts: %v\n", attempt, err2)
179+
os.Exit(1)
180+
}
181+
fmt.Printf("[qa-bandit] attempt %d failed (%v) — retrying in 3s...\n", attempt, err2)
182+
time.Sleep(3 * time.Second)
165183
}
166-
fmt.Printf("[qa-bandit] probe OK in %.2fs — egress IP: %s\n", dur.Seconds(), body)
167184
}
168185

169186
func banner(outboundSocks, platform, version, dataDir, socksIn string) {

config/fetcher.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/getlantern/radiance/account"
2626
"github.com/getlantern/radiance/common"
27+
"github.com/getlantern/radiance/common/env"
2728
"github.com/getlantern/radiance/common/settings"
2829
"github.com/getlantern/radiance/log"
2930
"github.com/getlantern/radiance/traces"
@@ -82,7 +83,7 @@ func (f *fetcher) fetchConfig(ctx context.Context, preferred common.PreferredLoc
8283
WGPublicKey: wgPublicKey,
8384
Backend: C.SINGBOX,
8485
Locale: f.locale,
85-
Protocols: protocol.SupportedProtocols(),
86+
Protocols: filterProtocolsForBridge(protocol.SupportedProtocols()),
8687
}
8788
if preferred.Country != "" {
8889
confReq.PreferredLocation = &preferred
@@ -229,3 +230,36 @@ func moduleVersion(modulePath ...string) (string, error) {
229230

230231
return "", fmt.Errorf("module %s not found", modulePath)
231232
}
233+
234+
// udpOnlyProtocols are sing-box outbound protocols whose entry-server
235+
// connection is UDP-only. When radiance's outbound dials are detoured
236+
// through an upstream SOCKS5 (the QA path), our bridge SOCKS5 listener
237+
// only implements TCP CONNECT — UDP ASSOCIATE isn't wired — so these
238+
// outbounds can't be reached and would just clutter URLTest with
239+
// failures. Drop them from the request so the bandit doesn't pick them.
240+
var udpOnlyProtocols = map[string]struct{}{
241+
"hysteria": {},
242+
"hysteria2": {},
243+
"tuic": {},
244+
"wireguard": {},
245+
"amnezia": {}, // wireguard-based; same UDP constraint
246+
}
247+
248+
// filterProtocolsForBridge returns the input slice unchanged unless
249+
// RADIANCE_OUTBOUND_SOCKS_ADDRESS is set, in which case UDP-only
250+
// protocols are filtered out.
251+
func filterProtocolsForBridge(in []string) []string {
252+
if addr, _ := env.Get(env.OutboundSocksAddress); addr == "" {
253+
return in
254+
}
255+
out := in[:0:0]
256+
for _, p := range in {
257+
if _, drop := udpOnlyProtocols[p]; drop {
258+
continue
259+
}
260+
out = append(out, p)
261+
}
262+
slog.Info("RADIANCE_OUTBOUND_SOCKS_ADDRESS set — dropping UDP-only protocols from config request",
263+
"kept", len(out), "dropped", len(in)-len(out))
264+
return out
265+
}

0 commit comments

Comments
 (0)