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}
This commit is contained in:
parent
7c78e42a50
commit
7627fdd68a
@ -720,38 +720,6 @@ void Connection::HandlePiggybackCheckAcknowledgementIfAny(StunMessage* msg) {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ ConnectionInfo Connection::stats() {
|
||||
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 @@ void Connection::MaybeUpdateLocalCandidate(ConnectionRequest* request,
|
||||
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);
|
||||
}
|
||||
|
||||
@ -238,10 +238,6 @@ class Connection : public CandidatePairInterface,
|
||||
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 @@ class Connection : public CandidatePairInterface,
|
||||
void MaybeUpdateLocalCandidate(ConnectionRequest* request,
|
||||
StunMessage* response);
|
||||
|
||||
void CopyCandidatesToStatsAndSanitizeIfNecessary();
|
||||
|
||||
void LogCandidatePairConfig(webrtc::IceCandidatePairConfigType type);
|
||||
void LogCandidatePairEvent(webrtc::IceCandidatePairEventType type,
|
||||
uint32_t transaction_id);
|
||||
|
||||
@ -387,7 +387,11 @@ P2PTransportChannel::GetSelectedCandidatePair() const {
|
||||
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 @@ bool P2PTransportChannel::GetStats(ConnectionInfos* candidate_pair_stats_list,
|
||||
|
||||
// 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 @@ void P2PTransportChannel::SetReceiving(bool receiving) {
|
||||
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) {
|
||||
|
||||
@ -404,6 +404,15 @@ class RTC_EXPORT P2PTransportChannel : public IceTransportInternal {
|
||||
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_);
|
||||
|
||||
@ -1556,6 +1556,68 @@ TEST_F(P2PTransportChannelTest, PeerReflexiveCandidateBeforeSignaling) {
|
||||
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) {
|
||||
|
||||
@ -83,27 +83,6 @@ bool PortAllocatorSession::IsStopped() const {
|
||||
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 @@ std::vector<IceParameters> PortAllocator::GetPooledIceCredentials() {
|
||||
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
|
||||
|
||||
@ -244,7 +244,7 @@ class RTC_EXPORT PortAllocatorSession : public sigslot::has_slots<> {
|
||||
// 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 @@ class RTC_EXPORT PortAllocator : public sigslot::has_slots<> {
|
||||
// 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 @@ class RTC_EXPORT PortAllocator : public sigslot::has_slots<> {
|
||||
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 {
|
||||
|
||||
@ -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::Regather(
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ void BasicPortAllocatorSession::GetCandidatesFromPort(
|
||||
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 @@ void BasicPortAllocatorSession::OnCandidateReady(Port* port,
|
||||
|
||||
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.";
|
||||
|
||||
@ -88,6 +88,8 @@ class RTC_EXPORT BasicPortAllocator : public PortAllocator {
|
||||
// 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 @@ class RTC_EXPORT BasicPortAllocatorSession : public PortAllocatorSession,
|
||||
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 @@ class RTC_EXPORT BasicPortAllocatorSession : public PortAllocatorSession,
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user