Files
safetynet/docs/native-runtime/chromium-152-webrtc-binder-backport.patch
T

904 lines
36 KiB
Diff

diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 7b194ea..f73edef 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -2521,8 +2521,17 @@ source_set("browser") {
deps += [ "//media/mojo/mojom" ]
}
+ if (enable_mdns) {
+ sources += [
+ "renderer_host/no_op_mdns_responder.cc",
+ "renderer_host/no_op_mdns_responder.h",
+ ]
+ }
+
if (is_p2p_enabled) {
sources += [
+ "renderer_host/p2p/no_op_p2p_socket_manager.cc",
+ "renderer_host/p2p/no_op_p2p_socket_manager.h",
"renderer_host/p2p/socket_dispatcher_host.cc",
"renderer_host/p2p/socket_dispatcher_host.h",
]
diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc
index 94c73d9..26ca7ce 100644
--- a/content/browser/browser_interface_binders.cc
+++ b/content/browser/browser_interface_binders.cc
@@ -27,6 +27,7 @@
#include "content/browser/bluetooth/web_bluetooth_service_impl.h"
#include "content/browser/browser_context_impl.h"
#include "content/browser/browser_main_loop.h"
+#include "content/browser/connection_allowlist_utils.h"
#include "content/browser/contacts/contacts_manager_impl.h"
#include "content/browser/content_index/content_index_service_impl.h"
#include "content/browser/cookie_store/cookie_store_manager.h"
@@ -119,6 +120,7 @@
#include "services/device/public/mojom/vibration_manager.mojom.h"
#include "services/metrics/public/mojom/ukm_interface.mojom.h"
#include "services/metrics/ukm_recorder_factory_impl.h"
+#include "services/network/public/cpp/connection_allowlist.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/network_service_buildflags.h"
@@ -225,6 +227,7 @@
#endif // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_P2P_ENABLED)
+#include "content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h"
#include "services/network/public/mojom/p2p.mojom.h"
#endif // BUILDFLAG(IS_P2P_ENABLED)
@@ -790,6 +793,17 @@ void BindMediaPlayerObserverClientHandler(
void BindSocketManager(
RenderFrameHost* frame,
mojo::PendingReceiver<network::mojom::P2PSocketManager> receiver) {
+ // If WebRTC is blocked by the document's Connection Allowlist, bind a no-op
+ // P2PSocketManager so that no sockets are ever created, while still letting
+ // the renderer fail gracefully (see NoOpP2PSocketManager). It is possible
+ // that a renderer which does not allow WebRTC could still reach this
+ // interface. TODO(crbug.com/571239543): Disable all WebRTC connections on the
+ // renderer side.
+ if (!IsWebRTCAllowedByConnectionAllowlist(frame)) {
+ NoOpP2PSocketManager::Create(std::move(receiver));
+ return;
+ }
+
static_cast<RenderProcessHostImpl*>(frame->GetProcess())
->BindP2PSocketManager(
frame->GetIsolationInfoForSubresources().network_anonymization_key(),
@@ -991,26 +1005,9 @@ void PopulateBinderMapWithContext(
map->Add<blink::mojom::IdleManager>(
&BindRenderFrameHostImpl<&RenderFrameHostImpl::BindIdleManager>);
-#if BUILDFLAG(IS_P2P_ENABLED) || BUILDFLAG(ENABLE_MDNS)
- // TODO(447954811): Remove the fenced frames check if we end up supporting
- // Connection Allowlist for fenced frames.
- bool should_ban_p2p_for_connection_allowlist =
- host->HasPolicyContainerHost() &&
- host->policy_container_host()
- ->connection_allowlists()
- .enforced.has_value() &&
- host->policy_container_host()
- ->connection_allowlists()
- .enforced->webrtc_behavior ==
- network::ConnectionAllowlist::WebRtcBehavior::kBlock &&
- !host->IsNestedWithinFencedFrame();
-# endif // BUILDFLAG(IS_P2P_ENABLED) || BUILDFLAG(ENABLE_MDNS)
-
#if BUILDFLAG(ENABLE_MDNS)
- if (!should_ban_p2p_for_connection_allowlist) {
- map->Add<network::mojom::MdnsResponder>(
+ map->Add<network::mojom::MdnsResponder>(
&BindRenderFrameHostImpl<&RenderFrameHostImpl::CreateMdnsResponder>);
- }
#endif // BUILDFLAG(ENABLE_MDNS)
// BrowserMainLoop::GetInstance() may be null on unit tests.
@@ -1027,18 +1024,7 @@ void PopulateBinderMapWithContext(
&RenderFrameHostImpl::CreateNotificationService>);
#if BUILDFLAG(IS_P2P_ENABLED)
- // WebRTC p2p connections are disallowed in fenced frames. Creation of
- // RTCPeerConnection is already disabled in the renderer, so in theory this
- // unbound interface should never present an issue.
- bool should_ban_p2p_for_fenced_frames =
- base::FeatureList::IsEnabled(
- blink::features::kFencedFramesLocalUnpartitionedDataAccess) &&
- host->IsNestedWithinFencedFrame();
-
- if (!should_ban_p2p_for_fenced_frames &&
- !should_ban_p2p_for_connection_allowlist) {
- map->Add<network::mojom::P2PSocketManager>(&BindSocketManager);
- }
+ map->Add<network::mojom::P2PSocketManager>(&BindSocketManager);
#endif // BUILDFLAG(IS_P2P_ENABLED)
map->Add<blink::mojom::PeerConnectionTrackerHost>(
diff --git a/content/browser/connection_allowlist_utils.cc b/content/browser/connection_allowlist_utils.cc
index 379f029..b366763 100644
--- a/content/browser/connection_allowlist_utils.cc
+++ b/content/browser/connection_allowlist_utils.cc
@@ -141,4 +141,22 @@ bool FrameConnectionAllowlistAllowsRequestAndReportIfNeeded(
return ConnectionAllowlistAllowsUrlAndReportIfNeeded(policies, url);
}
+bool IsWebRTCAllowedByConnectionAllowlist(RenderFrameHost* frame) {
+ // A speculative RFH doesn't have a PolicyContainerHost, and in theory it
+ // should not be binding WebRTC interfaces while speculative. So fail closed.
+ if (!frame->HasPolicyContainerHost()) {
+ return false;
+ }
+
+ const std::optional<network::ConnectionAllowlist>& enforced_ca =
+ frame->GetConnectionAllowlists().enforced;
+ if (enforced_ca.has_value() &&
+ enforced_ca->webrtc_behavior ==
+ network::ConnectionAllowlist::WebRtcBehavior::kBlock) {
+ return false;
+ }
+
+ return true;
+}
+
} // namespace content
diff --git a/content/browser/connection_allowlist_utils.h b/content/browser/connection_allowlist_utils.h
index ee65754..ab5ecae 100644
--- a/content/browser/connection_allowlist_utils.h
+++ b/content/browser/connection_allowlist_utils.h
@@ -15,6 +15,7 @@ class HttpResponseHeaders;
namespace content {
+class RenderFrameHost;
struct PolicyContainerPolicies;
// Returns true if the parsed response headers contains a valid
@@ -47,6 +48,12 @@ network::ConnectionAllowlists GetConnectionAllowlistsForWorker(
const PolicyContainerPolicies* creator_policies,
bool inherit_from_creator);
+// Determines if the Connection Allowlist for `frame` has allowed WebRTC. This
+// is controlled by the Connection Allowlist header's `webrtc` directive, and is
+// is used to prevent binding of downstream interfaces like P2PSocketManger and
+// MDNSResponder.
+bool IsWebRTCAllowedByConnectionAllowlist(RenderFrameHost* frame);
+
} // namespace content
#endif // CONTENT_BROWSER_CONNECTION_ALLOWLIST_UTILS_H_
diff --git a/content/browser/renderer_host/no_op_mdns_responder.cc b/content/browser/renderer_host/no_op_mdns_responder.cc
new file mode 100644
index 0000000..d2ff53d
--- /dev/null
+++ b/content/browser/renderer_host/no_op_mdns_responder.cc
@@ -0,0 +1,38 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/no_op_mdns_responder.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
+
+namespace content {
+
+// static
+void NoOpMdnsResponder::Create(
+ mojo::PendingReceiver<network::mojom::MdnsResponder> receiver) {
+ mojo::MakeSelfOwnedReceiver(std::make_unique<NoOpMdnsResponder>(),
+ std::move(receiver));
+}
+
+NoOpMdnsResponder::NoOpMdnsResponder() = default;
+NoOpMdnsResponder::~NoOpMdnsResponder() = default;
+
+void NoOpMdnsResponder::CreateNameForAddress(
+ const net::IPAddress& address,
+ CreateNameForAddressCallback callback) {
+ std::move(callback).Run(/*name=*/std::string(),
+ /*announcement_scheduled=*/false);
+}
+
+void NoOpMdnsResponder::RemoveNameForAddress(
+ const net::IPAddress& address,
+ RemoveNameForAddressCallback callback) {
+ std::move(callback).Run(/*removed=*/false, /*goodbye_scheduled=*/false);
+}
+
+} // namespace content
diff --git a/content/browser/renderer_host/no_op_mdns_responder.h b/content/browser/renderer_host/no_op_mdns_responder.h
new file mode 100644
index 0000000..3f6556b
--- /dev/null
+++ b/content/browser/renderer_host/no_op_mdns_responder.h
@@ -0,0 +1,39 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_RENDERER_HOST_NO_OP_MDNS_RESPONDER_H_
+#define CONTENT_BROWSER_RENDERER_HOST_NO_OP_MDNS_RESPONDER_H_
+
+#include "content/common/content_export.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "services/network/public/mojom/mdns_responder.mojom.h"
+
+namespace content {
+
+// An MdnsResponder that never registers or announces any names. It is bound in
+// place of the real (network service backed) MdnsResponder for documents whose
+// Connection Allowlist blocks WebRTC. All reply callbacks are run with failure
+// values so that no renderer request hangs.
+class CONTENT_EXPORT NoOpMdnsResponder : public network::mojom::MdnsResponder {
+ public:
+ // Creates a self-owned NoOpMdnsResponder bound to `receiver`. It is destroyed
+ // when the renderer closes the pipe.
+ static void Create(
+ mojo::PendingReceiver<network::mojom::MdnsResponder> receiver);
+
+ NoOpMdnsResponder();
+ NoOpMdnsResponder(const NoOpMdnsResponder&) = delete;
+ NoOpMdnsResponder& operator=(const NoOpMdnsResponder&) = delete;
+ ~NoOpMdnsResponder() override;
+
+ // network::mojom::MdnsResponder:
+ void CreateNameForAddress(const net::IPAddress& address,
+ CreateNameForAddressCallback callback) override;
+ void RemoveNameForAddress(const net::IPAddress& address,
+ RemoveNameForAddressCallback callback) override;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_RENDERER_HOST_NO_OP_MDNS_RESPONDER_H_
diff --git a/content/browser/renderer_host/no_op_mdns_responder_unittest.cc b/content/browser/renderer_host/no_op_mdns_responder_unittest.cc
new file mode 100644
index 0000000..0a256ad
--- /dev/null
+++ b/content/browser/renderer_host/no_op_mdns_responder_unittest.cc
@@ -0,0 +1,87 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/no_op_mdns_responder.h"
+
+#include <string>
+
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/ip_address.h"
+#include "services/network/public/mojom/mdns_responder.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+class NoOpMdnsResponderTest : public testing::Test {
+ public:
+ NoOpMdnsResponderTest() {
+ NoOpMdnsResponder::Create(responder_.BindNewPipeAndPassReceiver());
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ mojo::Remote<network::mojom::MdnsResponder> responder_;
+};
+
+// CreateNameForAddress() must always run its callback, without creating a name
+// or scheduling an announcement.
+TEST_F(NoOpMdnsResponderTest, CreateNameForAddressFails) {
+ base::test::TestFuture<const std::string&, bool> future;
+ responder_->CreateNameForAddress(net::IPAddress(192, 168, 0, 1),
+ future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+ EXPECT_FALSE(future.Get<1>());
+ EXPECT_TRUE(responder_.is_connected());
+}
+
+// Repeated requests for the same address also fail; no name is ever retained.
+TEST_F(NoOpMdnsResponderTest, CreateNameForAddressRepeatedlyFails) {
+ const net::IPAddress address(10, 0, 0, 1);
+ for (int i = 0; i < 3; ++i) {
+ base::test::TestFuture<const std::string&, bool> future;
+ responder_->CreateNameForAddress(address, future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+ EXPECT_FALSE(future.Get<1>());
+ }
+ EXPECT_TRUE(responder_.is_connected());
+}
+
+// RemoveNameForAddress() must always run its callback, reporting that nothing
+// was removed and no goodbye was scheduled.
+TEST_F(NoOpMdnsResponderTest, RemoveNameForAddressFails) {
+ base::test::TestFuture<bool, bool> future;
+ responder_->RemoveNameForAddress(net::IPAddress(192, 168, 0, 1),
+ future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+ EXPECT_FALSE(future.Get<0>());
+ EXPECT_FALSE(future.Get<1>());
+ EXPECT_TRUE(responder_.is_connected());
+}
+
+// Works for IPv6 addresses too.
+TEST_F(NoOpMdnsResponderTest, Ipv6AddressFails) {
+ base::test::TestFuture<const std::string&, bool> create_future;
+ responder_->CreateNameForAddress(net::IPAddress::IPv6Localhost(),
+ create_future.GetCallback());
+ ASSERT_TRUE(create_future.Wait());
+ EXPECT_TRUE(create_future.Get<0>().empty());
+ EXPECT_FALSE(create_future.Get<1>());
+
+ base::test::TestFuture<bool, bool> remove_future;
+ responder_->RemoveNameForAddress(net::IPAddress::IPv6Localhost(),
+ remove_future.GetCallback());
+ ASSERT_TRUE(remove_future.Wait());
+ EXPECT_FALSE(remove_future.Get<0>());
+ EXPECT_FALSE(remove_future.Get<1>());
+}
+
+} // namespace
+
+} // namespace content
diff --git a/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.cc b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.cc
new file mode 100644
index 0000000..b00ee09
--- /dev/null
+++ b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.cc
@@ -0,0 +1,57 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h"
+
+#include <memory>
+#include <utility>
+
+#include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "net/base/ip_address.h"
+
+namespace content {
+
+// static
+void NoOpP2PSocketManager::Create(
+ mojo::PendingReceiver<network::mojom::P2PSocketManager> receiver) {
+ mojo::MakeSelfOwnedReceiver(std::make_unique<NoOpP2PSocketManager>(),
+ std::move(receiver));
+}
+
+NoOpP2PSocketManager::NoOpP2PSocketManager() = default;
+NoOpP2PSocketManager::~NoOpP2PSocketManager() = default;
+
+void NoOpP2PSocketManager::StartNetworkNotifications(
+ mojo::PendingRemote<network::mojom::P2PNetworkNotificationClient> client) {
+ network_notification_client_.reset();
+ network_notification_client_.Bind(std::move(client));
+ // Report an empty network list so nothing is waiting on network changes.
+ network_notification_client_->NetworkListChanged(
+ /*networks=*/{}, /*default_ipv4_local_address=*/net::IPAddress(),
+ /*default_ipv6_local_address=*/net::IPAddress());
+}
+
+void NoOpP2PSocketManager::GetHostAddress(
+ const std::string& host_name,
+ std::optional<net::AddressFamily> address_family,
+ bool enable_mdns,
+ GetHostAddressCallback callback) {
+ // Never resolve anything.
+ std::move(callback).Run(/*addresses=*/{});
+}
+
+void NoOpP2PSocketManager::CreateSocket(
+ network::P2PSocketType type,
+ const net::IPEndPoint& local_address,
+ const network::P2PPortRange& port_range,
+ const network::P2PHostAndIPEndPoint& remote_address,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
+ const std::optional<base::UnguessableToken>& devtools_token,
+ mojo::PendingRemote<network::mojom::P2PSocketClient> client,
+ mojo::PendingReceiver<network::mojom::P2PSocket> receiver) {
+ // Never create a socket. Letting these pending pipes go out of scope should
+ // destroy them and treat them as disconnected.
+}
+
+} // namespace content
diff --git a/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h
new file mode 100644
index 0000000..cf05b8f
--- /dev/null
+++ b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h
@@ -0,0 +1,69 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_RENDERER_HOST_P2P_NO_OP_P2P_SOCKET_MANAGER_H_
+#define CONTENT_BROWSER_RENDERER_HOST_P2P_NO_OP_P2P_SOCKET_MANAGER_H_
+
+#include <optional>
+#include <string>
+
+#include "content/common/content_export.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/network/public/mojom/p2p.mojom.h"
+
+namespace content {
+
+// A P2PSocketManager that never performs any networking. It is bound in place
+// of the real (network service backed) P2PSocketManager for documents whose
+// Connection Allowlist blocks WebRTC.
+//
+// Binding a no-op implementation (rather than dropping the receiver or leaving
+// the interface unregistered) lets an unmodified renderer fail gracefully:
+// - The pipe stays connected, so the renderer's P2PSocketDispatcher does not
+// enter its reconnect loop.
+// - An empty network list is reported, so ICE gathering is allowed to proceed.
+// - All reply callbacks are run, so no request hangs.
+class CONTENT_EXPORT NoOpP2PSocketManager
+ : public network::mojom::P2PSocketManager {
+ public:
+ // Creates a self-owned NoOpP2PSocketManager bound to `receiver`. It is
+ // destroyed when the renderer closes the pipe.
+ static void Create(
+ mojo::PendingReceiver<network::mojom::P2PSocketManager> receiver);
+
+ NoOpP2PSocketManager();
+ NoOpP2PSocketManager(const NoOpP2PSocketManager&) = delete;
+ NoOpP2PSocketManager& operator=(const NoOpP2PSocketManager&) = delete;
+ ~NoOpP2PSocketManager() override;
+
+ // network::mojom::P2PSocketManager:
+ void StartNetworkNotifications(
+ mojo::PendingRemote<network::mojom::P2PNetworkNotificationClient> client)
+ override;
+ void GetHostAddress(const std::string& host_name,
+ std::optional<net::AddressFamily> address_family,
+ bool enable_mdns,
+ GetHostAddressCallback callback) override;
+ void CreateSocket(
+ network::P2PSocketType type,
+ const net::IPEndPoint& local_address,
+ const network::P2PPortRange& port_range,
+ const network::P2PHostAndIPEndPoint& remote_address,
+ const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
+ const std::optional<base::UnguessableToken>& devtools_token,
+ mojo::PendingRemote<network::mojom::P2PSocketClient> client,
+ mojo::PendingReceiver<network::mojom::P2PSocket> receiver) override;
+
+ private:
+ // Kept alive for the lifetime of this object so that the renderer does not
+ // observe a disconnect on its notification client.
+ mojo::Remote<network::mojom::P2PNetworkNotificationClient>
+ network_notification_client_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_RENDERER_HOST_P2P_NO_OP_P2P_SOCKET_MANAGER_H_
diff --git a/content/browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc
new file mode 100644
index 0000000..c83b26e
--- /dev/null
+++ b/content/browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc
@@ -0,0 +1,171 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/p2p/no_op_p2p_socket_manager.h"
+
+#include <optional>
+#include <vector>
+
+#include "base/functional/bind.h"
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "net/base/ip_address.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/network_interfaces.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/p2p_socket_type.h"
+#include "services/network/public/mojom/p2p.mojom.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+class FakeNetworkNotificationClient
+ : public network::mojom::P2PNetworkNotificationClient {
+ public:
+ FakeNetworkNotificationClient() = default;
+ FakeNetworkNotificationClient(const FakeNetworkNotificationClient&) = delete;
+ FakeNetworkNotificationClient& operator=(
+ const FakeNetworkNotificationClient&) = delete;
+ ~FakeNetworkNotificationClient() override = default;
+
+ mojo::PendingRemote<network::mojom::P2PNetworkNotificationClient>
+ BindNewPipeAndPassRemote() {
+ auto remote = receiver_.BindNewPipeAndPassRemote();
+ receiver_.set_disconnect_handler(base::BindOnce(
+ [](bool* disconnected) { *disconnected = true; }, &disconnected_));
+ return remote;
+ }
+
+ bool disconnected() const { return disconnected_; }
+
+ base::test::TestFuture<std::vector<net::NetworkInterface>,
+ net::IPAddress,
+ net::IPAddress>&
+ network_list_future() {
+ return network_list_future_;
+ }
+
+ // network::mojom::P2PNetworkNotificationClient:
+ void NetworkListChanged(
+ const std::vector<net::NetworkInterface>& networks,
+ const net::IPAddress& default_ipv4_local_address,
+ const net::IPAddress& default_ipv6_local_address) override {
+ network_list_future_.SetValue(networks, default_ipv4_local_address,
+ default_ipv6_local_address);
+ }
+
+ private:
+ mojo::Receiver<network::mojom::P2PNetworkNotificationClient> receiver_{this};
+ base::test::TestFuture<std::vector<net::NetworkInterface>,
+ net::IPAddress,
+ net::IPAddress>
+ network_list_future_;
+ bool disconnected_ = false;
+};
+
+class NoOpP2PSocketManagerTest : public testing::Test {
+ public:
+ NoOpP2PSocketManagerTest() {
+ NoOpP2PSocketManager::Create(manager_.BindNewPipeAndPassReceiver());
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ mojo::Remote<network::mojom::P2PSocketManager> manager_;
+};
+
+// StartNetworkNotifications() must report an (empty) network list so that the
+// renderer does not wait forever for one, and must keep the client connected.
+TEST_F(NoOpP2PSocketManagerTest, StartNetworkNotificationsReportsEmptyList) {
+ FakeNetworkNotificationClient client;
+ manager_->StartNetworkNotifications(client.BindNewPipeAndPassRemote());
+
+ auto& future = client.network_list_future();
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+ EXPECT_TRUE(future.Get<1>().empty());
+ EXPECT_TRUE(future.Get<2>().empty());
+
+ // The notification client must remain connected; otherwise the renderer
+ // would observe a disconnect.
+ manager_.FlushForTesting();
+ EXPECT_FALSE(client.disconnected());
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+// A second StartNetworkNotifications() call replaces the previous client and
+// also receives an empty network list.
+TEST_F(NoOpP2PSocketManagerTest, StartNetworkNotificationsTwice) {
+ FakeNetworkNotificationClient first_client;
+ manager_->StartNetworkNotifications(first_client.BindNewPipeAndPassRemote());
+ ASSERT_TRUE(first_client.network_list_future().Wait());
+
+ FakeNetworkNotificationClient second_client;
+ manager_->StartNetworkNotifications(second_client.BindNewPipeAndPassRemote());
+ auto& future = second_client.network_list_future();
+ ASSERT_TRUE(future.Wait());
+ EXPECT_TRUE(future.Get<0>().empty());
+
+ manager_.FlushForTesting();
+ EXPECT_TRUE(first_client.disconnected());
+ EXPECT_FALSE(second_client.disconnected());
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+// GetHostAddress() must always run its callback, with no addresses.
+TEST_F(NoOpP2PSocketManagerTest, GetHostAddressReturnsNoAddresses) {
+ base::test::TestFuture<const std::vector<net::IPAddress>&> future;
+ manager_->GetHostAddress("example.com", /*address_family=*/std::nullopt,
+ /*enable_mdns=*/true, future.GetCallback());
+ EXPECT_TRUE(future.Get().empty());
+
+ // mDNS-style names must not be resolved either.
+ base::test::TestFuture<const std::vector<net::IPAddress>&> mdns_future;
+ manager_->GetHostAddress("00000000-0000-0000-0000-000000000000.local",
+ /*address_family=*/std::nullopt,
+ /*enable_mdns=*/true, mdns_future.GetCallback());
+ EXPECT_TRUE(mdns_future.Get().empty());
+
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+// CreateSocket() must never create a socket. The socket pipe is closed, so the
+// renderer treats the socket as failed, while the manager stays connected (so
+// the renderer does not attempt to reconnect).
+TEST_F(NoOpP2PSocketManagerTest, CreateSocketClosesSocketPipe) {
+ mojo::PendingReceiver<network::mojom::P2PSocketClient> client_receiver;
+ mojo::PendingRemote<network::mojom::P2PSocketClient> client =
+ client_receiver.InitWithNewPipeAndPassRemote();
+
+ mojo::Remote<network::mojom::P2PSocket> socket;
+ base::test::TestFuture<void> socket_disconnected;
+ auto socket_receiver = socket.BindNewPipeAndPassReceiver();
+ socket.set_disconnect_handler(socket_disconnected.GetCallback());
+
+ manager_->CreateSocket(
+ network::P2P_SOCKET_UDP,
+ net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0),
+ network::P2PPortRange(),
+ network::P2PHostAndIPEndPoint(
+ "example.com", net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0)),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
+ /*devtools_token=*/std::nullopt, std::move(client),
+ std::move(socket_receiver));
+
+ EXPECT_TRUE(socket_disconnected.Wait());
+ EXPECT_FALSE(socket.is_connected());
+
+ manager_.FlushForTesting();
+ EXPECT_TRUE(manager_.is_connected());
+}
+
+} // namespace
+
+} // namespace content
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index a08ebce..2829cbb 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -88,6 +88,10 @@
#include "content/browser/can_commit_status.h"
#include "content/browser/closewatcher/close_listener_host.h"
#include "content/browser/code_cache/generated_code_cache_context.h"
+#include "content/browser/connection_allowlist_utils.h"
+#if BUILDFLAG(ENABLE_MDNS)
+#include "content/browser/renderer_host/no_op_mdns_responder.h"
+#endif
#include "content/browser/data_url_loader_factory.h"
#include "content/browser/declarative_performance_observer/declarative_performance_observer.h"
#include "content/browser/devtools/devtools_instrumentation.h"
@@ -19515,6 +19519,17 @@ RenderFrameHostImpl::CreateDeviceBoundSessionObserver() {
#if BUILDFLAG(ENABLE_MDNS)
void RenderFrameHostImpl::CreateMdnsResponder(
mojo::PendingReceiver<network::mojom::MdnsResponder> receiver) {
+ // If WebRTC is blocked by this document's Connection Allowlist, bind a no-op
+ // MdnsResponder so that no names are ever registered or announced, while
+ // still replying to every renderer request (see NoOpMdnsResponder).
+ // It is possible that a renderer which does not allow WebRTC could still
+ // reach this interface. TODO(crbug.com/571239543): Disable all WebRTC
+ // connections on the renderer side.
+ if (!IsWebRTCAllowedByConnectionAllowlist(this)) {
+ NoOpMdnsResponder::Create(std::move(receiver));
+ return;
+ }
+
GetStoragePartition()->GetNetworkContext()->CreateMdnsResponder(
std::move(receiver));
}
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 7ecab4f..454e0aa 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -21,6 +21,7 @@ import("//mojo/public/tools/bindings/mojom.gni")
import("//net/features.gni")
import("//printing/buildflags/buildflags.gni")
import("//services/device/public/cpp/compute_pressure/buildflags.gni")
+import("//services/network/public/cpp/features.gni")
import("//services/screen_ai/buildflags/features.gni")
import("//testing/test.gni")
import("//third_party/blink/public/public_features.gni")
@@ -3402,6 +3403,16 @@ test("content_unittests") {
deps += [ "//content/browser/smart_card/emulation" ]
}
+ if (enable_mdns) {
+ sources += [ "../browser/renderer_host/no_op_mdns_responder_unittest.cc" ]
+ }
+
+ if (is_p2p_enabled) {
+ sources +=
+ [ "../browser/renderer_host/p2p/no_op_p2p_socket_manager_unittest.cc" ]
+ deps += [ "//services/network/public/cpp:cpp_p2p" ]
+ }
+
if (enable_nocompile_tests) {
deps += [ ":content_nocompile_tests" ]
}
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js
new file mode 100644
index 0000000..cce0fd7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js
@@ -0,0 +1,74 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/content-security-policy/webrtc/webrtc.js
+//
+// The following tests assume the policy `Connection-Allowlist:
+// (response-origin);webrtc=allow` has been set. Child frames with a local
+// scheme (about:blank, about:srcdoc) inherit the parent's policy container,
+// and should continue to allow WebRTC, including when nested.
+
+const WEBRTC_HELPER = '/content-security-policy/webrtc/webrtc.js';
+// Loads webrtc.js into `win`'s own document, so `tryConnect` (and the
+// RTCPeerConnection it uses) run in the child's realm, not the parent's.
+function loadHelper(win) {
+ return new Promise((resolve, reject) => {
+ const script = win.document.createElement('script');
+ script.src = WEBRTC_HELPER;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error(`Failed to load ${WEBRTC_HELPER}`));
+ win.document.head.appendChild(script);
+ });
+}
+
+async function tryConnectIn(win) {
+ await loadHelper(win);
+ return win.tryConnect();
+}
+
+// Appends an iframe with no `src` to `doc`. The frame stays on its initial
+// about:blank document, which is usable synchronously.
+function addBlankFrame(t, doc) {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ doc.body.appendChild(frame);
+ return frame;
+}
+
+// Appends an about:srcdoc iframe to `doc`, and waits for the srcdoc document
+// to load (before that, `contentWindow` is still the initial about:blank
+// document).
+async function addSrcdocFrame(
+ t, doc, srcdoc = '<!doctype html><body><p>Hello World</p></body>') {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ const loaded = new Promise(resolve => frame.onload = resolve);
+ frame.srcdoc = srcdoc;
+ doc.body.appendChild(frame);
+ await loaded;
+ return frame;
+}
+
+promise_test(async t => {
+ assert_equals(await tryConnect(), 'allowed');
+}, 'WebRTC is allowed in the top-level document.');
+
+promise_test(async t => {
+ const frame = addBlankFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'allowed');
+}, 'WebRTC is allowed in an about:blank child frame.');
+
+promise_test(async t => {
+ const frame = await addSrcdocFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'allowed');
+}, 'WebRTC is allowed in an about:srcdoc child frame.');
+
+promise_test(async t => {
+ const outer = addBlankFrame(t, document);
+ const inner = addBlankFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'allowed');
+}, 'WebRTC is allowed in a nested about:blank frame.');
+
+promise_test(async t => {
+ const outer = await addSrcdocFrame(t, document);
+ const inner = await addSrcdocFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'allowed');
+}, 'WebRTC is allowed in a nested about:srcdoc frame.');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js.headers
new file mode 100644
index 0000000..e8ebc16
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-allow.sub.window.js.headers
@@ -0,0 +1 @@
+Connection-Allowlist: (response-origin);webrtc=allow
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js
new file mode 100644
index 0000000..e782a57
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js
@@ -0,0 +1,75 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/content-security-policy/webrtc/webrtc.js
+//
+// The following tests assume the policy `Connection-Allowlist:
+// (response-origin)` has been set. WebRTC has been blocked by default. Child
+// frames with a local scheme (about:blank, about:srcdoc) inherit the parent's
+// policy container, and should continue to block WebRTC, including when
+// nested.
+
+const WEBRTC_HELPER = '/content-security-policy/webrtc/webrtc.js';
+// Loads webrtc.js into `win`'s own document, so `tryConnect` (and the
+// RTCPeerConnection it uses) run in the child's realm, not the parent's.
+function loadHelper(win) {
+ return new Promise((resolve, reject) => {
+ const script = win.document.createElement('script');
+ script.src = WEBRTC_HELPER;
+ script.onload = resolve;
+ script.onerror = () => reject(new Error(`Failed to load ${WEBRTC_HELPER}`));
+ win.document.head.appendChild(script);
+ });
+}
+
+async function tryConnectIn(win) {
+ await loadHelper(win);
+ return win.tryConnect();
+}
+
+// Appends an iframe with no `src` to `doc`. The frame stays on its initial
+// about:blank document, which is usable synchronously.
+function addBlankFrame(t, doc) {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ doc.body.appendChild(frame);
+ return frame;
+}
+
+// Appends an about:srcdoc iframe to `doc`, and waits for the srcdoc document
+// to load (before that, `contentWindow` is still the initial about:blank
+// document).
+async function addSrcdocFrame(
+ t, doc, srcdoc = '<!doctype html><body><p>Hello World</p></body>') {
+ const frame = doc.createElement('iframe');
+ t.add_cleanup(() => frame.remove());
+ const loaded = new Promise(resolve => frame.onload = resolve);
+ frame.srcdoc = srcdoc;
+ doc.body.appendChild(frame);
+ await loaded;
+ return frame;
+}
+
+promise_test(async t => {
+ assert_equals(await tryConnect(), 'blocked');
+}, 'WebRTC is blocked in the top-level document.');
+
+promise_test(async t => {
+ const frame = addBlankFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'blocked');
+}, 'WebRTC is blocked in an about:blank child frame.');
+
+promise_test(async t => {
+ const frame = await addSrcdocFrame(t, document);
+ assert_equals(await tryConnectIn(frame.contentWindow), 'blocked');
+}, 'WebRTC is blocked in an about:srcdoc child frame.');
+
+promise_test(async t => {
+ const outer = addBlankFrame(t, document);
+ const inner = addBlankFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'blocked');
+}, 'WebRTC is blocked in a nested about:blank frame.');
+
+promise_test(async t => {
+ const outer = await addSrcdocFrame(t, document);
+ const inner = await addSrcdocFrame(t, outer.contentDocument);
+ assert_equals(await tryConnectIn(inner.contentWindow), 'blocked');
+}, 'WebRTC is blocked in a nested about:srcdoc frame.');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js.headers
new file mode 100644
index 0000000..0dd9194
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-local-scheme-block.sub.window.js.headers
@@ -0,0 +1 @@
+Connection-Allowlist: (response-origin)
\ No newline at end of file