网络
-
STUN协议
NAT墙 完全锥形 IP限制型锥形 端口限制型锥形 对称型 NAT穿透 STUN协议简介 STUN(Session Traversal Utilities for NAT)是一个用于帮助客户端发现其所在NAT之后的公网IP地址和端口号。通信的双方都能知道所在的公网IP,并且能够让公网的NAT帮助转发,从而实现NAT穿透。STUN协议广泛应用在WebRTC实时通信系统中。STUN交互过程主要有以下几个步骤: 客户端向 STUN 服务器发送请求。 STUN 服务器处理并响应。 客户端接收响应并解析公共IP和端口。 客户端根据收到的公共 IP 和端口进行 NAT 穿越和路径选择。 绑定请求与响应 客户端发STUN请求 客户端发送STUN请求,包的类型为Binding Request。请求的目标是让STUN 服务器返回客户端的公网IP地址和端口。 STUN Message Header: - Message Type: 0x0001 (Binding Request) //类型 - Message Length: 0x0000 - Magic Cookie: 0x2112A442 //固定值,用于标识STUN - Transaction ID: 0x63f96d584f90a66d18b68202 //用于唯一标识该请求的ID,匹配响应 STUN Attributes (Optional): //以下是附加信息 - Username: <username> - Integrity: <HMAC-SHA1 signature> - Fingerprint: <CRC-32C of the message> STUN服务端响应 STUN服务器收到客户端的请求后,会放binding response回复,内容中携带了客户端的公网IP和端口号。 STUN Message Header: - Message Type: 0x0101 (Binding Response) //表明是一个 Binding Response - Message Length: 0x000C - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a66d18b68202 STUN Attributes: - Mapped Address: Family: IPv4 Address: 203.0.113.1 //公网的IP Port: 3478 //公网的端口 - Username: <username> - Integrity: <HMAC-SHA1 signature> - Fingerprint: <CRC-32C of the message> 连接检查 以webrtc举例,当需要通信的双方,通过信令交互SDP(在libpeer文章中,是通过mqtt)中交互的双方各自的公网IP地址信息后,可以使用STUN的binding request和binding response检查P2P的连通性,以确保P2P链路可达。 首先一般由一端率先发起binding request。 STUN Message Header: - Message Type: 0x0001 (Binding Request) - Message Length: 0x0008 - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a32d18b68202 STUN Attributes: - Username: <peer-username> - Message Integrity: <HMAC-SHA1 signature> 接着等待对端回复binding response STUN Message Header: - Message Type: 0x0101 (Binding Response) - Message Length: 0x0008 - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a32d18b68202 STUN Attributes: - Mapped Address: Family: IPv4 Address: 203.0.113.10 Port: 3479 当发起方接受到binding response后,表明连通性正常,下面是代码的流程示例。 agent_connectivity_check() { /*1. 发送 binding request*/ agent_create_binding_request(agent, &msg) stun_msg_create(msg, STUN_CLASS_REQUEST | STUN_METHOD_BINDING); stun_msg_write_attr(msg, STUN_ATTR_TYPE_USERNAME, strlen(username), username); stun_msg_write_attr(msg, STUN_ATTR_TYPE_PRIORITY, 4, (char*)&agent->nominated_pair->priority); stun_msg_finish(msg, STUN_CREDENTIAL_SHORT_TERM, agent->remote_upwd, strlen(agent->remote_upwd)); agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size);//发送,这里目标的ip和端口就是对端的ip地址和端口号。 /*2. 等待接受*/ agent_recv switch (stun_msg.stunclass) { /*收到对端发送的binding request,构建一个response发送回去。*/ case STUN_CLASS_REQUEST: agent_process_stun_request agent_create_binding_response stun_set_mapped_address agent_socket_send /*收到对端回复的binding response,将state设置为success*/ case STUN_CLASS_RESPONSE: agent_process_stun_response agent->nominated_pair->state = ICE_CANDIDATE_STATE_SUCCEEDED; } } 实践流程分析 本次实验使用的stun服务器是,有两个设备,设备A是一块开发板,设备B是电脑,分别连接到同一个路由器上。 Resolved stun.l.google.com -> 74.125.250.129, Resolved stun/turn server 74.125.250.129:19302 设备A(开发板)的IP地址信息是 inet addr:192.168.51.127 设备B(电脑)的IP地址信息是 192.168.51.227 设备发送向STUN服务端发送请求 开发板发送binding request 这里是设备向stun.l.google.com:74.125.250.129:19302服务发送binding request请求。 电脑发送binding request 设备的电脑一下发了3个binding request,其中一个是发送给google stun服务器的,但是另外两个实际上是发给设备A的本地IP和NAT墙的IP,可能是此前连接过,浏览器缓存了信息。 服务端回复 设备A收到回复 stun.l.google.com回复了binding success response。并表明了设备的公网IP为120.236.240.177,其端口为8139。 电脑B收到回复 通过message transaction ID可以看到电脑收到了Google STUN回复的binding success response,告知其NAT地址为120.236.240.177,端口号是8140。因为连接的是同一个路由器,所以NAT是一样的。同时板子A设备192.168.51.127也对齐回复了binding success response,告知其地址为192.168.51.227:59777。 板子A请求检查 板子A发送了一个binding request,对端回复了一个binding success response,则表示联通已建立。 TURN协议 TURN协议是在STUN的基础上增加了中继功能,解决的是对称型NAT无法穿透的问题。如下图的结构体,需要注意的是,使用TURN作为中继转发,传输的设备一方式TURN Client,另一方是Peer A。即TURN Client需要与TURN Server建立TURN协议交互,而Peer A与TURN Server只需要做STUN协议交互。此前调试了两个通信设备,两端都是使用TURN协议发起连接,导致一端checking失败,最后将一端修改为STUN协议交互即可。 Peer A Server-Reflexive +---------+ Transport Address | | 192.0.2.150:32102 | | | /| | TURN | / ^| Peer A | Client’s Server | / || | Host Transport Transport | // || | Address Address | // |+---------+ 10.1.1.2:49721 192.0.2.15:3478 |+-+ // Peer A | | ||N| / Host Transport | +-+ | ||A|/ Address | | | | v|T| 192.168.100.2:49582 | | | | /+-+ +---------+| | | |+---------+ / +---------+ | || |N| || | // | | | TURN |v | | v| TURN |/ | | | Client |----|A|----------| Server |------------------| Peer B | | | | |^ | |^ ^| | | | |T|| | || || | +---------+ | || +---------+| |+---------+ | || | | | || | | +-+| | | | | | | | | Client’s | Peer B Server-Reflexive Relayed Transport Transport Address Transport Address Address 192.0.2.1:7000 192.0.2.15:50000 192.0.2.210:49191 Allocate 请求服务器,分配一个中继服务。中继建立连接后,客户端需要进行保活,定期发送refresh request给服务器端。 TURN TURN Peer Peer client server A B |-- Allocate request --------------->| | | | | | | |<--------------- Allocate failure --| | | | (401 Unauthorized) | | | | | | | |-- Allocate request --------------->| | | | | | | |<---------- Allocate success resp --| | | | (192.0.2.15:50000) | | | / / / / | | | | |-- Refresh request ---------------->| | | | | | | |<----------- Refresh success resp --| | | | | | | 如上示例,客户端先发送不带验证信息的Allocate请求,此时STUN服务器会返回error response,客户端收到错误后加上验证信息再次请求。以IPC通信为例,摄像头端是Peer A(资源有限,不支持证书请求等协议),发起的是STUN协议,而手机预览端是TURN Client,发起的是TURN交互协议。 Send机制 客户端和peer直接通信可以通过TURN server进行转发信息,主要有两种方式,第一种是使用Send/data方式,第二种是使用channels方式。其主要目的是通过某种方式告知服务器,从client接收到的数据应该发给那个peer。下面是Send/data机制。 TURN Server在进行转发前,需要client先安装一个到对等端的许可(permission),许可证可以通过CreatePermission request/resp来进行交互。 TURN TURN Peer Peer client server A B | | | | |-- CreatePermission req (Peer A) -->| | | |<-- CreatePermission success resp --| | | | | | | |--- Send ind (Peer A)-------------->| | | | |=== data ===>| | | | | | | |<== data ====| | |<-------------- Data ind (Peer A) --| | | | | | | | | | | |--- Send ind (Peer B)-------------->| | | | | dropped | | | | | | | |<== data ==================| | dropped | | | | | | | Chanel机制 等同于send/data机制。 TURN TURN Peer Peer client server A B | | | | |-- ChannelBind req ---------------->| | | | (Peer A to 0x4001) | | | | | | | |<---------- ChannelBind succ resp --| | | | | | | |-- [0x4001] data ------------------>| | | | |=== data ===>| | | | | | | |<== data ====| | |<------------------ [0x4001] data --| | | | | | | |--- Send ind (Peer A)-------------->| | | | |=== data ===>| | | | | | | |<== data ====| | |<------------------ [0x4001] data --| | | | | | | 参考: https://mp.weixin.qq.com/s/XMhSDABc74dpALIHrPPt7w https://www.ctyun.cn/developer/article/586260405821509 https://www.cnblogs.com/pannengzhi/p/5048965.html -
libpeer分析
关键数据结构 PeerConfiguration typedef struct PeerConfiguration { IceServer ice_servers[5]; MediaCodec audio_codec; MediaCodec video_codec; DataChannelType datachannel; void (*onaudiotrack)(uint8_t* data, size_t size, void* userdata); void (*onvideotrack)(uint8_t* data, size_t size, void* userdata); void (*on_request_keyframe)(void* userdata); void* user_data; } PeerConfiguration; 实例: PeerConfiguration config = { .ice_servers = { {.urls = "stun:stun.l.google.com:19302"}, }, .datachannel = DATA_CHANNEL_STRING, .video_codec = CODEC_H264, .audio_codec = CODEC_PCMA}; Agent Agent 结构体,主要用于实现 WebRTC 或类似协议中的 ICE (Interactive Connectivity Establishment) 机制。ICE 是一种 NAT 穿越协议,用于在点对点通信中建立连接并选择最佳的网络路径。Agent 结构体存储了与 ICE 协议操作相关的各种数据,用于管理连接的候选地址、协商过程以及状态。 struct Agent { char remote_ufrag[ICE_UFRAG_LENGTH + 1]; char remote_upwd[ICE_UPWD_LENGTH + 1]; char local_ufrag[ICE_UFRAG_LENGTH + 1]; char local_upwd[ICE_UPWD_LENGTH + 1]; IceCandidate local_candidates[AGENT_MAX_CANDIDATES]; IceCandidate remote_candidates[AGENT_MAX_CANDIDATES]; int local_candidates_count; int remote_candidates_count; UdpSocket udp_sockets[2]; Address host_addr; int b_host_addr; uint64_t binding_request_time; AgentState state; AgentMode mode; IceCandidatePair candidate_pairs[AGENT_MAX_CANDIDATE_PAIRS]; IceCandidatePair* selected_pair; IceCandidatePair* nominated_pair; int candidate_pairs_num; int use_candidate; uint32_t transaction_id[3]; }; ServiceConfiguration typedef struct ServiceConfiguration { const char* mqtt_url; int mqtt_port; const char* client_id; const char* http_url; int http_port; const char* username; const char* password; PeerConnection* pc; } ServiceConfiguration; 实例 #define SERVICE_CONFIG_DEFAULT() \ { \ .mqtt_url = "broker.emqx.io", \ //访问的MQTT服务器 .mqtt_port = 8883, \ //访问的MQTT端口 .client_id = "peer", \ //用于订阅的主题 webrtc/peer/jsonrpc //发布的主题 webrtc/peer/jsonrpc-reply .http_url = "", \ //信令使用的是MQTT,没有使用http, .http_port = 443, \ //因此HTTP的没有用。 .username = "", \ .password = "", \ .pc = NULL \ } PeerConnection struct PeerConnection { PeerConfiguration config; PeerConnectionState state; Agent agent; DtlsSrtp dtls_srtp; Sctp sctp; Sdp local_sdp; Sdp remote_sdp; void (*onicecandidate)(char* sdp, void* user_data); void (*oniceconnectionstatechange)(PeerConnectionState state, void* user_data); void (*on_connected)(void* userdata); void (*on_receiver_packet_loss)(float fraction_loss, uint32_t total_loss, void* user_data); uint8_t temp_buf[CONFIG_MTU]; uint8_t agent_buf[CONFIG_MTU]; int agent_ret; int b_local_description_created; Buffer* audio_rb; Buffer* video_rb; Buffer* data_rb; RtpEncoder artp_encoder; RtpEncoder vrtp_encoder; RtpDecoder vrtp_decoder; RtpDecoder artp_decoder; uint32_t remote_assrc; uint32_t remote_vssrc; }; WebRTC简介 概览 上图中一共有4中角色,分别是signaling server、STUN/RURN server、Client A,Client B; signaling server:信令指的是管理两个通信设备A和B建立和管理点对点连接过程中的控制消息交换机制。让通信双方能够交换各种信息,从而建立、维护和终止一个点对点的实时通信连接。 STUN/RURN server:用于设备A和设备B传过NAT。 Client A:通信设备A Client B:通信设备B 通信流程 WebRTC中客户端与信令服务器、STUN/TURN服务器的交互流程如下: ClientA首先创建PeerConnection对象,然后打开本地音视频设备,将音视频数据封装成MediaStream添加到PeerConnection中。 ClientA调用PeerConnection的CreateOffer方法创建一个用于offer的SDP对象,SDP对象中保存当前音视频的相关参数。ClientA通过PeerConnection的SetLocalDescription方法将该SDP对象保存起来,并通过Signal服务器发送给ClientB。 ClientB接收到ClientA发送过的offer SDP对象,通过PeerConnection的SetRemoteDescription方法将其保存起来,并调用PeerConnection的CreateAnswer方法创建一个应答的SDP对象,通过PeerConnection的SetLocalDescription的方法保存该应答SDP对象并将它通过Signal服务器发送给ClientA。 ClientA接收到ClientB发送过来的应答SDP对象,将其通过PeerConnection的SetRemoteDescription方法保存起来。 在SDP信息的offer/answer流程中,ClientA和ClientB已经根据SDP信息创建好相应的音频Channel和视频Channel并开启Candidate数据的收集,Candidate数据可以简单地理解成Client端的IP地址信息(本地IP地址、公网IP地址、Relay服务端分配的地址)。 当ClientA收集到Candidate信息后,PeerConnection会通过OnIceCandidate接口给ClientA发送通知,ClientA将收到的Candidate信息通过Signal服务器发送给ClientB,ClientB通过PeerConnection的AddIceCandidate方法保存起来。同样的操作ClientB对ClientA再来一次。 这样ClientA和ClientB就已经建立了音视频传输的P2P通道,ClientB接收到ClientA传送过来的音视频流,会通过PeerConnection的OnAddStream回调接口返回一个标识ClientA端音视频流的MediaStream对象,在ClientB端渲染出来即可。同样操作也适应ClientB到ClientA的音视频流的传输。 上述序列中,WebRTC并不提供Stun服务器和Signal服务器,服务器端需要自己实现。Stun服务器可以用google提供的实现stun协议的测试服务器(stun:stun.l.google.com:19302),Signal服务器则完全需要自己实现了,可以使用MQTT的broker.emqx.io来作为信令服务器。它需要在ClientA和ClientB之间传送彼此的SDP信息和candidate信息,ClientA和ClientB通过这些信息建立P2P连接来传送音视频数据。 信令 待补充 SDP SDP是一个比较老的协议,发布于2006年,以type=value的格式描述回话内容。WebRTC引入SDP来描述媒体信息,用于媒体协商时决定双方是否可以进行通信。当对端设备anser的时候会携带一个SDP格式的内容,如下。 v=0 o=- 8646007345366799659 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE video audio datachannel a=msid-semantic: WMS 5a162afd-e195-4207-a699-e977bb510327 m=video 10773 UDP/TLS/RTP/SAVPF 96 102 c=IN IP4 14.29.67.186 a=rtcp:9 IN IP4 0.0.0.0 a=candidate:1969734079 1 udp 2122260223 192.168.31.123 65492 typ host generation 0 network-id 1 network-cost 10 a=candidate:2345473323 1 tcp 1518280447 192.168.31.123 9 typ host tcptype active generation 0 network-id 1 network-cost 10 a=candidate:3819939686 1 udp 1686052607 14.29.67.186 10773 typ srflx raddr 192.168.31.123 rport 65492 generation 0 network-id 1 network-cost 10 a=ice-ufrag:JgBs a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 a=setup:active a=mid:video a=recvonly a=rtcp-mux a=rtpmap:96 H264/90000 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=rtpmap:102 H264/90000 a=rtcp-fb:102 nack a=rtcp-fb:102 nack pli a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f m=audio 9 UDP/TLS/RTP/SAVP 8 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:JgBs //对端用户名 a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v //对端的密码 a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 //指纹信息 a=setup:active a=mid:audio a=sendrecv a=msid:5a162afd-e195-4207-a699-e977bb510327 693effe8-ba8d-469d-ba6a-f58e2b5f73df a=rtcp-mux a=rtpmap:8 PCMA/8000 a=ssrc:1668486723 cname:o+PtcUckwEIRvBPp m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-ufrag:JgBs a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 a=setup:active a=mid:datachannel a=sctp-port:5000 a=max-message-size:262144 ICE candidates ICE (Interactive Connectivity Establishment 互动连接建立) candidates(候选人)简称ICE candidates,WebRTC中的ICE Candidate是用来描述可以连接的远端的基本信息,什么是candidate,两台设备的连接,需要知道设备的网络信息,如IP地址,端口号以及使用的协议,因此candidate至少包含{address,port,protocol}三元组信息集。通过SDP会话来交互candidate信息。 WebRTC将Candidate分成四种类型,且类型间存在优先级次序,从高到低分别为host、srflx、prflx和relay,本章节使用的是srflx类型,从STUN服务器获取的地址。 host:从本机网卡上获取到的地址,一般来说,一个网卡对应一个地址。 srflx(server reflexive):从STUN服务器获取到的地址。 relay:从TRUN服务器获取到的地址。 prflx(peer reflexive):在交互过程中从对端数据报文中获取到的地址。 其中,srflx和prflx地址可能是一样的,但获取的途径不一样,下面是描述ICE candidates的数据结构。 typedef enum IceCandidateType { ICE_CANDIDATE_TYPE_HOST, ICE_CANDIDATE_TYPE_SRFLX, ICE_CANDIDATE_TYPE_PRFLX, ICE_CANDIDATE_TYPE_RELAY, } IceCandidateType; typedef struct IceCandidate IceCandidate; struct IceCandidate { int foundation; int component; //1:RTP,2:RTCP uint32_t priority;//优先级 char transport[32 + 1]; //传输协议,基于UDP IceCandidateType type; //类型,如上。 IceCandidateState state; Address addr; Address raddr; }; main函数分析 /*1. 调用srtp_init,初始化srtp内核模块加密套件、debug等*/ peer_init(); /*2. 创建一个peer连接,其中创建了agent,打开了一个udp socket,初始化 了rtp 音频、视频编解码,主要是确定用什么编码格式。*/ g_pc = peer_connection_create(&config); /*3. 注册一个peer连接的状态切换回调函数,回调函数 onconnectionstatechange, 用于处理对等连接的 ICE 连接状态变更。当连接的状态发生变化(例如从new到connected, 或者 failed),这个回调函数将被调用。*/ peer_connection_oniceconnectionstatechange(g_pc, onconnectionstatechange); /*4. 设置数据通道(DataChannel)相关的回调函数。当数据通道打开时,会触发 onopen;接收到消息时,会触发 onmessage;当数据通道关闭时会触发onclose*/ peer_connection_ondatachannel(g_pc, onmessage, onopen, onclose); /*5. 设置信令服务器的配置,通常用于 WebRTC 会话中的信令部分。通过传入 service_config 配置,WebRTC 客户端可以向信令服务器注册或传递配置信息 (如 client_id 和对等连接句柄 pc) 这里的信令服务器用的是公用的mqtt服务器? broker.emqx.io 使用mqtt来作为信令通信,没有使用http,在里面会注册一个peer_connect的候选回调 */ /*什么是信令 在webrtc中,信令指的是建立和管理点对点连接过程中的控制消息交换机制。让通信双方 能够交换各种信息,从而建立、维护和终止一个点对点的实时通信连接。 WebRTC 的核心目标是实现浏览器之间的直接通信,支持音频、视频和数据传输, 但在建立这种连接之前,双方需要交换一些控制信息,包括: - 会话描述(如 SDP,Session Description Protocol):描述连接的媒体设置(如视频编码格式、音频参数等)。 - 网络候选(ICE候选,ICE candidates):用于寻找最佳的点对点网络路径,以确保即使在复杂的网络环境下也能建立连接。Interactive Connectivity Establishment(互动连接建立) - 连接状态:例如连接是否已建立、是否关闭等。 */ /*信令的实现 虽然 WebRTC 协议本身并没有指定信令的实现方式,但是它提供了用于交换数据的 API。 开发者可以选择适合自己的信令协议和传输方式,通常的实现方式包括: - WebSocket:一个常见的双向通信协议,可以实时地交换信令数据。 - HTTP 请求:通过轮询、长轮询等方式进行信令交换。 - SIP (Session Initiation Protocol):一些应用可能采用 SIP 作为信令协议。 - XMPP (Extensible Messaging and Presence Protocol):也是一种可以用来实现信令的协议,尤其适用于即时消息和通信应用。 - MQTT:通过订阅和发布的方式来进行传输,本文就是使用这种方式。 */ /*信令的工作流程 假设有两个用户 A 和 B 通过 WebRTC 建立视频通话,信令的过程大致如下: (1) 用户A生成offer:用 A创建一个 RTCPeerConnection 对象,并 生成一个offer。这个 offer 包含了 A 的媒体设置和网络候选信息(ICE 候选)。 A 将这个 offer 通过信令系统(例如 MQTT)发送给用户 B。 (2)用户B接收到 offer,并生成 answer:用户 B 收到 offer 后,使用它来创 建一个 RTCPeerConnection,并生成一个 answer(应答)。这个 answer 包含 B 的媒体配置和 ICE 候选信息。B 将 answer 通过信令系统发送给用户 A。 (3)交换 ICE 候选:在连接过程中,双方都会收集到 ICE 候选并通过信令进行交换, 直到双方都确定了最佳的网络路径。 (4)建立连接:当双方完成了 ICE 候选交换并且建立了连接时,通信就可以开始了。 (5)连接终止:当会话结束时,双方通过信令通知对方关闭连接。*/ service_config.client_id = argv[1]; service_config.pc = g_pc; peer_signaling_set_config(&service_config); /*6.初始化MQTT,连接broker.emqx.io, */ peer_signaling_join_channel(); /*7. 创建peer连接的task*/ pthread_create(&peer_connection_thread, NULL, peer_connection_task, NULL); /*8. 创建signal的task*/ pthread_create(&peer_singaling_thread, NULL, peer_singaling_task, NULL); /*9. 读取本地音视频初始化*/ reader_init(); /*10. 循环判断PEER的状态是否是连接态,如果是已经连接,则发送音视频数据*/ while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); // FPS 25 if (curr_time - video_time > 40) { video_time = curr_time; if (reader_get_video_frame(buf, &size) == 0) { peer_connection_send_video(g_pc, buf, size); } } if (curr_time - audio_time > 20) { if (reader_get_audio_frame(buf, &size) == 0) { peer_connection_send_audio(g_pc, buf, size); } audio_time = curr_time; } usleep(1000); } } 信令交互 int peer_signaling_join_channel() { /*1. 与信令服务器建立连接,实际上就是连接MQTT的broker*/ if (peer_signaling_mqtt_connect(g_ps.mqtt_host, g_ps.mqtt_port) < 0) { LOGW("Connect MQTT server failed"); return -1; } /*1. 发布信令的消息*/ peer_signaling_mqtt_subscribe(1); return 0; } 与信令服务器建立连接 因为与MQTT broker.emqx.io的交互是TLS加解密的,因此先使用ssl_transport_connect建立加解密通道,接着对MQTT进行初始化,将ssl的收发函数注册到MQTT中,这样MQTT后续的就可以用SSL的加解密通道进行通信,最后是发起MQTT连接。 static int peer_signaling_mqtt_connect(const char* hostname, int port) { MQTTStatus_t status; MQTTConnectInfo_t conn_info; bool session_present; /*1. 与mqtt的broker,其host="broker.emqx.io",port=8883建立TLS连接,MQTT的收发消息 是否通过加密的方式,因此需要先使用TLS进行连接*/ if (ssl_transport_connect(&g_ps.net_ctx, hostname, port, NULL) < 0) { LOGE("ssl transport connect failed"); return -1; } /*2. ssl建立连接之后,将发送和接收的函数通过MQTT_Init进行注册,后续其收发就会调 调用ssl_transport_recv和ssl_transport_send进行解加密收发。 MQTT_Init初始化时还注册回调函数, 对CONNACK/PUBLISH/SUBACK事件进处理 需要注意的时,当设备收到broker发过来的PUBLISH事件时,会回调这个函数进行 处理,这样的场景是当对端设备访问连接是,就会调用peer_signaling_mqtt_event_cb。*/ g_ps.transport.recv = ssl_transport_recv; g_ps.transport.send = ssl_transport_send; g_ps.transport.pNetworkContext = &g_ps.net_ctx; g_ps.mqtt_fixed_buf.pBuffer = g_ps.mqtt_buf; g_ps.mqtt_fixed_buf.size = sizeof(g_ps.mqtt_buf); status = MQTT_Init(&g_ps.mqtt_ctx, &g_ps.transport, ports_get_epoch_time, peer_signaling_mqtt_event_cb, &g_ps.mqtt_fixed_buf); memset(&conn_info, 0, sizeof(conn_info)); conn_info.cleanSession = false; if (strlen(g_ps.username) > 0) { conn_info.pUserName = g_ps.username; conn_info.userNameLength = strlen(g_ps.username); } if (strlen(g_ps.password) > 0) { conn_info.pPassword = g_ps.password; conn_info.passwordLength = strlen(g_ps.password); } if (strlen(g_ps.client_id) > 0) { conn_info.pClientIdentifier = g_ps.client_id; conn_info.clientIdentifierLength = strlen(g_ps.client_id); } conn_info.keepAliveSeconds = KEEP_ALIVE_TIMEOUT_SECONDS; /*3.建立MQTT的,MQTT协议与broker连接。前面步骤1是建立SSL连接,相当于是建立了加解密通道, 而这里是建立MQTT的协议通道。*/ status = MQTT_Connect(&g_ps.mqtt_ctx, &conn_info, NULL, CONNACK_RECV_TIMEOUT_MS, &session_present); if (status != MQTTSuccess) { LOGE("MQTT_Connect failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } LOGI("MQTT_Connect succeeded."); return 0; } 通知信令服务器已准备 通知服务器已经准备,主要是通过MQTT的订阅主题为\"webrtc/peer/jsonrpc\",等待接入设备接入。 static int peer_signaling_mqtt_subscribe(int subscribed) { MQTTStatus_t status = MQTTSuccess; MQTTSubscribeInfo_t sub_info; /*1. 获取MQTT的packet id*/ uint16_t packet_id = MQTT_GetPacketId(&g_ps.mqtt_ctx);、 /*2. MQTT QOS为0即只发一次,订阅的主题是"webrtc/peer/jsonrpc" */ memset(&sub_info, 0, sizeof(sub_info)); sub_info.qos = MQTTQoS0; sub_info.pTopicFilter = g_ps.subtopic; sub_info.topicFilterLength = strlen(g_ps.subtopic); /*3. 发送订阅消息*/ if (subscribed) { status = MQTT_Subscribe(&g_ps.mqtt_ctx, &sub_info, 1, packet_id); } else { status = MQTT_Unsubscribe(&g_ps.mqtt_ctx, &sub_info, 1, packet_id); } if (status != MQTTSuccess) { LOGE("MQTT_Subscribe failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } /*4. 等待MQTT 订阅消息发送成功*/ status = MQTT_ProcessLoop(&g_ps.mqtt_ctx); /* 调用该函数实时处理循环接收数据,并自定发送心跳包保持链接,当有数据到来时,会触发myEventCallback回调函数*/ if (status != MQTTSuccess) { LOGE("MQTT_ProcessLoop failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } LOGD("MQTT Subscribe/Unsubscribe succeeded."); return 0; } 接收信令服务器事件处理 在peer_signaling_mqtt_connect函数中,调用MQTT_Init初始化时,注册了一个peer_signaling_mqtt_event_cb回调函数,其对接收的MQTT消息进行处理,包括收到了Broker发过来的PUBLISH消息,因此当设备接入时,调用的是peer_signaling_mqtt_event_cb函数。 static void peer_signaling_mqtt_event_cb(MQTTContext_t* mqtt_ctx, MQTTPacketInfo_t* packet_info, MQTTDeserializedInfo_t* deserialized_info) { switch (packet_info->type) { case MQTT_PACKET_TYPE_CONNACK: LOGI("MQTT_PACKET_TYPE_CONNACK"); break; case MQTT_PACKET_TYPE_PUBLISH: LOGI("MQTT_PACKET_TYPE_PUBLISH"); /*收到了broker发送过来的PUBLISH消息*/ peer_signaling_on_pub_event(deserialized_info->pPublishInfo->pPayload, deserialized_info->pPublishInfo->payloadLength); break; case MQTT_PACKET_TYPE_SUBACK: LOGD("MQTT_PACKET_TYPE_SUBACK"); break; default: break; } } static void peer_signaling_on_pub_event(const char* msg, size_t size) { cJSON *req, *res, *item, *result, *error; int id = -1; char* payload = NULL; PeerConnectionState state; req = res = item = result = error = NULL; /*1. 先获取peer connect的状态*/ state = peer_connection_get_state(g_ps.pc); printf("%s,%d, msg:%s\n",__func__,__LINE__,msg); do { /*2. 解析json文件*/ req = cJSON_Parse(msg); if (!req) { error = cJSON_CreateRaw(RPC_ERROR_PARSE_ERROR); LOGW("Parse json failed"); break; } /*3. 解析jsion中字段的id值*/ item = cJSON_GetObjectItem(req, "id"); if (!item && !cJSON_IsNumber(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_REQUEST); LOGW("Cannot find id"); break; } id = item->valueint; /*4. 解析jison中的method字段是是什么?*/ item = cJSON_GetObjectItem(req, "method"); if (!item && cJSON_IsString(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_REQUEST); LOGW("Cannot find method"); break; } /* 5. 解析json文件,发现metho为offer,broker发过来要求offer,表示有设备加入了。*/ /* {"jsonrpc":"2.0","method":"offer","id":89} */ if (strcmp(item->valuestring, RPC_METHOD_OFFER) == 0) { switch (state) { case PEER_CONNECTION_NEW: case PEER_CONNECTION_DISCONNECTED: case PEER_CONNECTION_FAILED: case PEER_CONNECTION_CLOSED: { g_ps.id = id; /*6. 将peer connect状态切换为PEER_CONNECTION_NEW 在peer_connection_loop中就会调用到peer_connection_state_new, offer一个SDP*/ peer_connection_create_offer(g_ps.pc); } break; default: { error = cJSON_CreateRaw(RPC_ERROR_INTERNAL_ERROR); } break; } /*7. broker发送过来的是anwer,表示对端设备的应答*/ } else if (strcmp(item->valuestring, RPC_METHOD_ANSWER) == 0) { item = cJSON_GetObjectItem(req, "params"); if (!item && !cJSON_IsString(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_PARAMS); LOGW("Cannot find params"); break; } /*8. 收到对端应答后,解析对端anser的SDP内容,设置远端的描述信息*/ if (state == PEER_CONNECTION_NEW) { peer_connection_set_remote_description(g_ps.pc, item->valuestring); result = cJSON_CreateString(""); } } else if (strcmp(item->valuestring, RPC_METHOD_STATE) == 0) { result = cJSON_CreateString(peer_connection_state_to_string(state)); } else if (strcmp(item->valuestring, RPC_METHOD_CLOSE) == 0) { peer_connection_close(g_ps.pc); result = cJSON_CreateString(""); } else { error = cJSON_CreateRaw(RPC_ERROR_METHOD_NOT_FOUND); LOGW("Unsupport method"); } } while (0); /*9. 发布消息,发布什么内容??*/ if (result || error) { res = cJSON_CreateObject(); cJSON_AddStringToObject(res, "jsonrpc", RPC_VERSION); cJSON_AddNumberToObject(res, "id", id); if (result) { cJSON_AddItemToObject(res, "result", result); } else if (error) { cJSON_AddItemToObject(res, "error", error); } payload = cJSON_PrintUnformatted(res); if (payload) { peer_signaling_mqtt_publish(&g_ps.mqtt_ctx, payload); free(payload); } cJSON_Delete(res); } if (req) { cJSON_Delete(req); } } Offer SDP 当对端Client B设备通过信令服务器发起连接时,信令服务器的MQTT broker发送publish给当前Client A设备,接收到MQTT 的publish后,解析到method为offer,即将peer的状态切换为PEER_CONNECTION_NEW,继而peer_connection_loop中状态切换为peer_connection_state_new。 static void peer_connection_state_new(PeerConnection* pc, DtlsSrtpRole role, int isOfferer) { char* description = (char*)pc->temp_buf; memset(pc->temp_buf, 0, sizeof(pc->temp_buf)); dtls_srtp_reset_session(&pc->dtls_srtp); /*dtls srtp 初始化,在后续的章节再进行描述。*/ dtls_srtp_init(&pc->dtls_srtp, role, pc); pc->dtls_srtp.udp_recv = peer_connection_dtls_srtp_recv; pc->dtls_srtp.udp_send = peer_connection_dtls_srtp_send; pc->sctp.connected = 0; if (isOfferer) { agent_clear_candidates(&pc->agent); pc->agent.mode = AGENT_MODE_CONTROLLING; } else { pc->agent.mode = AGENT_MODE_CONTROLLED; } /*1. 通过stun传输获取candidate信息*/ agent_gather_candidate(&pc->agent, NULL, NULL, NULL); // host address for (int i = 0; i < sizeof(pc->config.ice_servers) / sizeof(pc->config.ice_servers[0]); ++i) { if (pc->config.ice_servers[i].urls) { LOGI("ice server: %s", pc->config.ice_servers[i].urls); agent_gather_candidate(&pc->agent, pc->config.ice_servers[i].urls, pc->config.ice_servers[i].username, pc->config.ice_servers[i].credential); } } /*将candidate信息转化为SDP格式?*/ agent_get_local_description(&pc->agent, description, sizeof(pc->temp_buf)); memset(&pc->local_sdp, 0, sizeof(pc->local_sdp)); // TODO: check if we have video or audio codecs /*创建一个SDP会话*/ sdp_create(&pc->local_sdp, pc->config.video_codec != CODEC_NONE, pc->config.audio_codec != CODEC_NONE, pc->config.datachannel); /*填充其他的sdp信息*/ if (pc->config.video_codec == CODEC_H264) { sdp_append_h264(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); } switch (pc->config.audio_codec) { case CODEC_PCMA: sdp_append_pcma(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); break; case CODEC_PCMU: sdp_append_pcmu(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); break; case CODEC_OPUS: sdp_append_opus(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); default: break; } if (pc->config.datachannel) { sdp_append_datachannel(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); } pc->b_local_description_created = 1; /*将SDP信息发送出去。*/ if (pc->onicecandidate) { pc->onicecandidate(pc->local_sdp.content, pc->config.user_data); } } anser SDP peer_signaling_on_pub_event strcmp(item->valuestring, RPC_METHOD_ANSWER peer_connection_set_remote_description(g_ps.pc, item->valuestring); 设备收到publish消息后,解析jsion判断出method是anser,调用peer_connection_set_remote_description进行处理。 void peer_connection_set_remote_description(PeerConnection* pc, const char* sdp_text) { char* start = (char*)sdp_text; char* line = NULL; char buf[256]; char* val_start = NULL; uint32_t* ssrc = NULL; DtlsSrtpRole role = DTLS_SRTP_ROLE_SERVER; int is_update = 0; Agent* agent = &pc->agent; /*1. 解析SDP会话内容*/ while ((line = strstr(start, "\n"))) { line = strstr(start, "\n"); strncpy(buf, start, line - start); buf[line - start] = '\0'; if (strstr(buf, "a=setup:passive")) { role = DTLS_SRTP_ROLE_CLIENT; } if (strstr(buf, "a=fingerprint")) { strncpy(pc->dtls_srtp.remote_fingerprint, buf + 22, DTLS_SRTP_FINGERPRINT_LENGTH); } if (strstr(buf, "a=ice-ufrag") && strlen(agent->remote_ufrag) != 0 && (strncmp(buf + strlen("a=ice-ufrag:"), agent->remote_ufrag, strlen(agent->remote_ufrag)) == 0)) { is_update = 1; } if (strstr(buf, "m=video")) { ssrc = &pc->remote_vssrc; } else if (strstr(buf, "m=audio")) { ssrc = &pc->remote_assrc; } if ((val_start = strstr(buf, "a=ssrc:")) && ssrc) { *ssrc = strtoul(val_start + 7, NULL, 10); LOGD("SSRC: %" PRIu32, *ssrc); } start = line + 2; } if (is_update) { return; } if (!pc->b_local_description_created) { peer_connection_state_new(pc, role, 0); } /*2. 设置远程的描述信息,主要是填充PeerConnection中agent的信息, 这个agent信息存储了本地和远端的icecandicate*/ agent_set_remote_description(&pc->agent, (char*)sdp_text); /*3. 将peer connect状态切换为checking*/ STATE_CHANGED(pc, PEER_CONNECTION_CHECKING); } 测试P2P通路 收到对端的anser SDP后,解析得到了ICE candidate,那么将peer connect切换为checking状态,先测试一下P2P的通路。 int agent_connectivity_check(Agent* agent) { char addr_string[ADDRSTRLEN]; uint8_t buf[1400]; StunMessage msg; if (agent->nominated_pair->state != ICE_CANDIDATE_STATE_INPROGRESS) { LOGI("nominated pair is not in progress"); return -1; } memset(&msg, 0, sizeof(msg)); /*1. 使用对端的ip地址发送数据*/ if (agent->nominated_pair->conncheck % AGENT_CONNCHECK_PERIOD == 0) { addr_to_string(&agent->nominated_pair->remote->addr, addr_string, sizeof(addr_string)); LOGD("send binding request to remote ip: %s, port: %d", addr_string, agent->nominated_pair->remote->addr.port); agent_create_binding_request(agent, &msg); agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size); } /*2. 接收对端的反馈数据,如果返回正确,表示链路连通正常*/ agent_recv(agent, buf, sizeof(buf)); if (agent->nominated_pair->state == ICE_CANDIDATE_STATE_SUCCEEDED) { agent->selected_pair = agent->nominated_pair; return 0; } return -1; } 测试P2P链路正常后,就将Peer Connection状态切换为PEER_CONNECTION_CONNECTED,接下来就是正式的P2P链路创建连接了。 P2P通信 通过上面信令的建立,P2P链路已经建立了连接,接下来就是创建P2P的通信链路了,P2P的传输使用的是SRTP链路。 初始化 在offer SDP的时候,函数peer_connection_state_new中会调用dtls_srtp_init进行初始化。 int dtls_srtp_init(DtlsSrtp* dtls_srtp, DtlsSrtpRole role, void* user_data) { static const mbedtls_ssl_srtp_profile default_profiles[] = { MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_UNSET}; /*1. 设置srtp的角色,目前是做server,并更新为INIT状态,并设置UDP收发的回调函数。*/ dtls_srtp->role = role; dtls_srtp->state = DTLS_SRTP_STATE_INIT; dtls_srtp->user_data = user_data; dtls_srtp->udp_send = dtls_srtp_udp_send; dtls_srtp->udp_recv = dtls_srtp_udp_recv; /*2.初始化 MBEDTLS 相关结构体*/ mbedtls_ssl_config_init(&dtls_srtp->conf); //初始化 ssl_config 结构体,这是 SSL 配置的核心,包含加密协议、证书、密钥等信息 mbedtls_ssl_init(&dtls_srtp->ssl); //初始化 ssl 结构体,表示一个 SSL 会话上下文。 mbedtls_x509_crt_init(&dtls_srtp->cert); //初始化 X.509 证书结构,用于存储公钥证书。 mbedtls_pk_init(&dtls_srtp->pkey); //初始化公钥对象结构,用于存储私钥。 mbedtls_entropy_init(&dtls_srtp->entropy); //初始化熵源,熵源是生成随机数所需的基础。 mbedtls_ctr_drbg_init(&dtls_srtp->ctr_drbg); //初始化 CTR_DRBG(Counter-based Deterministic Random Byte Generator), //它是用于生成随机数的伪随机数生成器。 /*3. 设置打印等级*/ #if CONFIG_MBEDTLS_DEBUG mbedtls_debug_set_threshold(3); mbedtls_ssl_conf_dbg(&dtls_srtp->conf, dtls_srtp_debug, NULL); #endif /*4.生成一个自签名证书。这个证书将在 DTLS 握手期间用于身份验证*/ dtls_srtp_selfsign_cert(dtls_srtp); /*5. 配置证书验证和认证模式*/ mbedtls_ssl_conf_verify(&dtls_srtp->conf, dtls_srtp_cert_verify, NULL); //配置证书验证回调函数 dtls_srtp_cert_verify,用来验证远端证书的有效性。 mbedtls_ssl_conf_authmode(&dtls_srtp->conf, MBEDTLS_SSL_VERIFY_REQUIRED); //设置身份验证模式为MBEDTLS_SSL_VERIFY_REQUIRED,即客户端必须提供有效证书进行验证(服务器验证客户端的证书)。 /*6. 配置证书链和私钥*/ mbedtls_ssl_conf_ca_chain(&dtls_srtp->conf, &dtls_srtp->cert, NULL); //配置服务器的根证书链,这里使用dtls_srtp->cert 作为证书链的起始证书。 mbedtls_ssl_conf_own_cert(&dtls_srtp->conf, &dtls_srtp->cert, &dtls_srtp->pkey); //配置本地证书和私钥。服务器和客户端都需要自己的证书和私钥来进行加密通信。 /*7. 配置随机数生成器和超时*/ mbedtls_ssl_conf_rng(&dtls_srtp->conf, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); //配置随机数生成器,使用 mbedtls_ctr_drbg_random 和 dtls_srtp->ctr_drbg 作为随机数源。 mbedtls_ssl_conf_read_timeout(&dtls_srtp->conf, 1000); //设置读取超时时间为 1000 毫秒,意味着在等待数据时,如果超过 1000 毫秒没有接收到数据,则超时。 /*8. 如果是服务器 (DTLS_SRTP_ROLE_SERVER),则配置为 DTLS 服务器模式,并设置用 于防止 DoS 攻击的 DTLS cookies(cookie 是用于防止重放攻击的)。 如果角色是客户端,则直接配置为 DTLS 客户端模式。这里是做服务端*/ if (dtls_srtp->role == DTLS_SRTP_ROLE_SERVER) { mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); mbedtls_ssl_cookie_init(&dtls_srtp->cookie_ctx); mbedtls_ssl_cookie_setup(&dtls_srtp->cookie_ctx, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); mbedtls_ssl_conf_dtls_cookies(&dtls_srtp->conf, mbedtls_ssl_cookie_write, mbedtls_ssl_cookie_check, &dtls_srtp->cookie_ctx); } else { mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); } /*10. 生成并打印本地指纹, 用于生成本地证书的指纹(一个哈希值),用于标识证书。*/ dtls_srtp_x509_digest(&dtls_srtp->cert, dtls_srtp->local_fingerprint); LOGD("local fingerprint: %s", dtls_srtp->local_fingerprint); /*11. 配置 DTLS-SRTP 的保护配置文件,default_profiles 是预定义的安全配置, 包含 SRTP 协议所支持的加密算法、密钥长度等信息。*/ mbedtls_ssl_conf_dtls_srtp_protection_profiles(&dtls_srtp->conf, default_profiles); /*12. 配置 SRTP MKI(主密钥标识符)的支持情况。在这里,设置为不支持 MKI。*/ mbedtls_ssl_conf_srtp_mki_value_supported(&dtls_srtp->conf, MBEDTLS_SSL_DTLS_SRTP_MKI_UNSUPPORTED); /*13. 禁用客户端证书请求中的 CA 列表。*/ mbedtls_ssl_conf_cert_req_ca_list(&dtls_srtp->conf, MBEDTLS_SSL_CERT_REQ_CA_LIST_DISABLED); /*14. 完成所有配置后,调用 mbedtls_ssl_setup 初始化 ssl 上下文并将配置应用于 当前的 DTLS 会话。*/ mbedtls_ssl_setup(&dtls_srtp->ssl, &dtls_srtp->conf); return 0; } 创建 case PEER_CONNECTION_CONNECTED: /*1. 建立DTLS SRTP链接*/ if (dtls_srtp_handshake(&pc->dtls_srtp, NULL) == 0) { LOGD("DTLS-SRTP handshake done"); /*2. 建立sctp 链接*/ if (pc->config.datachannel) { LOGI("SCTP create socket"); sctp_create_socket(&pc->sctp, &pc->dtls_srtp); pc->sctp.userdata = pc->config.user_data; } STATE_CHANGED(pc, PEER_CONNECTION_COMPLETED); } 主要是创建DTLS_SRTP和SCTP两条链路。 DTSL_SRTP dtls_srtp_handshake_server() { /* 循环发起*/ while(1) { mbedtls_ssl_session_reset(&dtls_srtp->ssl); mbedtls_ssl_set_client_transport_id(&dtls_srtp->ssl, client_ip, sizeof(client_ip)); /* 发起握手*/ ret = dtls_srtp_do_handshake(dtls_srtp); if (ret == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED) { LOGD("DTLS hello verification requested"); } else if (ret != 0) { LOGE("failed! mbedtls_ssl_handshake returned -0x%.4x", (unsigned int)-ret); break; } else { break; } } } 判断返回的错误是MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED,会进行再次循环发起。 static int dtls_srtp_do_handshake(DtlsSrtp* dtls_srtp) { int ret; /* timer 是 mbedtls_timing_delay_context 类型的一个静态变量,用于管理 定时器(通常是控制超时的机制)。这个定时器在 DTLS 协议中用于处理超时等操作。*/ static mbedtls_timing_delay_context timer; /*定时器是 DTLS 握手中用来处理超时的一个重要机制,因为 DTLS 是基于UDP协议的, 而 UDP 本身不保证数据的可靠传输,因此需要在握手过程中手动管理超时。*/ mbedtls_ssl_set_timer_cb(&dtls_srtp->ssl, &timer, mbedtls_timing_set_delay, mbedtls_timing_get_delay); #if CONFIG_MBEDTLS_2_X /*回调函数会在 DTLS 握手完成后用于派发加密密钥。*/ mbedtls_ssl_conf_export_keys_ext_cb(&dtls_srtp->conf, dtls_srtp_key_derivation_cb, dtls_srtp); #else mbedtls_ssl_set_export_keys_cb(&dtls_srtp->ssl, dtls_srtp_key_derivation_cb, dtls_srtp); #endif /*用于设置 DTLS 上下文的 BIO(Basic Input/Output)回调函数,这些回调函数 定义了数据的发送和接收方式。回调函数在dtls_srtp_init进行初始化的。*/ mbedtls_ssl_set_bio(&dtls_srtp->ssl, dtls_srtp, dtls_srtp->udp_send, dtls_srtp->udp_recv, NULL); /*执行 DTLS 握手,直到握手成功或发生错误。*/ do { ret = mbedtls_ssl_handshake(&dtls_srtp->ssl); } while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); return ret; } SSL的握手 /* Main handshake loop */ while (ssl->state != MBEDTLS_SSL_HANDSHAKE_OVER) { ret = mbedtls_ssl_handshake_step(ssl); if (ret != 0) { break; } } 主要就是调用mbedtls_ssl_handshake_step,下面只列出了服务端的代码示例。 int mbedtls_ssl_handshake_step(mbedtls_ssl_context *ssl) { int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED; ret = ssl_prepare_handshake_step(ssl); if (ret != 0) { return ret; } ret = mbedtls_ssl_handle_pending_alert(ssl); if (ret != 0) { goto cleanup; } #if defined(MBEDTLS_SSL_SRV_C) if (ssl->conf->endpoint == MBEDTLS_SSL_IS_SERVER) { /*调用mbedtls_ssl_handshake_server_step进行握手管理*/ if (mbedtls_ssl_conf_is_tls12_only(ssl->conf)) { ret = mbedtls_ssl_handshake_server_step(ssl); } } #endif if (ret != 0) { /* handshake_step return error. And it is same * with alert_reason. */ if (ssl->send_alert) { ret = mbedtls_ssl_handle_pending_alert(ssl); goto cleanup; } } cleanup: return ret; } 主要是接着调用mbedtls_ssl_handshake_server_step进行处理。 int mbedtls_ssl_handshake_server_step(mbedtls_ssl_context *ssl) { int ret = 0; MBEDTLS_SSL_DEBUG_MSG(2, ("server state: %d", ssl->state)); switch (ssl->state) { case MBEDTLS_SSL_HELLO_REQUEST: ssl->state = MBEDTLS_SSL_CLIENT_HELLO; break; /* * <== ClientHello */ case MBEDTLS_SSL_CLIENT_HELLO: ret = ssl_parse_client_hello(ssl); break; #if defined(MBEDTLS_SSL_PROTO_DTLS) case MBEDTLS_SSL_SERVER_HELLO_VERIFY_REQUEST_SENT: return MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED; #endif /* * ==> ServerHello * Certificate * ( ServerKeyExchange ) * ( CertificateRequest ) * ServerHelloDone */ case MBEDTLS_SSL_SERVER_HELLO: ret = ssl_write_server_hello(ssl); break; case MBEDTLS_SSL_SERVER_CERTIFICATE: ret = mbedtls_ssl_write_certificate(ssl); break; case MBEDTLS_SSL_SERVER_KEY_EXCHANGE: ret = ssl_write_server_key_exchange(ssl); break; case MBEDTLS_SSL_CERTIFICATE_REQUEST: ret = ssl_write_certificate_request(ssl); break; case MBEDTLS_SSL_SERVER_HELLO_DONE: ret = ssl_write_server_hello_done(ssl); /* * <== ( Certificate/Alert ) * ClientKeyExchange * ( CertificateVerify ) * ChangeCipherSpec * Finished */ case MBEDTLS_SSL_CLIENT_CERTIFICATE: ret = mbedtls_ssl_parse_certificate(ssl); break; case MBEDTLS_SSL_CLIENT_KEY_EXCHANGE: ret = ssl_parse_client_key_exchange(ssl); break; case MBEDTLS_SSL_CERTIFICATE_VERIFY: ret = ssl_parse_certificate_verify(ssl); break; case MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC: ret = mbedtls_ssl_parse_change_cipher_spec(ssl); break; case MBEDTLS_SSL_CLIENT_FINISHED: ret = mbedtls_ssl_parse_finished(ssl); break; /* * ==> ( NewSessionTicket ) * ChangeCipherSpec * Finished */ case MBEDTLS_SSL_SERVER_CHANGE_CIPHER_SPEC: #if defined(MBEDTLS_SSL_SESSION_TICKETS) if (ssl->handshake->new_session_ticket != 0) { ret = ssl_write_new_session_ticket(ssl); } else #endif ret = mbedtls_ssl_write_change_cipher_spec(ssl); break; case MBEDTLS_SSL_SERVER_FINISHED: ret = mbedtls_ssl_write_finished(ssl); break; case MBEDTLS_SSL_FLUSH_BUFFERS: MBEDTLS_SSL_DEBUG_MSG(2, ("handshake: done")); ssl->state = MBEDTLS_SSL_HANDSHAKE_WRAPUP; break; case MBEDTLS_SSL_HANDSHAKE_WRAPUP: mbedtls_ssl_handshake_wrapup(ssl); break; default: MBEDTLS_SSL_DEBUG_MSG(1, ("invalid state %d", ssl->state)); return MBEDTLS_ERR_SSL_BAD_INPUT_DATA; } return ret; } 主要就是针对握手的状态进行握手交互,具体这里就不阐述了,可以参考《SSL/TLS协议分析》文章。 SCTP SCTP主要使用的usrctp开源组件, 传输 while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); // FPS 25 if (curr_time - video_time > 40) { video_time = curr_time; if (reader_get_video_frame(buf, &size) == 0) { peer_connection_send_video(g_pc, buf, size); } } if (curr_time - audio_time > 20) { if (reader_get_audio_frame(buf, &size) == 0) { peer_connection_send_audio(g_pc, buf, size); } audio_time = curr_time; } usleep(1000); } } -
看wireshark报文技巧
TCP握手与挥手 握手看SYNC与ACK 第一次握手[SYNC]标志, 第二次握手看[SYNC,ACK]标志,第三次ACK。 挥手看FIN 报文交互 正常交互 先看len,当长度为0,这个包是ack包,长度不为0表示是发送的数据包。 再看ack,表示该值以前的序号都收到了。ack是对端上一个报文的seq+len,同时也是期望对端下一个发包。 警告交互 TCP Dup ACK 重传ACK[TCP dup ack AAA#B],其中A表示重复发送的是哪个number的报文(注意不是seq),B表示这是第几次重复发送ACK。主要有以下场景会导致。 回复的ACK丢了导致对端没有收到,导致对端重复发送报文,以至于期望接收的报文没有收到,需要重复发送ACK。 序列468的重发实际是多余,接收者在466序列已经收到了。 TCP Out-of-Order 报文乱序,发送报文的seq不对,一般情况下是对TCP Dup ACK的重传。 -
MQTT协议分析
什么是MQTT MQTT是基于TCP/IP网络协议栈构建的异步通信消息协议,基于发布-订阅模式进行传输。实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中, MQTT协议中有三种身份: 发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。 其中消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者,客户端之间的通信完全是空间解耦的。如上图,客户端C如果要接收客户端A的信息,先向broker subscibe想要接收的主题,当Client A发布消息是发送给broker,broker接收到后判断此前Client C订阅了该主题信息,那么就会转发给Client C。因此发布消息/订阅消息与发布者和订阅者是完全解耦的,这种耦合度有下面三个维度: 空间解耦:发布者与订阅者并不知道对方的存在。 时间解藕:发布者与订阅者并不一定需要同时运行。 同步 Synchronization 解藕:两个组件的操作比如发布和订阅都不会在发布或者接收过程中产生中断。 MQTT传输的消息分为: 主题(Topic) 和 负载(payload)两部分: Topic: 为消息的类型,订阅者订阅(Subscribe)主题后,就会收到目标主题的消息内容(payload) payload: 消息的内容,发布者要发布的数据内容和订阅者要接收的数据内容。 MQTT协议传输 主题 MQTT传输中,topic是broker为每个client过滤消息用的,主要用字符串来标识。使用目录分层的结构来进行表示,broker收到消息后会将该主题下的消息转发给所有订阅该主题(Topic)的设备。以“/”为分隔符区分不同的层级,包含通配符“+” 或 “#”的主题又称为主题过滤器(Topic Filters);,不含通配符的成为主题名(Topic Names) ,示例如下: sensor/10/temperature sensor/+/temperature $SYS/broker/metrics/packets/received $SYS/broker/metrics/# '+' : 表示通配一个层级, 例如a/+,匹配a/x, a/y MQTT数据包格式 如上图,MQTT的包格式有3部分组成,fixed header,variable header,playload,下面依次进行展开。 fixed header fixed header又分为MQTT control packet type 和每种packet type对应的flag组成,Packet type主要有以下,其决定了mqtt实际传输的方法。 packet types flag 在fixed header的bit[3]~bit[0]主要存储的是各个packet type对应的标志,如上图只有PUBLISH type才有意义,其他的都是保留。 DUP:设置为1,表明这个数据包是一条重复的消息;否则该数据包就是第一次发布的消息。 QOS:用于发布的传输质量,bit[2]bit[1]=00,只发一次不保证成功;01最少发一次,收不到ACK会一直重传,接收者可能会重复接受到数据;10只有一次,接收者不会重复收到数据。 PUBLISH数据包不得将两个QoS位都设置为1。如果服务器或客户端收到PUBLISH两个QoS位都设置为1 的数据包必须关闭网络连接。 RETAIN:如果RETAIN 标志设置为1,则在客户端向服务器发送的PUBLISH数据包中,服务器必须存储应用程序消息及其QoS,以便可以将其传递给订阅与其主题名称匹配的未来订阅者。 下面针对QOS质量再进行说明。使用QoS 0可能丢失消息,使用QoS 1可以保证收到消息,但消息可能重复,使用 QoS 2 可以保证消息既不丢失也不重复。 QOS 0 : QoS 0是最低的QoS 等级,消息即发即弃,不需要等待确认,不需要存储和重传,因此对于接收方来说,永远都不需要担心收到重复的消息。当我们使用 QoS 0 传递消息时,消息的可靠性完全依赖于底层的TCP协议。而TCP只能保证在连接稳定不关闭的情况下消息的可靠到达,一旦出现连接关闭、重置,仍有可能丢失当前处于网络链路或操作系统底层缓冲区中的消息。这也是 QoS 0 消息最主要的丢失场景。 QOS 1: 是加入了应答与重传机制,发送方只有在收到接收方的PUBACK报文以后,才能认为消息投递成功。在此之前,发送方需要存储该PUBLISH 报文以便下次重传。缺点就是Broker若从发布方收到了重复的PUBLISH报文,而在将这些报文转发给订阅方的过程中,再次发生重传,这将导致订阅方最终收到更多的重复消息。 QOS 2:解决QoS 0、1 消息可能丢失或者重复的问题,但它也带来了最复杂的交互流程和最高的开销。每一次的QoS 2 消息投递,都要求发送方与接收方进行至少两次请求/响应流程。QOS2是针对接受者不能同时收到重复报的场景需求。 步骤1:发送方(发布者或broker)存储并发布QoS2的报文,然后需要接收方(broker或订阅者)回复PUBREC报文。这里与QoS1流程基本一直,只是回复报文从PUBACK 变成了PUBREC。 步骤2:当发送方收到PUBREC报文后,表示对端已经收到了发送的报文,发送方将不能再重传这个报文,发送方所以此时可以删除本地存储的PUBLISH报文;接着发送一个PUBREL报文通知对端自己准备将本次使用的Packet ID标记为可用了。 步骤3:当接收方收到PUBREL报文后,回复PUBCOMP报文表示自己也准备好将当前的Packet ID用于新的消息了。 4)当发送方收到PUBCOMP报文,这一次的QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的PUBLISH报文时,也会将它视为一个全新的消息。 variable header 可变长度头部有些报文包含,有些报文则不包含。实际上可变长度的头部可以理解为实际的具体数据,因为不同的type对应的payload不同,相同的type也会有不同的数据可以区分,比如connect类型需要包含不少的格式信息。 协议版本 :号值表示客户端的版本。 clean sesion:MQTT客户端向服务器发起CONNECT请求时,可以通过’Clean Session’标志设置会话。‘Clean Session’设置为0,表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。‘Clean Session’设置为1,表示创建一个新的临时会话,在客户端断开时,会话自动销毁 Will Flag : 遗言标志位 Will QoS: 遗言的消息质量 Will Retain: 遗言的保持状态 Keep Alive timer(心跳时长):client与broker交互PINGREQ和PINGRESP。 payload 一些MQTT控制数据包包含有效载荷作为数据包的最后部分。对于 PUBLISH 数据包,这是应用程序消息。上图列出了需要有效载荷的控制数据包。 MQTT基本交互 CONNECT ClientId:每个客户端连接到broker的标识,具有唯一性。如果客户端与服务端不需要保持长连接状态,可以为空。 CleanSession:client告诉broker是否需要建立持久会话,在持久会话 (CleanSession = false)中,broker会存储client的所有订阅。如果会话不是持久的(CleanSession = true),那么 broker则不会为client存储任何内容并且会清除先前持久会话中的所有信息。 Username/Password :MQTT会发送username和password进行client认证和授权。如果此信息没有经过加密或者hash,那么密码将会以纯文本的形式发送。因此建议username 和 password 要经过加密安全传输。 LastWillxxx :表示遗愿,client在连接 broker的时候将会设立一个遗愿,这个遗愿会保存在 broker中,当 client 因为非正常原因与 broker断开连接时,broker会将遗愿发送给订阅了这个 topic(订阅遗愿的 topic)的client。 keepAlive:client在连接建立时与 broker 通信保活的时间间隔,通常以秒为单位。这个时间指的是client 与 broker 在不发送消息下所能承受的最大时长。client与broker交互PINGREQ和PINGRESP来保持连接。 CONNACK 当broker收到CONNECT消息后,需要发送CONNACK消息进行响应。CONNACK消息包括两部分内容。 PUBLISH 当MQTT client在连接上broker以后,就可以发布信息了。MQTT使用的是基于topic主题的过滤,每条消息都应该包含一个topic,broke 可以使用topic将消息发送给感兴趣 client。除此之外每条消息还会包含一个负载(Payload),Payload 中包含要以字节形式发送的数据。MQTT 是数据无关性的,也就是说数据是由发布者 - publisher 决定要发送的是 XML 、JSON 还是二进制数据、文本数据。 MQTT 中的 PUBLISH 消息结构如下。 Packet Identifier:这个PacketId标识在 client 和 broker 之间唯一的消息标识。packetId 仅与大于零的 Qos 级别相关。 TopicName:主题名称是一个简单的字符串,/ 代表着分层结构。 Qos:这个数字表示的是服务质量水平,服务质量水平有三个等级:0、1 和 2,服务级别决定了消息到达 client 或者 broker 的保证类型,来决定消息是否丢失。 RetainFlag:这个标志表示 broker 将最近收到的一条 RETAIN 标志位为true的消息保存在服务器端(内存或者文件)。 MQTT 服务器只会为每一个 Topic保存最近收到的一条RETAIN标志位为true的消息。如果MQTT 服务器上已经为某个 Topic 保存了一条 Retained 消息,当客户端再次发布一条新的 Retained 消息时,那么服务器上原来的那条消息会被覆盖。 Payload:这个是每条消息的实际内容。MQTT 是数据无关性的。可以发送任何文本、图像、加密数据以及二进制数据。 Dupflag:这个标志表示该消息是重复的并且由于预期的 client 或者 broker 没有确认所以重新发送了一次。这个标志仅仅与 Qos 大于 0 相关。 当 client 向 broker 发送消息时,broker 会读取消息,根据 Qos 的级别进行消息确认,然后处理消息。处理消息其实就是确定哪些 subscriber 订阅了 topic 并将消息发送给他们。 最初发布消息的 client 只关心将 PUBLISH 消息发送给 broker,一旦 broker 收到 PUBLISH 消息,broker 就有责任将其传递给所有 subscriber。发布消息的 client 不会知道是否有人对发布的消息感兴趣,同时也不知道多少 client 从 broker 收到了消息。 SUBSCRIBE client 会向 broker 发送 SUBSCRIBE 消息来接收有关感兴趣的 topic,这个 SUBSCRIBE 消息非常简单,它包含了一个唯一的数据包标识和一个订阅列表。 Packet Identifier:这个 PacketId 和上面的 PacketId 一样,都表示消息的唯一标识符。 ListOfSubscriptions:SUBSCRIBE 消息可以包含一个client的多个订阅,每个订阅都会由一个 topic和一个 Qos构成。 client在向broker发送 SUBSCRIBE 消息后,broker会向 client发送 SUBACK 确认消息。 Packet Identifier :这个数据包标识符和SUBSCRIBE中的相同。 ReturnCode:broker 为每个接收到的 SUBSCRIBE 消息的 topic/Qos 对发送一个返回码。例如,如果 SUBSCRIBE 消息有五个订阅消息,则SUBACK消息包含五个返回码作为响应。 到现在我们已经探讨过了三种消息类型,发布 - 订阅 - 确认消息,这三种消息的示意图如下。 参考文献: https://www.emqx.com/zh/blog/introduction-to-mqtt-qos https://blog.csdn.net/jackwmj12/article/details/129163012 https://www.cnblogs.com/cxuanBlog/p/14917187.html http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html -
SSL/TLS协议分析
什么是TLS 1994: SSL 1.0 NetScape公司提出SSL第一版,未公开。 1995: SLL 2.0 公开发布了第二版,与2011年弃用。 1996: SSL 3.0 第三版得到大规模应,于2015年弃用。 1999: TLS 1.0 RFC2246,被IETF纳入标准化,没太大改动,改名TLS。 2006: TLS 1.1 RFC4346,修复bug,增加参数。 2008: TLS 1.2 RFC,更多扩展和算法该节。 2018: TLS 1.3 减少时延,完全前向安全。 传输层安全性协议(英语:Transport Layer Security,缩写:TLS),前身称为安全套接层(英语:Secure Sockets Layer,缩写:SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。TLS(Transport Layer Security)是计算机网络通信用于安全加密的协议,HTTP+TLS后后即HTTPS。 TLS介于网络传输层和应用层之间,TLS主要用于数据加密过程,保证数据的安全传输。TLS是一套混合加密系统,使用了对称加密和非对称加密两种方式。非对称加密相对对称加密更安全,但是其复杂的加解密会使得通信效率降低,为了解决这种场景。先使用“非对称加密”的方式传输用于数据的“对称加密密钥”,以保证双方的对称加密秘钥是安全传输的,后续就可以直接使用对称加密秘钥进行传输了。关于非对称加密和对称加密接下来简单说明一下。 对称加密与非对称加密 对称加密 对称加密双方使用相同的秘钥进行加解密,秘钥被称为共享秘钥和对称秘钥。常见的对称加密算法有AES、DES、3DES等等,下面简要说明一下常用的AES算法。 AES加解密 AES是秘钥长度有128/192/256,其秘钥长度用于指定将明文转换为密文所需要的变化轮数,如当秘钥长度128位是,轮数是10;秘钥长度为192位时,轮数为12;秘钥长度为256时,轮数为14。 由于AES算法单次只能加解密固定长度的分组数据,如AES 单次只能加解密128位数据,而实际场景中的AES加解密长度并不是128位的整数倍,为了解决这个问题,使用AES可以使用分组密码模式配合消息填充的方法来解决。 分组密码模式 ECB模式 将明文进行分组加密,加密结果为密文分组,最后一个明文分组需要必须要填充为128位。 CBC模式 依旧是对明文进行分组加密,最后一个分组需要填充满128位。每一组明文在加密前都与前面的密文分组进行异或操作。由于第一个明文分组前没有密文分组,所以需要准备一个与密文分组长度相等的比特序列来代替密文分组,这个比特序列被称作初始化向量,简称IV。 CTR模式 CTR模式使用用于分组长度相同的计数值参与运算,通过对逐次累加的计数器进行加密来生成密钥流,通过加密计数器得到的密钥流与明文分组进行异或运算,得到密文分组。若明文长度不是分组长度的整数倍,假设最后一个明文分组N的 长度为L位,那么最后一个明文分组N只需与计数器N加密结果的左侧 L位异或,获得的密文分组N的长度也是N位。这种算法结构使得CTR 模式不需要对明文进行填充。 分组明文的填充 使用ECB/CBC模式,当加密明文不是分组密码长度的整数倍是,通常需要对明文进行填充,常用的填充方案是PKCS7。 以AES-CBC算法为例,若分组的长度是16字节,当加密明文是28字节是,则需要在明文末尾填充4字节,使其达到分组长度的整数倍;若待加密数据恰好是16字节,需要在明文后面额外填充16字节,并将其全部填充为16。 Mbedtls AES示例 uint8_t key[16] = { 0x06, 0xa9, 0x21, 0x40, 0x36, 0xb8, 0xa1, 0x5b, 0x51, 0x2e, 0x03, 0xd5, 0x34, 0x12, 0x00, 0x06 }; uint8_t iv[16] = { 0x3d, 0xaf, 0xba, 0x42, 0x9d, 0x9e, 0xb4, 0x30, 0xb4, 0x22, 0xda, 0x80, 0x2c, 0x9f, 0xac, 0x41 }; int cipher(int type) { size_t len; int olen = 0; uint8_t buf[64]; mbedtls_cipher_context_t ctx; const mbedtls_cipher_info_t *info; mbedtls_cipher_init(&ctx); info = mbedtls_cipher_info_from_type(type); //获取加密模式 mbedtls_cipher_setup(&ctx, info); //设置cipher结构体,内部是赋值的过程 mbedtls_cipher_setkey(&ctx, key, sizeof(key)*8, MBEDTLS_ENCRYPT); //设置密钥 mbedtls_cipher_set_iv(&ctx, iv, sizeof(iv)); //设置IV,CBC/CTR都需要IV,第一组的异或 mbedtls_cipher_update(&ctx, ptx, strlen(ptx), buf, &len);//更新cipher olen += len; mbedtls_cipher_finish(&ctx, buf + len, &len);//cipher完成 olen += len; mbedtls_cipher_free(&ctx); return 0; } int main(void) { cipher(MBEDTLS_CIPHER_AES_128_CBC); cipher(MBEDTLS_CIPHER_AES_128_CTR); return 0; } 非对称加密 上一小节说了对称加密算法,其特点就是加密速度快,效率高,但是其缺点就是在于密钥的传输存在安全性问题,因为大部分通信都是通过网络来进行传输的,密钥容易在传输过程中被窃取,一旦获得密钥,那么后面的加密就毫无意义了。为了解决这种问题,非对称加密就产生了,非对称加密的特点就是把密钥进行分离,分成公钥和私钥两个部分。公钥是传输的双方公有的密钥,用于数据的加密,而私钥用于解密,双方的私钥不一样,各自保管。通过公钥加密、各自的私钥解密这样即使公钥被泄露,也不用担心,没有私钥是无法解密的。常见的非对称加密算法有RSA,DSA,ECC等。下面重点简要介绍RSA加密算法。 RSA 1) Bob按照RSA算法标准生成密钥对,这个密钥对包含公钥和私钥。 2) Bob将公钥发送给Alice,私钥则自己进行保存起来。 3) Alice收到Bob的公钥后,使用该公钥加密明文,接着发送给Bob。 4) Bob接收到Alice使用公钥加密的密文后,使用自己的私钥进行解密得到明文,解密正确后则后续Bob就使用这套密钥。 mbedtls RSA示例 size_t olen = 0; uint8_t out[2048/8]; mbedtls_rsa_context ctx; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; const char *pers = "simple_rsa"; const char *msg = "Hello, World!"; mbedtls_platform_set_printf(printf); mbedtls_platform_set_snprintf(snprintf); mbedtls_entropy_init(&entropy); //初始化熵结构体 mbedtls_ctr_drbg_init(&ctr_drbg);//初始化随机数结构体 mbedtls_rsa_init(&ctx, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256); //初始化RSA结构体 mbedtls_entropy_add_source(&entropy, entropy_source, NULL, MBEDTLS_ENTROPY_MAX_GATHER, MBEDTLS_ENTROPY_SOURCE_STRONG); //添加熵源接口,设置熵源属性 mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const uint8_t *) pers, strlen(pers)); //根据个性化字符串更新种子 mbedtls_rsa_gen_key(&ctx, mbedtls_ctr_drbg_random, &ctr_drbg, 2048, 65537);//RSA生成密钥对 mbedtls_rsa_pkcs1_encrypt(&ctx, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PUBLIC, strlen(msg), msg, out);//RSA加密操作,通过指定公钥进行加密 mbedtls_rsa_pkcs1_decrypt(&ctx, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE, &olen, out, out, sizeof(out));//RSA解密操作,通过制定参数私钥解密 out[olen] = 0; memcmp(out, msg, olen); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); mbedtls_rsa_free(&ctx); return 0; 数字证书 使用非对称加密的好处就是,通过公钥加密、私钥解密,私钥是各自私有这样相比于对称加密就更安全了。一般情况下,公钥需要通过网络进行传输,而且公钥都是公开的。假设有这么一个场景,A和B在首次建立通信连接的时候,A发送给B公钥,但是在发送公钥的过程中被C劫持了,换成了C自己的公钥发送给B,那么B就误认为C的公钥是A发送的,那么后续C就可以作为中间人获取到通信内容,传输链路A->C-B。 为了解决这种在初次建立连接是被中间人中继的问题,因为非对称加密公私钥可以分离,所以可以找个大家信得过的机构来专门颁发公钥,这个机构颁发的就是数字证书,相当于就是身份证。让A和B的公钥是值得信赖的,不要让B误认为公钥是C的公钥。 签发证书的机构被称为 CA( Certificate Authority),理论上每个人都可以成为CA,因为每个人都可以自己签发证书,但是只有极少数的权威CA颁发的证书才会被承认。 一般来说数字证书可以按照安全程度分为以下三类: EV:EV证书(Extended Validation Certificate)是一种根据一系列特定标准颁发的X.509电子证书,根据要求,在颁发证书之前,证书颁发机构(CA)必须验证申请者的身份。不同机构根据证书标准发行的扩展验证证书并无太大差异,但是有时候根据一些具体的要求,特定机构发行的证书可以被特定的软件识别 OV:OV证书(Organization Validation SSL),指需要验证网站所有单位的真实身份的标准型SSL证书,此类证书不仅能够起到网站信息加密的作用,而且能向用户证明网站的真实身份 DV:DV证书(Domain Validation SSL),指需要验证域名的有效性。该类证书只提供基本的加密保障,不能提供域名所有者的信息 TLS协议 TLS协议可以分为记录层和握手层 记录层:负责对数据进行加密、压缩、分段,并保证数据的完整性和安全传输。 握手层:负责建立安全通信,完成密钥交换、身份认证以及协商加密算法等,确保双方通信的安全性。 握手层有3个协议,握手协议(Handshake Protocol)、更换加密规约协议(Change Cipher Spec Protocol)、告警协议(Alert Protocol)。 TLSv1.2 握手过程 步骤1:客户端通过明文的方式发送Client Hello 消息到服务器,消息中主要包含了客户端支持的ciphersuites, TLS 版本信息和客户端随机数。 步骤2:服务器接收到消息后,明文发送一个Server Hello给客户端,包括自己支持的ciphersuites, TLS 版本,自己的数字证书(证书中包含了公钥)和服务器端生成的随机数。在包的交互上,证书、随机数等可能是不包含在Server Hello一个包中单独进行发送,示服务器具体的行为。下图是Server Hello, 当客户端需要对服务器的身份进行验证时,服务器端发送 Certificate消息。该消息中包含证书清单,证书清单是一组X.509 v3证书列表。证书列表包含服务器证书、中间证书和根证书。通常情况下服务器并不会发送根证书,这就需要客户端提前导入根证书。通过Certificate消息,客户端将获得服务器的公钥,并通过根证书中的公钥验证服务器公钥的合法 性。下图是服务器证书Ceriticate,证书中包含了公钥。 如果服务器没有证书或者服务器的证书仅用来签名(如DSS证书、签名RSA证书),或者使用的是FORTEZZA KEA密钥交换算法,那么就需要发送Server Key Exchange。服务器会在 server Certificate 消息之后发送 Server Key Exchange 消息。 服务器Hello阶段结束后,一般会附上一条简单的Server Hello Done表示结束。 步骤3:客户端开始验证数字证书,可能会不断往上追溯 CA,直到一个可信任CA。验证证书合法之后,从证书中读取公钥信息。之后生成一个pre-master key(用来生成后续的对称秘钥),接着使用证书中的公钥来对pre-master key进行加密,然后发送给服务器。该过程是非对称加密传输。服务器接收到客户端发送过来的非对称加密的密文,使用自己的私钥进行解密,获得了pre-master key。注意此时是非对称加密传输,这样服务器就获得了后续对称加密的密钥。 经过1)2)3)不走,服务器和客户端有了3组数据,分别是客户端的随机数、服务器的随机数和pre-master key。其中由于客户端的随机数和服务器的随机数都是使用明文传输,所以这两个数字是有被暴露的风险的,但是由于pre-master key是使用非对称加密传输,十分安全,所以将这三者结合,使用之前协商好的特定的算法就可以生成一个密钥,这个密钥称为shared secert。也就是之后用来对称加密的密钥。 步骤4:客户端在计算出对称加密的密钥之后,使用该密钥进行对称加密通信,告知服务器之后都使用该密钥进行对称加密。注意此时是对称加密传输服务器接收到密文后,使用之前计算出的密钥来进行对称解密,解密成功之后,再使用该密钥进行对称加密通信。告知客户端密钥确认无误,可以使用该密钥进行通信。 至此,整个TLS的握手过程完整,之后就可以开始对称加密的通信了。总结一下SSL/TLS协议的基本过程,前两步又称为\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"握手阶段\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"(handshake),是SSL/TLS加密通信的基础。 通过CA体系交换公钥 使用非对称加密算法,交换用于对称加密的密钥 有效数据使用对称加密算法,进行密文传输 TLSv1.3握手过程 待补充。 TLS应用编程 创建连接 int ssl_transport_connect(NetworkContext_t* net_ctx, const char* host, uint16_t port, const char* cacert) { const char* pers = "ssl_client"; int ret; Address resolved_addr; mbedtls_ssl_init(&net_ctx->ssl); //初始化ssl结构体 mbedtls_ssl_config_init(&net_ctx->conf);//初始化ssl配置结构体 // mbedtls_x509_crt_init(&net_ctx->cacert);//初始化X.509证书结构体 mbedtls_ctr_drbg_init(&net_ctx->ctr_drbg);//初始化随机数结构体 mbedtls_entropy_init(&net_ctx->entropy);//初始化熵结构体 //初始化个性化字符串更新种子 if ((ret = mbedtls_ctr_drbg_seed(&net_ctx->ctr_drbg, mbedtls_entropy_func, &net_ctx->entropy, (const unsigned char*)pers, strlen(pers))) != 0) { return -1; } //加载ssl默认配置选项,可以指定端类型、传输协议等参数。 if ((ret = mbedtls_ssl_config_defaults(&net_ctx->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { LOGE("ssl config error: -0x%x", (unsigned int)-ret); return -1; } //配置认真方式,配置项包括 // VERIFY_NONE:不对证书进行验证。 // VERIFY_OPTIONAL:对证书进行验证,即使证书验证失败,继续完成握手操作 // VERIFY_REQUIRED: 对证书进行验证,而且要求证书必须通过验证,否则总之握手过程 mbedtls_ssl_conf_authmode(&net_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); /* XXX: not sure if this is needed ret = mbedtls_x509_crt_parse(&net_ctx->cacert, (const unsigned char *) cacert, strlen(cacert) + 1); if (ret < 0) { LOGE("ssl parse error: -0x%x", (unsigned int) -ret); } mbedtls_ssl_conf_ca_chain(&net_ctx->conf, &net_ctx->cacert, NULL); */ //设置随机数生成器回调接口 mbedtls_ssl_conf_rng(&net_ctx->conf, mbedtls_ctr_drbg_random, &net_ctx->ctr_drbg); //通过配置选项完成ssl的设置 if ((ret = mbedtls_ssl_setup(&net_ctx->ssl, &net_ctx->conf)) != 0) { LOGE("ssl setup error: -0x%x", (unsigned int)-ret); return -1; } //配置ssl hostname if ((ret = mbedtls_ssl_set_hostname(&net_ctx->ssl, host)) != 0) { LOGE("ssl set hostname error: -0x%x", (unsigned int)-ret); return -1; } //创建socket,配置地址和端口,发起tcp连接 memset(&resolved_addr, 0, sizeof(resolved_addr)); tcp_socket_open(&net_ctx->tcp_socket, AF_INET); ports_resolve_addr(host, &resolved_addr); addr_set_port(&resolved_addr, port); if ((ret = tcp_socket_connect(&net_ctx->tcp_socket, &resolved_addr) < 0)) { return -1; } //配置 SSL 连接的接收超时时间 mbedtls_ssl_conf_read_timeout(&net_ctx->conf, SSL_RECV_TIMEOUT); //设置 SSL 连接的 BIO(输入输出)接口,即定义 SSL/TLS 连接使用的网络 I/O 操作函数。 //&net_ctx->ssl:这是指向 mbedtls_ssl_context 结构体的指针,表示 SSL/TLS 会话上下文。 //&net_ctx->tcp_socket:这是指向 TCP 套接字的指针,表示底层的网络连接(通常是一个 TCP 套接字,用于在网络中传输加密数据)。 //ssl_transport_mbedtls_send:这是一个自定义的发送数据函数,用于通过网络连接发送加密后的数据。mbedtls_ssl_write 函数最终会调用这个发送函数,将数据从应用层发送到网络层。 //ssl_transport_mbedtls_recv_timeout:这是一个自定义的接收数据函数,用于在设置的超时限制下从网络接收数据。它会被 mbedtls_ssl_read 调用,接收经过加密的 SSL/TLS 数据并解密。 mbedtls_ssl_set_bio(&net_ctx->ssl, &net_ctx->tcp_socket, ssl_transport_mbedlts_send, NULL, ssl_transport_mbedtls_recv_timeout); LOGI("start to handshake"); //执行handshake握手 while ((ret = mbedtls_ssl_handshake(&net_ctx->ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { LOGE("ssl handshake error: -0x%x", (unsigned int)-ret); } } LOGI("handshake success"); return 0; } mbedtls_ssl_set_bio传入的函数。 static int ssl_transport_mbedtls_recv_timeout(void* ctx, unsigned char* buf, size_t len, uint32_t timeout) { int ret; fd_set read_fds; struct timeval tv; tv.tv_sec = timeout / 1000; tv.tv_usec = (timeout % 1000) * 1000; FD_ZERO(&read_fds); FD_SET(((TcpSocket*)ctx)->fd, &read_fds); ret = select(((TcpSocket*)ctx)->fd + 1, &read_fds, NULL, NULL, &tv); if (ret < 0) { return -1; } else if (ret == 0) { // timeout } else { if (FD_ISSET(((TcpSocket*)ctx)->fd, &read_fds)) { ret = tcp_socket_recv((TcpSocket*)ctx, buf, len); } } return ret; } static int ssl_transport_mbedlts_send(void* ctx, const uint8_t* buf, size_t len) { return tcp_socket_send((TcpSocket*)ctx, buf, len); } 收发数据 int32_t ssl_transport_recv(NetworkContext_t* net_ctx, void* buf, size_t len) { int ret; memset(buf, 0, len); ret = mbedtls_ssl_read(&net_ctx->ssl, buf, len); return ret; } int32_t ssl_transport_send(NetworkContext_t* net_ctx, const void* buf, size_t len) { int ret; while ((ret = mbedtls_ssl_write(&net_ctx->ssl, buf, len)) <= 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { LOGE("ssl write error: -0x%x", (unsigned int)-ret); } } return ret; } 断开连接 void ssl_transport_disconnect(NetworkContext_t* net_ctx) { mbedtls_ssl_config_free(&net_ctx->conf); // mbedtls_x509_crt_free(&net_ctx->cacert); mbedtls_ctr_drbg_free(&net_ctx->ctr_drbg); mbedtls_entropy_free(&net_ctx->entropy); mbedtls_ssl_free(&net_ctx->ssl); tcp_socket_close(&net_ctx->tcp_socket); } 参考: 1. 《密码技术与物联网安全》 2. https://tinychen.com/20200602-encryption-intro/