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

发布于:2025-03-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

【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

安装:

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 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;
}

网站公告

今日签到

点亮在社区的每一天
去签到