From 7087857afd5d946877e8fbf086afa55880f24b6f Mon Sep 17 00:00:00 2001 From: "guoweis@webrtc.org" Date: Tue, 26 Aug 2014 21:37:49 +0000 Subject: [PATCH] implement handling ALTERNATE-SERVER response from turn protocol as specified in RFC 5766, also created 2 test cases for both the normal redirection case as well as when a pingpong situation happens, the allocation should fail BUG=1986 TURN ALTERNATE-SERVER support R=juberti@webrtc.org Review URL: https://webrtc-codereview.appspot.com/21249004 git-svn-id: http://webrtc.googlecode.com/svn/trunk@6985 4adac7df-926f-26a2-2b94-8c16560cd09d --- talk/p2p/base/stun.cc | 3 +- talk/p2p/base/stun.h | 3 +- talk/p2p/base/testturnserver.h | 26 ++++++++ talk/p2p/base/turnport.cc | 90 ++++++++++++++++++++++++++ talk/p2p/base/turnport.h | 4 ++ talk/p2p/base/turnport_unittest.cc | 100 ++++++++++++++++++++++++++++- talk/p2p/base/turnserver.cc | 22 ++++++- talk/p2p/base/turnserver.h | 20 ++++++ 8 files changed, 264 insertions(+), 4 deletions(-) diff --git a/talk/p2p/base/stun.cc b/talk/p2p/base/stun.cc index be96b76403..061fd9a603 100644 --- a/talk/p2p/base/stun.cc +++ b/talk/p2p/base/stun.cc @@ -41,6 +41,7 @@ using rtc::ByteBuffer; namespace cricket { +const char STUN_ERROR_REASON_TRY_ALTERNATE_SERVER[] = "Try Alternate Server"; const char STUN_ERROR_REASON_BAD_REQUEST[] = "Bad Request"; const char STUN_ERROR_REASON_UNAUTHORIZED[] = "Unauthorized"; const char STUN_ERROR_REASON_FORBIDDEN[] = "Forbidden"; @@ -401,7 +402,7 @@ StunAttributeValueType StunMessage::GetAttributeValueType(int type) const { case STUN_ATTR_NONCE: return STUN_VALUE_BYTE_STRING; case STUN_ATTR_XOR_MAPPED_ADDRESS: return STUN_VALUE_XOR_ADDRESS; case STUN_ATTR_SOFTWARE: return STUN_VALUE_BYTE_STRING; - case STUN_ATTR_ALTERNATE_SERVER: return STUN_VALUE_BYTE_STRING; + case STUN_ATTR_ALTERNATE_SERVER: return STUN_VALUE_ADDRESS; case STUN_ATTR_FINGERPRINT: return STUN_VALUE_UINT32; case STUN_ATTR_RETRANSMIT_COUNT: return STUN_VALUE_UINT32; default: return STUN_VALUE_UNKNOWN; diff --git a/talk/p2p/base/stun.h b/talk/p2p/base/stun.h index b22b51ed6d..c4f522b906 100644 --- a/talk/p2p/base/stun.h +++ b/talk/p2p/base/stun.h @@ -63,7 +63,7 @@ enum StunAttributeType { STUN_ATTR_NONCE = 0x0015, // ByteString STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020, // XorAddress STUN_ATTR_SOFTWARE = 0x8022, // ByteString - STUN_ATTR_ALTERNATE_SERVER = 0x8023, // ByteString + STUN_ATTR_ALTERNATE_SERVER = 0x8023, // Address STUN_ATTR_FINGERPRINT = 0x8028, // UInt32 STUN_ATTR_RETRANSMIT_COUNT = 0xFF00 // UInt32 }; @@ -104,6 +104,7 @@ enum StunErrorCode { }; // Strings for the error codes above. +extern const char STUN_ERROR_REASON_TRY_ALTERNATE_SERVER[]; extern const char STUN_ERROR_REASON_BAD_REQUEST[]; extern const char STUN_ERROR_REASON_UNAUTHORIZED[]; extern const char STUN_ERROR_REASON_UNKNOWN_ATTRIBUTE[]; diff --git a/talk/p2p/base/testturnserver.h b/talk/p2p/base/testturnserver.h index e2c0ccb4e4..6c30afe069 100644 --- a/talk/p2p/base/testturnserver.h +++ b/talk/p2p/base/testturnserver.h @@ -29,6 +29,7 @@ #define TALK_P2P_BASE_TESTTURNSERVER_H_ #include +#include #include "talk/p2p/base/basicpacketsocketfactory.h" #include "talk/p2p/base/stun.h" @@ -41,6 +42,27 @@ namespace cricket { static const char kTestRealm[] = "example.org"; static const char kTestSoftware[] = "TestTurnServer"; +class TestTurnRedirector : public TurnRedirectInterface { + public: + explicit TestTurnRedirector(const std::vector& addresses) + : alternate_server_addresses_(addresses), + iter_(alternate_server_addresses_.begin()) { + } + + virtual bool ShouldRedirect(const rtc::SocketAddress&, + rtc::SocketAddress* out) { + if (!out || iter_ == alternate_server_addresses_.end()) { + return false; + } + *out = *iter_++; + return true; + } + + private: + const std::vector& alternate_server_addresses_; + std::vector::const_iterator iter_; +}; + class TestTurnServer : public TurnAuthInterface { public: TestTurnServer(rtc::Thread* thread, @@ -61,6 +83,10 @@ class TestTurnServer : public TurnAuthInterface { TurnServer* server() { return &server_; } + void set_redirect_hook(TurnRedirectInterface* redirect_hook) { + server_.set_redirect_hook(redirect_hook); + } + void AddInternalSocket(const rtc::SocketAddress& int_addr, ProtocolType proto) { rtc::Thread* thread = rtc::Thread::Current(); diff --git a/talk/p2p/base/turnport.cc b/talk/p2p/base/turnport.cc index 3faacd1011..6ab0e2b96a 100644 --- a/talk/p2p/base/turnport.cc +++ b/talk/p2p/base/turnport.cc @@ -78,6 +78,7 @@ class TurnAllocateRequest : public StunRequest { private: // Handles authentication challenge from the server. void OnAuthChallenge(StunMessage* response, int code); + void OnTryAlternate(StunMessage* response, int code); void OnUnknownAttribute(StunMessage* response); TurnPort* port_; @@ -253,6 +254,9 @@ void TurnPort::PrepareAddress() { return; } + // Insert the current address to prevent redirection pingpong. + attempted_server_addresses_.insert(server_address_.address); + LOG_J(LS_INFO, this) << "Trying to connect to TURN server via " << ProtoToString(server_address_.proto) << " @ " << server_address_.address.ToSensitiveString(); @@ -458,6 +462,38 @@ void TurnPort::OnReadyToSend(rtc::AsyncPacketSocket* socket) { } } + +// Update current server address port with the alternate server address port. +bool TurnPort::SetAlternateServer(const rtc::SocketAddress& address) { + // Check if we have seen this address before and reject if we did. + AttemptedServerSet::iterator iter = attempted_server_addresses_.find(address); + if (iter != attempted_server_addresses_.end()) { + LOG_J(LS_WARNING, this) << "Redirection to [" + << address.ToSensitiveString() + << "] ignored, allocation failed."; + return false; + } + + // If protocol family of server address doesn't match with local, return. + if (!IsCompatibleAddress(address)) { + LOG(LS_WARNING) << "Server IP address family does not match with " + << "local host address family type"; + return false; + } + + LOG_J(LS_INFO, this) << "Redirecting from TURN server [" + << server_address_.address.ToSensitiveString() + << "] to TURN server [" + << address.ToSensitiveString() + << "]"; + server_address_ = ProtocolAddress(address, server_address_.proto, + server_address_.secure); + + // Insert the current address to prevent redirection pingpong. + attempted_server_addresses_.insert(server_address_.address); + return true; +} + void TurnPort::ResolveTurnAddress(const rtc::SocketAddress& address) { if (resolver_) return; @@ -805,6 +841,9 @@ void TurnAllocateRequest::OnErrorResponse(StunMessage* response) { case STUN_ERROR_UNAUTHORIZED: // Unauthrorized. OnAuthChallenge(response, error_code->code()); break; + case STUN_ERROR_TRY_ALTERNATE: + OnTryAlternate(response, error_code->code()); + break; default: LOG_J(LS_WARNING, port_) << "Allocate response error, code=" << error_code->code(); @@ -849,6 +888,57 @@ void TurnAllocateRequest::OnAuthChallenge(StunMessage* response, int code) { port_->SendRequest(new TurnAllocateRequest(port_), 0); } +void TurnAllocateRequest::OnTryAlternate(StunMessage* response, int code) { + // TODO(guoweis): Currently, we only support UDP redirect + if (port_->server_address().proto != PROTO_UDP) { + LOG_J(LS_WARNING, port_) << "Receiving 300 Alternate Server on non-UDP " + << "allocating request from [" + << port_->server_address().address.ToSensitiveString() + << "], failed as currently not supported"; + port_->OnAllocateError(); + return; + } + + // According to RFC 5389 section 11, there are use cases where + // authentication of response is not possible, we're not validating + // message integrity. + + // Get the alternate server address attribute value. + const StunAddressAttribute* alternate_server_attr = + response->GetAddress(STUN_ATTR_ALTERNATE_SERVER); + if (!alternate_server_attr) { + LOG_J(LS_WARNING, port_) << "Missing STUN_ATTR_ALTERNATE_SERVER " + << "attribute in try alternate error response"; + port_->OnAllocateError(); + return; + } + if (!port_->SetAlternateServer(alternate_server_attr->GetAddress())) { + port_->OnAllocateError(); + return; + } + + // Check the attributes. + const StunByteStringAttribute* realm_attr = + response->GetByteString(STUN_ATTR_REALM); + if (realm_attr) { + LOG_J(LS_INFO, port_) << "Applying STUN_ATTR_REALM attribute in " + << "try alternate error response."; + port_->set_realm(realm_attr->GetString()); + } + + const StunByteStringAttribute* nonce_attr = + response->GetByteString(STUN_ATTR_NONCE); + if (nonce_attr) { + LOG_J(LS_INFO, port_) << "Applying STUN_ATTR_NONCE attribute in " + << "try alternate error response."; + port_->set_nonce(nonce_attr->GetString()); + } + + // Send another allocate request to alternate server, + // with the received realm and nonce values. + port_->SendRequest(new TurnAllocateRequest(port_), 0); +} + TurnRefreshRequest::TurnRefreshRequest(TurnPort* port) : StunRequest(new TurnMessage()), port_(port) { diff --git a/talk/p2p/base/turnport.h b/talk/p2p/base/turnport.h index d73b11da97..b9ec3b002e 100644 --- a/talk/p2p/base/turnport.h +++ b/talk/p2p/base/turnport.h @@ -30,6 +30,7 @@ #include #include +#include #include #include "talk/p2p/base/port.h" @@ -157,6 +158,7 @@ class TurnPort : public Port { typedef std::list EntryList; typedef std::map SocketOptionsMap; + typedef std::set AttemptedServerSet; virtual void OnMessage(rtc::Message* pmsg); @@ -170,6 +172,7 @@ class TurnPort : public Port { } } + bool SetAlternateServer(const rtc::SocketAddress& address); void ResolveTurnAddress(const rtc::SocketAddress& address); void OnResolveResult(rtc::AsyncResolverInterface* resolver); @@ -207,6 +210,7 @@ class TurnPort : public Port { ProtocolAddress server_address_; RelayCredentials credentials_; + AttemptedServerSet attempted_server_addresses_; rtc::AsyncPacketSocket* socket_; SocketOptionsMap socket_options_; diff --git a/talk/p2p/base/turnport_unittest.cc b/talk/p2p/base/turnport_unittest.cc index 44dc64fe37..14befa9d13 100644 --- a/talk/p2p/base/turnport_unittest.cc +++ b/talk/p2p/base/turnport_unittest.cc @@ -64,6 +64,8 @@ static const SocketAddress kTurnUdpIntAddr("99.99.99.3", static const SocketAddress kTurnTcpIntAddr("99.99.99.4", cricket::TURN_SERVER_PORT); static const SocketAddress kTurnUdpExtAddr("99.99.99.5", 0); +static const SocketAddress kTurnAlternateUdpIntAddr( + "99.99.99.6", cricket::TURN_SERVER_PORT); static const SocketAddress kTurnUdpIPv6IntAddr( "2400:4030:1:2c00:be30:abcd:efab:cdef", cricket::TURN_SERVER_PORT); static const SocketAddress kTurnUdpIPv6ExtAddr( @@ -445,6 +447,103 @@ TEST_F(TurnPortTest, TestTurnTlsTcpConnectionFails) { ASSERT_EQ(0U, turn_port_->Candidates().size()); } +// Test try-alternate-server feature. +TEST_F(TurnPortTest, TestTurnAlternateServer) { + std::vector redirect_addresses; + redirect_addresses.push_back(kTurnAlternateUdpIntAddr); + + cricket::TestTurnRedirector redirector(redirect_addresses); + turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr, + cricket::PROTO_UDP); + turn_server_.set_redirect_hook(&redirector); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + + // Retrieve the address before we run the state machine. + const SocketAddress old_addr = turn_port_->server_address().address; + + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_ready_, kTimeout); + // Retrieve the address again, the turn port's address should be + // changed. + const SocketAddress new_addr = turn_port_->server_address().address; + EXPECT_NE(old_addr, new_addr); + ASSERT_EQ(1U, turn_port_->Candidates().size()); + EXPECT_EQ(kTurnUdpExtAddr.ipaddr(), + turn_port_->Candidates()[0].address().ipaddr()); + EXPECT_NE(0, turn_port_->Candidates()[0].address().port()); +} + +// Test that we fail when we redirect to an address different from +// current IP family. +TEST_F(TurnPortTest, TestTurnAlternateServerV4toV6) { + std::vector redirect_addresses; + redirect_addresses.push_back(kTurnUdpIPv6IntAddr); + + cricket::TestTurnRedirector redirector(redirect_addresses); + turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr, + cricket::PROTO_UDP); + turn_server_.set_redirect_hook(&redirector); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_error_, kTimeout); +} + +// Test that we fail to handle alternate-server response over TCP protocol. +TEST_F(TurnPortTest, TestTurnAlternateServerTcp) { + std::vector redirect_addresses; + redirect_addresses.push_back(kTurnAlternateUdpIntAddr); + + cricket::TestTurnRedirector redirector(redirect_addresses); + turn_server_.set_redirect_hook(&redirector); + turn_server_.AddInternalSocket(kTurnTcpIntAddr, cricket::PROTO_TCP); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnTcpProtoAddr); + + turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr, cricket::PROTO_TCP); + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_error_, kTimeout); +} + +// Test try-alternate-server catches the case of pingpong. +TEST_F(TurnPortTest, TestTurnAlternateServerPingPong) { + std::vector redirect_addresses; + redirect_addresses.push_back(kTurnAlternateUdpIntAddr); + redirect_addresses.push_back(kTurnUdpIntAddr); + + cricket::TestTurnRedirector redirector(redirect_addresses); + + turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr, + cricket::PROTO_UDP); + turn_server_.set_redirect_hook(&redirector); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_error_, kTimeout); + ASSERT_EQ(0U, turn_port_->Candidates().size()); + rtc::SocketAddress address; + // Verify that we have exhausted all alternate servers instead of + // failure caused by other errors. + EXPECT_FALSE(redirector.ShouldRedirect(address, &address)); +} + +// Test try-alternate-server catch the case of repeated server. +TEST_F(TurnPortTest, TestTurnAlternateServerDetectRepetition) { + std::vector redirect_addresses; + redirect_addresses.push_back(kTurnAlternateUdpIntAddr); + redirect_addresses.push_back(kTurnAlternateUdpIntAddr); + + cricket::TestTurnRedirector redirector(redirect_addresses); + + turn_server_.AddInternalSocket(kTurnAlternateUdpIntAddr, + cricket::PROTO_UDP); + turn_server_.set_redirect_hook(&redirector); + CreateTurnPort(kTurnUsername, kTurnPassword, kTurnUdpProtoAddr); + + turn_port_->PrepareAddress(); + EXPECT_TRUE_WAIT(turn_error_, kTimeout); + ASSERT_EQ(0U, turn_port_->Candidates().size()); +} + + // Run TurnConnectionTest with one-time-use nonce feature. // Here server will send a 438 STALE_NONCE error message for // every TURN transaction. @@ -515,4 +614,3 @@ TEST_F(TurnPortTest, TestResolverShutdown) { EXPECT_EQ(last_fd_count, GetFDCount()); } #endif - diff --git a/talk/p2p/base/turnserver.cc b/talk/p2p/base/turnserver.cc index abc065ace9..08c060de47 100644 --- a/talk/p2p/base/turnserver.cc +++ b/talk/p2p/base/turnserver.cc @@ -208,6 +208,7 @@ TurnServer::TurnServer(rtc::Thread* thread) : thread_(thread), nonce_key_(rtc::CreateRandomString(kNonceKeySize)), auth_hook_(NULL), + redirect_hook_(NULL), enable_otu_nonce_(false) { } @@ -316,6 +317,15 @@ void TurnServer::HandleStunMessage(Connection* conn, const char* data, return; } + if (redirect_hook_ != NULL && msg.type() == STUN_ALLOCATE_REQUEST) { + rtc::SocketAddress address; + if (redirect_hook_->ShouldRedirect(conn->src(), &address)) { + SendErrorResponseWithAlternateServer( + conn, &msg, address); + return; + } + } + // Look up the key that we'll use to validate the M-I. If we have an // existing allocation, the key will already be cached. Allocation* allocation = FindAllocation(conn); @@ -334,7 +344,6 @@ void TurnServer::HandleStunMessage(Connection* conn, const char* data, } if (!allocation && msg.type() == STUN_ALLOCATE_REQUEST) { - // This is a new allocate request. HandleAllocateRequest(conn, &msg, key); } else if (allocation && (msg.type() != STUN_ALLOCATE_REQUEST || @@ -551,6 +560,17 @@ void TurnServer::SendErrorResponseWithRealmAndNonce( SendStun(conn, &resp); } +void TurnServer::SendErrorResponseWithAlternateServer( + Connection* conn, const StunMessage* msg, + const rtc::SocketAddress& addr) { + TurnMessage resp; + InitErrorResponse(msg, STUN_ERROR_TRY_ALTERNATE, + STUN_ERROR_REASON_TRY_ALTERNATE_SERVER, &resp); + VERIFY(resp.AddAttribute(new StunAddressAttribute( + STUN_ATTR_ALTERNATE_SERVER, addr))); + SendStun(conn, &resp); +} + void TurnServer::SendStun(Connection* conn, StunMessage* msg) { rtc::ByteBuffer buf; // Add a SOFTWARE attribute if one is set. diff --git a/talk/p2p/base/turnserver.h b/talk/p2p/base/turnserver.h index 4798232813..553d00ca8a 100644 --- a/talk/p2p/base/turnserver.h +++ b/talk/p2p/base/turnserver.h @@ -63,6 +63,14 @@ class TurnAuthInterface { std::string* key) = 0; }; +// An interface enables Turn Server to control redirection behavior. +class TurnRedirectInterface { + public: + virtual bool ShouldRedirect(const rtc::SocketAddress& address, + rtc::SocketAddress* out) = 0; + virtual ~TurnRedirectInterface() {} +}; + // The core TURN server class. Give it a socket to listen on via // AddInternalServerSocket, and a factory to create external sockets via // SetExternalSocketFactory, and it's ready to go. @@ -83,6 +91,10 @@ class TurnServer : public sigslot::has_slots<> { // Sets the authentication callback; does not take ownership. void set_auth_hook(TurnAuthInterface* auth_hook) { auth_hook_ = auth_hook; } + void set_redirect_hook(TurnRedirectInterface* redirect_hook) { + redirect_hook_ = redirect_hook; + } + void set_enable_otu_nonce(bool enable) { enable_otu_nonce_ = enable; } // Starts listening for packets from internal clients. @@ -155,6 +167,11 @@ class TurnServer : public sigslot::has_slots<> { const StunMessage* req, int code, const std::string& reason); + + void SendErrorResponseWithAlternateServer(Connection* conn, + const StunMessage* req, + const rtc::SocketAddress& addr); + void SendStun(Connection* conn, StunMessage* msg); void Send(Connection* conn, const rtc::ByteBuffer& buf); @@ -171,14 +188,17 @@ class TurnServer : public sigslot::has_slots<> { std::string realm_; std::string software_; TurnAuthInterface* auth_hook_; + TurnRedirectInterface* redirect_hook_; // otu - one-time-use. Server will respond with 438 if it's // sees the same nonce in next transaction. bool enable_otu_nonce_; + InternalSocketMap server_sockets_; ServerSocketMap server_listen_sockets_; rtc::scoped_ptr external_socket_factory_; rtc::SocketAddress external_addr_; + AllocationMap allocations_; };