首页 文章

基于TCP的打孔

提问于
浏览
1

我一直在尝试TCP打孔一段时间,而论坛在基于TCP的方法和C编程语言方面似乎没什么帮助 . 以下是互联网的主要参考资料,

一个 . http://www.brynosaurus.com/pub/net/p2pnat/
https://wuyongzheng.wordpress.com/2013/01/31/experiment-on-tcp-hole-punching/

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

假设客户端A知道B的公共和私有 endpoints ,并且B知道A的 endpoints (我已经编写了一个服务器'S',在对等体之间交换 endpoints 信息),并且假设两个NAT都不对称,那么就足够了(实现TCP)打孔),如果两个客户端都反复尝试连接()到彼此的公共 endpoints (对于上面的设置)?

如果没有,为了实现tcp打孔,究竟需要做些什么?

我在每个客户端上有两个线程,一个对其他客户端重复进行连接调用,另一个用于侦听来自其他客户端的传入连接 . 我确保两个线程中的套接字都绑定到给对等端的本地端口 . 此外,我看到两个NAT都保留了端口映射,即本地和公共端口是相同的 . 然而,我的计划不起作用 .

是否这样我上面提到的集合点服务器“S”在打孔或创建允许SYN请求通过的NAT映射时可以发挥作用 . 如果是,那该怎么办?

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

谢谢,
Dinkar

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 回答

  • 4

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

    并在EDIT2之后阅读该部分(摘录于此处) . 这可能是失败的原因 .

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

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

    对于AF_INET套接字,这意味着套接字可以绑定,除非有一个绑定到该地址的活动侦听套接字 . 当侦听套接字绑定到具有特定端口的INADDR_ANY时,则无法绑定到此端口以获取任何本地地址

    我认为不是之前的倾听会有所作为 .

    您不必尝试打开两次连接 .

    Build TCP连接的步骤摘要:左侧:(连接到服务器S的客户端C)是常见情况,右侧是两个对等方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#page-87(第4.2.2.10节)

    不同的是同时发送SYN * 2 / SYN ACK * 2而不是SYN / SYN ACK / ACK(在我的测试中有两个linux对等体,通常只有SYN ACK的“第一”答案,因为它永远不会同时发生 . 它不会真的很重要) .

    两个对等体都主动发起连接 . 他们最初并没有等待连接和 you don't have to call listen()/accept() at all . 你 don't have to use any threads at all .

    每个对等体应该(通过S)交换它们预期的本地端口以供另一个使用(并且在S的帮助下它们将交换它们的公共IP),假设端口不会被翻译 .

    现在,您只需尝试连接您的4-uple信息 . 每个都将与(INADDR_ANY,lport)绑定并连接到(peer_global_address,peer_global_port),而同时B也会这样做 . 双方之间 At the end there is an UNIQUE connection established .

    两个NAT盒都将看到传出的数据包并准备一个反向路径 .

    现在可能出问题了?

    • NAT盒子必须't cope with the expected packet having a SYN instead of the more common SYN+ACK. Sorry, if that happens you might be out of luck. TCP protocol allows for this case and it'强制性(rfc 1122上面4.2.2.10节) . 如果其他NAT盒正常,它应该仍然有效(一旦发回SYN ACK) .

    • NAT设备(来自同行做太晚的请求,比如说B前面的NAT-B)用RST数据包回答,而不是像大多数NAT设备那样静默地丢弃仍然未知的数据包 . A接收RST并中止连接 . 然后B发送它并发生类似的命运 . ping往返越快,你就越容易得到它 . 为避免这种情况,要么:

    • 如果你可以控制其中一个NAT设备,让它丢弃数据包而不是发送RST .

    • 真正同步(使用NTP,交换a通过S在对等体之间的预期动作的精确日期(以毫秒为单位),或者等待下一个5秒钟的多次开始)

    • 使用A和/或B上的自定义(和临时)防火墙规则丢弃传出RST数据包(优于丢弃传入RST,因为NAT设备可以决定在看到它时关闭期望)

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

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

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

    完成后,netcat都有 established the SAME UNIQUE connection together ,没有 Build 两个连接 . 不需要重试(无循环) . 当然程序将使用connect(),如果第二个命令(以及connect())被延迟,OS可能在connect()期间发送了多个SYN数据包作为自动重试机制 . 这是系统/内核级别,而不是您的级别 .

    我希望这有帮助,这样你就可以简化程序并让它运行起来 . 记住,不需要listen(),accept(),不得不分叉,使用线程 . 你甚至不需要select(),只需要在没有O_NONBLOCK的情况下使用connect()块 .

相关问题