基于TCP的NAT子网穿透实验

      不得不说,在国内IP紧缺的现状下,NAT发挥了无比巨大的作用:它以把IP和端口重新分配的方式,满足了广大人民群众上网的强烈需求。但是对于个人服务器以及在内网中基于网络的嵌入式设备,却是个比较尴尬的事情:因为它把端口和IP进行了重新分配,外网客户端访问的时候很难知道server端的IP和监听端口,尤其是监听端口。这个时候,子网穿透技术就应运而生了。
      这两天看了一些简单的子网穿透的基本原理,大多数都是UDP的。但是看完后觉得TCP实现起来更为简单,因此简单的写了一个测试程序实验了一下,结果还真行。下面是实现原理:
      首先是搭建了一下网络模型,一个是在子网中的需要穿透的Server A,一个是在公网上的辅助穿透的Server S,另一个是需要连接Server A的ClientB,由于是客户端,在不在子网倒无所谓了。下面是简图:

      其次简单介绍一下实现过程:首先是Server S建立一个监听socket,用于辅助打洞。然后在Server A上建立一个socket去连接ServerS的监听端口,然后这个时候NAT A就会给此连接分配一个端口和IP,而Server S是知道此IP和端口的,然后Server S将此信息记录下来。 接着Server A要在刚才那个连接ServerS上的端口上再建立一个监听socket。然后Client B先去和Server S建立联系,获取Server A的端口和IP信息,然后就可以去连接ServerA了。
      其中有两个地方要注意,其中ServerA上的两个socket要设置SO_REUSEADDR的属性;其次是建议ServerA在连接ServerS前也先绑定一下端口。
      最后是上代码,当然其中我为了省事儿,没有搭建NAT B,也没有去实现框图中的Step2,而是采取Server S打印的方式,然后配合手动修改ClientB的连接端口实现的……然后我的Server S的IP是10.10.10.66;然后client B的IP是10.10.10.88;NAT A用的是一个TP-LINK的家用路由器,WAN口的IP是10.10.10.77,LAN口是192.168.1.1;Server A的IP是192.168.1.99,网关是192.168.1.1。
      首先是ServerS上的代码,为了省事儿,请忽视句柄泄露等问题……

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <time.h>

int main()
{
        int fd_socket;
        int fd_connect;
        struct sockaddr_in serv_addr;
        struct sockaddr_in client_addr;
        socklen_t cli_len;

        fd_socket= socket(AF_INET, SOCK_STREAM, 0);
        if(fd_socket < 0)
        {
                printf("Init socket failed!\n");
                return -1;
        }
        int iOption_value = 1;
        int iLength = sizeof(int);
        if(setsockopt(fd_socket,SOL_SOCKET,SO_REUSEADDR,&iOption_value, iLength)<0)
        {
                printf("setsockopt error\n");
                return -1;
        }
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(1234);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

        if(bind(fd_socket, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0)
        {
                perror("bind");
                printf("Bind failed!\n");
                return -2;
        }
        if(listen(fd_socket, 4) < 0)
        {
                printf("Listen failed!\n");
        }
        printf("start to accept!\n");
        while(1)
        {
                fd_connect = accept(fd_socket, (struct sockaddr *)&client_addr, &cli_len);
                printf("the ServerA ip is %s, port is %d\n",inet_ntoa(client_addr.sin_addr),htons(client_addr.sin_port));
                usleep(0);
        }
        printf("over\n");
        
        return 0;
}

     然后是ClientB的代码,其中IP(10.10.10.77)和端口(1043)是在ServerS打印之后才写进去进行编译运行的。

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <time.h>

int main()
{
        int fd_socket;
        struct sockaddr_in serv_addr;
        fd_socket= socket(AF_INET, SOCK_STREAM, 0);
        if(fd_socket < 0)
        {
                printf("Init socket failed!\n");
                return -1;
        }

        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(1043);
        serv_addr.sin_addr.s_addr = inet_addr("10.10.10.77");
        if(connect(fd_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
        {
                printf("connect error\n");
                return -1;
        }
        printf("connect success\n");
        sleep(20);        
        return 0;
}

     最后是ServerA的代码:

    

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <time.h>

int main()
{
        int fd_socket;
        int fd_connect;
        int fd_server;
        struct sockaddr_in serv_addr;
        struct sockaddr_in client_addr;
        fd_socket= socket(AF_INET, SOCK_STREAM, 0);
        if(fd_socket < 0)
        {
                printf("Init socket failed!\n");
                return -1;
        }
        int iOption_value = 1;
        int iLength = sizeof(int);
        if(setsockopt(fd_socket,SOL_SOCKET,SO_REUSEADDR,&iOption_value, iLength)<0)
        {
                printf("setsockopt error\n");
                return -1;
        }
        memset(&client_addr, 0, sizeof(client_addr));
        client_addr.sin_family = AF_INET;
        client_addr.sin_port = htons(6666);
        client_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        if(bind(fd_socket,(struct sockaddr *)&client_addr, sizeof(client_addr)) < 0)
        {
                perror("bind");
                return -1;
        }
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(1234);
        serv_addr.sin_addr.s_addr = inet_addr("10.10.10.66");
        if(connect(fd_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
        {
                printf("connect error\n");
                return -1;
        }
        memset(&client_addr, 0, sizeof(client_addr));
        int iAddrLen = sizeof(client_addr);
        if(getsockname(fd_socket, (struct sockaddr *)&client_addr, &iAddrLen) < 0)
        {
                printf("getsockname\n");
                return -1;
        }
        printf("the port is %d\n", htons(client_addr.sin_port));
        fd_server = socket(AF_INET, SOCK_STREAM, 0);
        if(fd_server < 0)
        {
                printf("Init socket failed!\n");
                return -1;
        }
        if(setsockopt(fd_server,SOL_SOCKET,SO_REUSEADDR,&iOption_value, iLength)<0)
        {
                printf("setsockopt error\n");
                return -1;
        }
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = client_addr.sin_port;
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        if(bind(fd_server,(struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
        {
                perror("bind");
                return -1;
        }
        if(listen(fd_server, 12) < 0)
        {
                perror("listen");        
        }
        printf("listen success\n");
        sleep(20);
        
        return 0;
}

      根据打印消息以及抓包数据分析来看,clientB是可以和ServerA建立连接的。
      虽然这个实验是成功了,但是当我把这种方式应用到实际项目中却出现了两个问题:
      一是以这种模式建立的通讯模式,带宽很不稳定,有时候数据会阻塞很久才能发过去,应该是中间有丢包。
      二是一旦ServerA断掉ClientB的连接,ClientB就无法再与ServerA建立连接了。
      问题二我觉得还比较好理解,因为一般NAT建立TCP的端口映射是根据SYS和FIN包为起始终止标识来建立的,当ServerA向ClientB发FIN的时候,就会被NAT检测到,然后关掉这种端口映射关系。
     但是问题一我就想不明白了,因为当我把路由器的DMZ打开后,数据是很流畅的,可以证明物理线路是能保证足够带宽的,只有在这种应用下才会出现带宽下降问题,希望有哪位高手可以帮忙解答一下……

posted on 2013-12-16 14:40  守夜者  阅读(13606)  评论(8编辑  收藏  举报

导航