Bug 1960582 - Add LNA telemetry. r=necko-reviewers,valentin

Differential Revision: https://phabricator.services.mozilla.com/D249887
This commit is contained in:
smayya
2025-05-22 11:11:28 +00:00
committed by smayya@mozilla.com
parent d28564a0af
commit a0f3b68a20
6 changed files with 384 additions and 0 deletions

View File

@@ -4166,5 +4166,36 @@ nsresult AddExtraHeaders(nsIHttpChannel* aHttpChannel,
return NS_OK;
}
bool IsLocalNetworkAccess(nsILoadInfo::IPAddressSpace aParentIPAddressSpace,
nsILoadInfo::IPAddressSpace aTargetIPAddressSpace) {
// Determine if the request is moving to a more private address space
// i.e. Public -> Private or Local
// Private -> Local
// Refer
// https://wicg.github.io/private-network-access/#private-network-request-heading
// for private network access
// XXX (sunil) add link to LNA spec once it is published
if (aTargetIPAddressSpace == nsILoadInfo::IPAddressSpace::Public ||
aTargetIPAddressSpace == nsILoadInfo::IPAddressSpace::Unknown) {
return false;
}
// Check if this is an access to a local resource from Public or Private
// network
if ((aTargetIPAddressSpace == nsILoadInfo::IPAddressSpace::Local) &&
(aParentIPAddressSpace == nsILoadInfo::IPAddressSpace::Public ||
aParentIPAddressSpace == nsILoadInfo::IPAddressSpace::Private)) {
return true;
}
// Check if this is an access to a Private Network resource from a Public
// network
if ((aTargetIPAddressSpace == nsILoadInfo::IPAddressSpace::Private) &&
(aParentIPAddressSpace == nsILoadInfo::IPAddressSpace::Public)) {
return true;
}
return false;
}
} // namespace net
} // namespace mozilla

View File

@@ -1188,6 +1188,9 @@ void ParseSimpleURISchemes(const nsACString& schemeList);
nsresult AddExtraHeaders(nsIHttpChannel* aHttpChannel,
const nsACString& aExtraHeaders, bool aMerge = true);
bool IsLocalNetworkAccess(nsILoadInfo::IPAddressSpace aParentIPAddressSpace,
nsILoadInfo::IPAddressSpace aTargetIPAddressSpace);
} // namespace net
} // namespace mozilla

View File

@@ -1881,6 +1881,48 @@ networking:
- load_is_http
- load_is_http_for_local_domain
local_network_access:
type: labeled_counter
description: >
Whether the request is crossing to a more private addresspace
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1960582
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1960582
notification_emails:
- smayya@mozilla.com
- vgosu@mozilla.com
- necko@gmail.com
expires: 150
labels:
- private_to_local_http
- private_to_local_https
- public_to_local_http
- public_to_local_https
- public_to_private_http
- public_to_private_https
- success
- failure
local_network_access_port:
type: custom_distribution
description: >
port used for local network access
range_min: 1
range_max: 65535
bucket_count: 65535
unit: port number
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1960582
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1960582
notification_emails:
- smayya@mozilla.com
- vgosu@mozilla.com
- necko@gmail.com
expires: 150
http_channel_sub_open_to_first_sent_https_rr:
type: timing_distribution
time_unit: millisecond

View File

@@ -8533,6 +8533,64 @@ static void RecordIPAddressSpaceTelemetry(bool aLoadSuccess, nsIURI* aURI,
}
}
static void RecordLNATelemetry(bool aLoadSuccess, nsIURI* aURI,
nsILoadInfo* aLoadInfo, NetAddr& aPeerAddr) {
if (!aLoadInfo || !aURI) {
return;
}
RefPtr<mozilla::dom::BrowsingContext> bc;
aLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
nsILoadInfo::IPAddressSpace parentAddressSpace =
nsILoadInfo::IPAddressSpace::Unknown;
if (!bc) {
parentAddressSpace = aLoadInfo->GetParentIpAddressSpace();
} else {
parentAddressSpace = bc->GetCurrentIPAddressSpace();
}
if (!mozilla::net::IsLocalNetworkAccess(parentAddressSpace,
aLoadInfo->GetIpAddressSpace())) {
return;
}
if (aLoadSuccess) {
mozilla::glean::networking::local_network_access.Get("success"_ns).Add(1);
} else {
mozilla::glean::networking::local_network_access.Get("failure"_ns).Add(1);
}
uint16_t port = 0;
if (NS_SUCCEEDED(aPeerAddr.GetPort(&port))) {
mozilla::glean::networking::local_network_access_port
.AccumulateSingleSample(port);
}
// label format is <parentAddressSpace>_to_<targetAddressSpace>_<scheme>
// At this point we are sure that the request is a LNA,
// Hence we can safely assume few conditions to construct the label
nsAutoCString glean_lna_label;
if (aLoadInfo->GetParentIpAddressSpace() ==
nsILoadInfo::IPAddressSpace::Public) {
glean_lna_label.Append("public_to_"_ns);
} else {
glean_lna_label.Append("private_to_"_ns);
}
if (aLoadInfo->GetIpAddressSpace() == nsILoadInfo::IPAddressSpace::Private) {
glean_lna_label.Append("private_"_ns);
} else {
glean_lna_label.Append("local_"_ns);
}
if (aURI->SchemeIs("https")) {
glean_lna_label.Append("https"_ns);
} else {
glean_lna_label.Append("http"_ns);
}
mozilla::glean::networking::local_network_access.Get(glean_lna_label).Add(1);
}
NS_IMETHODIMP
nsHttpChannel::OnStopRequest(nsIRequest* request, nsresult status) {
MOZ_ASSERT(!mAsyncOpenTime.IsNull());
@@ -8684,6 +8742,7 @@ nsHttpChannel::OnStopRequest(nsIRequest* request, nsresult status) {
RecordIPAddressSpaceTelemetry(NS_SUCCEEDED(mStatus), mURI, mLoadInfo,
mPeerAddr);
RecordLNATelemetry(NS_SUCCEEDED(mStatus), mURI, mLoadInfo, mPeerAddr);
// If we are using the transaction to serve content, we also save the
// time since async open in the cache entry so we can compare telemetry

View File

@@ -0,0 +1,247 @@
"use strict";
const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
Ci.nsINativeDNSResolverOverride
);
const mockNetwork = Cc[
"@mozilla.org/network/mock-network-controller;1"
].getService(Ci.nsIMockNetworkLayerController);
const certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
const DOMAIN = "example.org";
function makeChan(url, expected) {
let chan = NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
if (
expected.PublicToPrivateHttp !== undefined ||
expected.PublicToLocalHttp !== undefined ||
expected.PublicToPublicHttp !== undefined ||
expected.PublicToPrivateHttps !== undefined ||
expected.PublicToLocalHttps !== undefined ||
expected.PublicToPublicHttp !== undefined
) {
chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;
} else if (
expected.PrivateToLocalHttp !== undefined ||
expected.PrivateToPrivateHttp !== undefined ||
expected.PrivateToLocalHttps !== undefined ||
expected.PrivateToPrivateHttps !== undefined
) {
chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Private;
}
return chan;
}
function channelOpenPromise(chan, flags) {
return new Promise(resolve => {
function finish(req, buffer) {
resolve([req, buffer]);
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
false
);
}
certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
true
);
chan.asyncOpen(new ChannelListener(finish, null, flags));
});
}
let server;
add_setup(async function setup() {
Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true);
Services.fog.initializeFOG();
server = new NodeHTTPServer();
await server.start();
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.disable-localhost-when-offline");
Services.prefs.clearUserPref("network.dns.use_override_as_peer_address");
Services.prefs.clearUserPref("dom.security.https_only_mode");
Services.prefs.clearUserPref("dom.security.https_first");
Services.prefs.clearUserPref("dom.security.https_first_schemeless");
Services.prefs.clearUserPref("network.socket.attach_mock_network_layer");
await server.stop();
});
});
function verifyGleanValues(aDescription, aExpected) {
info(aDescription);
let privateToLocalHttp = aExpected.PrivateToLocalHttp || null;
let publicToPrivateHttp = aExpected.PublicToPrivateHttp || null;
let publicToLocalHttp = aExpected.PublicToLocalHttp || null;
let privateToLocalHttps = aExpected.PrivateToLocalHttps || null;
let publicToPrivateHttps = aExpected.PublicToPrivateHttps || null;
let publicToLocalHttps = aExpected.PublicToLocalHttps || null;
let glean = Glean.networking.localNetworkAccess;
Assert.equal(
glean.private_to_local_http.testGetValue(),
privateToLocalHttp,
"verify private_to_local_http"
);
Assert.equal(
glean.public_to_private_http.testGetValue(),
publicToPrivateHttp,
"verify public_to_private_http"
);
Assert.equal(
glean.public_to_local_http.testGetValue(),
publicToLocalHttp,
"verify public_to_local_http"
);
Assert.equal(
glean.private_to_local_https.testGetValue(),
privateToLocalHttps,
"verify private_to_local_http"
);
Assert.equal(
glean.public_to_private_https.testGetValue(),
publicToPrivateHttps,
"verify public_to_private_http"
);
Assert.equal(
glean.public_to_local_https.testGetValue(),
publicToLocalHttps,
"verify public_to_local_http"
);
Assert.equal(
glean.public_to_local_https.testGetValue(),
publicToLocalHttps,
"verify public_to_local_http"
);
if (
privateToLocalHttp ||
publicToPrivateHttp ||
publicToLocalHttp ||
privateToLocalHttps ||
publicToPrivateHttps ||
publicToLocalHttps
) {
Assert.equal(
glean.success.testGetValue(),
1,
"verify local_network_access_success"
);
// XXX (sunil) add test for local_network_access_failure cases
}
}
async function do_test(ip, expected, srcPort, dstPort) {
Services.fog.testResetFOG();
override.addIPOverride(DOMAIN, ip);
let fromAddr = mockNetwork.createScriptableNetAddr(ip, srcPort ?? 80);
let toAddr = mockNetwork.createScriptableNetAddr(
fromAddr.family == Ci.nsINetAddr.FAMILY_INET ? "127.0.0.1" : "::1",
dstPort ?? server.port()
);
mockNetwork.addNetAddrOverride(fromAddr, toAddr);
info(`do_test ${ip}, ${fromAddr} -> ${toAddr}`);
let chan = makeChan(`http://${DOMAIN}`, expected);
let [req] = await channelOpenPromise(chan);
info(
"req.remoteAddress=" +
req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress
);
if (expected.PublicToPrivateHttp) {
Assert.equal(chan.loadInfo.ipAddressSpace, Ci.nsILoadInfo.Private);
} else if (expected.PrivateToLocalHttp || expected.PrivateToLocalHttp) {
Assert.equal(chan.loadInfo.ipAddressSpace, Ci.nsILoadInfo.Local);
}
verifyGleanValues(`test ip=${ip}`, expected);
Services.dns.clearCache(false);
override.clearOverrides();
mockNetwork.clearNetAddrOverrides();
Services.obs.notifyObservers(null, "net:prune-all-connections");
}
add_task(async function test_lna_http() {
Services.prefs.setBoolPref("dom.security.https_only_mode", false);
Services.prefs.setBoolPref("dom.security.https_first", false);
Services.prefs.setBoolPref("dom.security.https_first_schemeless", false);
await do_test("10.0.0.1", { PublicToPrivateHttp: 1 });
// NO LNA access do not increment
await do_test("10.0.0.1", { PrivateToPrivateHttp: 0 });
await do_test("2.2.2.2", { PublicToPublicHttp: 0 });
await do_test("100.64.0.1", { PublicToPrivateHttp: 1 });
await do_test("127.0.0.1", { PublicToLocalHttp: 1 });
await do_test("127.0.0.1", { PrivateToLocalHttp: 1 });
if (AppConstants.platform != "android") {
await do_test("::1", { PrivateToLocalHttp: 1 });
}
});
add_task(async function test_lna_https() {
Services.prefs.setBoolPref("dom.security.https_only_mode", true);
let httpsServer = new NodeHTTPSServer();
await httpsServer.start();
registerCleanupFunction(async () => {
await httpsServer.stop();
});
await do_test(
"10.0.0.1",
{ PublicToPrivateHttps: 1 },
443,
httpsServer.port()
);
// NO LNA access do not increment
await do_test(
"10.0.0.1",
{ PrivateToPrivateHttps: 0 },
443,
httpsServer.port()
);
await do_test(
"2.2.2.2",
{ PublicToPublicHttps: 0 },
443,
httpsServer.port(),
true
);
await do_test(
"100.64.0.1",
{ PublicToPrivateHttps: 1 },
443,
httpsServer.port()
);
await do_test(
"127.0.0.1",
{ PublicToLocalHttps: 1 },
443,
httpsServer.port()
);
await do_test(
"127.0.0.1",
{ PrivateToLocalHttps: 1 },
443,
httpsServer.port()
);
if (AppConstants.platform != "android") {
await do_test("::1", { PrivateToLocalHttps: 1 }, 443, httpsServer.port());
}
});

View File

@@ -911,6 +911,8 @@ run-sequentially = "node server exceptions dont replay well"
["test_ip_space_glean.js"]
skip-if = ["os == 'android' && android_version == '24' && processor == 'x86_64'"]
["test_ip_address_space_lna_glean.js"]
["test_large_port.js"]
["test_loadgroup_cancel.js"]