903 lines
36 KiB
Diff
903 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
|