NAT 原理以及 UDP 穿透
时间:2021年4月12日
0x00 前言
0x01 NAT基础和分类
基础NAT 仅对网络地址进行转换,要求对每一个当前连接都要对应一个公网IP地址,所以需要有一个公网 ip 池;基础NAT 内部有一张 NAT 表以记录对应关系,如下
基础NAT又分为:静态NAT 和 动态NAT,其区别在于:静态要求内网ip和外网ip存在固定的一一对应关系,而动态不存在这种固定的对应关系。
NAPT 需要对网络地址和端口进行转换,这种类型允许多台主机共用一个公网 ip 地址,NAPT 内部同样有一张 NAT 表,并标注了端口,以记录对应关系,如下:
NAPT又分为:锥型NAT 和 对称型NAT,其对于映射关系有不同的权限限制,锥型NAT 在网络拓扑图上像圆锥,我们在下文进行深入了解。
0x02 NAPT
从同一个内网地址端口(
192.168.1.1:7777
)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000
),192.168.1.1:7777
可以收到任意外部主机发到 1.2.3.4:10000
的数据报。受限锥型也称地址受限锥型,在完全锥型的基础上,对 ip 地址进行了限制。
192.168.1.1:7777
)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000
),其访问的服务器为 8.8.8.8:123
,只有当 192.168.1.1:7777
向 8.8.8.8:123
发送一个报文后,192.168.1.1:7777
才可以收到 8.8.8.8
发往 1.2.3.4:10000
的报文。[3.受限锥型NAT]
在受限锥型的基础上,对端口也进行了限制。
192.168.1.1:7777
)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000
),其访问的服务器为 8.8.8.8:123
,只有当 192.168.1.1:7777
向 8.8.8.8:123
发送一个报文后,192.168.1.1:7777
才可以收到 8.8.8.8:123
发往 1.2.3.4:10000
的报文。在 对称型NAT 中,只有来自于同一个内网地址端口 、且针对同一目标地址端口的请求才被 NAT 转换至同一个公网地址端口,否则的话,NAT 将为之分配一个新的公网地址端口。
192.168.1.1:7777
)发起请求到 8.8.8.8:123
,由 NAT 转换成公网地址端口(1.2.3.4:10000
),随后内网地址端口(192.168.1.1:7777
)又发起请求到 9.9.9.9:456
,NAT 将分配新的公网地址端口(1.2.3.4:20000
)可以这么来理解,在 锥型NAT 中:映射关系和目标地址端口无关,而在 对称型NAT 中则有关。锥型NAT 正因为其于目标地址端口无关,所以网络拓扑是圆锥型的。
补充下 锥型NAT 的网络拓扑图,和对称型进行比较
0x03 NAT的工作流程
当一个 TCP/UDP 的请求(
192.168.1.1:7777 => 8.8.8.8:123
)到达 NAT 网关时(1.2.3.4
),由 NAT 修改报文的源地址和源端口以及相应的校验码,随后再发往目标:192.168.1.1:7777 => 1.2.3.4:10000 => 8.8.8.8:123
随后
8.8.8.8:123
返回响应数据到 1.2.3.4:10000
,NAT 查询映射表,修改目的地址和目的端口以及相应的校验码,再将数据返回给真实的请求方:8.8.8.8:123 => 1.2.3.4:10000 => 192.168.1.1:7777
不同协议的工作特性不同,其和 TCP/UDP 协议的处理方式不同;比如 ICMP 协议工作在 IP 层,没有端口信息,NAT 以 ICMP 报文中的
identifier
作为标记,以此来判断这个报文是内网哪台主机发出的。Cisco Packet Tracer
下,在客户端发起 TCP/UDP/ICMP
请求后的 NAT translations
:当然还有一些特殊的协议,比如 FTP 协议,当请求一个文件传输时,主机在发送请求的同时也通知对方自己想要在哪个端口接受数据,NAT 必须进行特殊处理才能支持这种通信机制。
在 NAT 中有一个应用网关层(Application Layer Gateway, ALG),以此来统一处理这些协议问题。
建立了 NAT 映射关系后,这些映射什么时候失效呢?
0x04 NAT类型探测
1. 客户端使用同一个内网地址端口分别向主服务器和协助服务器(不同IP)发起 UDP 请求,主服务器获取到客户端出口地址端口后,返回给客户端,客户端对比自己本地地址和出口地址是否一致,如果是则表示处于 Open Internet 中。
2.协助服务器同样也获取到了客户端出口地址端口,将该信息转发给主服务器,同样将该信息返回给客户端,客户端对比两个出口地址端口(1.主服务器返回的,2.协助服务器返回的)是否一致,如果是则表示处于 Symmetric NAT 中。
#!/usr/bin/python3
#coding=utf-8
import socket
import sys
def server(addr):
print("[NAT CHECK launch as server on %s]" % str(addr))
# listen UDP service
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(addr)
# [1. check "Open Internet" and "Symmetric NAT"]
# recevie client request and return export ip
data, cconn = sock.recvfrom(1024)
print("server get client info: %s" % str(cconn))
data = "%s:%d" % (cconn[0], cconn[1])
sock.sendto(data.encode("utf-8"), cconn)
# receive assist data about client another export ip
data, aconn = sock.recvfrom(1024)
print("server get client info (from assist): %s" % data.decode("utf-8"))
sock.sendto(data, cconn)
# [2. check "Full-Cone NAT", "Restricted NAT" and "Restricted-Port NAT"]
# recevie client request
data, cconn = sock.recvfrom(1024)
print("server get client info: %s" % str(cconn))
# receive assist data about client another export ip
data, aconn = sock.recvfrom(1024)
print("server get client info (from assist): %s" % data.decode("utf-8"))
# send data to client through (assist get) export ip
print("send packet for testing Full-Cone NAT")
array = data.decode("utf-8").split(":")
caconn = (array[0], int(array[1]))
sock.sendto("TEST FOR FULL-CONE NAT".encode("utf-8"), caconn)
# send data to client through (server get) export ip and with different port
sock.recvfrom(1024) # NEXT flag
print("send packet for testing Restricted NAT")
cdconn = (cconn[0], cconn[1] - 1)
sock.sendto("TEST FOR Restricted NAT".encode("utf-8"), cdconn)
# send data to client through (server get) export ip
sock.recvfrom(1024) # NEXT flag
print("send packet for testing Restricted-Port NAT")
sock.sendto("TEST FOR Restricted-Port NAT".encode("utf-8"), cconn)
# server()
def assist(addr, serv):
print("[NAT CHECK launch as assist on %s && server=%s]" %
(str(addr), str(serv)))
# listen UDP service
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(addr)
# [1. check "Open Internet" and "Symmetric NAT"]
# recevie client request and forward to server
data, conn = sock.recvfrom(1024)
print("assist get client info: %s" % str(conn))
data = "%s:%d" % (conn[0], conn[1])
sock.sendto(data.encode("utf-8"), serv)
# [2. check "Full-Cone NAT", "Restricted NAT" and "Restricted-Port NAT"]
# recevie client request and forward to server
data, conn = sock.recvfrom(1024)
print("assist get client info: %s" % str(conn))
data = "%s:%d" % (conn[0], conn[1])
sock.sendto(data.encode("utf-8"), serv)
# assist()
def client(serv, ast):
print("[NAT CHECK launch as client to server=%s && assist=%s]" %
(str(serv), str(ast)))
# [1. check "Open Internet" and "Symmetric NAT"]
print("send data to server and assist")
# get local address
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(serv)
localaddr = sock.getsockname()
# send data to server and assist with same socket
# and register so that the server can obtain the export ip
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto("register".encode("utf-8"), serv)
sock.sendto("register".encode("utf-8"), ast)
# receive export ip from server
data, conn = sock.recvfrom(1024)
exportaddr = data.decode("utf-8")
print("get export ip: %s, localaddr: %s" % (exportaddr, str(localaddr)))
# check it is "Open Internet"
if exportaddr.split(":")[0] == localaddr[0]:
print("[Open Internet]")
return
# end if
# receive another export ip (assist) from server
data, conn = sock.recvfrom(1024)
anotheraddr = data.decode("utf-8")
print("get export ip(assist): %s, export ip(server): %s" % (anotheraddr, exportaddr))
# check it is "Symmetric NAT"
if exportaddr != anotheraddr:
print("[Symmetric NAT]")
return
# end if
# [2. check "Full-Cone NAT", "Restricted NAT" and "Restricted-Port NAT"]
# send data to server and assist with different socket
# receive the data sent back by the server through the export ip(assist) mapping
ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ssock.sendto("register".encode("utf-8"), serv)
asock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
asock.sendto("register".encode("utf-8"), ast)
asock.settimeout(5)
try:
data, conn = asock.recvfrom(1024)
print("[Full-Cone NAT]")
return
except:
pass
# receive the data sent back by the server with different port
ssock.sendto("NEXT".encode("utf-8"), serv)
ssock.settimeout(5)
try:
data, conn = ssock.recvfrom(1024)
print("[Restricted NAT]")
return
except:
pass
# receive the data sent back by the server
ssock.sendto("NEXT".encode("utf-8"), serv)
ssock.settimeout(5)
try:
data, conn = ssock.recvfrom(1024)
print("[Restricted-Port NAT]")
except:
print("[Unknown, something error]")
# client()
def usage():
print("Usage:")
print(" python3 nat_check.py server [ip:port]")
print(" python3 nat_check.py assist [ip:port] [server]")
print(" python3 nat_check.py client [server] [assist]")
# end usage()
if __name__ == "__main__":
if len(sys.argv) < 3:
usage()
exit(0)
# end if
role = sys.argv[1]
array = sys.argv[2].split(":")
address1 = (array[0], int(array[1]))
if role == "assist" or role == "client":
if len(sys.argv) > 3:
array = sys.argv[3].split(":")
address2 = (array[0], int(array[1]))
else:
usage()
exit(0)
# end if
# server/client launch
if role == "server":
server(address1)
elif role == "assist":
assist(address1, address2)
elif role == "client":
client(address1, address2)
else:
usage()
# end main()
实际网络往往都更加复杂,比如:防火墙、多层 NAT 等原因,会导致无法准确的探测 NAT 类型。
0x05 UDP穿透
我们以 Restricted-Port NAT
类型作为例子,因为其使用得最为广泛,同时权限也是最为严格的,在理解Restricted-Port NAT
类型穿透后,Full-Cone NAT
和Restricted NAT
就触类旁通了;
在实际网络场景下往往都是非常复杂的,比如:防火墙、多层NAT、单侧NAT,这里我们选择了两端都处于一层 NAT 的场景来进行演示讲解,可以让我们更容易的进行理解。
PC1,Router1,PC2,Router2,Server
五台设备;公网服务器用于获取客户端实际的出口地址端口,UDP 穿透的流程如下:1.PC1(192.168.1.1:7777)
发送 UDP 请求到 Server(9.9.9.9:1024)
,此时 Server 可以获取到 PC1 的出口地址端口(也就是 Router1 的出口地址端口) 1.2.3.4:10000
,同时 Router1 添加一条映射 192.168.1.1:7777 <=> 1.2.3.4:10000 <=> 9.9.9.9:1024
2.PC2(192.168.2.1:8888)
同样发送 UDP 请求到 Server,Router2 添加一条映射 192.168.2.1:8888 <=> 5.6.7.8:20000 <=> 9.9.9.9:1024
3.Server 将 PC2 的出口地址端口(5.6.7.8:20000
) 发送给 PC1
4.Server 将 PC1 的出口地址端口(1.2.3.4:10000
) 发送给 PC2
5.PC1 使用相同的内网地址端口(192.168.1.1:7777
)发送 UDP 请求到 PC2 的出口地址端口(Router2 5.6.7.8:20000
),此时 Router1 添加一条映射 192.168.1.1:7777 <=> 1.2.3.4:10000 <=> 5.6.7.8:20000
,与此同时 Router2 没有关于 1.2.3.4:10000
的映射,这个请求将被 Router2 丢弃
6.PC2 使用相同的内网地址端口(192.168.2.1:8888
)发送 UDP 请求到 PC1 的出口地址端口(Router1 1.2.3.4:10000
),此时 Router2 添加一条映射 192.168.2.1:8888 <=> 5.6.7.8:20000 <=> 1.2.3.4:10000
,与此同时 Router1 有一条关于 5.6.7.8:20000
的映射(上一步中添加的),Router1 将报文转发给 PC1(192.168.1.1:7777)
7.在 Router1 和 Router2 都有了对方的映射关系,此时 PC1 和 PC2 通过 UDP 穿透建立通信。
#!/usr/bin/python3
#coding=utf-8
import socket
if __name__ == "__main__":
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 1024))
# 1.receive message and get one export ip:port (PC1)
data, conn1 = sock.recvfrom(1024)
addr1 = "%s:%d" % (conn1[0], conn1[1])
print("1.get PC1 export ip:port = %s" % addr1)
# 2.receive message and get another export ip:port (PC2)
data, conn2 = sock.recvfrom(1024)
addr2 = "%s:%d" % (conn2[0], conn2[1])
print("2.get PC2 export ip:port = %s" % addr2)
# 3.send export address of PC1 to PC2
sock.sendto(addr1.encode("utf-8"), conn2)
print("3.send export address of PC1(%s) to PC2(%s)" % (addr1, addr2))
# 4.send export address of PC2 to PC1
sock.sendto(addr2.encode("utf-8"), conn1)
print("4.send export address of PC2(%s) to PC1(%s)" % (addr2, addr1))
print("done")
sock.close()
# end main()
#!/usr/bin/python3#coding=utf-8import randomimport socketimport stringimport timeif __name__ == "__main__": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #serv = ("10.0.1.1", 1024) serv = ("192.168.50.55", 1024) print("server =>", serv) # 1/2.send message to server, server can get our export ip:port sock.sendto("REGISTER".encode("utf-8"), serv) print("1/2.send REGISTER message to server") # 3/4.receive the export address of the peer from the server data, conn = sock.recvfrom(1024) array = data.decode("utf-8").split(":") addr = (array[0], int(array[1])) print("3/4.receive the export address of the peer, %s" % str(addr)) # 5/6.send KNOCK message to export address of peer wait = random.randint(2, 5) print("5/6.send KNOCK message to export address of peer (wait %d s)" % wait) # in order to stagger the two clients # so that the router can better create the mapping time.sleep(wait) sock.sendto("KNOCK".encode("utf-8"), addr) name = "".join(random.sample(string.ascii_letters, 8)) print("my name is %s, start to communicate" % name) # 7.communicate each other count = 0 while True: sock.settimeout(5) try: data, conn = sock.recvfrom(1024) print("%s => %s" % (str(conn), data.decode("utf-8"))) except Exception as e: print(e) msg = "%s: %d" % (name, count) count += 1 sock.sendto(msg.encode("utf-8"), conn) time.sleep(1) # end while() sock.close()# end main()
0x06 拓展
根据
Symmetric NAT
的特性我们可以知道当请求的目标端口地址改变后,会创建新的一对映射关系,我们无法知晓新的映射关系中的端口号;但是在实际场景下,部分路由器对于 Symmetric NAT
的生成算法过于简单,新的端口可能呈现于:递增、递减、跳跃等特征,所以这种条件下,我们可以基于端口猜测,来穿透 Symmetric NAT
。如果两端的 Symmetric NAT
路由器是已知的,我们可以直接逆向分析映射生成算法,即可准确预测端口号。
TCP 穿透的流程基本和 UDP 穿透一样。
listen
又进行 connect
;不过在部分操作系统下 socket 提供了端口复用选项(SO_REUSEADDR / SO_REUSEPORT
) 可以允许 TCP 绑定多个 socket。syn
报文了,其中靠后的 syn
报文就可以正确穿透完成 TCP 握手并建立连接。我们回到文章开头提到的「不需要第三方服务器实现 NAT 穿透」的方法,文中作者先提出了一种便于理解的网络拓扑,客户端位于公网,服务器位于 NAT 下,我们必须预先知道服务器的公网地址;在这个方法下,服务器不断的向外部未分配的地址发送
ICMP(ECHO REQUEST)
消息,服务器端的 NAT 将保留一条 ICMP 响应的映射,由于目的地址未分配所以没有设备会响应服务器发出的请求,此时由客户端发送一条伪装的 ICMP(DESTINATION UNREACHABLE)
给服务器,服务器可以收到该条消息并从中获取到客户端的地址;随后便可以根据预先约定的端口进行穿透并通信了。Symmetric NAT
穿透一样进行端口猜测。
0x07 总结
https://en.wikipedia.org/wiki/Network_address_translation
https://tools.ietf.org/html/rfc1631
https://tools.ietf.org/html/rfc2663
https://tools.ietf.org/html/rfc3022
https://tools.ietf.org/html/rfc7857
https://www.cnblogs.com/GO-NO-1/p/7241556.html
http://xdxd.love/2016/10/18/对称NAT穿透的一种新方法/
https://www.cnblogs.com/monjeo/p/9394825.html
http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt
https://www.linkinstar.wiki/2020/04/25/network/nat/
https://bford.info/pub/net/p2pnat/index.html
https://stackoverflow.com/questions/39545461/tcp-based-hole-punching
https://github.com/samyk/pwnat
http://samy.pl/pwnat/pwnat.pdf
https://help.cisco.yueplus.ink/Simplified%20Chinese/index.htm
https://so.csdn.net/so/search/blog?q=packet&t=blog&p=1&s=0&tm=0&lv=-1&ft=0&l=&u=gengkui9897
往 期 热 门
(点击图片跳转)