又探内网穿透——穿透工具ngrok,frp

搜索内网穿透,蹦出来一大堆的内网穿透工具,这不禁让我怀疑人生:已经有这么成熟的产品了,还研究内网穿透干啥?

事实证明,这些内网穿透的工具,从原理上看,一是用的开源代码FRP、NGROK包装的,然后就是走的公网服务器中转。

并不是我想要的TCP-P2P穿透通信。1

而且调研过程中发现一个非常令人失望的事情,如图:

可惜成功率不高

又探内网穿透——穿透工具ngrok,frp_第1张图片只不过是中转,有什么意义呢?

可见,目前比较成熟的内网穿透的FRP,尚且对P2P-TCP的成功率不高(可能原理上讲无法穿透对称型NAT),然后另一个小众的穿透工具的设计大佬直接就说了,穿透用的就是转发。

 

所以说对我而言,P2P 点对点的内网穿透,并不是这些穿透工具所实现的,虽然有开源的FRP、NGROK,但是都没有实现点对点的内网穿透。

同时,在纯粹的P2P网络研究论文来说,TCP的内网穿透也是难以实现的,因为对称型NAT的存在,使得P2P穿透几乎变成了不可能的事情。

所以,内网穿透的研究暂时告一段落,结论:UDP穿透是成熟可行的,已经试验通过。TCP穿透试验失败,尚不清楚原因。

无论是哪一种穿透,都无法穿透对称型NAT,虽然可以采用基于端口预测的方式来穿透,但是暂时看来这种想法只能存在与理论之中。

现阶段的两种方式:STUN/TURN就是点对点穿透和中转,中转为了应付对称型NAT。ICE方式大家都说好,找遍全网找不到相关文档,只找到一个15年前的官方文档,但是却又不说应用只说怎么编写。ICE真的有人用吗?GITHUB都没多少人关注。

网上的内网穿透工具,本质原理分为Frp和ngrok两种,这两种原理一句话来说,就是中转数据。

查阅frp文档发现,frp已经在研究点对点的TCP大数据穿透了,但是却有一个括号说成功率不高,令人失望,我也没时间去试验了,毕竟人家frp商业软件都说不行...(还是懒)

又探内网穿透——穿透工具ngrok,frp_第2张图片

然后看了看国内比较多的内网穿透工具:

NATAPP

NAT123

小蚂蚁内网穿透

花生壳

FRP

哲西信科续断

https://www.jianshu.com/p/cdc446e51675

但要使用第三方的公网服务器就必须为第三方付费,并且这些服务都有各种各样的限制,此外,由于数据包会流经第三方,因此对数据安全也是一大隐患。

http://www.ngrok.cc/_book/start/frp_windows.html

https://github.com/fatedier/frp

https://developer.github.com/webhooks/configuring/

 

最后,附上我写的测试Natapp的测试代码:测试成功,公网程序和局域网程序实现TCP通信。(但是又有什么意义呢?这样的转发一个是安全问题,一个是效率问题,都是无法商用的)

又探内网穿透——穿透工具ngrok,frp_第3张图片

/*
文件:server.c
PS:第一个连接上服务器的客户端,称为client1,第二个连接上服务器的客户端称为client2
这个服务器的功能是:
1:对于client1,它返回"first",并在client2连接上之后,将client2经过转换后的IP和port发给client1;
2:对于client2,它返回client1经过转换后的IP和port和自身的port,并在随后断开与他们的连接。
 */

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 128
#define SERV_PORT 7788

//发生了致命错误,退出程序

void error_quit(const char *str) {
	fprintf(stderr, "%s", str);
	//如果设置了错误号,就输入出错原因
	if (errno != 0)
		fprintf(stderr, " : %s", strerror(errno));
	printf("\n");
	exit(1);
}

int main(void) {
	int res, cur_port;
	int connfd, firstfd, listenfd;
	int count = 0;
	char str_ip[MAXLINE] = {0}; //当前IP地址
	char str_ip1[MAXLINE] = {0}; //缓存IP地址1
	char cur_inf[MAXLINE] = {0}; //当前的连接信息[IP+port]
	char first_inf[MAXLINE] = {0}; //第一个链接的信息[IP+port]
	char buffer[MAXLINE] = {0}; //临时发送缓冲区
	struct sockaddr_in cliaddr;
	struct sockaddr_in servaddr;
	socklen_t clilen;
	int aport = 0;
	memset(&cliaddr, 0, sizeof (sockaddr_in));
	clilen = (socklen_t)sizeof (cliaddr);

	//创建用于监听TCP协议套接字        
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	memset(&servaddr, 0, sizeof (servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	//把socket和socket地址结构联系起来       
	res = bind(listenfd, (struct sockaddr *) &servaddr, sizeof (servaddr));
	if (-1 == res)
		error_quit("bind error");
	//开始监听端口,等待客户端连接
	res = listen(listenfd, INADDR_ANY);
	if (-1 == res)
		error_quit("listen error");
	while (1) {
		printf("waiting\n");
		fflush(stdout);
		//接收来自客户端的连接

		sockaddr_in addr, addr1;
		memset(&addr, 0, sizeof (sockaddr_in));
		socklen_t addrlen = (socklen_t)sizeof(addr);
		connfd = accept4(listenfd, (sockaddr*)&addr, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC); 
	
		if (-1 == connfd)
			error_quit("accept error");
		inet_ntop(AF_INET, (void*) &addr.sin_addr, str_ip, sizeof (str_ip)); //转换成192.168的格式
		count++;
		cur_port = ntohs(addr.sin_port);
		if (count == 1) {
			printf("accept %s\n", buffer);

			printf("accept1:  %s %d\n", str_ip, cur_port);
			fflush(stdout);
			firstfd = connfd;
			snprintf(first_inf, MAXLINE, "%s %d", str_ip, cur_port);
			aport = cur_port;
			strcpy(str_ip1, str_ip);
			strcpy(cur_inf, "first\n");	
			res = write(firstfd, cur_inf, strlen(cur_inf) + 1);
			if (-1 == res)
				error_quit("sendto error");
			memset(&addr1, 0, sizeof (sockaddr_in));
			inet_pton(AF_INET, str_ip, &addr1.sin_addr);//把C1的地址信息赋值给addr1
			addr1.sin_port = htons(cur_port);
			while (1) {
				int i=0;
			strcpy(buffer, "123 \n");
			res = read(firstfd, buffer, MAXLINE);
			printf("rev message: %s,%d", buffer, i++);
			if (res <= 0)
				error_quit("write error");
			sleep(3);
		}
		} else if (count == 2) {
			printf("accept %s\n", buffer);
			printf("accept2:  %s %d\n", str_ip, cur_port);
			fflush(stdout);
			//cur_port = ntohs(cliaddr.sin_port);
			snprintf(cur_inf, MAXLINE, "%s %d\n", str_ip, cur_port);
			snprintf(buffer, MAXLINE, "%s %d\n", str_ip1, aport);
			write(connfd, buffer, strlen(buffer) + 1);//buffer放C1的地址 发给C2
			write(firstfd, cur_inf, strlen(cur_inf) + 1);//cur_inf放C2的地址 发给C1
			sleep(10);
			printf("close\n");
			fflush(stdout);
			close(listenfd);
			return 0;
		} else
			error_quit("Bad required");
	}
	close(listenfd);
	return 0;
}

C2:

/*
文件:client.c
PS:第一个连接上服务器的客户端,称为client1,第二个连接上服务器的客户端称为client2
这个程序的功能是:先连接上服务器,根据服务器的返回决定它是client1还是client2,
若是client1,它就从服务器上得到client2的IP和Port,连接上client2,
若是client2,它就从服务器上得到client1的IP和Port和自身经转换后的port,
在尝试连接了一下client1后(这个操作会失败),然后根据服务器返回的port进行监听。
这样以后,就能在两个客户端之间进行点对点通信了。
 */


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define MAXLINE 128
#define SERV_PORT 40852
#define CLIENT1_PORT 4002
#define CLIENT2_PORT 4002
#define SERV_IP "122.152.206.xxx"
//"192.168.23.232"
//"122.152.206.144"

#define SIZE 128

typedef struct {
	char ip[32];
	int port;
} server;

void error_quit(const char *str) {
	fprintf(stderr, "%s", str);
	//如果设置了错误号,就输入出错原因

	if (errno != 0)
		fprintf(stderr, " : %s", strerror(errno));
	printf("\n");
	exit(1);
}
void * recvfd(void * sockfd) {
	char buf[MAXLINE] = {0};
	socklen_t addrlen(0);
	sockaddr_in remote;
	int acceptfd = 0;
	int funcosckfd = *(int *)sockfd;
	acceptfd = accept4(funcosckfd, (sockaddr*)&remote, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC); 
	if (acceptfd < 0)
		error_quit("acceptfd error");
	int res = read(funcosckfd, buf, MAXLINE);
	if (res < 0)
		error_quit("read error");
	printf("Get: %s", buf);

}


int main(int argc, char **argv) {
	int res, port;
	int connfd, sockfd, listenfd;
	unsigned int value = 1;
	char buffer[MAXLINE];
	socklen_t clilen;
	struct sockaddr_in servaddr, remote_addr, connaddr, localsockaddr;
	server other;
	memset(&other, 0, sizeof (other));
	memset(&remote_addr, 0, sizeof (remote_addr));
	memset(&localsockaddr, 0, sizeof (localsockaddr));
	//if( argc != 2 )		error_quit("Using: ./client ");

	//创建用于链接(主服务器)的套接字       
	char *url = "server.natappfree.cc";
    struct hostent *hptr = gethostbyname(url);
	if(hptr == NULL) return -EFAULT;
	
	sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
	//sockfd = socket(AF_INET, SOCK_STREAM, 0);
	remote_addr.sin_family = AF_INET;
	remote_addr.sin_addr.s_addr = *((unsigned long*)hptr->h_addr_list[0]);
	//inet_pton(AF_INET, SERV_IP, &remote_addr.sin_addr);
	remote_addr.sin_port = htons(SERV_PORT); //连接SERVERport
	//本地socket端口 客户端绑定固定端口
	localsockaddr.sin_family = AF_INET;
	localsockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//inet_pton(AF_INET, "127.0.0.1", &localsockaddr.sin_addr);
	localsockaddr.sin_port = htons(CLIENT1_PORT); //本地拍p2p port

	//设置端口可以被重用
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (void *) &value, sizeof (value));
	res = bind(sockfd, (struct sockaddr *) &localsockaddr, sizeof (localsockaddr)); //客户端connect时候绑定本地短偶
	if (-1 == res)
		error_quit("bind error");

	//连接主服务器
	res = connect(sockfd, (struct sockaddr *) &remote_addr, sizeof (remote_addr)); //非阻塞  连接失败直接退出
	if (res < 0) {
		printf("Get: %d", res);
		error_quit("connect error");
	}

	//从主服务器中读取出信息  对于A 读取first  对于B 读取A的端口和ip
	res = read(sockfd, buffer, MAXLINE);
	if (res < 0)
		error_quit("read error");
	printf("Get: %s", buffer);

	//若服务器返回的是first,则证明是第一个客户端
	if ('f' == buffer[0]) {
		//从服务器中读取第二个客户端的IP+port
		
		while (1) {
			
			strcpy(buffer, "Hello, world \n");
			res = write(sockfd, buffer, strlen(buffer) + 1);

			if (res <= 0)
				error_quit("write error");
			sleep(3);
		}
		res = read(sockfd, buffer, MAXLINE); //阻塞
		sscanf(buffer, "%s %d", other.ip, &other.port);
		printf("C2'addr: %s %d\n", other.ip, other.port);
		fflush(stdout);
		close(sockfd);
		//创建用于的套接字        
		sleep(3);
		connfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
		//connfd = socket(AF_INET, SOCK_STREAM, 0);
		memset(&connaddr, 0, sizeof (connaddr));
		connaddr.sin_family = AF_INET;
		connaddr.sin_addr.s_addr = htonl(INADDR_ANY);
		connaddr.sin_port = htons(other.port);
		inet_pton(AF_INET, other.ip, &connaddr.sin_addr);

		//设置端口可以被重用
		setsockopt(connfd, SOL_SOCKET, SO_REUSEADDR, (void *) &value, sizeof (value));
		res = bind(connfd, (struct sockaddr *) &localsockaddr, sizeof (localsockaddr)); // bind自己端口才是对的。
		if (-1 == res)
			error_quit("bind error");

		listenfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
		setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof (value));
		res = bind(listenfd, (struct sockaddr *) &localsockaddr, sizeof (localsockaddr));
		if (-1 == res)
			error_quit("bind error");
		res = listen(listenfd, INADDR_ANY);

		pthread_t pthreadfd;
		int rc = pthread_create(&pthreadfd, NULL, recvfd, (void *) &listenfd); //accepte
		if (rc) {
			error_quit("无法创建线程 error");
			exit(-1);
		}
		//尝试去连接第二个客户端,前几次可能会失败,因为穿透还没成功,
		//如果连接10次都失败,就证明穿透失败了(可能是硬件不支持)
		int j = 1;
		while (1) {
			printf("connect B'addr: %s %d\n", other.ip, other.port);
			res = connect(connfd, (sockaddr *) & connaddr, (socklen_t)sizeof (connaddr));
			if (res < 0) {
				if (j >= 10)
					error_quit("can't connect to the other client\n");
				printf("connect error, try again. %d\n", j++);
				fflush(stdout);
				//sleep(5);
			} else
				break;
		}
		printf("connected\n");
		fflush(stdout);

		strcpy(buffer, "Hello, world \n");
		//连接成功后,每隔一秒钟向对方(客户端2)发送一句hello, world
		int i = 1;
		while (1) {
			res = write(connfd, buffer, strlen(buffer) + 1);

			if (res <= 0)
				error_quit("write error");
			printf("send message: %s,%d", buffer, i);
			sleep(1);
			i++;
		}
	}//第二个客户端的行为
	else {
		//从主服务器返回的信息中取出客户端1的IP+port和自己公网映射后的port
		sscanf(buffer, "%s %d %d", other.ip, &other.port, &port);
		close(sockfd);
		//创建用于TCP协议的套接字     //此处有问题 他创建了两个套接字 实际上是否创建一个套接字就行了。
		sleep(3);
		sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
		memset(&connaddr, 0, sizeof (connaddr));
		connaddr.sin_family = AF_INET;
		connaddr.sin_addr.s_addr = htonl(INADDR_ANY);
		connaddr.sin_port = htons(other.port);
		inet_pton(AF_INET, other.ip, &connaddr.sin_addr);
		//设置端口重用
		setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof (value));
		res = bind(sockfd, (struct sockaddr *) &localsockaddr, sizeof (localsockaddr));
		if (-1 == res)
			error_quit("bind error");


		//创建用于监听的套接字   
		listenfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
		//设置端口重用 必须在bind之前进行设置。
		setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof (value));
		res = bind(listenfd, (struct sockaddr *) &localsockaddr, sizeof (localsockaddr));
		if (-1 == res)
			error_quit("bind error");
		/* TCP 的连接队列满后,Linux 不会如书中所说的拒绝连接,只是有些会延时连接,而且accept()未必能把已经建立好的连接全部取出来(如:当队列的长度指定为 0 ),
		 * 写程序时服务器的 listen() 的第二个参数最好还是根据需要填写,写太大不好(具体可以看cat /proc/sys/net/core/somaxconn,默认最大值限制是 128),浪费资源,写太小也不好,延时建立连接*/

		//开始监听端口       listen() 函数的第二个参数( backlog)的作用:告诉内核连接队列的长度。 
		res = listen(listenfd, INADDR_ANY);
		if (-1 == res)
			error_quit("listen error");

		pthread_t pthreadfd;
		int rc = pthread_create(&pthreadfd, NULL, recvfd, (void *) &listenfd); //accepte
		if (rc) {
			error_quit("无法创建线程 error");
			exit(-1);
		}
		while (1) {
			printf("test connect A: %s %d\n", other.ip, other.port);
			fflush(stdout);
			res = connect(sockfd, (sockaddr *) & connaddr, (socklen_t)sizeof (connaddr));
			if (res < 0) {
				printf("connect error but its ok\n");
			}
			close(connfd);
		}
	}

	return 0;
}

顺便一提,测试了SOCKET连接域名的方法:

需要    gethostbyname来解析域名,解析的原理是通过本机LINUX配置的DNS服务器地址来解析域名获得IP。

    char *url = "server.natappfree.cc";
    struct hostent *hptr = gethostbyname(url);
    if(hptr == NULL) return -EFAULT;
    
    sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP);
    //sockfd = socket(AF_INET, SOCK_STREAM, 0);
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_addr.s_addr = *((unsigned long*)hptr->h_addr_list[0]);

测试时也可以通过PING域名来获得IP真正地址

 

最后说一下这个struct hostent:

struct hostent * gethostbyname(const char * hostname);    (成功返回 hostent结构体地址,失败时返回NULL指针)。

只要传递域名字符串,就会返回域名对应的IP地址。只是返回时,地址信息装入hostent结构体。此结构体如下。

struct hostent

{

      char * h_name;                                //official name

      char ** h_aliases;                            //alias list

      int h_addrtype;                                //host address type

      int h_length;                                    //address length

      char **h_addr_list;                          //address list

}

从上述结构体定义可以看出,不只返回IP信息,同时还连带着其他信息。各位不用想得太过复杂。域名转IP时只需

关注h_addr_list。下面简要说明上述结构体各成员。

------ h_name

       该变量中存有官方域名(official domain name)。官方域名代表某一主页,但实际上,一些著名公司的域名并未用官方域名注册。

------h_aliases

       可以通过多个域名访问同一主页。同一IP可以绑定多个域名,因此除官方域名外还可指定其他域名。这些信息可以通过h_aliases获得。

------h_addrtype

       gethostbyname函数不仅支持IPv4,还支持IPv6。因此可以通过此变量获取保存在h_addr_list的IP地址的地址族信息。若是IPv4,则此变量存有AF_INET.

------h_length

       保存IP地址长度。若是IPv4地址,因为是4个字节,则保存4;IPv6时,因为是16个字节,故保存16。

------h_addr_list

       这是最重要的成员。通过此变量以整数形式保存域名对应的IP地址。另外,用户较多的网站有可能分配多个IP给同一域名,利用多个服务器进行负载均衡。此时同样可以通过此变量获取IP地址信息。

printf("IP addr %d: %s \n",i+1,inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));获得ip地址。

同样可以通过IP来获得域名。

#include 

struct hostent * gethostbyaddr(const char *addr, socklen_t len, int family);

成功时返回hostent结构体变量地址值,失败时返回NULL指针。

-------addr

        含有IP地址信息的in_addr结构体指针。为了同时传递IPv4地址之外的其他信息,该变量的类型声明为char指针。

-------len 

        向第一个参数传递的地址信息的字节数,IPv4时为4,IPv6时为16。

-------family

        传递地址族信息,IPv4时为AF_INET, IPv6为AF_INET6。

 

此外,还可以通过抓包工具来分析数据是否是走的中转还是直连。

。tcptump比较简单,如tcpdump tcp port 23 and host 210.27.48.1

此时知道了tcpreplay,tcprewrite,暂时,没有深入的了解、

 

你可能感兴趣的:(技术类杂项,C++)