Bug 1816677 - Allow to retry diffrent IP family for Http/3, r=necko-reviewers,valentin

Differential Revision: https://phabricator.services.mozilla.com/D180679
This commit is contained in:
Kershaw Chang
2023-06-28 17:20:41 +00:00
parent 316511ef09
commit b766c33afe
19 changed files with 445 additions and 37 deletions

View File

@@ -12510,6 +12510,19 @@
value: false
mirror: always
# When a Http/3 connection failed, whether to retry with a different IP address.
- name: network.http.http3.retry_different_ip_family
type: RelaxedAtomicBool
value: true
mirror: always
# This is for testing purpose. When true, nsUDPSocket::SendWithAddress will
# return NS_ERROR_CONNECTION_REFUSED for address "::1".
- name: network.http.http3.block_loopback_ipv6_addr
type: RelaxedAtomicBool
value: false
mirror: always
# When true, a http request will be upgraded to https when HTTPS RR is
# available.
- name: network.dns.upgrade_with_https_rr

View File

@@ -1147,6 +1147,11 @@ nsUDPSocket::SendWithAddress(const NetAddr* aAddr,
NS_ENSURE_ARG(aAddr);
NS_ENSURE_ARG_POINTER(_retval);
if (StaticPrefs::network_http_http3_block_loopback_ipv6_addr() &&
aAddr->raw.family == AF_INET6 && aAddr->IsLoopbackAddr()) {
return NS_ERROR_CONNECTION_REFUSED;
}
*_retval = 0;
PRNetAddr prAddr;

View File

@@ -1004,5 +1004,55 @@ nsresult ConnectionEntry::CreateDnsAndConnectSocket(
return NS_OK;
}
bool ConnectionEntry::AllowToRetryDifferentIPFamilyForHttp3(nsresult aError) {
LOG(
("ConnectionEntry::AllowToRetryDifferentIPFamilyForHttp3 %p "
"error=%" PRIx32,
this, static_cast<uint32_t>(aError)));
if (!IsHttp3()) {
MOZ_ASSERT(false, "Should not be called for non Http/3 connection");
return false;
}
if (!StaticPrefs::network_http_http3_retry_different_ip_family()) {
return false;
}
// Only allow to retry with these two errors.
if (aError != NS_ERROR_CONNECTION_REFUSED &&
aError != NS_ERROR_PROXY_CONNECTION_REFUSED) {
return false;
}
// Already retried once.
if (mRetriedDifferentIPFamilyForHttp3) {
return false;
}
return true;
}
void ConnectionEntry::SetRetryDifferentIPFamilyForHttp3(uint16_t aIPFamily) {
LOG(("ConnectionEntry::SetRetryDifferentIPFamilyForHttp3 %p, af=%u", this,
aIPFamily));
mPreferIPv4 = false;
mPreferIPv6 = false;
if (aIPFamily == AF_INET) {
mPreferIPv6 = true;
}
if (aIPFamily == AF_INET6) {
mPreferIPv4 = true;
}
mRetriedDifferentIPFamilyForHttp3 = true;
LOG((" %p prefer ipv4=%d, ipv6=%d", this, (bool)mPreferIPv4,
(bool)mPreferIPv6));
MOZ_DIAGNOSTIC_ASSERT(mPreferIPv4 ^ mPreferIPv6);
}
} // namespace net
} // namespace mozilla

View File

@@ -195,6 +195,9 @@ class ConnectionEntry {
void MaybeUpdateEchConfig(nsHttpConnectionInfo* aConnInfo);
bool AllowToRetryDifferentIPFamilyForHttp3(nsresult aError);
void SetRetryDifferentIPFamilyForHttp3(uint16_t aIPFamily);
private:
void InsertIntoIdleConnections_internal(nsHttpConnection* conn);
void RemoveFromIdleConnectionsIndex(size_t inx);
@@ -211,6 +214,8 @@ class ConnectionEntry {
PendingTransactionQueue mPendingQ;
~ConnectionEntry();
bool mRetriedDifferentIPFamilyForHttp3 = false;
};
} // namespace net

View File

@@ -417,8 +417,10 @@ DnsAndConnectSocket::GetName(nsACString& aName) {
NS_IMETHODIMP
DnsAndConnectSocket::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec,
nsresult status) {
LOG(("DnsAndConnectSocket::OnLookupComplete: this=%p status %" PRIx32 ".",
this, static_cast<uint32_t>(status)));
LOG((
"DnsAndConnectSocket::OnLookupComplete: this=%p mState=%d status %" PRIx32
".",
this, mState, static_cast<uint32_t>(status)));
if (nsCOMPtr<nsIDNSAddrRecord> addrRecord = do_QueryInterface((rec))) {
nsIRequest::TRRMode effectivemode = nsIRequest::TRR_DEFAULT_MODE;
@@ -465,6 +467,13 @@ DnsAndConnectSocket::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec,
}
if (NS_FAILED(rv) || mIsHttp3) {
// If we are retrying DNS, we should not setup the connection.
if (mIsHttp3 && mPrimaryTransport.mState ==
TransportSetup::TransportSetupState::RETRY_RESOLVING) {
LOG(("Retry DNS for Http3"));
return NS_OK;
}
// Before calling SetupConn we need to hold reference to this, i.e. a
// delete protector, because the corresponding ConnectionEntry may be
// abandoned and that will abandon this DnsAndConnectSocket.

View File

@@ -230,11 +230,18 @@ void Http3Session::Shutdown() {
bool isEchRetry = mError == mozilla::psm::GetXPCOMFromNSSError(
SSL_ERROR_ECH_RETRY_WITH_ECH);
bool allowToRetryWithDifferentIPFamily =
mBeforeConnectedError &&
gHttpHandler->ConnMgr()->AllowToRetryDifferentIPFamilyForHttp3(mConnInfo,
mError);
LOG(("Http3Session::Shutdown %p allowToRetryWithDifferentIPFamily=%d", this,
allowToRetryWithDifferentIPFamily));
if ((mBeforeConnectedError ||
(mError == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR)) &&
(mError !=
mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN)) &&
!isEchRetry && !mConnInfo->GetWebTransport()) {
!isEchRetry && !mConnInfo->GetWebTransport() &&
!allowToRetryWithDifferentIPFamily && !mDontExclude) {
gHttpHandler->ExcludeHttp3(mConnInfo);
}
@@ -248,9 +255,30 @@ void Http3Session::Shutdown() {
// We have to propagate this error to nsHttpTransaction, so the
// transaction will be restarted with a new echConfig.
stream->Close(mError);
} else {
if (allowToRetryWithDifferentIPFamily && mNetAddr) {
NetAddr addr;
mNetAddr->GetNetAddr(&addr);
gHttpHandler->ConnMgr()->SetRetryDifferentIPFamilyForHttp3(
mConnInfo, addr.raw.family);
nsHttpTransaction* trans =
stream->Transaction()->QueryHttpTransaction();
if (trans) {
// This is a bit hacky. We redispatch the transaction here to avoid
// touching the complicated retry logic in nsHttpTransaction.
trans->RemoveConnection();
Unused << gHttpHandler->InitiateTransaction(trans,
trans->Priority());
} else {
stream->Close(NS_ERROR_NET_RESET);
}
// Since Http3Session::Shutdown can be called multiple times, we set
// mDontExclude for not putting this domain into the excluded list.
mDontExclude = true;
} else {
stream->Close(NS_ERROR_NET_RESET);
}
}
} else if (!stream->HasStreamId()) {
if (NS_SUCCEEDED(mError)) {
// Connection has not been started yet. We can restart it.

View File

@@ -370,6 +370,8 @@ class Http3Session final : public nsAHttpTransaction, public nsAHttpConnection {
nsTArray<RefPtr<Http3StreamBase>> mWebTransportStreams;
bool mHasWebTransportSession = false;
// When true, we don't add this connection info into the Http/3 excluded list.
bool mDontExclude = false;
};
NS_DEFINE_STATIC_IID_ACCESSOR(Http3Session, NS_HTTP3SESSION_IID);

View File

@@ -5962,9 +5962,15 @@ HttpBaseChannel::CancelByURLClassifier(nsresult aErrorCode) {
return Cancel(aErrorCode);
}
void HttpBaseChannel::SetIPv4Disabled() { mCaps |= NS_HTTP_DISABLE_IPV4; }
NS_IMETHODIMP HttpBaseChannel::SetIPv4Disabled() {
mCaps |= NS_HTTP_DISABLE_IPV4;
return NS_OK;
}
void HttpBaseChannel::SetIPv6Disabled() { mCaps |= NS_HTTP_DISABLE_IPV6; }
NS_IMETHODIMP HttpBaseChannel::SetIPv6Disabled() {
mCaps |= NS_HTTP_DISABLE_IPV6;
return NS_OK;
}
NS_IMETHODIMP HttpBaseChannel::GetResponseEmbedderPolicy(
bool aIsOriginTrialCoepCredentiallessEnabled,

View File

@@ -331,8 +331,8 @@ class HttpBaseChannel : public nsHashPropertyBag,
NS_IMETHOD GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) override;
NS_IMETHOD SetNavigationStartTimeStamp(TimeStamp aTimeStamp) override;
NS_IMETHOD CancelByURLClassifier(nsresult aErrorCode) override;
virtual void SetIPv4Disabled(void) override;
virtual void SetIPv6Disabled(void) override;
NS_IMETHOD SetIPv4Disabled(void) override;
NS_IMETHOD SetIPv6Disabled(void) override;
NS_IMETHOD GetCrossOriginOpenerPolicy(
nsILoadInfo::CrossOriginOpenerPolicy* aCrossOriginOpenerPolicy) override;
NS_IMETHOD ComputeCrossOriginOpenerPolicy(

View File

@@ -55,7 +55,6 @@ nsresult HttpConnectionUDP::Init(nsHttpConnectionInfo* info,
LOG1(("HttpConnectionUDP::Init this=%p", this));
NS_ENSURE_ARG_POINTER(info);
NS_ENSURE_TRUE(!mConnInfo, NS_ERROR_ALREADY_INITIALIZED);
MOZ_ASSERT(dnsRecord || NS_FAILED(status));
mConnInfo = info;
MOZ_ASSERT(mConnInfo);
@@ -72,7 +71,6 @@ nsresult HttpConnectionUDP::Init(nsHttpConnectionInfo* info,
}
nsCOMPtr<nsIDNSAddrRecord> dnsAddrRecord = do_QueryInterface(dnsRecord);
MOZ_ASSERT(dnsAddrRecord);
if (!dnsAddrRecord) {
return NS_ERROR_FAILURE;
}

View File

@@ -405,18 +405,6 @@ void ObliviousHttpChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() {
}
}
void ObliviousHttpChannel::SetIPv6Disabled() {
if (mInnerChannelInternal) {
mInnerChannelInternal->SetIPv6Disabled();
}
}
void ObliviousHttpChannel::SetIPv4Disabled() {
if (mInnerChannelInternal) {
mInnerChannelInternal->SetIPv4Disabled();
}
}
void ObliviousHttpChannel::DisableAltDataCache() {
if (mInnerChannelInternal) {
mInnerChannelInternal->DisableAltDataCache();

View File

@@ -3818,4 +3818,24 @@ void nsHttpConnectionMgr::CheckTransInPendingQueue(nsHttpTransaction* aTrans) {
#endif
}
bool nsHttpConnectionMgr::AllowToRetryDifferentIPFamilyForHttp3(
nsHttpConnectionInfo* ci, nsresult aError) {
ConnectionEntry* ent = mCT.GetWeak(ci->HashKey());
if (!ent) {
return false;
}
return ent->AllowToRetryDifferentIPFamilyForHttp3(aError);
}
void nsHttpConnectionMgr::SetRetryDifferentIPFamilyForHttp3(
nsHttpConnectionInfo* ci, uint16_t aIPFamily) {
ConnectionEntry* ent = mCT.GetWeak(ci->HashKey());
if (!ent) {
return;
}
ent->SetRetryDifferentIPFamilyForHttp3(aIPFamily);
}
} // namespace mozilla::net

View File

@@ -191,6 +191,11 @@ class nsHttpConnectionMgr final : public HttpConnectionMgrShell,
// connection
bool BeConservativeIfProxied(nsIProxyInfo* proxy);
bool AllowToRetryDifferentIPFamilyForHttp3(nsHttpConnectionInfo* ci,
nsresult aError);
void SetRetryDifferentIPFamilyForHttp3(nsHttpConnectionInfo* ci,
uint16_t aIPFamily);
protected:
friend class ConnectionEntry;
void IncrementActiveConnCount();

View File

@@ -3534,4 +3534,9 @@ void nsHttpTransaction::SetIsForWebTransport(bool aIsForWebTransport) {
mIsForWebTransport = aIsForWebTransport;
}
void nsHttpTransaction::RemoveConnection() {
MutexAutoLock lock(mLock);
mConnection = nullptr;
}
} // namespace mozilla::net

View File

@@ -88,7 +88,7 @@ class nsHttpTransaction final : public nsAHttpTransaction,
void MakeNonSticky() override { mCaps &= ~NS_HTTP_STICKY_CONNECTION; }
void MakeRestartable() override { mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; }
void MakeNonRestartable() { mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; }
void RemoveConnection();
void SetIsHttp2Websocket(bool h2ws) override { mIsHttp2Websocket = h2ws; }
bool IsHttp2Websocket() override { return mIsHttp2Websocket; }

View File

@@ -434,13 +434,11 @@ interface nsIHttpChannelInternal : nsISupports
/**
* The channel will be loaded over IPv6, disabling IPv4.
*/
[noscript, notxpcom, nostdcall]
void setIPv4Disabled();
/**
* The channel will be loaded over IPv4, disabling IPv6.
*/
[noscript, notxpcom, nostdcall]
void setIPv6Disabled();
/**

View File

@@ -1098,18 +1098,6 @@ nsViewSourceChannel::PreferredAlternativeDataTypes() {
return mEmptyArray;
}
void nsViewSourceChannel::SetIPv4Disabled() {
if (mHttpChannelInternal) {
mHttpChannelInternal->SetIPv4Disabled();
}
}
void nsViewSourceChannel::SetIPv6Disabled() {
if (mHttpChannelInternal) {
mHttpChannelInternal->SetIPv6Disabled();
}
}
void nsViewSourceChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() {
if (mHttpChannelInternal) {
mHttpChannelInternal->DoDiagnosticAssertWhenOnStopNotCalledOnDestroy();

View File

@@ -0,0 +1,283 @@
/* 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/. */
"use strict";
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
let h2Port;
let h3Port;
let trrServer;
const { TestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TestUtils.sys.mjs"
);
const certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
add_setup(async function setup() {
h2Port = Services.env.get("MOZHTTP2_PORT");
Assert.notEqual(h2Port, null);
Assert.notEqual(h2Port, "");
h3Port = Services.env.get("MOZHTTP3_PORT");
Assert.notEqual(h3Port, null);
Assert.notEqual(h3Port, "");
trr_test_setup();
if (mozinfo.socketprocess_networking) {
Services.dns; // Needed to trigger socket process.
await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched);
}
Services.prefs.setIntPref("network.trr.mode", 2); // TRR first
Services.prefs.setBoolPref("network.http.http3.enable", true);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
Services.prefs.setBoolPref(
"network.http.http3.block_loopback_ipv6_addr",
true
);
registerCleanupFunction(async () => {
trr_clear_prefs();
Services.prefs.clearUserPref("network.http.http3.block_loopback_ipv6_addr");
if (trrServer) {
await trrServer.stop();
}
});
});
function makeChan(url) {
let chan = NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
return chan;
}
function channelOpenPromise(chan, flags) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
function finish(req, buffer) {
resolve([req, buffer]);
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
false
);
}
let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
internal.setWaitForHTTPSSVCRecord();
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
true
);
chan.asyncOpen(new ChannelListener(finish, null, flags));
});
}
async function registerDoHAnswers(host, ipv4Answers, ipv6Answers, httpsRecord) {
trrServer = new TRRServer();
await trrServer.start();
Services.prefs.setIntPref("network.trr.mode", 3);
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port}/dns-query`
);
await trrServer.registerDoHAnswers(host, "HTTPS", {
answers: httpsRecord,
});
await trrServer.registerDoHAnswers(host, "AAAA", {
answers: ipv6Answers,
});
await trrServer.registerDoHAnswers(host, "A", {
answers: ipv4Answers,
});
Services.dns.clearCache(true);
}
// Test if we retry IPv4 address for Http/3 properly.
add_task(async function test_retry_with_ipv4() {
let host = "test.http3_retry.com";
let ipv4answers = [
{
name: host,
ttl: 55,
type: "A",
flush: false,
data: "127.0.0.1",
},
];
// The UDP socket will return connection refused error because we set
// "network.http.http3.block_loopback_ipv6_addr" to true.
let ipv6answers = [
{
name: host,
ttl: 55,
type: "AAAA",
flush: false,
data: "::1",
},
];
let httpsRecord = [
{
name: host,
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: host,
values: [
{ key: "alpn", value: "h3-29" },
{ key: "port", value: h3Port },
],
},
},
];
await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
let chan = makeChan(`https://${host}`);
let [req] = await channelOpenPromise(chan);
Assert.equal(req.protocolVersion, "h3-29");
await trrServer.stop();
});
add_task(async function test_retry_with_ipv4_disabled() {
let host = "test.http3_retry_ipv4_blocked.com";
let ipv4answers = [
{
name: host,
ttl: 55,
type: "A",
flush: false,
data: "127.0.0.1",
},
];
// The UDP socket will return connection refused error because we set
// "network.http.http3.block_loopback_ipv6_addr" to true.
let ipv6answers = [
{
name: host,
ttl: 55,
type: "AAAA",
flush: false,
data: "::1",
},
];
let httpsRecord = [
{
name: host,
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: host,
values: [
{ key: "alpn", value: "h3-29" },
{ key: "port", value: h3Port },
],
},
},
];
await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
let chan = makeChan(`https://${host}`);
chan.QueryInterface(Ci.nsIHttpChannelInternal);
chan.setIPv4Disabled();
await channelOpenPromise(chan, CL_EXPECT_FAILURE);
await trrServer.stop();
});
// See bug 1837252. There is no way to observe the outcome of this test, because
// the crash in bug 1837252 is only triggered by speculative connection.
// The outcome of this test is no crash.
add_task(async function test_retry_with_ipv4_failed() {
let host = "test.http3_retry_failed.com";
// Return a wrong answer intentionally.
let ipv4answers = [
{
name: host,
ttl: 55,
type: "AAAA",
flush: false,
data: "127.0.0.1",
},
];
// The UDP socket will return connection refused error because we set
// "network.http.http3.block_loopback_ipv6_addr" to true.
let ipv6answers = [
{
name: host,
ttl: 55,
type: "AAAA",
flush: false,
data: "::1",
},
];
let httpsRecord = [
{
name: host,
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: host,
values: [
{ key: "alpn", value: "h3-29" },
{ key: "port", value: h3Port },
],
},
},
];
await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord);
// This speculative connection is used to trigger the mechanism to retry
// Http/3 connection with a IPv4 address.
// We want to make the connection entry's IP preference known,
// so DnsAndConnectSocket::mRetryWithDifferentIPFamily will be set to true
// before the second speculative connection.
let uri = Services.io.newURI(`https://test.http3_retry_failed.com`);
Services.io.speculativeConnect(
uri,
Services.scriptSecurityManager.getSystemPrincipal(),
null,
false
);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 3000));
// When this speculative connection is created, the connection entry is
// already set to prefer IPv4. Since we provided an invalid A response,
// DnsAndConnectSocket::OnLookupComplete is called with an error.
// Since DnsAndConnectSocket::mRetryWithDifferentIPFamily is true, we do
// retry DNS lookup. During retry, we should not create UDP connection.
Services.io.speculativeConnect(
uri,
Services.scriptSecurityManager.getSystemPrincipal(),
null,
false
);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 3000));
await trrServer.stop();
});

View File

@@ -771,3 +771,8 @@ skip-if =
skip-if =
os == 'android'
socketprocess_networking
[test_http3_dns_retry.js]
skip-if =
os == 'android'
os == 'win' && msix
run-sequentially = node server exceptions dont replay well