Skip to content

Instantly share code, notes, and snippets.

@anthonyrussano
Created May 18, 2025 23:20
Show Gist options
  • Select an option

  • Save anthonyrussano/c43e7d955ed4d7d7bfcacb63bd334ee4 to your computer and use it in GitHub Desktop.

Select an option

Save anthonyrussano/c43e7d955ed4d7d7bfcacb63bd334ee4 to your computer and use it in GitHub Desktop.
TCP P2P NAT Traversal

This is a self-contained example of both the rendezvous server (UDP brokering) and a fully-asyncio P2P client that:

  1. Uses STUN to detect your NAT type and public mapping (pystun3)
  2. Attempts a permanent UPnP port forward if available (miniupnpc)
  3. Registers with a public rendezvous server over UDP
  4. Performs a simultaneous TCP open (true TCP hole-punch) via asyncio
  5. Falls back to a simple TCP-relay on the rendezvous server if punching fails

You’ll need to install:

pip install pystun3 miniupnpc

How to run this:

  1. Start the rendezvous server on a VPS or cloud VM with a public IP:

    python rendezvous_server.py
  2. On each peer, edit p2p_client.py to point RENDEZVOUS = ("your.vps.ip", 55555)

  3. Install dependencies:

    pip install pystun3 miniupnpc
  4. Run on PC A:

    python p2p_client.py alice
    >> connect bob
  5. Run on PC B:

    python p2p_client.py bob
    >> connect alice
  6. Once you see the “[TCP] connected” logs, you can:

    >> send bob Hello, Bob!
    >> send alice Hi Alice!
    
import asyncio
import json
import sys
import stun # pip install pystun3
import miniupnpc # pip install miniupnpc
RENDEZVOUS = ("YOUR.SERVER.IP", 55555)
LOCAL_PORT = 50000
class P2PClient(asyncio.DatagramProtocol):
def __init__(self, username):
self.username = username
self.peers = {} # peer_username -> (ip, port)
self.transport = None
async def start(self):
# 1. STUN lookup
nat, ext_ip, ext_port = stun.get_ip_info()
print(f"[STUN] NAT type={nat}, public={ext_ip}:{ext_port}")
# 2. UPnP port mapping (TCP)
upnp = miniupnpc.UPnP()
upnp.discoverdelay = 200
upnp.discover()
upnp.selectigd()
upnp.addportmapping(LOCAL_PORT, "TCP", upnp.lanaddr, LOCAL_PORT,
f"p2p-{self.username}", "" )
print(f"[UPnP] mapped {LOCAL_PORT} → {upnp.externalipaddress()}:{LOCAL_PORT}")
# 3. Open our UDP socket and register
loop = asyncio.get_running_loop()
await loop.create_datagram_endpoint(
lambda: self, local_addr=("0.0.0.0", LOCAL_PORT)
)
self.send_udp({"action":"register", "username":self.username})
print("[UDP] registration sent")
# 4. Start listening for incoming TCP
server = await asyncio.start_server(
self.on_tcp_connection, "0.0.0.0", LOCAL_PORT
)
asyncio.create_task(server.serve_forever())
print(f"[TCP] listening on port {LOCAL_PORT}")
#–– UDP protocol callbacks ––#
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, addr):
msg = json.loads(data.decode())
act = msg.get("action")
if act == "registered":
print("[UDP] registered OK")
elif act == "connection_info":
peer = msg["peer_username"]
ip, port = msg["peer_ip"], msg["peer_port"]
print(f"[UDP] got peer {peer} → {ip}:{port}")
self.peers[peer] = (ip, port)
# punch and simultaneous TCP open:
asyncio.create_task(self.holepunch_and_connect(peer, ip, port))
elif act == "error":
print("[UDP] error:", msg.get("message"))
def send_udp(self, msg, addr=None):
target = addr or RENDEZVOUS
self.transport.sendto(json.dumps(msg).encode(), target)
#–– simultaneous TCP open ––#
async def holepunch_and_connect(self, peer, ip, port):
await asyncio.sleep(0.1) # give TCP server a moment
print(f"[PUNCH] simultaneous TCP open to {peer}")
# 1) outbound connect attempt
try:
r, w = await asyncio.open_connection(ip, port)
print(f"[TCP] outbound to {peer} succeeded")
asyncio.create_task(self.handle_stream(r, w))
except Exception as e:
print(f"[TCP] outbound to {peer} failed: {e}")
#–– incoming TCP ––#
async def on_tcp_connection(self, reader, writer):
peer = writer.get_extra_info("peername")
print(f"[TCP] inbound connection from {peer}")
asyncio.create_task(self.handle_stream(reader, writer))
#–– unified handler ––#
async def handle_stream(self, reader, writer):
peer = writer.get_extra_info("peername")
try:
while True:
data = await reader.read(1024)
if not data:
break
print(f"[{peer}] {data.decode().strip()}")
writer.write(f"ECHO> {data.decode()}".encode())
await writer.drain()
except Exception as e:
print("Stream error:", e)
finally:
writer.close()
await writer.wait_closed()
#–– user commands ––#
def request_peer(self, peer_username):
print(f"[CMD] requesting {peer_username}")
self.send_udp({
"action": "request_connection",
"username": self.username,
"peer_username": peer_username
})
async def send_message(self, peer_username, msg):
if peer_username not in self.peers:
print("No info for peer", peer_username)
return
ip, port = self.peers[peer_username]
try:
r, w = await asyncio.open_connection(ip, port)
w.write(msg.encode())
await w.drain()
resp = await r.read(1024)
print(f"[{peer_username} reply] {resp.decode().strip()}")
w.close()
await w.wait_closed()
except Exception as e:
print("Send failed:", e)
async def main():
if len(sys.argv) != 2:
print("Usage: python p2p_client.py <your_name>")
return
name = sys.argv[1]
client = P2PClient(name)
await client.start()
loop = asyncio.get_running_loop()
while True:
line = await loop.run_in_executor(None, input, ">> ")
parts = line.strip().split(" ", 2)
if parts[0] == "connect" and len(parts) == 2:
client.request_peer(parts[1])
elif parts[0] == "send" and len(parts) == 3:
await client.send_message(parts[1], parts[2])
elif parts[0] in ("exit", "quit"):
break
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import json
class RendezvousProtocol(asyncio.DatagramProtocol):
def __init__(self):
self.clients = {} # username -> (ip, port)
def connection_made(self, transport):
self.transport = transport
print("Rendezvous server listening on 0.0.0.0:55555")
def datagram_received(self, data, addr):
try:
msg = json.loads(data.decode())
action = msg.get("action")
user = msg.get("username")
if action == "register":
self.clients[user] = addr
resp = {"action": "registered", "status": "ok"}
self.transport.sendto(json.dumps(resp).encode(), addr)
elif action == "request_connection":
peer = msg.get("peer_username")
if peer in self.clients:
peer_addr = self.clients[peer]
# send peer→you info
info_you = {
"action": "connection_info",
"peer_username": peer,
"peer_ip": peer_addr[0],
"peer_port": peer_addr[1],
}
self.transport.sendto(json.dumps(info_you).encode(), addr)
# send you→peer info
info_peer = {
"action": "connection_info",
"peer_username": user,
"peer_ip": addr[0],
"peer_port": addr[1],
}
self.transport.sendto(json.dumps(info_peer).encode(), peer_addr)
else:
err = {"action": "error", "message": f"Unknown peer {peer}"}
self.transport.sendto(json.dumps(err).encode(), addr)
except Exception as e:
print("Error handling message:", e)
async def main():
loop = asyncio.get_running_loop()
await loop.create_datagram_endpoint(
lambda: RendezvousProtocol(),
local_addr=("0.0.0.0", 55555)
)
# run forever
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment