From 414706543d88922b21bc4619079ff5522212ae9e Mon Sep 17 00:00:00 2001 From: Mirko Bonadei Date: Fri, 6 Dec 2024 10:20:01 +0000 Subject: [PATCH] Revert "Remove stun_prober" This reverts commit 03f56d75d5a4bbbc6b6fe93e119f73c69ff98267. Reason for revert: Breaks downstream project. Original change's description: > Remove stun_prober > > The STUN prober shows the old RFC 3489 way of determining the NAT type > by pinging two different servers. This is known to be faulty as pointed > out by > https://datatracker.ietf.org/doc/html/rfc5389#section-2 > > Chromium dependency removed in > https://chromium-review.googlesource.com/c/chromium/src/+/6036622 > > BUG=None > > Change-Id: I2b61dfe2ff899ce71ec9d2253dc836c5908cf8c6 > Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/368182 > Commit-Queue: Philipp Hancke > Reviewed-by: Harald Alvestrand > Reviewed-by: Mirko Bonadei > Cr-Commit-Position: refs/heads/main@{#43503} Bug: None Change-Id: I08d01d4c9d882aca883e1c889aed8bddbca65b91 No-Presubmit: true No-Tree-Checks: true No-Try: true Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/370540 Bot-Commit: rubber-stamper@appspot.gserviceaccount.com Reviewed-by: Jeremy Leconte Commit-Queue: Mirko Bonadei Reviewed-by: Harald Alvestrand Cr-Commit-Position: refs/heads/main@{#43506} --- BUILD.gn | 1 + examples/BUILD.gn | 33 ++ examples/stunprober/main.cc | 153 +++++++ p2p/BUILD.gn | 55 ++- p2p/stunprober/stun_prober.cc | 602 +++++++++++++++++++++++++ p2p/stunprober/stun_prober.h | 251 +++++++++++ p2p/stunprober/stun_prober_unittest.cc | 177 ++++++++ 7 files changed, 1271 insertions(+), 1 deletion(-) create mode 100644 examples/stunprober/main.cc create mode 100644 p2p/stunprober/stun_prober.cc create mode 100644 p2p/stunprober/stun_prober.h create mode 100644 p2p/stunprober/stun_prober_unittest.cc diff --git a/BUILD.gn b/BUILD.gn index d829c0fccc..f05f71b687 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -650,6 +650,7 @@ if (rtc_include_tests && !build_with_chromium) { "api/video_codecs/test:video_codecs_api_unittests", "api/voip:compile_all_headers", "call:fake_network_pipe_unittests", + "p2p:libstunprober_unittests", "p2p:rtc_p2p_unittests", "rtc_base:async_dns_resolver_unittests", "rtc_base:async_packet_socket_unittest", diff --git a/examples/BUILD.gn b/examples/BUILD.gn index 6183b9fe3d..d21d70535e 100644 --- a/examples/BUILD.gn +++ b/examples/BUILD.gn @@ -42,6 +42,10 @@ group("examples") { } } + if (!build_with_chromium) { + deps += [ ":stun_prober" ] + } + if (is_ios || (is_mac && target_cpu != "x86")) { deps += [ ":AppRTCMobile" ] } @@ -865,3 +869,32 @@ if (is_android) { ] ] } } + +if (!build_with_chromium) { + # Doesn't build within Chrome on Win. + rtc_executable("stun_prober") { + testonly = true + sources = [ "stunprober/main.cc" ] + deps = [ + "../p2p:basic_packet_socket_factory", + "../p2p:libstunprober", + "../p2p:rtc_p2p", + "../rtc_base:checks", + "../rtc_base:crypto_random", + "../rtc_base:logging", + "../rtc_base:network", + "../rtc_base:socket_address", + "../rtc_base:ssl_adapter", + "../rtc_base:threading", + "../rtc_base:timeutils", + "../test:scoped_key_value_config", + "//third_party/abseil-cpp/absl/flags:flag", + "//third_party/abseil-cpp/absl/flags:parse", + ] + if (is_win) { + deps += [ + "../rtc_base:win32_socket_init", + ] + } + } +} diff --git a/examples/stunprober/main.cc b/examples/stunprober/main.cc new file mode 100644 index 0000000000..023c52fa0e --- /dev/null +++ b/examples/stunprober/main.cc @@ -0,0 +1,153 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "p2p/base/basic_packet_socket_factory.h" +#include "p2p/stunprober/stun_prober.h" +#include "rtc_base/crypto_random.h" +#include "rtc_base/logging.h" +#include "rtc_base/network.h" +#include "rtc_base/physical_socket_server.h" +#include "rtc_base/socket_address.h" +#include "rtc_base/ssl_adapter.h" +#include "rtc_base/thread.h" +#include "rtc_base/time_utils.h" +#include "test/scoped_key_value_config.h" + +#ifdef WEBRTC_WIN +#include "rtc_base/win32_socket_init.h" +#endif // WEBRTC_WIN + +using stunprober::AsyncCallback; +using stunprober::StunProber; + +ABSL_FLAG(int, + interval, + 10, + "Interval of consecutive stun pings in milliseconds"); +ABSL_FLAG(bool, + shared_socket, + false, + "Share socket mode for different remote IPs"); +ABSL_FLAG(int, + pings_per_ip, + 10, + "Number of consecutive stun pings to send for each IP"); +ABSL_FLAG(int, + timeout, + 1000, + "Milliseconds of wait after the last ping sent before exiting"); +ABSL_FLAG( + std::string, + servers, + "stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302", + "Comma separated STUN server addresses with ports"); + +namespace { + +const char* PrintNatType(stunprober::NatType type) { + switch (type) { + case stunprober::NATTYPE_NONE: + return "Not behind a NAT"; + case stunprober::NATTYPE_UNKNOWN: + return "Unknown NAT type"; + case stunprober::NATTYPE_SYMMETRIC: + return "Symmetric NAT"; + case stunprober::NATTYPE_NON_SYMMETRIC: + return "Non-Symmetric NAT"; + default: + return "Invalid"; + } +} + +void PrintStats(StunProber* prober) { + StunProber::Stats stats; + if (!prober->GetStats(&stats)) { + RTC_LOG(LS_WARNING) << "Results are inconclusive."; + return; + } + + RTC_LOG(LS_INFO) << "Shared Socket Mode: " << stats.shared_socket_mode; + RTC_LOG(LS_INFO) << "Requests sent: " << stats.num_request_sent; + RTC_LOG(LS_INFO) << "Responses received: " << stats.num_response_received; + RTC_LOG(LS_INFO) << "Target interval (ns): " + << stats.target_request_interval_ns; + RTC_LOG(LS_INFO) << "Actual interval (ns): " + << stats.actual_request_interval_ns; + RTC_LOG(LS_INFO) << "NAT Type: " << PrintNatType(stats.nat_type); + RTC_LOG(LS_INFO) << "Host IP: " << stats.host_ip; + RTC_LOG(LS_INFO) << "Server-reflexive ips: "; + for (auto& ip : stats.srflx_addrs) { + RTC_LOG(LS_INFO) << "\t" << ip; + } + + RTC_LOG(LS_INFO) << "Success Precent: " << stats.success_percent; + RTC_LOG(LS_INFO) << "Response Latency:" << stats.average_rtt_ms; +} + +void StopTrial(rtc::Thread* thread, StunProber* prober, int result) { + thread->Quit(); + if (prober) { + RTC_LOG(LS_INFO) << "Result: " << result; + if (result == StunProber::SUCCESS) { + PrintStats(prober); + } + } +} + +} // namespace + +int main(int argc, char* argv[]) { +#ifdef WEBRTC_WIN + rtc::WinsockInitializer winsock_init; +#endif // WEBRTC_WIN + absl::ParseCommandLine(argc, argv); + + std::vector server_addresses; + std::istringstream servers(absl::GetFlag(FLAGS_servers)); + std::string server; + while (getline(servers, server, ',')) { + rtc::SocketAddress addr; + if (!addr.FromString(server)) { + RTC_LOG(LS_ERROR) << "Parsing " << server << " failed."; + return -1; + } + server_addresses.push_back(addr); + } + + rtc::InitializeSSL(); + rtc::InitRandom(rtc::Time32()); + webrtc::test::ScopedKeyValueConfig field_trials; + rtc::PhysicalSocketServer socket_server; + rtc::AutoSocketServerThread thread(&socket_server); + auto socket_factory = + std::make_unique(&socket_server); + std::unique_ptr network_manager( + new rtc::BasicNetworkManager(&socket_server, &field_trials)); + std::vector networks = network_manager->GetNetworks(); + auto prober = std::make_unique(socket_factory.get(), + rtc::Thread::Current(), networks); + auto finish_callback = [&thread](StunProber* prober, int result) { + StopTrial(&thread, prober, result); + }; + prober->Start(server_addresses, absl::GetFlag(FLAGS_shared_socket), + absl::GetFlag(FLAGS_interval), + absl::GetFlag(FLAGS_pings_per_ip), absl::GetFlag(FLAGS_timeout), + AsyncCallback(finish_callback)); + thread.Run(); + return 0; +} diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn index 0c58b5e482..d0de88b60b 100644 --- a/p2p/BUILD.gn +++ b/p2p/BUILD.gn @@ -9,7 +9,10 @@ import("../webrtc.gni") group("p2p") { - deps = [ ":rtc_p2p" ] + deps = [ + ":libstunprober", + ":rtc_p2p", + ] } rtc_library("rtc_p2p") { @@ -1255,3 +1258,53 @@ rtc_library("p2p_server_utils") { "//third_party/abseil-cpp/absl/strings:string_view", ] } + +rtc_library("libstunprober") { + visibility = [ "*" ] + sources = [ + "stunprober/stun_prober.cc", + "stunprober/stun_prober.h", + ] + + deps = [ + "../api:array_view", + "../api:async_dns_resolver", + "../api:packet_socket_factory", + "../api:sequence_checker", + "../api/task_queue:pending_task_safety_flag", + "../api/transport:stun_types", + "../api/units:time_delta", + "../rtc_base:async_packet_socket", + "../rtc_base:byte_buffer", + "../rtc_base:checks", + "../rtc_base:ip_address", + "../rtc_base:logging", + "../rtc_base:network", + "../rtc_base:socket_address", + "../rtc_base:ssl", + "../rtc_base:threading", + "../rtc_base:timeutils", + "../rtc_base/network:received_packet", + "../rtc_base/system:rtc_export", + ] +} + +if (rtc_include_tests) { + rtc_library("libstunprober_unittests") { + testonly = true + + sources = [ "stunprober/stun_prober_unittest.cc" ] + deps = [ + ":basic_packet_socket_factory", + ":libstunprober", + ":p2p_test_utils", + "../rtc_base:checks", + "../rtc_base:gunit_helpers", + "../rtc_base:ip_address", + "../rtc_base:rtc_base_tests_utils", + "../rtc_base:ssl_adapter", + "../test:test_support", + "//testing/gtest", + ] + } +} diff --git a/p2p/stunprober/stun_prober.cc b/p2p/stunprober/stun_prober.cc new file mode 100644 index 0000000000..d130d780dc --- /dev/null +++ b/p2p/stunprober/stun_prober.cc @@ -0,0 +1,602 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "p2p/stunprober/stun_prober.h" + +#include +#include +#include +#include +#include +#include + +#include "api/array_view.h" +#include "api/packet_socket_factory.h" +#include "api/task_queue/pending_task_safety_flag.h" +#include "api/transport/stun.h" +#include "api/units/time_delta.h" +#include "rtc_base/async_packet_socket.h" +#include "rtc_base/checks.h" +#include "rtc_base/network/received_packet.h" +#include "rtc_base/thread.h" +#include "rtc_base/time_utils.h" + +namespace stunprober { + +namespace { +using ::webrtc::SafeTask; +using ::webrtc::TimeDelta; + +const int THREAD_WAKE_UP_INTERVAL_MS = 5; + +template +void IncrementCounterByAddress(std::map* counter_per_ip, const T& ip) { + counter_per_ip->insert(std::make_pair(ip, 0)).first->second++; +} + +} // namespace + +// A requester tracks the requests and responses from a single socket to many +// STUN servers +class StunProber::Requester : public sigslot::has_slots<> { + public: + // Each Request maps to a request and response. + struct Request { + // Actual time the STUN bind request was sent. + int64_t sent_time_ms = 0; + // Time the response was received. + int64_t received_time_ms = 0; + + // Server reflexive address from STUN response for this given request. + rtc::SocketAddress srflx_addr; + + rtc::IPAddress server_addr; + + int64_t rtt() { return received_time_ms - sent_time_ms; } + void ProcessResponse(rtc::ArrayView payload); + }; + + // StunProber provides `server_ips` for Requester to probe. For shared + // socket mode, it'll be all the resolved IP addresses. For non-shared mode, + // it'll just be a single address. + Requester(StunProber* prober, + rtc::AsyncPacketSocket* socket, + const std::vector& server_ips); + ~Requester() override; + + Requester(const Requester&) = delete; + Requester& operator=(const Requester&) = delete; + + // There is no callback for SendStunRequest as the underneath socket send is + // expected to be completed immediately. Otherwise, it'll skip this request + // and move to the next one. + void SendStunRequest(); + + void OnStunResponseReceived(rtc::AsyncPacketSocket* socket, + const rtc::ReceivedPacket& packet); + + const std::vector& requests() { return requests_; } + + // Whether this Requester has completed all requests. + bool Done() { + return static_cast(num_request_sent_) == server_ips_.size(); + } + + private: + Request* GetRequestByAddress(const rtc::IPAddress& ip); + + StunProber* prober_; + + // The socket for this session. + std::unique_ptr socket_; + + // Temporary SocketAddress and buffer for RecvFrom. + rtc::SocketAddress addr_; + std::unique_ptr response_packet_; + + std::vector requests_; + std::vector server_ips_; + int16_t num_request_sent_ = 0; + int16_t num_response_received_ = 0; + + webrtc::SequenceChecker& thread_checker_; +}; + +StunProber::Requester::Requester( + StunProber* prober, + rtc::AsyncPacketSocket* socket, + const std::vector& server_ips) + : prober_(prober), + socket_(socket), + response_packet_(new rtc::ByteBufferWriter(nullptr, kMaxUdpBufferSize)), + server_ips_(server_ips), + thread_checker_(prober->thread_checker_) { + socket_->RegisterReceivedPacketCallback( + [&](rtc::AsyncPacketSocket* socket, const rtc::ReceivedPacket& packet) { + OnStunResponseReceived(socket, packet); + }); +} + +StunProber::Requester::~Requester() { + if (socket_) { + socket_->Close(); + } + for (auto* req : requests_) { + if (req) { + delete req; + } + } +} + +void StunProber::Requester::SendStunRequest() { + RTC_DCHECK(thread_checker_.IsCurrent()); + requests_.push_back(new Request()); + Request& request = *(requests_.back()); + // Random transaction ID, STUN_BINDING_REQUEST + cricket::StunMessage message(cricket::STUN_BINDING_REQUEST); + + std::unique_ptr request_packet( + new rtc::ByteBufferWriter(nullptr, kMaxUdpBufferSize)); + if (!message.Write(request_packet.get())) { + prober_->ReportOnFinished(WRITE_FAILED); + return; + } + + auto addr = server_ips_[num_request_sent_]; + request.server_addr = addr.ipaddr(); + + // The write must succeed immediately. Otherwise, the calculating of the STUN + // request timing could become too complicated. Callback is ignored by passing + // empty AsyncCallback. + rtc::PacketOptions options; + int rv = socket_->SendTo(request_packet->Data(), request_packet->Length(), + addr, options); + if (rv < 0) { + prober_->ReportOnFinished(WRITE_FAILED); + return; + } + + request.sent_time_ms = rtc::TimeMillis(); + + num_request_sent_++; + RTC_DCHECK(static_cast(num_request_sent_) <= server_ips_.size()); +} + +void StunProber::Requester::Request::ProcessResponse( + rtc::ArrayView payload) { + int64_t now = rtc::TimeMillis(); + rtc::ByteBufferReader message(payload); + cricket::StunMessage stun_response; + if (!stun_response.Read(&message)) { + // Invalid or incomplete STUN packet. + received_time_ms = 0; + return; + } + + // Get external address of the socket. + const cricket::StunAddressAttribute* addr_attr = + stun_response.GetAddress(cricket::STUN_ATTR_MAPPED_ADDRESS); + if (addr_attr == nullptr) { + // Addresses not available to detect whether or not behind a NAT. + return; + } + + if (addr_attr->family() != cricket::STUN_ADDRESS_IPV4 && + addr_attr->family() != cricket::STUN_ADDRESS_IPV6) { + return; + } + + received_time_ms = now; + + srflx_addr = addr_attr->GetAddress(); +} + +void StunProber::Requester::OnStunResponseReceived( + rtc::AsyncPacketSocket* socket, + const rtc::ReceivedPacket& packet) { + RTC_DCHECK(thread_checker_.IsCurrent()); + RTC_DCHECK(socket_); + Request* request = GetRequestByAddress(packet.source_address().ipaddr()); + if (!request) { + // Something is wrong, finish the test. + prober_->ReportOnFinished(GENERIC_FAILURE); + return; + } + + num_response_received_++; + request->ProcessResponse(packet.payload()); +} + +StunProber::Requester::Request* StunProber::Requester::GetRequestByAddress( + const rtc::IPAddress& ipaddr) { + RTC_DCHECK(thread_checker_.IsCurrent()); + for (auto* request : requests_) { + if (request->server_addr == ipaddr) { + return request; + } + } + + return nullptr; +} + +StunProber::Stats::Stats() = default; + +StunProber::Stats::~Stats() = default; + +StunProber::ObserverAdapter::ObserverAdapter() = default; + +StunProber::ObserverAdapter::~ObserverAdapter() = default; + +void StunProber::ObserverAdapter::OnPrepared(StunProber* stunprober, + Status status) { + if (status == SUCCESS) { + stunprober->Start(this); + } else { + callback_(stunprober, status); + } +} + +void StunProber::ObserverAdapter::OnFinished(StunProber* stunprober, + Status status) { + callback_(stunprober, status); +} + +StunProber::StunProber(rtc::PacketSocketFactory* socket_factory, + rtc::Thread* thread, + std::vector networks) + : interval_ms_(0), + socket_factory_(socket_factory), + thread_(thread), + networks_(std::move(networks)) {} + +StunProber::~StunProber() { + RTC_DCHECK(thread_checker_.IsCurrent()); + for (auto* req : requesters_) { + if (req) { + delete req; + } + } + for (auto* s : sockets_) { + if (s) { + delete s; + } + } +} + +bool StunProber::Start(const std::vector& servers, + bool shared_socket_mode, + int interval_ms, + int num_request_per_ip, + int timeout_ms, + const AsyncCallback callback) { + observer_adapter_.set_callback(callback); + return Prepare(servers, shared_socket_mode, interval_ms, num_request_per_ip, + timeout_ms, &observer_adapter_); +} + +bool StunProber::Prepare(const std::vector& servers, + bool shared_socket_mode, + int interval_ms, + int num_request_per_ip, + int timeout_ms, + StunProber::Observer* observer) { + RTC_DCHECK(thread_checker_.IsCurrent()); + interval_ms_ = interval_ms; + shared_socket_mode_ = shared_socket_mode; + + requests_per_ip_ = num_request_per_ip; + if (requests_per_ip_ == 0 || servers.size() == 0) { + return false; + } + + timeout_ms_ = timeout_ms; + servers_ = servers; + observer_ = observer; + // Remove addresses that are already resolved. + for (auto it = servers_.begin(); it != servers_.end();) { + if (it->ipaddr().family() != AF_UNSPEC) { + all_servers_addrs_.push_back(*it); + it = servers_.erase(it); + } else { + ++it; + } + } + if (servers_.empty()) { + CreateSockets(); + return true; + } + return ResolveServerName(servers_.back()); +} + +bool StunProber::Start(StunProber::Observer* observer) { + observer_ = observer; + if (total_ready_sockets_ != total_socket_required()) { + return false; + } + MaybeScheduleStunRequests(); + return true; +} + +bool StunProber::ResolveServerName(const rtc::SocketAddress& addr) { + RTC_DCHECK(!resolver_); + resolver_ = socket_factory_->CreateAsyncDnsResolver(); + if (!resolver_) { + return false; + } + resolver_->Start(addr, [this] { OnServerResolved(resolver_->result()); }); + return true; +} + +void StunProber::OnSocketReady(rtc::AsyncPacketSocket* socket, + const rtc::SocketAddress& addr) { + total_ready_sockets_++; + if (total_ready_sockets_ == total_socket_required()) { + ReportOnPrepared(SUCCESS); + } +} + +void StunProber::OnServerResolved( + const webrtc::AsyncDnsResolverResult& result) { + RTC_DCHECK(thread_checker_.IsCurrent()); + rtc::SocketAddress received_address; + if (result.GetResolvedAddress(AF_INET, &received_address)) { + // Construct an address without the name in it. + rtc::SocketAddress addr(received_address.ipaddr(), received_address.port()); + all_servers_addrs_.push_back(addr); + } + resolver_.reset(); + servers_.pop_back(); + if (servers_.size()) { + if (!ResolveServerName(servers_.back())) { + ReportOnPrepared(RESOLVE_FAILED); + } + return; + } + + if (all_servers_addrs_.size() == 0) { + ReportOnPrepared(RESOLVE_FAILED); + return; + } + + CreateSockets(); +} + +void StunProber::CreateSockets() { + // Dedupe. + std::set addrs(all_servers_addrs_.begin(), + all_servers_addrs_.end()); + all_servers_addrs_.assign(addrs.begin(), addrs.end()); + + // Prepare all the sockets beforehand. All of them will bind to "any" address. + while (sockets_.size() < total_socket_required()) { + std::unique_ptr socket( + socket_factory_->CreateUdpSocket(rtc::SocketAddress(INADDR_ANY, 0), 0, + 0)); + if (!socket) { + ReportOnPrepared(GENERIC_FAILURE); + return; + } + // Chrome and WebRTC behave differently in terms of the state of a socket + // once returned from PacketSocketFactory::CreateUdpSocket. + if (socket->GetState() == rtc::AsyncPacketSocket::STATE_BINDING) { + socket->SignalAddressReady.connect(this, &StunProber::OnSocketReady); + } else { + OnSocketReady(socket.get(), rtc::SocketAddress(INADDR_ANY, 0)); + } + sockets_.push_back(socket.release()); + } +} + +StunProber::Requester* StunProber::CreateRequester() { + RTC_DCHECK(thread_checker_.IsCurrent()); + if (!sockets_.size()) { + return nullptr; + } + StunProber::Requester* requester; + if (shared_socket_mode_) { + requester = new Requester(this, sockets_.back(), all_servers_addrs_); + } else { + std::vector server_ip; + server_ip.push_back( + all_servers_addrs_[(num_request_sent_ % all_servers_addrs_.size())]); + requester = new Requester(this, sockets_.back(), server_ip); + } + + sockets_.pop_back(); + return requester; +} + +bool StunProber::SendNextRequest() { + if (!current_requester_ || current_requester_->Done()) { + current_requester_ = CreateRequester(); + requesters_.push_back(current_requester_); + } + if (!current_requester_) { + return false; + } + current_requester_->SendStunRequest(); + num_request_sent_++; + return true; +} + +bool StunProber::should_send_next_request(int64_t now) { + if (interval_ms_ < THREAD_WAKE_UP_INTERVAL_MS) { + return now >= next_request_time_ms_; + } else { + return (now + (THREAD_WAKE_UP_INTERVAL_MS / 2)) >= next_request_time_ms_; + } +} + +int StunProber::get_wake_up_interval_ms() { + if (interval_ms_ < THREAD_WAKE_UP_INTERVAL_MS) { + return 1; + } else { + return THREAD_WAKE_UP_INTERVAL_MS; + } +} + +void StunProber::MaybeScheduleStunRequests() { + RTC_DCHECK_RUN_ON(thread_); + int64_t now = rtc::TimeMillis(); + + if (Done()) { + thread_->PostDelayedTask( + SafeTask(task_safety_.flag(), [this] { ReportOnFinished(SUCCESS); }), + TimeDelta::Millis(timeout_ms_)); + return; + } + if (should_send_next_request(now)) { + if (!SendNextRequest()) { + ReportOnFinished(GENERIC_FAILURE); + return; + } + next_request_time_ms_ = now + interval_ms_; + } + thread_->PostDelayedTask( + SafeTask(task_safety_.flag(), [this] { MaybeScheduleStunRequests(); }), + TimeDelta::Millis(get_wake_up_interval_ms())); +} + +bool StunProber::GetStats(StunProber::Stats* prob_stats) const { + // No need to be on the same thread. + if (!prob_stats) { + return false; + } + + StunProber::Stats stats; + + int rtt_sum = 0; + int64_t first_sent_time = 0; + int64_t last_sent_time = 0; + NatType nat_type = NATTYPE_INVALID; + + // Track of how many srflx IP that we have seen. + std::set srflx_ips; + + // If we're not receiving any response on a given IP, all requests sent to + // that IP should be ignored as this could just be an DNS error. + std::map num_response_per_server; + std::map num_request_per_server; + + for (auto* requester : requesters_) { + std::map num_response_per_srflx_addr; + for (auto* request : requester->requests()) { + if (request->sent_time_ms <= 0) { + continue; + } + + ++stats.raw_num_request_sent; + IncrementCounterByAddress(&num_request_per_server, request->server_addr); + + if (!first_sent_time) { + first_sent_time = request->sent_time_ms; + } + last_sent_time = request->sent_time_ms; + + if (request->received_time_ms < request->sent_time_ms) { + continue; + } + + IncrementCounterByAddress(&num_response_per_server, request->server_addr); + IncrementCounterByAddress(&num_response_per_srflx_addr, + request->srflx_addr); + rtt_sum += request->rtt(); + stats.srflx_addrs.insert(request->srflx_addr.ToString()); + srflx_ips.insert(request->srflx_addr.ipaddr()); + } + + // If we're using shared mode and seeing >1 srflx addresses for a single + // requester, it's symmetric NAT. + if (shared_socket_mode_ && num_response_per_srflx_addr.size() > 1) { + nat_type = NATTYPE_SYMMETRIC; + } + } + + // We're probably not behind a regular NAT. We have more than 1 distinct + // server reflexive IPs. + if (srflx_ips.size() > 1) { + return false; + } + + int num_sent = 0; + int num_received = 0; + int num_server_ip_with_response = 0; + + for (const auto& kv : num_response_per_server) { + RTC_DCHECK_GT(kv.second, 0); + num_server_ip_with_response++; + num_received += kv.second; + num_sent += num_request_per_server[kv.first]; + } + + // Shared mode is only true if we use the shared socket and there are more + // than 1 responding servers. + stats.shared_socket_mode = + shared_socket_mode_ && (num_server_ip_with_response > 1); + + if (stats.shared_socket_mode && nat_type == NATTYPE_INVALID) { + nat_type = NATTYPE_NON_SYMMETRIC; + } + + // If we could find a local IP matching srflx, we're not behind a NAT. + rtc::SocketAddress srflx_addr; + if (stats.srflx_addrs.size() && + !srflx_addr.FromString(*(stats.srflx_addrs.begin()))) { + return false; + } + for (const auto* net : networks_) { + if (srflx_addr.ipaddr() == net->GetBestIP()) { + nat_type = stunprober::NATTYPE_NONE; + stats.host_ip = net->GetBestIP().ToString(); + break; + } + } + + // Finally, we know we're behind a NAT but can't determine which type it is. + if (nat_type == NATTYPE_INVALID) { + nat_type = NATTYPE_UNKNOWN; + } + + stats.nat_type = nat_type; + stats.num_request_sent = num_sent; + stats.num_response_received = num_received; + stats.target_request_interval_ns = interval_ms_ * 1000; + + if (num_sent) { + stats.success_percent = static_cast(100 * num_received / num_sent); + } + + if (stats.raw_num_request_sent > 1) { + stats.actual_request_interval_ns = + (1000 * (last_sent_time - first_sent_time)) / + (stats.raw_num_request_sent - 1); + } + + if (num_received) { + stats.average_rtt_ms = static_cast((rtt_sum / num_received)); + } + + *prob_stats = stats; + return true; +} + +void StunProber::ReportOnPrepared(StunProber::Status status) { + if (observer_) { + observer_->OnPrepared(this, status); + } +} + +void StunProber::ReportOnFinished(StunProber::Status status) { + if (observer_) { + observer_->OnFinished(this, status); + } +} + +} // namespace stunprober diff --git a/p2p/stunprober/stun_prober.h b/p2p/stunprober/stun_prober.h new file mode 100644 index 0000000000..07f3a17233 --- /dev/null +++ b/p2p/stunprober/stun_prober.h @@ -0,0 +1,251 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef P2P_STUNPROBER_STUN_PROBER_H_ +#define P2P_STUNPROBER_STUN_PROBER_H_ + +#include +#include +#include +#include + +#include "api/async_dns_resolver.h" +#include "api/sequence_checker.h" +#include "api/task_queue/pending_task_safety_flag.h" +#include "rtc_base/network.h" +#include "rtc_base/socket_address.h" +#include "rtc_base/system/rtc_export.h" +#include "rtc_base/thread.h" + +namespace rtc { +class AsyncPacketSocket; +class PacketSocketFactory; +class Thread; +class NetworkManager; +class AsyncResolverInterface; +} // namespace rtc + +namespace stunprober { + +class StunProber; + +static const int kMaxUdpBufferSize = 1200; + +typedef std::function AsyncCallback; + +enum NatType { + NATTYPE_INVALID, + NATTYPE_NONE, // Not behind a NAT. + NATTYPE_UNKNOWN, // Behind a NAT but type can't be determine. + NATTYPE_SYMMETRIC, // Behind a symmetric NAT. + NATTYPE_NON_SYMMETRIC // Behind a non-symmetric NAT. +}; + +class RTC_EXPORT StunProber : public sigslot::has_slots<> { + public: + enum Status { // Used in UMA_HISTOGRAM_ENUMERATION. + SUCCESS, // Successfully received bytes from the server. + GENERIC_FAILURE, // Generic failure. + RESOLVE_FAILED, // Host resolution failed. + WRITE_FAILED, // Sending a message to the server failed. + READ_FAILED, // Reading the reply from the server failed. + }; + + class Observer { + public: + virtual ~Observer() = default; + virtual void OnPrepared(StunProber* prober, StunProber::Status status) = 0; + virtual void OnFinished(StunProber* prober, StunProber::Status status) = 0; + }; + + struct RTC_EXPORT Stats { + Stats(); + ~Stats(); + + // `raw_num_request_sent` is the total number of requests + // sent. `num_request_sent` is the count of requests against a server where + // we see at least one response. `num_request_sent` is designed to protect + // against DNS resolution failure or the STUN server is not responsive + // which could skew the result. + int raw_num_request_sent = 0; + int num_request_sent = 0; + + int num_response_received = 0; + NatType nat_type = NATTYPE_INVALID; + int average_rtt_ms = -1; + int success_percent = 0; + int target_request_interval_ns = 0; + int actual_request_interval_ns = 0; + + // Also report whether this trial can't be considered truly as shared + // mode. Share mode only makes sense when we have multiple IP resolved and + // successfully probed. + bool shared_socket_mode = false; + + std::string host_ip; + + // If the srflx_addrs has more than 1 element, the NAT is symmetric. + std::set srflx_addrs; + }; + + StunProber(rtc::PacketSocketFactory* socket_factory, + rtc::Thread* thread, + std::vector networks); + ~StunProber() override; + + StunProber(const StunProber&) = delete; + StunProber& operator=(const StunProber&) = delete; + + // Begin performing the probe test against the `servers`. If + // `shared_socket_mode` is false, each request will be done with a new socket. + // Otherwise, a unique socket will be used for a single round of requests + // against all resolved IPs. No single socket will be used against a given IP + // more than once. The interval of requests will be as close to the requested + // inter-probe interval `stun_ta_interval_ms` as possible. After sending out + // the last scheduled request, the probe will wait `timeout_ms` for request + // responses and then call `finish_callback`. `requests_per_ip` indicates how + // many requests should be tried for each resolved IP address. In shared mode, + // (the number of sockets to be created) equals to `requests_per_ip`. In + // non-shared mode, (the number of sockets) equals to requests_per_ip * (the + // number of resolved IP addresses). TODO(guoweis): Remove this once + // everything moved to Prepare() and Run(). + bool Start(const std::vector& servers, + bool shared_socket_mode, + int stun_ta_interval_ms, + int requests_per_ip, + int timeout_ms, + AsyncCallback finish_callback); + + // TODO(guoweis): The combination of Prepare() and Run() are equivalent to the + // Start() above. Remove Start() once everything is migrated. + bool Prepare(const std::vector& servers, + bool shared_socket_mode, + int stun_ta_interval_ms, + int requests_per_ip, + int timeout_ms, + StunProber::Observer* observer); + + // Start to send out the STUN probes. + bool Start(StunProber::Observer* observer); + + // Method to retrieve the Stats once `finish_callback` is invoked. Returning + // false when the result is inconclusive, for example, whether it's behind a + // NAT or not. + bool GetStats(Stats* stats) const; + + int estimated_execution_time() { + return static_cast(requests_per_ip_ * all_servers_addrs_.size() * + interval_ms_); + } + + private: + // A requester tracks the requests and responses from a single socket to many + // STUN servers. + class Requester; + + // TODO(guoweis): Remove this once all dependencies move away from + // AsyncCallback. + class ObserverAdapter : public Observer { + public: + ObserverAdapter(); + ~ObserverAdapter() override; + + void set_callback(AsyncCallback callback) { callback_ = callback; } + void OnPrepared(StunProber* stunprober, Status status) override; + void OnFinished(StunProber* stunprober, Status status) override; + + private: + AsyncCallback callback_; + }; + + bool ResolveServerName(const rtc::SocketAddress& addr); + void OnServerResolved(const webrtc::AsyncDnsResolverResult& resolver); + + void OnSocketReady(rtc::AsyncPacketSocket* socket, + const rtc::SocketAddress& addr); + + void CreateSockets(); + + bool Done() { + return num_request_sent_ >= requests_per_ip_ * all_servers_addrs_.size(); + } + + size_t total_socket_required() { + return (shared_socket_mode_ ? 1 : all_servers_addrs_.size()) * + requests_per_ip_; + } + + bool should_send_next_request(int64_t now); + int get_wake_up_interval_ms(); + + bool SendNextRequest(); + + // Will be invoked in 1ms intervals and schedule the next request from the + // `current_requester_` if the time has passed for another request. + void MaybeScheduleStunRequests(); + + void ReportOnPrepared(StunProber::Status status); + void ReportOnFinished(StunProber::Status status); + + Requester* CreateRequester(); + + Requester* current_requester_ = nullptr; + + // The time when the next request should go out. + int64_t next_request_time_ms_ = 0; + + // Total requests sent so far. + uint32_t num_request_sent_ = 0; + + bool shared_socket_mode_ = false; + + // How many requests should be done against each resolved IP. + uint32_t requests_per_ip_ = 0; + + // Milliseconds to pause between each STUN request. + int interval_ms_; + + // Timeout period after the last request is sent. + int timeout_ms_; + + // STUN server name to be resolved. + std::vector servers_; + + // Weak references. + rtc::PacketSocketFactory* socket_factory_; + rtc::Thread* thread_; + + // Accumulate all resolved addresses. + std::vector all_servers_addrs_; + + // The set of STUN probe sockets and their state. + std::vector requesters_; + + webrtc::SequenceChecker thread_checker_; + + // Temporary storage for created sockets. + std::vector sockets_; + // This tracks how many of the sockets are ready. + size_t total_ready_sockets_ = 0; + + Observer* observer_ = nullptr; + // TODO(guoweis): Remove this once all dependencies move away from + // AsyncCallback. + ObserverAdapter observer_adapter_; + + const std::vector networks_; + std::unique_ptr resolver_; + + webrtc::ScopedTaskSafety task_safety_; +}; + +} // namespace stunprober + +#endif // P2P_STUNPROBER_STUN_PROBER_H_ diff --git a/p2p/stunprober/stun_prober_unittest.cc b/p2p/stunprober/stun_prober_unittest.cc new file mode 100644 index 0000000000..1aa2be2844 --- /dev/null +++ b/p2p/stunprober/stun_prober_unittest.cc @@ -0,0 +1,177 @@ +/* + * Copyright 2015 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "p2p/stunprober/stun_prober.h" + +#include + +#include +#include + +#include "p2p/base/basic_packet_socket_factory.h" +#include "p2p/base/test_stun_server.h" +#include "rtc_base/gunit.h" +#include "rtc_base/ip_address.h" +#include "rtc_base/ssl_adapter.h" +#include "rtc_base/virtual_socket_server.h" +#include "test/gtest.h" + +using stunprober::AsyncCallback; +using stunprober::StunProber; + +namespace stunprober { + +namespace { + +const rtc::SocketAddress kLocalAddr("192.168.0.1", 0); +const rtc::SocketAddress kStunAddr1("1.1.1.1", 3478); +const rtc::SocketAddress kStunAddr2("1.1.1.2", 3478); +const rtc::SocketAddress kFailedStunAddr("1.1.1.3", 3478); +const rtc::SocketAddress kStunMappedAddr("77.77.77.77", 0); + +} // namespace + +class StunProberTest : public ::testing::Test { + public: + StunProberTest() + : ss_(std::make_unique()), + main_(ss_.get()), + result_(StunProber::SUCCESS), + stun_server_1_( + cricket::TestStunServer::Create(ss_.get(), kStunAddr1, main_)), + stun_server_2_( + cricket::TestStunServer::Create(ss_.get(), kStunAddr2, main_)) { + stun_server_1_->set_fake_stun_addr(kStunMappedAddr); + stun_server_2_->set_fake_stun_addr(kStunMappedAddr); + rtc::InitializeSSL(); + } + + static constexpr int pings_per_ip = 3; + + void set_expected_result(int result) { result_ = result; } + + void CreateProber(rtc::PacketSocketFactory* socket_factory, + std::vector networks) { + prober_ = std::make_unique(socket_factory, &main_, + std::move(networks)); + } + + void StartProbing(rtc::PacketSocketFactory* socket_factory, + const std::vector& addrs, + std::vector networks, + bool shared_socket, + uint16_t interval, + uint16_t pings_per_ip) { + CreateProber(socket_factory, networks); + prober_->Start(addrs, shared_socket, interval, pings_per_ip, + 100 /* timeout_ms */, + [this](StunProber* prober, int result) { + StopCallback(prober, result); + }); + } + + void RunProber(bool shared_mode) { + std::vector addrs; + addrs.push_back(kStunAddr1); + addrs.push_back(kStunAddr2); + // Add a non-existing server. This shouldn't pollute the result. + addrs.push_back(kFailedStunAddr); + RunProber(shared_mode, addrs, /* check_results= */ true); + } + + void RunProber(bool shared_mode, + const std::vector& addrs, + bool check_results) { + rtc::Network ipv4_network1("test_eth0", "Test Network Adapter 1", + rtc::IPAddress(0x12345600U), 24); + ipv4_network1.AddIP(rtc::IPAddress(0x12345678)); + std::vector networks; + networks.push_back(&ipv4_network1); + + auto socket_factory = + std::make_unique(ss_.get()); + + // Set up the expected results for verification. + std::set srflx_addresses; + srflx_addresses.insert(kStunMappedAddr.ToString()); + const uint32_t total_pings_tried = + static_cast(pings_per_ip * addrs.size()); + + // The reported total_pings should not count for pings sent to the + // kFailedStunAddr. + const uint32_t total_pings_reported = total_pings_tried - pings_per_ip; + + StartProbing(socket_factory.get(), addrs, std::move(networks), shared_mode, + 3, pings_per_ip); + + WAIT(stopped_, 1000); + + EXPECT_TRUE(prober_->GetStats(&stats_)); + if (check_results) { + EXPECT_EQ(stats_.success_percent, 100); + EXPECT_TRUE(stats_.nat_type > stunprober::NATTYPE_NONE); + EXPECT_EQ(stats_.srflx_addrs, srflx_addresses); + EXPECT_EQ(static_cast(stats_.num_request_sent), + total_pings_reported); + EXPECT_EQ(static_cast(stats_.num_response_received), + total_pings_reported); + } + } + + StunProber* prober() { return prober_.get(); } + StunProber::Stats& stats() { return stats_; } + + private: + void StopCallback(StunProber* prober, int result) { + EXPECT_EQ(result, result_); + stopped_ = true; + } + + std::unique_ptr ss_; + rtc::AutoSocketServerThread main_; + std::unique_ptr prober_; + int result_ = 0; + bool stopped_ = false; + cricket::TestStunServer::StunServerPtr stun_server_1_; + cricket::TestStunServer::StunServerPtr stun_server_2_; + StunProber::Stats stats_; +}; + +TEST_F(StunProberTest, NonSharedMode) { + RunProber(false); +} + +TEST_F(StunProberTest, SharedMode) { + RunProber(true); +} + +TEST_F(StunProberTest, ResolveNonexistentHostname) { + std::vector addrs; + addrs.push_back(kStunAddr1); + // Add a non-existing server by name. This should cause a failed lookup. + addrs.push_back(rtc::SocketAddress("nonexistent.test", 3478)); + RunProber(false, addrs, false); + // One server is pinged + EXPECT_EQ(stats().raw_num_request_sent, pings_per_ip); +} + +TEST_F(StunProberTest, ResolveExistingHostname) { + std::vector addrs; + addrs.push_back(kStunAddr1); + // Add a non-existing server by name. This should cause a failed lookup. + addrs.push_back(rtc::SocketAddress("localhost", 3478)); + RunProber(false, addrs, false); + // Two servers are pinged, only one responds. + // TODO(bugs.webrtc.org/15559): Figure out why this doesn't always work + // EXPECT_EQ(stats().raw_num_request_sent, pings_per_ip * 2); + EXPECT_EQ(stats().num_request_sent, pings_per_ip); +} + +} // namespace stunprober