0%

UDP hole punching

使用udp打洞实现内网穿透

做项目的时候发现需要进行udp打洞,这里进行简单的验证

因为是测试,代码就先写在一个cpp文件里了,最后再封装。

github项目连接:Chen-Jin-yuan/UDP-hole-punching (github.com)


日志

公网环境:ubuntu 20.04 LTS,g++

本地环境:win10,vs2022,网络利用nattypetest测试cone类型。

  • 首先进行udp收发测试
  • 初次尝试udp打洞失败,思考可能与NAT类型有关,因为校园网和手机热点都是对称型,难以穿透;而如果两台内网机器连同一个wifi的话,可能会因为NAT回环而被丢弃,但是又没有其他网络可以连,错误的可能很多,先尝试逆向连接。
  • 进行逆向连接测试,成功。
  • 进行NAT回环测试(客户端A、B在同一个内网下,A向NAT发往B的包被丢弃),成功,并修改服务器逻辑,当双方在一个NAT内时,发的是主机ip(私有ip)和绑定的端口(不是NAT中的端口)。
  • 逆向连接成功后进行tcp连接,失败,与conntrack连接跟踪有关,可能被防火墙根据协议号阻拦了。
  • 根据连接跟踪,使用tcp打洞打穿NAT和防火墙,实现tcp的逆向连接。
  • tcp的内网穿透失败,防火墙有诸多限制。
  • udp打洞测试成功,成功内网穿透了两个NAT。
  • 整理代码与封装,并有使用示例。

简单的收发测试

Linux服务器端server.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
using namespace std;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
if(listenfd < 0)
{
printf("create listen socket error, port-%d\n",port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//端口复用,在bind前设置
int optval = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1)
{
close(listenfd);
exit(1);
}

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
close(listenfd);
exit(1);
}
}

string udp_hole_punching(int listenfd)
{
//定义sockaddr_in
struct sockaddr_in gateway;//表示网关
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));
//区分client1和client2
if(strcmp(recvbuf,"client1")==0)
{
printf("udp hole punching client1 ip: %s, port: %s\n",ip.c_str(),port.c_str());
return ip+" "+port;
}
else if(strcmp(recvbuf,"client2")==0)
{
printf("udp hole punching client2 ip: %s, port: %s\n",ip.c_str(),port.c_str());
return ip+" "+port;
}
}
return "";
}

int main()
{
int listenudp;
const int port = 10000;
init_udp_Socket(listenudp , port);
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

size_t pos1 = ip_port1.find(" ");
string ip1 = ip_port1.substr(0,pos1);
string port1 = ip_port1.substr(pos1+1);

//定义sockaddr_in
struct sockaddr_in socketaddr1;//告知要发送的目标ip及端口
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));//字节序转换
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);




size_t pos2 = ip_port2.find(" ");
string ip2 = ip_port2.substr(0,pos2);
string port2 = ip_port2.substr(pos2+1);

//定义sockaddr_in
struct sockaddr_in socketaddr2;//告知要发送的目标ip及端口
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));//字节序转换
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

//对着发,ipport1发给addr2
int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

return 0;
}

windows客户端client.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,
recvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client1";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}
int main()
{
initSocket();
SOCKET udpfd;
const int port = 10000;//同一台机器的话要改一下
init_udp_Socket(udpfd, port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

getchar();//阻塞
closesocket(udpfd);
WSACleanup();
return 0;
}

结果

云服务器里运行server.cpp

本机使用vs2022,先release一份exe文件,然后改端口和client1还是client2的信息,在debug模式再运行一个程序,就可以做到两台主机了。

image-20221030221930721

使用另一台电脑运行程序,绑定端口10000,另一台电脑连不同的网,结果如下:

image-20221030223259010


现在产生p2p发送,发送端添加如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getchar();//阻塞

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}

closesocket(udpfd);

接收端把getchar去掉,添加如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//client2接收信息
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s", recvbuf);
getchar();//阻塞

结果是发不过去,接收端没有收到一直在阻塞。这需要进一步去思考。可能是NAT类型问题,也可能是NAT回环问题(注:经后面测试,发现是NAT回环问题)

逆向连接-Connection reversal

这种方法在两个端点中有一个不存在中间件(如NAT)的时候有效。例如,Client A在NAT之后而Client B拥有全局IP地址,如图所示:

img

Client A内网地址为10.0.0.1,使用TCP,端口为1234。A和Server S建立了一个连接,Server的IP地址为18.181.0.31,监听1235端口。NAT A给Client A分配了TCP端口62000,地址为NAT的公网IP地址155.99.25.11,作为Client A对外当前会话的临时IP和端口。因此Server S认为Client A就是155.99.25.11:62000。而Client B由于有公网地址,所以对Server S来说Client B就是138.76.29.7:1234。

当Client B想要主动发起对Client A的P2P连接时,需要指定目的地址及端口为155.99.25.11:62000。由于NAT工作的原理问题,NAT A会拒绝将收到的对Client A的请求转发给Client A。拒绝该请求主要有如下原因:

  1. NAT A没有映射过62000端口,NAT A不知道该请求是给谁的

  2. NAT A映射过62000端口,但是需要首先从Client A发起请求,然后才能转发应答(限制锥形NAT的保护)

在直接连接Client A失败之后,Client B可以通过Server S向Client A中继一个连接请求,从而从Client A方向“逆向“地建立起Client A- Client B之间的点对点连接(因为Client A连接到了Server S)。

很多当前的P2P系统都实现了这种技术,但其局限性也是很明显的,只有当其中一方有公网IP时连接才能建立。越来越多的情况下,通信的双方都在NAT之后,因此就要用到打洞技术了。


现在我们假设公网服务器有一个clientB,本地主机clientA在内网里,我们的目标是实现B发送udp请求能连接到A。

  • 首先通过服务器分发对方的IP和port
  • 然后clientA向clientB发送信息(正常情况下能收到),此时NAT A已经建立了NAT A->clientB的映射,表示信任B
  • 现在B可以先A发送信息了,NAT A会转发到A

注意B是否接收A的消息是无关紧要,为了验证,这里本地客户端B接收并打印出来。实际上不需要接收,A发的这个包只是告诉NAT A:“我要发的目的ip和端口是被我信任的,它可以发包给我,不要丢弃这个ip和端口发来的信息”。

先写一个在公网云服务器运行的clientB,先接收然后发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include "time.h"
using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
close(listenfd);
exit(1);
}
}

bool udp_hole_punching(int& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client2";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res < 0 )
{
printf("udp sendto error!\n");
return false;
}
return true;

}
int main()
{
int udpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
close(udpfd);
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

//getchar();//阻塞
//先接收A发送的消息
printf("recv...\n");
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
return 0;
}
printf("recv: %s\n",recvbuf);
//发送消息,一直发
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello";

while (1)
{
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");
sleep(2);
}
return 0;
}

本地客户端如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client1";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}
int main()
{
initSocket();
SOCKET udpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

Sleep(2000);

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");


while (1)
{
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s\n", recvbuf);
Sleep(2000);

}

closesocket(udpfd);
WSACleanup();
return 0;
}

结果

如图,公网clientB成功发送udp包到内网clientA

image-20221031125624691


现在,我们不让A发送给B,B也不阻塞接收,而是一直发送信息给A。

注意,必须等NAT A中的映射自然删除(大概几分钟的存活时间),没删除前B仍然可以直接发给A。

删除后,B就无法发送给A了,A一直在阻塞,因为B发送的消息给NAT A丢弃了,即保护了A。

image-20221031130440814

NAT回环(环回)

现在我们处理同一个局域网的问题,有些NAT对同一个局域网的信息会丢弃,这时服务器根据ip判断是否是一个局域网,是的话把客户端本机的ip和port发送回去即可。

客户端:

下面是一个获取本机ip的函数,一般取最后一个即可,然后把ip和端口发给服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma warning(disable: 4996)
vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

发送包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip +" "+to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(),myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

服务器端:

服务器不再接收”client1”或”client2”,而是接收客户端本机的ip和port,接收后判断NAT的ip是否相同,如果相同就是同一个局域网。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
string udp_hole_punching(int listenfd)
{
//定义sockaddr_in
struct sockaddr_in gateway;//表示网关
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));

string host = string(recvbuf);
printf("udp hole punching NAT ip: %s, port: %s; host:%s\n",ip.c_str(),port.c_str(),host.c_str());

return ip+" "+port+" "+host;
}
return "";
}

由于有空格分隔,这里用个解析函数解析两个ip-port

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<string> parse(string str)//解析函数
{
str = str + " ";//最后补个空格
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = str.find(' ', pos)) != string::npos)
{
res.push_back(str.substr(pos, pos1 - pos));
while (str[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}
return res;//返回值是右值,外部vector会接收右值,调用移动构造
}

最后在main里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

vector<string> host1 = parse(ip_port1);
vector<string> host2 = parse(ip_port2);

//服务器发回去的还是使用NAT ip
string ip1 = host1[0];
string port1 = host1[1];
string ip2 = host2[0];
string port2 = host2[1];

//定义sockaddr_in
struct sockaddr_in socketaddr1;//告知要发送的目标ip及端口
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));//字节序转换
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);

//定义sockaddr_in
struct sockaddr_in socketaddr2;//告知要发送的目标ip及端口
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));//字节序转换
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

//但是发送的数据不一样了
//对着发,ipport1发给addr2
if(ip1 != ip2)//不同的内网
{
ip_port1 = ip1 + " " + port1;
ip_port2 = ip2 + " " + port2;
}
else//在同一个NAT里
{
ip_port1 = host1[2] + " " + host1[3];
ip_port2 = host2[2] + " " + host2[3];
}

int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

结果

在主机上跑两个程序,像逆向连接一样,A先B发送一个包,然后B就可以一直向A发包了。

当然这里服务器判断两个主机都在内网里,所以返回它们本地的ip和各自绑定的端口(这个端口不是NAT映射中的端口),此时这两个主机能互相发包。

image-20221031150918382

下面修改服务器代码,不管怎么样都返回NAT的ip和port,这时客户端A发给NAT发现目标就是自己内网里的机器,就直接丢弃了,因此B不会收到A的hello!

这个例子就说明了有些NAT不支持回环。

image-20221031151655791

完整代码

完整的源码再放一下:

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include <vector>

using namespace std;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
if(listenfd < 0)
{
printf("create listen socket error, port-%d\n",port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//端口复用,在bind前设置,否则bind时出错就晚了
int optval = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1)
{
close(listenfd);
exit(1);
}

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
close(listenfd);
exit(1);
}
//完事了
}

string udp_hole_punching(int listenfd)
{
//定义sockaddr_in
struct sockaddr_in gateway;//表示网关
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];
memset(&recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));

string host = string(recvbuf);
printf("udp hole punching NAT ip: %s, port: %s; host:%s\n",ip.c_str(),port.c_str(),host.c_str());

return ip+" "+port+" "+host;
}
return "";
}

vector<string> parse(string str)//解析函数
{
str = str + " ";//最后补个空格
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = str.find(' ', pos)) != string::npos)
{
res.push_back(str.substr(pos, pos1 - pos));
while (str[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}
return res;//返回值是右值,外部vector会接收右值,调用移动构造
}

int main()
{
int listenudp;
const int port = 10000;
init_udp_Socket(listenudp , port);
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

vector<string> host1 = parse(ip_port1);
vector<string> host2 = parse(ip_port2);

//服务器发回去的还是使用NAT ip
string ip1 = host1[0];
string port1 = host1[1];
string ip2 = host2[0];
string port2 = host2[1];

//定义sockaddr_in
struct sockaddr_in socketaddr1;//告知要发送的目标ip及端口
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));//字节序转换
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);

//定义sockaddr_in
struct sockaddr_in socketaddr2;//告知要发送的目标ip及端口
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));//字节序转换
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

//但是发送的数据不一样了
//对着发,ipport1发给addr2
if(ip1 != ip2)//不同的内网
{
ip_port1 = ip1 + " " + port1;
ip_port2 = ip2 + " " + port2;
}
else//在同一个NAT里
{
ip_port1 = host1[2] + " " + host1[3];
ip_port2 = host2[2] + " " + host2[3];
}

int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

return 0;
}

客户端1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 22222;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip +" "+to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(),myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}



int main()
{
initSocket();

SOCKET udpfd;
init_udp_Socket(udpfd, myport);

if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

Sleep(2000);

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");


while (1)
{
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s\n", recvbuf);
Sleep(2000);

}

closesocket(udpfd);
WSACleanup();
return 0;
}

客户端2,相对于客户端1只修改了绑定的port和接收发送的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 11111;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip + " " + to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}



int main()
{
initSocket();

SOCKET udpfd;
init_udp_Socket(udpfd, myport);

if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());


//发送消息,一直发
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello";

while (1)
{
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");
Sleep(2000);
}

closesocket(udpfd);
WSACleanup();
return 0;
}

逆向连接后进行tcp连接

因为还没有找到合适的网络来测试udp的内网穿透(大多都是校园网,对称型无法处理),所以先等几天,然后开始测试如何逆向连接后,进行p2p的tcp连接。

目前的想法是,当逆向连接完成后,将原来的udp套接字关闭,创建tcp套接字,然后绑定相同的端口(因为NAT记录的端口)。

  • 首先公网客户端接收内网客户端的udp信息,逆向连接完成(可以不接收,内网客户端发送即可,为了验证这里还是接收)
  • 公网客户端发个udp给内网客户端,作为验证
  • 接着两个客户端创建tcp套接字并且都绑定原来的端口(udp套接字可以不关闭,tcp和udp可以共用端口,所以可同时初始化)
  • 内网客户端进入listen状态,阻塞在accept
  • 公网客户端connect内网客户端,tcp连接建立

公网客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include "time.h"
using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
close(listenfd);
exit(1);
}
}

bool udp_hole_punching(int& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client2";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res < 0 )
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(int& listenfd, const int port)//初始化一个端口
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;


//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
exit(1);
}
}

int main()
{
int udpfd;
int tcpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
init_tcp_Socket(tcpfd,port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
close(udpfd);
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

//getchar();//阻塞
//先接收A发送的消息
printf("recv...\n");
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
return 0;
}
printf("recv: %s\n",recvbuf);

char sendbuf[10] = "hello";

//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);

res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");

if (connect(tcpfd, (struct sockaddr*)&gateway, sizeof(gateway)) == -1)
{
printf("connect fail !\n");
return 0;
}
printf("connect to server successfully!\n");
send(tcpfd, sendbuf, strlen(sendbuf), 0);

close(udpfd);
close(tcpfd);
return 0;
}

内网客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 22222;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip + " " + to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(SOCKET& tcpfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
tcpfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(tcpfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind error" << std::endl;
return;
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}
//开始监听
if (listen(tcpfd, SOMAXCONN) == -1)
{
std::cerr << "listen error" << std::endl;
return;
}
}

int main()
{
initSocket();

SOCKET udpfd;
SOCKET tcpfd;

init_udp_Socket(udpfd, myport);
init_tcp_Socket(tcpfd, myport);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
printf("recv from server...\n");
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

Sleep(2000);

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");

memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s\n", recvbuf);
//准备接收连接

///客户端套接字
memset(recvbuf, 0, sizeof(recvbuf));

struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
SOCKET conn = accept(tcpfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect error" << std::endl;
closesocket(tcpfd);
return 0;
}
int nRecv = recv(conn, recvbuf, sizeof(recvbuf), 0);
if (nRecv == SOCKET_ERROR)//copy出错
{
std::cerr << "connection to client has been failed" << std::endl;
closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}


printf("recv: %s\n", recvbuf);
closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}

结果

连接失败,双方阻塞在connect和accept处,connect的syn包根本到达不了内网机器(反向测试:内网可以connect外网)。为什么udp可以而tcp就不可以了呢?这两个协议的差异就是,我们在发udp之前,先用内网向外网发了udp;而tcp包是直接从外网发到内网的,这可能导致了问题。

连接跟踪

tcp连接失败也许是因为conntrack(连接跟踪),在防火墙、内核(linux)中会跟踪连接,这个连接不是专门指tcp连接。连接跟踪可以让Netfilter(框架)知道某个特定连接的状态,运行连接跟踪的防火墙称作带有状态机制的防火墙,这种防火墙更安全。连接跟踪的信息一般是五元组,当然针对不同协议和特殊修改也有四元组和七元组:

  • 四元组:源IP地址、目的IP地址、源端口、目的端口
  • 五元组:源IP地址、目的IP地址、协议号、源端口、目的端口
  • 七元组:源IP地址、目的IP地址、协议号、源端口、目的端口,服务类型以及接口索引

可以看到,五元组中不仅有ip、端口,还有协议号这一内容,这标识了是tcp还是udp等等。在各种过滤机制下,连接跟踪会发挥作用。

当然我们的内网客户端是Windows下的,Windows下的防火墙是如何写的并不能知道(没开源),但我们可以大概看一下防火墙高级设置:

image-20221101084903251

在入站规则中,可以看到除了地址端口,还有协议。借助连接跟踪的想法,我们猜测:

  • 假如通过udp打洞后,NAT不检测协议,而允许把tcp包转发到主机上,那么主机的防火墙就会把包丢弃。
  • 因为NAT是基于地址端口的,从理论上说端口限制锥形只要报文源、目的地址端口匹配即可;但也只是理论,不排除NAT也会把tcp拦下来

因此可以得出简单的结论:内网穿透时,使用udp打洞后只能打出一条udp的隧道;要打出tcp隧道,就要让某个内网客户端先发tcp的包。也就是说:在基于NAT的情况下,内网穿透只要打穿地址和端口;而在基于防火墙的情况下,内网穿透还要打穿特定协议的隧道


TCP打洞

我们可以重新设计:

  • 首先服务器通知双方端口和ip
  • 不进行udp打洞,直接发tcp包。这里的实现是:双方同时进行connect,利用connect在内核的重传机制(syn是会重传的),第一个tcp包会给对方NAT丢掉(因为没有udp打洞),因为有重传机制不会马上就返回错误。然后对方能进行connect过来(本地防火墙和NAT都允许入站了),本地发现在connect也就建立连接了。同时进行connect建立tcp连接可以参考我为此研究后写的博客:TCP同时打开-深度剖析 | JySama

这里面:

  • 服务器通知双方的NAT地址和端口是必要的,否则对方根本不知道网关的地址;其次,如果不通知端口,对方只能请求到预设端口,但是NAT中端口映射不一定相同,会给NAT丢弃
  • udp、tcp套接字绑定端口也是必要的,这主要是为了udp和tcp端口同步。因为
    • 回环时,服务器发回去的是客户端通告的“我要绑定的端口”,然后双方根据这个通告的端口要连接。
    • 没有回环时,使用的是NAT的端口,这只需要让udp套接字(与服务器交互)和tcp套接字(p2p交互)绑定在一个端口上即可,因为服务器发回的是NAT是建立了udp的端口,如果tcp套接字使用了不同的端口,那么在NAT上就重新建立了一条映射,这时请求udp端口的tcp连接肯定失败了,因为tcp的syn经过服务器发的端口到了udp的套接字。
  • 如果udp和tcp不绑定端口的话,当使用udp时,内核分配一个端口;使用tcp时connect时,又分配一个端口,可能就导致端口不一致了。

关于connect的建立机制,而不用listen-accept,这个还需要更详细地研究。(注:已研究透彻,为此写了一篇博客:TCP同时打开-深度剖析 | JySama

  • 目前是1对1传输文件,不涉及1对多的p2p,互相connect即可。
  • 如果是1对多,那么tcp的一个端口要connect多个对端,只用设置端口可重用即可SO_REUSEADDR,将多个套接字绑定到一个端口。这里不做研究

外网客户端:取消了udp打洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include "time.h"
using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
close(listenfd);
exit(1);
}
}

bool udp_hole_punching(int& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client2";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res < 0 )
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(int& listenfd, const int port)//初始化一个端口
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;


//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
exit(1);
}
}

int main()
{
int udpfd;
int tcpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
init_tcp_Socket(tcpfd,port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
close(udpfd);
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

//getchar();//阻塞
//先接收A发送的消息
printf("recv...\n");
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
return 0;
}
//printf("recv: %s\n",recvbuf);

char sendbuf[10] = "hello";

//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);

//res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
//printf("send...\n");

if (connect(tcpfd, (struct sockaddr*)&gateway, sizeof(gateway)) == -1)
{
printf("connect fail !\n");
return 0;
}
printf("connect to server successfully!\n");

send(tcpfd, sendbuf, strlen(sendbuf), 0);

close(udpfd);
close(tcpfd);
return 0;
}

内网客户端:取消了udp打洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 22222;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip + " " + to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(SOCKET& tcpfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
tcpfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(tcpfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind error" << std::endl;
return;
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}

}

int main()
{
initSocket();

SOCKET udpfd;
SOCKET tcpfd;

init_udp_Socket(udpfd, myport);
init_tcp_Socket(tcpfd, myport);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
printf("recv from server...\n");
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());


//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
//res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
//printf("send...\n");

memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
//res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

//printf("recv: %s\n", recvbuf);
//准备接收连接

if (connect(tcpfd, (struct sockaddr*)&gateway, sizeof(gateway)) == -1)//先发个tcp连接包
{
printf("connect fail !\n");
}

//开始监听
//if (listen(tcpfd, SOMAXCONN) == -1)
//{
// printf("listen error %d!\n", WSAGetLastError());
// return 0;
// }
///客户端套接字

/*
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
SOCKET conn = accept(tcpfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect error" << std::endl;
closesocket(tcpfd);
return 0;
}*/
memset(recvbuf, 0, sizeof(recvbuf));
int nRecv = recv(tcpfd, recvbuf, sizeof(recvbuf), 0);
if (nRecv == SOCKET_ERROR)//copy出错
{
std::cerr << "connection to client has been failed" << std::endl;
closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}


printf("recv: %s\n", recvbuf);

closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}

有udp打洞的tcp连接:

image-20221101000011682

无udp打洞的tcp连接:

image-20221101091010430

补充:已利用udp打洞成功穿透两个NAT

代码用NAT回环的代码即可。对方是另一个大学的寝室网络,接了路由器不是校园网,所以是圆锥型的NAT,可以成功。

image-20221105114055816

代码整理(封装)

客户端

头文件udp_hole_punch.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#ifndef UDP_HOLE_PUNCH_H
#define UDP_HOLE_PUNCH_H

#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")

//Using UDP requires disabling errors
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

void initSocket(); //init WSA
void initUdpSocket(SOCKET& listenfd, const int port); //init blocking socket
vector<string> getIpList(); //get host private ip

//send udp segment to server
bool notifyServer(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort);

//send one or more messages to the other party(punch a hole)
bool udpHolePunch(SOCKET& udpfd, const char* gateway_ip, const int gatewayPort);

//active, receiver
bool udpPunchSide(SOCKET& udpfd, const char* server_ip, const int serverPort , const int myPort);
//passive, sender, return gateway ip and port
pair<string,int> udpPunchedSide(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#include "udp_hole_punch.h"

void initSocket()
{
//init WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;

if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

//blocking socket
void initUdpSocket(SOCKET& listenfd, const int port)
{
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}

struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);

//bind port
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//Using UDP requires disabling errors
/*
* If sending a datagram using the sendto function results in an "ICMP port unreachable" response and the select function is set for readfds,
* the program returns 1 and the subsequent call to the recvfrom function does not work with a WSAECONNRESET (10054) error response.
* In Microsoft Windows NT 4.0, this situation causes the select function to block or time out.
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//get host name
//get private ip list
hostent* host = gethostbyname(name);//need to disable c4996 error

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
result.push_back(addr);
}

return result;
}

//send udp segment to server
bool notifyServer(SOCKET& udpfd, const char* server_ip, const int serverPort , const int myPort)
{
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(serverPort);
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1]; //send the last one
string myip_port = myip + " " + to_string(myPort);
//send the private ip
size_t res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

//send one or more messages to the other party(punch a hole)
bool udpHolePunch(SOCKET& udpfd, const char* gateway_ip, const int gatewayPort)
{
struct sockaddr_in gateway;
gateway.sin_family = AF_INET;
gateway.sin_port = htons(gatewayPort);
inet_pton(AF_INET, gateway_ip, &gateway.sin_addr);


char sendbuf[10] = "hello!";
//send two messages
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error1!\n");
return false;
}
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error2!\n");
return false;
}
return true;
}

bool udpPunchSide(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort)
{
initSocket();

initUdpSocket(udpfd, myPort);

//send message to server
if (!notifyServer(udpfd, server_ip, serverPort, myPort))
return false;

//recvfrom server
struct sockaddr_in server;
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

size_t res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv from server error %d!\n", WSAGetLastError());
return false;
}
string gateway_ip_port = recvbuf;
size_t pos1 = gateway_ip_port.find(" ");
string gateway_ip = gateway_ip_port.substr(0, pos1);
string gateway_port = gateway_ip_port.substr(pos1 + 1);


printf("client recv: ip: %s, port: %s\n", gateway_ip.c_str(), gateway_port.c_str()); //debug

if (!udpHolePunch(udpfd, gateway_ip.c_str(), stoi(gateway_port)))
{
printf("udp hole punch error!\n");
return false;
}

return true;
}

pair<string, int> udpPunchedSide(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort)
{
initSocket();

initUdpSocket(udpfd, myPort);
pair<string, int> pairRes;
pairRes.first = "";
pairRes.second = -1;
//send message to server
if (!notifyServer(udpfd, server_ip, serverPort, myPort))
return pairRes;

//recvfrom server
struct sockaddr_in server;
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

size_t res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv from server error %d!\n", WSAGetLastError());
return pairRes;
}
string gateway_ip_port = recvbuf;
size_t pos1 = gateway_ip_port.find(" ");
string gateway_ip = gateway_ip_port.substr(0, pos1);
string gateway_port = gateway_ip_port.substr(pos1 + 1);


printf("client recv: ip: %s, port: %s\n", gateway_ip.c_str(), gateway_port.c_str()); //debug
pairRes.first = gateway_ip;
pairRes.second = stoi(gateway_port);
return pairRes;
}

客户端调用示例

main函数调用,接收方主动打洞,发送方不打洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "udp_hole_punch.h"
const char* SERVER_IP = "a.b.c.d"; //your server
const int SERVERUDPPORT = 10000;
const int MYPORT = 22222;
int main()
{
SOCKET udpfd;
if (!udpPunchSide(udpfd, SERVER_IP, SERVERUDPPORT, MYPORT))
{
closesocket(udpfd);
WSACleanup();
return 0;
}
//do other thing...

//end
closesocket(udpfd);
WSACleanup();
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "udp_hole_punch.h"
const char* SERVER_IP = "a.b.c.d"; //your server
const int SERVERUDPPORT = 10000;
const int MYPORT = 22222;
int main()
{
SOCKET udpfd;
pair<string, int> gateway = udpPunchedSide(udpfd, SERVER_IP, SERVERUDPPORT, MYPORT);
if (gateway.first == "")
{
closesocket(udpfd);
WSACleanup();
return 0;
}
const char* gateway_ip = gateway.first.c_str();
const int gateway_port = gateway.second;

//do other thing...

//end
closesocket(udpfd);
WSACleanup();
return 0;
}

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include <vector>

using namespace std;

void init_udp_Socket(int& listenfd, const int port)
{
listenfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
if(listenfd < 0)
{
printf("create listen socket error, port-%d\n",port);
exit(1);
}
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);

//Port reused
int optval = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1)
{
close(listenfd);
exit(1);
}

//bind
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
close(listenfd);
exit(1);
}
}

string udp_hole_punching(int listenfd)
{
struct sockaddr_in gateway;
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];
memset(&recvbuf, 0, sizeof(recvbuf));

int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));

string host = string(recvbuf);
printf("udp hole punching NAT ip: %s, port: %s; host:%s\n",ip.c_str(),port.c_str(),host.c_str());

return ip+" "+port+" "+host;
}
return "";
}

vector<string> parse(string str)
{
str = str + " ";//add a space
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = str.find(' ', pos)) != string::npos)
{
res.push_back(str.substr(pos, pos1 - pos));
while (str[pos1] == ' ')
pos1++;
pos = pos1;
}
return res;//move rvalue
}

int main()
{
int listenudp;
const int port = 10000;
init_udp_Socket(listenudp , port);
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

vector<string> host1 = parse(ip_port1);
vector<string> host2 = parse(ip_port2);

//The server sends back using a NAT ip
string ip1 = host1[0];
string port1 = host1[1];
string ip2 = host2[0];
string port2 = host2[1];

struct sockaddr_in socketaddr1;
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);

struct sockaddr_in socketaddr2;
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

if(ip1 != ip2) //Different intranets
{
ip_port1 = ip1 + " " + port1;
ip_port2 = ip2 + " " + port2;
}
else //In the same NAT
{
ip_port1 = host1[2] + " " + host1[3];
ip_port2 = host2[2] + " " + host2[3];
}

int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

return 0;
}