/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // gtests for HappyEyeballsConnectionAttempt. Two injected seams exercise the // dispatch logic without real sockets: a fake ConnectionEstablisherFactory // (establishers complete on demand) and a recording // HappyEyeballsConnMgrDelegate (intercepts connection-manager/entry calls). The // real Rust engine stays live, driven via its FFI inputs; an IP-literal origin // avoids DNS. #include "gtest/gtest.h" #include "ConnectionEntry.h" #include "ConnectionEstablisher.h" #include "HappyEyeballsConnMgrDelegate.h" #include "HappyEyeballsConnectionAttempt.h" #include "HappyEyeballsTransaction.h" #include "HttpConnectionBase.h" #include "HttpTrafficAnalyzer.h" #include "NullHttpTransaction.h" #include "PendingTransactionInfo.h" #include "ZeroRttHandle.h" #include "mozilla/Preferences.h" #include "mozilla/net/ClassOfService.h" #include "mozilla/net/DNS.h" #include "nsHttpConnectionInfo.h" #include "nsHttpRequestHead.h" #include "nsHttpTransaction.h" #include "nsISeekableStream.h" #include "nsISocketTransportService.h" #include "nsIHttpProtocolHandler.h" #include "nsNetAddr.h" #include "nsServiceManagerUtils.h" #include "nsSocketTransportService2.h" #include "nsThreadUtils.h" namespace mozilla { namespace net { namespace { // Minimal HttpConnectionBase: trivial pure-virtual bodies plus a few knobs // (UsingSpdy/UsingHttp3, Activate result) the success path reads. class FakeHttpConnection final : public HttpConnectionBase { public: NS_DECL_THREADSAFE_ISUPPORTS explicit FakeHttpConnection(bool aUsingHttp3, bool aUsingSpdy) : mUsingHttp3(aUsingHttp3), mUsingSpdy(aUsingSpdy) {} bool UsingSpdy() override { return mUsingSpdy; } bool UsingHttp3() override { return mUsingHttp3; } [[nodiscard]] nsresult Activate(nsAHttpTransaction*, uint32_t, int32_t) override { return NS_OK; } void Close(nsresult, bool) override {} bool CanReuse() override { return false; } bool CanDirectlyActivate() override { return false; } void DontReuse() override {} nsAHttpTransaction* Transaction() override { return nullptr; } void CloseTransaction(nsAHttpTransaction*, nsresult, bool) override {} [[nodiscard]] nsresult OnHeadersAvailable(nsAHttpTransaction*, nsHttpRequestHead*, nsHttpResponseHead*, bool*) override { return NS_OK; } [[nodiscard]] nsresult TakeTransport(nsISocketTransport**, nsIAsyncInputStream**, nsIAsyncOutputStream**) override { return NS_ERROR_NOT_IMPLEMENTED; } void PrintDiagnostics(nsCString&) override {} bool TestJoinConnection(const nsACString&, int32_t) override { return false; } bool JoinConnection(const nsACString&, int32_t) override { return false; } void GetTLSSocketControl(nsITLSSocketControl** aResult) override { *aResult = nullptr; } [[nodiscard]] nsresult ResumeSend() override { return NS_OK; } [[nodiscard]] nsresult ResumeRecv() override { return NS_OK; } [[nodiscard]] nsresult ForceSend() override { return NS_OK; } [[nodiscard]] nsresult ForceRecv() override { return NS_OK; } HttpVersion Version() override { return mUsingHttp3 ? HttpVersion::v3_0 : (mUsingSpdy ? HttpVersion::v2_0 : HttpVersion::v1_1); } bool LastTransactionExpectedNoContent() override { return false; } void SetLastTransactionExpectedNoContent(bool) override {} int64_t BytesWritten() override { return 0; } bool IsPersistent() override { return true; } bool IsReused() override { return false; } [[nodiscard]] nsresult PushBack(const char*, uint32_t) override { return NS_ERROR_NOT_IMPLEMENTED; } void SetEvent(nsresult) override {} nsresult GetSelfAddr(NetAddr* aAddr) override { *aAddr = NetAddr(); return NS_OK; } nsresult GetPeerAddr(NetAddr* aAddr) override { *aAddr = NetAddr(); return NS_OK; } bool ResolvedByTRR() override { return false; } nsIRequest::TRRMode EffectiveTRRMode() override { return nsIRequest::TRR_DEFAULT_MODE; } TRRSkippedReason TRRSkipReason() override { return nsITRRSkipReason::TRR_UNSET; } bool GetEchConfigUsed() override { return false; } PRIntervalTime LastWriteTime() override { return 0; } [[nodiscard]] nsresult CreateTunnelStream(nsAHttpTransaction*, HttpConnectionBase**, bool) override { return NS_ERROR_NOT_IMPLEMENTED; } private: ~FakeHttpConnection() = default; bool mUsingHttp3; bool mUsingSpdy; }; NS_IMPL_ISUPPORTS(FakeHttpConnection, nsISupportsWeakReference) // Establisher that captures the completion callback instead of opening a // socket. The test fires the result whenever it wants. class FakeConnectionEstablisher final : public ConnectionEstablisher { public: NS_INLINE_DECL_REFCOUNTING_INHERITED(FakeConnectionEstablisher, ConnectionEstablisher) FakeConnectionEstablisher(nsHttpConnectionInfo* aConnInfo, const NetAddr& aAddr, uint32_t aCaps, bool aIsUDP) : ConnectionEstablisher(aConnInfo, aAddr, aCaps), mIsUDP(aIsUDP) {} bool Start(DoneCallback&& aCallback) override { mCallback = std::move(aCallback); return true; } void Close(nsresult) override { ++mCloseCount; } void ResetSpeculativeFlags() override {} bool IsUDP() const override { return mIsUDP; } void FireSuccess(HttpConnectionBase* aConn) { mResultConn = aConn; mCallback(RefPtr(aConn)); } void FireError(nsresult aError) { mCallback(Err(aError)); } uint32_t mCloseCount = 0; private: ~FakeConnectionEstablisher() = default; void Finish(nsresult) override {} bool mIsUDP; }; class FakeConnectionEstablisherFactory final : public ConnectionEstablisherFactory { public: already_AddRefed Create( ConnectionEstablisherType aType, nsHttpConnectionInfo* aConnInfo, const NetAddr& aAddr, uint32_t aCaps, bool, bool) override { bool isUDP = aType == ConnectionEstablisherType::UDP; RefPtr e = new FakeConnectionEstablisher(aConnInfo, aAddr, aCaps, isUDP); (isUDP ? mUDP : mTCP).AppendElement(e); return e.forget(); } nsTArray> mTCP; nsTArray> mUDP; }; class RecordingConnMgrDelegate final : public HappyEyeballsConnMgrDelegate { public: already_AddRefed FindTransaction( bool, ConnectionEntry*, nsAHttpTransaction*) override { mCalls.AppendElement("FindTransaction"_ns); return do_AddRef(mFindResult); } nsresult DispatchTransaction(ConnectionEntry*, nsHttpTransaction* aTrans, HttpConnectionBase* aConn) override { mCalls.AppendElement("DispatchTransaction"_ns); // Like the real ConnMgr, bind the transaction to the connection. Keep the // handle so the test can Reset it before teardown (~ConnectionHandle would // otherwise reclaim the fake connection and crash). if (mSimulateDispatchBindsConnection && aTrans && aConn) { RefPtr handle = new ConnectionHandle(aConn); aTrans->SetConnection(handle); mDispatchHandles.AppendElement(handle); } return mDispatchRv; } void AddTransaction(nsHttpTransaction*, int32_t) override { mCalls.AppendElement("AddTransaction"_ns); } void ReportSpdyConnection(HttpConnectionBase*, bool, bool) override { mCalls.AppendElement("ReportSpdyConnection"_ns); } void ReportHttp3Connection(HttpConnectionBase*, ConnectionEntry*) override { mCalls.AppendElement("ReportHttp3Connection"_ns); } void ReclaimConnection(HttpConnectionBase*) override { mCalls.AppendElement("ReclaimConnection"_ns); } void ProcessSpdyPendingQ(ConnectionEntry*) override { mCalls.AppendElement("ProcessSpdyPendingQ"_ns); } void InsertIntoActiveConns(ConnectionEntry*, HttpConnectionBase*) override { mCalls.AppendElement("InsertIntoActiveConns"_ns); } void RemoveConnectionAttempt(ConnectionEntry*, ConnectionAttempt*, bool) override { mCalls.AppendElement("RemoveConnectionAttempt"_ns); } void RecordIPFamilyPreference(ConnectionEntry*, uint16_t) override { mCalls.AppendElement("RecordIPFamilyPreference"_ns); } void ResetIPFamilyPreference(ConnectionEntry*) override { mCalls.AppendElement("ResetIPFamilyPreference"_ns); } bool MaybeProcessCoalescingKeys(ConnectionEntry*, const nsTArray&, bool) override { mCalls.AppendElement("MaybeProcessCoalescingKeys"_ns); return false; } bool RemoveTransFromPendingQ(ConnectionEntry*, nsHttpTransaction*) override { mCalls.AppendElement("RemoveTransFromPendingQ"_ns); return false; } nsresult StartRetryWithoutTRR(ConnectionEntry*, nsHttpTransaction*, uint32_t, bool, bool, bool) override { mCalls.AppendElement("StartRetryWithoutTRR"_ns); return mStartRetryWithoutTRRRv; } int32_t Count(const char* aName) const { int32_t n = 0; for (const auto& s : mCalls) { if (s.EqualsASCII(aName)) { n++; } } return n; } // Clear the connection handles created by DispatchTransaction so their // destructors don't reclaim the fake connections. void ResetHandles() { for (auto& handle : mDispatchHandles) { handle->Reset(); } } nsTArray mCalls; RefPtr mFindResult; nsresult mDispatchRv = NS_OK; nsresult mStartRetryWithoutTRRRv = NS_OK; bool mSimulateDispatchBindsConnection = false; nsTArray> mDispatchHandles; }; struct TestHarness { RefPtr mConnInfo; RefPtr mTrans; nsTHashSet mPendingQSet; RefPtr mEntry; RefPtr mFactory; RefPtr mDelegate; RefPtr mHE; // aRealTransaction: use a real nsHttpTransaction (QueryHttpTransaction() // non-null) for paths that touch the real request; else a // NullHttpTransaction. explicit TestHarness(uint32_t aCaps = 0, bool aRealTransaction = false) { mConnInfo = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); if (aRealTransaction) { mTrans = new nsHttpTransaction(); } else { mTrans = new NullHttpTransaction(mConnInfo, nullptr, aCaps); } mEntry = new ConnectionEntry(mConnInfo, mPendingQSet); mFactory = new FakeConnectionEstablisherFactory(); mDelegate = new RecordingConnMgrDelegate(); mHE = new HappyEyeballsConnectionAttempt(mConnInfo, mTrans, aCaps, /*speculative*/ false, /*urgentStart*/ false); mHE->SetConnectionEstablisherFactoryForTesting(mFactory); mHE->SetConnMgrDelegateForTesting(mDelegate); } void Init() { mHE->Init(mEntry); } }; // Ensure gHttpHandler is initialized (the HCA ctor notifies HTTP activity // through it). Must run on the main thread. static void EnsureHttpHandler() { nsCOMPtr http = do_GetService("@mozilla.org/network/protocol;1?name=http"); ASSERT_TRUE(http); } // Run `aFn` on the socket thread and block until it completes. static void RunOnSocketThread(std::function&& aFn) { nsCOMPtr sts = gSocketTransportService; ASSERT_TRUE(sts); NS_DispatchAndSpinEventLoopUntilComplete( "TestHappyEyeballs"_ns, sts, NS_NewRunnableFunction("TestHappyEyeballs", std::move(aFn))); } } // namespace // Init() with an IP-literal origin should drive the engine to attempt a // connection through the injected factory rather than opening a real socket. TEST(HappyEyeballsConnectionAttempt, FactorySeamUsed) { EnsureHttpHandler(); RunOnSocketThread([]() { TestHarness h; h.Init(); EXPECT_GE(h.mFactory->mTCP.Length(), 1u) << "engine should have requested a TCP establisher via the factory"; EXPECT_EQ(h.mFactory->mUDP.Length(), 0u); h.mHE->Abandon(); }); } // Firing a successful TCP connection runs Succeeded -> EnterSucceeded -> // ProcessTCPConn, which dispatches the pending real transaction onto the // winning connection through the delegate. TEST(HappyEyeballsConnectionAttempt, TcpSuccessRunsDispatchViaDelegate) { EnsureHttpHandler(); RunOnSocketThread([]() { TestHarness h(/*caps*/ 0, /*realTransaction*/ true); nsHttpTransaction* realTrans = h.mTrans->QueryHttpTransaction(); ASSERT_TRUE(realTrans); // The pending-queue lookup returns the real transaction so ProcessTCPConn // dispatches it rather than reclaiming the connection. h.mDelegate->mFindResult = new PendingTransactionInfo(realTrans); h.Init(); ASSERT_GE(h.mFactory->mTCP.Length(), 1u); RefPtr conn = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false); h.mFactory->mTCP[0]->FireSuccess(conn); EXPECT_TRUE(h.mHE->IsTerminal()); EXPECT_EQ(h.mDelegate->Count("RecordIPFamilyPreference"), 1); EXPECT_EQ(h.mDelegate->Count("InsertIntoActiveConns"), 1); EXPECT_EQ(h.mDelegate->Count("FindTransaction"), 1); EXPECT_EQ(h.mDelegate->Count("DispatchTransaction"), 1) << "the pending transaction should be dispatched onto the connection"; EXPECT_TRUE(h.mHE->WasTransactionAdoptedForTesting()); EXPECT_EQ(h.mDelegate->Count("ReportSpdyConnection"), 1); }); } // A failing connection result (the only address) drives the engine to Failed // and EnterFailed, which removes the attempt and never dispatches. TEST(HappyEyeballsConnectionAttempt, TcpFailureDoesNotDispatch) { EnsureHttpHandler(); RunOnSocketThread([]() { TestHarness h; h.Init(); ASSERT_GE(h.mFactory->mTCP.Length(), 1u); h.mFactory->mTCP[0]->FireError(NS_ERROR_CONNECTION_REFUSED); EXPECT_TRUE(h.mHE->IsTerminal()); EXPECT_EQ(h.mDelegate->Count("DispatchTransaction"), 0); EXPECT_EQ(h.mDelegate->Count("InsertIntoActiveConns"), 0); EXPECT_GE(h.mDelegate->Count("RemoveConnectionAttempt"), 1); }); } // 0-RTT started but no winner, then a non-0-RTT conn wins. EnterSucceeded's // fallback (AnyStarted && !HadWinner) must re-queue the real transaction once // and not mark it adopted. Uses the ZeroRttHandle seam to set AnyStarted. TEST(HappyEyeballsConnectionAttempt, ZeroRttNoWinnerFallback) { EnsureHttpHandler(); RunOnSocketThread([]() { TestHarness h(/*caps*/ 0, /*realTransaction*/ true); h.Init(); ASSERT_GE(h.mFactory->mTCP.Length(), 1u); // A racer entered 0-RTT but never became the winner. h.mHE->ZeroRttHandleForTesting()->SetAnyStartedForTesting(); RefPtr conn = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false); h.mFactory->mTCP[0]->FireSuccess(conn); EXPECT_TRUE(h.mHE->IsTerminal()); EXPECT_EQ(h.mDelegate->Count("AddTransaction"), 1) << "fallback should re-queue the real transaction once"; EXPECT_EQ(h.mDelegate->Count("DispatchTransaction"), 0); EXPECT_FALSE(h.mHE->WasTransactionAdoptedForTesting()); }); } // ReleaseRealTransaction guard (bug 2048392), real-world race: two HCAs race // one transaction. The winner dispatches it onto its connection; the loser's // attempt then fails and runs ReleaseRealTransaction. Since the transaction is // already on the winner's connection, the loser must not close it or remove it // from the pending queue. TEST(HappyEyeballsConnectionAttempt, ReleaseRealTransactionSkipsCloseWhenConnected) { EnsureHttpHandler(); RunOnSocketThread([]() { RefPtr ci = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); RefPtr realTrans = new nsHttpTransaction(); nsTHashSet pendingQSet; // --- HCA #1: wins and dispatches the shared transaction. --- RefPtr entry1 = new ConnectionEntry(ci, pendingQSet); RefPtr factory1 = new FakeConnectionEstablisherFactory(); RefPtr delegate1 = new RecordingConnMgrDelegate(); // Winner's dispatch binds the transaction to its connection. delegate1->mSimulateDispatchBindsConnection = true; delegate1->mFindResult = new PendingTransactionInfo(realTrans); RefPtr hca1 = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); hca1->SetConnectionEstablisherFactoryForTesting(factory1); hca1->SetConnMgrDelegateForTesting(delegate1); hca1->Init(entry1); ASSERT_GE(factory1->mTCP.Length(), 1u); RefPtr conn1 = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false); factory1->mTCP[0]->FireSuccess(conn1); ASSERT_TRUE(hca1->WasTransactionAdoptedForTesting()); ASSERT_NE(realTrans->Connection(), nullptr) << "the winning attempt should have bound the transaction to its conn"; // --- HCA #2: races the same transaction and fails. --- RefPtr entry2 = new ConnectionEntry(ci, pendingQSet); RefPtr factory2 = new FakeConnectionEstablisherFactory(); RefPtr delegate2 = new RecordingConnMgrDelegate(); RefPtr hca2 = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); hca2->SetConnectionEstablisherFactoryForTesting(factory2); hca2->SetConnMgrDelegateForTesting(delegate2); hca2->Init(entry2); ASSERT_GE(factory2->mTCP.Length(), 1u); factory2->mTCP[0]->FireError(NS_ERROR_CONNECTION_REFUSED); EXPECT_TRUE(hca2->IsTerminal()); EXPECT_FALSE(realTrans->Closed()) << "the loser must not close a transaction already on the winner conn"; EXPECT_EQ(delegate2->Count("RemoveTransFromPendingQ"), 0) << "guard returns before touching the pending queue"; EXPECT_EQ(hca2->RealHttpTransaction(), nullptr) << "the loser drops its reference to the real transaction"; // Teardown: detach the connection and reset the handle the winner's // dispatch created so ~ConnectionHandle doesn't reclaim the fake conn. realTrans->SetConnection(nullptr); delegate1->ResetHandles(); }); } // ProcessTCPConn guard (bug 2048294), real-world race: a WebSocket/WebTransport // upgrade is raced by two HCAs. The first wins and dispatches it (Connection() // set). The second wins an H2 connection, whose ProcessTCPConn would normally // re-queue the upgrade for the extended CONNECT tunnel; but since it's already // on a connection it must NOT re-queue (re-dispatch would hit a transport-less // connection and crash in Activate). TEST(HappyEyeballsConnectionAttempt, ProcessTCPConnSkipsRequeueWhenConnected) { EnsureHttpHandler(); RunOnSocketThread([]() { RefPtr ci = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); RefPtr realTrans = new nsHttpTransaction(); // Makes deferExtendedConnect fire on an H2 connection. realTrans->SetIsForWebTransport(true); nsTHashSet pendingQSet; // --- HCA #1: wins on H1 and dispatches the shared transaction. --- RefPtr entry1 = new ConnectionEntry(ci, pendingQSet); RefPtr factory1 = new FakeConnectionEstablisherFactory(); RefPtr delegate1 = new RecordingConnMgrDelegate(); delegate1->mSimulateDispatchBindsConnection = true; delegate1->mFindResult = new PendingTransactionInfo(realTrans); RefPtr hca1 = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); hca1->SetConnectionEstablisherFactoryForTesting(factory1); hca1->SetConnMgrDelegateForTesting(delegate1); hca1->Init(entry1); ASSERT_GE(factory1->mTCP.Length(), 1u); RefPtr conn1 = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false); factory1->mTCP[0]->FireSuccess(conn1); ASSERT_NE(realTrans->Connection(), nullptr); // --- HCA #2: wins on H2; deferExtendedConnect must skip the re-queue. --- RefPtr entry2 = new ConnectionEntry(ci, pendingQSet); RefPtr factory2 = new FakeConnectionEstablisherFactory(); RefPtr delegate2 = new RecordingConnMgrDelegate(); RefPtr hca2 = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); hca2->SetConnectionEstablisherFactoryForTesting(factory2); hca2->SetConnMgrDelegateForTesting(delegate2); hca2->Init(entry2); ASSERT_GE(factory2->mTCP.Length(), 1u); RefPtr conn2 = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ true); factory2->mTCP[0]->FireSuccess(conn2); EXPECT_TRUE(hca2->IsTerminal()); EXPECT_EQ(delegate2->Count("AddTransaction"), 0) << "must not re-queue a transaction already on a connection"; EXPECT_EQ(delegate2->Count("FindTransaction"), 0) << "guard returns before touching the pending queue"; EXPECT_FALSE(realTrans->Closed()); EXPECT_EQ(hca2->RealHttpTransaction(), nullptr) << "deferExtendedConnect clears the loser's transaction reference"; realTrans->SetConnection(nullptr); delegate1->ResetHandles(); }); } // Bug 2051415: on H2->H1 fallback the connection manager abandons the stale // in-flight attempt. Verifies the property the fix relies on: Abandon() closes // the establisher, so a late success can't fire and re-queue the transaction. TEST(HappyEyeballsConnectionAttempt, AbandonClosesInFlightEstablisher) { EnsureHttpHandler(); RunOnSocketThread([]() { RefPtr ci = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); RefPtr realTrans = new nsHttpTransaction(); // Stand-in for the WebSocket upgrade. realTrans->SetIsForWebTransport(true); nsTHashSet pendingQSet; RefPtr entry = new ConnectionEntry(ci, pendingQSet); RefPtr factory = new FakeConnectionEstablisherFactory(); RefPtr delegate = new RecordingConnMgrDelegate(); RefPtr hca = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); hca->SetConnectionEstablisherFactoryForTesting(factory); hca->SetConnMgrDelegateForTesting(delegate); hca->Init(entry); ASSERT_GE(factory->mTCP.Length(), 1u); EXPECT_FALSE(hca->IsTerminal()); EXPECT_EQ(factory->mTCP[0]->mCloseCount, 0u) << "the establisher is still in flight before abandonment"; // The connection manager abandons the stale attempt on H2->H1 fallback. hca->Abandon(); EXPECT_TRUE(hca->IsTerminal()) << "an abandoned attempt is terminal and cannot reach ProcessTCPConn"; EXPECT_EQ(factory->mTCP[0]->mCloseCount, 1u) << "abandoning closes the in-flight establisher, so a late success " "can't fire and re-queue the restarted transaction (bug 2051415)"; EXPECT_FALSE(realTrans->Closed()) << "abandoning the attempt must not close the restarted transaction"; }); } // Bug 2051415, 0-RTT corner. The manager detaches the transaction // (ForgetRealTransaction) before abandoning, so EnterDone's "0-RTT started, no // winner" path can't re-queue it (cf. ZeroRttNoWinnerFallback, which does). TEST(HappyEyeballsConnectionAttempt, ForgetRealTransactionPreventsAbandonRequeue) { EnsureHttpHandler(); RunOnSocketThread([]() { TestHarness h(/*caps*/ 0, /*realTransaction*/ true); h.Init(); ASSERT_GE(h.mFactory->mTCP.Length(), 1u); // A racer entered 0-RTT but never won -- the case EnterDone re-queues. h.mHE->ZeroRttHandleForTesting()->SetAnyStartedForTesting(); // Manager takes over the transaction, detaching it before abandoning. h.mHE->ForgetRealTransaction(); h.mHE->Abandon(); EXPECT_TRUE(h.mHE->IsTerminal()); EXPECT_EQ(h.mDelegate->Count("AddTransaction"), 0) << "a detached, abandoned attempt must not re-queue the transaction " "the connection manager is restarting (bug 2051415)"; EXPECT_EQ(h.mFactory->mTCP[0]->mCloseCount, 1u) << "abandoning still closes the in-flight establisher"; }); } // 0-RTT winner-offset handling: several racers each advance the real // transaction's request stream to a different Request0RttStreamOffset (the // bytes that racer sent as early data). When one wins via Finish0RTT(accept), // ZeroRttHandle must seek the real request stream to the *winner's* offset (so // the real transaction resumes exactly where the winner left off) — not the // loser's. TEST(HappyEyeballsConnectionAttempt, ZeroRttWinnerUsesItsOwnStreamOffset) { EnsureHttpHandler(); RunOnSocketThread([]() { RefPtr ci = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); // The request head is held by weak reference inside the transaction, so it // must outlive realTrans (declared first). nsHttpRequestHead reqHead; reqHead.SetMethod("GET"_ns); reqHead.SetVersion(HttpVersion::v1_1); reqHead.SetRequestURI("/"_ns); RefPtr realTrans = new nsHttpTransaction(); nsresult rv = realTrans->Init( NS_HTTP_USE_HAPPY_EYEBALLS, ci, &reqHead, /*reqBody*/ nullptr, /*reqContentLength*/ 0, /*target*/ gSocketTransportService, /*callbacks*/ nullptr, /*eventsink*/ nullptr, /*browserId*/ 0, HttpTrafficCategory::eInvalid, /*requestContext*/ nullptr, ClassOfService(), /*initialRwin*/ 0, /*responseTimeoutEnabled*/ false, /*channelId*/ 0, /*transactionObserver*/ nullptr, nsILoadInfo::IPAddressSpace::Unknown, LNAPerms{}); ASSERT_TRUE(NS_SUCCEEDED(rv)); // The serialized GET request is the (seekable) request stream; pick two // distinct in-range offsets for the winning and losing racers. nsCOMPtr seekable = do_QueryInterface(realTrans->RequestStream()); ASSERT_TRUE(seekable); ASSERT_TRUE( NS_SUCCEEDED(seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0))); uint64_t streamLen = 0; ASSERT_TRUE( NS_SUCCEEDED(realTrans->RequestStream()->Available(&streamLen))); ASSERT_GE(streamLen, 4u); uint64_t winnerOffset = streamLen / 4; uint64_t loserOffset = streamLen / 2; ASSERT_NE(winnerOffset, loserOffset); RefPtr hca = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); ZeroRttHandle* zrh = hca->ZeroRttHandleForTesting(); ASSERT_TRUE(zrh); auto noStatus = [](nsITransport*, nsresult, int64_t) {}; auto noAuth = []() {}; RefPtr hetLoser = new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0, noStatus, noAuth, noAuth, zrh); RefPtr hetWinner = new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0, noStatus, noAuth, noAuth, zrh); // Each racer sent a different amount of early data. hetLoser->Request0RttStreamOffset() = Some(loserOffset); hetWinner->Request0RttStreamOffset() = Some(winnerOffset); // Finish0RTT -> AdoptWinner -> Adopt requires the winner to be on a // connection; an H2 fake makes Adopt reuse our ConnectionHandle (the // SwapTransaction is skipped because the fake isn't a real Http2Session). RefPtr winnerConn = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ true); RefPtr winnerHandle = new ConnectionHandle(winnerConn); hetWinner->SetConnection(winnerHandle); hetWinner->SetConnectedCallback([](nsresult) {}); // Winner accepts 0-RTT (restart=false). hetWinner->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false); int64_t pos = -1; ASSERT_TRUE(NS_SUCCEEDED(seekable->Tell(&pos))); EXPECT_EQ(static_cast(pos), winnerOffset) << "request stream must be seeked to the winner's 0-RTT offset, not " "the " "loser's"; // Teardown: detach + reset the handle so ~ConnectionHandle doesn't reclaim // the fake connection. realTrans->SetConnection(nullptr); winnerHandle->Reset(); }); } // Helper: a real, 0-RTT-capable transaction for the ZeroRttHandle tests. static already_AddRefed MakeRealTransaction( nsHttpConnectionInfo* aCi, nsHttpRequestHead* aReqHead) { aReqHead->SetMethod("GET"_ns); aReqHead->SetVersion(HttpVersion::v1_1); aReqHead->SetRequestURI("/"_ns); RefPtr trans = new nsHttpTransaction(); nsresult rv = trans->Init( NS_HTTP_USE_HAPPY_EYEBALLS, aCi, aReqHead, /*reqBody*/ nullptr, /*reqContentLength*/ 0, /*target*/ gSocketTransportService, /*callbacks*/ nullptr, /*eventsink*/ nullptr, /*browserId*/ 0, HttpTrafficCategory::eInvalid, /*requestContext*/ nullptr, ClassOfService(), /*initialRwin*/ 0, /*responseTimeoutEnabled*/ false, /*channelId*/ 0, /*transactionObserver*/ nullptr, nsILoadInfo::IPAddressSpace::Unknown, LNAPerms{}); MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); return trans.forget(); } // Regression for bug 2051776 (winner-then-loser). Once a winner is declared, // ZeroRttHandle::Cleanup() nulls mWinner and leaves mState == CleanedUp, so a // late Finish0RTT from a losing racer must be a no-op -- not trip the // MOZ_ASSERT(mState == State::Open) in ZeroRttHandle::Finish0RTT. TEST(HappyEyeballsConnectionAttempt, ZeroRttLateLoserFinish0RTTAfterWinner) { EnsureHttpHandler(); RunOnSocketThread([]() { RefPtr ci = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); // The request head is held by weak reference; it must outlive realTrans. nsHttpRequestHead reqHead; RefPtr realTrans = MakeRealTransaction(ci, &reqHead); RefPtr hca = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); ZeroRttHandle* zrh = hca->ZeroRttHandleForTesting(); ASSERT_TRUE(zrh); auto noStatus = [](nsITransport*, nsresult, int64_t) {}; auto noAuth = []() {}; RefPtr hetWinner = new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0, noStatus, noAuth, noAuth, zrh); RefPtr hetLoser = new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0, noStatus, noAuth, noAuth, zrh); hetWinner->Request0RttStreamOffset() = Some(0); hetLoser->Request0RttStreamOffset() = Some(0); RefPtr winnerConn = new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ true); RefPtr winnerHandle = new ConnectionHandle(winnerConn); hetWinner->SetConnection(winnerHandle); hetWinner->SetConnectedCallback([](nsresult) {}); // Declare the winner -> Cleanup() nulls mWinner, mState = CleanedUp. hetWinner->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false); // Late loser Finish0RTT after cleanup: must be a no-op (asserts before // fix). hetLoser->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false); EXPECT_FALSE(realTrans->Closed()); realTrans->SetConnection(nullptr); winnerHandle->Reset(); }); } // Regression for bug 2051776 (real-txn-gone path). The first Finish0RTT takes // the !realTxn branch and calls Cleanup() without ever declaring a winner // (mHadWinner stays false), so a second Finish0RTT must still be a no-op. Only // a state-based guard covers this path. TEST(HappyEyeballsConnectionAttempt, ZeroRttLateLoserFinish0RTTAfterRealTxnGone) { EnsureHttpHandler(); Preferences::SetBool("network.http.0rtt_force_txn_gone_for_testing", true); RunOnSocketThread([]() { RefPtr ci = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr, OriginAttributes(), /*endToEndSSL*/ true); nsHttpRequestHead reqHead; RefPtr realTrans = MakeRealTransaction(ci, &reqHead); RefPtr hca = new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0, /*speculative*/ false, /*urgentStart*/ false); ZeroRttHandle* zrh = hca->ZeroRttHandleForTesting(); ASSERT_TRUE(zrh); auto noStatus = [](nsITransport*, nsresult, int64_t) {}; auto noAuth = []() {}; RefPtr het1 = new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0, noStatus, noAuth, noAuth, zrh); RefPtr het2 = new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0, noStatus, noAuth, noAuth, zrh); het1->Request0RttStreamOffset() = Some(0); het2->Request0RttStreamOffset() = Some(0); // First Finish0RTT: the force pref makes the real txn "gone", taking the // !realTxn branch -> Cleanup() without declaring a winner. het1->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false); // Second Finish0RTT after that cleanup: must be a no-op (asserts before // fix). het2->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false); realTrans->SetConnection(nullptr); }); Preferences::ClearUser("network.http.0rtt_force_txn_gone_for_testing"); } } // namespace net } // namespace mozilla