Python3 NAT打孔

Posted

技术标签:

【中文标题】Python3 NAT打孔【英文标题】:Python3 NAT hole punching 【发布时间】:2021-03-15 12:59:49 【问题描述】:

我知道这个话题并不新鲜。虽然有各种信息,但没有提供强大的解决方案(至少我没有找到)。我有一个用 python3 编写的 P2P 守护程序,馅饼上的最后一个元素是通过 TCP 连接 NAT 后面的两个客户端。我对这个主题的参考:

https://bford.info/pub/net/p2pnat/

How to make 2 clients connect each other directly, after having both connected a meeting-point server?

Problems with TCP hole punching

到目前为止我做了什么:

服务器:

#!/usr/bin/env python3

import threading
import socket

MY_AS_SERVER_PORT = 9001

TIMEOUT = 120.0
BUFFER_SIZE = 4096

def get_my_local_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        # doesn't even have to be reachable
        s.connect(('10.255.255.255', 1))
        IP = s.getsockname()[0]
    except Exception:
        IP = '127.0.0.1'
    finally:
        s.close()
    return bytes(IP, encoding='utf-8')

def wait_for_msg(new_connection, client_address):
    while True:
        try:
            packet = new_connection.recv(BUFFER_SIZE)
            if packet:
                msg_from_client = packet.decode('utf-8')
                client_connected_from_ip = client_address[0]
                client_connected_from_port = client_address[1]

                print("We have a client. Client advertised his local IP as:", msg_from_client)
                print(f"Although, our connection is from: [client_connected_from_ip]:client_connected_from_port")

                msg_back = bytes("SERVER registered your data. Your local IP is: " + str(msg_from_client) + " You are connecting to the server FROM: " + str(client_connected_from_ip) + ":" + str(client_connected_from_port), encoding='utf-8')
                new_connection.sendall(msg_back)
                break

        except ConnectionResetError:
            break

        except OSError:
            break

def server():
    sock = socket.socket()

    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

    sock.bind((get_my_local_ip().decode('utf-8'), MY_AS_SERVER_PORT))
    sock.listen(8)
    sock.settimeout(TIMEOUT)
    while True:
        try:
            new_connection, client_address = sock.accept()

            if new_connection:
                threading.Thread(target=wait_for_msg, args=(new_connection,client_address,)).start()
#               print("connected!")
#               print("")
#               print(new_connection)
#               print("")
#               print(client_address)
                msg = bytes("Greetings! This message came from SERVER as message back!", encoding='utf-8')
                new_connection.sendall(msg)
        except socket.timeout:
            pass


if __name__ == '__main__':
    server()

客户:

#!/usr/bin/python3

import sys
import socket
import time
import threading

SERVER_IP = '1.2.3.4'
SERVER_PORT = 9001
# We don't want to establish a connection with a static port. Let the OS pick a random empty one.
#MY_AS_CLIENT_PORT = 8510

TIMEOUT = 3
BUFFER_SIZE = 4096

def get_my_local_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        # doesn't even have to be reachable
        s.connect(('10.255.255.255', 1))
        IP = s.getsockname()[0]
    except Exception:
        IP = '127.0.0.1'
    finally:
        s.close()
    return bytes(IP, encoding='utf-8')

def constantly_try_to_connect(sock):
    while True:
        try:
            sock.connect((SERVER_IP, SERVER_PORT))
        except ConnectionRefusedError:
            print(f"Can't connect to the SERVER IP [SERVER_IP]:SERVER_PORT - does the server alive? Sleeping for a while...")
            time.sleep(1)
        except OSError:
            #print("Already connected to the server. Kill current session to reconnect...")
            pass

def client():
    sock = socket.socket()

    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

    #sock.bind((get_my_local_ip().decode('utf-8'), MY_AS_CLIENT_PORT))
    sock.settimeout(TIMEOUT)

    threading.Thread(target=constantly_try_to_connect, args=(sock,)).start()

    while True:
        try:
            packet = sock.recv(BUFFER_SIZE)

            if packet:
                print(packet)
                sock.sendall(get_my_local_ip())

        except OSError:
            pass

if __name__ == '__main__':
    client()

现在的代码结果:

./tcphole_server.py 
We have a client. Client advertised his local IP as: 10.10.10.50
Although, our connection is from: [89.22.11.50]:32928
We have a client. Client advertised his local IP as: 192.168.1.20
Although, our connection is from: [78.88.77.66]:51928

./tcphole_client1.py              
b'Greetings! This message came from SERVER as message back!'
b'SERVER registered your data. Your local IP is: 192.168.1.20 You are connecting to the server FROM: 89.22.11.50:32928'

./tcphole_client2.py             
b'Greetings! This message came from SERVER as message back!'
b'SERVER registered your data. Your local IP is: 10.10.10.50 You are connecting to the server FROM: 78.88.77.66:51928'

如您所见,服务器拥有连接两个客户端的所有信息。我们可以通过当前的服务器-客户端连接单独发送有关其他对等方的详细信息。

现在我脑子里还有两个问题:

    假设 SERVER 为每个对等方发送有关 CLIENT 1 和 CLIENT 2 的信息。现在客户端开始连接,如 [89.22.11.50]:32928 [78.88.77.66]:51928 服务器是否应该关闭与客户端的当前连接?

    客户端路由器的行为如何?我假设它期望相同的外部服务器 SRC IP [1.2.3.4],而不是获取客户端 EXT IP 之一,例如 [89.22.11.50] 或 [78.88.77.66]?

这比我想象的要混乱。任何有助于前进的帮助表示赞赏。希望这也能帮助其他开发人员/DevOps。

【问题讨论】:

如果您想发布自己的答案,那很好,但不在问题中。 【参考方案1】:

终于找到了预期的行为!不想在这里提供太多代码,但我希望在此之后您将了解如何实现它的基础知识。最好在每个客户端的文件夹中都有一个单独的文件 - 附近的 ./tcphole_client1.py./tcphole_client2.py。在我们启动与 SERVER 的会话后,我们需要快速连接。现在例如:

./tcphole_client_connector1.py 32928 51928

./tcphole_client_connector2.py 51928 32928

还记得吗?我们需要连接到我们使用 SERVER 启动的相同端口:

[89.22.11.50]:32928 <> [78.88.77.66]:51928

需要第一个端口来绑定套接字 (OUR)。使用第二个端口,我们正在尝试连接到客户端。另一个客户端执行相同的过程,只是它绑定到他的端口并连接到您的绑定端口。如果 ROUTER 仍有活动连接 - 成功。

【讨论】:

太棒了!!我找到了这个解决方案,但是如果我想在更多客户端与我的服务器所有链接端口之间建立隧道,例如 61480?我将所有客户端和服务器都通过该端口进行通信。非常感谢!

以上是关于Python3 NAT打孔的主要内容,如果未能解决你的问题,请参考以下文章

boost::asio 中的 NAT 打孔

libutp (µTP) 和 NAT 遍历(UDP 打孔)

为啥 p2p 的 nat 打孔需要服务器

用于 UDP NAT 打孔的 PHP 和 Java...?

UDP 打孔在非对称 NAT 上失败

NAT 后面的 UDP 打孔