pythonsocketsnetwork-programmingnathole-punching

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


I'm writing a toy meeting-point/relay server listening on port 5555 for two clients "A" and "B".

It works like this: every byte received by the server from the firstly-connected client A will be sent to the secondly-connected client B, even if A and B don't know their respective IP:

A -----------> server <----------- B     # they both connect the server first
A --"hello"--> server                    # A sends a message to server
               server --"hello"--> B     # the server sends the message to B

This code is currently working:

# server.py
import socket, time
from threading import Thread
socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket.bind(('', 5555))
socket.listen(5)
buf = ''
i = 0

def handler(client, i):
    global buf
    print 'Hello!', client, i 
    if i == 0:  # client A, who sends data to server
        while True:
            req = client.recv(1000)
            buf = str(req).strip()  # removes end of line 
            print 'Received from Client A: %s' % buf
    elif i == 1:  # client B, who receives data sent to server by client A
        while True:
            if buf != '':
                client.send(buf)
                buf = ''
            time.sleep(0.1)

while True:  # very simple concurrency: accept new clients and create a Thread for each one
    client, address = socket.accept()
    print "{} connected".format(address)
    Thread(target=handler, args=(client, i)).start()
    i += 1

and you can test it by launching it on a server, and do two netcat connections to it: nc <SERVER_IP> 5555.

How can I then pass the information to the clients A and B that they can talk directly to each other without making the bytes transit via the server?

There are 2 cases:


Remark: a previous unsuccesful attempt here: UDP or TCP hole punching to connect two peers (each one behind a router) and UDP hole punching with a third party


Solution

  • Since the server knows the addresses of both clients, it can send that information to them and so they would know each others adress. There are many ways the server can send this data - pickled, json-encoded, raw bytes. I think the best option is to convert the address to bytes, because the client will know exactly how many bytes to read: 4 for the IP (integer) and 2 for the port (unsigned short). We can convert an address to bytes and back with the functions below.

    import socket
    import struct
    
    def addr_to_bytes(addr):
        return socket.inet_aton(addr[0]) + struct.pack('H', addr[1])
    
    def bytes_to_addr(addr):
        return (socket.inet_ntoa(addr[:4]), struct.unpack('H', addr[4:])[0])
    

    When the clients receive and decode the address, they no longer need the server, and they can establish a new connection between them.

    Now we have two main otions, as far as I know.

    A Python example for UDP Hole Punching:

    Server:

    import socket
    
    def udp_server(addr):
        soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        soc.bind(addr)
    
        _, client_a = soc.recvfrom(0)
        _, client_b = soc.recvfrom(0)
        soc.sendto(addr_to_bytes(client_b), client_a)
        soc.sendto(addr_to_bytes(client_a), client_b)
    
    addr = ('0.0.0.0', 4000)
    udp_server(addr)
    

    Client:

    import socket
    from threading import Thread
    
    def udp_client(server):
        soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        soc.sendto(b'', server)
        data, _ = soc.recvfrom(6)
        peer = bytes_to_addr(data)
        print('peer:', *peer)
    
        Thread(target=soc.sendto, args=(b'hello', peer)).start()
        data, addr = soc.recvfrom(1024)
        print('{}:{} says {}'.format(*addr, data))
    
    server_addr = ('server_ip', 4000) # the server's  public address
    udp_client(server_addr)
    

    This code requires for the rendezvous server to have a port open (4000 in this case), and be accessible by both clients. The clients can be on the same or on different local networks. The code was tested on Windows and it works well, either with a local or a public IP.

    I have experimented with TCP hole punching but I had limited success (sometimes it seems that it works, sometimes it doesn't). I can include the code if someone wants to experiment. The concept is more or less the same, both clients start sending and receiving simultaneously, and it is described in detail in Peer-to-Peer Communication Across Network Address Translators, section 4, TCP Hole Punching.


    If both clients are on the same network, it will be much easier to communicate with each other. They would have to choose somehow which one will be a server, then they can create a normal server-client connection. The only problem here is that the clients must detect if they are on the same network. Again, the server can help with this problem, as it knows the public address of both clients. For example:

    def tcp_server(addr):
        soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        soc.bind(addr)
        soc.listen()
    
        client_a, addr_a = soc.accept()
        client_b, addr_b = soc.accept()
        client_a.send(addr_to_bytes(addr_b) + addr_to_bytes(addr_a))
        client_b.send(addr_to_bytes(addr_a) + addr_to_bytes(addr_b))
    
    def tcp_client(server):
        soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        soc.connect(server)
    
        data = soc.recv(12)
        peer_addr = bytes_to_addr(data[:6])
        my_addr = bytes_to_addr(data[6:])
    
        if my_addr[0] == peer_addr[0]:
            local_addr = (soc.getsockname()[0], peer_addr[1])
            ... connect to local address ...
    

    Here the server sends two addresses to each client, the peer's public address and the client's own public address. The clients compare the two IPs, if they match then they must be on the same local network.