Python3 NAT打洞
Python3 NAT hole punching
我知道这个话题并不新鲜。虽然有各种信息,但没有提供可靠的解决方案(至少我没有找到)。我有一个用 python3 编写的 P2P 守护程序,馅饼上的最后一个元素是通过 TCP 连接 NAT 后面的两个客户端。我对这个主题的参考:
https://bford.info/pub/net/p2pnat/
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 和客户端 2 的信息。现在客户端开始连接 [89.22.11.50]:32928 <> [78.88.77.66]:51928 服务器是否应该关闭与客户端的当前连接?
客户端路由器的行为如何?我假设它期待相同的 EXTERNAL SERVER SRC IP [1.2.3.4],而不是获取 CLIENTS EXT IP 之一,例如 [89.22.11.50] 或 [78.88.77.66]?
这比我想象的还要混乱。任何对前进的帮助表示赞赏。希望这对其他 Devs/DevOps 也有帮助。
终于找到预期的行为了!不想在这里给出太多代码,但我希望在此之后您能理解如何实现它的基础知识。最好在每个客户的文件夹中有一个单独的文件 - 在 ./tcphole_client1.py 和 ./tcphole_client2.py 附近。在我们启动与服务器的会话后,我们需要快速连接。现在举例:
./tcphole_client_connector1.py 32928 51928
./tcphole_client_connector2.py 51928 32928
还记得吗?我们需要连接到与 SERVER 启动时相同的端口:
[89.22.11.50]:32928 <> [78.88.77.66]:51928
需要第一个端口来绑定套接字 (OUR)。使用第二个端口,我们正在尝试连接到客户端。另一个 CLIENT 执行相同的过程,除了它绑定到他的端口并连接到你的绑定端口。如果 ROUTER 仍然有活动连接 - 成功。
我知道这个话题并不新鲜。虽然有各种信息,但没有提供可靠的解决方案(至少我没有找到)。我有一个用 python3 编写的 P2P 守护程序,馅饼上的最后一个元素是通过 TCP 连接 NAT 后面的两个客户端。我对这个主题的参考:
https://bford.info/pub/net/p2pnat/
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 和客户端 2 的信息。现在客户端开始连接 [89.22.11.50]:32928 <> [78.88.77.66]:51928 服务器是否应该关闭与客户端的当前连接?
客户端路由器的行为如何?我假设它期待相同的 EXTERNAL SERVER SRC IP [1.2.3.4],而不是获取 CLIENTS EXT IP 之一,例如 [89.22.11.50] 或 [78.88.77.66]?
这比我想象的还要混乱。任何对前进的帮助表示赞赏。希望这对其他 Devs/DevOps 也有帮助。
终于找到预期的行为了!不想在这里给出太多代码,但我希望在此之后您能理解如何实现它的基础知识。最好在每个客户的文件夹中有一个单独的文件 - 在 ./tcphole_client1.py 和 ./tcphole_client2.py 附近。在我们启动与服务器的会话后,我们需要快速连接。现在举例:
./tcphole_client_connector1.py 32928 51928
./tcphole_client_connector2.py 51928 32928
还记得吗?我们需要连接到与 SERVER 启动时相同的端口:
[89.22.11.50]:32928 <> [78.88.77.66]:51928
需要第一个端口来绑定套接字 (OUR)。使用第二个端口,我们正在尝试连接到客户端。另一个 CLIENT 执行相同的过程,除了它绑定到他的端口并连接到你的绑定端口。如果 ROUTER 仍然有活动连接 - 成功。