【Linux】【網絡】UDP打洞–>不同子網下的客戶端和服務器通信(未成功版)
上次說基于UDP的打洞程序改了五版一直沒有成功,要寫一下問題所在,但是我后續又查詢了一些資料,成功實現了,這次先寫一下未成功的邏輯,我認為未成功的排查錯誤的部分也很重要,如果想直接看成功的可以直接看我的下一篇文章。
首先 基于上篇文章的UDP打洞邏輯
這里直接將圖貼出來:
我的邏輯思路:(ps:會把代碼貼到最后面。)
邏輯梳理
1 服務器端(server.c)
- 監聽與接收注冊
- 服務器創建 UDP 套接字并綁定到固定端口(5050)。
- 依次調用 recvfrom() 接收兩個客戶端(先后為 C1 和 C2)的注冊消息,獲取各自的源地址(即 NAT 映射后的公網 IP 和端口)。
- 地址交換
- 服務器把 C2 的公網地址(IP 和端口)格式化成字符串(用“^”分隔)發送給 C1。
- 同樣把 C1 的地址發送給 C2。
- 后續處理
- 服務器完成地址交換后退出(沒有額外發送探測包)。
客戶端 C1(UDPClientcc1.c)
- 兩個套接字
- 使用一個套接字(sockS)與服務器通信,另一個(sockC)用于后續對等通信,并綁定到固定端口(6003)。
- 注冊階段
- C1 向服務器發送注冊消息(“I am C1”)。
- 接收服務器返回的字符串,解析出對方地址信息(格式 “ip^port”),存入 oppositeSideAddr。
- P2P 交互循環
- 在循環中,每隔 500ms 使用 sockC 向 oppositeSideAddr 發送數據(keep-alive/消息),并嘗試接收對方回復。
客戶端 C2(UDPClientcc2.c)
- 邏輯與 C1 類似
- 使用兩個套接字,一個與服務器通信(sockS),一個用于 P2P(sockC),綁定固定端口(6002)。
- 向服務器發送注冊消息(“I am C2”),接收并解析服務器返回的對方地址信息,存入 oppositeSideAddr。
- 進入循環,每隔 500ms 向 oppositeSideAddr 發送數據,并等待回復。
執行結果
服務器:
客戶端c1:
客戶端c2:
可以看到c1,c2 一直在向從服務器獲取的公網ip和端口發送數據 但是一直未收到對端回復。
服務器在向雙方發送數據后就直接退出了。
排查問題:
考慮可能存在的問題并逐步排查:
- 服務器配置問題
- 確保服務器S正確交換了雙方的公網IP和端口信息,并且客戶端解析無誤。
- 防火墻設置
- 檢查云服務器、客戶端以及NAT設備的防火墻是否允許UDP流量通過,特別是目標端口是否開放。
- 也需要確保雙方的UDP打洞程序所在主機允許接收來自對端的UDP數據包。
- NAT映射問題
- 可能兩端的NAT設備類型不支持直接UDP打洞,或映射策略比較嚴格(例如對稱NAT)。
- 您可以檢查客戶端所在網絡的NAT類型,嘗試在不同網絡環境下測試。
- 端口綁定和映射問題
- 確認代碼中綁定的本地端口(6003、6002)與NAT映射結果是否符合預期。
- 有些NAT設備可能會復用端口或調整外部映射,導致雙方看到相同的公網端口,從而影響打洞效果。
- 代碼邏輯問題
- 您的代碼中目前只是不斷發送數據包,但并未實現對收到數據包進行有效處理。如果對端也沒有收到數據包,可能是由于發送方向NAT設備發送的數據包沒有成功映射到對端。
1 服務器是否正確交換了雙方的ip和端口
這個測試結果是我第四版的結果在里面已經打印出來對應的ip,端口我這邊對比了并未出現問題 你們可以再看看上面的圖片
結論:正常
2防火墻設置
本地防火墻: 檢查客戶端和服務器上的防火墻狀態(使用 ufw status、iptables -L 等命令),確認UDP目標端口是否被允許。
云防火墻: 登錄云服務器控制臺或路由器管理界面,檢查是否設置了安全組或防火墻規則,確保允許相應的UDP流量(包括注冊端口和通信端口)。
2.1 本地防火墻
本地防火墻未打開
2.2 云服務器
防火墻對應端口已開啟
結論:正常
3 抓包查看數據包是否發送出去
在Ubuntu下,使用抓包工具來監控和分析網絡數據包的流向,常用的工具包括 tcpdump(命令行)和 Wireshark(圖形界面)。
3.1. 使用 tcpdump
安裝:
sudo apt-get update
sudo apt-get install tcpdump
基本用法:
-
抓取所有數據包:
sudo tcpdump -i eth0
其中
eth0
是您要監控的網絡接口,可以通過命令ifconfig
查看接口名稱。
我的就是ens33
-
過濾特定協議和端口:
例如,抓取UDP數據包:
sudo tcpdump -i ens33 udp
抓取目的端口為5050的UDP數據包:
sudo tcpdump -i ens33 udp port 5050
抓包發現數據發送出去了
3 NAT映射問題
使用 stun
工具
1. 安裝 stun 客戶端:
在 Ubuntu系統上運行:
sudo apt update
sudo apt install stun-client -y
2. 運行 STUN 客戶端測試 NAT 類型
stun stun.l.google.com
或者:
stun stun.sipgate.net
這是我的結果
-
Independent Mapping(獨立映射):
每個內部端口的映射是獨立的,即無論目標地址如何變化,都保持相同的映射。對 UDP 打洞來說,這通常是有利的。 -
Independent Filter(獨立過濾):
外部數據包只要符合映射的端口,就會被放行,與發送目標無關。這意味著只要內網設備先發起通信,外部的回復通常能通過 NAT 設備到達內網。 -
Random Port(隨機端口):
每個新連接可能會被 NAT 分配一個隨機的外部端口,這可能會導致端口映射不固定。為了保持連接,客戶端需要持續發送數據包以維持映射。 -
No Hairpin:
表示 NAT 不支持內部設備通過公網地址直接訪問同一 NAT 內的其他設備(NAT 回環)。這通常對 UDP 打洞影響不大,因為 C1 和 C2 是處于不同 NAT 或在不同網絡下。 -
Return value is 0x000012:
表示 STUN 客戶端檢測成功,但沒有顯示映射的端口詳細信息,通常這意味著端口由 NAT 設備隨機分配。
這個結果說明 NAT 環境是相對有利于 UDP 打洞的(非對稱 NAT),但由于隨機端口的特性,客戶端必須持續發送保持 UDP 映射(Keep-Alive)。
3. NAT 類型
- Full Cone NAT(全錐形 NAT) ? UDP 打洞最容易成功
- Restricted Cone NAT(受限錐形 NAT) ? 需要雙向數據包打洞
- Port-Restricted Cone NAT(端口受限錐形 NAT) ? 可能無法直接打洞
- Symmetric NAT(對稱 NAT) ? UDP 打洞幾乎不可能成功
證明NAT映射支持打洞
最后懷疑問題出在代碼邏輯上,服務器返回的端口雖然正確,但 NAT設備 在一段時間后修改了端口映射,或者端口映射被丟棄,導致 C1 發送到錯誤端口。因此后續需要修改端口。
server.c
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>#define DEFAULT_PORT 5050
#define BUFFER_SIZE 100int main() {// server即外網服務器int serverPort = DEFAULT_PORT;int serverListen;struct sockaddr_in serverAddr;// 建立監聽socketserverListen = socket(AF_INET, SOCK_DGRAM, 0);if (serverListen == -1) {perror("socket() failed");return -1;}serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(serverPort);serverAddr.sin_addr.s_addr = INADDR_ANY;if (bind(serverListen, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("bind() failed");return -1;}// 接收來自客戶端的連接,source1即先連接到S的客戶端C1struct sockaddr_in sourceAddr1;socklen_t sourceAddrLen1 = sizeof(sourceAddr1);char bufRecv1[BUFFER_SIZE];int len;len = recvfrom(serverListen, bufRecv1, sizeof(bufRecv1), 0, (struct sockaddr *)&sourceAddr1, &sourceAddrLen1);if (len == -1) {perror("recvfrom() failed");return -1;}bufRecv1[len] = '\0';printf("C1 IP:[%s],PORT:[%d]\n", inet_ntoa(sourceAddr1.sin_addr), ntohs(sourceAddr1.sin_port));// 接收來自客戶端的連接,source2即后連接到S的客戶端C2struct sockaddr_in sourceAddr2;socklen_t sourceAddrLen2 = sizeof(sourceAddr2);char bufRecv2[BUFFER_SIZE];len = recvfrom(serverListen, bufRecv2, sizeof(bufRecv2), 0, (struct sockaddr *)&sourceAddr2, &sourceAddrLen2);if (len == -1) {perror("recvfrom() failed");return -1;}bufRecv2[len] = '\0';printf("C2 IP:[%s],PORT:[%d]\n", inet_ntoa(sourceAddr2.sin_addr), ntohs(sourceAddr2.sin_port));// 向C1發送C2的外網ip和portchar bufSend1[BUFFER_SIZE];// bufSend1中存儲C2的外網ip和portmemset(bufSend1, '\0', sizeof(bufSend1));char *ip2 = inet_ntoa(sourceAddr2.sin_addr);// C2的ipchar port2[10];// C2的portsnprintf(port2, sizeof(port2), "%d", ntohs(sourceAddr2.sin_port));snprintf(bufSend1, sizeof(bufSend1), "%s^%s", ip2, port2);len = sendto(serverListen, bufSend1, strlen(bufSend1), 0, (struct sockaddr *)&sourceAddr1, sourceAddrLen1);if (len == -1) {perror("sendto() failed");return -1;} else {printf("send() byte:%d\n", len);}// 向C2發送C1的外網ip和portchar bufSend2[BUFFER_SIZE];// bufSend2中存儲C1的外網ip和portmemset(bufSend2, '\0', sizeof(bufSend2));char *ip1 = inet_ntoa(sourceAddr1.sin_addr);// C1的ipchar port1[10];// C1的portsnprintf(port1, sizeof(port1), "%d", ntohs(sourceAddr1.sin_port));snprintf(bufSend2, sizeof(bufSend2), "%s^%s", ip1, port1);len = sendto(serverListen, bufSend2, strlen(bufSend2), 0, (struct sockaddr *)&sourceAddr2, sourceAddrLen2);if (len == -1) {perror("sendto() failed");return -1;} else {printf("send() byte:%d\n", len);}// server的中間人工作已完成,退出即可,剩下的交給C1與C2相互通信close(serverListen);return 0;
}
client1.c
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>#define PORT 6003
#define BUFFER_SIZE 100int main(int argc, char* argv[]) {struct sockaddr_in serverAddr;struct sockaddr_in thisAddr;thisAddr.sin_family = AF_INET;thisAddr.sin_port = htons(PORT);thisAddr.sin_addr.s_addr = INADDR_ANY;if (argc < 3) {printf("Usage: UDPClient1 <Server IP address> <Server Port>\n");return -1;}int sockS = socket(AF_INET, SOCK_DGRAM, 0);if (sockS == -1) {perror("socket() failed");return -1;}if (bind(sockS, (struct sockaddr *)&thisAddr, sizeof(thisAddr)) == -1) {perror("bind() failed");return -1;}int sockC = socket(AF_INET, SOCK_DGRAM, 0);if (sockC == -1) {perror("socket() failed");return -1;}// 允許端口復用int optval = 1;setsockopt(sockC, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));// 綁定固定端口 6003struct sockaddr_in bindAddr;bindAddr.sin_family = AF_INET;bindAddr.sin_port = htons(6003);bindAddr.sin_addr.s_addr = INADDR_ANY;bind(sockC, (struct sockaddr *)&bindAddr, sizeof(bindAddr));char bufSend[] = "I am C1";char bufRecv[BUFFER_SIZE];memset(bufRecv, '\0', sizeof(bufRecv));struct sockaddr_in sourceAddr;socklen_t sourceAddrLen = sizeof(sourceAddr);struct sockaddr_in oppositeSideAddr;int len;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(atoi(argv[2]));serverAddr.sin_addr.s_addr = inet_addr(argv[1]);len = sendto(sockS, bufSend, sizeof(bufSend), 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr));if (len == -1) {perror("sendto() to S failed");return -1;}printf("C1 sent registration packet to server S.\n");len = recvfrom(sockS, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)&sourceAddr, &sourceAddrLen);if (len == -1) {perror("recvfrom() from S failed");return -1;}bufRecv[len] = '\0';printf("C1 received from S: %s\n", bufRecv);close(sockS);char ip[20];char port[10];int i = 0;while (i < strlen(bufRecv) && bufRecv[i] != '^') {ip[i] = bufRecv[i];i++;}ip[i] = '\0';int j = 0;i++;while (i < strlen(bufRecv)) {port[j++] = bufRecv[i++];}port[j] = '\0';oppositeSideAddr.sin_family = AF_INET;oppositeSideAddr.sin_port = htons(atoi(port));oppositeSideAddr.sin_addr.s_addr = inet_addr(ip);int flags = fcntl(sockC, F_GETFL, 0);fcntl(sockC, F_SETFL, flags | O_NONBLOCK);printf("C1 will now try to communicate directly with C2 at %s:%s\n", ip, port);int attempts = 0;while (1) {usleep(500000); // 500ms 發送一次len = sendto(sockC, bufSend, sizeof(bufSend), 0, (struct sockaddr *)&oppositeSideAddr, sizeof(oppositeSideAddr));if (len == -1) {perror("sendto() to C2 failed");} else {printf("Sent keep-alive UDP packet to %s:%d\n", inet_ntoa(oppositeSideAddr.sin_addr), ntohs(oppositeSideAddr.sin_port));}len = recvfrom(sockC, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)&sourceAddr, &sourceAddrLen);if (len == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {attempts++;if (attempts % 10 == 0) {printf("No response from C2 after 5 seconds. Retrying...\n");}continue;} else {perror("recvfrom() failed");break;}} else {bufRecv[len] = '\0';printf("C1 received from C2 [%s:%d]: %s\n", inet_ntoa(sourceAddr.sin_addr), ntohs(sourceAddr.sin_port), bufRecv);attempts = 0; // 成功收到數據,重置重試計數}}close(sockC);return 0;
}
client2.c
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>#define PORT 6002
#define BUFFER_SIZE 100int main(int argc, char* argv[]) {struct sockaddr_in serverAddr;struct sockaddr_in thisAddr;thisAddr.sin_family = AF_INET;thisAddr.sin_port = htons(PORT);thisAddr.sin_addr.s_addr = INADDR_ANY;if (argc < 3) {printf("Usage: UDPClient2 <Server IP address> <Server Port>\n");return -1;}int sockS = socket(AF_INET, SOCK_DGRAM, 0);if (sockS == -1) {perror("socket() failed");return -1;}if (bind(sockS, (struct sockaddr *)&thisAddr, sizeof(thisAddr)) == -1) {perror("bind() failed");return -1;}int sockC = socket(AF_INET, SOCK_DGRAM, 0);if (sockC == -1) {perror("socket() failed");return -1;}// 允許端口復用int optval = 1;setsockopt(sockC, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));// 綁定固定端口 6002struct sockaddr_in bindAddr;bindAddr.sin_family = AF_INET;bindAddr.sin_port = htons(6002);bindAddr.sin_addr.s_addr = INADDR_ANY;bind(sockC, (struct sockaddr *)&bindAddr, sizeof(bindAddr));char bufSend[] = "I am C2";char bufRecv[BUFFER_SIZE];memset(bufRecv, '\0', sizeof(bufRecv));struct sockaddr_in sourceAddr;socklen_t sourceAddrLen = sizeof(sourceAddr);struct sockaddr_in oppositeSideAddr;int len;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(atoi(argv[2]));serverAddr.sin_addr.s_addr = inet_addr(argv[1]);len = sendto(sockS, bufSend, sizeof(bufSend), 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr));if (len == -1) {perror("sendto() to S failed");return -1;}printf("C2 sent registration packet to server S.\n");len = recvfrom(sockS, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)&sourceAddr, &sourceAddrLen);if (len == -1) {perror("recvfrom() from S failed");return -1;}bufRecv[len] = '\0';printf("C2 received from S: %s\n", bufRecv);close(sockS);char ip[20];char port[10];int i = 0;while (i < strlen(bufRecv) && bufRecv[i] != '^') {ip[i] = bufRecv[i];i++;}ip[i] = '\0';int j = 0;i++;while (i < strlen(bufRecv)) {port[j++] = bufRecv[i++];}port[j] = '\0';oppositeSideAddr.sin_family = AF_INET;oppositeSideAddr.sin_port = htons(atoi(port));oppositeSideAddr.sin_addr.s_addr = inet_addr(ip);int flags = fcntl(sockC, F_GETFL, 0);fcntl(sockC, F_SETFL, flags | O_NONBLOCK);printf("C2 will now try to communicate directly with C1 at %s:%s\n", ip, port);int attempts = 0;while (1) {usleep(500000); // 500ms 發送一次len = sendto(sockC, bufSend, sizeof(bufSend), 0, (struct sockaddr *)&oppositeSideAddr, sizeof(oppositeSideAddr));if (len == -1) {perror("sendto() to C1 failed");} else {printf("Sent keep-alive UDP packet to %s:%d\n", inet_ntoa(oppositeSideAddr.sin_addr), ntohs(oppositeSideAddr.sin_port));}len = recvfrom(sockC, bufRecv, sizeof(bufRecv), 0, (struct sockaddr *)&sourceAddr, &sourceAddrLen);if (len == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {attempts++;if (attempts % 10 == 0) {printf("No response from C1 after 5 seconds. Retrying...\n");}continue;} else {perror("recvfrom() failed");break;}} else {bufRecv[len] = '\0';printf("C2 received from C1 [%s:%d]: %s\n", inet_ntoa(sourceAddr.sin_addr), ntohs(sourceAddr.sin_port), bufRecv);attempts = 0; // 成功收到數據,重置重試計數}}close(sockC);return 0;
}