Sanitize the address field of peer-reflexive remote candidates.
Per the latest WebRTC stats spec
(https://w3c.github.io/webrtc-stats/#dom-rtcicecandidatestats)
the address field of a peer-reflexive remote candidate should be concealed
until the same address is learnt via addIceCandidate.
This CL also refactors the sanitization-related code paths.
Bug: chromium:968161
Change-Id: I74c5da78232b2f604689867bda2937b8af827c4f
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/149381
Reviewed-by: Steve Anton <steveanton@webrtc.org>
Commit-Queue: Qingsi Wang <qingsi@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#28909}
diff --git a/p2p/base/connection.cc b/p2p/base/connection.cc
index 7c6ae5c..8b2c8d9 100644
--- a/p2p/base/connection.cc
+++ b/p2p/base/connection.cc
@@ -720,38 +720,6 @@
}
}
-CandidatePair Connection::ToCandidatePairAndSanitizeIfNecessary() const {
- auto get_sanitized_copy = [](const Candidate& c) {
- bool use_hostname_address = c.type() == LOCAL_PORT_TYPE;
- bool filter_related_address = c.type() == STUN_PORT_TYPE;
- return c.ToSanitizedCopy(use_hostname_address, filter_related_address);
- };
-
- CandidatePair pair;
- if (port_->Network()->GetMdnsResponder() != nullptr) {
- // When the mDNS obfuscation of local IPs is enabled, we sanitize local
- // candidates.
- pair.local = get_sanitized_copy(local_candidate());
- } else {
- pair.local = local_candidate();
- }
-
- if (!remote_candidate().address().hostname().empty()) {
- // If the remote endpoint signaled us a hostname candidate, we assume it is
- // supposed to be sanitized in the stats.
- //
- // A prflx remote candidate should not have a hostname set.
- RTC_DCHECK(remote_candidate().type() != PRFLX_PORT_TYPE);
- // A remote hostname candidate should have a resolved IP before we can form
- // a candidate pair.
- RTC_DCHECK(!remote_candidate().address().IsUnresolvedIP());
- pair.remote = get_sanitized_copy(remote_candidate());
- } else {
- pair.remote = remote_candidate();
- }
- return pair;
-}
-
void Connection::ReceivedPingResponse(
int rtt,
const std::string& request_id,
@@ -1061,7 +1029,8 @@
stats_.nominated = nominated();
stats_.total_round_trip_time_ms = total_round_trip_time_ms_;
stats_.current_round_trip_time_ms = current_round_trip_time_ms_;
- CopyCandidatesToStatsAndSanitizeIfNecessary();
+ stats_.local_candidate = local_candidate();
+ stats_.remote_candidate = remote_candidate();
return stats_;
}
@@ -1138,12 +1107,6 @@
SignalStateChange(this);
}
-void Connection::CopyCandidatesToStatsAndSanitizeIfNecessary() {
- auto pair = ToCandidatePairAndSanitizeIfNecessary();
- stats_.local_candidate = pair.local_candidate();
- stats_.remote_candidate = pair.remote_candidate();
-}
-
bool Connection::rtt_converged() const {
return rtt_samples_ > (RTT_RATIO + 1);
}
diff --git a/p2p/base/connection.h b/p2p/base/connection.h
index 92fc2ed..b872dbf 100644
--- a/p2p/base/connection.h
+++ b/p2p/base/connection.h
@@ -238,10 +238,6 @@
void HandlePiggybackCheckAcknowledgementIfAny(StunMessage* msg);
int64_t last_data_received() const { return last_data_received_; }
- // Returns the equivalent candidate pair and sanitizes the local and the
- // remote candidates if necessary.
- CandidatePair ToCandidatePairAndSanitizeIfNecessary() const;
-
// Debugging description of this connection
std::string ToDebugId() const;
std::string ToString() const;
@@ -346,8 +342,6 @@
void MaybeUpdateLocalCandidate(ConnectionRequest* request,
StunMessage* response);
- void CopyCandidatesToStatsAndSanitizeIfNecessary();
-
void LogCandidatePairConfig(webrtc::IceCandidatePairConfigType type);
void LogCandidatePairEvent(webrtc::IceCandidatePairEventType type,
uint32_t transaction_id);
diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc
index 01caaa9..f6a8bbc 100644
--- a/p2p/base/p2p_transport_channel.cc
+++ b/p2p/base/p2p_transport_channel.cc
@@ -387,7 +387,11 @@
return absl::nullopt;
}
- return selected_connection_->ToCandidatePairAndSanitizeIfNecessary();
+ CandidatePair pair;
+ pair.local = SanitizeLocalCandidate(selected_connection_->local_candidate());
+ pair.remote =
+ SanitizeRemoteCandidate(selected_connection_->remote_candidate());
+ return pair;
}
// A channel is considered ICE completed once there is at most one active
@@ -1502,9 +1506,11 @@
// TODO(qingsi): Remove naming inconsistency for candidate pair/connection.
for (Connection* connection : connections_) {
- ConnectionInfo candidate_pair_stats = connection->stats();
- candidate_pair_stats.best_connection = (selected_connection_ == connection);
- candidate_pair_stats_list->push_back(std::move(candidate_pair_stats));
+ ConnectionInfo stats = connection->stats();
+ stats.local_candidate = SanitizeLocalCandidate(stats.local_candidate);
+ stats.remote_candidate = SanitizeRemoteCandidate(stats.remote_candidate);
+ stats.best_connection = (selected_connection_ == connection);
+ candidate_pair_stats_list->push_back(std::move(stats));
connection->set_reported(true);
}
@@ -2645,6 +2651,27 @@
SignalReceivingState(this);
}
+Candidate P2PTransportChannel::SanitizeLocalCandidate(
+ const Candidate& c) const {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ // Delegates to the port allocator.
+ return allocator_->SanitizeCandidate(c);
+}
+
+Candidate P2PTransportChannel::SanitizeRemoteCandidate(
+ const Candidate& c) const {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ // If the remote endpoint signaled us a hostname host candidate, we assume it
+ // is supposed to be sanitized.
+ bool use_hostname_address =
+ c.type() == LOCAL_PORT_TYPE && !c.address().hostname().empty();
+ // Remove the address for prflx remote candidates. See
+ // https://w3c.github.io/webrtc-stats/#dom-rtcicecandidatestats.
+ use_hostname_address |= c.type() == PRFLX_PORT_TYPE;
+ return c.ToSanitizedCopy(use_hostname_address,
+ false /* filter_related_address */);
+}
+
void P2PTransportChannel::LogCandidatePairConfig(
Connection* conn,
webrtc::IceCandidatePairConfigType type) {
diff --git a/p2p/base/p2p_transport_channel.h b/p2p/base/p2p_transport_channel.h
index f9ea59d..6fa64e0 100644
--- a/p2p/base/p2p_transport_channel.h
+++ b/p2p/base/p2p_transport_channel.h
@@ -404,6 +404,15 @@
void SetWritable(bool writable);
// Sets the receiving state, signaling if necessary.
void SetReceiving(bool receiving);
+ // Clears the address and the related address fields of a local candidate to
+ // avoid IP leakage. This is applicable in several scenarios as commented in
+ // |PortAllocator::SanitizeCandidate|.
+ Candidate SanitizeLocalCandidate(const Candidate& c) const;
+ // Clears the address field of a remote candidate to avoid IP leakage. This is
+ // applicable in the following scenarios:
+ // 1. mDNS candidates are received.
+ // 2. Peer-reflexive remote candidates.
+ Candidate SanitizeRemoteCandidate(const Candidate& c) const;
std::string transport_name_ RTC_GUARDED_BY(network_thread_);
int component_ RTC_GUARDED_BY(network_thread_);
diff --git a/p2p/base/p2p_transport_channel_unittest.cc b/p2p/base/p2p_transport_channel_unittest.cc
index 1b93475..65f7d20 100644
--- a/p2p/base/p2p_transport_channel_unittest.cc
+++ b/p2p/base/p2p_transport_channel_unittest.cc
@@ -1556,6 +1556,68 @@
DestroyChannels();
}
+// Test that if we learn a prflx remote candidate, its address is concealed in
+// 1. the selected candidate pair accessed via the public API, and
+// 2. the candidate pair stats
+// until we learn the same address from signaling.
+TEST_F(P2PTransportChannelTest, PeerReflexiveRemoteCandidateIsSanitized) {
+ ConfigureEndpoints(OPEN, OPEN, kOnlyLocalPorts, kOnlyLocalPorts);
+ // Emulate no remote parameters coming in.
+ set_remote_ice_parameter_source(FROM_CANDIDATE);
+ CreateChannels();
+ // Only have remote parameters come in for ep2, not ep1.
+ ep2_ch1()->SetRemoteIceParameters(kIceParams[0]);
+
+ // Pause sending ep2's candidates to ep1 until ep1 receives the peer reflexive
+ // candidate.
+ PauseCandidates(1);
+
+ ASSERT_TRUE_WAIT(ep2_ch1()->selected_connection() != nullptr, kMediumTimeout);
+ ep1_ch1()->SetRemoteIceParameters(kIceParams[1]);
+ ASSERT_TRUE_WAIT(ep1_ch1()->selected_connection() != nullptr, kMediumTimeout);
+
+ // Check the selected candidate pair.
+ auto pair_ep1 = ep1_ch1()->GetSelectedCandidatePair();
+ ASSERT_TRUE(pair_ep1.has_value());
+ EXPECT_EQ(PRFLX_PORT_TYPE, pair_ep1->remote_candidate().type());
+ EXPECT_TRUE(pair_ep1->remote_candidate().address().ipaddr().IsNil());
+
+ ConnectionInfos pair_stats;
+ CandidateStatsList candidate_stats;
+ ep1_ch1()->GetStats(&pair_stats, &candidate_stats);
+ // Check the candidate pair stats.
+ ASSERT_EQ(1u, pair_stats.size());
+ EXPECT_EQ(PRFLX_PORT_TYPE, pair_stats[0].remote_candidate.type());
+ EXPECT_TRUE(pair_stats[0].remote_candidate.address().ipaddr().IsNil());
+
+ // Let ep1 receive the remote candidate to update its type from prflx to host.
+ ResumeCandidates(1);
+ ASSERT_TRUE_WAIT(
+ ep1_ch1()->selected_connection() != nullptr &&
+ ep1_ch1()->selected_connection()->remote_candidate().type() ==
+ LOCAL_PORT_TYPE,
+ kMediumTimeout);
+
+ // We should be able to reveal the address after it is learnt via
+ // AddIceCandidate.
+ //
+ // Check the selected candidate pair.
+ auto updated_pair_ep1 = ep1_ch1()->GetSelectedCandidatePair();
+ ASSERT_TRUE(updated_pair_ep1.has_value());
+ EXPECT_EQ(LOCAL_PORT_TYPE, updated_pair_ep1->remote_candidate().type());
+ EXPECT_TRUE(
+ updated_pair_ep1->remote_candidate().address().EqualIPs(kPublicAddrs[1]));
+
+ ep1_ch1()->GetStats(&pair_stats, &candidate_stats);
+ // Check the candidate pair stats.
+ ASSERT_EQ(1u, pair_stats.size());
+ EXPECT_EQ(LOCAL_PORT_TYPE, pair_stats[0].remote_candidate.type());
+ EXPECT_TRUE(
+ pair_stats[0].remote_candidate.address().EqualIPs(kPublicAddrs[1]));
+
+ DestroyChannels();
+}
+
// Test that we properly create a connection on a STUN ping from unknown address
// when the signaling is slow and the end points are behind NAT.
TEST_F(P2PTransportChannelTest, PeerReflexiveCandidateBeforeSignalingWithNAT) {
diff --git a/p2p/base/port_allocator.cc b/p2p/base/port_allocator.cc
index 6228791..a9d7cb6 100644
--- a/p2p/base/port_allocator.cc
+++ b/p2p/base/port_allocator.cc
@@ -83,27 +83,6 @@
return false;
}
-void PortAllocatorSession::GetCandidateStatsFromReadyPorts(
- CandidateStatsList* candidate_stats_list) const {
- auto ports = ReadyPorts();
- for (auto* port : ports) {
- auto candidates = port->Candidates();
- for (const auto& candidate : candidates) {
- CandidateStats candidate_stats(candidate);
- port->GetStunStats(&candidate_stats.stun_stats);
- bool mdns_obfuscation_enabled =
- port->Network()->GetMdnsResponder() != nullptr;
- if (mdns_obfuscation_enabled) {
- bool use_hostname_address = candidate.type() == LOCAL_PORT_TYPE;
- bool filter_related_address = candidate.type() == STUN_PORT_TYPE;
- candidate_stats.candidate = candidate_stats.candidate.ToSanitizedCopy(
- use_hostname_address, filter_related_address);
- }
- candidate_stats_list->push_back(std::move(candidate_stats));
- }
- }
-}
-
uint32_t PortAllocatorSession::generation() {
return generation_;
}
@@ -318,4 +297,25 @@
return list;
}
+Candidate PortAllocator::SanitizeCandidate(const Candidate& c) const {
+ CheckRunOnValidThreadAndInitialized();
+ // For a local host candidate, we need to conceal its IP address candidate if
+ // the mDNS obfuscation is enabled.
+ bool use_hostname_address =
+ c.type() == LOCAL_PORT_TYPE && MdnsObfuscationEnabled();
+ // If adapter enumeration is disabled or host candidates are disabled,
+ // clear the raddr of STUN candidates to avoid local address leakage.
+ bool filter_stun_related_address =
+ ((flags() & PORTALLOCATOR_DISABLE_ADAPTER_ENUMERATION) &&
+ (flags() & PORTALLOCATOR_DISABLE_DEFAULT_LOCAL_CANDIDATE)) ||
+ !(candidate_filter_ & CF_HOST) || MdnsObfuscationEnabled();
+ // If the candidate filter doesn't allow reflexive addresses, empty TURN raddr
+ // to avoid reflexive address leakage.
+ bool filter_turn_related_address = !(candidate_filter_ & CF_REFLEXIVE);
+ bool filter_related_address =
+ ((c.type() == STUN_PORT_TYPE && filter_stun_related_address) ||
+ (c.type() == RELAY_PORT_TYPE && filter_turn_related_address));
+ return c.ToSanitizedCopy(use_hostname_address, filter_related_address);
+}
+
} // namespace cricket
diff --git a/p2p/base/port_allocator.h b/p2p/base/port_allocator.h
index d78b6cb..c0b0e60 100644
--- a/p2p/base/port_allocator.h
+++ b/p2p/base/port_allocator.h
@@ -244,7 +244,7 @@
// Get candidate-level stats from all candidates on the ready ports and return
// the stats to the given list.
virtual void GetCandidateStatsFromReadyPorts(
- CandidateStatsList* candidate_stats_list) const;
+ CandidateStatsList* candidate_stats_list) const {}
// Set the interval at which STUN candidates will resend STUN binding requests
// on the underlying ports to keep NAT bindings open.
// The default value of the interval in implementation is restored if a null
@@ -430,6 +430,13 @@
// Discard any remaining pooled sessions.
void DiscardCandidatePool();
+ // Clears the address and the related address fields of a local candidate to
+ // avoid IP leakage. This is applicable in several scenarios:
+ // 1. Sanitization is configured via the candidate filter.
+ // 2. Sanitization is configured via the port allocator flags.
+ // 3. mDNS concealment of private IPs is enabled.
+ Candidate SanitizeCandidate(const Candidate& c) const;
+
uint32_t flags() const {
CheckRunOnValidThreadIfInitialized();
return flags_;
@@ -594,6 +601,9 @@
return pooled_sessions_;
}
+ // Returns true if there is an mDNS responder attached to the network manager.
+ virtual bool MdnsObfuscationEnabled() const { return false; }
+
// The following thread checks are only done in DCHECK for the consistency
// with the exsiting thread checks.
void CheckRunOnValidThreadIfInitialized() const {
diff --git a/p2p/client/basic_port_allocator.cc b/p2p/client/basic_port_allocator.cc
index b1f147d..316bc87 100644
--- a/p2p/client/basic_port_allocator.cc
+++ b/p2p/client/basic_port_allocator.cc
@@ -14,6 +14,7 @@
#include <functional>
#include <set>
#include <string>
+#include <utility>
#include <vector>
#include "absl/algorithm/container.h"
@@ -530,6 +531,19 @@
}
}
+void BasicPortAllocatorSession::GetCandidateStatsFromReadyPorts(
+ CandidateStatsList* candidate_stats_list) const {
+ auto ports = ReadyPorts();
+ for (auto* port : ports) {
+ auto candidates = port->Candidates();
+ for (const auto& candidate : candidates) {
+ CandidateStats candidate_stats(allocator_->SanitizeCandidate(candidate));
+ port->GetStunStats(&candidate_stats.stun_stats);
+ candidate_stats_list->push_back(std::move(candidate_stats));
+ }
+ }
+}
+
void BasicPortAllocatorSession::SetStunKeepaliveIntervalForReadyPorts(
const absl::optional<int>& stun_keepalive_interval) {
RTC_DCHECK_RUN_ON(network_thread_);
@@ -578,35 +592,12 @@
if (!CheckCandidateFilter(candidate)) {
continue;
}
- auto sanitized_candidate = SanitizeCandidate(candidate);
- candidates->push_back(sanitized_candidate);
+ candidates->push_back(allocator_->SanitizeCandidate(candidate));
}
}
-bool BasicPortAllocatorSession::MdnsObfuscationEnabled() const {
- return allocator_->network_manager()->GetMdnsResponder() != nullptr;
-}
-
-Candidate BasicPortAllocatorSession::SanitizeCandidate(
- const Candidate& c) const {
- RTC_DCHECK_RUN_ON(network_thread_);
- // If the candidate has a generated hostname, we need to obfuscate its IP
- // address when signaling this candidate.
- bool use_hostname_address =
- !c.address().hostname().empty() && !c.address().IsUnresolvedIP();
- // If adapter enumeration is disabled or host candidates are disabled,
- // clear the raddr of STUN candidates to avoid local address leakage.
- bool filter_stun_related_address =
- ((flags() & PORTALLOCATOR_DISABLE_ADAPTER_ENUMERATION) &&
- (flags() & PORTALLOCATOR_DISABLE_DEFAULT_LOCAL_CANDIDATE)) ||
- !(candidate_filter_ & CF_HOST) || MdnsObfuscationEnabled();
- // If the candidate filter doesn't allow reflexive addresses, empty TURN raddr
- // to avoid reflexive address leakage.
- bool filter_turn_related_address = !(candidate_filter_ & CF_REFLEXIVE);
- bool filter_related_address =
- ((c.type() == STUN_PORT_TYPE && filter_stun_related_address) ||
- (c.type() == RELAY_PORT_TYPE && filter_turn_related_address));
- return c.ToSanitizedCopy(use_hostname_address, filter_related_address);
+bool BasicPortAllocator::MdnsObfuscationEnabled() const {
+ return network_manager()->GetMdnsResponder() != nullptr;
}
bool BasicPortAllocatorSession::CandidatesAllocationDone() const {
@@ -1014,7 +1005,7 @@
if (data->ready() && CheckCandidateFilter(c)) {
std::vector<Candidate> candidates;
- candidates.push_back(SanitizeCandidate(c));
+ candidates.push_back(allocator_->SanitizeCandidate(c));
SignalCandidatesReady(this, candidates);
} else {
RTC_LOG(LS_INFO) << "Discarding candidate because it doesn't match filter.";
diff --git a/p2p/client/basic_port_allocator.h b/p2p/client/basic_port_allocator.h
index 13611e7..50cb83d 100644
--- a/p2p/client/basic_port_allocator.h
+++ b/p2p/client/basic_port_allocator.h
@@ -88,6 +88,8 @@
// This function makes sure that relay_port_factory_ is set properly.
void InitRelayPortFactory(RelayPortFactoryInterface* relay_port_factory);
+ bool MdnsObfuscationEnabled() const override;
+
rtc::NetworkManager* network_manager_;
rtc::PacketSocketFactory* socket_factory_;
bool allow_tcp_listen_;
@@ -147,6 +149,8 @@
bool CandidatesAllocationDone() const override;
void RegatherOnFailedNetworks() override;
void RegatherOnAllNetworks() override;
+ void GetCandidateStatsFromReadyPorts(
+ CandidateStatsList* candidate_stats_list) const override;
void SetStunKeepaliveIntervalForReadyPorts(
const absl::optional<int>& stun_keepalive_interval) override;
void PruneAllPorts() override;
@@ -248,14 +252,6 @@
bool CheckCandidateFilter(const Candidate& c) const;
bool CandidatePairable(const Candidate& c, const Port* port) const;
- // Returns true if there is an mDNS responder attached to the network manager
- bool MdnsObfuscationEnabled() const;
-
- // Clears 1) the address if the candidate is supposedly a hostname candidate;
- // 2) the related address according to the flags and candidate filter in order
- // to avoid leaking any information.
- Candidate SanitizeCandidate(const Candidate& c) const;
-
std::vector<PortData*> GetUnprunedPorts(
const std::vector<rtc::Network*>& networks);
// Prunes ports and signal the remote side to remove the candidates that