diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 01c47fd513..3cc03e3129 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4633,11 +4633,12 @@ processChatCommand cxt nm = \case -- | Failure modes for 'resolveOnUserServers' / 'iterateResolvers'. data ResolveError - = -- | No enabled server can resolve: every candidate answered CMD UNKNOWN / - -- PROHIBITED (does not speak RSLV), or none were configured / reachable. - -- Iterating across unsupported servers is privacy-safe -- the client degrades - -- RSLV to a no-op for relays below namesSMPVersion (see Protocol.hs), so an - -- unsupported relay never receives the queried name. + = -- | No enabled server can resolve: every candidate answered CMD UNKNOWN + -- (predates RSLV) or CMD PROHIBITED (speaks RSLV but has no resolver + -- configured), or none were configured / reachable. Iterating across these is + -- safe: a CMD UNKNOWN relay never received the name (the client degrades RSLV + -- to a no-op below namesSMPVersion, see Protocol.hs); a CMD PROHIBITED relay + -- did receive it but is one the user already trusts as an SMP server. ResolverUnavailable | -- | AUTH from a name-capable server. Every name server reads the same on-chain state, so we trust the first one's no. NameNotRegistered @@ -4664,13 +4665,15 @@ enabledSMPServersForUser user = do | otherwise = Nothing -- | Resolve a SimpleX name by trying the user's enabled SMP servers in order. --- Transport-level failures (NETWORK, TIMEOUT, host-unreachable) and unsupported --- servers (CMD UNKNOWN / PROHIBITED, i.e. relays that don't speak RSLV) both fall --- through to the next server. Skipping an unsupported relay discloses nothing: --- the client degrades RSLV to a no-op below namesSMPVersion, so the name is never --- sent to it. A definitive answer from a name-capable relay terminates iteration: --- AUTH is definitive NotFound (every name server reads the same on-chain state); --- any other definite error surfaces as ResolverTransport. +-- Transport-level failures (NETWORK, TIMEOUT, host-unreachable) and servers that +-- cannot resolve (CMD UNKNOWN -- predates RSLV; or CMD PROHIBITED -- speaks RSLV +-- but has no resolver configured) all fall through to the next server. A CMD +-- UNKNOWN relay never received the name (the client degrades RSLV below +-- namesSMPVersion); a CMD PROHIBITED relay did, but it is one the user already +-- trusts as an SMP server. A definitive answer from a name-capable relay +-- terminates iteration: AUTH is definitive NotFound (every name server reads the +-- same on-chain state); any other definite error (e.g. INTERNAL on a resolver +-- backend failure) surfaces as ResolverTransport. -- Privacy: a name-capable relay does see the queried name, so once one has -- answered we do not broadcast the miss to every other operator the user has. resolveOnUserServers :: User -> SimplexNameDomain -> CM (Either ResolveError NameRecord) @@ -4858,12 +4861,14 @@ iterateResolvers servers resolve = go servers isNotRegistered = \case SMP _ SMP.AUTH -> True _ -> False - -- A server that doesn't speak RSLV answers CMD UNKNOWN (predates the - -- command, e.g. the official servers) or CMD PROHIBITED (knows it but gates - -- on namesSMPVersion) -- directly, or wrapped by a proxy. We skip it and try - -- the next server: the client degrades RSLV to a no-op below namesSMPVersion - -- (Protocol.hs), so an unsupported relay never received the queried name. - -- ResolverUnavailable is returned only when no server can resolve. + -- A server that cannot resolve answers CMD UNKNOWN -- it predates RSLV (e.g. + -- an old official server), and the client degraded RSLV to a no-op below + -- namesSMPVersion so it never received the name -- or CMD PROHIBITED -- it + -- speaks RSLV but has no resolver configured (names role off), so it did + -- receive the name but cannot help. Either form may arrive directly or wrapped + -- by a proxy. We skip it and try the next server; ResolverUnavailable is + -- returned only when no server can resolve. A resolver-backed server's + -- transient failure is INTERNAL (-> ResolverTransport), not handled here. isUnsupported = \case SMP _ (SMP.CMD SMP.UNKNOWN) -> True SMP _ (SMP.CMD SMP.PROHIBITED) -> True diff --git a/tests/ResolveNameTests.hs b/tests/ResolveNameTests.hs index bb692aac2d..090db51ce2 100644 --- a/tests/ResolveNameTests.hs +++ b/tests/ResolveNameTests.hs @@ -62,6 +62,15 @@ resolveNameTests = do -- second server must NOT be consulted: definite error means the server -- answered, so iterating would leak the queried name. readIORef callsRef `shouldReturn` [srv1] + it "surfaces a resolver-backend INTERNAL as ResolverTransport, not NotFound" $ do + callsRef <- newIORef [] + r <- iterateResolvers [srv1, srv2] (recording callsRef (\_ -> pure $ Left backendErr)) + case r of + Left (ResolverTransport e) -> e `shouldBe` backendErr + other -> expectationFailure $ "expected ResolverTransport, got " <> show other + -- a backend failure on srv1 stops iteration (the name reached a capable + -- relay); it must not be reported as the authoritative NameNotRegistered. + readIORef callsRef `shouldReturn` [srv1] it "iterates on transport-level errors and uses the next server's success" $ do callsRef <- newIORef [] r <- iterateResolvers [srv1, srv2] (recording callsRef stubTransportThenHit) @@ -230,7 +239,9 @@ sampleRecord = authErr :: AgentErrorType authErr = SMP "smp1.example" SMP.AUTH --- CMD PROHIBITED on the PFWD path: surfaces as PROXY ... (ProxyProtocolError ...). +-- A relay with no resolver configured (names role off) answers CMD PROHIBITED +-- (Server.hs). A relay error is transparent over the proxy (SMP host ...); the +-- PROXY-wrapped form here exercises a proxy-level rejection. Both -> skip. prohibitedErr :: AgentErrorType prohibitedErr = PROXY "proxy.example" "smp1.example" (ProxyProtocolError (SMP.CMD SMP.PROHIBITED)) @@ -245,3 +256,10 @@ networkErr = BROKER "smp1.example" (NETWORK (NEConnectError "simulated network f -- as ResolverTransport without iterating, to avoid broadcasting the name. otherDefiniteErr :: AgentErrorType otherDefiniteErr = INTERNAL "simulated definite error" + +-- A resolver-backed relay whose backing store failed (resolver 5xx, timeout, +-- decode error) answers ERR INTERNAL (Server.hs), surfacing as SMP host INTERNAL. +-- This is transient -- it must surface as ResolverTransport, NOT collapse to the +-- authoritative NameNotRegistered the way the old ERR-AUTH-for-everything did. +backendErr :: AgentErrorType +backendErr = SMP "smp1.example" SMP.INTERNAL