UDP打洞原理与N2N内网穿透
UDP打洞原理
通过UDP路由验证实现NAT穿越是一种在处于使用了NAT的私有网络中的Internet主机之间建立双向UDP连接的方法。由于NAT的行为是非标准化的,因此它并不能应用于所有类型的NAT。
其基本思想是这样的:让位于NAT后的两台主机都与处于公共地址空间的、众所周知的第三台服务器相连,然后,一旦NAT设备建立好UDP状态信息就转为直接通信,并寄希望于NAT设备会在分组其实是从另外一个主机传送过来的情况下仍然保持当前状态。
这项技术需要一个圆锥型NAT设备才能够正常工作。对称型NAT不能使用这项技术。
这项技术在P2P软件和VoIP电话领域被广泛采用。它是Skype用以绕过防火墙和NAT设备的技术之一。
相同的技术有时还被用于TCP连接——尽管远没有UDP成功。
原理
假设有两台分别处于各自的私有网络中的主机:A和B;NA和NB是两个网络的NAT设备,分别拥有IP地址P1和P2;S是一个使用了一个众所周知的、从全球任何地方都能访问得到的IP地址的公共服务器
步骤一:A和B分别和S建立UDP连接;NAT设备NA和NB创建UDP转换状态并分配临时的外部端口号
步骤二:S检查UDP包,看A和B的端口是否是正在被使用的(否则的话N1和N2应该是应用了端口随机分配,这会让路由验证变得更麻烦)
步骤三:如果端口不是随机化的,那么A和B各自选择端口X和Y,并告知S。S会让A发送UDP包到P2:Y,让B发送UDP包到P1:X
步骤四:A和B通过转换好的IP地址和端口直接联系到对方的NAT设备;
Server 端部分代码
//这里是对UDT的启动记性初始化操作
if (UDT::ERROR == UDT::startup())
{
cout<<"startup: "<<UDT::getlasterror().getErrorMessage()<<endl;
}else{
cout<<"startup suc..."<<endl;
}
//socket
//像声明一个普通的socket一样声明一个UDTSOCKET
UDTSOCKET serv = UDT::socket(AF_INET, SOCK_DGRAM, 0);
if (UDT::ERROR == serv)
{
cout<<"socket: "<<UDT::getlasterror().getErrorMessage()<<endl;
}else{
cout<<"client suc..."<<endl;
}
//声明udp socket,这里是udp的哈,不是udt
int sersocket = socket(AF_INET,SOCK_DGRAM,0);
if (SOCKET_ERROR == sersocket)
{
cout<<"udp socket error!"<<endl;
}else{
cout<<"clientsocket suc..."<<endl;
}
//为了能够在局域网中直接进行处理,先默认设置两个
sockaddr_in my_addr,client_addr;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[1]));
my_addr.sin_addr.s_addr = INADDR_ANY;
memset(&(my_addr.sin_zero), '\0', 8);
bind(sersocket,(struct sockaddr*)&my_addr,sizeof(my_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(atoi(argv[3]));
client_addr.sin_addr.s_addr = inet_addr(argv[2]);
//client_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
memset(&(client_addr.sin_zero), '\0', 8);
int mss = 1052;//最大传输单位
//设置收发缓冲区大小 接收限时 和地址重用
if( !( UDT::ERROR != (UDT::setsockopt(serv, 0, UDT_SNDBUF, new int(32000), sizeof(int)))
&& UDT::ERROR != (UDT::setsockopt(serv, 0, UDP_RCVBUF, new int(32000), sizeof(int)))
&& UDT::ERROR != (UDT::setsockopt(serv,0,UDT_REUSEADDR,new int(1),sizeof(int)))
&& UDT::ERROR != (UDT::setsockopt(serv, 0, UDT_RENDEZVOUS, new bool(true), sizeof(bool))))
&& UDT::ERROR != (UDT::setsockopt(serv, 0, UDT_MSS, &mss, sizeof(int)) ))
{
cout<<"udt socket: "<<UDT::getlasterror().getErrorMessage()<<endl;
UDT::close(serv);
return 0;
}
//这里是直接将udp的接口绑定在udt的接口之上,如果不这样做的话是没法使用UDT中的SOCK_DGRAM的
if (UDT::ERROR == UDT::bind2(serv,sersocket))
{
cout<<"udt bind2:"<<UDT::getlasterror().getErrorMessage()<<endl;
return 0;
}else{
cout<<"bind2 suc"<<endl;
}
//这里也是关键部分,与client端对应的connect操作,就是相互之间的打洞处理
if (UDT::ERROR == UDT::connect(serv, (sockaddr*)&client_addr, sizeof(client_addr)))
{
cout << "connect: " << UDT::getlasterror().getErrorMessage();
UDT::close(serv);
return 0;
}else{
cout<<"connetc suc"<<endl;
}
Client 端部分代码
//startup
if (UDT::ERROR == UDT::startup())
{
cout<<"startup: "<<UDT::getlasterror().getErrorMessage()<<endl;
}else{
cout<<"startup suc..."<<endl;
}
//Initialize the UDT library
UDTSOCKET client = UDT::socket(AF_INET, SOCK_DGRAM, 0);
if (UDT::ERROR == client)
{
cout<<"socket: "<<UDT::getlasterror().getErrorMessage()<<endl;
}else{
cout<<"client suc..."<<endl;
}
//声明udp socket
int clientsocket = socket(AF_INET,SOCK_DGRAM,0);
if (SOCKET_ERROR == clientsocket)
{
cout<<"udp socket error!"<<endl;
}else{
cout<<"clientsocket suc..."<<endl;
}
sockaddr_in serv_addr,my_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[3]));
serv_addr.sin_addr.s_addr = inet_addr(argv[2]);
memset(&(serv_addr.sin_zero), '\0', 8);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[1]));
my_addr.sin_addr.s_addr = INADDR_ANY;
memset(&(my_addr.sin_zero), '\0', 8);
bind(clientsocket,(struct sockaddr*)&my_addr,sizeof(my_addr));
int mss = 1052;//最大传输单位
//设置收发缓冲区大小 接收限时 和地址重用
if( !( UDT::ERROR != (UDT::setsockopt(client, 0, UDT_SNDBUF, new int(32000), sizeof(int)))
&& UDT::ERROR != (UDT::setsockopt(client, 0, UDP_RCVBUF, new int(32000), sizeof(int)))
&& UDT::ERROR != (UDT::setsockopt(client,0,UDT_REUSEADDR,new int(1),sizeof(int)))
&& UDT::ERROR != (UDT::setsockopt(client, 0, UDT_RENDEZVOUS, new bool(true), sizeof(bool))))
&& UDT::ERROR != (UDT::setsockopt(client, 0, UDT_MSS, &mss, sizeof(int))))
{
cout<<"udt socket: "<<UDT::getlasterror().getErrorMessage()<<endl;
UDT::close(client);
return 0;
}
if (UDT::ERROR == UDT::bind2(client,clientsocket))
{
cout<<"udt bind2:"<<UDT::getlasterror().getErrorMessage()<<endl;
return 0;
}else{
cout<<"bind2 suc"<<endl;
}
// connect to the server, implict bind
if (UDT::ERROR == UDT::connect(client, (sockaddr*)&serv_addr, sizeof(serv_addr)))
{
cout << "connect: " << UDT::getlasterror().getErrorMessage();
UDT::close(client);
return 0;
}else{
cout<<"connect suc"<<endl;
}
以上代码使用UDT协议进行互相打洞,没有经过Server服务器(没有),简单的实现了网络穿透,可穿越防火墙。
N2N网络穿透
N2N是一个开放源代码的2层跨越3层的×××程序,该程序利用了点对点的架构来处理网络间的成员关系和路由,N2N的原理如下图,在搭建的过程中需要一个super节点和多个edge节点,super节点建立一个通信中心,用来路由edge之间的通讯,对于×××使用来说,super node节点必须有一个公网的IP地址
安装N2N
sudo apt-get install libssl-dev //安装openssl
git clone https://github.com/meyerd/n2n
cd n2n/n2n_v2
cmake CMakeLists.txt
make
make install
需要root权限
配置Supernode
supernode -l 1000 -v >/dev/null & //监听1000端口
[email protected]:# supernode -h //可使用 -h查看命令参数
supernode usage
-l <lport> Set UDP main listen port to <lport> // UDP 监听端口
-f Run in foreground. //前台运行
-u <UID> User ID (numeric) to use when privileges are dropped. // 指定运行所用的UID
-g <GID> Group ID (numeric) to use when privileges are dropped. // 指定运行所用的GID
-v Increase verbosity. Can be used multiple times. // 输出比较详细的log
-h This help message.
配置Edgenode
edge -d n2n0 -c mynetwork -k encryptme -a 10.10.10.3 -l X.55.150.X:1000 >/dev/null &
-d 虚拟网卡名
-a [static:| dhcp:](虚拟网段ip)
-c 用于区分节点的组名
-k 用于加密的字符串
-l supernode的IP:端口,可以指定多个supernode
以上为节点配置,配置完成后 ifconfig 会多出个虚拟网卡
可在服务器上配置 supernode节点, 其余内网client配置edge节点,-a 自定义ip(网段需一致)-l 参数填服务器与监听的端口
Ping测试
搭建成功则可互相ping通
一键脚本代码
shell脚本代码(抄来的,略作修改),需root权限运行,centos系统 apt-get 改为 yum
#!/bin/bash
#####此脚本用来实现安装N2N的客户端,实现内网之间的穿透
####应用场景:
###客服的服务器有A、B、C三台,其中有一台可以上外网,此处以A为例子,ABC之间的SSH互通
####N2N的server,即super node为114.114.114.114,端口1000,在阿里云端,可以实现外网访问
####此脚本用来在客户的内网搭建N2N的client,可以实现和阿里云supernode的通信,这样通过阿里云端就可以SSH到客户服务器内网
N2N_super_node_ip=100.100.100.100 ###改为自己的服务器IP
N2N_super_node_port=1000 ###自行更改
###N2N_edge_ip为搭建的edge的IP,需要设置,网段为10.10.10.*
####但有一个前提,设置的这个IP地址在虚拟局域网中不能冲突,所以需要先判断IP地址是否冲突
N2N_edge_ip=10.10.10.3 ###自行更改
judge_ip_confilct() {
if `ping -c 2 ${N2N_edge_ip} &>/dev/null`;then
echo -e "\033[32m ${N2N_edge_ip} can ping,has client used,please motified N2N_edge_ip,系统退出\033[0m"
exit 0
else
echo -e "\033[31m ${N2N_edge_ip} not can ping,N2N_edge_ip can be userd\033[0m"
fi
}
check_super_node_service() {
ping -c 6 ${N2N_super_node_ip}
if `ping -c 2 ${N2N_super_node_ip} &>/dev/null`;then
echo -e "\033[32m super node :${N2N_edge_ip} can ping, N2N server can be used\033[0m"
else
echo -e "\033[31m super node :${N2N_edge_ip} can not ping ,n2n server can not be used ,please check system quit\033[0m"
exit
fi
}
n2n_install_super_node() {
if `sudo apt-get install bc &>/dev/null`;then
echo -e "\033[32m yum can be use,starting yum install n2n relative paket:\033[0m"
sudo apt-get install subversion gcc-c++ openssl-devel
echo "git clone install n2n:"
git clone https://svn.ntop.org/svn/ntop/trunk/n2n
if [ -e n2n ];then
echo "n2n file download successful,beging install n2n"
cd n2n/n2n_v2
cmake CMakeLists.txt
make
make
sudo make install
else
echo "n2n file download failed ,has some problems ,please check"
exit 0
fi
echo "n2n install over,beginging start n2n services"
supernode -l ${N2N_super_node_port} -v >/dev/null &
echo "查看 ps -ef | grep supernode"
ps -ef | grep supernode
echo "supernode -l ${N2N_super_node_port} -v >/dev/null &" >> /etc/rc.local
else
echo -e "\033[31m yum not can be use,yum install n2 has some problem,please check\033[0m"
exit
fi
}
n2n_install_edge_node() {
if `sudo apt-get install bc &>/dev/null`;then
echo -e "\033[32m yum can be use,starting yum install n2n relative paket:\033[0m"
sudo apt-get install subversion gcc-c++ openssl-devel
echo "git clone install n2n:"
git clone https://github.com/meyerd/n2n.git
if [ -e n2n ];then
echo "n2n file download successful,beging install n2n"
cd n2n/n2n_v2
cmake CMakeLists.txt
make
make
sudo make install
else
echo "n2n file download failed ,has some problems ,please check"
exit 0
fi
echo "check super node server is ok or not"
check_super_node_service
echo "n2n install over,begining start edge node"
edge -d n2n0 -c mynetwork -k encryptme -M 1200 -a $N2N_edge_ip -l $N2N_super_node_ip:$N2N_super_node_port >/dev/null &
echo "查看 ps -ef | grep edge,进程是否启动OK"
ps -ef | grep edge
sudo echo "edge -d n2n0 -c mynetwork -k encryptme -a $N2N_edge_ip -l $N2N_super_node_ip:$N2N_super_node_port" >> /etc/rc.local
else
echo -e "\033[31m yum not can be use,yum install n2 has some problem,please check\033[0m"
exit
fi
}
real=`grep -l '\^H' /root/.bash_profile`
if [ $? -eq 1 ];then
echo 'stty erase ^H' >> /root/.bash_profile
source /root/.bash_profile #这几行主要就是让在使用read键时能使用回删键。写错了,回删了,重启写。不用这段的话,回删键会变成乱码。
fi
echo -e '\033[0;33;1m #################nagios################## \033[0m' #让echo能弄点颜色出来好看点。。。
echo "n2n supernode install please input : 1"
echo "n2n edgenode install please input : 2"
echo -e '\033[0;33;1m ######################################### \033[0m'
read -p "please chose : " frist #定义输入的值
if [ $frist -eq 1 ];then
n2n_install_super_node
else
n2n_install_edge_node
fi