Decentralization, and with it peer-to-peer (p2p), technologies has gradually become more popular with the advent of “Web 3.0”. This project aims to utilize several key concepts, such as UDP hole punching and signalling/rendezvous servers to implement a p2p chatting application using Python.

What is peer to peer (p2p)

Peer to peer communication is a form of decentralized communication. Traditionally, communication relied on the familiar client-server model, where each client sends messages to a central server and it is the role of the server to broadcast messages to the appropriate client/clients. However, in a peer to peer model, the idea is to bypass the need for a central server. Each client will directly communicate with the desired recipient. How you may ask? This is where UDP hole punching comes in.

Project link/source code

What is NAT and UDP hole punching

The main obstacle when it comes to trivial p2p communication is NAT, or network address translation. To summarize, NAT is a method of binding several private IP addresses (individual devices) to one public IP address (router), in order to conserve IP addresses. Instead of each device having its separate public IP, different devices connect to one router, who has one public IP, and incoming traffic is handled by NAT, who forwards the right packets to the right computers.

Obviously, this poses a problem if an external client wishes to initiate a connection with a device connected to the network. The remote device can only see the public IP, under which multiple “internal” devices could be connected to (think a home router with different devices). This problem could be mitigated by use of a signalling server (or a rendezvous server), which essentially acts as an exchange medium between two devices wishing to communicate with each other (will elaborate later). However, this brings us to the second problem, which is firewalls.

As you may expect, routers cannot, and should not be, accepting random data sent from unknown external computers, which is the purpose of firewalls. Firewalls are designed to protect the network against possible attacks. As such, (most) NATs are designed to stop unsolicited communication from external sources.

There are four main types of NATs: Full-cone, address-restricted cone, port-restricted cone, and symmetric NAT (details). UDP hole punching works with all the above configurations except for symmetric NAT (which is only typically used in large scale corporate network), and as such will not be discussed.

Full-cone NAT is where an internal IP:port is mapped to an external IP:port, and any traffic sent to said external IP:port will be sent to the internal device. As you can imagine, this configuration is not very secure, and as such most modern routers opts to use either address or port restricted cone NAT.

Full cone NAT

Both address and port restricted cone NATs operate under the same premise, albeit with slight differences. Both NATs will only allow packets sent to an IP:port if an internal device has previously voluntarily sent a packet to the sender. In other words, B can only talk to A if A have previously talked to B. The only difference between an address and a port restricted cone NAT is that an address restricted cone NAT allows inbound traffic from any port as long as an internal device has previously sent a packet to the host. Whereas in a port restricted cone NAT, the inbound traffic have to be from the correct host and correct port. If A sent a packet to B’s port 5000, then B can only talk back to A if B sent its traffic through its own port 5000.

Address restricted cone NAT Port restricted cone NAT

This work great in a typical use case, such as web browsing, as it allows inbound traffic if and only if the user voluntarily requested for it. However, this makes p2p communication challenging, as the receiver has to initiate the communication process in order for it to work. Or, to put it simply. if A want to talk to B, then B has to somehow magically know that A wishes to talk (the magic is known as a signalling server), and initiate the conversation first.

So where does UDP hole punching comes in?

UDP hole punching refers to fact that the receiver has to “talk first”. It is a technique where the recipient sends a single UDP packet to the sender in order to “punch a hole” in the firewall, in order for the sender to be able to send information to the recipient. Why UDP you may ask? Isn’t UDP horrible? In this case, because of the fact UDP does not expect a response (unlike TCP), it is perfect for the initial “hole punch” process, as the hole punching packet does not need a response. UDP vs TCP As mentioned above, this technique relies on some magical intuition from the receiver that someone is wants to talk to them, as the receiver has to initiate the punch. This leads to the purpose of a signalling server

What is a signalling server

Sadly, mind reading does not exist in this world (at least yet), which means decentralized p2p cannot be 100% decentralized, due to aforementioned problems. However, if one is to look past this slight inconvenience, signalling servers is a great way to overcome the lack of magic.

To put simply, signalling server (or a rendezvous server) is a way for two clients to express their intent to talk to each other, but not actually talk to each other. Although it is a form of centralization, the server merely captures intent to communicate, rather than the actual content of communication, which is of course transmitted directly via p2p.

If A wishes to talk to B, then A could make an “offer” via the signalling server. All clients would be constantly listening/subscribed to the signalling server, so B should receive the offer from A. From the on, B could initiate a UDP hole punch using information in the offer (such as sender/receiver IP, and sender port, which is the port to punch), whilst providing an answer to the offer to the signalling server. A would then receive answer via the signalling server (with information such as receiver listening port), and assuming the UDP hole punch is successful, could begin communicating with B.

How does this all work together

In this project, we will be using Python sockets and firebase-firestore as the signalling server (due to its event listening capabilities). The workflow will look something like this:

  1. Client A and Client B both initiate program and begin to listen for offers directed to them (from firebase)
  2. Client A wishes to send Client B a message (knowing their IP). Client A makes an offer with sender’s IP, receiver’s IP and sender’s port that will be punched (subsequently the port that A will communicate on)
  3. Since Client B is listening to firebase, they should receive the offer almost instantaneously, and using the information in the offer, initiate a UDP hole punch wherein Client B sends a single UDP packet to Client A on the port specified by A.
  4. After punching has been completed, Client B will respond to the offer by providing an answer with the port that it will be listening on
  5. A will receive the answer, and begins the communication with B.

Conclusion

Decentralized p2p communication is an exciting technology as it bypasses the need for a remote, central server in a world with ever-growing privacy concerns. This project aims to provide a basis for any application that requires communication, by providing an alternative as opposed to the standard server-client model.