Bug 725587 - Firefox jumps randomly from IPv6 to IPv4 and vice versa in dual-stack environment, r=mcmanus
authorHonza Bambas <honzab.moz@firemni.cz>
Tue, 29 Jan 2013 23:49:35 +0100
changeset 120304 51a772f811e2564cb21c2669c8544f3e7cd4e094
parent 120303 a6ec2ed0650436f561fb5270fe6a9f1f9353d134
child 120305 fce9b4a08399bb9fa7a4e8ddf1b3aefabd0c10b3
push id24246
push userryanvm@gmail.com
push dateWed, 30 Jan 2013 13:05:37 +0000
treeherdermozilla-central@5f9775715519 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcmanus
bugs725587
milestone21.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 725587 - Firefox jumps randomly from IPv6 to IPv4 and vice versa in dual-stack environment, r=mcmanus
netwerk/base/public/nsISocketTransport.idl
netwerk/base/src/nsSocketTransport2.cpp
netwerk/dns/nsDNSService2.cpp
netwerk/dns/nsIDNSService.idl
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpConnectionMgr.cpp
netwerk/protocol/http/nsHttpConnectionMgr.h
--- a/netwerk/base/public/nsISocketTransport.idl
+++ b/netwerk/base/public/nsISocketTransport.idl
@@ -159,16 +159,22 @@ interface nsISocketTransport : nsITransp
      * there should be no state shared between connections that are private
      * and those that are not; it is OK for multiple private connections
      * to share state with each other, and it is OK for multiple non-private
      * connections to share state with each other.
      */
     const unsigned long NO_PERMANENT_STORAGE = (1 << 3);
 
     /**
+     * If set, we will skip all IPv4 addresses the host may have and only
+     * connect to IPv6 ones.
+     */
+    const unsigned long DISABLE_IPV4 = (1 << 4);
+
+    /**
      * Socket QoS/ToS markings. Valid values are IPTOS_DSCP_AFxx or
      * IPTOS_CLASS_CSx (or IPTOS_DSCP_EF, but currently no supported
      * services require expedited-forwarding).
      * Not setting this value will leave the socket with the default
      * ToS value, which on most systems if IPTOS_CLASS_CS0 (formerly
      * IPTOS_PREC_ROUTINE).
      */
     attribute octet QoSBits;
--- a/netwerk/base/src/nsSocketTransport2.cpp
+++ b/netwerk/base/src/nsSocketTransport2.cpp
@@ -921,16 +921,22 @@ nsSocketTransport::ResolveHost()
 
     mResolving = true;
 
     uint32_t dnsFlags = 0;
     if (mConnectionFlags & nsSocketTransport::BYPASS_CACHE)
         dnsFlags = nsIDNSService::RESOLVE_BYPASS_CACHE;
     if (mConnectionFlags & nsSocketTransport::DISABLE_IPV6)
         dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV6;
+    if (mConnectionFlags & nsSocketTransport::DISABLE_IPV4)
+        dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV4;
+
+    NS_ASSERTION(!(dnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV6) ||
+                 !(dnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV4),
+                 "Setting both RESOLVE_DISABLE_IPV6 and RESOLVE_DISABLE_IPV4");
 
     SendStatus(NS_NET_STATUS_RESOLVING_HOST);
     rv = dns->AsyncResolve(SocketHost(), dnsFlags, this, nullptr,
                            getter_AddRefs(mDNSRequest));
     if (NS_SUCCEEDED(rv)) {
         SOCKET_LOG(("  advancing to STATE_RESOLVING\n"));
         mState = STATE_RESOLVING;
     }
@@ -1262,42 +1268,43 @@ nsSocketTransport::RecoverFromError()
         mCondition != NS_ERROR_PROXY_CONNECTION_REFUSED &&
         mCondition != NS_ERROR_NET_TIMEOUT &&
         mCondition != NS_ERROR_UNKNOWN_HOST &&
         mCondition != NS_ERROR_UNKNOWN_PROXY_HOST)
         return false;
 
     bool tryAgain = false;
 
-    if (mConnectionFlags & DISABLE_IPV6 &&
+    if (mConnectionFlags & (DISABLE_IPV6 | DISABLE_IPV4) &&
         mCondition == NS_ERROR_UNKNOWN_HOST &&
         mState == STATE_RESOLVING &&
         !mProxyTransparentResolvesHost) {
         SOCKET_LOG(("  trying lookup again with both ipv4/ipv6 enabled\n"));
-        mConnectionFlags &= ~DISABLE_IPV6;
+        mConnectionFlags &= ~(DISABLE_IPV6 | DISABLE_IPV4);
         tryAgain = true;
     }
 
     // try next ip address only if past the resolver stage...
     if (mState == STATE_CONNECTING && mDNSRecord) {
         mDNSRecord->ReportUnusable(SocketPort());
         
         nsresult rv = mDNSRecord->GetNextAddr(SocketPort(), &mNetAddr);
         if (NS_SUCCEEDED(rv)) {
             SOCKET_LOG(("  trying again with next ip address\n"));
             tryAgain = true;
         }
-        else if (mConnectionFlags & DISABLE_IPV6) {
+        else if (mConnectionFlags & (DISABLE_IPV6 | DISABLE_IPV4)) {
             // Drop state to closed.  This will trigger new round of DNS
             // resolving bellow.
-            // XXX Here should idealy be set now non-existing flag DISABLE_IPV4
-            SOCKET_LOG(("  failed to connect all ipv4 hosts,"
+            // XXX Could be optimized to only switch the flags to save duplicate
+            // connection attempts.
+            SOCKET_LOG(("  failed to connect all ipv4-only or ipv6-only hosts,"
                         " trying lookup/connect again with both ipv4/ipv6\n"));
             mState = STATE_CLOSED;
-            mConnectionFlags &= ~DISABLE_IPV6;
+            mConnectionFlags &= ~(DISABLE_IPV6 | DISABLE_IPV4);
             tryAgain = true;
         }
     }
 
 #if defined(XP_WIN) || defined(MOZ_PLATFORM_MAEMO)
     // If not trying next address, try to make a connection using dialup. 
     // Retry if that connection is made.
     if (!tryAgain) {
--- a/netwerk/dns/nsDNSService2.cpp
+++ b/netwerk/dns/nsDNSService2.cpp
@@ -845,16 +845,19 @@ nsDNSService::GetAFForLookup(const nsACS
                     }
                 }
             }
 
             domain = end + 1;
         } while (*end);
     }
 
+    if ((af != PR_AF_INET) && (flags & RESOLVE_DISABLE_IPV4))
+        af = PR_AF_INET6;
+
     return af;
 }
 
 NS_IMETHODIMP
 nsDNSService::GetDNSCacheEntries(nsTArray<mozilla::net::DNSCacheEntries> *args)
 {
     NS_ENSURE_TRUE(mResolver, NS_ERROR_NOT_INITIALIZED);
     mResolver->GetDNSCacheEntries(args);
--- a/netwerk/dns/nsIDNSService.idl
+++ b/netwerk/dns/nsIDNSService.idl
@@ -129,9 +129,14 @@ interface nsIDNSService : nsISupports
      */
     const unsigned long RESOLVE_DISABLE_IPV6 = (1 << 5);
 
     /**
      * If set, only literals and cached entries will be returned from resolve/
      * asyncResolve.
      */
     const unsigned long RESOLVE_OFFLINE = (1 << 6);
+
+    /**
+     * If set, only IPv6 addresses will be returned from resolve/asyncResolve.
+     */
+    const unsigned long RESOLVE_DISABLE_IPV4 = (1 << 7);
 };
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -4479,18 +4479,20 @@ nsHttpChannel::BeginConnect()
             mCaps |= NS_HTTP_LOAD_AS_BLOCKING;
         if (mLoadUnblocked)
             mCaps |= NS_HTTP_LOAD_UNBLOCKED;
     }
 
     // Force-Reload should reset the persistent connection pool for this host
     if (mLoadFlags & LOAD_FRESH_CONNECTION) {
         // just the initial document resets the whole pool
-        if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)
+        if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) {
             gHttpHandler->ConnMgr()->ClosePersistentConnections();
+            gHttpHandler->ConnMgr()->ResetIPFamillyPreference(mConnectionInfo);
+        }
         // each sub resource gets a fresh connection
         mCaps &= ~(NS_HTTP_ALLOW_KEEPALIVE | NS_HTTP_ALLOW_PIPELINING);
     }
 
     // We may have been cancelled already, either by on-modify-request
     // listeners or by load group observers; in that case, we should
     // not send the request to the server
     if (mCanceled)
--- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp
@@ -2516,18 +2516,23 @@ nsHalfOpenSocket::SetupStreams(nsISocket
     if (mEnt->mConnInfo->GetPrivate())
         tmpFlags |= nsISocketTransport::NO_PERMANENT_STORAGE;
 
     // For backup connections, we disable IPv6. That's because some users have
     // broken IPv6 connectivity (leading to very long timeouts), and disabling
     // IPv6 on the backup connection gives them a much better user experience
     // with dual-stack hosts, though they still pay the 250ms delay for each new
     // connection. This strategy is also known as "happy eyeballs".
-    if (isBackup && gHttpHandler->FastFallbackToIPv4())
+    if (mEnt->mPreferIPv6) {
+        tmpFlags |= nsISocketTransport::DISABLE_IPV4;
+    }
+    else if (mEnt->mPreferIPv4 ||
+             (isBackup && gHttpHandler->FastFallbackToIPv4())) {
         tmpFlags |= nsISocketTransport::DISABLE_IPV6;
+    }
 
     socketTransport->SetConnectionFlags(tmpFlags);
 
     socketTransport->SetQoSBits(gHttpHandler->GetQoSBits());
 
     rv = socketTransport->SetEventSink(this, nullptr);
     NS_ENSURE_SUCCESS(rv, rv);
 
@@ -2709,39 +2714,46 @@ nsHalfOpenSocket::OnOutputStreamReady(ns
 
     CancelBackupTimer();
 
     // assign the new socket to the http connection
     nsRefPtr<nsHttpConnection> conn = new nsHttpConnection();
     LOG(("nsHalfOpenSocket::OnOutputStreamReady "
          "Created new nshttpconnection %p\n", conn.get()));
 
+    NetAddr peeraddr;
     nsCOMPtr<nsIInterfaceRequestor> callbacks;
     mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks));
     if (out == mStreamOut) {
         TimeDuration rtt = TimeStamp::Now() - mPrimarySynStarted;
         rv = conn->Init(mEnt->mConnInfo,
                         gHttpHandler->ConnMgr()->mMaxRequestDelay,
                         mSocketTransport, mStreamIn, mStreamOut,
                         callbacks,
                         PR_MillisecondsToInterval(rtt.ToMilliseconds()));
 
+        if (NS_SUCCEEDED(mSocketTransport->GetPeerAddr(&peeraddr)))
+            mEnt->RecordIPFamilyPreference(peeraddr.raw.family);
+
         // The nsHttpConnection object now owns these streams and sockets
         mStreamOut = nullptr;
         mStreamIn = nullptr;
         mSocketTransport = nullptr;
     }
     else {
         TimeDuration rtt = TimeStamp::Now() - mBackupSynStarted;
         rv = conn->Init(mEnt->mConnInfo,
                         gHttpHandler->ConnMgr()->mMaxRequestDelay,
                         mBackupTransport, mBackupStreamIn, mBackupStreamOut,
                         callbacks,
                         PR_MillisecondsToInterval(rtt.ToMilliseconds()));
 
+        if (NS_SUCCEEDED(mBackupTransport->GetPeerAddr(&peeraddr)))
+            mEnt->RecordIPFamilyPreference(peeraddr.raw.family);
+
         // The nsHttpConnection object now owns these streams and sockets
         mBackupStreamOut = nullptr;
         mBackupStreamIn = nullptr;
         mBackupTransport = nullptr;
     }
 
     if (NS_FAILED(rv)) {
         LOG(("nsHalfOpenSocket::OnOutputStreamReady "
@@ -2944,16 +2956,18 @@ nsConnectionEntry::nsConnectionEntry(nsH
     , mYellowGoodEvents(0)
     , mYellowBadEvents(0)
     , mYellowConnection(nullptr)
     , mGreenDepth(kPipelineOpen)
     , mPipeliningPenalty(0)
     , mUsingSpdy(false)
     , mTestedSpdy(false)
     , mSpdyPreferred(false)
+    , mPreferIPv4(false)
+    , mPreferIPv6(false)
 {
     NS_ADDREF(mConnInfo);
     if (gHttpHandler->GetPipelineAggressive()) {
         mGreenDepth = kPipelineUnlimited;
         mPipelineState = PS_GREEN;
     }
     mInitialGreenDepth = mGreenDepth;
     memset(mPipeliningClassPenalty, 0, sizeof(int16_t) * nsAHttpTransaction::CLASS_MAX);
@@ -3099,17 +3113,18 @@ nsConnectionEntry::SetYellowConnection(n
 {
     NS_ABORT_IF_FALSE(!mYellowConnection && mPipelineState == PS_YELLOW,
                       "yellow connection already set or state is not yellow");
     mYellowConnection = conn;
     mYellowGoodEvents = mYellowBadEvents = 0;
 }
 
 void
-nsHttpConnectionMgr::nsConnectionEntry::OnYellowComplete()
+nsHttpConnectionMgr::
+nsConnectionEntry::OnYellowComplete()
 {
     if (mPipelineState == PS_YELLOW) {
         if (mYellowGoodEvents && !mYellowBadEvents) {
             LOG(("transition %s to green\n", mConnInfo->Host()));
             mPipelineState = PS_GREEN;
             mGreenDepth = mInitialGreenDepth;
         }
         else {
@@ -3122,17 +3137,18 @@ nsHttpConnectionMgr::nsConnectionEntry::
             mPipelineState = PS_RED;
         }
     }
 
     mYellowConnection = nullptr;
 }
 
 void
-nsHttpConnectionMgr::nsConnectionEntry::CreditPenalty()
+nsHttpConnectionMgr::
+nsConnectionEntry::CreditPenalty()
 {
     if (mLastCreditTime.IsNull())
         return;
     
     // Decrease penalty values by 1 for every 16 seconds
     // (i.e 3.7 per minute, or 1000 every 4h20m)
 
     TimeStamp now = TimeStamp::Now();
@@ -3218,18 +3234,27 @@ nsHttpConnectionMgr::ReadConnectionEntry
 
 bool
 nsHttpConnectionMgr::GetConnectionData(nsTArray<mozilla::net::HttpRetParams> *aArg)
 {
     mCT.Enumerate(ReadConnectionEntry, aArg);
     return true;
 }
 
+void
+nsHttpConnectionMgr::ResetIPFamillyPreference(nsHttpConnectionInfo *ci)
+{
+    nsConnectionEntry *ent = LookupConnectionEntry(ci, nullptr, nullptr);
+    if (ent)
+        ent->ResetIPFamilyPreference();
+}
+
 uint32_t
-nsHttpConnectionMgr::nsConnectionEntry::UnconnectedHalfOpens()
+nsHttpConnectionMgr::
+nsConnectionEntry::UnconnectedHalfOpens()
 {
     uint32_t unconnectedHalfOpens = 0;
     for (uint32_t i = 0; i < mHalfOpens.Length(); ++i) {
         if (!mHalfOpens[i]->HasConnected())
             ++unconnectedHalfOpens;
     }
     return unconnectedHalfOpens;
 }
@@ -3245,8 +3270,27 @@ nsConnectionEntry::RemoveHalfOpen(nsHalf
     gHttpHandler->ConnMgr()->mNumHalfOpenConns--;
 
     if (!UnconnectedHalfOpens())
         // perhaps this reverted RestrictConnections()
         // use the PostEvent version of processpendingq to avoid
         // altering the pending q vector from an arbitrary stack
         gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo);
 }
+
+void
+nsHttpConnectionMgr::
+nsConnectionEntry::RecordIPFamilyPreference(uint16_t family)
+{
+  if (family == PR_AF_INET && !mPreferIPv6)
+    mPreferIPv4 = true;
+
+  if (family == PR_AF_INET6 && !mPreferIPv4)
+    mPreferIPv6 = true;
+}
+
+void
+nsHttpConnectionMgr::
+nsConnectionEntry::ResetIPFamilyPreference()
+{
+  mPreferIPv4 = false;
+  mPreferIPv6 = false;
+}
--- a/netwerk/protocol/http/nsHttpConnectionMgr.h
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.h
@@ -220,16 +220,19 @@ public:
     // upgraded to SPDY because the dispatch and idle semantics are a little
     // bit different.
     void ReportSpdyConnection(nsHttpConnection *, bool usingSpdy);
 
     
     bool     SupportsPipelining(nsHttpConnectionInfo *);
 
     bool GetConnectionData(nsTArray<mozilla::net::HttpRetParams> *);
+
+    void ResetIPFamillyPreference(nsHttpConnectionInfo *);
+
 private:
     virtual ~nsHttpConnectionMgr();
 
     enum PipeliningState {
         // Host has proven itself pipeline capable through past experience and
         // large pipeline depths are allowed on multiple connections.
         PS_GREEN,
 
@@ -338,16 +341,30 @@ private:
 
         // mTestedSpdy is set after NPN negotiation has occurred and we know
         // with confidence whether a host speaks spdy or not (which is reflected
         // in mUsingSpdy). Before mTestedSpdy is set, handshake parallelism is
         // minimized so that we can multiplex on a single spdy connection.
         bool mTestedSpdy;
 
         bool mSpdyPreferred;
+
+        // Flags to remember our happy-eyeballs decision.
+        // Reset only by Ctrl-F5 reload.
+        // True when we've first connected an IPv4 server for this host,
+        // initially false.
+        bool mPreferIPv4 : 1;
+        // True when we've first connected an IPv6 server for this host,
+        // initially false.
+        bool mPreferIPv6 : 1;
+
+        // Set the IP family preference flags according the connected family
+        void RecordIPFamilyPreference(uint16_t family);
+        // Resets all flags to their default values
+        void ResetIPFamilyPreference();
     };
 
     // nsConnectionHandle
     //
     // thin wrapper around a real connection, used to keep track of references
     // to the connection to determine when the connection may be reused.  the
     // transaction (or pipeline) owns a reference to this handle.  this extra
     // layer of indirection greatly simplifies consumer code, avoiding the