tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 081d14091f01a00a6097f30bf40f614129a83992
parent c491c80c0b0fcf7215cc22f079761f841e72a02f
Author: smayya <smayya@mozilla.com>
Date:   Thu, 16 Oct 2025 19:34:14 +0000

Bug 1988152 - support configuration to skip LNA checks target domains. r=necko-reviewers,valentin

Support suffix wildcard matching and exact domain matching configuration through the pref.

Differential Revision: https://phabricator.services.mozilla.com/D266982

Diffstat:
Mmodules/libpref/init/StaticPrefList.yaml | 7+++++++
Mnetwerk/base/LNAPermissionRequest.cpp | 14++++++++++++++
Mnetwerk/base/nsIOService.cpp | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/base/nsIOService.h | 4++++
Mnetwerk/protocol/http/nsHttpTransaction.cpp | 8++++++++
Mnetwerk/test/gtest/TestLocalNetworkAccess.cpp | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/unit/test_local_network_access.js | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 462 insertions(+), 0 deletions(-)

diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -14475,6 +14475,13 @@ value: true mirror: always +# Comma-separated list of domains to skip LNA checks for. +# Supports suffix wildcard patterns (*.example.com) +- name: network.lna.skip-domains + type: String + value: "" + mirror: never + # The proxy type. See nsIProtocolProxyService.idl # PROXYCONFIG_DIRECT = 0 # PROXYCONFIG_MANUAL = 1 diff --git a/netwerk/base/LNAPermissionRequest.cpp b/netwerk/base/LNAPermissionRequest.cpp @@ -13,6 +13,8 @@ #include "mozilla/glean/NetwerkMetrics.h" #include "mozilla/dom/WindowGlobalParent.h" +#include "nsIIOService.h" +#include "nsIOService.h" namespace mozilla::net { @@ -107,6 +109,18 @@ nsresult LNAPermissionRequest::RequestPermission() { return Cancel(); } + // Check if the domain should skip LNA checks + if (mPrincipal && gIOService) { + nsAutoCString origin; + nsresult rv = mPrincipal->GetAsciiHost(origin); + if (NS_SUCCEEDED(rv) && !origin.IsEmpty()) { + if (gIOService->ShouldSkipDomainForLNA(origin)) { + // Domain is in the skip list, grant permission automatically + return Allow(JS::UndefinedHandleValue); + } + } + } + PromptResult pr = CheckPromptPrefs(); if (pr == PromptResult::Granted) { return Allow(JS::UndefinedHandleValue); diff --git a/netwerk/base/nsIOService.cpp b/netwerk/base/nsIOService.cpp @@ -105,6 +105,7 @@ using mozilla::dom::ServiceWorkerDescriptor; #define PREF_LNA_IP_ADDR_SPACE_PRIVATE \ "network.lna.address_space.private.override" #define PREF_LNA_IP_ADDR_SPACE_LOCAL "network.lna.address_space.local.override" +#define PREF_LNA_SKIP_DOMAINS "network.lna.skip-domains" nsIOService* gIOService; static bool gCaptivePortalEnabled = false; @@ -234,6 +235,7 @@ static const char* gCallbackPrefs[] = { PREF_LNA_IP_ADDR_SPACE_PUBLIC, PREF_LNA_IP_ADDR_SPACE_PRIVATE, PREF_LNA_IP_ADDR_SPACE_LOCAL, + PREF_LNA_SKIP_DOMAINS, nullptr, }; @@ -1662,6 +1664,10 @@ void nsIOService::PrefsChanged(const char* pref) { UpdateAddressSpaceOverrideList(PREF_LNA_IP_ADDR_SPACE_LOCAL, mLocalAddressSpaceOverrideList); } + if (!pref || strncmp(pref, PREF_LNA_SKIP_DOMAINS, + strlen(PREF_LNA_SKIP_DOMAINS)) == 0) { + UpdateSkipDomainsList(); + } } void nsIOService::UpdateAddressSpaceOverrideList( @@ -1680,6 +1686,52 @@ void nsIOService::UpdateAddressSpaceOverrideList( aTargetList = std::move(addressSpaceOverridesArray); } +void nsIOService::UpdateSkipDomainsList() { + nsAutoCString skipDomains; + Preferences::GetCString(PREF_LNA_SKIP_DOMAINS, skipDomains); + + nsTArray<nsCString> skipDomainsArray; + nsCCharSeparatedTokenizer tokenizer(skipDomains, ','); + while (tokenizer.hasMoreTokens()) { + nsAutoCString token(tokenizer.nextToken()); + token.StripWhitespace(); + if (!token.IsEmpty()) { + skipDomainsArray.AppendElement(token); + } + } + + AutoWriteLock lock(mLock); + mLNASkipDomainsList = std::move(skipDomainsArray); +} + +bool nsIOService::ShouldSkipDomainForLNA(const nsACString& aDomain) { + AutoReadLock lock(mLock); + + // Check each domain pattern + for (const auto& pattern : mLNASkipDomainsList) { + // Special case: plain "*" matches all domains + if (pattern.Equals("*"_ns)) { + return true; + } + + // Suffix wildcard pattern (starts with *.) + if (StringBeginsWith(pattern, "*."_ns)) { + nsDependentCSubstring suffix(Substring(pattern, 2)); + nsDependentCSubstring suffixWithDot(Substring(pattern, 1)); + if (aDomain == suffix || StringEndsWith(aDomain, suffixWithDot)) { + return true; + } + } + + // Exact match + if (pattern == aDomain) { + return true; + } + } + + return false; +} + void nsIOService::ParsePortList(const char* pref, bool remove) { nsAutoCString portList; nsTArray<int32_t> restrictedPortList; diff --git a/netwerk/base/nsIOService.h b/netwerk/base/nsIOService.h @@ -158,6 +158,8 @@ class nsIOService final : public nsIIOService, NS_IMETHODIMP GetOverridenIpAddressSpace( nsILoadInfo::IPAddressSpace* aIpAddressSpace, const NetAddr& aAddr); + bool ShouldSkipDomainForLNA(const nsACString& aDomain); + private: // These shouldn't be called directly: // - construct using GetInstance @@ -213,6 +215,7 @@ class nsIOService final : public nsIIOService, void UpdateAddressSpaceOverrideList(const char* aPrefName, nsTArray<nsCString>& aTargetList); + void UpdateSkipDomainsList(); private: mozilla::Atomic<bool, mozilla::Relaxed> mOffline{true}; @@ -247,6 +250,7 @@ class nsIOService final : public nsIIOService, nsTArray<nsCString> mPublicAddressSpaceOverridesList MOZ_GUARDED_BY(mLock); nsTArray<nsCString> mPrivateAddressSpaceOverridesList MOZ_GUARDED_BY(mLock); nsTArray<nsCString> mLocalAddressSpaceOverrideList MOZ_GUARDED_BY(mLock); + nsTArray<nsCString> mLNASkipDomainsList MOZ_GUARDED_BY(mLock); nsTHashMap<nsCString, RuntimeProtocolHandler> mRuntimeProtocolHandlers MOZ_GUARDED_BY(mLock); diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -63,6 +63,7 @@ #include "nsTransportUtils.h" #include "sslerr.h" #include "SpeculativeTransaction.h" +#include "mozilla/Preferences.h" //----------------------------------------------------------------------------- @@ -3737,10 +3738,17 @@ nsILoadInfo::IPAddressSpace nsHttpTransaction::GetTargetIPAddressSpace() { bool nsHttpTransaction::AllowedToConnectToIpAddressSpace( nsILoadInfo::IPAddressSpace aTargetIpAddressSpace) { // skip checks if LNA feature is disabled + if (!StaticPrefs::network_lna_enabled()) { return true; } + // Skip LNA checks if domain is in skip list + if (mConnInfo && gIOService && + gIOService->ShouldSkipDomainForLNA(mConnInfo->GetOrigin())) { + return true; + } + // store targetIpAddress space which is required later by nsHttpChannel for // permission prompts { diff --git a/netwerk/test/gtest/TestLocalNetworkAccess.cpp b/netwerk/test/gtest/TestLocalNetworkAccess.cpp @@ -8,6 +8,8 @@ #include "mozilla/StaticPrefs_network.h" #include "mozilla/Preferences.h" #include "mozilla/net/DNS.h" +#include "nsNetUtil.h" +#include "nsIOService.h" TEST(TestNetAddrLNAUtil, IPAddressSpaceCategorization) { @@ -150,3 +152,114 @@ TEST(TestNetAddrLNAUtil, DefaultAndOverrideTransitions) << "Expected reset back to default space for " << tc.ip; } } + +TEST(TestNetAddrLNAUtil, ShouldSkipDomainForLNA) +{ + using mozilla::Preferences; + // Get nsIOService instance + mozilla::net::nsIOService* ioService = mozilla::net::gIOService; + ASSERT_NE(ioService, nullptr); + + // Test with empty preference (should not skip any domains) + Preferences::SetCString("network.lna.skip-domains", ""_ns); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("example.com"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("test.example.com"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("localhost"_ns)); + + // Test exact domain matching + Preferences::SetCString("network.lna.skip-domains", + "example.com,test.org"_ns); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("example.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("test.org"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("sub.example.com"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("example.org"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("notexample.com"_ns)); + + // Test wildcard domain matching + Preferences::SetCString("network.lna.skip-domains", + "*.example.com,*.test.org"_ns); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("sub.example.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("deep.sub.example.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("api.test.org"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "example.com"_ns)); // Should match exact domain too + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "test.org"_ns)); // Should match exact domain too + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("example.net"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("notexample.com"_ns)); + + // Test more suffix wildcard patterns + Preferences::SetCString("network.lna.skip-domains", + "*.local,*.internal,*.test"_ns); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("server.local"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("api.internal"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("service.test"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("deep.subdomain.local"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "local"_ns)); // Should match exact domain too + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "internal"_ns)); // Should match exact domain too + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("local.example.com"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("localhost"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("example.com"_ns)); + + // Test mixed patterns (exact and suffix wildcard) + Preferences::SetCString( + "network.lna.skip-domains", + "localhost,*.dev.local,*.staging.com,production.example.com"_ns); + // Exact matches + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("localhost"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("production.example.com"_ns)); + // Suffix wildcard matches + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("api.dev.local"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("web.dev.local"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("dev.local"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("test.staging.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("api.staging.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("staging.com"_ns)); + // Non-matches + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("example.com"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("dev.example.com"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("staging.example.com"_ns)); + + // Test with whitespace and empty entries + Preferences::SetCString( + "network.lna.skip-domains", + " example.com , , *.test.local , admin.internal "_ns); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("example.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("api.test.local"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("admin.internal"_ns)); + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("test.com"_ns)); + + // Test invalid patterns (unknown patterns treated as exact match) + Preferences::SetCString("network.lna.skip-domains", + "example.com,invalid.pattern"_ns); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "example.com"_ns)); // Valid exact match + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "invalid.pattern"_ns)); // Treated as exact match + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA("test.com"_ns)); // No match + + // Test case sensitivity + Preferences::SetCString("network.lna.skip-domains", + "Example.COM,*.Test.ORG"_ns); + EXPECT_TRUE( + ioService->ShouldSkipDomainForLNA("Example.COM"_ns)); // Exact case match + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA( + "example.com"_ns)); // Different case (no match) + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA( + "api.Test.ORG"_ns)); // Wildcard case match + EXPECT_FALSE(ioService->ShouldSkipDomainForLNA( + "api.test.org"_ns)); // Different case (no match) + + // Test plain "*" matches all domains + Preferences::SetCString("network.lna.skip-domains", "*"_ns); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("example.com"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("test.org"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("localhost"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("any.domain.here"_ns)); + EXPECT_TRUE(ioService->ShouldSkipDomainForLNA("server.local"_ns)); + + // Reset preference for cleanup + Preferences::SetCString("network.lna.skip-domains", ""_ns); +} diff --git a/netwerk/test/unit/test_local_network_access.js b/netwerk/test/unit/test_local_network_access.js @@ -7,6 +7,10 @@ const { NodeHTTP2Server } = ChromeUtils.importESModule( "resource://testing-common/NodeServer.sys.mjs" ); +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + function makeChannel(url) { let uri2 = NetUtil.newURI(url); // by default system principal is used, which cannot be used for permission based tests @@ -50,6 +54,22 @@ ChromeUtils.defineLazyGetter(this, "H2_URL", function () { return "https://localhost:" + server.port(); }); +ChromeUtils.defineLazyGetter(this, "H1_EXAMPLE_URL", function () { + return "http://example.com:" + httpServer.identity.primaryPort; +}); + +ChromeUtils.defineLazyGetter(this, "H1_TEST_EXAMPLE_URL", function () { + return "http://test.example.com:" + httpServer.identity.primaryPort; +}); + +ChromeUtils.defineLazyGetter(this, "H1_SERVER_LOCAL_URL", function () { + return "http://server.local:" + httpServer.identity.primaryPort; +}); + +ChromeUtils.defineLazyGetter(this, "H1_API_DEV_LOCAL_URL", function () { + return "http://api.dev.local:" + httpServer.identity.primaryPort; +}); + let httpServer = null; let server = new NodeHTTP2Server(); function pathHandler(metadata, response) { @@ -73,6 +93,11 @@ add_setup(async () => { httpServer = new HttpServer(); httpServer.registerPathHandler("/test_lna", pathHandler); httpServer.start(-1); + // Add domain identities for testing domain skip patterns + httpServer.identity.add("http", "example.com", 80); + httpServer.identity.add("http", "test.example.com", 80); + httpServer.identity.add("http", "server.local", 80); + httpServer.identity.add("http", "api.dev.local", 80); // H2 Server let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( @@ -310,3 +335,242 @@ add_task(async function lna_blocking_tests_local_network() { } } }); + +// Test the network.lna.skip-domains preference +add_task(async function lna_domain_skip_tests() { + // Add DNS overrides to map test domains to 127.0.0.1 + override.clearOverrides(); + Services.dns.clearCache(true); + + override.addIPOverride("example.com", "127.0.0.1"); + override.addIPOverride("test.example.com", "127.0.0.1"); + override.addIPOverride("server.local", "127.0.0.1"); + override.addIPOverride("api.dev.local", "127.0.0.1"); + + // Add override such that target servers are considered as local network (and not localhost) + // This includes all the domains we're testing with + var override_value = + "127.0.0.1" + + ":" + + httpServer.identity.primaryPort + + "," + + "127.0.0.1" + + ":" + + server.port(); + + Services.prefs.setCharPref( + "network.lna.address_space.private.override", + override_value + ); + + const domainSkipTestCases = [ + // [skipDomains, parentSpace, expectedStatus, baseURL, description] + // Exact domain match + [ + "localhost", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_URL, + "exact domain match - localhost", + ], + [ + "localhost", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H2_URL, + "exact domain match - localhost H2", + ], + [ + "example.com", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_EXAMPLE_URL, + "exact domain match - example.com", + ], + + // Wildcard domain match + [ + "*.localhost", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_URL, + "wildcard domain match - *.localhost matches localhost", + ], + [ + "*.example.com", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_TEST_EXAMPLE_URL, + "wildcard domain match - *.example.com matches test.example.com", + ], + [ + "*.example.com", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_EXAMPLE_URL, + "wildcard domain match - *.example.com matches example.com", + ], + [ + "*.test.com", + Ci.nsILoadInfo.Public, + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_EXAMPLE_URL, + "wildcard no match - *.test.com doesn't match example.com", + ], + + // Multiple domains (comma-separated) + [ + "example.com,localhost,test.org", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_URL, + "multiple domains - localhost match", + ], + [ + "example.com,localhost,test.org", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_EXAMPLE_URL, + "multiple domains - example.com match", + ], + [ + "foo.com,test.org", + Ci.nsILoadInfo.Public, + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_EXAMPLE_URL, + "multiple domains no match - example.com not in list", + ], + + // Empty skip domains (should apply normal LNA rules) + [ + "", + Ci.nsILoadInfo.Public, + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_URL, + "empty skip domains - should block", + ], + + // .local domain tests + [ + "*.local", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_SERVER_LOCAL_URL, + "wildcard .local - *.local matches server.local", + ], + [ + "*.local", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_API_DEV_LOCAL_URL, + "wildcard .local - *.local matches api.dev.local", + ], + [ + "*.dev.local", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_API_DEV_LOCAL_URL, + "wildcard subdomain .local - *.dev.local matches api.dev.local", + ], + [ + "server.local", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_SERVER_LOCAL_URL, + "exact match .local - server.local matches server.local", + ], + [ + "*.local", + Ci.nsILoadInfo.Public, + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_URL, + "wildcard .local - *.local doesn't match localhost", + ], + + // localhost variations + [ + "localhost,*.local,*.internal", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_URL, + "combined patterns - localhost matches localhost", + ], + [ + "localhost,*.local,*.internal", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_SERVER_LOCAL_URL, + "combined patterns - *.local matches server.local", + ], + + // Plain "*" wildcard matches all domains + [ + "*", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_URL, + "wildcard all - * matches localhost", + ], + [ + "*", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_EXAMPLE_URL, + "wildcard all - * matches example.com", + ], + [ + "*", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_SERVER_LOCAL_URL, + "wildcard all - * matches server.local", + ], + [ + "*", + Ci.nsILoadInfo.Public, + Cr.NS_OK, + H1_TEST_EXAMPLE_URL, + "wildcard all - * matches test.example.com", + ], + ]; + + for (let [ + skipDomains, + parentSpace, + expectedStatus, + url, + description, + ] of domainSkipTestCases) { + info(`Testing domain skip: ${description} - domains: "${skipDomains}"`); + + // Set the domain skip preference + Services.prefs.setCharPref("network.lna.skip-domains", skipDomains); + + // Disable prompt simulation for clean testing + Services.prefs.setBoolPref("network.localhost.prompt.testing.allow", false); + + let chan = makeChannel(url + "/test_lna"); + chan.loadInfo.parentIpAddressSpace = parentSpace; + + let expectFailure = expectedStatus !== Cr.NS_OK ? CL_EXPECT_FAILURE : 0; + + await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null, expectFailure)); + }); + + Assert.equal( + chan.status, + expectedStatus, + `Status should match for: ${description}` + ); + if (expectedStatus === Cr.NS_OK) { + Assert.equal(chan.protocolVersion, url === H2_URL ? "h2" : "http/1.1"); + } + } + + // Cleanup + Services.prefs.clearUserPref("network.lna.skip-domains"); + Services.prefs.clearUserPref("network.lna.address_space.private.override"); + override.clearOverrides(); + Services.dns.clearCache(true); +});