本文基于NAT3+NAT3实现upd打洞(假设你对NAT类型已经很清楚)
如果A网络的NATA+B网络的NATB的值大于6则打洞会失败,需要使用turn中继服务
STUN协议解析
#pragma once
#include "hv/UdpClient.h"
#include "fmt/format.h"
/*
stun 模块,用户获取外网地址
*/namespace stun_cli
{#define MAPPED_ADDRESS (0x0001)
#define XOR_MAPPED_ADDRESS (0x0020)//返回异或处理的公网IP/端口
#define MESSAGE_INTEGRITY (0x0008)//HMAC-SHA1消息完整性校验
#define ERROR_CODE (0x0009)//错误响应代码
#define FINGERPRINT (0x8028)//包尾CRC32校验防止粘包#pragma pack(push, 1)struct StunHeader {uint16_t msg_type; // 消息类型(高2位为0,中6位方法,低8位类别)uint16_t msg_length; // 消息长度(不含头部)uint32_t magic_cookie = htonl(0x2112A442); // 固定魔术字uint8_t transaction_id[12]; // 96位事务ID};struct StunAttribute {uint16_t type; // 属性类型uint16_t length; // 值长度(需4字节对齐)uint8_t value[]; // 变长数据};
#pragma pack(pop)void build_binding_request(StunHeader* pheader){pheader->msg_type = htons(0x0001); // Binding Requestpheader->msg_length = 0;srand(time(nullptr));for (int i = 0; i < 12; ++i){pheader->transaction_id[i] = std::rand() / 255;}}bool parse_response(uint8_t* buffer, unsigned int buf_size,unsigned short &port,std::string& ip) {StunHeader* hdr = reinterpret_cast<StunHeader*>(buffer);if (buf_size <= sizeof(StunHeader)) return false;if (ntohl(hdr->magic_cookie) != 0x2112A442) return false;uint8_t* attr_start = buffer + sizeof(StunHeader);int offset = 0;while (offset < buf_size) {StunAttribute* attr = reinterpret_cast<StunAttribute*>(attr_start + offset);unsigned short attr_nlen = ntohs(attr->length);switch (ntohs(attr->type)) {case MAPPED_ADDRESS: {//8字节 1-保留 1-Family -2port 4/16 ipunsigned char* pval = attr->value;unsigned char family = pval[1];port = ntohs(*(unsigned short*)(pval + 2));if (family == 0x01)//IPV4 4bytes{unsigned char* pAddr = pval + 4;printf("IP:%d.%d.%d.%d:%d\r\n", pAddr[0], pAddr[1], pAddr[2], pAddr[3], port);ip = fmt::format("{0}.{1}.{2}.{3}", pAddr[0], pAddr[1], pAddr[2], pAddr[3]);return true;}else if (family == 0x02)//IPV6 16bytes{unsigned char* pAddr = pval + 4;}break;}case XOR_MAPPED_ADDRESS:{int a = 0;// 解析异或地址:IP/Port ^ Magic_Cookie//res.public_ip = ntohl(*reinterpret_cast<uint32_t*>(attr->value)) ^ 0x2112A442;break;}case MESSAGE_INTEGRITY:{int a = 0;//verify_hmac(attr->value); // 验证HMAC-SHA1break;}}//长度4字节对齐if (attr_nlen % 4 == 0)offset += attr_nlen;elseoffset += (attr_nlen / 4 + 1) * 4;offset += sizeof(StunAttribute);}return true;}
};
UDP 打洞demo
udp打洞流程
- 公网地址交换
1. A、B分别从STUN服务器获取公网地址 A_public:PortA 和 B_public:PortB
2. 通过信令服务器交换地址A、B外网地址
- 双向触发
1. A向 B_public:PortB 发送探测包 → NAT A 创建规则:“允许来自B公网地址的包进入”
2. B向 A_public:PortA 发送探测包 → NAT B 创建规则:“允许来自A公网地址的包进入” #如果在规则创建前收到对方的包,则会被NAT丢去。因为NAT规则不允许。
- 直连通信、保持心跳
#经过上述两个规则之后NAT已经创建规则,支持对应的IP:PORT通讯了
1. 双方收到探测包好,确认NAT打洞完成
2. 定期发送空数据包(如每20秒),防止NAT表项超时关闭(默认30-60秒)
udp代码实现
#pragma once
#include "stun.h"
#include <string>
#include <hv/UdpClient.h>
#include "Heap/XTimer.h"
#include <atomic>class UDPCli
{
public:UDPCli(){m_btunnel_ok = false;std::vector<std::string> ipv4s, ipv6s;get_host_addr("stun.voipbuster.com", ipv4s, ipv6s);m_pcli = std::make_shared<hv::UdpClient>();m_pcli->createsocket(3478, ipv4s[0].c_str());m_pcli->onMessage = [this](const hv::SocketChannelPtr&, hv::Buffer* data) {unsigned short sport = 0;std::string ip;if (!stun_cli::parse_response((uint8_t*)data->data(), data->size(), sport, ip)){//这里简化处理,默认收到非STUP的包则认为探测包成功//实际的时候需要检验包是否为探测包才能标记完成m_btunnel_ok = true;printf("[%I64d] Recv Message:%s\r\n",this, std::string((char *)data->data(), (char *)data->data() + data->size()).c_str());return;}if (!ip.empty())update(ip, sport);};m_pcli->start(true);//增加定时器和STUN通讯,防止UDP丢包,实际应该增加重试次数//demo只为验证流程是否准确,不做过多的业务处理CXTimer::Instance().set_timer(2000, INVALID_TIMER_ID, [this](uint64_t timeid) {if (is_ready()){CXTimer::Instance().kill_timer(timeid);return;}//通过STUN服务获取外网IP:PORTstun_cli::StunHeader req_hdr;stun_cli::build_binding_request(&req_hdr);m_pcli->sendto(&req_hdr, sizeof(req_hdr));}, true);}void set_peer(const std::string& ip, const unsigned short port){std::lock_guard<std::mutex> lk(m_mtx);m_peer_ip = ip;m_peer_port = port;//交换地址后发送探测包do_detect();}void get_own_info(std::string& ip, unsigned short &port){std::lock_guard<std::mutex> lk(m_mtx);ip = m_ip;port = m_port;}private://发送探测包void do_detect(){CXTimer::Instance().set_timer(10000, INVALID_TIMER_ID, [this](uint64_t timeid) {//这里是为了方便代码测试,应该在检测完成之后do_msg//这里直接放定时器里面检查,实际代码不应该这么做if (m_btunnel_ok){CXTimer::Instance().kill_timer(timeid);do_msg();return;}unsigned short sport = 0;std::string ip;get_peer_info(ip, sport);if (!ip.empty()){sockaddr_u local_addr;memset(&local_addr, 0, sizeof(local_addr));int ret = sockaddr_set_ipport(&local_addr, ip.c_str(), sport);m_pcli->sendto("{}", &local_addr.sa);}},true);}void do_msg(){CXTimer::Instance().set_timer(10000, INVALID_TIMER_ID, [this](uint64_t timeid) {unsigned short sport = 0;std::string ip;get_peer_info(ip, sport);sockaddr_u local_addr;memset(&local_addr, 0, sizeof(local_addr));int ret = sockaddr_set_ipport(&local_addr, ip.c_str(), sport);m_pcli->sendto(fmt::format("[{0}] say:hellow {1}",(ULONGLONG)this,time(nullptr)), &local_addr.sa);});}void update(const std::string &ip,const unsigned short port){std::lock_guard<std::mutex> lk(m_mtx);m_ip = ip;m_port = port;}void get_peer_info(std::string& ip, unsigned short &port){std::lock_guard<std::mutex> lk(m_mtx);ip = m_peer_ip;port = m_peer_port;}bool is_ready(){std::lock_guard<std::mutex> lk(m_mtx);return !m_ip.empty();}
private:std::mutex m_mtx;std::shared_ptr<hv::UdpClient> m_pcli;std::string m_ip;unsigned short m_port;std::atomic_bool m_btunnel_ok;std::string m_peer_ip;unsigned short m_peer_port;};class UdpNAT
{
public:static UdpNAT& Instance(){static UdpNAT sInstance;return sInstance;}//测试通过void Test(){m_pcliA = std::make_shared<UDPCli>();m_pcliB = std::make_shared<UDPCli>();bool bChange = false;while (true){if (!bChange){//交换地址std::string ipA, ipB;unsigned short portA, portB;m_pcliA->get_own_info(ipA, portA);m_pcliB->get_own_info(ipB, portB);if (!ipA.empty() && !ipB.empty()){printf("ready A[%s:%d] B[%s:%d]\r\b", ipA.c_str(), portA,ipB.c_str(), portB);m_pcliA->set_peer(ipB, portB);m_pcliB->set_peer(ipA, portA);bChange = true;}}Sleep(10);}}
private:UdpNAT(){m_pcliA = m_pcliB = nullptr;}private:std::shared_ptr<UDPCli> m_pcliA;std::shared_ptr<UDPCli> m_pcliB;
};