Skip to content

Commit c01a9c9

Browse files
committed
fix(node): force HTTP/1.1 for the sync long-poll (stop GOAWAY flapping)
The node reaches the panel through :443 — Xray (VLESS-Vision) with the panel behind its fallback. The sync is a 30-60s HELD request; over HTTP/2 that path recycles the connection with a GOAWAY before the hold completes, so nearly every poll ended in "unexpected EOF" (~50/hour). Each was treated as a failure, pinning the agent at the 60s backoff ceiling (slow-polling, not long-polling), and every check-in that slipped past the 2-minute window flapped a "node not responding" alert. Forcing HTTP/1.1 on the sync client (empty TLSNextProto + no ForceAttempt HTTP2) carries the held request through the fallback intact. Node-side only; the master's :443 offers both h2 and h1.
1 parent 476152b commit c01a9c9

2 files changed

Lines changed: 50 additions & 4 deletions

File tree

internal/nodeagent/agent.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,16 +350,33 @@ func geoStale(dir string, maxAge time.Duration) bool {
350350
return false
351351
}
352352

353+
// syncTransport builds the transport for the node's long-poll. It forces HTTP/1.1.
354+
//
355+
// The sync is a 30-60s HELD request, reached through the panel's :443 — which is Xray
356+
// (VLESS-Vision), with the panel served behind its fallback. Over HTTP/2 that path
357+
// recycles the connection with a GOAWAY before the hold completes, so every poll ends
358+
// in "unexpected EOF": the node then treats each as a failure, backs off to the 60s
359+
// ceiling (slow-polling instead of long-polling), and every missed check-in past the
360+
// 2-minute window flaps a "node not responding" alert. A plain HTTP/1.1 held request
361+
// carries through the fallback intact. A non-nil (empty) TLSNextProto disables the
362+
// automatic h2 upgrade; ForceAttemptHTTP2=false keeps it off.
363+
func syncTransport(insecure bool) *http.Transport {
364+
tr := http.DefaultTransport.(*http.Transport).Clone()
365+
tr.ForceAttemptHTTP2 = false
366+
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
367+
if insecure {
368+
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // opt-in via --insecure
369+
}
370+
return tr
371+
}
372+
353373
func newAgent(dataDir string, ident *Identity) (*Agent, error) {
354374
if err := os.MkdirAll(dataDir, 0o700); err != nil {
355375
return nil, err
356376
}
357377
bin := resolveNodeXrayBin(filepath.Join(dataDir, "bin"))
358378
sup := xray.NewSupervisor(bin, filepath.Join(dataDir, "xray", "config.json"), filepath.Join(dataDir, "geo"))
359-
client := &http.Client{Timeout: syncTimeout}
360-
if ident.Insecure {
361-
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec // opt-in via --insecure
362-
}
379+
client := &http.Client{Timeout: syncTimeout, Transport: syncTransport(ident.Insecure)}
363380
operaDir := filepath.Join(dataDir, "opera")
364381
a := &Agent{
365382
dataDir: dataDir,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package nodeagent
2+
3+
import "testing"
4+
5+
// The long-poll must not use HTTP/2: over the panel's :443 Xray-fallback an h2 hold
6+
// gets GOAWAY'd mid-poll. Guard that the transport keeps h2 disabled.
7+
func TestSyncTransportForcesHTTP1(t *testing.T) {
8+
tr := syncTransport(false)
9+
if tr.ForceAttemptHTTP2 {
10+
t.Error("ForceAttemptHTTP2 is true; the sync long-poll would negotiate h2 and get GOAWAY'd")
11+
}
12+
if tr.TLSNextProto == nil {
13+
t.Error("TLSNextProto is nil; h2 auto-upgrade is not disabled")
14+
}
15+
if len(tr.TLSNextProto) != 0 {
16+
t.Errorf("TLSNextProto has %d entries, want an empty map (no h2)", len(tr.TLSNextProto))
17+
}
18+
if tr.TLSClientConfig != nil && tr.TLSClientConfig.InsecureSkipVerify {
19+
t.Error("secure transport must not skip TLS verification")
20+
}
21+
// Insecure opt-in still forces h1 and only then skips verification.
22+
ins := syncTransport(true)
23+
if ins.TLSNextProto == nil || len(ins.TLSNextProto) != 0 {
24+
t.Error("insecure transport did not disable h2")
25+
}
26+
if ins.TLSClientConfig == nil || !ins.TLSClientConfig.InsecureSkipVerify {
27+
t.Error("insecure transport should skip verification")
28+
}
29+
}

0 commit comments

Comments
 (0)