我已经尝试 TCP 打孔一段时间了,在涉及基于 TCP 的方法和 C 编程语言时,论坛似乎没有多大帮助。以下是互联网的主要参考资料,
一种。http://www.brynosaurus.com/pub/net/p2pnat/
b. https://wuyongzheng.wordpress.com/2013/01/31/experiment-on-tcp-hole-punching/
我的设置是
客户端 A -- NAT-A -- Internet -- NAT-B -- 客户端 B。
假设客户端 A 知道 B 的公共和私有端点,而 B 知道 A 的端点(我已经编写了一个服务器“S”,用于在对等方之间交换端点信息),并且考虑到两个 NAT 都不对称,是否就足够了(实现 TCP打孔),如果两个客户端反复尝试 connect() 到彼此的公共端点(对于上述设置)?
如果没有,究竟需要做什么才能实现tcp打孔?
我在每个客户端上有两个线程,一个重复向其他客户端发出连接调用,另一个监听来自其他客户端的传入连接。我已确保两个线程中的套接字都绑定到提供给对等方的本地端口。另外,我看到两个 NAT 都保留了端口映射,即本地和公共端口是相同的。然而,我的程序不起作用。
我上面提到的集合服务器“S”是否可以在打洞或创建 NAT 映射中发挥作用,以允许 SYN 请求通过,到达对等方。如果是,必须做什么?
附上代码的相关部分。
connect_with_peer() 是入口点,在服务器“S”提供对等方的公共 ip:port 元组之后,该元组与完成绑定的本地端口一起提供给此函数。此函数产生一个线程( accept_handler() ),该线程也绑定到本地端口并侦听来自对等方的传入连接。如果connect() [主线程] 或accept() [子线程] 成功,connect_with_peer() 将返回一个套接字。
谢谢,
丁卡尔
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;
}
Run Code Online (Sandbox Code Playgroud)
首先,阅读这个非常相似的问题:
TCP Hole Punching
并阅读 EDIT2 之后的部分(此处摘录)。这可能是失败的原因。
一旦第二个套接字成功绑定,绑定到该端口的所有套接字的行为就不确定了。
不用担心 linux 在 socket(7) 和 SO_REUSEADDR 中有类似的限制:
对于 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
Run Code Online (Sandbox Code Playgroud)
参考资料:
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 的“第一个”答案,因为它永远不会同时出现。它其实无所谓)。
两个对等方主动发起连接。他们最初并不等待连接,您根本不必调用 listen()/accept()。您根本不必使用任何线程。
每个对等点都应该(通过 S)交换他们想要使用的本地端口(并且在 S 的帮助下,他们将交换他们的公共 IP),假设端口不会被转换。
现在,您只需尝试连接您的 4-upple 信息即可。每个都将与 (INADDR_ANY,lport) 绑定并连接到 (peer_global_address,peer_global_port) 而同时 B 做同样的事情。最后在双方之间建立了一个 UNIQUE 连接。
两个 NAT 盒都将看到传出数据包并准备反向路径。
现在会出什么问题?
NAT 设备(来自执行请求太晚的对等方,例如 B 前面的 NAT-B)用 RST 数据包进行响应,而不是像大多数 NAT 设备那样默默地丢弃仍然未知的数据包。A 收到 RST 并中止连接。然后 B 发送它并发生类似的命运。ping 往返速度越快,您就越容易做到这一点。为了避免这种情况,要么:
我只能说我可以让 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
Run Code Online (Sandbox Code Playgroud)
完成后,两个netcat 一起建立了SAME UNIQUE 连接,没有建立两个连接。不需要重试(无循环)。当然,程序将使用 connect(),如果第二个命令(以及 connect())延迟,操作系统可能会在 connect() 期间发送多个 SYN 数据包作为自动重试机制。这是在系统/内核级别,而不是您的级别。
我希望这有助于您简化程序并使其工作。记住,不需要listen()、accept()、必须fork、使用线程。您甚至不需要 select(),只需在没有 O_NONBLOCK 的情况下正常使用 connect() 块即可。