]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Bug 4864: !Comm::MonitorsRead assertion in maybeReadVirginBody() (#351) M-staged-PR351
authorChristos Tsantilas <christos@chtsanti.net>
Fri, 15 Feb 2019 15:50:45 +0000 (15:50 +0000)
committerSquid Anubis <squid-anubis@squid-cache.org>
Mon, 18 Feb 2019 12:08:47 +0000 (12:08 +0000)
This assertion is probably triggered when Squid retries/reforwards
server-first or step2+ bumped connections (after they fail).
Retrying/reforwarding such pinned connections is wrong because the
corresponding client-to-Squid TLS connection was negotiated based on the
now-failed Squid-to-server TLS connection, and there is no mechanism to
ensure that the new Squid-to-server TLS connection will have exactly the
same properties. Squid should forward the error to client instead.

Also fixed peer selection code that could return more than one PINNED
paths with only the first path having the destination of the actual
pinned connection. To reduce the chances of similar future bugs, and to
polish the code, peer selection now returns a nil path to indicate a
PINNED decision. After all, the selection code decides to use a pinned
connection (whatever it is) rather than a specific pinned _destination_.

This is a Measurement Factory project.

src/FwdState.cc
src/FwdState.h
src/PeerSelectState.h
src/client_side.cc
src/client_side.h
src/peer_select.cc
src/tests/stub_client_side.cc
src/tunnel.cc

index d4a30e93ecc95b5375996231da5f2d53b490980e..301da5a4a65ec5541a00ed385e7c778c8809e137 100644 (file)
@@ -165,6 +165,9 @@ void FwdState::start(Pointer aSelf)
     if (!request->flags.ftpNative)
         entry->registerAbort(FwdState::abort, this);
 
+    // just in case; should already be initialized to false
+    request->flags.pinned = false;
+
 #if STRICT_ORIGINAL_DST
     // Bug 3243: CVE 2009-0801
     // Bypass of browser same-origin access control in intercepted communication
@@ -173,9 +176,7 @@ void FwdState::start(Pointer aSelf)
     const bool useOriginalDst = Config.onoff.client_dst_passthru || (request && !request->flags.hostVerified);
     if (isIntercepted && useOriginalDst) {
         selectPeerForIntercepted();
-        // 3.2 does not suppro re-wrapping inside CONNECT.
-        // our only alternative is to fake destination "found" and continue with the forwarding.
-        startConnectionOrFail();
+        useDestinations();
         return;
     }
 #endif
@@ -199,28 +200,20 @@ FwdState::stopAndDestroy(const char *reason)
 void
 FwdState::selectPeerForIntercepted()
 {
+    // We do not support re-wrapping inside CONNECT.
+    // Our only alternative is to fake a noteDestination() call.
+
     // use pinned connection if available
-    Comm::ConnectionPointer p;
     if (ConnStateData *client = request->pinnedConnection()) {
-        p = client->validatePinnedConnection(request, NULL);
-        if (Comm::IsConnOpen(p)) {
-            /* duplicate peerSelectPinned() effects */
-            p->peerType = PINNED;
-            entry->ping_status = PING_DONE;     /* Skip ICP */
-
-            debugs(17, 3, "reusing a pinned conn: " << *p);
-            serverDestinations.push_back(p);
-        } else {
-            debugs(17,2, "Pinned connection is not valid: " << p);
-            ErrorState *anErr = new ErrorState(ERR_ZERO_SIZE_OBJECT, Http::scServiceUnavailable, request);
-            fail(anErr);
-        }
-        // Either use the valid pinned connection or fail if it is invalid.
+        // emulate the PeerSelector::selectPinned() "Skip ICP" effect
+        entry->ping_status = PING_DONE;
+
+        serverDestinations.push_back(nullptr);
         return;
     }
 
     // use client original destination as second preferred choice
-    p = new Comm::Connection();
+    const auto p = new Comm::Connection();
     p->peerType = ORIGINAL_DST;
     p->remote = clientConn->local;
     getOutgoingAddress(request, p);
@@ -421,24 +414,14 @@ FwdState::EnoughTimeToReForward(const time_t fwdStart)
 }
 
 void
-FwdState::startConnectionOrFail()
+FwdState::useDestinations()
 {
-    debugs(17, 3, HERE << entry->url());
-
-    if (serverDestinations.size() > 0) {
-        // Ditch error page if it was created before.
-        // A new one will be created if there's another problem
-        delete err;
-        err = NULL;
-
-        // Update the logging information about this new server connection.
-        // Done here before anything else so the errors get logged for
-        // this server link regardless of what happens when connecting to it.
-        // IF sucessfuly connected this top destination will become the serverConnection().
-        syncHierNote(serverDestinations[0], request->url.host());
-        request->clearError();
-
-        connectStart();
+    debugs(17, 3, serverDestinations.size() << " paths to " << entry->url());
+    if (!serverDestinations.empty()) {
+        if (!serverDestinations[0])
+            usePinned();
+        else
+            connectStart();
     } else {
         if (PeerSelectionInitiator::subscribed) {
             debugs(17, 4, "wait for more destinations to try");
@@ -476,7 +459,6 @@ FwdState::fail(ErrorState * errorState)
 
     if (ConnStateData *pinned_connection = request->pinnedConnection()) {
         pinned_connection->pinning.zeroReply = true;
-        flags.dont_retry = true; // we want to propagate failure to the client
         debugs(17, 4, "zero reply on pinned connection");
     }
 }
@@ -532,7 +514,7 @@ FwdState::complete()
         // drop the last path off the selection list. try the next one.
         if (!serverDestinations.empty()) // paranoid
             serverDestinations.erase(serverDestinations.begin());
-        startConnectionOrFail();
+        useDestinations();
 
     } else {
         if (Comm::IsConnOpen(serverConn))
@@ -552,9 +534,14 @@ void
 FwdState::noteDestination(Comm::ConnectionPointer path)
 {
     const bool wasBlocked = serverDestinations.empty();
+    // XXX: Push even a nil path so that subsequent noteDestination() calls
+    // can rely on wasBlocked to detect ongoing/concurrent attempts.
+    // Upcoming Happy Eyeballs changes will handle this properly.
     serverDestinations.push_back(path);
+    assert(wasBlocked || path); // pinned destinations are always selected first
+
     if (wasBlocked)
-        startConnectionOrFail();
+        useDestinations();
     // else continue to use one of the previously noted destinations;
     // if all of them fail, we may try this path
 }
@@ -575,7 +562,7 @@ FwdState::noteDestinationsEnd(ErrorState *selectionError)
         else
             debugs(17, 3, "Will abort forwarding because path selection found no paths.");
 
-        startConnectionOrFail(); // will choose the OrFail code path
+        useDestinations(); // will detect and handle the lack of paths
         return;
     }
     // else continue to use one of the previously noted destinations;
@@ -629,6 +616,9 @@ FwdState::checkRetry()
     if (exhaustedTries())
         return false;
 
+    if (request->flags.pinned && !pinnedCanRetry())
+        return false;
+
     if (!EnoughTimeToReForward(start_t))
         return false;
 
@@ -683,7 +673,7 @@ FwdState::retryOrBail()
             debugs(17, 4, HERE << "retrying the same destination");
         else
             serverDestinations.erase(serverDestinations.begin()); // last one failed. try another.
-        startConnectionOrFail();
+        useDestinations();
         return;
     }
 
@@ -743,32 +733,34 @@ FwdState::connectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, in
 
     closeHandler = comm_add_close_handler(serverConnection()->fd, fwdServerClosedWrapper, this);
 
-    if (!request->flags.pinned) {
-        const CachePeer *p = serverConnection()->getPeer();
-        const bool peerWantsTls = p && p->secure.encryptTransport;
-        // userWillTlsToPeerForUs assumes CONNECT == HTTPS
-        const bool userWillTlsToPeerForUs = p && p->options.originserver &&
-                                            request->method == Http::METHOD_CONNECT;
-        const bool needTlsToPeer = peerWantsTls && !userWillTlsToPeerForUs;
-        const bool needTlsToOrigin = !p && request->url.getScheme() == AnyP::PROTO_HTTPS;
-        if (needTlsToPeer || needTlsToOrigin || request->flags.sslPeek) {
-            HttpRequest::Pointer requestPointer = request;
-            AsyncCall::Pointer callback = asyncCall(17,4,
-                                                    "FwdState::ConnectedToPeer",
-                                                    FwdStatePeerAnswerDialer(&FwdState::connectedToPeer, this));
-            // Use positive timeout when less than one second is left.
-            const time_t connTimeout = serverDestinations[0]->connectTimeout(start_t);
-            const time_t sslNegotiationTimeout = positiveTimeout(connTimeout);
-            Security::PeerConnector *connector = nullptr;
+    // request->flags.pinned cannot be true in connectDone(). The flag is
+    // only set when we dispatch the request to an existing (pinned) connection.
+    assert(!request->flags.pinned);
+
+    const CachePeer *p = serverConnection()->getPeer();
+    const bool peerWantsTls = p && p->secure.encryptTransport;
+    // userWillTlsToPeerForUs assumes CONNECT == HTTPS
+    const bool userWillTlsToPeerForUs = p && p->options.originserver &&
+                                        request->method == Http::METHOD_CONNECT;
+    const bool needTlsToPeer = peerWantsTls && !userWillTlsToPeerForUs;
+    const bool needTlsToOrigin = !p && request->url.getScheme() == AnyP::PROTO_HTTPS;
+    if (needTlsToPeer || needTlsToOrigin || request->flags.sslPeek) {
+        HttpRequest::Pointer requestPointer = request;
+        AsyncCall::Pointer callback = asyncCall(17,4,
+                                                "FwdState::ConnectedToPeer",
+                                                FwdStatePeerAnswerDialer(&FwdState::connectedToPeer, this));
+        // Use positive timeout when less than one second is left.
+        const time_t connTimeout = serverDestinations[0]->connectTimeout(start_t);
+        const time_t sslNegotiationTimeout = positiveTimeout(connTimeout);
+        Security::PeerConnector *connector = nullptr;
 #if USE_OPENSSL
-            if (request->flags.sslPeek)
-                connector = new Ssl::PeekingPeerConnector(requestPointer, serverConnection(), clientConn, callback, al, sslNegotiationTimeout);
-            else
+        if (request->flags.sslPeek)
+            connector = new Ssl::PeekingPeerConnector(requestPointer, serverConnection(), clientConn, callback, al, sslNegotiationTimeout);
+        else
 #endif
-                connector = new Security::BlindPeerConnector(requestPointer, serverConnection(), callback, al, sslNegotiationTimeout);
-            AsyncJob::Start(connector); // will call our callback
-            return;
-        }
+            connector = new Security::BlindPeerConnector(requestPointer, serverConnection(), callback, al, sslNegotiationTimeout);
+        AsyncJob::Start(connector); // will call our callback
+        return;
     }
 
     // if not encrypting just run the post-connect actions
@@ -867,6 +859,22 @@ FwdState::connectStart()
 
     debugs(17, 3, "fwdConnectStart: " << entry->url());
 
+    // pinned connections go through usePinned() rather than connectStart()
+    assert(serverDestinations[0] != nullptr);
+    request->flags.pinned = false;
+
+    // Ditch the previous error if any.
+    // A new error page will be created if there is another problem.
+    delete err;
+    err = nullptr;
+    request->clearError();
+
+    // Update logging information with the upcoming server connection
+    // destination. Do this early so that any connection establishment errors
+    // are attributed to this destination. If successfully connected, this
+    // destination becomes serverConnection().
+    syncHierNote(serverDestinations[0], request->url.host());
+
     request->hier.startPeerClock();
 
     // Do not fowrward bumped connections to parent proxy unless it is an
@@ -879,40 +887,6 @@ FwdState::connectStart()
         return;
     }
 
-    request->flags.pinned = false; // XXX: what if the ConnStateData set this to flag existing credentials?
-    // XXX: answer: the peer selection *should* catch it and give us only the pinned peer. so we reverse the =0 step below.
-    // XXX: also, logs will now lie if pinning is broken and leads to an error message.
-    if (serverDestinations[0]->peerType == PINNED) {
-        ConnStateData *pinned_connection = request->pinnedConnection();
-        debugs(17,7, "pinned peer connection: " << pinned_connection);
-        // pinned_connection may become nil after a pconn race
-        serverConn = pinned_connection ? pinned_connection->borrowPinnedConnection(request, serverDestinations[0]->getPeer()) : nullptr;
-        if (Comm::IsConnOpen(serverConn)) {
-            flags.connected_okay = true;
-            ++n_tries;
-            request->flags.pinned = true;
-
-            if (pinned_connection->pinnedAuth())
-                request->flags.auth = true;
-
-            closeHandler = comm_add_close_handler(serverConn->fd,  fwdServerClosedWrapper, this);
-
-            syncWithServerConn(pinned_connection->pinning.host);
-
-            // the server may close the pinned connection before this request
-            pconnRace = racePossible;
-            dispatch();
-            return;
-        }
-
-        // Pinned connection failure.
-        debugs(17,2,HERE << "Pinned connection failed: " << pinned_connection);
-        ErrorState *anErr = new ErrorState(ERR_ZERO_SIZE_OBJECT, Http::scServiceUnavailable, request);
-        fail(anErr);
-        stopAndDestroy("pinned connection failure");
-        return;
-    }
-
     // Use pconn to avoid opening a new connection.
     const char *host = NULL;
     if (!serverDestinations[0]->getPeer())
@@ -964,6 +938,51 @@ FwdState::connectStart()
     AsyncJob::Start(cs);
 }
 
+/// send request on an existing connection dedicated to the requesting client
+void
+FwdState::usePinned()
+{
+    // we only handle pinned destinations; others are handled by connectStart()
+    assert(!serverDestinations.empty());
+    assert(!serverDestinations[0]);
+
+    const auto connManager = request->pinnedConnection();
+    debugs(17, 7, "connection manager: " << connManager);
+
+    // the client connection may close while we get here, nullifying connManager
+    const auto temp = connManager ? connManager->borrowPinnedConnection(request) : nullptr;
+    debugs(17, 5, "connection: " << temp);
+
+    // the previously pinned idle peer connection may get closed (by the peer)
+    if (!Comm::IsConnOpen(temp)) {
+        syncHierNote(temp, connManager ? connManager->pinning.host : request->url.host());
+        serverConn = nullptr;
+        const auto anErr = new ErrorState(ERR_ZERO_SIZE_OBJECT, Http::scServiceUnavailable, request);
+        fail(anErr);
+        // Connection managers monitor their idle pinned to-server
+        // connections and close from-client connections upon seeing
+        // a to-server connection closure. Retrying here is futile.
+        stopAndDestroy("pinned connection failure");
+        return;
+    }
+
+    serverConn = temp;
+    flags.connected_okay = true;
+    ++n_tries;
+    request->flags.pinned = true;
+
+    if (connManager->pinnedAuth())
+        request->flags.auth = true;
+
+    closeHandler = comm_add_close_handler(temp->fd,  fwdServerClosedWrapper, this);
+
+    syncWithServerConn(connManager->pinning.host);
+
+    // the server may close the pinned connection before this request
+    pconnRace = racePossible;
+    dispatch();
+}
+
 void
 FwdState::dispatch()
 {
@@ -1107,6 +1126,11 @@ FwdState::reforward()
 
     debugs(17, 3, HERE << e->url() << "?" );
 
+    if (request->flags.pinned && !pinnedCanRetry()) {
+        debugs(17, 3, "pinned connection; cannot retry");
+        return 0;
+    }
+
     if (!EBIT_TEST(e->flags, ENTRY_FWD_HDR_WAIT)) {
         debugs(17, 3, HERE << "No, ENTRY_FWD_HDR_WAIT isn't set");
         return 0;
@@ -1268,6 +1292,28 @@ FwdState::exhaustedTries() const
     return n_tries >= Config.forward_max_tries;
 }
 
+bool
+FwdState::pinnedCanRetry() const
+{
+    assert(request->flags.pinned);
+
+    // pconn race on pinned connection: Currently we do not have any mechanism
+    // to retry current pinned connection path.
+    if (pconnRace == raceHappened)
+        return false;
+
+    // If a bumped connection was pinned, then the TLS client was given our peer
+    // details. Do not retry because we do not ensure that those details stay
+    // constant. Step1-bumped connections do not get our TLS peer details, are
+    // never pinned, and, hence, never reach this method.
+    if (request->flags.sslBumped)
+        return false;
+
+    // The other pinned cases are FTP proxying and connection-based HTTP
+    // authentication. TODO: Do these cases have restrictions?
+    return true;
+}
+
 /**** PRIVATE NON-MEMBER FUNCTIONS ********************************************/
 
 /*
index 943981a7212b784ca9ff3bac138938f7a8dfa267..57707026b720c06ffa4d7486058931c3965077d6 100644 (file)
@@ -78,7 +78,7 @@ public:
     /// This is the real beginning of server connection. Call it whenever
     /// the forwarding server destination has changed and a new one needs to be opened.
     /// Produces the cannot-forward error on fail if no better error exists.
-    void startConnectionOrFail();
+    void useDestinations();
 
     void fail(ErrorState *err);
     void unregister(Comm::ConnectionPointer &conn);
@@ -123,6 +123,13 @@ private:
     void doneWithRetries();
     void completed();
     void retryOrBail();
+
+    void usePinned();
+
+    /// whether a pinned to-peer connection can be replaced with another one
+    /// (in order to retry or reforward a failed request)
+    bool pinnedCanRetry() const;
+
     ErrorState *makeConnectingError(const err_type type) const;
     void connectedToPeer(Security::EncryptorAnswer &answer);
     static void RegisterWithCacheManager(void);
index 14b5402bff9ae7664e45bf134b07b679c8a1569e..f168a8158a4a5c2a89e14efe2380f897db98b23f 100644 (file)
@@ -79,7 +79,7 @@ public:
     bool wantsMoreDestinations() const;
 
     /// processes a newly discovered/finalized path
-    void handlePath(Comm::ConnectionPointer &path, FwdServer &fs);
+    void handlePath(const Comm::ConnectionPointer &path, FwdServer &fs);
 
     /// a single selection loop iteration: attempts to add more destinations
     void selectMore();
index e4285eedd60b3937003097488b7536a9c3240235..2ee7518c13c2a5fa77a99ec5e8c4ce358e5c15de 100644 (file)
@@ -3875,7 +3875,7 @@ ConnStateData::clientPinnedConnectionRead(const CommIoCbParams &io)
 }
 
 const Comm::ConnectionPointer
-ConnStateData::validatePinnedConnection(HttpRequest *request, const CachePeer *aPeer)
+ConnStateData::validatePinnedConnection(HttpRequest *request)
 {
     debugs(33, 7, HERE << pinning.serverConnection);
 
@@ -3888,8 +3888,6 @@ ConnStateData::validatePinnedConnection(HttpRequest *request, const CachePeer *a
         valid = false;
     else if (pinning.peer && !cbdataReferenceValid(pinning.peer))
         valid = false;
-    else if (aPeer != pinning.peer)
-        valid = false;
 
     if (!valid) {
         /* The pinning info is not safe, remove any pinning info */
@@ -3900,10 +3898,10 @@ ConnStateData::validatePinnedConnection(HttpRequest *request, const CachePeer *a
 }
 
 Comm::ConnectionPointer
-ConnStateData::borrowPinnedConnection(HttpRequest *request, const CachePeer *aPeer)
+ConnStateData::borrowPinnedConnection(HttpRequest *request)
 {
     debugs(33, 7, pinning.serverConnection);
-    if (validatePinnedConnection(request, aPeer) != NULL)
+    if (validatePinnedConnection(request) != nullptr)
         stopPinnedConnectionMonitoring();
 
     return pinning.serverConnection; // closed if validation failed
index 19530e72c8b126e9faa36b6fa080285cb0ddcf92..ba955dc8ae6ed380c37285c9d8c9cacfe0af559d 100644 (file)
@@ -182,15 +182,14 @@ public:
     /// Undo pinConnection() and, optionally, close the pinned connection.
     void unpinConnection(const bool andClose);
     /// Returns validated pinnned server connection (and stops its monitoring).
-    Comm::ConnectionPointer borrowPinnedConnection(HttpRequest *request, const CachePeer *aPeer);
+    Comm::ConnectionPointer borrowPinnedConnection(HttpRequest *request);
     /**
      * Checks if there is pinning info if it is valid. It can close the server side connection
      * if pinned info is not valid.
      \param request   if it is not NULL also checks if the pinning info refers to the request client side HttpRequest
-     \param CachePeer      if it is not NULL also check if the CachePeer is the pinning CachePeer
      \return          The details of the server side connection (may be closed if failures were present).
      */
-    const Comm::ConnectionPointer validatePinnedConnection(HttpRequest *request, const CachePeer *peer);
+    const Comm::ConnectionPointer validatePinnedConnection(HttpRequest *request);
     /**
      * returts the pinned CachePeer if exists, NULL otherwise
      */
index 5d9769df6436965ae6fef1997876519e639dca71..298c785c6d6ffaa6fe10496faff0825cfe072e91 100644 (file)
@@ -301,6 +301,17 @@ PeerSelector::resolveSelected()
         return;
     }
 
+    if (fs && fs->code == PINNED) {
+        // Nil path signals a PINNED destination selection. Our initiator should
+        // borrow and use clientConnectionManager's pinned connection object
+        // (regardless of that connection destination).
+        handlePath(nullptr, *fs);
+        servers = fs->next;
+        delete fs;
+        resolveSelected();
+        return;
+    }
+
     // convert the list of FwdServer destinations into destinations IP addresses
     if (fs && wantsMoreDestinations()) {
         // send the next one off for DNS lookup.
@@ -552,8 +563,9 @@ PeerSelector::selectPinned()
     // TODO: Avoid all repeated calls. Relying on PING_DONE is not enough.
     if (!request->pinnedConnection())
         return;
-    CachePeer *pear = request->pinnedConnection()->pinnedPeer();
-    if (Comm::IsConnOpen(request->pinnedConnection()->validatePinnedConnection(request, pear))) {
+
+    if (Comm::IsConnOpen(request->pinnedConnection()->validatePinnedConnection(request))) {
+        const auto pear = request->pinnedConnection()->pinnedPeer();
         const bool usePinned = pear ? peerAllowedToUse(pear, this) : (direct != DIRECT_NO);
         if (usePinned) {
             addSelection(pear, PINNED);
@@ -1003,19 +1015,22 @@ PeerSelector::wantsMoreDestinations() const {
 }
 
 void
-PeerSelector::handlePath(Comm::ConnectionPointer &path, FwdServer &fs)
+PeerSelector::handlePath(const Comm::ConnectionPointer &path, FwdServer &fs)
 {
     ++foundPaths;
 
-    path->peerType = fs.code;
-    path->setPeer(fs._peer.get());
+    if (path) {
+        path->peerType = fs.code;
+        path->setPeer(fs._peer.get());
 
-    // check for a configured outgoing address for this destination...
-    getOutgoingAddress(request, path);
+        // check for a configured outgoing address for this destination...
+        getOutgoingAddress(request, path);
+        debugs(44, 2, id << " found " << path << ", destination #" << foundPaths << " for " << url());
+    } else
+        debugs(44, 2, id << " found pinned, destination #" << foundPaths << " for " << url());
 
     request->hier.ping = ping; // may be updated later
 
-    debugs(44, 2, id << " found " << path << ", destination #" << foundPaths << " for " << url());
     debugs(44, 2, "  always_direct = " << always_direct);
     debugs(44, 2, "   never_direct = " << never_direct);
     debugs(44, 2, "       timedout = " << ping.timedout);
index d43202cfea7e4f43a92351b419254bf8ed286a78..75f2d8deec1792e6d932729fcb1b7d88c367807d 100644 (file)
@@ -34,7 +34,7 @@ bool ConnStateData::handleRequestBodyData() STUB_RETVAL(false)
 void ConnStateData::pinBusyConnection(const Comm::ConnectionPointer &, const HttpRequest::Pointer &) STUB
 void ConnStateData::notePinnedConnectionBecameIdle(PinnedIdleContext) STUB
 void ConnStateData::unpinConnection(const bool) STUB
-const Comm::ConnectionPointer ConnStateData::validatePinnedConnection(HttpRequest *, const CachePeer *) STUB_RETVAL(NULL)
+const Comm::ConnectionPointer ConnStateData::validatePinnedConnection(HttpRequest *) STUB_RETVAL(nullptr)
 void ConnStateData::clientPinnedConnectionClosed(const CommCloseCbParams &) STUB
 void ConnStateData::connStateClosed(const CommCloseCbParams &) STUB
 void ConnStateData::requestTimeout(const CommTimeoutCbParams &) STUB
index 01b140873f9195c99c19a59539af7a7d5110015a..5af4a5665d53d29bb756c0da2d8a3fe49c76c8c8 100644 (file)
@@ -217,6 +217,8 @@ private:
         Security::EncryptorAnswer answer_;
     };
 
+    void usePinned();
+
     /// callback handler after connection setup (including any encryption)
     void connectedToPeer(Security::EncryptorAnswer &answer);
 
@@ -1205,11 +1207,11 @@ tunnelRelayConnectRequest(const Comm::ConnectionPointer &srv, void *data)
 }
 
 static Comm::ConnectionPointer
-borrowPinnedConnection(HttpRequest *request, Comm::ConnectionPointer &serverDestination)
+borrowPinnedConnection(HttpRequest *request)
 {
     // pinned_connection may become nil after a pconn race
     if (ConnStateData *pinned_connection = request ? request->pinnedConnection() : nullptr) {
-        Comm::ConnectionPointer serverConn = pinned_connection->borrowPinnedConnection(request, serverDestination->getPeer());
+        Comm::ConnectionPointer serverConn = pinned_connection->borrowPinnedConnection(request);
         return serverConn;
     }
 
@@ -1220,7 +1222,19 @@ void
 TunnelStateData::noteDestination(Comm::ConnectionPointer path)
 {
     const bool wasBlocked = serverDestinations.empty();
+    // XXX: Push even a nil path so that subsequent noteDestination() calls
+    // can rely on wasBlocked to detect ongoing/concurrent attempts.
+    // Upcoming Happy Eyeballs changes will handle this properly.
     serverDestinations.push_back(path);
+
+    if (!path) { // decided to use a pinned connection
+        // We can call usePinned() without fear of clashing with an earlier
+        // forwarding attempt because PINNED must be the first destination.
+        assert(wasBlocked);
+        usePinned();
+        return;
+    }
+
     if (wasBlocked)
         startConnecting();
     // else continue to use one of the previously noted destinations;
@@ -1292,19 +1306,7 @@ TunnelStateData::startConnecting()
     assert(!serverDestinations.empty());
     Comm::ConnectionPointer &dest = serverDestinations.front();
     debugs(26, 3, "to " << dest);
-
-    if (dest->peerType == PINNED) {
-        Comm::ConnectionPointer serverConn = borrowPinnedConnection(request.getRaw(), dest);
-        debugs(26,7, "pinned peer connection: " << serverConn);
-        if (Comm::IsConnOpen(serverConn)) {
-            tunnelConnectDone(serverConn, Comm::OK, 0, (void *)this);
-            return;
-        }
-        // a PINNED path failure is fatal; do not wait for more paths
-        sendError(new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw()),
-                  "pinned path failure");
-        return;
-    }
+    assert(dest != nullptr);
 
     GetMarkingsToServer(request.getRaw(), *dest);
 
@@ -1315,6 +1317,22 @@ TunnelStateData::startConnecting()
     AsyncJob::Start(cs);
 }
 
+/// send request on an existing connection dedicated to the requesting client
+void
+TunnelStateData::usePinned()
+{
+    const auto serverConn = borrowPinnedConnection(request.getRaw());
+    debugs(26,7, "pinned peer connection: " << serverConn);
+    if (!Comm::IsConnOpen(serverConn)) {
+        // a PINNED path failure is fatal; do not wait for more paths
+        sendError(new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw()),
+                  "pinned path failure");
+        return;
+    }
+
+    tunnelConnectDone(serverConn, Comm::OK, 0, (void *)this);
+}
+
 CBDATA_CLASS_INIT(TunnelStateData);
 
 bool