【问题标题】:TCP based hole punching基于 TCP 的打孔
【发布时间】:2017-01-25 12:10:59
【问题描述】:

我一直在尝试 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 -- 互联网 -- NAT-B -- 客户端 B。

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

如果没有,具体要怎么做才能实现tcp打孔?

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

是不是我上面提到的集合服务器“S”在打孔或创建允许 SYN 请求通过的 NAT 映射中发挥作用,到达对等点。如果是,需要做什么?

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

谢谢,
丁卡

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

【问题讨论】:

    标签: c tcp hole-punching


    【解决方案1】:

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

    并阅读 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://www.rfc-editor.org/rfc/rfc793#section-3.4 图 8。

    不同之处在于同步 SYN2/SYN+ACK2 而不是 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 设备(丢弃未知数据包),不需要任何完美的同步,即使这两个命令之间有 5 秒也可以(当然第一个会等待):

    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(),不用fork,使用线程。您甚至不需要 select(),只需正常使用 connect() 块,而无需 O_NONBLOCK。

    【讨论】:

    • 首先,非常感谢 A.B,因为描述性的答案让我有了更多的洞察力。更新:我更换了一侧的 NAT 盒子,事情开始工作了!!然而,要弄清楚它是什么,这个 NAT 并不高兴。一些问题。您提到根本不需要听。如果我的设置是 A -- NAT-A -- 互联网 -- B 我可以看到在这种情况下接受成功,并且没有侦听器线程不起作用。好吧,我不能假设 NAT 会一直存在。你认为这应该只适用于连接。再次感谢。丁卡尔
    • 上一条评论的继续:如果客户端 A 和 B 位于组织内的不同子网(例如低于 2 个 NAT)上,如果他们尝试在专用端点上连接,相同的逻辑是否会起作用。我这几天一直在尝试,目前没有成功。
    • 我的解释主要是为了让您了解必须没有接受,只有连接。所以确实你不能使用接受,我已经写过了。如果您接受工作,那可能是因为您成功混淆了 NAT 框。但这不是正确的方法。我测试了我写的东西,它真的在没有接受的情况下工作,也没有任何重试循环。只是来自每一侧的连接。至于一个组织,它通常有防火墙(NAT 不是防火墙,它恰好有一些防火墙属性)所以可能源端口 80 以及两边的目标端口......并且有日志......
    • 并且不要因为某个地方告诉使用connect+accept就认为它是正确的。如果您阅读有关链接内容的 cmets,您会发现它无法按预期工作
    • 同意。如果两端都有 NAT(NAT-A 和 NAT-B),则永远不需要接受,我在我的程序中也看到了它。但是,如果 NAT-A 或 NAT-B 之一不存在,则“仅在两端连接”不能可靠地工作,它需要“连接 + 接受”。无论如何,我感谢您的回答,并感谢您抽出宝贵时间回复我的问题。
    猜你喜欢
    • 2012-02-07
    • 2016-12-05
    • 2011-09-19
    • 2014-03-08
    • 2014-06-08
    • 2015-02-10
    • 1970-01-01
    • 2014-12-28
    • 2015-05-25
    相关资源
    最近更新 更多