diff options
author | Jonas Oreland <jonaso@webrtc.org> | 2019-12-11 11:35:48 +0100 |
---|---|---|
committer | Commit Bot <commit-bot@chromium.org> | 2019-12-11 14:23:30 +0000 |
commit | 9a52bd733ca963fb8a9883e5cc3d3974ecccffb8 (patch) | |
tree | 51acb1833c34ffded91c5f8872871fecfda19450 /p2p/base | |
parent | c907d4f223ebb49780bdee23840a618604f317c4 (diff) | |
download | webrtc-9a52bd733ca963fb8a9883e5cc3d3974ecccffb8.tar.gz |
STUN PING request
This patch introduces a new type of STUN ping,
GOOG_PING_REQUEST/RESPONSE which is similar
to a STUN_BINDING but does not transmit any values.
The Connection class automatically sends these if
no STUN attributes has changed since last call to Connection::Ping()
if the remote peer has signaled that it supports it.
BUG=webrtc:11100
Change-Id: Ib1b590f0b90ca6cb56f2eb07cd62f976e246bc8c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/159961
Commit-Queue: Jonas Oreland <jonaso@webrtc.org>
Reviewed-by: Taylor <deadbeef@webrtc.org>
Reviewed-by: Björn Terelius <terelius@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#30062}
Diffstat (limited to 'p2p/base')
-rw-r--r-- | p2p/base/connection.cc | 156 | ||||
-rw-r--r-- | p2p/base/connection.h | 22 | ||||
-rw-r--r-- | p2p/base/p2p_transport_channel.cc | 11 | ||||
-rw-r--r-- | p2p/base/p2p_transport_channel_ice_field_trials.h | 6 | ||||
-rw-r--r-- | p2p/base/p2p_transport_channel_unittest.cc | 34 | ||||
-rw-r--r-- | p2p/base/port.cc | 108 | ||||
-rw-r--r-- | p2p/base/port_unittest.cc | 305 |
7 files changed, 562 insertions, 80 deletions
diff --git a/p2p/base/connection.cc b/p2p/base/connection.cc index fbbd85344b..e11b4bcba2 100644 --- a/p2p/base/connection.cc +++ b/p2p/base/connection.cc @@ -143,11 +143,15 @@ constexpr int64_t kMinExtraPingDelayMs = 100; // Default field trials. const cricket::IceFieldTrials kDefaultFieldTrials; +constexpr int kSupportGoogPingVersionIndex = + static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex:: + SUPPORT_GOOG_PING_VERSION); + } // namespace namespace cricket { -// A ConnectionRequest is a simple STUN ping used to determine writability. +// A ConnectionRequest is a STUN binding used to determine writability. ConnectionRequest::ConnectionRequest(Connection* connection) : StunRequest(new IceMessage()), connection_(connection) {} @@ -220,10 +224,14 @@ void ConnectionRequest::Prepare(StunMessage* request) { request->AddAttribute(std::make_unique<StunUInt32Attribute>( STUN_ATTR_PRIORITY, prflx_priority)); - // Adding Message Integrity attribute. - request->AddMessageIntegrity(connection_->remote_candidate().password()); - // Adding Fingerprint. - request->AddFingerprint(); + if (connection_->ShouldSendGoogPing(request)) { + request->SetType(GOOG_PING_REQUEST); + request->ClearAttributes(); + request->AddMessageIntegrity32(connection_->remote_candidate().password()); + } else { + request->AddMessageIntegrity(connection_->remote_candidate().password()); + request->AddFingerprint(); + } } void ConnectionRequest::OnResponse(StunMessage* response) { @@ -451,11 +459,11 @@ void Connection::OnReadPacket(const char* data, rtc::LoggingSeverity sev = (!writable() ? rtc::LS_INFO : rtc::LS_VERBOSE); switch (msg->type()) { case STUN_BINDING_REQUEST: - RTC_LOG_V(sev) << ToString() << ": Received STUN ping, id=" - << rtc::hex_encode(msg->transaction_id()); - + RTC_LOG_V(sev) << ToString() << ": Received " + << StunMethodToString(msg->type()) + << ", id=" << rtc::hex_encode(msg->transaction_id()); if (remote_ufrag == remote_candidate_.username()) { - HandleBindingRequest(msg.get()); + HandleStunBindingOrGoogPingRequest(msg.get()); } else { // The packet had the right local username, but the remote username // was not the right one for the remote address. @@ -487,7 +495,16 @@ void Connection::OnReadPacket(const char* data, case STUN_BINDING_INDICATION: ReceivedPing(msg->transaction_id()); break; - + case GOOG_PING_REQUEST: + HandleStunBindingOrGoogPingRequest(msg.get()); + break; + case GOOG_PING_RESPONSE: + case GOOG_PING_ERROR_RESPONSE: + if (msg->ValidateMessageIntegrity32(data, size, + remote_candidate().password())) { + requests_.CheckResponse(msg.get()); + } + break; default: RTC_NOTREACHED(); break; @@ -495,7 +512,7 @@ void Connection::OnReadPacket(const char* data, } } -void Connection::HandleBindingRequest(IceMessage* msg) { +void Connection::HandleStunBindingOrGoogPingRequest(IceMessage* msg) { // This connection should now be receiving. ReceivedPing(msg->transaction_id()); if (webrtc::field_trial::IsEnabled("WebRTC-ExtraICEPing") && @@ -523,12 +540,14 @@ void Connection::HandleBindingRequest(IceMessage* msg) { } const rtc::SocketAddress& remote_addr = remote_candidate_.address(); - const std::string& remote_ufrag = remote_candidate_.username(); - // Check for role conflicts. - if (!port_->MaybeIceRoleConflict(remote_addr, msg, remote_ufrag)) { - // Received conflicting role from the peer. - RTC_LOG(LS_INFO) << "Received conflicting role from the peer."; - return; + if (msg->type() == STUN_BINDING_REQUEST) { + // Check for role conflicts. + const std::string& remote_ufrag = remote_candidate_.username(); + if (!port_->MaybeIceRoleConflict(remote_addr, msg, remote_ufrag)) { + // Received conflicting role from the peer. + RTC_LOG(LS_INFO) << "Received conflicting role from the peer."; + return; + } } stats_.recv_ping_requests++; @@ -536,7 +555,12 @@ void Connection::HandleBindingRequest(IceMessage* msg) { msg->reduced_transaction_id()); // This is a validated stun request from remote peer. - SendBindingResponse(msg); + if (msg->type() == STUN_BINDING_REQUEST) { + SendStunBindingResponse(msg); + } else { + RTC_DCHECK(msg->type() == GOOG_PING_REQUEST); + SendGoogPingResponse(msg); + } // If it timed out on writing check, start up again if (!pruned_ && write_state_ == STATE_WRITE_TIMEOUT) { @@ -587,12 +611,9 @@ void Connection::HandleBindingRequest(IceMessage* msg) { } } -void Connection::SendBindingResponse(const StunMessage* request) { +void Connection::SendStunBindingResponse(const StunMessage* request) { RTC_DCHECK(request->type() == STUN_BINDING_REQUEST); - // Where I send the response. - const rtc::SocketAddress& addr = remote_candidate_.address(); - // Retrieve the username from the request. const StunByteStringAttribute* username_attr = request->GetByteString(STUN_ATTR_USERNAME); @@ -623,10 +644,36 @@ void Connection::SendBindingResponse(const StunMessage* request) { } response.AddAttribute(std::make_unique<StunXorAddressAttribute>( - STUN_ATTR_XOR_MAPPED_ADDRESS, addr)); + STUN_ATTR_XOR_MAPPED_ADDRESS, remote_candidate_.address())); + + if (field_trials_->announce_goog_ping) { + auto list = + StunAttribute::CreateUInt16ListAttribute(STUN_ATTR_GOOG_MISC_INFO); + list->AddTypeAtIndex(kSupportGoogPingVersionIndex, kGoogPingVersion); + response.AddAttribute(std::move(list)); + } + response.AddMessageIntegrity(local_candidate().password()); response.AddFingerprint(); + SendResponseMessage(response); +} + +void Connection::SendGoogPingResponse(const StunMessage* request) { + RTC_DCHECK(request->type() == GOOG_PING_REQUEST); + + // Fill in the response message. + StunMessage response; + response.SetType(GOOG_PING_RESPONSE); + response.SetTransactionID(request->transaction_id()); + response.AddMessageIntegrity32(local_candidate().password()); + SendResponseMessage(response); +} + +void Connection::SendResponseMessage(const StunMessage& response) { + // Where I send the response. + const rtc::SocketAddress& addr = remote_candidate_.address(); + // Send the response message. rtc::ByteBufferWriter buf; response.Write(&buf); @@ -635,21 +682,22 @@ void Connection::SendBindingResponse(const StunMessage* request) { rtc::PacketType::kIceConnectivityCheckResponse; auto err = port_->SendTo(buf.Data(), buf.Length(), addr, options, false); if (err < 0) { - RTC_LOG(LS_ERROR) << ToString() - << ": Failed to send STUN ping response, to=" - << addr.ToSensitiveString() << ", err=" << err + RTC_LOG(LS_ERROR) << ToString() << ": Failed to send " + << StunMethodToString(response.type()) + << ", to=" << addr.ToSensitiveString() << ", err=" << err << ", id=" << rtc::hex_encode(response.transaction_id()); } else { // Log at LS_INFO if we send a stun ping response on an unwritable // connection. rtc::LoggingSeverity sev = (!writable()) ? rtc::LS_INFO : rtc::LS_VERBOSE; - RTC_LOG_V(sev) << ToString() << ": Sent STUN ping response, to=" - << addr.ToSensitiveString() + RTC_LOG_V(sev) << ToString() << ": Sent " + << StunMethodToString(response.type()) + << ", to=" << addr.ToSensitiveString() << ", id=" << rtc::hex_encode(response.transaction_id()); stats_.sent_ping_responses++; LogCandidatePairEvent(webrtc::IceCandidatePairEventType::kCheckResponseSent, - request->reduced_transaction_id()); + response.reduced_transaction_id()); } } @@ -786,7 +834,8 @@ void Connection::ReceivedPing(const absl::optional<std::string>& request_id) { } void Connection::HandlePiggybackCheckAcknowledgementIfAny(StunMessage* msg) { - RTC_DCHECK(msg->type() == STUN_BINDING_REQUEST); + RTC_DCHECK(msg->type() == STUN_BINDING_REQUEST || + msg->type() == GOOG_PING_REQUEST); const StunByteStringAttribute* last_ice_check_received_attr = msg->GetByteString(STUN_ATTR_LAST_ICE_CHECK_RECEIVED); if (last_ice_check_received_attr) { @@ -981,8 +1030,9 @@ void Connection::OnConnectionRequestResponse(ConnectionRequest* request, if (RTC_LOG_CHECK_LEVEL_V(sev)) { std::string pings; PrintPingsSinceLastResponse(&pings, 5); - RTC_LOG_V(sev) << ToString() << ": Received STUN ping response, id=" - << rtc::hex_encode(request->id()) + RTC_LOG_V(sev) << ToString() << ": Received " + << StunMethodToString(response->type()) + << ", id=" << rtc::hex_encode(request->id()) << ", code=0" // Makes logging easier to parse. ", rtt=" << rtt << ", pings_since_last_response=" << pings; @@ -1002,17 +1052,33 @@ void Connection::OnConnectionRequestResponse(ConnectionRequest* request, webrtc::IceCandidatePairEventType::kCheckResponseReceived, response->reduced_transaction_id()); - MaybeUpdateLocalCandidate(request, response); + if (request->msg()->type() == STUN_BINDING_REQUEST) { + auto goog_misc = response->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO); + if (goog_misc != nullptr && + goog_misc->Size() >= kSupportGoogPingVersionIndex && + goog_misc->GetType(kSupportGoogPingVersionIndex) >= kGoogPingVersion) { + // The remote peer has indicated that it supports GOOG_PING. + remote_support_goog_ping_ = true; + } + + MaybeUpdateLocalCandidate(request, response); + + if (field_trials_->enable_goog_ping && remote_support_goog_ping_) { + cached_stun_binding_ = request->msg()->Clone(); + } + } } void Connection::OnConnectionRequestErrorResponse(ConnectionRequest* request, StunMessage* response) { int error_code = response->GetErrorCodeValue(); - RTC_LOG(LS_WARNING) << ToString() << ": Received STUN error response id=" - << rtc::hex_encode(request->id()) + RTC_LOG(LS_WARNING) << ToString() << ": Received " + << StunMethodToString(response->type()) + << " id=" << rtc::hex_encode(request->id()) << " code=" << error_code << " rtt=" << request->Elapsed(); + cached_stun_binding_.reset(); if (error_code == STUN_ERROR_UNKNOWN_ATTRIBUTE || error_code == STUN_ERROR_SERVER_ERROR || error_code == STUN_ERROR_UNAUTHORIZED) { @@ -1021,6 +1087,8 @@ void Connection::OnConnectionRequestErrorResponse(ConnectionRequest* request, // Race failure, retry } else if (error_code == STUN_ERROR_ROLE_CONFLICT) { HandleRoleConflictFromPeer(); + } else if (request->msg()->type() == GOOG_PING_REQUEST) { + // Race, retry. } else { // This is not a valid connection. RTC_LOG(LS_ERROR) << ToString() @@ -1041,8 +1109,9 @@ void Connection::OnConnectionRequestTimeout(ConnectionRequest* request) { void Connection::OnConnectionRequestSent(ConnectionRequest* request) { // Log at LS_INFO if we send a ping on an unwritable connection. rtc::LoggingSeverity sev = !writable() ? rtc::LS_INFO : rtc::LS_VERBOSE; - RTC_LOG_V(sev) << ToString() - << ": Sent STUN ping, id=" << rtc::hex_encode(request->id()) + RTC_LOG_V(sev) << ToString() << ": Sent " + << StunMethodToString(request->msg()->type()) + << ", id=" << rtc::hex_encode(request->id()) << ", use_candidate=" << use_candidate_attr() << ", nomination=" << nomination(); stats_.sent_ping_requests_total++; @@ -1219,6 +1288,19 @@ bool Connection::TooManyOutstandingPings( return true; } +bool Connection::ShouldSendGoogPing(const StunMessage* message) { + if (remote_support_goog_ping_ && cached_stun_binding_ && + cached_stun_binding_->EqualAttributes(message, [](int type) { + // Ignore these attributes. + return type != STUN_ATTR_FINGERPRINT && + type != STUN_ATTR_MESSAGE_INTEGRITY && + type != STUN_ATTR_RETRANSMIT_COUNT; + })) { + return true; + } + return false; +} + ProxyConnection::ProxyConnection(Port* port, size_t index, const Candidate& remote_candidate) diff --git a/p2p/base/connection.h b/p2p/base/connection.h index 39066f406f..9f3ad21125 100644 --- a/p2p/base/connection.h +++ b/p2p/base/connection.h @@ -11,6 +11,7 @@ #ifndef P2P_BASE_CONNECTION_H_ #define P2P_BASE_CONNECTION_H_ +#include <memory> #include <string> #include <vector> @@ -31,6 +32,10 @@ namespace cricket { +// Version number for GOOG_PING, this is added to have the option of +// adding other flavors in the future. +constexpr int kGoogPingVersion = 1; + // Connection and Port has circular dependencies. // So we use forward declaration rather than include. class Port; @@ -227,7 +232,7 @@ class Connection : public CandidatePairInterface, void ReceivedPing( const absl::optional<std::string>& request_id = absl::nullopt); // Handles the binding request; sends a response if this is a valid request. - void HandleBindingRequest(IceMessage* msg); + void HandleStunBindingOrGoogPingRequest(IceMessage* msg); // Handles the piggyback acknowledgement of the lastest connectivity check // that the remote peer has received, if it is indicated in the incoming // connectivity check from the peer. @@ -298,7 +303,9 @@ class Connection : public CandidatePairInterface, return rtt_estimate_; } - void SendBindingResponse(const StunMessage* request); + void SendStunBindingResponse(const StunMessage* request); + void SendGoogPingResponse(const StunMessage* request); + void SendResponseMessage(const StunMessage& response); // An accessor for unit tests. Port* PortForTest() { return port_; } @@ -368,6 +375,10 @@ class Connection : public CandidatePairInterface, void LogCandidatePairEvent(webrtc::IceCandidatePairEventType type, uint32_t transaction_id); + // Check if this IceMessage is identical + // to last message ack:ed STUN_BINDING_REQUEST. + bool ShouldSendGoogPing(const StunMessage* message); + WriteState write_state_; bool receiving_; bool connected_; @@ -424,6 +435,13 @@ class Connection : public CandidatePairInterface, absl::optional<webrtc::IceCandidatePairDescription> log_description_; webrtc::IceEventLog* ice_event_log_ = nullptr; + // GOOG_PING_REQUEST is sent in place of STUN_BINDING_REQUEST + // if configured via field trial, the remote peer supports it (signaled + // in STUN_BINDING) and if the last STUN BINDING is identical to the one + // that is about to be sent. + absl::optional<bool> remote_support_goog_ping_; + std::unique_ptr<StunMessage> cached_stun_binding_; + const IceFieldTrials* field_trials_; rtc::EventBasedExponentialMovingAverage rtt_estimate_; diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc index c11a6b4efb..781f709f06 100644 --- a/p2p/base/p2p_transport_channel.cc +++ b/p2p/base/p2p_transport_channel.cc @@ -641,12 +641,21 @@ void P2PTransportChannel::SetIceConfig(const IceConfig& config) { } webrtc::StructParametersParser::Create( + // go/skylift-light "skip_relay_to_non_relay_connections", &field_trials_.skip_relay_to_non_relay_connections, + // Limiting pings sent. "max_outstanding_pings", &field_trials_.max_outstanding_pings, + // Delay initial selection of connection. "initial_select_dampening", &field_trials_.initial_select_dampening, + // Delay initial selection of connections, that are receiving. "initial_select_dampening_ping_received", &field_trials_.initial_select_dampening_ping_received, + // Reply that we support goog ping. + "announce_goog_ping", &field_trials_.announce_goog_ping, + // Use goog ping if remote support it. + "enable_goog_ping", &field_trials_.enable_goog_ping, + // How fast does a RTT sample decay. "rtt_estimate_halftime_ms", &field_trials_.rtt_estimate_halftime_ms) ->Parse(webrtc::field_trial::FindFullName("WebRTC-IceFieldTrials")); @@ -1028,7 +1037,7 @@ void P2PTransportChannel::OnUnknownAddress(PortInterface* port, : "resurrected") << " candidate: " << remote_candidate.ToSensitiveString(); AddConnection(connection); - connection->HandleBindingRequest(stun_msg); + connection->HandleStunBindingOrGoogPingRequest(stun_msg); // Update the list of connections since we just added another. We do this // after sending the response since it could (in principle) delete the diff --git a/p2p/base/p2p_transport_channel_ice_field_trials.h b/p2p/base/p2p_transport_channel_ice_field_trials.h index e0854a15c7..20836f75b3 100644 --- a/p2p/base/p2p_transport_channel_ice_field_trials.h +++ b/p2p/base/p2p_transport_channel_ice_field_trials.h @@ -32,6 +32,12 @@ struct IceFieldTrials { // give us chance to find a better connection before starting. absl::optional<int> initial_select_dampening_ping_received; + // Announce GOOG_PING support in STUN_BINDING_RESPONSE. + bool announce_goog_ping = true; + + // Enable sending GOOG_PING if remote announce it. + bool enable_goog_ping = false; + // Decay rate for RTT estimate using EventBasedExponentialMovingAverage // expressed as halving time. int rtt_estimate_halftime_ms = 500; diff --git a/p2p/base/p2p_transport_channel_unittest.cc b/p2p/base/p2p_transport_channel_unittest.cc index 23ed57cdd9..c2299ccf84 100644 --- a/p2p/base/p2p_transport_channel_unittest.cc +++ b/p2p/base/p2p_transport_channel_unittest.cc @@ -609,7 +609,7 @@ class P2PTransportChannelTestBase : public ::testing::Test, return CheckConnected(ch1, ch2) && CheckCandidatePair(ch1, ch2, from, to); } - void Test(const Result& expected) { + virtual void Test(const Result& expected) { rtc::ScopedFakeClock clock; int64_t connect_start = rtc::TimeMillis(); int64_t connect_time; @@ -1195,16 +1195,26 @@ const P2PTransportChannelTest::Result* LTRT}, }; +class P2PTransportChannelTestWithFieldTrials + : public P2PTransportChannelTest, + public ::testing::WithParamInterface<std::string> { + public: + void Test(const Result& expected) override { + webrtc::test::ScopedFieldTrials field_trials(GetParam()); + P2PTransportChannelTest::Test(expected); + } +}; + // The actual tests that exercise all the various configurations. // Test names are of the form P2PTransportChannelTest_TestOPENToNAT_FULL_CONE -#define P2P_TEST_DECLARATION(x, y, z) \ - TEST_F(P2PTransportChannelTest, z##Test##x##To##y) { \ - ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_SOCKET, \ - PORTALLOCATOR_ENABLE_SHARED_SOCKET); \ - if (kMatrix[x][y] != NULL) \ - Test(*kMatrix[x][y]); \ - else \ - RTC_LOG(LS_WARNING) << "Not yet implemented"; \ +#define P2P_TEST_DECLARATION(x, y, z) \ + TEST_P(P2PTransportChannelTestWithFieldTrials, z##Test##x##To##y) { \ + ConfigureEndpoints(x, y, PORTALLOCATOR_ENABLE_SHARED_SOCKET, \ + PORTALLOCATOR_ENABLE_SHARED_SOCKET); \ + if (kMatrix[x][y] != NULL) \ + Test(*kMatrix[x][y]); \ + else \ + RTC_LOG(LS_WARNING) << "Not yet implemented"; \ } #define P2P_TEST(x, y) P2P_TEST_DECLARATION(x, y, /* empty argument */) @@ -1236,6 +1246,12 @@ P2P_TEST_SET(BLOCK_ALL_BUT_OUTGOING_HTTP) P2P_TEST_SET(PROXY_HTTPS) P2P_TEST_SET(PROXY_SOCKS) +INSTANTIATE_TEST_SUITE_P( + P2PTransportChannelTestWithFieldTrials, + P2PTransportChannelTestWithFieldTrials, + // Each field-trial is ~144 tests (some return not-yet-implemented). + testing::Values("", "WebRTC-IceFieldTrials/enable_goog_ping:true/")); + // Test that we restart candidate allocation when local ufrag&pwd changed. // Standard Ice protocol is used. TEST_F(P2PTransportChannelTest, HandleUfragPwdChange) { diff --git a/p2p/base/port.cc b/p2p/base/port.cc index b92f1226c0..dbc04a484b 100644 --- a/p2p/base/port.cc +++ b/p2p/base/port.cc @@ -392,8 +392,8 @@ void Port::OnReadPacket(const char* data, } else if (!msg) { // STUN message handled already } else if (msg->type() == STUN_BINDING_REQUEST) { - RTC_LOG(LS_INFO) << "Received STUN ping id=" - << rtc::hex_encode(msg->transaction_id()) + RTC_LOG(LS_INFO) << "Received " << StunMethodToString(msg->type()) + << " id=" << rtc::hex_encode(msg->transaction_id()) << " from unknown address " << addr.ToSensitiveString(); // We need to signal an unknown address before we handle any role conflict // below. Otherwise there would be no candidate pair and TURN entry created @@ -404,12 +404,20 @@ void Port::OnReadPacket(const char* data, RTC_LOG(LS_INFO) << "Received conflicting role from the peer."; return; } + } else if (msg->type() == GOOG_PING_REQUEST) { + // This is a PING sent to a connection that was destroyed. + // Send back that this is the case and a authenticated BINDING + // is needed. + SendBindingErrorResponse(msg.get(), addr, STUN_ERROR_BAD_REQUEST, + STUN_ERROR_REASON_BAD_REQUEST); } else { // NOTE(tschmelcher): STUN_BINDING_RESPONSE is benign. It occurs if we // pruned a connection for this port while it had STUN requests in flight, // because we then get back responses for them, which this code correctly // does not handle. - if (msg->type() != STUN_BINDING_RESPONSE) { + if (msg->type() != STUN_BINDING_RESPONSE && + msg->type() != GOOG_PING_RESPONSE && + msg->type() != GOOG_PING_ERROR_RESPONSE) { RTC_LOG(LS_ERROR) << ToString() << ": Received unexpected STUN message type: " << msg->type() << " from unknown address: " @@ -444,7 +452,11 @@ bool Port::GetStunMessage(const char* data, // Don't bother parsing the packet if we can tell it's not STUN. // In ICE mode, all STUN packets will have a valid fingerprint. - if (!StunMessage::ValidateFingerprint(data, size)) { + // Except GOOG_PING_REQUEST/RESPONSE that does not send fingerprint. + int types[] = {GOOG_PING_REQUEST, GOOG_PING_RESPONSE, + GOOG_PING_ERROR_RESPONSE}; + if (!StunMessage::IsStunMethod(types, data, size) && + !StunMessage::ValidateFingerprint(data, size)) { return false; } @@ -461,8 +473,9 @@ bool Port::GetStunMessage(const char* data, // If not present, fail with a 400 Bad Request. if (!stun_msg->GetByteString(STUN_ATTR_USERNAME) || !stun_msg->GetByteString(STUN_ATTR_MESSAGE_INTEGRITY)) { - RTC_LOG(LS_ERROR) << ToString() - << ": Received STUN request without username/M-I from: " + RTC_LOG(LS_ERROR) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) + << " without username/M-I from: " << addr.ToSensitiveString(); SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_BAD_REQUEST, STUN_ERROR_REASON_BAD_REQUEST); @@ -474,9 +487,10 @@ bool Port::GetStunMessage(const char* data, std::string remote_ufrag; if (!ParseStunUsername(stun_msg.get(), &local_ufrag, &remote_ufrag) || local_ufrag != username_fragment()) { - RTC_LOG(LS_ERROR) << ToString() - << ": Received STUN request with bad local username " - << local_ufrag << " from " << addr.ToSensitiveString(); + RTC_LOG(LS_ERROR) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) + << " with bad local username " << local_ufrag + << " from " << addr.ToSensitiveString(); SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_UNAUTHORIZED, STUN_ERROR_REASON_UNAUTHORIZED); return true; @@ -484,9 +498,9 @@ bool Port::GetStunMessage(const char* data, // If ICE, and the MESSAGE-INTEGRITY is bad, fail with a 401 Unauthorized if (!stun_msg->ValidateMessageIntegrity(data, size, password_)) { - RTC_LOG(LS_ERROR) << ToString() - << ": Received STUN request with bad M-I from " - << addr.ToSensitiveString() + RTC_LOG(LS_ERROR) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) + << " with bad M-I from " << addr.ToSensitiveString() << ", password_=" << password_; SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_UNAUTHORIZED, STUN_ERROR_REASON_UNAUTHORIZED); @@ -497,30 +511,51 @@ bool Port::GetStunMessage(const char* data, (stun_msg->type() == STUN_BINDING_ERROR_RESPONSE)) { if (stun_msg->type() == STUN_BINDING_ERROR_RESPONSE) { if (const StunErrorCodeAttribute* error_code = stun_msg->GetErrorCode()) { - RTC_LOG(LS_ERROR) << ToString() - << ": Received STUN binding error: class=" - << error_code->eclass() + RTC_LOG(LS_ERROR) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) + << ": class=" << error_code->eclass() << " number=" << error_code->number() << " reason='" << error_code->reason() << "' from " << addr.ToSensitiveString(); // Return message to allow error-specific processing } else { - RTC_LOG(LS_ERROR) - << ToString() - << ": Received STUN binding error without a error code from " - << addr.ToSensitiveString(); + RTC_LOG(LS_ERROR) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) + << " without a error code from " + << addr.ToSensitiveString(); return true; } } // NOTE: Username should not be used in verifying response messages. out_username->clear(); } else if (stun_msg->type() == STUN_BINDING_INDICATION) { - RTC_LOG(LS_VERBOSE) << ToString() - << ": Received STUN binding indication: from " + RTC_LOG(LS_VERBOSE) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) << ": from " << addr.ToSensitiveString(); out_username->clear(); // No stun attributes will be verified, if it's stun indication message. // Returning from end of the this method. + } else if (stun_msg->type() == GOOG_PING_REQUEST) { + if (!stun_msg->ValidateMessageIntegrity32(data, size, password_)) { + RTC_LOG(LS_ERROR) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) + << " with bad M-I from " << addr.ToSensitiveString() + << ", password_=" << password_; + SendBindingErrorResponse(stun_msg.get(), addr, STUN_ERROR_UNAUTHORIZED, + STUN_ERROR_REASON_UNAUTHORIZED); + return true; + } + RTC_LOG(LS_VERBOSE) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) << " from " + << addr.ToSensitiveString(); + out_username->clear(); + } else if (stun_msg->type() == GOOG_PING_RESPONSE || + stun_msg->type() == GOOG_PING_ERROR_RESPONSE) { + // note: the MessageIntegrity32 will be verified in Connection.cc + RTC_LOG(LS_VERBOSE) << ToString() << ": Received " + << StunMethodToString(stun_msg->type()) << " from " + << addr.ToSensitiveString(); + out_username->clear(); } else { RTC_LOG(LS_ERROR) << ToString() << ": Received STUN packet with invalid type (" @@ -665,11 +700,16 @@ void Port::SendBindingErrorResponse(StunMessage* request, const rtc::SocketAddress& addr, int error_code, const std::string& reason) { - RTC_DCHECK(request->type() == STUN_BINDING_REQUEST); + RTC_DCHECK(request->type() == STUN_BINDING_REQUEST || + request->type() == GOOG_PING_REQUEST); // Fill in the response message. StunMessage response; - response.SetType(STUN_BINDING_ERROR_RESPONSE); + if (request->type() == STUN_BINDING_REQUEST) { + response.SetType(STUN_BINDING_ERROR_RESPONSE); + } else { + response.SetType(GOOG_PING_ERROR_RESPONSE); + } response.SetTransactionID(request->transaction_id()); // When doing GICE, we need to write out the error code incorrectly to @@ -682,9 +722,18 @@ void Port::SendBindingErrorResponse(StunMessage* request, // Per Section 10.1.2, certain error cases don't get a MESSAGE-INTEGRITY, // because we don't have enough information to determine the shared secret. if (error_code != STUN_ERROR_BAD_REQUEST && - error_code != STUN_ERROR_UNAUTHORIZED) - response.AddMessageIntegrity(password_); - response.AddFingerprint(); + error_code != STUN_ERROR_UNAUTHORIZED && + request->type() != GOOG_PING_REQUEST) { + if (request->type() == STUN_BINDING_REQUEST) { + response.AddMessageIntegrity(password_); + } else { + response.AddMessageIntegrity32(password_); + } + } + + if (request->type() == STUN_BINDING_REQUEST) { + response.AddFingerprint(); + } // Send the response message. rtc::ByteBufferWriter buf; @@ -693,9 +742,10 @@ void Port::SendBindingErrorResponse(StunMessage* request, options.info_signaled_after_sent.packet_type = rtc::PacketType::kIceConnectivityCheckResponse; SendTo(buf.Data(), buf.Length(), addr, options, false); - RTC_LOG(LS_INFO) << ToString() - << ": Sending STUN binding error: reason=" << reason - << " to " << addr.ToSensitiveString(); + RTC_LOG(LS_INFO) << ToString() << ": Sending STUN " + << StunMethodToString(response.type()) + << ": reason=" << reason << " to " + << addr.ToSensitiveString(); } void Port::KeepAliveUntilPruned() { diff --git a/p2p/base/port_unittest.cc b/p2p/base/port_unittest.cc index 55ff5be5ad..9abeb3ada4 100644 --- a/p2p/base/port_unittest.cc +++ b/p2p/base/port_unittest.cc @@ -302,7 +302,7 @@ class TestChannel : public sigslot::has_slots<> { c.set_address(remote_address_); conn_ = port_->CreateConnection(c, Port::ORIGIN_MESSAGE); conn_->SignalDestroyed.connect(this, &TestChannel::OnDestroyed); - conn_->SendBindingResponse(remote_request_.get()); + conn_->SendStunBindingResponse(remote_request_.get()); remote_request_.reset(); } void Ping() { Ping(0); } @@ -406,6 +406,8 @@ class PortTest : public ::testing::Test, public sigslot::has_slots<> { ports_destroyed_(0) {} protected: + std::string password() { return password_; } + void TestLocalToLocal() { auto port1 = CreateUdpPort(kLocalAddr1); port1->SetIceRole(cricket::ICEROLE_CONTROLLING); @@ -2621,7 +2623,7 @@ TEST_F(PortTest, TestIceLiteConnectivity) { auto* con = ice_lite_port->CreateConnection( ice_full_port_ptr->Candidates()[0], cricket::Port::ORIGIN_MESSAGE); std::unique_ptr<IceMessage> request = CopyStunMessage(*msg); - con->SendBindingResponse(request.get()); + con->SendStunBindingResponse(request.get()); // Feeding the respone message from litemode to the full mode connection. ch1.conn()->OnReadPacket(ice_lite_port->last_stun_buf()->data<char>(), @@ -2643,6 +2645,305 @@ TEST_F(PortTest, TestIceLiteConnectivity) { ch1.Stop(); } +namespace { + +// Utility function for testing goog ping. +absl::optional<int> GetSupportedGoogPingVersion(const StunMessage* response) { + auto goog_misc = response->GetUInt16List(STUN_ATTR_GOOG_MISC_INFO); + if (goog_misc == nullptr) { + return absl::nullopt; + } + + if (goog_misc->Size() < + static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex:: + SUPPORT_GOOG_PING_VERSION)) { + return absl::nullopt; + } + + return goog_misc->GetType( + static_cast<int>(cricket::IceGoogMiscInfoBindingResponseAttributeIndex:: + SUPPORT_GOOG_PING_VERSION)); +} + +} // namespace + +class GoogPingTest + : public PortTest, + public ::testing::WithParamInterface<std::pair<bool, bool>> {}; + +// This test verifies the announce/enable on/off behavior +TEST_P(GoogPingTest, TestGoogPingAnnounceEnable) { + IceFieldTrials trials; + trials.announce_goog_ping = GetParam().first; + trials.enable_goog_ping = GetParam().second; + RTC_LOG(LS_INFO) << "Testing combination: " + << " announce: " << trials.announce_goog_ping + << " enable:" << trials.enable_goog_ping; + + auto port1_unique = + CreateTestPort(kLocalAddr1, "lfrag", "lpass", + cricket::ICEROLE_CONTROLLING, kTiebreaker1); + auto* port1 = port1_unique.get(); + auto port2 = CreateTestPort(kLocalAddr2, "rfrag", "rpass", + cricket::ICEROLE_CONTROLLED, kTiebreaker2); + + TestChannel ch1(std::move(port1_unique)); + // Block usage of STUN_ATTR_USE_CANDIDATE so that + // ch1.conn() will sent GOOG_PING_REQUEST directly. + // This only makes test a bit shorter... + ch1.SetIceMode(ICEMODE_LITE); + // Start gathering candidates. + ch1.Start(); + port2->PrepareAddress(); + + ASSERT_EQ_WAIT(1, ch1.complete_count(), kDefaultTimeout); + ASSERT_FALSE(port2->Candidates().empty()); + + ch1.CreateConnection(GetCandidate(port2.get())); + ASSERT_TRUE(ch1.conn() != NULL); + EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state()); + ch1.conn()->SetIceFieldTrials(&trials); + + // Send ping. + ch1.Ping(); + + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* request1 = port1->last_stun_msg(); + auto* con = port2->CreateConnection(port1->Candidates()[0], + cricket::Port::ORIGIN_MESSAGE); + con->SetIceFieldTrials(&trials); + + con->SendStunBindingResponse(request1); + + // Then check the response matches the settings. + const auto* response = port2->last_stun_msg(); + ASSERT_EQ(response->type(), STUN_BINDING_RESPONSE); + ASSERT_EQ(trials.announce_goog_ping, + GetSupportedGoogPingVersion(response) && + GetSupportedGoogPingVersion(response) >= kGoogPingVersion); + + // Feeding the respone message back. + ch1.conn()->OnReadPacket(port2->last_stun_buf()->data<char>(), + port2->last_stun_buf()->size(), + /* packet_time_us */ -1); + + port1->Reset(); + port2->Reset(); + + ch1.Ping(); + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* request2 = port1->last_stun_msg(); + + // It should be a GOOG_PING if both of these are TRUE + if (trials.announce_goog_ping && trials.enable_goog_ping) { + ASSERT_EQ(request2->type(), GOOG_PING_REQUEST); + con->SendGoogPingResponse(request2); + } else { + ASSERT_EQ(request2->type(), STUN_BINDING_REQUEST); + con->SendStunBindingResponse(request2); + } + + const auto* response2 = port2->last_stun_msg(); + ASSERT_TRUE(response2 != nullptr); + + // It should be a GOOG_PING_RESPONSE if both of these are TRUE + if (trials.announce_goog_ping && trials.enable_goog_ping) { + ASSERT_EQ(response2->type(), GOOG_PING_RESPONSE); + } else { + ASSERT_EQ(response2->type(), STUN_BINDING_RESPONSE); + } + + ch1.Stop(); +} + +INSTANTIATE_TEST_SUITE_P(GoogPingTest, + GoogPingTest, + // test all combinations of <announce, enable> pairs. + ::testing::Values(std::make_pair(false, false), + std::make_pair(true, false), + std::make_pair(false, true), + std::make_pair(true, true))); + +// This test checks that a change in attributes falls back to STUN_BINDING +TEST_F(PortTest, TestChangeInAttributeMakesGoogPingFallsbackToStunBinding) { + IceFieldTrials trials; + trials.announce_goog_ping = true; + trials.enable_goog_ping = true; + + auto port1_unique = + CreateTestPort(kLocalAddr1, "lfrag", "lpass", + cricket::ICEROLE_CONTROLLING, kTiebreaker1); + auto* port1 = port1_unique.get(); + auto port2 = CreateTestPort(kLocalAddr2, "rfrag", "rpass", + cricket::ICEROLE_CONTROLLED, kTiebreaker2); + + TestChannel ch1(std::move(port1_unique)); + // Block usage of STUN_ATTR_USE_CANDIDATE so that + // ch1.conn() will sent GOOG_PING_REQUEST directly. + // This only makes test a bit shorter... + ch1.SetIceMode(ICEMODE_LITE); + // Start gathering candidates. + ch1.Start(); + port2->PrepareAddress(); + + ASSERT_EQ_WAIT(1, ch1.complete_count(), kDefaultTimeout); + ASSERT_FALSE(port2->Candidates().empty()); + + ch1.CreateConnection(GetCandidate(port2.get())); + ASSERT_TRUE(ch1.conn() != nullptr); + EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state()); + ch1.conn()->SetIceFieldTrials(&trials); + + // Send ping. + ch1.Ping(); + + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* msg = port1->last_stun_msg(); + auto* con = port2->CreateConnection(port1->Candidates()[0], + cricket::Port::ORIGIN_MESSAGE); + con->SetIceFieldTrials(&trials); + + // Feed the message into the connection. + con->SendStunBindingResponse(msg); + + // The check reply wrt to settings. + const auto* response = port2->last_stun_msg(); + ASSERT_EQ(response->type(), STUN_BINDING_RESPONSE); + ASSERT_TRUE(GetSupportedGoogPingVersion(response) >= kGoogPingVersion); + + // Feeding the respone message back. + ch1.conn()->OnReadPacket(port2->last_stun_buf()->data<char>(), + port2->last_stun_buf()->size(), + /* packet_time_us */ -1); + + port1->Reset(); + port2->Reset(); + + ch1.Ping(); + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* msg2 = port1->last_stun_msg(); + + // It should be a GOOG_PING if both of these are TRUE + ASSERT_EQ(msg2->type(), GOOG_PING_REQUEST); + con->SendGoogPingResponse(msg2); + + const auto* response2 = port2->last_stun_msg(); + ASSERT_TRUE(response2 != nullptr); + + // It should be a GOOG_PING_RESPONSE. + ASSERT_EQ(response2->type(), GOOG_PING_RESPONSE); + + // And now the third ping. + port1->Reset(); + port2->Reset(); + + // Modify the message to be sent. + ch1.conn()->set_use_candidate_attr(!ch1.conn()->use_candidate_attr()); + + ch1.Ping(); + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* msg3 = port1->last_stun_msg(); + + // It should be a STUN_BINDING_REQUEST + ASSERT_EQ(msg3->type(), STUN_BINDING_REQUEST); + + ch1.Stop(); +} + +// This test that an error response fall back to STUN_BINDING. +TEST_F(PortTest, TestErrorResponseMakesGoogPingFallBackToStunBinding) { + IceFieldTrials trials; + trials.announce_goog_ping = true; + trials.enable_goog_ping = true; + + auto port1_unique = + CreateTestPort(kLocalAddr1, "lfrag", "lpass", + cricket::ICEROLE_CONTROLLING, kTiebreaker1); + auto* port1 = port1_unique.get(); + auto port2 = CreateTestPort(kLocalAddr2, "rfrag", "rpass", + cricket::ICEROLE_CONTROLLED, kTiebreaker2); + + TestChannel ch1(std::move(port1_unique)); + // Block usage of STUN_ATTR_USE_CANDIDATE so that + // ch1.conn() will sent GOOG_PING_REQUEST directly. + // This only makes test a bit shorter... + ch1.SetIceMode(ICEMODE_LITE); + // Start gathering candidates. + ch1.Start(); + port2->PrepareAddress(); + + ASSERT_EQ_WAIT(1, ch1.complete_count(), kDefaultTimeout); + ASSERT_FALSE(port2->Candidates().empty()); + + ch1.CreateConnection(GetCandidate(port2.get())); + ASSERT_TRUE(ch1.conn() != NULL); + EXPECT_EQ(Connection::STATE_WRITE_INIT, ch1.conn()->write_state()); + ch1.conn()->SetIceFieldTrials(&trials); + + // Send ping. + ch1.Ping(); + + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* msg = port1->last_stun_msg(); + auto* con = port2->CreateConnection(port1->Candidates()[0], + cricket::Port::ORIGIN_MESSAGE); + con->SetIceFieldTrials(&trials); + + // Feed the message into the connection. + con->SendStunBindingResponse(msg); + + // The check reply wrt to settings. + const auto* response = port2->last_stun_msg(); + ASSERT_EQ(response->type(), STUN_BINDING_RESPONSE); + ASSERT_TRUE(GetSupportedGoogPingVersion(response) >= kGoogPingVersion); + + // Feeding the respone message back. + ch1.conn()->OnReadPacket(port2->last_stun_buf()->data<char>(), + port2->last_stun_buf()->size(), + /* packet_time_us */ -1); + + port1->Reset(); + port2->Reset(); + + ch1.Ping(); + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* msg2 = port1->last_stun_msg(); + + // It should be a GOOG_PING. + ASSERT_EQ(msg2->type(), GOOG_PING_REQUEST); + con->SendGoogPingResponse(msg2); + + const auto* response2 = port2->last_stun_msg(); + ASSERT_TRUE(response2 != nullptr); + + // It should be a GOOG_PING_RESPONSE. + ASSERT_EQ(response2->type(), GOOG_PING_RESPONSE); + + // But rather than the RESPONSE...feedback an error. + StunMessage error_response; + error_response.SetType(GOOG_PING_ERROR_RESPONSE); + error_response.SetTransactionID(response2->transaction_id()); + error_response.AddMessageIntegrity32("rpass"); + rtc::ByteBufferWriter buf; + error_response.Write(&buf); + + ch1.conn()->OnReadPacket(buf.Data(), buf.Length(), + /* packet_time_us */ -1); + + // And now the third ping...this should be a binding. + port1->Reset(); + port2->Reset(); + + ch1.Ping(); + ASSERT_TRUE_WAIT(port1->last_stun_msg() != NULL, kDefaultTimeout); + const IceMessage* msg3 = port1->last_stun_msg(); + + // It should be a STUN_BINDING_REQUEST + ASSERT_EQ(msg3->type(), STUN_BINDING_REQUEST); + + ch1.Stop(); +} + // This test case verifies that both the controlling port and the controlled // port will time out after connectivity is lost, if they are not marked as // "keep alive until pruned." |