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
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.
Not ready: needs Android testing with the core library built from simplex-chat/simplex-chat#7739. Desktop testing is done.
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 while the relay restarts, doesn't move the destination to another proxy.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).SMP proxytests: 52 examples, 0 failures.Desktop app (simplex-chat with this branch, 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 connects once P1 has failed for 30 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
Pending: needs the core library built from simplex-chat/simplex-chat#7739. The previous version of this PR, which forgot the proxy on the first failure, passed fixed-outcome checks on Android.
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 that took 3–6 Retries, up to about 3 minutes.
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).