STUN协议与ICE
1、 什么是STUN协议
https://datatracker.ietf.org/doc/rfc5389/
STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。该协议由RFC 5389定义。
在WEBRTC中,STUN协议主要用于NAT穿越和认证消息(ICE的流程)
2、 什么是ICE
交互式连接建立是一种标准穿透协议,利用Stun和Turn服务器来帮助端点建立连接,ICE主要完成的就是NAT穿透和用户认证的过程
ICE的具体流程
在上图过程中,1.3.5步骤均采用STUN协议完成,可以联想到,STUN协议在其中主要有三个功能:
帮助获取主叫/被叫的外网地址
帮助完成主叫和被叫之间的权限认证
保活并处理地址变化情况
由上述三个功能,那么就能引出如何做到这三个功能的问题,那么就要从STUN协议的内容来了解
3、 STUN协议的内容
STUN协议基于TCP或者UDP,大部分是UDP,发送数据报格式为STUN HEADER+STUN BODY
STUN HEADER
字段解析:
前两位00,代表STUN版本
Message Length 代表整个报文长度
Magic Cookie 标识STUN协议,固定为0x2112A442
Transaction ID 标识固定的事务ID,根据这个可以判断STUN协议的来源
STUN Message Type:
格式如下:
消息类型许可的值如下:
0x0001:捆绑请求
0x0101:捆绑响应
0x0111:捆绑错误响应
其实主要就是看C0,C1的值
STUN body内容:
Body中可以加上众多的属性(STUN Attributes),结构如下:
具体Value为:
MAPPED-ADDRESS同时也是classic STUN的一个属性,之所以还存在也是为了前向兼容。其包含了NAT客户端的反射地址,
Family为IP类型,即IPV4(0x01)或IPV6(0x02),Port为端口,Address为32位或128位的IP地址。注意高8位必须全部置零,
而且接收端必须要将其忽略掉。
USERNAME 和PASSWORD,
这个内容里可以添加上交互的用户名和密码,随后对端进行校验
4、 ICE中P2P的实现
回到第2结那个图里,P2P的实现第一步,即和STUN服务器交互,获取到对应的外网IP,这里采用的就是MAPPED-ADRESS,此时SDP信息中就可以添加上主叫的内网IP和外网IP了(通过STUN服务器获取)
对端接收到这两个ip后,也回复自己的外网IP和内网IP,此时就可以进入打洞的阶段,打洞时A端先从SDP中选择所有对端ip发送建立连接,此时,如果对端收到后,也对A发来的所有SDP信息进行连接,那么此时如果A端收到了B的返回,那么就可以从中获取到对应的STUN协议里的ip地址,与之前发送地址比较,确认是哪个ip发送回来的,这个时候就可以建立keepalive连接,打洞就完成了,在这个过程中,STUN报文里可以带上SDP信息交换的username 和password,此时也可以完成用户的鉴权
对于流媒体服务器来讲,大多数本身就架设在外网,因此服务器来说,STUN服务的第一步就可以省略了,可从STUN检查的过程开始,那么其主要的目的也就是要完成ICE中的用户名和密码了确认了
MediaSoup中的STUN代码部分
创建webrtcTransport时会随机生成本机的username和password,随后创建udpSocket负责接受客户端发送过来的消息以及创建ICEserver来应对接受到数据后的stun数据处理工作,
此后libuv接收到数据后会将数据传递至webrtcTransport的OnStunDataReceived接口,然后调用iceServer->ProcessStunPacket,至此就把消息传递到了STUN处理接口里:
webrtcTransport构造函数代码:
WebRtcTransport::WebRtcTransport(const std::string& id, RTC::Transport::Listener* listener, json& data)
: RTC::Transport::Transport(id, listener, data)
{
//省略一部分代码
// This may throw.这里会创建接受服务器
auto* udpSocket = new RTC::UdpSocket(this, listenIp.ip);
// Create a ICE server.创建ICEserver负责处理stun
this->iceServer = new RTC::IceServer(
this, Utils::Crypto::GetRandomString(16), Utils::Crypto::GetRandomString(32));
// Create a DTLS transport.创建DTLS负责处理dtls握手
this->dtlsTransport = new RTC::DtlsTransport(this);
}
创建ICEusername和password代码:
void WebRtcTransport::HandleRequest(Channel::Request* request)
{
//随机生成password和username
std::string usernameFragment = Utils::Crypto::GetRandomString(16);
std::string password = Utils::Crypto::GetRandomString(32);
this->iceServer->SetUsernameFragment(usernameFragment);
this->iceServer->SetPassword(password);
}
mediasoup处理STUN协议只有BINDING REQUEST,主要代码如下:
void IceServer::ProcessStunPacket(RTC::StunPacket* packet, RTC::TransportTuple* tuple)
{
switch (packet->GetClass())
{
case RTC::StunPacket::Class::REQUEST:
{
switch (packet->CheckAuthentication(this->usernameFragment, this->password))
{
case RTC::StunPacket::Authentication::OK:
{
if (!this->oldPassword.empty())
{
MS_DEBUG_TAG(ice, "new ICE credentials applied");
this->oldUsernameFragment.clear();
this->oldPassword.clear();
}
break;
}
// Create a success response.
RTC::StunPacket* response = packet->CreateSuccessResponse();
// Add XOR-MAPPED-ADDRESS.
response->SetXorMappedAddress(tuple->GetRemoteAddress());
// Authenticate the response.
if (this->oldPassword.empty())
response->Authenticate(this->password);
else
response->Authenticate(this->oldPassword);
// Send back.
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
// Handle the tuple.
HandleTuple(tuple, packet->HasUseCandidate());
break;
}
}
}
从上代码中可以看到,服务器在接受到bunding request之后认证了username和password,完成之后发送XOR-MAPADDRESS并带上了客户端的外网地址。可以看出来media’soup采用的时ice-lite,只能部署在公网环境下,并没有实现对STUN协议的完整使用,在STUN上只是用来进行身份校验。