-
Notifications
You must be signed in to change notification settings - Fork 38
fix(intra,ipn,dialers): restore proxyFor lock-read timeout and enforce read/write deadlines end-to-end #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: n2
Are you sure you want to change the base?
Changes from all commits
8ea75cc
44f5c03
c73f5da
c50f77e
76f5197
bb9723a
90986db
48e24e6
6d24690
5e0d505
f748fa3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ package intra | |||||||||||||
| import ( | ||||||||||||||
| "context" | ||||||||||||||
| "fmt" | ||||||||||||||
| "io" | ||||||||||||||
| "math/rand" | ||||||||||||||
| "net" | ||||||||||||||
| "net/netip" | ||||||||||||||
|
|
@@ -312,12 +313,20 @@ func (h *baseHandler) forward(local, remote net.Conn, smm *FlowSummary) { | |||||||||||||
| isrwext := false | ||||||||||||||
| didSet := false | ||||||||||||||
| timeoutsecs := 0 | ||||||||||||||
| // enable core.Pipe (sendfile/zero-copy) optimizations on TCP if | ||||||||||||||
| // read & write deadlines are not set (as in rwext is effectively | ||||||||||||||
| // a no-op) by unwrapping the underlying remote conn from rwext. | ||||||||||||||
| // enable core.Pipe (sendfile/zero-copy) optimizations on TCP only | ||||||||||||||
| // when no read/write deadline is actually configured (timeoutsecs | ||||||||||||||
| // <= 0), in which case rwext is a no-op wrapper and unwrapping is | ||||||||||||||
| // safe. Do NOT unwrap merely because didSet is true: SetTimeout | ||||||||||||||
| // (via core.SetTimeoutSockOpt) only sets TCP_USER_TIMEOUT, which | ||||||||||||||
| // bounds unacknowledged *writes*, not idle *reads*. If remote is | ||||||||||||||
| // unwrapped here while a positive timeoutsecs is configured, the | ||||||||||||||
| // only mechanism that can bound a stalled Read() (rwext's | ||||||||||||||
| // extendr/extendw, which set a real per-call deadline) is lost, | ||||||||||||||
| // and a peer that silently stops sending (no RST/FIN) causes | ||||||||||||||
| // Read() -- and thus this whole forward() -- to block forever. | ||||||||||||||
| if r, ok := remote.(rwext); ok { | ||||||||||||||
| isrwext = true | ||||||||||||||
| if timeoutsecs, didSet = r.SetTimeout(); didSet || timeoutsecs <= 0 { | ||||||||||||||
| if timeoutsecs, didSet = r.SetTimeout(); timeoutsecs <= 0 { | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] This matters for plain TCP conns with a configured dialer read/write timeout: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Since |
||||||||||||||
| remote = r.Unwrap() // c may be *net.TCPConn or *demuxconn or *dialers.retrier|splitter | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
@@ -568,8 +577,41 @@ func (h *baseHandler) Reset() { | |||||||||||||
| log.I("com: %s: handler reset", h.proto) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // aborter is implemented by conns (currently just *netstack.GTCPConn) that | ||||||||||||||
| // can forcibly terminate with a TCP RST, as opposed to a graceful FIN. | ||||||||||||||
| type aborter interface { | ||||||||||||||
| Abort() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // resetOrClose closes op on c, unless err is a genuine abnormal error (not a | ||||||||||||||
| // plain io.EOF, which indicates the peer closed gracefully) and c supports | ||||||||||||||
| // aborting -- in which case a TCP RST is sent instead of a graceful FIN. | ||||||||||||||
| // | ||||||||||||||
| // This addresses a previously-known gap (see the old "TODO: Propagate TCP | ||||||||||||||
| // RST using local.Abort(), on appropriate errors" note this replaces): | ||||||||||||||
| // download() closing the app-facing conn's write-half gracefully (FIN) even | ||||||||||||||
| // when the actual cause was an abnormal upstream failure (eg: our own | ||||||||||||||
| // idle-read timeout firing because the remote peer silently stopped | ||||||||||||||
| // sending) meant the app could observe what looks like a clean, complete | ||||||||||||||
| // close -- rather than an unambiguous connection-reset -- for a connection | ||||||||||||||
| // that, in fact, terminated abnormally. Some HTTP clients treat a graceful | ||||||||||||||
| // close of an in-flight response as ambiguous/retryable at best, or a | ||||||||||||||
| // silently-truncated "complete" response at worst; a RST is unambiguous. | ||||||||||||||
| func resetOrClose(c io.Closer, op core.CloserOp, err error) { | ||||||||||||||
| if err != nil && err != io.EOF { | ||||||||||||||
| if a, ok := c.(aborter); ok && a != nil && core.IsNotNil(a) { | ||||||||||||||
| // DEBUG-INSTRUMENTATION (temporary, remove once Zee5/PMTUD | ||||||||||||||
| // investigation is closed): confirms this RST-on-error path | ||||||||||||||
| // actually engages (vs falling through to a graceful close). | ||||||||||||||
| log.I("com: dbg: resetOrClose: aborting (RST) %T on err: %v", c, err) | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [maintainability · low] Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [maintainability · low] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [maintainability · low] Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [maintainability · low] Suggestion:
Suggested change
|
||||||||||||||
| a.Abort() | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| core.CloseOp(c, op) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // upload copies data from remote to local, and returns the number of bytes copied and error, if any. | ||||||||||||||
| // TODO: Propagate TCP RST using local.Abort(), on appropriate errors. | ||||||||||||||
| func upload(id string, local, remote net.Conn, ioch chan<- ioinfo) { | ||||||||||||||
| defer core.Recover(core.Exit11, "c.upload."+id) | ||||||||||||||
| defer core.CloseOp(local, core.CopR) | ||||||||||||||
|
|
@@ -587,7 +629,7 @@ func upload(id string, local, remote net.Conn, ioch chan<- ioinfo) { | |||||||||||||
|
|
||||||||||||||
| // download copies data from local to remote, and returns the number of bytes copied and error, if any. | ||||||||||||||
| func download(id string, local, remote net.Conn) (n int64, err error) { | ||||||||||||||
| defer core.CloseOp(local, core.CopW) | ||||||||||||||
| defer func() { resetOrClose(local, core.CopW, err) }() | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] |
||||||||||||||
| defer core.CloseOp(remote, core.CopR) | ||||||||||||||
|
|
||||||||||||||
| n, err = core.Pipe(local, remote) | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -40,6 +40,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/celzero/firestack/intra/log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/celzero/firestack/intra/protect" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/celzero/firestack/intra/settings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "golang.org/x/sys/unix" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type zeroNetAddr struct{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -67,6 +68,38 @@ const ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: with context.TODO, expmap's reaper goroutine will leak. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var ippPins = core.NewSieve[netip.AddrPort, string](context.TODO(), "d.ippPins", desync_cache_ttl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // dbgTCPInfo is a DEBUG-INSTRUMENTATION helper (temporary, remove once | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Zee5/PMTUD investigation is closed). It fetches kernel-level TCP_INFO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // stats (retransmits, rtt, last-data-received) for c, if c is backed by a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // real *net.TCPConn (best-effort; returns "" if unavailable). This lets us | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // see, precisely at the moment a Read() stalls or times out, whether the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // kernel itself ever observed a retransmit from the peer during the stall | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // -- distinguishing a genuine network-path blackhole (no retransmits seen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // peer/path truly silent) from data arriving-but-undelivered (retransmits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // seen, rx_queue would be non-zero) which would point back at our code. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func dbgTCPInfo(c protect.Conn) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sc, ok := c.(syscall.Conn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !ok || sc == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw, err := sc.SyscallConn() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil || raw == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var info *unix.TCPInfo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var operr error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cerr := raw.Control(func(fd uintptr) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info, operr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if cerr != nil || operr != nil || info == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf("tcpinfo-err(ctl=%v,op=%v)", cerr, operr) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf("tcpi[state=%d rtt=%dus rttvar=%dus retx=%d total_retx=%d last_data_recv=%dms last_data_sent=%dms unacked=%d snd_mss=%d rcv_mss=%d pmtu=%d]", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info.State, info.Rtt, info.Rttvar, info.Retransmits, info.Total_retrans, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info.Last_data_recv, info.Last_data_sent, info.Unacked, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info.Snd_mss, info.Rcv_mss, info.Pmtu) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // retrier implements the DuplexConn interface and must | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // be typecastable to *net.TCPConn (see: xdial.DialTCP) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // inheritance: go.dev/play/p/mMiQgXsPM7Y | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -308,6 +341,13 @@ func (r *retrier) dialStratLocked() (strat int32, err error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| strat = r.dialerOpts.Strat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DEBUG-INSTRUMENTATION (temporary, remove once Zee5/PMTUD investigation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // is closed): confirms at the dial layer exactly which split-strategy is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // actually being used per attempt, independent of what the UI/settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // claim is configured. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.I("retrier: dbg: %s: dialStrat: %s: strat=%d auto=%t retryStrat=%d split=%t retryCount=%d/%d", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.dialerID(), r.raddr, strat, auto, retryStrat, split, r.retryCount, r.maxRetries) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -476,9 +516,25 @@ func (r *retrier) Read(buf []byte) (n int, err error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.mu.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c := r.conn // r.conn may be provisional or final connection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rdeadline := r.readDeadline | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.mu.Unlock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if c != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // always (re)apply the caller's current read deadline (as set via | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SetReadDeadline, eg: rwext.extendr) to the underlying conn before | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // reading from it; otherwise, once the retry sequence completes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (see below), this deadline is applied to c just once and never | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // again, so subsequent idle reads on c can block indefinitely even | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // as callers keep extending r.readDeadline on every call. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = c.SetReadDeadline(rdeadline) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+523
to
+529
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Crucially, rdeadline is frequently the zero time here: forward() unwraps rwext when no timeout is configured, and when rwext does wrap the retrier, rwext.WriteTo/ReadFrom call extendForever() (which zeroes the retrier's deadlines) before invoking retrier.WriteTo/ReadFrom. Also note WriteTo/ReadFrom now stream via r (this file), so all of their reads hit this line. Result: c's read deadline is cleared, so the first read can block indefinitely on a peer that silently stops responding, and the timeout error that is supposed to trigger a retry (see the retry loop at line 524) never occurs. Suggest only (re)applying the caller deadline after the retry has completed, and only when it is non-zero. Suggestion:
Suggested change
Comment on lines
+519
to
+529
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Serialize deadline application with deadline updates.
🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Concretely: Gate the refresh on retry completion: the post-retry idle reads this change targets all happen with Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Because Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · high] Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DEBUG-INSTRUMENTATION (temporary, remove once Zee5/PMTUD | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // investigation is closed): kernel TCP_INFO snapshot right before | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // issuing the read, so we can diff against the post-read snapshot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // below to see exactly what changed (or didn't) at the kernel level | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // during this call, especially across a timeout/stall. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| preTCPInfo := dbgTCPInfo(c) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · low] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · medium] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · low] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · medium] Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for reads := range maxEmptyReads { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n, err = c.Read(buf) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if n == 0 && err == nil { // no data and no error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -488,6 +544,19 @@ func (r *retrier) Read(buf []byte) (n int, err error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } // else: check if retry is needed (c == nil or err != nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DEBUG-INSTRUMENTATION (temporary, remove once Zee5/PMTUD | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // investigation is closed): post-read snapshot. If Total_retrans | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // increased between pre/post while err is a timeout, the kernel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DID see retransmits from the peer during the stall (data was | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // attempted but never fully arrived/ack'd) -- a genuine network | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // issue. If Total_retrans is unchanged, the peer never even tried | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // to resend, consistent with a silent path blackhole (eg PMTUD) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // upstream of this device entirely. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| postTCPInfo := dbgTCPInfo(c) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · medium] |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.I("retrier: dbg: %s: read-tcpinfo: [%s<=%s]; pre: %s; post: %s; n=%d err=%v", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.dialerID(), laddr(c), r.raddr, preTCPInfo, postTCPInfo, n, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+557
to
+558
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Guard TCP diagnostics with the debug setting. For a non-nil TCP connection, 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if n == 0 && err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| err = io.ErrNoProgress | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -670,13 +739,16 @@ func (r *retrier) Write(b []byte) (int, error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.mu.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c := r.conn // retry has completed, so r.conn is final and may not need locking? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wdeadline := r.writeDeadline | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.mu.Unlock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if c == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cerr := log.EE("retrier: write: %s: [] => %s (b: %d, tee: %d), not retrying, but no conn; after: %s", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r.dialerID(), r.raddr, len(b), len(r.tee), core.FmtTimeAsPeriod(start)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0, core.JoinErr(cerr, errNilConn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // always (re)apply the caller's current write deadline; see Read() for why. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = c.SetWriteDeadline(wdeadline) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n, err := c.Write(b) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| err = log.EE("retrier: write: %s: [%s=>%s]; b: %d/%d (retried? %t); after: %s; err? %v", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -746,8 +818,11 @@ func (r *retrier) WriteTo(w io.Writer) (bytes int64, err error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !optimizedWriteTo { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // write to w from c until EOF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| b, err = core.Stream(w, c) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // write to w from r (not raw c) until EOF, so that r.Read's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // per-call deadline refresh (see Read()) stays in effect; reading | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // from c directly bypasses that refresh and can hang indefinitely | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // once the retry sequence above has completed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| b, err = core.Stream(w, r) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytes += b | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -842,8 +917,8 @@ func (r *retrier) ReadFrom(reader io.Reader) (bytes int64, err error) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !optimizedReadFrom { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // read from reader into c until EOF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| b, err = core.Stream(c, reader) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // read from reader into r (not raw c) until EOF; see WriteTo for why. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| b, err = core.Stream(r, reader) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytes += b | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1366,8 +1366,10 @@ func (t *dnsgateway) querySecondary(t2 Transport, uid, fid, network string, msg | |
| result.smm.Blocklists = blocklistnames | ||
| result.smm.BlockedTarget = blockedtarget | ||
| } | ||
| if xdns.AQuadAUnspecified(r) { | ||
| // A/AAAA must be 0.0.0.0/::, set UpstreamBlocks to true | ||
| if xdns.AQuadAUnspecified(r) || xdns.AQuadALoopback(r) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] |
||
| // A/AAAA must be 0.0.0.0/:: (or, for some upstream resolvers, | ||
| // sinkholed to loopback 127.0.0.1/::1); either way, set | ||
| // UpstreamBlocks to true | ||
| result.smm.UpstreamBlocks = true | ||
| // discard all other answers | ||
| result.ips = append(result.ips, anyaddr4, anyaddr6) | ||
|
|
@@ -1518,7 +1520,7 @@ func (t *dnsgateway) q(t1, t2 Transport, preset []netip.Addr, origin, network, s | |
| // for t1, ansin's already evaluated for ans0000 in querySecondary | ||
| // (secans.pri is set to true). ansin may be from t2 (if t2 != nil), | ||
| // ans64, which is a modified ansin, depending on settings.PtMode | ||
| ans0000 := xdns.AQuadAUnspecified(ansin) // ansin is not nil; ans64 may be nil | ||
| ans0000 := xdns.AQuadAUnspecified(ansin) || xdns.AQuadALoopback(ansin) // ansin is not nil; ans64 may be nil | ||
|
|
||
| if ans0000 { | ||
| smm.BlockedTarget = qname | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,21 @@ const ( | |
| ttl30s = 30 * time.Second | ||
| shortdelay = 100 * time.Millisecond | ||
| delayForUnhealthyProxies = 2 * time.Second | ||
|
|
||
| // STREAMSHIELD HOOK: short-interval TCP keepalive tuning for Exit-proxied | ||
| // connections. Some video-CDN/load-balancer stacks (observed: Zee5's | ||
| // Akamai-fronted CDN) close idle-but-alive connections at ~120s via a | ||
| // clean FIN, and the app's HTTP client fails to transparently retry on | ||
| // the dead pooled connection (permanent hang). sockopt.go's global | ||
| // defaults (600s idle) are far too slow to matter here. These constants | ||
| // are used only when the user enables the existing "TCP keep alive" | ||
| // toggle (settings.DialerOpts.LowerKeepAlive / PersistentState.tcpKeepAlive) | ||
| // -- best-effort experiment: only helps if the peer's idle-timeout tracks | ||
| // raw TCP socket activity (keepalive probes are pure ACKs) rather than | ||
| // HTTP-level request/response activity. | ||
| exitKeepAliveIdleSec = 20 // secs of inactivity before first probe | ||
| exitKeepAliveIntervalSec = 5 // secs between probes | ||
| exitKeepAliveCount = 4 // unacked probes before conn is declared dead | ||
| ) | ||
|
|
||
| // auto is a proxy that dials multiple, preset outbounds. | ||
|
|
@@ -624,8 +639,9 @@ func maybeKeepAlive2(c net.Conn) (keepingalive, ok bool) { | |
| } | ||
|
|
||
| if opts := settings.GetDialerOpts(); opts.LowerKeepAlive { | ||
| // adjust socket's keepalive config | ||
| lowered := core.SetKeepAliveConfigSockOpt(c) | ||
| // STREAMSHIELD HOOK: use the short-interval tuning above instead of | ||
| // sockopt.go's slow global defaults (600s idle) -- see const block. | ||
| lowered := core.SetKeepAliveConfigSockOpt(c, exitKeepAliveIdleSec, exitKeepAliveIntervalSec, exitKeepAliveCount) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] |
||
| keepingalive = lowered | ||
| ok = lowered | ||
| return | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -929,7 +929,26 @@ func (px *proxifier) proxyFor(id string) (Proxy, error) { | |||||||||
| // Ingress (dummy): no fast path, fall through to general lookup | ||||||||||
| } | ||||||||||
|
|
||||||||||
| timeout := time.Duration(minWaitPeriodSec/2) * time.Second | ||||||||||
| // Regression fix: this used to be getproxytimeout (5s) and was | ||||||||||
| // inadvertently shortened to minWaitPeriodSec/2 (1s) in 8677a52c | ||||||||||
| // ("core/volatile: cr by muse spark" era commit chain). proxyFor is | ||||||||||
| // called for every proxy id, including non-wellknown, app-registered | ||||||||||
| // ids (see isWellknown/ProxyFor above) for which there is NO retry/ | ||||||||||
| // wait fallback -- ProxyFor returns immediately with errProxyNotFound | ||||||||||
| // for those ids, so this is the *only* window a caller gets to find | ||||||||||
| // a just-registered proxy. The lookup itself is a cheap RLock'd map | ||||||||||
| // read (see below), but on loaded/low-RAM devices the paired Lock() | ||||||||||
| // in AddProxy/RemoveProxy can legitimately hold the mutex for longer | ||||||||||
| // than 1s during proxy setup/teardown, especially for proxies that do | ||||||||||
| // real I/O in their constructor. Shortening this guard to 1s turns a | ||||||||||
| // rare, recoverable stall into a hard, unretried lookup failure for | ||||||||||
| // any non-wellknown proxy id registered right around this window -- | ||||||||||
| // observed in production as a permanently-failing custom local proxy | ||||||||||
| // route until the next reconnect. Restoring getproxytimeout (5s) | ||||||||||
| // keeps this a deadlock-recovery guard (its original documented | ||||||||||
| // purpose, see the ProxyFor doc-comment above) rather than a | ||||||||||
| // register-race timeout. | ||||||||||
| timeout := getproxytimeout | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · medium] Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · medium] The stated rationale does not hold up: proxy construction happens in Suggestion:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · medium] |
||||||||||
| // go.dev/play/p/xCug1W3OcMH | ||||||||||
| p, completed := core.Grx("pxr.ProxyFor: "+id, func(_ context.Context) (Proxy, error) { | ||||||||||
| px.RLock() | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -326,14 +326,26 @@ func (g *GTCPConn) RemoteAddr() net.Addr { | |
|
|
||
| func (g *GTCPConn) Write(data []byte) (int, error) { | ||
| if c := g.conn(); c != nil { | ||
| return c.Write(data) | ||
| n, err := c.Write(data) | ||
| // DEBUG-INSTRUMENTATION (temporary, remove once Zee5/PMTUD | ||
| // investigation is closed): app-facing (netstack) side write | ||
| // visibility, to check whether the app itself ever stops writing | ||
| // (eg: waiting on a stalled read) vs our exit-side relay stalling | ||
| // independently. | ||
| log.VV("netstack: dbg: gconn(%s): write: b=%d/%d; err=%v", g.o, n, len(data), err) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [maintainability · low] |
||
| return n, err | ||
| } | ||
| return 0, netError(g, "tcp", g.o+":write", io.ErrClosedPipe) | ||
| } | ||
|
|
||
| func (g *GTCPConn) Read(data []byte) (int, error) { | ||
| if c := g.conn(); c != nil { | ||
| return c.Read(data) | ||
| n, err := c.Read(data) | ||
| // DEBUG-INSTRUMENTATION (temporary, remove once Zee5/PMTUD | ||
| // investigation is closed): app-facing (netstack) side read | ||
| // visibility, paired with the Write() log above. | ||
| log.VV("netstack: dbg: gconn(%s): read: b=%d/%d; err=%v", g.o, n, len(data), err) | ||
| return n, err | ||
| } | ||
| return 0, netError(g, "tcp", g.o+":read", io.ErrNoProgress) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[bug · medium]
Keeping the
rwextwrapper here changes connection teardown semantics.rwextonly embedsnet.Conn, so it does not implementCloseRead/CloseWrite(note the commented-outcore.DuplexCloserassertion in intra/rwconn.go). As a result,core.CloseOp(remote, core.CopR)/core.CloseOp(remote, core.CopW)inupload/downloadno longer matchescore.TCPConn/core.DuplexCloserand falls through to the genericio.Closercase, i.e. a fullClose()instead of a half-close.Previously
remotewas unwrapped wheneverSetTimeoutset a socket option (didSet), so for the usual*net.TCPConnegress these were realCloseRead/CloseWritecalls. Sinceuploadanddownloadcopy concurrently over the sameremote, the first direction to finish will now forcibly close the socket and abort the other direction (truncated response / RST) for any user with a positive dialer timeout. Please preserve half-close, e.g. by forwardingCloseRead/CloseWritefromrwext, before keeping the wrapper.