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

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

上次说基于UDP的打洞程序改了五版一直没有成功,要写一下问题所在,但是我后续又查询了一些资料,成功实现了,这次先写一下未成功的逻辑,我认为未成功的排查错误的部分也很重要,如果想直接看成功的可以直接看我的下一篇文章。

首先 基于上篇文章的UDP打洞逻辑

这里直接将图贴出来:

我的逻辑思路:(ps:会把代码贴到最后面。)

逻辑梳理

1 服务器端(server.c)

  1. 监听与接收注册
    • 服务器创建 UDP 套接字并绑定到固定端口(5050)。
    • 依次调用 recvfrom() 接收两个客户端(先后为 C1 和 C2)的注册消息,获取各自的源地址(即 NAT 映射后的公网 IP 和端口)。
  2. 地址交换
    • 服务器把 C2 的公网地址(IP 和端口)格式化成字符串(用"^"分隔)发送给 C1。
    • 同样把 C1 的地址发送给 C2。
  3. 后续处理
    • 服务器完成地址交换后退出(没有额外发送探测包)。

客户端 C1(UDPClientcc1.c)

  1. 两个套接字
    • 使用一个套接字(sockS)与服务器通信,另一个(sockC)用于后续对等通信,并绑定到固定端口(6003)。
  2. 注册阶段
    • C1 向服务器发送注册消息("I am C1")。
    • 接收服务器返回的字符串,解析出对方地址信息(格式 "ip^port"),存入 oppositeSideAddr。
  3. P2P 交互循环
    • 在循环中,每隔 500ms 使用 sockC 向 oppositeSideAddr 发送数据(keep-alive/消息),并尝试接收对方回复。

客户端 C2(UDPClientcc2.c)

  1. 逻辑与 C1 类似
    • 使用两个套接字,一个与服务器通信(sockS),一个用于 P2P(sockC),绑定固定端口(6002)。
    • 向服务器发送注册消息("I am C2"),接收并解析服务器返回的对方地址信息,存入 oppositeSideAddr。
    • 进入循环,每隔 500ms 向 oppositeSideAddr 发送数据,并等待回复。

执行结果

服务器:

客户端c1:

客户端c2:

可以看到c1,c2 一直在向从服务器获取的公网ip和端口 发送数据 但是一直未收到对端回复。

服务器在向双方发送数据后就直接退出了。

排查问题:

考虑可能存在的问题并逐步排查:

  1. 服务器配置问题
    • 确保服务器S正确交换了双方的公网IP和端口信息,并且客户端解析无误。
  2. 防火墙设置
    • 检查云服务器、客户端以及NAT设备的防火墙是否允许UDP流量通过,特别是目标端口是否开放。
    • 也需要确保双方的UDP打洞程序所在主机允许接收来自对端的UDP数据包。
  3. NAT映射问题
    • 可能两端的NAT设备类型不支持直接UDP打洞,或映射策略比较严格(例如对称NAT)。
    • 您可以检查客户端所在网络的NAT类型,尝试在不同网络环境下测试。
  4. 端口绑定和映射问题
    • 确认代码中绑定的本地端口(6003、6002)与NAT映射结果是否符合预期。
    • 有些NAT设备可能会复用端口或调整外部映射,导致双方看到相同的公网端口,从而影响打洞效果。
  5. 代码逻辑问题
    • 您的代码中目前只是不断发送数据包,但并未实现对收到数据包进行有效处理。如果对端也没有收到数据包,可能是由于发送方向NAT设备发送的数据包没有成功映射到对端。

1 服务器是否正确交换了双方的ip和端口

这个测试结果是我第四版的结果在里面已经打印出来对应的ip,端口我这边对比了并未出现问题 你们可以再看看上面的图片

结论:正常

2防火墙设置

本地防火墙: 检查客户端和服务器上的防火墙状态(使用 ufw status、iptables -L 等命令),确认UDP目标端口是否被允许。

云防火墙: 登录云服务器控制台或路由器管理界面,检查是否设置了安全组或防火墙规则,确保允许相应的UDP流量(包括注册端口和通信端口)。

2.1 本地防火墙

本地防火墙未打开

2.2 云服务器

防火墙对应端口已开启

结论:正常

3 抓包查看数据包是否发送出去

在Ubuntu下,使用抓包工具来监控和分析网络数据包的流向,常用的工具包括 tcpdump (命令行)和 Wireshark(图形界面)。


3.1. 使用 tcpdump

安装:

bash 复制代码
sudo apt-get update
sudo apt-get install tcpdump

基本用法:

  • 抓取所有数据包:

    bash 复制代码
    sudo tcpdump -i eth0

    其中 eth0 是您要监控的网络接口 ,可以通过命令 ifconfig 查看接口名称。

    我的就是ens33

  • 过滤特定协议和端口:

    例如,抓取UDP数据包:

    bash 复制代码
    sudo tcpdump -i ens33 udp

    抓取目的端口为5050的UDP数据包:

    bash 复制代码
    sudo tcpdump -i ens33 udp port 5050

    抓包发现数据发送出去了

3 NAT映射问题

使用 stun 工具

1. 安装 stun 客户端:

在 Ubuntu系统上运行:

bash 复制代码
sudo apt update
sudo apt install stun-client -y

2. 运行 STUN 客户端测试 NAT 类型

bash 复制代码
stun stun.l.google.com

或者:

bash 复制代码
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

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

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

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;
}
相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5204 天前
Linux 11 动态监控指令top
linux