本文基于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;
};