搜索内网穿透,蹦出来一大堆的内网穿透工具,这不禁让我怀疑人生:已经有这么成熟的产品了,还研究内网穿透干啥?
事实证明,这些内网穿透的工具,从原理上看,一是用的开源代码FRP、NGROK包装的,然后就是走的公网服务器中转。
并不是我想要的TCP-P2P穿透通信。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商业软件都说不行...(还是懒)
然后看了看国内比较多的内网穿透工具:
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通信。(但是又有什么意义呢?这样的转发一个是安全问题,一个是效率问题,都是无法商用的)
/*
文件: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,暂时,没有深入的了解、