主页 > 互联网  > 

【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)

【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)
【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

或者:

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 100 int main() { // server即外网服务器 int serverPort = DEFAULT_PORT; int serverListen; struct sockaddr_in serverAddr; // 建立监听socket serverListen = 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的客户端C1 struct 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的客户端C2 struct 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和port char bufSend1[BUFFER_SIZE];// bufSend1中存储C2的外网ip和port memset(bufSend1, '\0', sizeof(bufSend1)); char *ip2 = inet_ntoa(sourceAddr2.sin_addr);// C2的ip char port2[10];// C2的port snprintf(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和port char bufSend2[BUFFER_SIZE];// bufSend2中存储C1的外网ip和port memset(bufSend2, '\0', sizeof(bufSend2)); char *ip1 = inet_ntoa(sourceAddr1.sin_addr);// C1的ip char port1[10];// C1的port snprintf(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 100 int 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)); // 绑定固定端口 6003 struct 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 100 int 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)); // 绑定固定端口 6002 struct 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; }
标签:

【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)