瀏覽代碼

Merge pull request #287 from paullouisageneau/turn

TURN support for libjuice
Paul-Louis Ageneau 4 年之前
父節點
當前提交
1427c9e1e4
共有 8 個文件被更改,包括 316 次插入19 次删除
  1. 1 0
      CMakeLists.txt
  2. 1 2
      README.md
  3. 1 1
      deps/libjuice
  4. 31 10
      src/icetransport.cpp
  5. 0 2
      src/icetransport.hpp
  6. 6 4
      test/connectivity.cpp
  7. 10 0
      test/main.cpp
  8. 266 0
      test/turn_connectivity.cpp

+ 1 - 0
CMakeLists.txt

@@ -100,6 +100,7 @@ set(LIBDATACHANNEL_HEADERS
 set(TESTS_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/test/main.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/connectivity.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/test/turn_connectivity.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/track.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/capi_connectivity.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/capi_track.cpp

+ 1 - 2
README.md

@@ -21,7 +21,7 @@ Protocol stack:
 - SCTP-based Data Channels ([draft-ietf-rtcweb-data-channel-13](https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13))
 - SRTP-based Media Transport ([draft-ietf-rtcweb-rtp-usage-26](https://tools.ietf.org/html/draft-ietf-rtcweb-rtp-usage-26))
 - DTLS/UDP ([RFC7350](https://tools.ietf.org/html/rfc7350) and [RFC8261](https://tools.ietf.org/html/rfc8261))
-- ICE ([RFC8445](https://tools.ietf.org/html/rfc8445)) with STUN ([RFC5389](https://tools.ietf.org/html/rfc5389))
+- ICE ([RFC8445](https://tools.ietf.org/html/rfc8445)) with STUN ([RFC8489](https://tools.ietf.org/html/rfc8489)) and its extension TURN ([RFC8656](https://tools.ietf.org/html/rfc8656))
 
 Features:
 - Full IPv6 support
@@ -30,7 +30,6 @@ Features:
 - Multicast DNS candidates ([draft-ietf-rtcweb-mdns-ice-candidates-04](https://tools.ietf.org/html/draft-ietf-rtcweb-mdns-ice-candidates-04))
 - SRTP and SRTCP key derivation from DTLS ([RFC5764](https://tools.ietf.org/html/rfc5764))
 - Differentiated Services QoS ([draft-ietf-tsvwg-rtcweb-qos-18](https://tools.ietf.org/html/draft-ietf-tsvwg-rtcweb-qos-18))
-- TURN relaying ([RFC5766](https://tools.ietf.org/html/rfc5766)) with [libnice](https://github.com/libnice/libnice) as ICE backend
 
 Note only SDP BUNDLE mode is supported for media multiplexing ([draft-ietf-mmusic-sdp-bundle-negotiation-54](https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54)). The behavior is equivalent to the JSEP bundle-only policy: the library always negociates one unique network component, where SRTP media streams are multiplexed with SRTCP control packets ([RFC5761](https://tools.ietf.org/html/rfc5761)) and SCTP/DTLS data traffic ([RFC5764](https://tools.ietf.org/html/rfc5764)).
 

+ 1 - 1
deps/libjuice

@@ -1 +1 @@
-Subproject commit 421c650d9dd85c4824410f09274d95ce64b6e2e3
+Subproject commit a8926857f4f2a56df2dddc80889c6c17931c1fec

+ 31 - 10
src/icetransport.cpp

@@ -44,6 +44,8 @@ using std::chrono::system_clock;
 
 #if !USE_NICE
 
+#define MAX_TURN_SERVERS_COUNT 2
+
 namespace rtc {
 
 IceTransport::IceTransport(const Configuration &config, candidate_callback candidateCallback,
@@ -98,21 +100,39 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 	auto seed = static_cast<unsigned int>(system_clock::now().time_since_epoch().count());
 	std::shuffle(servers.begin(), servers.end(), std::default_random_engine(seed));
 
-	// Pick a STUN server (TURN support is not implemented in libjuice yet)
+	// Pick a STUN server
 	for (auto &server : servers) {
 		if (!server.hostname.empty() && server.type == IceServer::Type::Stun) {
 			if (server.service.empty())
 				server.service = "3478"; // STUN UDP port
-			PLOG_DEBUG << "Using STUN server \"" << server.hostname << ":" << server.service
-			           << "\"";
-			mStunHostname = server.hostname;
-			mStunService = server.service;
-			jconfig.stun_server_host = mStunHostname.c_str();
-			jconfig.stun_server_port = uint16_t(std::stoul(mStunService));
+			PLOG_INFO << "Using STUN server \"" << server.hostname << ":" << server.service << "\"";
+			jconfig.stun_server_host = server.hostname.c_str();
+			jconfig.stun_server_port = uint16_t(std::stoul(server.service));
 			break;
 		}
 	}
 
+	juice_turn_server_t turn_servers[MAX_TURN_SERVERS_COUNT];
+	std::memset(turn_servers, 0, sizeof(turn_servers));
+
+	// Add TURN servers
+	int k = 0;
+	for (auto &server : servers) {
+		if (!server.hostname.empty() && server.type == IceServer::Type::Turn) {
+			if (server.service.empty())
+				server.service = "3478"; // TURN UDP port
+			PLOG_INFO << "Using TURN server \"" << server.hostname << ":" << server.service << "\"";
+			turn_servers[k].host = server.hostname.c_str();
+			turn_servers[k].username = server.username.c_str();
+			turn_servers[k].password = server.password.c_str();
+			turn_servers[k].port = uint16_t(std::stoul(server.service));
+			if (++k >= MAX_TURN_SERVERS_COUNT)
+				break;
+		}
+	}
+	jconfig.turn_servers = k > 0 ? turn_servers : nullptr;
+	jconfig.turn_servers_count = k;
+
 	// Port range
 	if (config.portRangeBegin > 1024 ||
 	    (config.portRangeEnd != 0 && config.portRangeEnd != 65535)) {
@@ -424,8 +444,8 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 				if (getnameinfo(p->ai_addr, p->ai_addrlen, nodebuffer, MAX_NUMERICNODE_LEN,
 				                servbuffer, MAX_NUMERICNODE_LEN,
 				                NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
-					PLOG_DEBUG << "Using STUN server \"" << server.hostname << ":" << server.service
-					           << "\"";
+					PLOG_INFO << "Using STUN server \"" << server.hostname << ":" << server.service
+					          << "\"";
 					g_object_set(G_OBJECT(mNiceAgent.get()), "stun-server", nodebuffer, nullptr);
 					g_object_set(G_OBJECT(mNiceAgent.get()), "stun-server-port",
 					             std::stoul(servbuffer), nullptr);
@@ -470,7 +490,8 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 				if (getnameinfo(p->ai_addr, p->ai_addrlen, nodebuffer, MAX_NUMERICNODE_LEN,
 				                servbuffer, MAX_NUMERICNODE_LEN,
 				                NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
-
+					PLOG_INFO << "Using TURN server \"" << server.hostname << ":" << server.service
+					          << "\"";
 					NiceRelayType niceRelayType;
 					switch (server.relayType) {
 					case IceServer::RelayType::TurnTcp:

+ 0 - 2
src/icetransport.hpp

@@ -86,8 +86,6 @@ private:
 
 #if !USE_NICE
 	std::unique_ptr<juice_agent_t, void (*)(juice_agent_t *)> mAgent;
-	string mStunHostname;
-	string mStunService;
 
 	static void StateChangeCallback(juice_agent_t *agent, juice_state_t state, void *user_ptr);
 	static void CandidateCallback(juice_agent_t *agent, const char *sdp, void *user_ptr);

+ 6 - 4
test/connectivity.cpp

@@ -33,14 +33,16 @@ void test_connectivity() {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
-	// STUN server example
-	// config1.iceServers.emplace_back("stun:stun.l.google.com:19302");
+	// STUN server example (not necessary to connect locally)
+	// Please do not use outside of libdatachannel tests
+	config1.iceServers.emplace_back("stun:stun.ageneau.net:3478");
 
 	auto pc1 = std::make_shared<PeerConnection>(config1);
 
 	Configuration config2;
-	// STUN server example
-	// config2.iceServers.emplace_back("stun:stun.l.google.com:19302");
+	// STUN server example (not necessary to connect locally)
+	// Please do not use outside of libdatachannel tests
+	config2.iceServers.emplace_back("stun:stun.ageneau.net:3478");
 	// Port range example
 	config2.portRangeBegin = 5000;
 	config2.portRangeEnd = 6000;

+ 10 - 0
test/main.cpp

@@ -24,6 +24,7 @@ using namespace std;
 using namespace chrono_literals;
 
 void test_connectivity();
+void test_turn_connectivity();
 void test_track();
 void test_capi_connectivity();
 void test_capi_track();
@@ -51,6 +52,15 @@ int main(int argc, char **argv) {
 		return -1;
 	}
 	this_thread::sleep_for(1s);
+	try {
+		cout << endl << "*** Running WebRTC TURN connectivity test..." << endl;
+		test_turn_connectivity();
+		cout << "*** Finished WebRTC TURN connectivity test" << endl;
+	} catch (const exception &e) {
+		cerr << "WebRTC TURN connectivity test failed: " << e.what() << endl;
+		return -1;
+	}
+	this_thread::sleep_for(1s);
 	try {
 		cout << endl << "*** Running WebRTC C API connectivity test..." << endl;
 		test_capi_connectivity();

+ 266 - 0
test/turn_connectivity.cpp

@@ -0,0 +1,266 @@
+/**
+ * Copyright (c) 2019 Paul-Louis Ageneau
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "rtc/rtc.hpp"
+
+#include <atomic>
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <thread>
+
+using namespace rtc;
+using namespace std;
+
+template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
+
+void test_turn_connectivity() {
+	InitLogger(LogLevel::Debug);
+
+	Configuration config1;
+	// STUN server example (not necessary, just here for testing)
+	// Please do not use outside of libdatachannel tests
+	config1.iceServers.emplace_back("stun:stun.ageneau.net:3478");
+	// TURN server example
+	// Please do not use outside of libdatachannel tests
+	config1.iceServers.emplace_back("turn:datachannel_test:[email protected]:3478");
+
+	auto pc1 = std::make_shared<PeerConnection>(config1);
+
+	Configuration config2;
+	// STUN server example (not necessary, just here for testing)
+	// Please do not use outside of libdatachannel tests
+	config1.iceServers.emplace_back("stun:stun.ageneau.net:3478");
+	// TURN server example
+	// Please do not use outside of libdatachannel tests
+	config2.iceServers.emplace_back("turn:datachannel_test:[email protected]:3478");
+
+	auto pc2 = std::make_shared<PeerConnection>(config2);
+
+	pc1->onLocalDescription([wpc2 = make_weak_ptr(pc2)](Description sdp) {
+		auto pc2 = wpc2.lock();
+		if (!pc2)
+			return;
+		cout << "Description 1: " << sdp << endl;
+		pc2->setRemoteDescription(string(sdp));
+	});
+
+	pc1->onLocalCandidate([wpc2 = make_weak_ptr(pc2)](Candidate candidate) {
+		auto pc2 = wpc2.lock();
+		if (!pc2)
+			return;
+		// For this test, filter out non-relay candidates to force TURN
+		string str(candidate);
+		if(str.find("relay") != string::npos) {
+			cout << "Candidate 1: " << str << endl;
+			pc2->addRemoteCandidate(str);
+		}
+	});
+
+	pc1->onStateChange([](PeerConnection::State state) { cout << "State 1: " << state << endl; });
+
+	pc1->onGatheringStateChange([](PeerConnection::GatheringState state) {
+		cout << "Gathering state 1: " << state << endl;
+	});
+
+	pc1->onSignalingStateChange([](PeerConnection::SignalingState state) {
+		cout << "Signaling state 1: " << state << endl;
+	});
+
+	pc2->onLocalDescription([wpc1 = make_weak_ptr(pc1)](Description sdp) {
+		auto pc1 = wpc1.lock();
+		if (!pc1)
+			return;
+		cout << "Description 2: " << sdp << endl;
+		pc1->setRemoteDescription(string(sdp));
+	});
+
+	pc2->onLocalCandidate([wpc1 = make_weak_ptr(pc1)](Candidate candidate) {
+		auto pc1 = wpc1.lock();
+		if (!pc1)
+			return;
+		// For this test, filter out non-relay candidates to force TURN
+		string str(candidate);
+		if(str.find("relay") != string::npos) {
+			cout << "Candidate 1: " << str << endl;
+			pc1->addRemoteCandidate(str);
+		}
+	});
+
+	pc2->onStateChange([](PeerConnection::State state) { cout << "State 2: " << state << endl; });
+
+	pc2->onGatheringStateChange([](PeerConnection::GatheringState state) {
+		cout << "Gathering state 2: " << state << endl;
+	});
+
+	pc2->onSignalingStateChange([](PeerConnection::SignalingState state) {
+		cout << "Signaling state 2: " << state << endl;
+	});
+
+	shared_ptr<DataChannel> dc2;
+	pc2->onDataChannel([&dc2](shared_ptr<DataChannel> dc) {
+		cout << "DataChannel 2: Received with label \"" << dc->label() << "\"" << endl;
+		if (dc->label() != "test") {
+			cerr << "Wrong DataChannel label" << endl;
+			return;
+		}
+
+		dc->onMessage([](variant<binary, string> message) {
+			if (holds_alternative<string>(message)) {
+				cout << "Message 2: " << get<string>(message) << endl;
+			}
+		});
+
+		dc->send("Hello from 2");
+
+		std::atomic_store(&dc2, dc);
+	});
+
+	auto dc1 = pc1->createDataChannel("test");
+	dc1->onOpen([wdc1 = make_weak_ptr(dc1)]() {
+		auto dc1 = wdc1.lock();
+		if (!dc1)
+			return;
+
+		cout << "DataChannel 1: Open" << endl;
+		dc1->send("Hello from 1");
+	});
+	dc1->onMessage([](const variant<binary, string> &message) {
+		if (holds_alternative<string>(message)) {
+			cout << "Message 1: " << get<string>(message) << endl;
+		}
+	});
+
+	// Wait a bit
+	int attempts = 10;
+	shared_ptr<DataChannel> adc2;
+	while ((!(adc2 = std::atomic_load(&dc2)) || !adc2->isOpen() || !dc1->isOpen()) && attempts--)
+		this_thread::sleep_for(1s);
+
+	if (pc1->state() != PeerConnection::State::Connected &&
+	    pc2->state() != PeerConnection::State::Connected)
+		throw runtime_error("PeerConnection is not connected");
+
+	if (!adc2 || !adc2->isOpen() || !dc1->isOpen())
+		throw runtime_error("DataChannel is not open");
+
+	if (auto addr = pc1->localAddress())
+		cout << "Local address 1:  " << *addr << endl;
+	if (auto addr = pc1->remoteAddress())
+		cout << "Remote address 1: " << *addr << endl;
+	if (auto addr = pc2->localAddress())
+		cout << "Local address 2:  " << *addr << endl;
+	if (auto addr = pc2->remoteAddress())
+		cout << "Remote address 2: " << *addr << endl;
+
+	Candidate local, remote;
+	if (pc1->getSelectedCandidatePair(&local, &remote)) {
+		cout << "Local candidate 1:  " << local << endl;
+		cout << "Remote candidate 1: " << remote << endl;
+	}
+	if (pc2->getSelectedCandidatePair(&local, &remote)) {
+		cout << "Local candidate 2:  " << local << endl;
+		cout << "Remote candidate 2: " << remote << endl;
+	}
+
+	// Try to open a second data channel with another label
+	shared_ptr<DataChannel> second2;
+	pc2->onDataChannel([&second2](shared_ptr<DataChannel> dc) {
+		cout << "Second DataChannel 2: Received with label \"" << dc->label() << "\"" << endl;
+		if (dc->label() != "second") {
+			cerr << "Wrong second DataChannel label" << endl;
+			return;
+		}
+
+		dc->onMessage([](variant<binary, string> message) {
+			if (holds_alternative<string>(message)) {
+				cout << "Second Message 2: " << get<string>(message) << endl;
+			}
+		});
+
+		dc->send("Send hello from 2");
+
+		std::atomic_store(&second2, dc);
+	});
+
+	auto second1 = pc1->createDataChannel("second");
+	second1->onOpen([wsecond1 = make_weak_ptr(dc1)]() {
+		auto second1 = wsecond1.lock();
+		if (!second1)
+			return;
+
+		cout << "Second DataChannel 1: Open" << endl;
+		second1->send("Second hello from 1");
+	});
+	dc1->onMessage([](const variant<binary, string> &message) {
+		if (holds_alternative<string>(message)) {
+			cout << "Second Message 1: " << get<string>(message) << endl;
+		}
+	});
+
+	// Wait a bit
+	attempts = 10;
+	shared_ptr<DataChannel> asecond2;
+	while (
+	    (!(asecond2 = std::atomic_load(&second2)) || !asecond2->isOpen() || !second1->isOpen()) &&
+	    attempts--)
+		this_thread::sleep_for(1s);
+
+	if (!asecond2 || !asecond2->isOpen() || !second1->isOpen())
+		throw runtime_error("Second DataChannel is not open");
+
+	// Try to open a negotiated channel
+	DataChannelInit init;
+	init.negotiated = true;
+	init.id = 42;
+	auto negotiated1 = pc1->createDataChannel("negotiated", init);
+	auto negotiated2 = pc2->createDataChannel("negoctated", init);
+
+	if (!negotiated1->isOpen() || !negotiated2->isOpen())
+		throw runtime_error("Negociated DataChannel is not open");
+
+	std::atomic<bool> received = false;
+	negotiated2->onMessage([&received](const variant<binary, string> &message) {
+		if (holds_alternative<string>(message)) {
+			cout << "Second Message 2: " << get<string>(message) << endl;
+			received = true;
+		}
+	});
+
+	negotiated1->send("Hello from negotiated channel");
+
+	// Wait a bit
+	attempts = 5;
+	while (!received && attempts--)
+		this_thread::sleep_for(1s);
+
+	if (!received)
+		throw runtime_error("Negociated DataChannel failed");
+
+	// Delay close of peer 2 to check closing works properly
+	pc1->close();
+	this_thread::sleep_for(1s);
+	pc2->close();
+	this_thread::sleep_for(1s);
+
+	// You may call rtc::Cleanup() when finished to free static resources
+	rtc::Cleanup();
+	this_thread::sleep_for(1s);
+
+	cout << "Success" << endl;
+}