tor-browser

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

commit f194a4ff6fb765ea03a5c801debc21037a4e06df
parent e8b4dd7575923c50344bee6e34810d020f9b7120
Author: smayya <smayya@mozilla.com>
Date:   Mon, 20 Oct 2025 16:46:41 +0000

Bug 1993938 - Add preference to skip LNA checks for local network to localhost requests. r=necko-reviewers,valentin,geckoview-reviewers,nalexander

This adds a new preference network.lna.local-network-to-localhost.skip-checks
that allows local network requests to localhost to bypass LNA checks when enabled.
This is useful for development scenarios where local network clients need
to access localhost services.

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

Diffstat:
Mmobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PermissionDelegateTest.kt | 2++
Mmodules/libpref/init/StaticPrefList.yaml | 7+++++++
Mnetwerk/base/nsIOService.cpp | 3+++
Mnetwerk/protocol/http/nsHttpTransaction.cpp | 7+++++++
Mnetwerk/test/browser/browser_test_local_network_access.js | 2++
Mnetwerk/test/unit/test_local_network_access.js | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 127 insertions(+), 0 deletions(-)

diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PermissionDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PermissionDelegateTest.kt @@ -1208,6 +1208,8 @@ class PermissionDelegateTest : BaseSessionTest() { @Test fun localDeviceAccessPermission() { sessionRule.setPrefsUntilTestEnd(mapOf("network.lna.blocking" to true)) + // enable LNA checks for local network to localhost checks + sessionRule.setPrefsUntilTestEnd(mapOf("network.lna.local-network-to-localhost.skip-checks" to false)) mainSession.loadUri("https://example.com/") mainSession.waitForPageStop() diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -14490,6 +14490,13 @@ value: false mirror: always +# When this pref is true, skip LNA checks for requests from private network +# to localhost (private -> local IP address space transitions). +- name: network.lna.local-network-to-localhost.skip-checks + type: RelaxedAtomicBool + value: true + mirror: always + # The proxy type. See nsIProtocolProxyService.idl # PROXYCONFIG_DIRECT = 0 # PROXYCONFIG_MANUAL = 1 diff --git a/netwerk/base/nsIOService.cpp b/netwerk/base/nsIOService.cpp @@ -260,6 +260,9 @@ static const char* gCallbackPrefsForSocketProcess[] = { "network.lna.enabled", "network.lna.blocking", "network.lna.address_space.private.override", + "network.lna.address_space.public.override", + "network.lna.websocket.enabled", + "network.lna.local-network-to-localhost.skip-checks", nullptr, }; diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -3775,6 +3775,13 @@ bool nsHttpTransaction::AllowedToConnectToIpAddressSpace( // for private network access // XXX add link to LNA spec once it is published + // Skip LNA checks for private network to localhost if preference is enabled + if (StaticPrefs::network_lna_local_network_to_localhost_skip_checks() && + mParentIPAddressSpace == nsILoadInfo::IPAddressSpace::Private && + aTargetIpAddressSpace == nsILoadInfo::IPAddressSpace::Local) { + return true; // Allow private->localhost access + } + if (mozilla::net::IsLocalOrPrivateNetworkAccess(mParentIPAddressSpace, aTargetIpAddressSpace)) { if (aTargetIpAddressSpace == nsILoadInfo::IPAddressSpace::Local && diff --git a/netwerk/test/browser/browser_test_local_network_access.js b/netwerk/test/browser/browser_test_local_network_access.js @@ -27,6 +27,8 @@ add_setup(async function () { ["network.lna.block_trackers", true], ["network.lna.blocking", true], ["network.http.rcwn.enabled", false], + ["network.lna.websocket.enabled", true], + ["network.lna.local-network-to-localhost.skip-checks", false], ], }); Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); diff --git a/netwerk/test/unit/test_local_network_access.js b/netwerk/test/unit/test_local_network_access.js @@ -89,6 +89,13 @@ add_setup(async () => { Services.prefs.setBoolPref("network.localhost.prompt.testing", true); Services.prefs.setBoolPref("network.localnetwork.prompt.testing", true); + Services.prefs.setBoolPref( + "network.lna.local-network-to-localhost.skip-checks", + false + ); + + Services.prefs.setBoolPref("network.lna.websocket.enabled", true); + // H1 Server httpServer = new HttpServer(); httpServer.registerPathHandler("/test_lna", pathHandler); @@ -114,6 +121,10 @@ add_setup(async () => { Services.prefs.clearUserPref("network.lna.blocking.prompt.testing"); Services.prefs.clearUserPref("network.localhost.prompt.testing.allow"); Services.prefs.clearUserPref("network.localnetwork.prompt.testing.allow"); + Services.prefs.clearUserPref( + "network.lna.local-network-to-localhost.skip-checks" + ); + Services.prefs.clearUserPref("network.lna.websocket.enabled"); Services.prefs.clearUserPref( "network.lna.address_space.private.override" @@ -334,6 +345,7 @@ add_task(async function lna_blocking_tests_local_network() { Assert.equal(chan.protocolVersion, url === H1_URL ? "http/1.1" : "h2"); } } + Services.prefs.clearUserPref("network.lna.address_space.private.override"); }); // Test the network.lna.skip-domains preference @@ -574,3 +586,97 @@ add_task(async function lna_domain_skip_tests() { override.clearOverrides(); Services.dns.clearCache(true); }); +// Test the new network.lna.local-network-to-localhost.skip-checks preference +add_task(async function lna_local_network_to_localhost_skip_checks() { + // Test cases: [skipPref, parentSpace, urlSuffix, expectedStatus, baseURL] + const skipTestCases = [ + // Skip pref disabled (false) - existing behavior should be preserved + [ + false, + Ci.nsILoadInfo.Private, + "/test_lna", + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_URL, + ], + [ + false, + Ci.nsILoadInfo.Private, + "/test_lna", + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H2_URL, + ], + [ + false, + Ci.nsILoadInfo.Public, + "/test_lna", + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_URL, + ], + [ + false, + Ci.nsILoadInfo.Public, + "/test_lna", + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H2_URL, + ], + + // Skip pref enabled (true) - new behavior: Private->Local allowed, Public->Local still blocked + [true, Ci.nsILoadInfo.Private, "/test_lna", Cr.NS_OK, H1_URL], // Private->Local now allowed + [true, Ci.nsILoadInfo.Private, "/test_lna", Cr.NS_OK, H2_URL], // Private->Local now allowed + [ + true, + Ci.nsILoadInfo.Public, + "/test_lna", + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H1_URL, + ], // Public->Local still blocked + [ + true, + Ci.nsILoadInfo.Public, + "/test_lna", + Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED, + H2_URL, + ], // Public->Local still blocked + ]; + + for (let [ + skipPref, + parentSpace, + suffix, + expectedStatus, + url, + ] of skipTestCases) { + info( + `Testing skip pref: ${skipPref}, ${parentSpace} -> Local, expect: ${expectedStatus}` + ); + + // Set the new skip preference + Services.prefs.setBoolPref( + "network.lna.local-network-to-localhost.skip-checks", + skipPref + ); + + // Disable prompt simulation for clean testing (prompt should not affect skip logic) + Services.prefs.setBoolPref("network.localhost.prompt.testing.allow", false); + + let chan = makeChannel(url + suffix); + chan.loadInfo.parentIpAddressSpace = parentSpace; + // Target is always Local (localhost) since we're testing localhost servers + + 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); + if (expectedStatus === Cr.NS_OK) { + Assert.equal(chan.protocolVersion, url === H1_URL ? "http/1.1" : "h2"); + } + } + + // Cleanup + Services.prefs.clearUserPref( + "network.lna.local-network-to-localhost.skip-checks" + ); +});