Files
firefox/netwerk/protocol/http/HappyEyeballsConnectionAttempt.cpp

2264 lines
82 KiB
C++

/* 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/. */
// HttpLog.h should generally be included first
#include "HappyEyeballsConnectionAttempt.h"
#include <algorithm>
#include "ConnectionEntry.h"
#include "HttpConnectionUDP.h"
#include "HttpLog.h"
#include "NSSErrorsService.h"
#include "NetworkConnectivityService.h"
#include "PendingTransactionInfo.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/net/NeckoChannelParams.h"
#include "nsDNSService2.h"
#include "nsHttpConnectionMgr.h"
#include "nsHttpHandler.h"
#include "nsHttpTransaction.h"
#include "nsIDNSAdditionalInfo.h"
#include "nsIHttpActivityObserver.h"
#include "nsQueryObject.h"
#include "nsSocketTransport2.h"
#include "nsSocketTransportService2.h"
#include "sslerr.h"
// Log on level :5, instead of default :4.
#undef LOG
#define LOG(args) LOG5(args)
#undef LOG_ENABLED
#define LOG_ENABLED() LOG5_ENABLED()
namespace mozilla::net {
using happy_eyeballs::happy_eyeballs_process_connection_result;
using happy_eyeballs::happy_eyeballs_process_dns_response_a;
using happy_eyeballs::happy_eyeballs_process_dns_response_aaaa;
using happy_eyeballs::happy_eyeballs_process_dns_response_https;
using happy_eyeballs::happy_eyeballs_process_ech_retry;
using happy_eyeballs::happy_eyeballs_process_output;
static void NotifyConnectionActivity(nsHttpConnectionInfo* aConnInfo,
uint32_t aSubtype) {
HttpConnectionActivity activity(
aConnInfo->HashKey(), aConnInfo->GetOrigin(), aConnInfo->OriginPort(),
aConnInfo->EndToEndSSL(), !aConnInfo->GetEchConfig().IsEmpty(),
aConnInfo->IsHttp3());
gHttpHandler->ObserveHttpActivityWithArgs(
activity, NS_ACTIVITY_TYPE_HTTP_CONNECTION, aSubtype, PR_Now(), 0, ""_ns);
}
NS_IMPL_ADDREF_INHERITED(HappyEyeballsConnectionAttempt, ConnectionAttempt)
NS_IMPL_RELEASE_INHERITED(HappyEyeballsConnectionAttempt, ConnectionAttempt)
NS_INTERFACE_MAP_BEGIN(HappyEyeballsConnectionAttempt)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
NS_INTERFACE_MAP_ENTRY(nsINamed)
NS_INTERFACE_MAP_ENTRY(nsIDNSListener)
NS_INTERFACE_MAP_ENTRY_CONCRETE(HappyEyeballsConnectionAttempt)
NS_INTERFACE_MAP_END
// Production delegate: forwards verbatim to the connection manager / entry.
// Friended by nsHttpConnectionMgr to reach its private dispatch helpers.
class DefaultHappyEyeballsConnMgrDelegate final
: public HappyEyeballsConnMgrDelegate {
public:
already_AddRefed<PendingTransactionInfo> FindTransaction(
bool aRemoveWhenFound, ConnectionEntry* aEntry,
nsAHttpTransaction* aTrans) override {
return gHttpHandler->ConnMgr()->FindTransactionHelper(aRemoveWhenFound,
aEntry, aTrans);
}
nsresult DispatchTransaction(ConnectionEntry* aEntry,
nsHttpTransaction* aTrans,
HttpConnectionBase* aConn) override {
return gHttpHandler->ConnMgr()->DispatchTransaction(aEntry, aTrans, aConn);
}
void AddTransaction(nsHttpTransaction* aTrans, int32_t aPriority) override {
gHttpHandler->ConnMgr()->AddTransaction(aTrans, aPriority);
}
void ReportSpdyConnection(HttpConnectionBase* aConn, bool aUsingSpdy,
bool aDisallowHttp3) override {
if (RefPtr<nsHttpConnection> h1 = do_QueryObject(aConn)) {
gHttpHandler->ConnMgr()->ReportSpdyConnection(h1, aUsingSpdy,
aDisallowHttp3);
}
}
void ReportHttp3Connection(HttpConnectionBase* aConn,
ConnectionEntry* aEntry) override {
gHttpHandler->ConnMgr()->ReportHttp3Connection(aConn, aEntry);
}
void ReclaimConnection(HttpConnectionBase* aConn) override {
gHttpHandler->ConnMgr()->OnMsgReclaimConnection(aConn);
}
void ProcessSpdyPendingQ(ConnectionEntry* aEntry) override {
gHttpHandler->ConnMgr()->ProcessSpdyPendingQ(aEntry);
}
void InsertIntoActiveConns(ConnectionEntry* aEntry,
HttpConnectionBase* aConn) override {
aEntry->InsertIntoActiveConns(aConn);
}
void RemoveConnectionAttempt(ConnectionEntry* aEntry,
ConnectionAttempt* aAttempt,
bool aAbandon) override {
aEntry->RemoveConnectionAttempt(aAttempt, aAbandon);
}
void RecordIPFamilyPreference(ConnectionEntry* aEntry,
uint16_t aFamily) override {
aEntry->RecordIPFamilyPreference(aFamily);
}
void ResetIPFamilyPreference(ConnectionEntry* aEntry) override {
aEntry->ResetIPFamilyPreference();
}
bool MaybeProcessCoalescingKeys(ConnectionEntry* aEntry,
const nsTArray<NetAddr>& aAddresses,
bool aIsHttp3) override {
return aEntry->MaybeProcessCoalescingKeys(aAddresses, aIsHttp3);
}
bool RemoveTransFromPendingQ(ConnectionEntry* aEntry,
nsHttpTransaction* aTrans) override {
return aEntry->RemoveTransFromPendingQ(aTrans);
}
nsresult StartRetry(ConnectionEntry* aEntry, nsHttpTransaction* aTrans,
uint32_t aCaps, bool aSpeculative, bool aUrgentStart,
bool aAllow1918, bool aRetryWithoutTRR) override {
RefPtr<PendingTransactionInfo> pendingTransInfo =
new PendingTransactionInfo(aTrans);
return aEntry->CreateDnsAndConnectSocket(
aTrans, aCaps, aSpeculative, aUrgentStart, aAllow1918, pendingTransInfo,
aRetryWithoutTRR);
}
};
HappyEyeballsConnectionAttempt::HappyEyeballsConnectionAttempt(
nsHttpConnectionInfo* ci, nsAHttpTransaction* trans, uint32_t caps,
bool speculative, bool urgentStart, bool retryWithoutTRR)
: ConnectionAttempt(ci, trans, caps, speculative, urgentStart),
mEstablisherFactory(new DefaultConnectionEstablisherFactory()),
mConnMgrDelegate(new DefaultHappyEyeballsConnMgrDelegate()),
mRetryWithoutTRR(retryWithoutTRR),
mZeroRttHandle(new ZeroRttHandle(this)) {
LOG(("HappyEyeballsConnectionAttempt ctor %p retryWithoutTRR=%d", this,
retryWithoutTRR));
// mHost is the origin host: the base Happy Eyeballs resolves and falls back
// to. An alt-svc route's alternate host is passed to Happy Eyeballs as an
// alt-svc entry (see CreateHappyEyeballs) so it is attempted ahead of, but
// still falls back to, the origin.
mHost = mConnInfo->GetOrigin();
NotifyConnectionActivity(
mConnInfo, mSpeculative
? NS_HTTP_ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED
: NS_HTTP_ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED);
}
HappyEyeballsConnectionAttempt::~HappyEyeballsConnectionAttempt() {
LOG(("HappyEyeballsConnectionAttempt dtor %p", this));
}
// Returns true if the network connectivity service has positively determined
// that IPv6 is unreachable on the current network while IPv4 works. The UNKNOWN
// state (probes still in flight) is intentionally treated as "no signal" so we
// stay optimistic.
static bool ShouldPreferIPv4DueToNoIPv6Connectivity() {
RefPtr<NetworkConnectivityService> ncs =
NetworkConnectivityService::GetSingleton();
if (!ncs) {
return false;
}
return ncs->GetIPv6() == nsINetworkConnectivityService::NOT_AVAILABLE &&
ncs->GetIPv4() == nsINetworkConnectivityService::OK;
}
nsresult HappyEyeballsConnectionAttempt::CreateHappyEyeballs(
ConnectionEntry* ent) {
happy_eyeballs::IpPreference ipPref =
happy_eyeballs::IpPreference::DualStackPreferV6;
if (mConnInfo->GetIPv6Disabled()) {
ipPref = happy_eyeballs::IpPreference::Ipv4Only;
} else if (ent->PreferenceKnown() && ent->mPreferIPv4) {
ipPref = happy_eyeballs::IpPreference::DualStackPreferV4;
} else if (!ent->PreferenceKnown() &&
ShouldPreferIPv4DueToNoIPv6Connectivity()) {
ipPref = happy_eyeballs::IpPreference::DualStackPreferV4;
}
// Restrict the protocols the Happy Eyeballs engine may attempt to those
// enabled by prefs, so disabled protocols are never raced (from HTTPS
// records, IP hints, or alt-svc). NS_HTTP_DISALLOW_HTTP3 is set for
// transactions that can't use HTTP/3 (e.g. WebSocket upgrades), so honor it
// here too. An http3Only conn info (eager Alt-Svc h3 validation) must not
// race h1/h2: the point is to warm an h3 connection, and racing could settle
// on h2 instead.
const bool http3Only = mConnInfo->GetHttp3Only();
happy_eyeballs::HttpVersions httpVersions{
/* h1 */ !http3Only,
/* h2 */ !http3Only && StaticPrefs::network_http_http2_enabled(),
/* h3 */ nsHttpHandler::IsHttp3Enabled() &&
!(mCaps & NS_HTTP_DISALLOW_HTTP3),
};
LOG(("CreateHappyEyeballs ipPref=%d", static_cast<uint32_t>(ipPref)));
// Happy Eyeballs always resolves and races the origin (mHost / origin port)
// as the baseline. An alt-svc route is expressed as an alt-svc entry that
// names the alternate host, port and protocol, so the engine attempts the
// alternate ahead of the origin but still falls back to the origin if the
// alternate is unreachable (bug 2052534).
const uint16_t originPort = static_cast<uint16_t>(mConnInfo->OriginPort());
const nsCString& routedHost = mConnInfo->GetRoutedHost();
// Add an alt-svc entry when the connection must race HTTP/3 (an explicit
// HTTP/3 connection info: an alt-svc HTTP/3 route, or a direct HTTP/3
// connection such as WebTransport) or when there is an HTTP/2 alt-svc route.
// A routed host names the alternate; without one (a direct HTTP/3 connection)
// the entry carries only the protocol and the state machine races the origin
// over HTTP/3 at the origin port.
nsTArray<happy_eyeballs::AltSvc> altSvcArray;
const bool isHttp3 = mConnInfo->IsHttp3();
if (isHttp3 || !routedHost.IsEmpty()) {
happy_eyeballs::AltSvc altsvc{};
altsvc.http_version = isHttp3 ? happy_eyeballs::HttpVersion::H3
: happy_eyeballs::HttpVersion::H2;
if (!routedHost.IsEmpty()) {
altsvc.host = routedHost;
altsvc.port = static_cast<uint16_t>(mConnInfo->RoutedPort());
}
altSvcArray.AppendElement(std::move(altsvc));
}
return HappyEyeballs::Init(getter_AddRefs(mHappyEyeballs), mHost, originPort,
&altSvcArray, ipPref, httpVersions);
}
nsresult HappyEyeballsConnectionAttempt::Init(ConnectionEntry* ent) {
mEntry = ent;
nsresult rv = CreateHappyEyeballs(ent);
if (NS_FAILED(rv)) {
return rv;
}
Transition(State::Connecting);
return ProcessHappyEyeballsOutput();
}
static Result<NetAddr, nsresult> ToNetAddr(
const happy_eyeballs::IpAddr& aIpAddr, uint16_t aPort) {
NetAddr addr;
memset(&addr, 0, sizeof(NetAddr));
uint16_t port = htons(aPort);
switch (aIpAddr.tag) {
case happy_eyeballs::IpAddr::Tag::V4:
addr.inet.family = AF_INET;
addr.inet.port = port;
memcpy(&addr.inet.ip, aIpAddr.v4._0, 4);
break;
case happy_eyeballs::IpAddr::Tag::V6:
addr.inet6.family = AF_INET6;
addr.inet6.port = port;
memcpy(&addr.inet6.ip, aIpAddr.v6._0, 16);
break;
default:
return Err(NS_ERROR_UNEXPECTED);
}
return addr;
}
HappyEyeballsConnectionAttempt::ConnResultOutcome
HappyEyeballsConnectionAttempt::ClassifyConnectionResult(
nsresult aStatus) const {
if (PossibleZeroRTTRetryError(aStatus)) {
return ConnResultOutcome::RestartTransaction;
}
// NET_RESET after 0-RTT was sent: the server rejected the early data
// (stale PSK ticket, mismatched QUIC transport params, H3 protocol
// error, TCP RST on TLS 1.3 early data, …). Restart on the same
// endpoint without 0-RTT rather than letting Restart()'s default
// CloneAsDirectRoute() strip the alt-svc / H3 route.
if (aStatus == NS_ERROR_NET_RESET && mZeroRttHandle->AnyStarted()) {
return ConnResultOutcome::RestartTransaction;
}
// Local Network Access denial: the target IP space is not permitted.
// No address for this server will succeed; stop all attempts.
if (aStatus == NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED) {
return ConnResultOutcome::AbortTransaction;
}
// NSS / TLS errors are server-state-specific (cert verification, PSK
// resumption alert, transport-level alert during handshake, ...).
// Trying another resolved address won't help — they'll all fail the same way.
if (NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_SECURITY) {
return ConnResultOutcome::AbortTransaction;
}
return ConnResultOutcome::ForwardAndContinue;
}
void HappyEyeballsConnectionAttempt::ReleaseRealTransaction(
nsresult aCloseReason, ConnectionEntry* aEntry) {
if (!mTransaction) {
return;
}
// Adopted: the carrier's stream still references the transaction;
// closing it here would null mConnection mid-flight (bug 2040246).
// Just drop our ref and let the carrier drive it.
if (mTransactionAdopted) {
LOG(
("HappyEyeballsConnectionAttempt::ReleaseRealTransaction %p skipping "
"Close — real transaction already adopted",
this));
mTransaction = nullptr;
return;
}
if (nsHttpTransaction* trans = mTransaction->QueryHttpTransaction()) {
// Another attempt may have won and dispatched this still-pending
// transaction onto its connection (e.g. via DispatchSpdyPendingQ). Once it
// has a connection it's no longer ours to close.
if (trans->Connection()) {
LOG(
("HappyEyeballsConnectionAttempt::ReleaseRealTransaction %p skipping "
"Close — real transaction already dispatched to a connection",
this));
mTransaction = nullptr;
return;
}
if (aEntry) {
mConnMgrDelegate->RemoveTransFromPendingQ(aEntry, trans);
}
}
mTransaction->Close(aCloseReason);
// Null out so later paths can't race with the Restart() / AddTransaction
// that Close just triggered.
mTransaction = nullptr;
}
nsresult HappyEyeballsConnectionAttempt::ProcessConnectionResult(
const NetAddr& aAddr, nsresult aStatus, uint64_t aId) {
LOG(
("HappyEyeballsConnectionAttempt::ProcessConnectionResult %p addr=[%s] "
"id=%" PRIu64 " aStatus=%x",
this, aAddr.ToString().get(), aId, static_cast<uint32_t>(aStatus)));
RefPtr<HappyEyeballsConnectionAttempt> self(this);
// Late establisher results can arrive after we've wound down. No-op.
if (IsTerminal()) {
return NS_OK;
}
if (mPausedForClientAuth && aId == mClientAuthHolderId) {
mPausedForClientAuth = false;
mClientAuthHolderId = 0;
}
if (mState != State::ProcessingConnectionResult) {
Transition(State::ProcessingConnectionResult);
}
ConnResultOutcome outcome = ClassifyConnectionResult(aStatus);
switch (outcome) {
case ConnResultOutcome::RestartTransaction: {
TransitionPayload payload;
payload.mCloseReason = aStatus;
Transition(State::RestartTransaction, std::move(payload));
return NS_OK;
}
case ConnResultOutcome::AbortTransaction: {
nsresult closeReason = aStatus;
if (NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_SECURITY) {
PRErrorCode prCode =
-static_cast<PRErrorCode>(NS_ERROR_GET_CODE(aStatus));
if (!mozilla::psm::IsNSSErrorCode(prCode)) {
// NSPR-base error (e.g. PR_END_OF_FILE_ERROR): translate to the
// network-module nsresult that nsSocketTransport would have
// produced.
closeReason = ErrorAccordingToNSPR(prCode);
}
}
TransitionPayload payload;
payload.mCloseReason = closeReason;
Transition(State::AbortTransaction, std::move(payload));
return NS_OK;
}
case ConnResultOutcome::ForwardAndContinue:
break;
}
if (NS_FAILED(aStatus)) {
mLastConnectionError = aStatus;
}
nsresult rv =
happy_eyeballs_process_connection_result(mHappyEyeballs, aId, aStatus);
if (NS_FAILED(rv)) {
LOG(("process_connection_result failed rv=%x", static_cast<uint32_t>(rv)));
}
rv = ProcessHappyEyeballsOutput();
// If drain didn't reach a terminal state, recompute activity state.
if (mState == State::ProcessingConnectionResult) {
if (mZeroRttHandle->AnyStarted() && !mZeroRttHandle->HadWinner()) {
Transition(State::ZeroRttRacing);
} else {
Transition(State::Connecting);
}
}
return rv;
}
Maybe<nsCString> HappyEyeballsConnectionAttempt::MaybeExtractRetryEchConfig(
ConnectionEstablisher* aEstablisher, nsresult aStatus) {
if (aStatus == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITHOUT_ECH)) {
LOG(
("HappyEyeballsConnectionAttempt::MaybeExtractRetryEchConfig %p "
"SSL_ERROR_ECH_RETRY_WITHOUT_ECH",
this));
return Some(nsCString());
}
if (aStatus != psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH)) {
return Nothing();
}
RefPtr<HttpConnectionBase> conn =
aEstablisher ? aEstablisher->ResultConn() : nullptr;
if (!conn) {
return Nothing();
}
// H1/H2: NSS retry_configs aren't queryable from this callback; use the
// bytes cached by nsHttpConnection::PostProcessNPNSetup. H3: read from
// QuicSocketControl, populated by Http3Session::ProcessEvents.
nsAutoCString retryEchConfig;
if (RefPtr<nsHttpConnection> httpConn = do_QueryObject(conn)) {
retryEchConfig = httpConn->CachedRetryEchConfig();
} else {
nsCOMPtr<nsITLSSocketControl> tlsCtrl;
conn->GetTLSSocketControl(getter_AddRefs(tlsCtrl));
if (tlsCtrl && NS_FAILED(tlsCtrl->GetRetryEchConfig(retryEchConfig))) {
return Nothing();
}
}
if (retryEchConfig.IsEmpty()) {
return Nothing();
}
LOG(
("HappyEyeballsConnectionAttempt::MaybeExtractRetryEchConfig %p "
"SSL_ERROR_ECH_RETRY_WITH_ECH retryEchConfig.len=%zu",
this, retryEchConfig.Length()));
return Some(nsCString(retryEchConfig));
}
nsresult HappyEyeballsConnectionAttempt::ProcessEchRetryConnectionResult(
const NetAddr& aAddr, uint64_t aId, const nsACString& aEchBytes) {
LOG(
("HappyEyeballsConnectionAttempt::ProcessEchRetryConnectionResult %p "
"addr=[%s] id=%" PRIu64 " ech.len=%zu",
this, aAddr.ToString().get(), aId, aEchBytes.Length()));
RefPtr<HappyEyeballsConnectionAttempt> self(this);
if (IsTerminal()) {
return NS_OK;
}
if (mState != State::ProcessingConnectionResult) {
Transition(State::ProcessingConnectionResult);
}
nsTArray<uint8_t> echBytes;
echBytes.AppendElements(
reinterpret_cast<const uint8_t*>(aEchBytes.BeginReading()),
aEchBytes.Length());
nsresult rv =
happy_eyeballs_process_ech_retry(mHappyEyeballs, aId, &echBytes);
if (NS_FAILED(rv)) {
LOG(("process_ech_retry failed rv=%x", static_cast<uint32_t>(rv)));
}
rv = ProcessHappyEyeballsOutput();
if (mState == State::ProcessingConnectionResult) {
if (mZeroRttHandle->AnyStarted() && !mZeroRttHandle->HadWinner()) {
Transition(State::ZeroRttRacing);
} else {
Transition(State::Connecting);
}
}
return rv;
}
void HappyEyeballsConnectionAttempt::DnsLookupTimings(TimeStamp& aStart,
TimeStamp& aEnd) const {
aStart = mFirstDnsLookupStart;
aEnd = mDnsResolutionEnd.IsNull() ? mFirstConnectionStart : mDnsResolutionEnd;
}
void HappyEyeballsConnectionAttempt::FillConnectTimings(
bool aIsQuic, TimingStruct& aTimings) const {
aTimings.connectStart = mFirstConnectionStart;
aTimings.connectEnd = mFirstConnectEnd;
if (aIsQuic) {
// QUIC has no separate TCP handshake; secureConnectionStart coincides
// with the connection start.
aTimings.secureConnectionStart = mFirstConnectionStart;
} else {
aTimings.tcpConnectEnd = mFirstTcpConnectEnd;
aTimings.secureConnectionStart = mFirstSecureConnectionStart;
}
}
nsresult HappyEyeballsConnectionAttempt::ProcessHappyEyeballsOutput() {
LOG(("HappyEyeballsConnectionAttempt::ProcessHappyEyeballsOutput %p", this));
if (IsTerminal()) {
return NS_OK;
}
// Paused on a client-cert prompt: stop polling so no new attempts start
// (running ones continue). Resumed by OnClientAuthCertificateSelected or
// the holder's result.
if (mPausedForClientAuth) {
LOG((" paused for client-auth (holder id=%" PRIu64 "); not polling",
mClientAuthHolderId));
return NS_OK;
}
nsresult rv = NS_OK;
while (!IsTerminal()) {
happy_eyeballs::Output event{};
nsTArray<uint8_t> echConfig;
nsCString dnsHostname;
rv = happy_eyeballs_process_output(mHappyEyeballs, &event, &echConfig,
&dnsHostname);
if (NS_FAILED(rv)) {
LOG(("process_output failed rv=%x", static_cast<uint32_t>(rv)));
return rv;
}
switch (event.tag) {
case happy_eyeballs::Output::Tag::SendDnsQuery: {
LOG(("HappyEyeballsEvent::Tag::SendDnsQuery id=%" PRIu64 " hostname=%s",
event.send_dns_query.id, dnsHostname.get()));
DNSLookup(event.send_dns_query.record_type,
SetupDnsFlags(event.send_dns_query.record_type),
event.send_dns_query.id, dnsHostname);
break;
}
case happy_eyeballs::Output::Tag::Timer: {
SetupTimer(event.timer.duration_ms);
return NS_OK;
}
case happy_eyeballs::Output::Tag::AttemptConnection: {
LOG(("HappyEyeballsEvent::Tag::AttemptConnection id=%" PRIu64
" protocol=%d port=%d ",
event.attempt_connection.id,
static_cast<uint32_t>(event.attempt_connection.http_version),
event.attempt_connection.port));
if (mFirstConnectionStart.IsNull()) {
mFirstConnectionStart = TimeStamp::Now();
// If no DNS response arrived before the first connection attempt,
// report domainLookupEnd as connectStart (matches Chrome's
// null-dns_resolution_end fallback).
if (mDnsResolutionEnd.IsNull()) {
mDnsResolutionEnd = mFirstConnectionStart;
}
}
auto res = ToNetAddr(event.attempt_connection.addr,
event.attempt_connection.port);
if (res.isErr()) {
LOG(("Failed to convert to NetAddr"));
// TODO: how to handle this error?
MOZ_ASSERT(false, "Failed to convert to NetAddr");
return res.unwrapErr();
}
LOG(("connect to:[%s] ech_config_len=%zu",
res.unwrap().ToString().get(), echConfig.Length()));
bool isEchRetry = event.attempt_connection.is_ech_retry;
if (event.attempt_connection.http_version ==
happy_eyeballs::ConnectionAttemptHttpVersions::H3) {
EstablishUDPConnection(res.unwrap(), event.attempt_connection.port,
std::move(echConfig),
event.attempt_connection.id, isEchRetry);
} else {
EstablishTCPConnection(res.unwrap(), event.attempt_connection.port,
std::move(echConfig),
event.attempt_connection.id, isEchRetry);
}
break;
}
case happy_eyeballs::Output::Tag::CancelConnection: {
LOG(("CancelConnection id=%" PRIu64, event.cancel_connection.id));
CancelConnection(event.cancel_connection.id);
break;
}
case happy_eyeballs::Output::Tag::Succeeded:
LOG(("happy_eyeballs::Output::Tag::Succeeded"));
Transition(State::Succeeded);
return NS_OK;
case happy_eyeballs::Output::Tag::Failed: {
LOG(("happy_eyeballs::Output::Tag::Failed reason=%d",
static_cast<uint32_t>(event.failed.reason)));
if (ShouldRetryWithoutTRR(event.failed.reason)) {
RetryWithoutTRR();
return NS_OK;
}
if (ShouldRetryForLocalAddress(event.failed.reason)) {
RetryForLocalAddress();
return NS_OK;
}
TransitionPayload payload;
payload.mFailureReason = Some(event.failed.reason);
Transition(State::Failed, std::move(payload));
return NS_OK;
}
case happy_eyeballs::Output::Tag::None:
LOG(("happy_eyeballs::Output::Tag::None"));
// No more events to process
return NS_OK;
}
}
return NS_OK;
}
Result<nsIDNSService::DNSFlags, nsresult>
HappyEyeballsConnectionAttempt::SetupDnsFlags(
happy_eyeballs::DnsRecordType aType) {
LOG(("HappyEyeballsConnectionAttempt::SetupDnsFlags [this=%p aType=%d] ",
this, static_cast<uint32_t>(aType)));
nsIDNSService::DNSFlags dnsFlags = nsIDNSService::RESOLVE_DEFAULT_FLAGS;
if (mCaps & NS_HTTP_REFRESH_DNS) {
dnsFlags = nsIDNSService::RESOLVE_BYPASS_CACHE;
}
// Fallback attempt after TRR-resolved addresses failed to connect: bypass TRR
// and the (TRR-populated) cache to re-resolve natively.
if (mRetryWithoutTRR) {
dnsFlags |= nsIDNSService::RESOLVE_DISABLE_TRR |
nsIDNSService::RESOLVE_BYPASS_CACHE |
nsIDNSService::RESOLVE_REFRESH_CACHE;
}
switch (aType) {
case happy_eyeballs::DnsRecordType::Https:
if (!mRetryWithoutTRR) {
dnsFlags |= nsIDNSService::GetFlagsFromTRRMode(mConnInfo->GetTRRMode());
}
return dnsFlags;
case happy_eyeballs::DnsRecordType::Aaaa:
if (mCaps & NS_HTTP_DISABLE_IPV6) {
return Err(NS_ERROR_NOT_AVAILABLE);
}
dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV4;
break;
case happy_eyeballs::DnsRecordType::A:
if (mCaps & NS_HTTP_DISABLE_IPV4) {
return Err(NS_ERROR_NOT_AVAILABLE);
}
dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV6;
break;
}
if (!mRetryWithoutTRR) {
dnsFlags |=
nsIDNSService::GetFlagsFromTRRMode(NS_HTTP_TRR_MODE_FROM_FLAGS(mCaps));
}
// When we get here, we are not resolving using any configured proxy likely
// because of individual proxy setting on the request or because the host is
// excluded from proxying. Hence, force resolution despite global proxy-DNS
// configuration.
dnsFlags |= nsIDNSService::RESOLVE_IGNORE_SOCKS_DNS;
NS_ASSERTION(!(dnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV6) ||
!(dnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV4),
"Setting both RESOLVE_DISABLE_IPV6 and RESOLVE_DISABLE_IPV4");
LOG(("dnsFlags=%u", dnsFlags));
return dnsFlags;
}
void HappyEyeballsConnectionAttempt::MaybeSendTransportStatus(
nsresult aStatus, nsITransport* aTransport, int64_t aProgress) {
// Capture the first racer to reach each connect milestone across all racers.
// connectStart (mFirstConnectionStart) and connectEnd (mFirstConnectEnd) are
// set elsewhere.
if (aStatus == NS_NET_STATUS_CONNECTED_TO && mFirstTcpConnectEnd.IsNull()) {
mFirstTcpConnectEnd = TimeStamp::Now();
} else if (aStatus == NS_NET_STATUS_TLS_HANDSHAKE_STARTING &&
mFirstSecureConnectionStart.IsNull()) {
mFirstSecureConnectionStart = TimeStamp::Now();
}
if (!mSentTransportStatuses.EnsureInserted(static_cast<uint32_t>(aStatus)) ||
!mTransaction) {
return;
}
// Skip forwarding to NullTransaction/SpeculativeTransaction. They fire the
// activity distributor themselves, causing duplicate events. The statuses
// will be replayed to the real transaction when Claim() replaces it.
if (mTransaction->IsNullTransaction()) {
return;
}
mTransaction->OnTransportStatus(aTransport, aStatus, aProgress);
}
nsresult HappyEyeballsConnectionAttempt::CheckLNA(
nsISocketTransport* aTransport) {
if (!aTransport) {
return NS_OK;
}
NetAddr peerAddr;
if (NS_FAILED(aTransport->GetPeerAddr(&peerAddr))) {
return NS_OK;
}
return CheckLNAForAddr(peerAddr);
}
nsresult HappyEyeballsConnectionAttempt::CheckLNAForAddr(const NetAddr& aAddr) {
if (!mConnInfo->FirstHopSSL() || mConnInfo->UsingProxy()) {
return NS_OK;
}
auto addrSpace = aAddr.GetIpAddressSpace();
// Local targets are always checked pre-TLS. Private targets on HTTPS can
// be deferred until after the TLS handshake succeeds (see
// nsHttpConnection::HandshakeDoneInternal) when
// network.lna.defer_https_check is set.
// This is in order to prevent unactionable LNA prompts when captive portals
// temporarily intercept DNS - See bug 2017712.
bool deferPrivate = addrSpace == nsILoadInfo::IPAddressSpace::Private &&
StaticPrefs::network_lna_defer_https_check();
if (addrSpace != nsILoadInfo::IPAddressSpace::Local &&
(addrSpace != nsILoadInfo::IPAddressSpace::Private || deferPrivate)) {
return NS_OK;
}
if (mTransaction &&
!mTransaction->AllowedToConnectToIpAddressSpace(addrSpace)) {
LOG((
"HappyEyeballsConnectionAttempt::CheckLNAForAddr %p "
"blocking connection to %s address space",
this,
addrSpace == nsILoadInfo::IPAddressSpace::Local ? "local" : "private"));
return NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED;
}
return NS_OK;
}
void HappyEyeballsConnectionAttempt::DNSLookup(
happy_eyeballs::DnsRecordType aType,
Result<nsIDNSService::DNSFlags, nsresult> aFlags, uint64_t aId,
const nsACString& aHostname) {
// Track A/AAAA lookups for the origin host (mHost) so the coalescing keys are
// built from their combined addresses once both finish. Lookups for an HTTPS
// RR target name could use a different hostname and are intentionally
// excluded. The id is removed in OnARecord/OnAAAARecord, which run on both
// the success and failure paths.
if ((aType == happy_eyeballs::DnsRecordType::A ||
aType == happy_eyeballs::DnsRecordType::Aaaa) &&
aHostname.Equals(mHost)) {
mOriginDnsLookupIds.Insert(aId);
}
nsCOMPtr<nsIDNSService> dns = aFlags.isOk() ? GetOrInitDNSService() : nullptr;
if (dns) {
if (mFirstDnsLookupStart.IsNull()) {
mFirstDnsLookupStart = TimeStamp::Now();
}
MaybeSendTransportStatus(NS_NET_STATUS_RESOLVING_HOST);
}
RefPtr<DnsRequestInfo> requestInfo = new DnsRequestInfo(aId, aType);
nsCOMPtr<nsICancelable> request;
nsresult rv = NS_ERROR_UNEXPECTED;
if (dns) {
nsIDNSService::DNSFlags flags = aFlags.unwrap();
switch (aType) {
case happy_eyeballs::DnsRecordType::Https: {
// Skip the HTTPS RR lookup in two cases:
// 1. Plain-HTTP origin (!FirstHopSSL). HTTPS RR is an HTTPS-upgrade
// mechanism; nsHttpChannel normally does the upgrade before
// creating connection, but some edge cases slip through with an
// http:// conn info.
// 2. NS_HTTP_DISALLOW_HTTPS_RR is set.
// Setting rv = NS_ERROR_NOT_AVAILABLE feeds an empty HTTPS RR to the
// state machine instead of issuing a query.
if (!mConnInfo->FirstHopSSL() ||
(!StaticPrefs::network_dns_force_use_https_rr() &&
(mCaps & NS_HTTP_DISALLOW_HTTPS_RR))) {
rv = NS_ERROR_NOT_AVAILABLE;
} else {
nsCOMPtr<nsIDNSAdditionalInfo> info;
if (mConnInfo->OriginPort() != NS_HTTPS_DEFAULT_PORT) {
dns->NewAdditionalInfo(""_ns, mConnInfo->OriginPort(),
getter_AddRefs(info));
}
rv = dns->AsyncResolveNative(
aHostname, nsIDNSService::RESOLVE_TYPE_HTTPSSVC,
flags | nsIDNSService::RESOLVE_WANT_RECORD_ON_ERROR, info, this,
gSocketTransportService, mConnInfo->GetOriginAttributes(),
getter_AddRefs(request));
}
break;
}
case happy_eyeballs::DnsRecordType::Aaaa:
rv = dns->AsyncResolveNative(
aHostname, nsIDNSService::RESOLVE_TYPE_DEFAULT,
flags | nsIDNSService::RESOLVE_WANT_RECORD_ON_ERROR, nullptr, this,
gSocketTransportService, mConnInfo->GetOriginAttributes(),
getter_AddRefs(request));
break;
case happy_eyeballs::DnsRecordType::A:
rv = dns->AsyncResolveNative(
aHostname, nsIDNSService::RESOLVE_TYPE_DEFAULT,
flags | nsIDNSService::RESOLVE_WANT_RECORD_ON_ERROR, nullptr, this,
gSocketTransportService, mConnInfo->GetOriginAttributes(),
getter_AddRefs(request));
break;
}
}
if (NS_SUCCEEDED(rv) && request) {
requestInfo->SetRequest(request);
mDnsRequestTable.InsertOrUpdate(request, requestInfo);
return;
}
// Notify the state machine about DNS failure asynchronously.
NS_DispatchToCurrentThread(
NS_NewRunnableFunction("HappyEyeballsConnectionAttempt::DNSLookup",
[self = RefPtr{this}, rv, aType, aId]() {
switch (aType) {
case happy_eyeballs::DnsRecordType::Https:
(void)self->OnHTTPSRecord(nullptr, rv, aId);
break;
case happy_eyeballs::DnsRecordType::Aaaa:
(void)self->OnAAAARecord(nullptr, rv, aId);
break;
case happy_eyeballs::DnsRecordType::A:
(void)self->OnARecord(nullptr, rv, aId);
break;
}
}));
}
void HappyEyeballsConnectionAttempt::MaybeForward0RTTSecurityInfo(
ConnectionEstablisher* aEstablisher) {
if (!mZeroRttHandle->AnyStarted()) {
return;
}
RefPtr<HttpConnectionBase> conn = aEstablisher->ResultConn();
if (!conn) {
return;
}
nsCOMPtr<nsITLSSocketControl> tlsCtrl;
conn->GetTLSSocketControl(getter_AddRefs(tlsCtrl));
nsCOMPtr<nsITransportSecurityInfo> secInfo;
if (tlsCtrl) {
tlsCtrl->GetSecurityInfo(getter_AddRefs(secInfo));
}
if (secInfo && mTransaction) {
if (nsHttpTransaction* trans = mTransaction->QueryHttpTransaction()) {
trans->SetSecurityInfo(secInfo);
}
}
}
void HappyEyeballsConnectionAttempt::HandleConnectionResult(
Result<RefPtr<HttpConnectionBase>, nsresult> aResult,
ConnectionEstablisher* aEstablisher, uint64_t aId) {
RefPtr<ConnectionEstablisher> establisher = aEstablisher;
mConnectionEstablisherTable.Remove(aId);
NetAddr addr = establisher->Addr();
// A local establisher refuses synchronously in Start() and never reaches
// here, so this means an establisher actually attempted a connection.
mAllAttemptsRefusedLocal = false;
LOG((
"HappyEyeballsConnectionAttempt::HandleConnectionResult %p addr=[%s] "
"family=[%d] id=%" PRIu64 " isUDP=%d",
this, addr.ToString().get(), addr.raw.family, aId, establisher->IsUDP()));
if (aResult.isErr()) {
nsresult status = aResult.unwrapErr();
MaybeForward0RTTSecurityInfo(establisher);
Maybe<nsCString> retryEch = MaybeExtractRetryEchConfig(establisher, status);
establisher->Close(status);
if (retryEch) {
ProcessEchRetryConnectionResult(addr, aId, *retryEch);
} else {
ProcessConnectionResult(addr, status, aId);
}
return;
}
if (IsTerminal()) {
establisher->Close(NS_BASE_STREAM_CLOSED);
ProcessConnectionResult(addr, NS_BASE_STREAM_CLOSED, aId);
return;
}
mOutputConn = aResult.unwrap();
mOutputTrans = establisher->Transaction();
mOutputConnId = aId;
mAddrFamily = addr.raw.family;
// The winner is the first connection to fully succeed.
mFirstConnectEnd = TimeStamp::Now();
// The ownership of connection is moved to HappyEyeballsConnectionAttempt now.
establisher->ClearResultConnection();
ProcessConnectionResult(addr, NS_OK, aId);
}
void HappyEyeballsConnectionAttempt::AdoptWinner(
HappyEyeballsTransaction* aWinner) {
MOZ_ASSERT(OnSocketThread());
if (!aWinner || aWinner->IsAdopted()) {
return;
}
nsHttpTransaction* realTransaction = RealHttpTransaction();
if (!realTransaction) {
LOG(
("HappyEyeballsConnectionAttempt::AdoptWinner %p no real transaction; "
"closing winner=%p",
this, aWinner));
aWinner->Close(NS_ERROR_ABORT);
return;
}
// The trans must have been removed from the pending queue by
// LockInRealTransactionFromPendingQueue at Do0RTT time.
#ifdef DEBUG
{
RefPtr<ConnectionEntry> entry(mEntry);
if (entry) {
RefPtr<PendingTransactionInfo> pendingInfo =
mConnMgrDelegate->FindTransaction(
/*removeWhenFound*/ false, entry, realTransaction);
MOZ_ASSERT(
!pendingInfo,
"real transaction must have been removed from the pending queue "
"by LockInRealTransactionFromPendingQueue");
}
}
#endif
aWinner->Adopt(realTransaction);
// Real transaction now lives on the carrier's stream — disarm
// ReleaseRealTransaction for the rest of this HCA's lifetime (bug 2040246).
mTransactionAdopted = true;
}
bool HappyEyeballsConnectionAttempt::LockInRealTransactionFromPendingQueue() {
nsHttpTransaction* realTransaction = RealHttpTransaction();
if (!realTransaction) {
return false;
}
RefPtr<ConnectionEntry> entry(mEntry);
if (!entry) {
return false;
}
RefPtr<PendingTransactionInfo> pendingInfo =
mConnMgrDelegate->FindTransaction(
/*removeWhenFound*/ true, entry, realTransaction);
LOG(
("HappyEyeballsConnectionAttempt::LockInRealTransactionFromPendingQueue "
"%p realTransaction=%p removed=%d",
this, realTransaction, !!pendingInfo));
return !!pendingInfo;
}
already_AddRefed<HappyEyeballsTransaction>
HappyEyeballsConnectionAttempt::CreateAttemptTransaction(
nsHttpConnectionInfo* aInfo, uint64_t aEstablisherId) {
nsCOMPtr<nsIInterfaceRequestor> callbacks;
uint64_t browserId = 0;
if (mTransaction) {
mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks));
browserId = mTransaction->BrowserId();
}
RefPtr<HappyEyeballsTransaction> trans = new HappyEyeballsTransaction(
aInfo, callbacks, mCaps, browserId,
[self = RefPtr{this}](nsITransport* t, nsresult s, int64_t p) {
self->MaybeSendTransportStatus(s, t, p);
},
[self = RefPtr{this}, id = aEstablisherId]() {
self->OnClientAuthCertificateRequested(id);
},
[self = RefPtr{this}, id = aEstablisherId]() {
self->OnClientAuthCertificateSelected(id);
},
mZeroRttHandle);
return trans.forget();
}
nsresult HappyEyeballsConnectionAttempt::EstablishTCPConnection(
NetAddr aAddr, uint16_t aPort, nsTArray<uint8_t>&& aEchConfig, uint64_t aId,
bool aIsEchRetry) {
// TODO: we always use happy_eyeballs::ConnectionAttemptHttpVersions::H2OrH1
// for now. Do we really want to race H2 and H1?
RefPtr<nsHttpConnectionInfo> info = mConnInfo->CloneAndAdoptPortAndAlpn(
aPort, happy_eyeballs::ConnectionAttemptHttpVersions::H2OrH1);
if (!aEchConfig.IsEmpty()) {
info->SetEchConfig(
nsCString((const char*)aEchConfig.Elements(), aEchConfig.Length()));
NotifyConnectionActivity(info, NS_HTTP_ACTIVITY_SUBTYPE_ECH_SET);
}
NotifyConnectionActivity(info, NS_HTTP_ACTIVITY_SUBTYPE_CONNECTION_CREATED);
uint32_t caps = mCaps | (aIsEchRetry ? NS_HTTP_IS_RETRY : 0);
RefPtr<ConnectionEstablisher> establisher =
mEstablisherFactory->Create(ConnectionEstablisherType::TCP, info, aAddr,
caps, mSpeculative, mAllow1918);
establisher->SetDnsMetadata(mDnsMetadata);
nsCOMPtr<nsIInterfaceRequestor> callbacks;
mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks));
establisher->SetSecurityCallbacks(callbacks);
establisher->SetTransportStatusCallback(
[self = RefPtr{this}](nsITransport* trans, nsresult status,
int64_t progress) {
self->MaybeSendTransportStatus(status, trans, progress);
});
establisher->SetLnaCheckCallback(
[self = RefPtr{this}](nsISocketTransport* aTransport) -> nsresult {
return self->CheckLNA(aTransport);
});
RefPtr<HappyEyeballsTransaction> attempt =
CreateAttemptTransaction(info, aId);
establisher->SetTransaction(attempt);
auto callback = [self = RefPtr{this}, establisher,
aId](Result<RefPtr<HttpConnectionBase>, nsresult> aResult) {
self->HandleConnectionResult(std::move(aResult), establisher, aId);
};
if (establisher->Start(std::move(callback))) {
mConnectionEstablisherTable.InsertOrUpdate(aId, std::move(establisher));
} else {
// Start() refused synchronously. A speculative attempt refuses local
// (RFC1918) peers; surface it as a refusal and remember it so a later Claim
// can retry with local addresses allowed.
nsresult reason = NS_ERROR_FAILURE;
if (establisher->RefusedForLocalAddress()) {
mLocalAddrRefused = true;
reason = NS_ERROR_CONNECTION_REFUSED;
} else {
mAllAttemptsRefusedLocal = false;
}
ProcessConnectionResult(aAddr, reason, aId);
}
return NS_OK;
}
nsresult HappyEyeballsConnectionAttempt::EstablishUDPConnection(
NetAddr aAddr, uint16_t aPort, nsTArray<uint8_t>&& aEchConfig, uint64_t aId,
bool aIsEchRetry) {
RefPtr<nsHttpConnectionInfo> info = mConnInfo->CloneAndAdoptPortAndAlpn(
aPort, happy_eyeballs::ConnectionAttemptHttpVersions::H3);
if (!aEchConfig.IsEmpty()) {
info->SetEchConfig(
nsCString((const char*)aEchConfig.Elements(), aEchConfig.Length()));
NotifyConnectionActivity(info, NS_HTTP_ACTIVITY_SUBTYPE_ECH_SET);
}
NotifyConnectionActivity(info, NS_HTTP_ACTIVITY_SUBTYPE_CONNECTION_CREATED);
uint32_t caps = mCaps | (aIsEchRetry ? NS_HTTP_IS_RETRY : 0);
RefPtr<ConnectionEstablisher> establisher =
mEstablisherFactory->Create(ConnectionEstablisherType::UDP, info, aAddr,
caps, mSpeculative, mAllow1918);
establisher->SetDnsMetadata(mDnsMetadata);
establisher->SetTransportStatusCallback(
[self = RefPtr{this}](nsITransport* trans, nsresult status,
int64_t progress) {
self->MaybeSendTransportStatus(status, trans, progress);
});
RefPtr<HappyEyeballsTransaction> attempt =
CreateAttemptTransaction(info, aId);
establisher->SetTransaction(attempt);
auto callback = [self = RefPtr{this}, establisher,
aId](Result<RefPtr<HttpConnectionBase>, nsresult> aResult) {
self->HandleConnectionResult(std::move(aResult), establisher, aId);
};
if (establisher->Start(std::move(callback))) {
mConnectionEstablisherTable.InsertOrUpdate(aId, std::move(establisher));
} else {
// Start() refused synchronously. A speculative attempt refuses local
// (RFC1918) peers; surface it as a refusal and remember it so a later Claim
// can retry with local addresses allowed.
nsresult reason = NS_ERROR_FAILURE;
if (establisher->RefusedForLocalAddress()) {
mLocalAddrRefused = true;
reason = NS_ERROR_CONNECTION_REFUSED;
} else {
mAllAttemptsRefusedLocal = false;
}
ProcessConnectionResult(aAddr, reason, aId);
}
return NS_OK;
}
void HappyEyeballsConnectionAttempt::OnClientAuthCertificateRequested(
uint64_t aEstablisherId) {
LOG(
("HappyEyeballsConnectionAttempt::OnClientAuthCertificateRequested %p "
"id=%" PRIu64,
this, aEstablisherId));
if (IsTerminal()) {
return;
}
if (mPausedForClientAuth) {
return;
}
mPausedForClientAuth = true;
mClientAuthHolderId = aEstablisherId;
}
void HappyEyeballsConnectionAttempt::OnClientAuthCertificateSelected(
uint64_t aEstablisherId) {
LOG(
("HappyEyeballsConnectionAttempt::OnClientAuthCertificateSelected %p "
"id=%" PRIu64,
this, aEstablisherId));
if (!mPausedForClientAuth || aEstablisherId != mClientAuthHolderId) {
return;
}
mPausedForClientAuth = false;
mClientAuthHolderId = 0;
}
void HappyEyeballsConnectionAttempt::CancelConnection(uint64_t aId) {
LOG(("HappyEyeballsConnectionAttempt::CancelConnection id=%" PRIu64, aId));
RefPtr<ConnectionEstablisher> conn = mConnectionEstablisherTable.Get(aId);
if (conn) {
conn->Close(NS_ERROR_ABORT);
mConnectionEstablisherTable.Remove(aId);
} else {
LOG(("No matching connection found for id=%" PRIu64, aId));
}
}
void HappyEyeballsConnectionAttempt::CloseHttpTransaction(
happy_eyeballs::FailureReason aReason, ConnectionEntry* aEntry) {
LOG(("HappyEyeballsConnectionAttempt::CloseHttpTransaction %p reason=%d",
this, static_cast<uint32_t>(aReason)));
nsresult reason = NS_ERROR_ABORT;
switch (aReason) {
case happy_eyeballs::FailureReason::DnsResolution:
reason = NS_FAILED(mLastDnsError) ? mLastDnsError : NS_ERROR_UNKNOWN_HOST;
break;
case happy_eyeballs::FailureReason::Connection:
reason = (NS_FAILED(mLastConnectionError) &&
mLastConnectionError != NS_ERROR_NET_RESET)
? mLastConnectionError
: NS_ERROR_CONNECTION_REFUSED;
break;
default:
MOZ_ASSERT_UNREACHABLE("Unknown FailureReason");
break;
}
// Defensive: Failed implies no winner, so should never be adopted.
// Route through ReleaseRealTransaction to keep the invariant uniform.
ReleaseRealTransaction(reason, aEntry);
}
bool HappyEyeballsConnectionAttempt::ShouldRetryWithoutTRR(
happy_eyeballs::FailureReason aReason) const {
if (!StaticPrefs::network_http_happy_eyeballs_retry_without_trr()) {
return false;
}
// Only when TRR-resolved addresses all failed to *connect* (not a pure DNS
// failure), for a real transaction, outside TRR_ONLY mode, and only once (a
// TRR-disabled retry attempt must not retry again).
if (aReason != happy_eyeballs::FailureReason::Connection ||
mRetryWithoutTRR || !mDnsMetadata.mIsTRR ||
mDnsMetadata.mEffectiveTRRMode == nsIRequest::TRR_ONLY_MODE ||
!RealHttpTransaction()) {
return false;
}
// Don't fall back when TRR intentionally returned 0.0.0.0 / :: (a blocked
// answer), unless the pref opts in. Matches
// nsSocketTransport::RecoverFromError.
if (!StaticPrefs::network_trr_fallback_on_zero_response() &&
!mOriginAddresses.IsEmpty()) {
bool allZero = true;
for (const auto& addr : mOriginAddresses) {
bool isZero = (addr.raw.family == AF_INET && addr.inet.ip == 0) ||
(addr.raw.family == AF_INET6 && addr.inet6.ip.u64[0] == 0 &&
addr.inet6.ip.u64[1] == 0);
if (!isZero) {
allZero = false;
break;
}
}
if (allZero) {
return false;
}
}
return true;
}
void HappyEyeballsConnectionAttempt::DoRetry(bool aRetryWithoutTRR,
const char* aLogTag) {
RefPtr<HappyEyeballsConnectionAttempt> self(this);
RefPtr<ConnectionEntry> entry(mEntry);
RefPtr<nsHttpTransaction> realTrans = RealHttpTransaction();
LOG(("HappyEyeballsConnectionAttempt::%s %p trans=%p", aLogTag, this,
realTrans.get()));
MOZ_ASSERT(entry && realTrans);
nsresult rv =
mConnMgrDelegate->StartRetry(entry, realTrans, mCaps, mSpeculative,
mUrgentStart, mAllow1918, aRetryWithoutTRR);
if (NS_FAILED(rv)) {
LOG((" %s: StartRetry failed rv=%" PRIx32 "; failing normally", aLogTag,
static_cast<uint32_t>(rv)));
TransitionPayload payload;
payload.mFailureReason = Some(happy_eyeballs::FailureReason::Connection);
Transition(State::Failed, std::move(payload));
return;
}
// The new attempt owns the transaction now. Detach so this attempt's teardown
// can't close or re-queue it, then remove ourselves and abandon.
ForgetRealTransaction();
mConnMgrDelegate->RemoveConnectionAttempt(entry, this, /* abandon = */ true);
}
void HappyEyeballsConnectionAttempt::RetryWithoutTRR() {
// Start a fresh attempt that re-resolves this transaction with TRR disabled.
DoRetry(/* aRetryWithoutTRR = */ true, "RetryWithoutTRR");
}
bool HappyEyeballsConnectionAttempt::ShouldRetryForLocalAddress(
happy_eyeballs::FailureReason aReason) const {
// A speculative attempt must not open a connection to a local (RFC1918) peer:
// that would let a remote origin use a preconnect/speculative connection to
// probe the user's local network before any real navigation (Local Network
// Access). So while speculative the establisher refuses local peers outright
// rather than "just connecting" -- there is no safe way to speculatively
// reach a local address. Claim() lifts that restriction (mAllow1918 becomes
// true) once a real transaction takes over, but an establisher may already
// have refused the peer before the claim raced in, leaving the attempt
// terminally failed. This retry recovers that case.
//
// Retry only when every attempt was a local-peer refusal and the attempt has
// since been claimed (mAllow1918 true, so a non-speculative attempt never
// qualifies) with a real transaction to reconnect. The fresh retry allows
// local addresses, so it can't refuse again or loop.
return aReason == happy_eyeballs::FailureReason::Connection &&
mLocalAddrRefused && mAllAttemptsRefusedLocal && mAllow1918 &&
RealHttpTransaction();
}
void HappyEyeballsConnectionAttempt::RetryForLocalAddress() {
// Start a fresh attempt for this transaction; mAllow1918 is now true, so the
// establisher no longer refuses the local peer.
DoRetry(/* aRetryWithoutTRR = */ false, "RetryForLocalAddress");
}
void HappyEyeballsConnectionAttempt::Abandon() {
LOG(("HappyEyeballsConnectionAttempt::Abandon %p", this));
// Route every path (external + outcome entry actions) through Done.
// Idempotent — repeat calls are no-ops.
if (mState == State::Done) {
return;
}
Transition(State::Done);
}
void HappyEyeballsConnectionAttempt::ProcessTCPConn(
HttpConnectionBase* aConn, ConnectionEntry* aEntry,
bool aTransactionAlreadyOnConn) {
RefPtr<ConnectionEntry> entry(mEntry);
if (!entry) {
return;
}
RefPtr<HttpConnectionBase> connTCP = aConn;
LOG(("Got connTCP:%p transactionAlreadyOnConn=%d", connTCP.get(),
aTransactionAlreadyOnConn));
mConnMgrDelegate->InsertIntoActiveConns(entry, connTCP);
bool isHttp2 = connTCP->UsingSpdy();
nsHttpTransaction* realTrans =
mTransaction ? mTransaction->QueryHttpTransaction() : nullptr;
// WebSocket / WebTransport upgrades on an HTTP/2 connection must be
// dispatched through the extended CONNECT tunnel (Http2StreamTunnel)
// instead of being activated directly on the Http2Session.
bool deferExtendedConnect =
isHttp2 && realTrans &&
(realTrans->IsWebsocketUpgrade() || realTrans->IsForWebTransport());
if (!aTransactionAlreadyOnConn && deferExtendedConnect) {
LOG(
("ProcessTCPConn deferring extended CONNECT upgrade trans=%p to "
"ProcessPendingQ\n",
realTrans));
// Another racing attempt may have already dispatched this transaction onto
// a connection (bug 2048392). It's no longer ours to re-queue; doing so
// would re-dispatch onto its now transport-less connection.
if (realTrans->Connection()) {
LOG(
("ProcessTCPConn trans=%p already dispatched to a connection; not "
"re-queuing\n",
realTrans));
} else {
RefPtr<PendingTransactionInfo> existing =
mConnMgrDelegate->FindTransaction(
/* removeWhenFound = */ false, entry, realTrans);
if (!existing) {
mConnMgrDelegate->AddTransaction(realTrans, realTrans->Priority());
}
}
mTransaction = nullptr;
} else if (!aTransactionAlreadyOnConn) {
RefPtr<PendingTransactionInfo> pendingTransInfo =
mConnMgrDelegate->FindTransaction(true, entry, mTransaction);
if (pendingTransInfo) {
MOZ_ASSERT(!mSpeculative, "Speculative HE attempt found mTransaction");
nsresult rv = mConnMgrDelegate->DispatchTransaction(
entry, pendingTransInfo->Transaction(), connTCP);
if (NS_FAILED(rv)) {
mTransaction->Close(rv);
} else {
// Real transaction now on connTCP — disarm ReleaseRealTransaction.
mTransactionAdopted = true;
}
} else if (!isHttp2) {
// After about 1 second allow for the possibility of restarting a
// transaction due to server close. Keep at sub 1 second as that is the
// minimum granularity we can expect a server to be timing out with.
if (RefPtr<nsHttpConnection> h1 = do_QueryObject(connTCP)) {
h1->SetIsReusedAfter(950);
}
LOG(
("ProcessTCPConn no transaction match "
"returning conn %p to pool\n",
connTCP.get()));
mConnMgrDelegate->ReclaimConnection(connTCP);
}
}
connTCP->SetIsRacing(false);
mConnMgrDelegate->ReportSpdyConnection(
connTCP, isHttp2, isHttp2 && (mCaps & NS_HTTP_DISALLOW_HTTP3));
}
void HappyEyeballsConnectionAttempt::ProcessUDPConn(
HttpConnectionBase* aConn, ConnectionEntry* aEntry,
bool aTransactionAlreadyOnConn) {
RefPtr<ConnectionEntry> entry(mEntry);
if (!entry) {
return;
}
LOG(("Got connUDP:%p transactionAlreadyOnConn=%d", aConn,
aTransactionAlreadyOnConn));
if (!mFirstConnectionStart.IsNull()) {
TimingStruct connectTimings;
FillConnectTimings(/* aIsQuic = */ true, connectTimings);
aConn->SetConnectBootstrapTimings(
connectTimings.connectStart, connectTimings.tcpConnectEnd,
connectTimings.secureConnectionStart, connectTimings.connectEnd);
if (aTransactionAlreadyOnConn) {
// Activate already ran before timings were set on the connection,
// so transfer them directly to the transaction.
// mTransaction may be null if restartedFallback0Rtt cleared it.
nsHttpTransaction* trans =
mTransaction ? mTransaction->QueryHttpTransaction() : nullptr;
if (trans) {
TimingStruct timings;
DnsLookupTimings(timings.domainLookupStart, timings.domainLookupEnd);
FillConnectTimings(/* aIsQuic = */ true, timings);
trans->BootstrapTimings(timings);
}
}
}
mConnMgrDelegate->InsertIntoActiveConns(entry, aConn);
if (!aTransactionAlreadyOnConn) {
RefPtr<PendingTransactionInfo> pendingTransInfo =
mConnMgrDelegate->FindTransaction(true, entry, mTransaction);
nsresult rv = NS_OK;
if (pendingTransInfo) {
MOZ_ASSERT(!mSpeculative, "Speculative HE attempt found mTransaction");
rv = mConnMgrDelegate->DispatchTransaction(
entry, pendingTransInfo->Transaction(), aConn);
if (NS_FAILED(rv)) {
mTransaction->Close(rv);
} else {
// Real transaction now on aConn — see ProcessTCPConn.
mTransactionAdopted = true;
}
} else {
nsHttpTransaction* trans = mTransaction->QueryHttpTransaction();
if (trans && trans->IsDone()) {
LOG(("ProcessUDPConn transaction already done, not activating"));
} else if (trans && trans->Connection()) {
// Another racing attempt already dispatched this transaction onto a
// connection (cf. bug 2048392 / bug 2048294); don't re-activate it.
LOG(
("ProcessUDPConn transaction already dispatched to a connection, "
"not activating"));
} else {
rv = aConn->Activate(mTransaction, mCaps, 0);
if (NS_SUCCEEDED(rv)) {
mTransactionAdopted = true;
}
}
}
}
aConn->SetIsRacing(false);
mConnMgrDelegate->ReportHttp3Connection(aConn, entry);
}
void HappyEyeballsConnectionAttempt::EnterSucceeded() {
LOG(("HappyEyeballsConnectionAttempt::EnterSucceeded %p", this));
MOZ_ASSERT(mState == State::Succeeded);
RefPtr<HappyEyeballsConnectionAttempt> self(this);
RefPtr<ConnectionEntry> entry(mEntry);
MOZ_ASSERT(entry);
// RecordIPFamilyPreference is sticky (set-once) and can't flip an existing
// preference. If the winning family contradicts the entry's learned
// preference (e.g. the preferred family has since gone dead and the other
// family won), reset first so the winner becomes the new preference instead
// of being ignored -- otherwise the stale preference persists and every
// subsequent connection keeps giving the dead family a head start.
if ((mAddrFamily == AF_INET && entry->mPreferIPv6) ||
(mAddrFamily == AF_INET6 && entry->mPreferIPv4)) {
mConnMgrDelegate->ResetIPFamilyPreference(entry);
}
mConnMgrDelegate->RecordIPFamilyPreference(entry, mAddrFamily);
TimeStamp dnsLookupStart, dnsLookupEnd;
DnsLookupTimings(dnsLookupStart, dnsLookupEnd);
if (!dnsLookupStart.IsNull()) {
mOutputConn->SetDnsBootstrapTimings(dnsLookupStart, dnsLookupEnd);
}
// Build the real transaction's timings from the first-racer domainLookup
// and connect spans (rather than the winning attempt's own collected
// timings) before dispatch. We preserve transactionPending explicitly —
// BootstrapTimings does a full struct overwrite, and DispatchTransaction
// will read the pending time to record wait-time metrics.
if (mOutputTrans && mTransaction) {
if (nsHttpTransaction* realTransaction =
mTransaction->QueryHttpTransaction()) {
TimingStruct timings;
DnsLookupTimings(timings.domainLookupStart, timings.domainLookupEnd);
FillConnectTimings(/* aIsQuic = */ mOutputConn->UsingHttp3(), timings);
timings.transactionPending = realTransaction->GetPendingTime();
realTransaction->BootstrapTimings(timings);
}
}
mOutputTrans = nullptr;
// Fallback for the case where ShouldDisqualify didn't fire. A racer that did
// 0-RTT advanced the real transaction's request stream; its flags are
// half-set and the winner isn't a 0-RTT racer. Tell the real transaction the
// 0-RTT attempt was effectively rejected — FinishAdopted0RTT(restart=true)
// rewinds the stream to 0 and marks mDoNotTryEarlyData /
// mEarlyDataWasAvailable so the real transaction re-sends a fresh request on
// the winning conn.
bool restartedFallback0Rtt = false;
nsHttpTransaction* trans =
mTransaction ? mTransaction->QueryHttpTransaction() : nullptr;
if (mZeroRttHandle->AnyStarted() && !mZeroRttHandle->HadWinner()) {
if (!mTransaction) {
// ReleaseRealTransaction already restarted the real transaction via
// Close()/Restart(). The winning connection goes into the pool below
// and the CM will dispatch the restarted transaction on it.
} else {
// AnyStarted() is set only after LockInRealTransactionFromPendingQueue()
// succeeds, which requires QueryHttpTransaction() to return non-null.
// So trans is always non-null here when mTransaction is non-null.
MOZ_ASSERT(trans,
"AnyStarted implies a live real transaction; "
"QueryHttpTransaction() should not be null");
if (trans) {
trans->FinishAdopted0RTT(/*aRestart=*/true);
// LockInRealTransactionFromPendingQueue removed the real transaction
// from the pending queue when 0-RTT was entered. Re-queue it so the
// conn manager can dispatch it on the winning conn or open a new
// connection. Guard against double-queuing (which would trip
// CheckTransInPendingQueue's assertion in AddTransaction) by checking
// first.
// If another racing attempt already dispatched this transaction onto a
// connection it's no longer ours to re-queue (cf. bug 2048392 /
// bug 2048294).
if (!trans->Connection()) {
RefPtr<PendingTransactionInfo> existing;
if (entry) {
existing = mConnMgrDelegate->FindTransaction(
/*removeWhenFound=*/false, entry, trans);
}
if (!existing) {
mConnMgrDelegate->AddTransaction(trans, trans->Priority());
}
}
restartedFallback0Rtt = true;
mTransaction = nullptr;
}
}
}
MOZ_DIAGNOSTIC_ASSERT(
!mZeroRttHandle->AnyStarted() || mZeroRttHandle->HadWinner() ||
!mTransaction,
"EnterSucceeded: 0-RTT transaction not re-queued and not adopted");
// Adopted: real transaction is on the conn and already out of the pending
// queue. Skip ProcessTCPConn's pending-queue branch — on H1 it
// would otherwise reclaim the live conn to the idle pool; on H2/H3
// it's a no-op.
// Also skip FindTransactionHelper for the fallback restart case: the
// re-inserted trans will be dispatched by ReportSpdyConnection →
// ProcessPendingQ once the conn is in the active pool.
bool alreadyOnConn = mZeroRttHandle->HadWinner() || restartedFallback0Rtt;
if (!mOutputConn->UsingHttp3()) {
// If the original request had an alt-svc route but a direct TCP
// connection won, remove the Alt-Used header since we're not using
// the alt-svc route.
if (!mConnInfo->GetRoutedHost().IsEmpty()) {
if (trans) {
trans->RemoveAltSvcUsedHeader();
}
}
ProcessTCPConn(mOutputConn, entry, alreadyOnConn);
} else {
ProcessUDPConn(mOutputConn, entry, alreadyOnConn);
}
mOutputConn = nullptr;
// Make sure everything is released.
Abandon();
mConnMgrDelegate->RemoveConnectionAttempt(entry, this, false);
}
double HappyEyeballsConnectionAttempt::Duration(TimeStamp epoch) {
if (mFirstConnectionStart.IsNull()) {
return 0;
}
return (epoch - mFirstConnectionStart).ToMilliseconds();
}
void HappyEyeballsConnectionAttempt::OnTimeout() {
LOG(("HappyEyeballsConnectionAttempt::OnTimeout %p" PRIx32, this));
if (IsTerminal()) {
return;
}
Transition(State::TimedOut);
}
void HappyEyeballsConnectionAttempt::EnterTimedOut() {
LOG(("HappyEyeballsConnectionAttempt::EnterTimedOut %p", this));
MOZ_ASSERT(mState == State::TimedOut);
RefPtr<ConnectionEntry> entry(mEntry);
ReleaseRealTransaction(NS_ERROR_NET_TIMEOUT, entry);
Abandon();
}
void HappyEyeballsConnectionAttempt::EnterFailed(
happy_eyeballs::FailureReason aReason) {
LOG(("HappyEyeballsConnectionAttempt::EnterFailed %p reason=%d", this,
static_cast<uint32_t>(aReason)));
MOZ_ASSERT(mState == State::Failed);
RefPtr<HappyEyeballsConnectionAttempt> self(this);
RefPtr<ConnectionEntry> entry(mEntry);
if (entry) {
mConnMgrDelegate->RemoveConnectionAttempt(entry, this, false);
}
CloseHttpTransaction(aReason, entry);
Abandon();
}
void HappyEyeballsConnectionAttempt::EnterRestartTransaction(
nsresult aCloseReason) {
LOG(("HappyEyeballsConnectionAttempt::EnterRestartTransaction %p reason=%x",
this, static_cast<uint32_t>(aCloseReason)));
MOZ_ASSERT(mState == State::RestartTransaction);
RefPtr<HappyEyeballsConnectionAttempt> self(this);
RefPtr<ConnectionEntry> entry(mEntry);
if (entry) {
// abandon=true routes through Abandon() -> Done; cleanup happens there.
// mEntry will be nulled by EnterDone, but |entry| is a local RefPtr
// and stays valid for the ReleaseRealTransaction call below.
mConnMgrDelegate->RemoveConnectionAttempt(entry, this, true);
}
// Both classifier inputs that land here (PossibleZeroRTTRetryError TLS
// alerts and NET_RESET while a racer had entered 0-RTT) mean "the
// alt-svc endpoint is fine, retry on the same endpoint without 0-RTT".
// Keep the alt-svc route so Restart()'s CloneAsDirectRoute() doesn't
// strip it and force a TCP/H2 downgrade.
if (mTransaction) {
if (nsHttpTransaction* trans = mTransaction->QueryHttpTransaction()) {
trans->DoNotRemoveAltSvc();
// FinishAdopted0RTT mutates state that only makes sense if a HET
// actually adopted 0-RTT (mResumptionAttempted, mDoNotTryEarlyData,
// rewind request stream); otherwise leave the txn alone and let
// Restart() itself disable 0-RTT and evict the stale token.
if (aCloseReason == NS_ERROR_NET_RESET && mZeroRttHandle->AnyStarted()) {
trans->FinishAdopted0RTT(/* aRestart = */ true);
}
}
}
ReleaseRealTransaction(aCloseReason, entry);
// Fallback if the entry==nullptr path skipped the RemoveConnectionAttempt
// -> Abandon route above.
if (mState != State::Done) {
Abandon();
}
}
void HappyEyeballsConnectionAttempt::EnterAbortTransaction(
nsresult aCloseReason) {
LOG(
("HappyEyeballsConnectionAttempt::EnterAbortTransaction %p "
"reason=%x",
this, static_cast<uint32_t>(aCloseReason)));
MOZ_ASSERT(mState == State::AbortTransaction);
RefPtr<HappyEyeballsConnectionAttempt> self(this);
RefPtr<ConnectionEntry> entry(mEntry);
ReleaseRealTransaction(aCloseReason, entry);
Abandon();
if (entry) {
mConnMgrDelegate->RemoveConnectionAttempt(entry, this, false);
}
}
void HappyEyeballsConnectionAttempt::EnterDone() {
LOG(("HappyEyeballsConnectionAttempt::EnterDone %p", this));
MOZ_ASSERT(mState == State::Done);
for (auto iter = mDnsRequestTable.Iter(); !iter.Done(); iter.Next()) {
iter.Data()->Cancel();
}
mDnsRequestTable.Clear();
// Snapshot first — Close() callbacks may re-enter and mutate the table.
nsTArray<RefPtr<ConnectionEstablisher>> establishers;
for (auto iter = mConnectionEstablisherTable.Iter(); !iter.Done();
iter.Next()) {
establishers.AppendElement(iter.Data());
}
mConnectionEstablisherTable.Clear();
for (auto& conn : establishers) {
conn->Close(NS_ERROR_ABORT);
}
if (mTimer) {
mTimer->Cancel();
}
mTimer = nullptr;
// 0-RTT started but no winner: the real transaction was pulled from the
// pending queue by LockInRealTransactionFromPendingQueue. Re-queue it
// (guarded against double-queue via FindTransactionHelper).
if (mTransaction && mZeroRttHandle->AnyStarted() &&
!mZeroRttHandle->HadWinner()) {
if (nsHttpTransaction* realTransaction =
mTransaction->QueryHttpTransaction()) {
if (!realTransaction->Closed()) {
realTransaction->FinishAdopted0RTT(/*aRestart=*/true);
// If another racing attempt already dispatched this transaction onto a
// connection it's no longer ours to re-queue (cf. bug 2048392 /
// bug 2048294).
if (!realTransaction->Connection()) {
RefPtr<ConnectionEntry> entry(mEntry);
RefPtr<PendingTransactionInfo> existing;
if (entry) {
existing = mConnMgrDelegate->FindTransaction(
/*removeWhenFound=*/false, entry, realTransaction);
}
if (!existing) {
mConnMgrDelegate->AddTransaction(realTransaction,
realTransaction->Priority());
}
}
}
}
mTransaction = nullptr;
}
MOZ_DIAGNOSTIC_ASSERT(!mZeroRttHandle->AnyStarted() ||
mZeroRttHandle->HadWinner() || !mTransaction,
"transaction not re-queued and not adopted");
mZeroRttHandle->Cleanup();
mEntry = nullptr;
}
void HappyEyeballsConnectionAttempt::Transition(State aNext) {
Transition(aNext, TransitionPayload{});
}
void HappyEyeballsConnectionAttempt::Transition(State aNext,
TransitionPayload aPayload) {
LOG(("HappyEyeballsConnectionAttempt::Transition %p mState=%d aNext=%d", this,
static_cast<int>(mState), static_cast<int>(aNext)));
// Activity-state self-loops are no-ops.
if (mState == aNext &&
(aNext == State::Connecting || aNext == State::ZeroRttRacing ||
aNext == State::ProcessingConnectionResult)) {
return;
}
switch (aNext) {
case State::Init:
MOZ_ASSERT_UNREACHABLE("Init is the initial state");
break;
case State::Connecting:
MOZ_ASSERT(
mState == State::Init || mState == State::ProcessingConnectionResult,
"Connecting entered from Init or "
"ProcessingConnectionResult only");
mState = State::Connecting;
break;
case State::ZeroRttRacing:
MOZ_ASSERT(mState == State::Connecting ||
mState == State::ProcessingConnectionResult,
"ZeroRttRacing entered from Connecting or "
"ProcessingConnectionResult only");
mState = State::ZeroRttRacing;
break;
case State::ProcessingConnectionResult:
MOZ_ASSERT(mState == State::Connecting || mState == State::ZeroRttRacing,
"ProcessingConnectionResult entered from Connecting or "
"ZeroRttRacing only");
mState = State::ProcessingConnectionResult;
break;
case State::Succeeded:
MOZ_ASSERT(!IsTerminal(), "Succeeded from a non-terminal state only");
mState = State::Succeeded;
EnterSucceeded();
break;
case State::Failed:
MOZ_ASSERT(!IsTerminal(), "Failed from a non-terminal state only");
MOZ_ASSERT(aPayload.mFailureReason.isSome(),
"Failed requires a FailureReason payload");
mState = State::Failed;
EnterFailed(aPayload.mFailureReason.ref());
break;
case State::RestartTransaction:
MOZ_ASSERT(!IsTerminal(),
"RestartTransaction from a non-terminal state only");
mState = State::RestartTransaction;
EnterRestartTransaction(aPayload.mCloseReason);
break;
case State::AbortTransaction:
MOZ_ASSERT(!IsTerminal(),
"AbortTransaction from a non-terminal state only");
mState = State::AbortTransaction;
EnterAbortTransaction(aPayload.mCloseReason);
break;
case State::TimedOut:
MOZ_ASSERT(!IsTerminal(), "TimedOut from a non-terminal state only");
mState = State::TimedOut;
EnterTimedOut();
break;
case State::Done:
// Idempotent — external Abandon() and every outcome entry action
// both route here.
if (mState == State::Done) {
return;
}
mState = State::Done;
EnterDone();
break;
}
}
void HappyEyeballsConnectionAttempt::PrintDiagnostics(nsCString& log) {}
uint32_t HappyEyeballsConnectionAttempt::UnconnectedUDPConnsLength() const {
uint32_t len = 0;
for (auto iter = mConnectionEstablisherTable.ConstIter(); !iter.Done();
iter.Next()) {
if (iter.Data()->IsUDP()) {
len++;
}
}
if (len == 0) {
if (mConnInfo->IsHttp3()) {
return 1;
}
}
return len;
}
bool HappyEyeballsConnectionAttempt::Claim(nsHttpTransaction* newTransaction) {
if (mSpeculative) {
mSpeculative = false;
mAllow1918 = true;
for (auto iter = mConnectionEstablisherTable.Iter(); !iter.Done();
iter.Next()) {
RefPtr<ConnectionEstablisher> conn = iter.Data();
conn->ResetSpeculativeFlags();
}
}
if (mFreeToUse) {
mFreeToUse = false;
if (newTransaction && mTransaction &&
mTransaction->QueryNullTransaction()) {
LOG(
("HappyEyeballsConnectionAttempt::Claim %p replacing null "
"transaction %p with %p",
this, mTransaction.get(), newTransaction));
mTransaction->Close(NS_ERROR_ABORT);
mTransaction = newTransaction;
// Replay transport statuses that were sent while the null transaction
// was in place, in the correct order.
static const nsresult kStatusOrder[] = {
NS_NET_STATUS_RESOLVING_HOST, NS_NET_STATUS_RESOLVED_HOST,
NS_NET_STATUS_CONNECTING_TO, NS_NET_STATUS_CONNECTED_TO};
for (nsresult status : kStatusOrder) {
if (mSentTransportStatuses.Contains(static_cast<uint32_t>(status))) {
mTransaction->OnTransportStatus(nullptr, status, 0);
}
}
}
return true;
}
return false;
}
NS_IMETHODIMP
HappyEyeballsConnectionAttempt::OnLookupComplete(nsICancelable* request,
nsIDNSRecord* rec,
nsresult status) {
LOG(("HappyEyeballsConnectionAttempt::OnLookupComplete"));
// domainLookupEnd tracks the latest DNS response received before the first
// connection attempt starts. Later responses are ignored; if no lookup has
// completed by the time the first connection starts, it is set to
// mFirstConnectionStart there instead.
if (mFirstConnectionStart.IsNull()) {
mDnsResolutionEnd = TimeStamp::Now();
}
if (!request) {
return NS_OK;
}
RefPtr<DnsRequestInfo> info = mDnsRequestTable.Get(request);
if (!info) {
LOG(("OnLookupComplete: Unknown DNS request"));
return NS_OK;
}
uint64_t id = info->Id();
happy_eyeballs::DnsRecordType type = info->Type();
mDnsRequestTable.Remove(request);
switch (type) {
case happy_eyeballs::DnsRecordType::A:
return OnARecord(rec, status, id);
case happy_eyeballs::DnsRecordType::Aaaa:
return OnAAAARecord(rec, status, id);
case happy_eyeballs::DnsRecordType::Https:
return OnHTTPSRecord(rec, status, id);
}
return NS_OK;
}
nsresult HappyEyeballsConnectionAttempt::OnARecord(nsIDNSRecord* aRecord,
nsresult status,
uint64_t aId) {
LOG(("HappyEyeballsConnectionAttempt::OnARecord: this=%p status %" PRIx32
" id=%" PRIu64,
this, static_cast<uint32_t>(status), aId));
if (NS_SUCCEEDED(status)) {
MaybeSendTransportStatus(NS_NET_STATUS_RESOLVED_HOST);
} else if (NS_FAILED(status)) {
mLastDnsError = status;
}
// TODO: use NS_ERROR_UNKNOWN_PROXY_HOST if stasus is failed and proxy is used
nsCOMPtr<nsIDNSAddrRecord> addrRecord = do_QueryInterface(aRecord);
if (addrRecord) {
mDnsMetadata.Fill(addrRecord);
if (mTransaction && !mTRRInfoForwarded) {
mTransaction->SetTRRInfo(mDnsMetadata.mEffectiveTRRMode,
mDnsMetadata.mTrrSkipReason);
mTRRInfoForwarded = true;
}
}
nsresult rv;
if (NS_FAILED(status) || !addrRecord) {
if (mOriginDnsLookupIds.Contains(aId)) {
mOriginDnsLookupIds.Remove(aId);
MaybeBuildOriginCoalescingKeys();
}
nsTArray<NetAddr> emptyArray;
rv = happy_eyeballs_process_dns_response_a(mHappyEyeballs, aId, &emptyArray,
mDnsMetadata.mIsTRR);
if (NS_FAILED(rv)) {
return rv;
}
return ProcessHappyEyeballsOutput();
}
nsTArray<NetAddr> addresses;
addrRecord->GetAddresses(addresses);
// Filter to only IPv4 addresses
nsTArray<NetAddr> ipv4Addresses;
for (const auto& addr : addresses) {
if (addr.raw.family == AF_INET) {
LOG(("Addr=[%s]", addr.ToString().get()));
ipv4Addresses.AppendElement(addr);
}
}
if (mOriginDnsLookupIds.Contains(aId)) {
mOriginDnsLookupIds.Remove(aId);
mOriginAddresses.AppendElements(ipv4Addresses);
MaybeBuildOriginCoalescingKeys();
}
rv = happy_eyeballs_process_dns_response_a(
mHappyEyeballs, aId, &ipv4Addresses, mDnsMetadata.mIsTRR);
if (NS_FAILED(rv)) {
return rv;
}
return ProcessHappyEyeballsOutput();
}
nsresult HappyEyeballsConnectionAttempt::OnAAAARecord(nsIDNSRecord* aRecord,
nsresult status,
uint64_t aId) {
LOG(("HappyEyeballsConnectionAttempt::OnAAAARecord: this=%p status %" PRIx32
" id=%" PRIu64,
this, static_cast<uint32_t>(status), aId));
if (NS_SUCCEEDED(status)) {
MaybeSendTransportStatus(NS_NET_STATUS_RESOLVED_HOST);
} else if (NS_FAILED(status)) {
mLastDnsError = status;
}
// TODO: use NS_ERROR_UNKNOWN_PROXY_HOST if stasus is failed and proxy is used
nsCOMPtr<nsIDNSAddrRecord> addrRecord = do_QueryInterface(aRecord);
nsresult rv;
if (NS_FAILED(status) || !addrRecord) {
if (mOriginDnsLookupIds.Contains(aId)) {
mOriginDnsLookupIds.Remove(aId);
MaybeBuildOriginCoalescingKeys();
}
nsTArray<NetAddr> emptyArray;
rv = happy_eyeballs_process_dns_response_aaaa(
mHappyEyeballs, aId, &emptyArray, mDnsMetadata.mIsTRR);
if (NS_FAILED(rv)) {
return rv;
}
return ProcessHappyEyeballsOutput();
}
nsTArray<NetAddr> addresses;
addrRecord->GetAddresses(addresses);
// Filter to only IPv6 addresses
nsTArray<NetAddr> ipv6Addresses;
for (const auto& addr : addresses) {
if (addr.raw.family == AF_INET6) {
LOG(("Addr=[%s]", addr.ToString().get()));
ipv6Addresses.AppendElement(addr);
}
}
if (mOriginDnsLookupIds.Contains(aId)) {
mOriginDnsLookupIds.Remove(aId);
mOriginAddresses.AppendElements(ipv6Addresses);
MaybeBuildOriginCoalescingKeys();
}
rv = happy_eyeballs_process_dns_response_aaaa(
mHappyEyeballs, aId, &ipv6Addresses, mDnsMetadata.mIsTRR);
if (NS_FAILED(rv)) {
return rv;
}
return ProcessHappyEyeballsOutput();
}
void HappyEyeballsConnectionAttempt::MaybeBuildOriginCoalescingKeys() {
// Called on each origin A/AAAA completion; wait until both have finished.
// Both lookups are issued up front, so this proceeds exactly once (when the
// last one completes).
if (!mOriginDnsLookupIds.IsEmpty()) {
return;
}
if (mOriginAddresses.IsEmpty() ||
!StaticPrefs::network_http_http2_coalesce_hostnames() ||
(!StaticPrefs::network_http_http2_enabled() &&
!nsHttpHandler::IsHttp3Enabled())) {
return;
}
RefPtr<ConnectionEntry> entry(mEntry);
if (!entry) {
return;
}
// The H2/H3 outcome isn't known yet; the coalescing keys are IP-based and
// identical either way. MaybeProcessCoalescingKeys still applies the
// EndToEndSSL / AllowHttp2 / proxy gating, and the connection is only
// registered in the coalescing hash at success (UpdateCoalescingForNewConn).
if (mConnMgrDelegate->MaybeProcessCoalescingKeys(entry, mOriginAddresses,
mConnInfo->IsHttp3())) {
mConnMgrDelegate->ProcessSpdyPendingQ(entry);
}
}
// Helper function to convert ALPN string to HttpVersion enum
static Maybe<happy_eyeballs::HttpVersion> AlpnStringToProtocol(
const nsACString& aAlpn) {
if (aAlpn.EqualsLiteral("h3")) {
return Some(happy_eyeballs::HttpVersion::H3);
}
if (aAlpn.EqualsLiteral("h2")) {
return Some(happy_eyeballs::HttpVersion::H2);
}
if (aAlpn.EqualsLiteral("http/1.1")) {
return Some(happy_eyeballs::HttpVersion::H1);
}
// Unknown ALPN protocol
return Nothing();
}
nsresult HappyEyeballsConnectionAttempt::OnHTTPSRecord(nsIDNSRecord* aRecord,
nsresult status,
uint64_t aId) {
LOG(("HappyEyeballsConnectionAttempt::OnHTTPSRecord %p status=%x id=%" PRIu64,
this, static_cast<uint32_t>(status), aId));
nsCOMPtr<nsIDNSHTTPSSVCRecord> httpsRecord = do_QueryInterface(aRecord);
if (!httpsRecord || NS_FAILED(status)) {
nsTArray<happy_eyeballs::ServiceInfo> emptyArray;
(void)happy_eyeballs_process_dns_response_https(
mHappyEyeballs, aId, &emptyArray, mDnsMetadata.mIsTRR);
return ProcessHappyEyeballsOutput();
}
bool httpsIsTRR = false;
(void)httpsRecord->IsTRR(&httpsIsTRR);
if (httpsIsTRR) {
mDnsMetadata.mIsTRR = true;
mDnsMetadata.mEffectiveTRRMode =
static_cast<nsIRequest::TRRMode>(StaticPrefs::network_trr_mode());
mDnsMetadata.mTrrSkipReason = nsITRRSkipReason::TRR_OK;
if (mTransaction && !mTRRInfoForwarded) {
mTransaction->SetTRRInfo(mDnsMetadata.mEffectiveTRRMode,
mDnsMetadata.mTrrSkipReason);
mTRRInfoForwarded = true;
}
}
nsTArray<RefPtr<nsISVCBRecord>> svcbRecords;
// TODO: Handle aNoHttp2, aNoHttp3, and aCname.
(void)httpsRecord->GetRecords(svcbRecords);
if (svcbRecords.IsEmpty()) {
nsTArray<happy_eyeballs::ServiceInfo> emptyArray;
(void)happy_eyeballs_process_dns_response_https(mHappyEyeballs, aId,
&emptyArray, httpsIsTRR);
return ProcessHappyEyeballsOutput();
}
nsTArray<happy_eyeballs::ServiceInfo> serviceInfos;
for (const auto& svcbRecord : svcbRecords) {
happy_eyeballs::ServiceInfo svcInfo;
(void)svcbRecord->GetPriority(&svcInfo.priority);
(void)svcbRecord->GetName(svcInfo.target_name);
svcInfo.port = svcbRecord->GetPort().valueOr(0);
nsTArray<RefPtr<nsISVCParam>> values;
(void)svcbRecord->GetValues(values);
nsTArray<nsCString> alpn;
bool noDefaultAlpn = false;
nsTArray<RefPtr<nsINetAddr>> ipv4Hint;
nsTArray<RefPtr<nsINetAddr>> ipv6Hint;
for (const auto& value : values) {
uint16_t type;
(void)value->GetType(&type);
switch (type) {
case SvcParamKeyAlpn: {
nsCOMPtr<nsISVCParamAlpn> alpnParam = do_QueryInterface(value);
(void)alpnParam->GetAlpn(alpn);
break;
}
case SvcParamKeyNoDefaultAlpn:
noDefaultAlpn = true;
break;
case SvcParamKeyIpv4Hint: {
nsCOMPtr<nsISVCParamIPv4Hint> ipv4Param = do_QueryInterface(value);
(void)ipv4Param->GetIpv4Hint(ipv4Hint);
break;
}
case SvcParamKeyIpv6Hint: {
nsCOMPtr<nsISVCParamIPv6Hint> ipv6Param = do_QueryInterface(value);
(void)ipv6Param->GetIpv6Hint(ipv6Hint);
break;
}
case SvcParamKeyEchConfig: {
nsCOMPtr<nsISVCParamEchConfig> echConfigParam =
do_QueryInterface(value);
nsCString echConfig;
(void)echConfigParam->GetEchconfig(echConfig);
svcInfo.ech_config.AppendElements(
reinterpret_cast<const uint8_t*>(echConfig.BeginReading()),
echConfig.Length());
break;
}
default:
break;
}
}
for (const auto& alpnStr : alpn) {
auto protocol = AlpnStringToProtocol(alpnStr);
if (protocol) {
svcInfo.alpn_http_versions.AppendElement(protocol.ref());
}
}
// The SVCB ALPN set includes the scheme default ("http/1.1") unless
// "no-default-alpn" is present (RFC 9460 Section 7.1.2).
if (!noDefaultAlpn &&
!svcInfo.alpn_http_versions.Contains(happy_eyeballs::HttpVersion::H1)) {
svcInfo.alpn_http_versions.AppendElement(happy_eyeballs::HttpVersion::H1);
}
for (const auto& addr : ipv4Hint) {
NetAddr netAddr;
addr->GetNetAddr(&netAddr);
svcInfo.ipv4_hints.AppendElement(netAddr);
}
for (const auto& addr : ipv6Hint) {
NetAddr netAddr;
addr->GetNetAddr(&netAddr);
svcInfo.ipv6_hints.AppendElement(netAddr);
}
serviceInfos.AppendElement(std::move(svcInfo));
}
(void)happy_eyeballs_process_dns_response_https(mHappyEyeballs, aId,
&serviceInfos, httpsIsTRR);
return ProcessHappyEyeballsOutput();
}
NS_IMETHODIMP // method for nsITimerCallback
HappyEyeballsConnectionAttempt::Notify(nsITimer* timer) {
return ProcessHappyEyeballsOutput();
}
NS_IMETHODIMP // method for nsINamed
HappyEyeballsConnectionAttempt::GetName(nsACString& aName) {
aName.AssignLiteral("HappyEyeballsConnectionAttempt");
return NS_OK;
}
void HappyEyeballsConnectionAttempt::SetupTimer(uint64_t aTimeout) {
if (!aTimeout) {
MOZ_ASSERT(false, "aTimeout should not be 0");
return;
}
LOG(("HappyEyeballsConnectionAttempt::SetupTimer to %" PRIu64 "ms [this=%p].",
aTimeout, this));
if (!mTimer) {
// This can only fail on OOM and we'd crash.
mTimer = NS_NewTimer();
}
DebugOnly<nsresult> rv =
mTimer->InitWithCallback(this, aTimeout, nsITimer::TYPE_ONE_SHOT);
// There is no meaningful error handling we can do here. But an error here
// should only be possible if the timer thread did already shut down.
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
} // namespace mozilla::net