当前位置: 首页 > 知识库问答 >
问题:

基于TCP的打孔

朱欣荣
2023-03-14

我已经尝试TCP打孔一段时间了,论坛似乎对基于TCP的方法和C编程语言没有多大帮助。主要参考文献如下,

我的设置是
客户端A--NAT-A--Internet--NAT-B--Client B。

假设客户机A知道B的公共endpoint和私有endpoint,而B知道A的endpoint(我已经编写了一个服务器'S',用于在对等点之间交换endpoint信息),并且两个NAT都不对称,如果两个客户机都尝试重复连接()到对方的公共endpoint(对于上述设置),这是否足够(实现TCP穿孔)?

如果不是,到底要做些什么才能实现tcp打孔呢?

我在每个客户端上都有两个线程,一个反复对其他客户端进行connect调用,另一个监听来自其他客户端的传入连接。我已经确保两个线程中的套接字都绑定到给对等体的本地端口。另外,我看到两个NAT都保留了端口映射,即本地端口和公共端口是相同的。但是,我的程序不起作用。

是不是让我上面提到的会合服务器“S”在打孔或创建允许SYN请求通过的NAT映射中发挥作用。若有,应如何处理?

后面附上了代码的相关部分。
connect_with_peer()是入口点,服务器“%s”提供了对等方的公共ip:port元组,该元组连同绑定到的本地端口一起提供给此函数。这个函数生成一个线程(accept_handler()),它也绑定到本地端口,并侦听来自对等端口的传入连接。如果connect()[主线程]或accept()[子线程]成功,则connect_with_peer()返回一个套接字。

谢谢,
丁卡

volatile int quit_connecting=0;

void *accept_handler(void *arg)
{
    int i,psock,cnt=0;
    int port = *((int *)arg);
    ssize_t len;
    int asock,opt,fdmax;
    char str[BUF_SIZE];
    struct sockaddr_in peer,local;
    socklen_t peer_len = sizeof(peer);
    fd_set master,read_fds;    // master file descriptor list
    struct timeval tv = {10, 0}; // 10 sec timeout
    int *ret_sock = NULL;
    struct linger lin;
    lin.l_onoff=1;
    lin.l_linger=0;

    opt=1;
    //Create socket
    asock = socket(AF_INET , SOCK_STREAM, IPPROTO_TCP);

    if (asock == -1)
    {
        fprintf(stderr,"Could not create socket");
        goto quit_ah;
    }
    else if (setsockopt(asock, SOL_SOCKET, SO_LINGER, &lin,
                        (socklen_t) sizeof lin) < 0)
    {
        fprintf(stderr,"\nTCP set linger socket options failure");
        goto quit_ah;
    }
    else if (setsockopt(asock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
                        (socklen_t) sizeof opt) < 0)
    {
        fprintf(stderr,"\nTCP set csock options failure");
        goto quit_ah;
    }


    local.sin_family = AF_INET;         /* host byte order */
    local.sin_port = htons(port);     /* short, network byte order */
    local.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
    bzero(&(local.sin_zero), 8);        /* zero the rest of the struct */

fprintf(stderr,"\naccept_handler: binding to port %d",port);

    if (bind(asock, (struct sockaddr *)&local, sizeof(struct sockaddr)) == -1) {
        perror("accept_handler bind error :");
        goto quit_ah;
    }

    if (listen(asock, 1) == -1) {
        perror(" accept_handler listen");
        goto quit_ah;
    }

    memset(&peer, 0, sizeof(peer));
    peer.sin_addr.s_addr = inet_addr(peer_global_address);
    peer.sin_family = AF_INET;
    peer.sin_port = htons( peer_global_port );

    FD_ZERO(&master);    // clear the master and temp sets
    FD_SET(asock, &master);
    fdmax = asock; // so far, it's this one

    // Try accept
    fprintf(stderr,"\n listen done; accepting next ... ");

    while(quit_connecting == 0){
        read_fds = master; // copy it
        if (select(fdmax+1, &read_fds, NULL, NULL, &tv) == -1) {
            perror("accept_handler select");
            break;
        }
        // run through the existing connections looking for data to read
        for(i = 0; i <= fdmax; i++) {
            if (FD_ISSET(i, &read_fds)) { // we got one!!
                if (i == asock) {
                    // handle new connections
                    psock = accept(asock, (struct sockaddr *)&peer, (socklen_t*)&peer_len);

                    if (psock == -1) {
                        perror("accept_handler accept");
                    } else {
                        fprintf(stderr,"\n Punch accept in thread succeeded soc=%d....",psock);
                        quit_connecting = 1;

                        ret_sock = malloc(sizeof(int));
                        if(ret_sock){
                            *ret_sock = psock;
                        }

                    }
                }
            }
        } // end for
    }


quit_ah:

    if(asock>=0) {
        shutdown(asock,2);
        close(asock);
    }
    pthread_exit((void *)ret_sock);

    return (NULL);
}



int connect_with_peer(char *ip, int port, int lport)
{
    int retval=-1, csock=-1;
    int *psock=NULL;
    int attempts=0, cnt=0;
    int rc=0, opt;
    ssize_t len=0;
    struct sockaddr_in peer, apeer;
    struct sockaddr_storage from;
    socklen_t peer_len = sizeof(peer);
    socklen_t fromLen = sizeof(from);
    char str[64];
    int connected = 0;
    pthread_t accept_thread;
    long arg;
    struct timeval tv;
    fd_set myset;
    int so_error;

    struct linger lin;
    lin.l_onoff=1;
    lin.l_linger=0;

    opt=1;

    //Create socket
    csock = socket(AF_INET , SOCK_STREAM, IPPROTO_TCP);

    if (csock == -1)
    {
        fprintf(stderr,"Could not create socket");
        return -1;
    }
    else if (setsockopt(csock, SOL_SOCKET, SO_LINGER, &lin,
                        (socklen_t) sizeof lin) < 0)
    {
        fprintf(stderr,"\nTCP set linger socket options failure");
    }

#if 1
    else if (setsockopt(csock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
                        (socklen_t) sizeof opt) < 0)
    {
        fprintf(stderr,"\nTCP set csock options failure");
    }
#endif

    quit_connecting = 0;

///////////

    if( pthread_create( &accept_thread , NULL ,  accept_handler , &lport) < 0)
    {
        perror("could not create thread");
        return 1;
    }
    sleep(2); // wait for listen/accept to begin in accept_thread.

///////////
    peer.sin_family = AF_INET;         /* host byte order */
    peer.sin_port = htons(lport);     /* short, network byte order */
    peer.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
    bzero(&(peer.sin_zero), 8);        /* zero the rest of the struct */

fprintf(stderr,"\n connect_with_peer: binding to port %d",lport);

    if (bind(csock, (struct sockaddr *)&peer, sizeof(struct sockaddr)) == -1) {
        perror("connect_with_peer bind error :");
        goto quit_connect_with_peer;
    }

    // Set non-blocking 
    arg = fcntl(csock, F_GETFL, NULL); 
    arg |= O_NONBLOCK; 
    fcntl(csock, F_SETFL, arg); 

    memset(&peer, 0, sizeof(peer));
    peer.sin_addr.s_addr = inet_addr(ip);
    peer.sin_family = AF_INET;
    peer.sin_port = htons( port );

    //Connect to remote server
    fprintf(stderr,"\n Attempting to connect/punch to %s; attempt=%d",ip,attempts);
    rc = connect(csock , (struct sockaddr *)&peer , peer_len);

    if(rc == 0){ //succeeded
        fprintf(stderr,"\n Punch Connect succeeded first time....");
    } else { 
        if (errno == EINPROGRESS) { 


            while((attempts<5) && (quit_connecting==0)){
            tv.tv_sec = 10; 
            tv.tv_usec = 0; 
            FD_ZERO(&myset); 
            FD_SET(csock, &myset); 
                if (select(csock+1, NULL, &myset, NULL, &tv) > 0) { 

                    len = sizeof(so_error);
                    getsockopt(csock, SOL_SOCKET, SO_ERROR, &so_error, (socklen_t *)&len);

                    if (so_error == 0) {
                        fprintf(stderr,"\n Punch Connect succeeded ....");
                        // Set it back to blocking mode
                        arg = fcntl(csock, F_GETFL, NULL); 
                        arg &= ~(O_NONBLOCK); 
                        fcntl(csock, F_SETFL, arg);

                        quit_connecting=1;
                        retval = csock;
                    } else { // error
                        fprintf(stderr,"\n Punch select error: %s\n", strerror(so_error));
                        goto quit_connect_with_peer;
                    }

                } else { 
                    fprintf(stderr,"\n Punch select timeout: %s\n", strerror(so_error));
                } 
                attempts++;
            }// end while

        } else { //errorno is not EINPROGRESS
            fprintf(stderr, "\n Punch connect error: %s\n", strerror(errno)); 
        } 
    } 

quit_connect_with_peer:

    quit_connecting=1;
    fprintf(stderr,"\n Waiting for accept_thread to close..");
    pthread_join(accept_thread,(void **)&psock);

    if(retval == -1 ) {
        if(psock && ((*psock) != -1)){
            retval = (*psock); // Success from accept socket
        }
    }

    fprintf(stderr,"\n After accept_thread psock = %d csock=%d, retval=%d",psock?(*psock):-1,csock,retval);

    if(psock) free(psock); // Free the socket pointer , not the socket.

    if((retval != csock) && (csock>=0)){ // close connect socket if accept succeeded
        shutdown(csock,2);
        close(csock);
    }

    return retval;
}

共有1个答案

姚乐家
2023-03-14

首先,阅读这个非常相似的问题:
TCP打孔

并阅读EDIT2之后的部分(此处节选)。那可能是失败的原因。

一旦第二个套接字成功绑定,绑定到该端口的所有套接字的行为都是不确定的。

不要担心linux在带有so_reuseaddr的socket(7)中也有类似的限制:

对于AF_INET套接字,这意味着套接字可以绑定,除非有一个激活的侦听套接字绑定到该地址。当侦听套接字绑定到具有特定端口的INADDR_ANY时,则不可能为任何本地地址绑定到此端口

我不认为听后而听前会有什么不同。

建立TCP连接的步骤总结:左侧:(客户端C连接到服务器S)是通常的情况,右侧是两个对等体a和B的同时连接(您正在尝试执行的操作):

C                           A       B
  \ (SYN)                     \   /
   \                      (SYN)\ /(SYN)
     > S                        X
    /                          / \
   /(SYN+ACK)                 /   \
  /                       A <       > B
C<                            \   /
  \                   (SYN+ACK)\ / (SYN+ACK)
   \(ACK)                       X
    \                          / \
     \                        /   \
      > S                  A <     > B 
 ESTABLISHED               ESTABLISHED

参考文献:
https://tools.ietf.org/html/rfc793#section-3.4图8。+图8第7行更正:
https://tools.ietf.org/html/rfc1122#第87页(第4.2.2.10节)

区别是同步的syn*2/syn+ack*2,而不是syn/syn+ack/ack(在我对两个linux对等项的测试中,通常只有“第一个”回答syn+ack,因为它从来不是同步的,这并不重要)。

两个对等体都主动发起连接。它们最初并不等待连接,您根本不需要调用listen()/accept()。您根本不需要使用任何线程。

每个对等方都应该(通过%S)交换他们想要使用的本地端口给另一个对等方(并且在%S的帮助下,他们将交换他们的公共IP),假设端口不会被转换。

现在您只需尝试与您的4倍信息连接。每个将与(INADDR_ANY,lport)绑定并连接到(peer_global_address,peer_global_port),同时B也会这样做。最后,双方之间建立了一种独特的联系。

两个NAT框都将看到传出数据包并准备反向路径。

现在会有什么问题呢?

>

  • NAT盒无法处理具有SYN而不是更常见的SYN+ACK的预期数据包。抱歉,如果那样的话,你可能就倒霉了。TCP协议允许这种情况,并且它是强制性的(上面rfc 1122第4.2.2.10节)。如果另一个NAT盒正常,它应该仍然可以工作(一旦发送回SYN+ACK)。
  • NAT设备(来自请求执行得太晚的对等体,例如B前面的NAT-B)使用RST数据包应答,而不是像大多数NAT设备所做的那样静默丢弃仍然未知的数据包。A接收RST并中止连接。然后B发送它,类似的命运发生了。ping往返的速度越快,就越容易得到它。为了避免这种情况,可以:

    • 如果您可以控制其中一个NAT设备,请让它丢弃数据包,而不是发送RST。
    • 真正同步(使用NTP,通过%S在对等方之间交换预期操作的精确日期(以毫秒为单位),或者等待下一个5秒的倍数开始)
    • 在a和/或B上使用自定义(和临时)防火墙规则丢弃传出的RST数据包(比丢弃传入的RST更好,因为NAT设备在看到它时可以决定关闭它)

    我可以告诉你,只需在两个对等体之间使用netcat就可以让TCP打孔“手工”可靠地工作,就像你的情况一样。

    例如在带有Netcat的Linux上:同时键入两个对等体A和B上的数据,每个对等体都在其NAT设备后面的专用LAN中。对于通常的NAT设备(丢弃未知数据包),不需要任何完美的同步,即使是这两个命令之间的5s也可以(当然第一个命令会等待):

    host-a$ nc -p 7777 public-ip-host-b 8888
    host-b$ nc -p 8888 public-ip-host-a 7777
    

    完成后,两个netcat都在一起建立了相同的唯一连接,而不是建立了两个连接。不需要重试(不需要循环)。当然,程序将使用connect(),如果第二个命令(因此connect())被延迟,则操作系统可能在connect()期间发送多个SYN数据包作为自动重试机制。这是在系统/内核级别,而不是在您的级别。

    我希望这有助于您简化您的程序并使其工作。记住,不需要listen()、accept(),必须分叉,使用线程。您甚至不需要select(),只需要正常使用connect()块而不使用O_nonblock。

  •  类似资料:
    • 主要内容:本节引言:,1.运行效果图:,2.实现流程图:,3.代码示例:,4.代码下载:,5.本节小结:本节引言: 上节中我们给大家接触了Socket的一些基本概念以及使用方法,然后写了一个小猪简易聊天室的 Demo,相信大家对Socket有了初步的掌握,本节我们来学习下使用Socket来实现大文件的断点续传! 这里讲解的是别人写好的一个Socket上传大文件的例子,不要求我们自己可以写出来,需要的时候会用 就好! 1.运行效果图: 1.先把我们编写好的Socket服务端运行起来: 2.将一个音

    • 主要内容:本节引言:,1.什么是Socket?,2.Socket通信模型:,3.Socket服务端的编写:,4.Socket客户端的编写:,5.增强版案例:小猪简易聊天室,本节小结:本节引言: 上一节的概念课枯燥无味是吧,不过总有点收获是吧,本节开始我们来研究基于TCP协议的Socket 通信,先来了解下Socket的概念,以及Socket通信的模型,实现Socket的步骤,以及作为Socket服务 端与客户端的两位各做要做什么事情!好的,我们由浅入深来扣这个Socket吧! 1.什么是Sock

    • 2. 基于TCP协议的网络程序 下图是基于TCP协议的客户端/服务器程序的一般流程: 图 37.2. TCP协议通讯流程 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()

    • 本文向大家介绍nginx基于tcp做负载均衡的方法,包括了nginx基于tcp做负载均衡的方法的使用技巧和注意事项,需要的朋友参考一下 配置多台服务器时,经常需要让各个服务器之间的时间保持同步,如果服务器有外网环境,可以直接同外部的时间服务器更新时间,可以采用rdate命令更新时间: rdate -s tick.greyware.com 可以写个脚本放在/etc/cron.hourly中每小时校

    • 问题在下面。这是我当前的测试代码,它没有成功。 如何做TCP打孔?我正在使用远程服务器进行测试。我正在运行。我已经为端口80设置了路由器,所以它不需要打孔。我的代码有关联。现在我尝试其他端口,但我不知道如何打孔。 我所做的是(C#代码) 如何做TCP打孔?

    • 本文向大家介绍python 基于TCP协议的套接字编程详解,包括了python 基于TCP协议的套接字编程详解的使用技巧和注意事项,需要的朋友参考一下 基于TCP协议的套接字编程 实现电话沟通为例,这里传递的是字符,可以自己尝试去发送一个文件 模拟ssh远程执行命令(linux系统) 学习最有效的方法一定是有输入,一定要输出,这样学的东西才能真正得有用 以上就是本文的全部内容,希望对大家的学习有所