在非对称NAT上,UDP Hole Punching失败

时间:2017-04-02 21:22:00

标签: python sockets networking udp nat

现在有一段时间,我的UDP打孔系统出现了严重的问题,这可能涉及到我现在无法触及的事情。我会尽可能准确地解释,因为这真的让我感到烦恼:

UDP打孔方法涉及第三个主机S,它基本上是公共的(不在任何类型的NAT或持久防火墙后面),两个客户端A和B将使用它们相互了解。客户端A向S发送udp数据包,以便S可以知道其公共IP和端口(由A&#NAT映射)。客户B也这样做。然后,S通过发送到A B的公共地址和A的公共地址来匹配A和B.因此,他们每个人都知道对方的地址并且可以进行通信。

到目前为止一切顺利,但这里有一个问题:它仅适用于特殊情况。

我知道对称NAT(将您的私有IP和端口映射到您想要覆盖的每个目标地址的特定不同地址的NAT)所引起的问题,所以我要说我向S1发送一个数据包, S2,S1将看到具有端口50263的公共地址,而S2将看到50264)。我也知道某些非对称NAT要求您在允许从同一地址接收UDP数据包之前将UDP数据包发送到特定地址。

所以我到目前为止所理解的是,只要你的NAT不是Symmetric,当打开一个新套接字并发送任何字节数组时,你的NAT就会为它分配一个公共IP和端口。公共服务器告诉您的对等方这个公共地址,以便它可以与您通信。然后,您必须向该对等方发送数据包,以便他回复。

但它实际上并不是每次都有效。我已经在我的大学公寓里尝试过,并设法使用我的公共IP地址连接到自己,即使是在NAT后面。我还设法向朋友发送了一个UDP数据包,但它在我做的一百次测试中只运行了一次。我现在正在家里尝试,但它根本不起作用。

我在互联网上设置了2个公共VPS,并向每个人发送了一个数据包以查看我的NAT是否是对称的,但不是,两个端口上显示的端口是相同的。我只能从那些公共服务器上找到自己。使用Wireshark,我观察到数据包离开了我的计算机,但似乎它们被丢弃了,但是回过头来。

我编写了简单的python脚本来尝试简单地发送给自己:

服务器

import socket
import sys
from helper import *

# This script is used to allow a client to know which external address it has.

def main():

    # Port used to receive client requests
    port = 6666

    # We create a UDP socket on this port
    sockfd = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
    sockfd.bind (("", port))

    print ("Address discoverer started on port %d" % port)

    # Server loop
    while True:

        # We wait for any client to request for his address.
        data, addr = sockfd.recvfrom(1)

        # Once a client has sent a request, we send him back his external address, which we obtained using the packet he sent us.
        sockfd.sendto (addr2bytes(addr), addr)

        print ("Client %s:%d requested his address" % addr)

if __name__ == "__main__":
    main()

客户端

import sys
import socket
from helper import *

def main():

    master = ("xxx.xxx.xxx.xxx", 6666)
    me = ("yyy.yyy.yyy.yyy", 6666)

    sockfd = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)
    sockfd.bind(('',6666))
    sockfd.sendto (bytearray([0]), master)

    data, addr = sockfd.recvfrom (6)
    a = bytes2addr(data)
    print ('My address is %s:%d' % a)

    for i in range (0, 10):

        sockfd.sendto ('hi myself'.encode('utf-8'), me)
        data, addr = sockfd.recvfrom (1024)
        print ("Received : " + data.decode('utf-8'))

if __name__ == "__main__":
    main()

在我的一个公共VPS上执行,它有效。在家里执行,我没有收到任何东西。似乎从公共主机发送授予所有访问权限,但NAT后面完全不同。

我错过了一些基本的东西吗?我想我几乎每次都做过测试但没有成功。

1 个答案:

答案 0 :(得分:0)

根据您要实现的目标,您可能希望使用IGDspec)来检索您的公共IP和/或自动配置端口转发。据我所知,大多数家用路由器都支持UPnP IGD。

这是一个使用miniupnpc的示例脚本,一个最小的UPnP IGD客户端,以及来自python标准库的ipaddresssocket模块:

[30, 30, 1000]

示例输出:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
"""
    IGDTestScript.py
    ================

    Description:           UPnP IGD example script
    Author:                shaggyrogers
    Creation Date:         2018-03-28
    License:               None / Public domain
"""

import ipaddress
import socket
import sys

import miniupnpc


def main(args):
    # Get IP address for default interface
    allhosts = ipaddress.ip_address(socket.INADDR_ALLHOSTS_GROUP)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.connect((allhosts.compressed, 0))
    localIP = ipaddress.ip_address(sock.getsockname()[0])

    if localIP.is_global:
        print(f'Public IP: {localIP} (no NAT)')
        return 0

    print(f'Local IP: {localIP}')

    # Discover and connect to IGD device
    igd = miniupnpc.UPnP()
    assert igd.discover() > 0
    assert igd.selectigd() != ''

    # Get public IP
    publicIP = ipaddress.ip_address(igd.externalipaddress())
    assert publicIP.is_global
    print(f'Public IP: {publicIP}')

    # Dump port mappings
    i, mapping = 0, igd.getgenericportmapping(0)

    while mapping:
        print(f'Port mapping {i}: {mapping}')
        i += 1
        mapping = igd.getgenericportmapping(i)

    # Create/update port mapping
    externalPort, internalPort, protocol = 1234, 4321, 'TCP'
    assert igd.addportmapping(externalPort, protocol, localIP.compressed,
                            internalPort, 'test mapping', '')
    print((f'Added port mapping: {protocol} {publicIP}:{externalPort}'
           f' --> {localIP.compressed}:{internalPort}'))

    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))