Repository navigation
agent: choose another proxy when the chosen proxy keeps failing - #1950
Open
Narasimha-sc wants to merge 4 commits into
Open
Narasimha-sc wants to merge 4 commits into
Narasimha-sc wants to merge 4 commits into
Conversation
The proxy chosen for a destination relay is kept in smpProxiedRelays and reused on every retry. It was forgotten only when the proxy could not create a session to the relay (other than a host error), when the proxy connection was closed, or when the agent was closed. Two failures kept it: - connecting to the proxy itself fails (proxy offline): with no proxy connection to close, retries used this proxy until it was reachable again or the app restarted; - a forwarded command fails with PROXY (BROKER e) other than a host error (e.g. TIMEOUT: the proxy has a session to the relay but cannot reach it): retries used this proxy while its connection stayed up. A failed connection to the proxy now forgets it at once: without a live connection the proxy normally has not received this destination, so choosing again adds no privacy cost. A request that only waits for another request's connection does not forget the proxy when its wait times out. A forwarded command failing with PROXY (BROKER e) other than a host error starts a failure streak, stored next to the chosen proxy; a successful command, an error from the destination relay returned through the proxy, or PROXY (PROTOCOL e), the relay's error to the proxy, clears it. A failure that comes at least proxyFailureInterval (30 s) after the start of the streak forgets the proxy, so a single error or a burst of errors keeps it as long as a later forward reaches the relay. The streak functions are exported for tests. Failures, successes and deletes change the entry only while it still names that proxy, also when the proxy disconnects or its relay session fails, so that they do not change a new choice. After PROXY NO_SESSION, the dead relay session is removed from the proxy client that handled the request, so that the retry through the same proxy does not use it again, and that retry no longer records the proxy as the choice when there is none, so that a concurrently forgotten proxy is not put back.
Narasimha-sc
force-pushed
the
nd/proxy-stall-repick
branch
from
October 19, 2026 01:37
4da7f89 to
5c61334
Compare
Narasimha-sc
marked this pull request as draft
October 21, 2026 20:37
Narasimha-sc
marked this pull request as ready for review
October 22, 2026 21:01
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
With private routing, a connection can stay at "connecting…" until the app is restarted, and joining a link can fail on every "Retry". The agent keeps one proxy per destination relay. When that proxy can't forward to the relay, every retry goes through it again. Seen with public servers on 2026-10-14 (smp4 → smp10).
The existing delayed resets don't help here. A failed connection is retried after 3 s (
persistErrorInterval) to the same remembered proxy. A connection is dropped only after 3 timeouts and 15 minutes with no response, and a proxy that answers with errors keeps its connection alive. When the proxy's own connection to the relay is dropped, the client getsNO_SESSIONand by design (#1178) makes a new session through the same proxy.Fix
Forget the chosen proxy when it keeps failing, so the next attempt chooses again:
PROXY (BROKER e)other than a host error (e.g. the proxy times out reaching the relay): the first failure starts a failure streak, stored next to the chosen proxy. A forward that reaches the relay clears it: a success, an error from the destination relay inside the response, orPROXY (PROTOCOL _). A failure at leastproxyFailureInterval(newAgentConfigfield, 30 s) after the start of the streak forgets the proxy. So a single timeout, or a burst of errors from forwards in flight when the relay restarts, doesn't move the destination to another proxy (a failed PRXY still forgets it at once, as before).Updates are guarded: an entry is changed only while it still names the proxy that failed or succeeded (also in
smpClientDisconnectedandnewProxiedRelay), so a late result from the old proxy can't change a new choice. AfterPROXY NO_SESSION, the dead relay session is removed from the proxy client that handled the request. The retry through the same proxy (#1178) doesn't record that proxy as the choice when there is none, so a proxy forgotten concurrently isn't put back.Details:
plans/2026-10-15-proxy-repick-on-failure.md.Tests
SMPProxyTests:proxiedRelayFailedandproxiedRelaySucceededexported).5c613348, which has the same fix before the merge of8bc4d360): with the 30 s forget disabled, "cannot forward" fails (2/2). With the connect-failure forget disabled, "offline" fails (2/2). With the fix, all 3 pass (3/3 runs).f9aa3fe5: passed.5c613348:SMP proxytests 52 examples, 0 failures. Full suite 896 examples, 8 failures, none caused by this change. 2 pass when re-run alone. 3 are environment failures on this machine that also fail on the base code (batching 200 subscriptions, XRCP multicast, SMP server CLI static files). The other 3 also fail on the base-code binary when run alone: delivery receipts concurrently (2/2), XFTP resume deleting after restart (2/2) and join when reply queue creation fails (1/2).Desktop app (simplex-chat with this branch at
5c613348, before the merge of8bc4d360; local servers)Destination relay R; proxy P1 whose forwarding to R is black-holed (a SOCKS relay passes the handshakes and then drops all bytes); good proxy P2. The app's message servers are only P1 and P2. Trials where the first pick was P1:
The longer trials picked P1 again after forgetting it: the failed proxy isn't excluded, so with two proxies that happens half of the time.
Recordings (the app's proxies: 127.0.0.1 = P1, broken, and 127.0.0.2 = P2; the red box marks what to look at):
"connecting…" after the contact joins the app's link. master stays "connecting…":
This branch forgets P1 after 30 s of failures and chooses again; in this recording, going by the timing, the first re-pick chose P1 again, so the contact connects after 130 s, with no user action:
Joining a link with Retry. master fails on every Retry:
This branch: the first "Private routing timeout" starts the streak, the next one 30 s later forgets P1, and the next Retry connects:
Android
The APK built from simplex-chat/simplex-chat#7739 at 26f470276 (before its later master merge, same simplexmq pin), with the core library built from it (this branch at
f9aa3fe5), on Waydroid with local servers; P1 (here 192.168.241.1) can't reach the contact's server, P2 can. Before: the master APK, which bundles an older core library (core 7.1.0.6, September 2026) without this change; there is no master core library built for Android here.The trials that needed more Retries are those where the re-pick chose P1 again (the re-pick is random, so with two proxies it can choose P1 again; here, in the Retry trials, it did in 9 of 14 re-picks). Regression: messages each way before and after an app restart, no crashes.
Open questions
Interactive recovery takes a few Retries. Client timeouts don't count, so a join needs the proxy's report twice, 30 s apart. In the trials: 3–6 Retries on desktop and 4–10 on Android, up to about 5 minutes; 2 Android trials were still failing at the 6-Retry cap.
The failed proxy can be chosen again. Excluding it from the next pick would make recovery take one streak instead of about two on average with two proxies.
The streak doesn't expire. With rare sends, one timeout, then another hours later with nothing reaching the relay in between, forgets the proxy.
Not covered by tests:
deleteRelaySessionchange;The re-pick tests also don't assert that the bad proxy was tried first; they rely on the seeded pick.
Unchanged: a host error that the proxy reports about the relay keeps the entry (e.g. an onion-only relay through a proxy without Tor).