This document describes the protocol used to create a socket between two Peers
using p2psc
. All messages in the p2psc
protocol are encoded in JSON format,
and consist of a message type, a protocol version, and a payload.
Before attempting to create a socket between two Peers using p2psc
, a
Keypair
for both Peers must be either created or initialised. This is done
independently by both Peers. The protocol assumes that the connecting Peer, (the
Client), is aware of the other Peer's (the Peer) public key. The keypairs
are used so peers can prove their identities to each other, and can encrypt
their communication.
We also assume the presence of a Mediator, a server trusted by both Peers, to mediate the handshake. A Mediator specification is outlined here.
This diagram shows an overview of the happy path of the protocol, to connect a Client and a Peer using a Mediator:
A socket creation handshake happens in two stages; first, the Client registers with the Mediator (the Mediator Handshake). Then, the Mediator facilitates the creation of a connection between the Client and its desired Peer (the Peer Handshake).
A Peer advertises itself to the Mediator by sending an Advertise
message:
{
'type': kMessageTypeAdvertise,
'payload': {
'version': [p2psc protocol version],
'our_key': [Public Key],
'their_key': [Public Key]
}
}
The Mediator will then return one of three message types:
- If the Mediator considers the
Advertise
message valid, it will attempt to verify the identity of the Peer by sending anAdvertiseChallenge
message:
{
'type': kMessageTypeAdvertiseChallenge,
'payload': {
'encrypted_nonce': [Nonce encrypted with our_key]
}
}
- If the
Advertise
message is errenous (for example if the Peer sends anAdvertise
with aversion
greater than that supported by the Mediator) anAdvertiseAbort
message will be sent to the Peer, along with a reason for the abort:
{
'type': kMessageTypeAdvertiseAbort,
'payload': {
'reason': [String, reason for aborting the Mediator handshake]
}
}
- If the Mediator wants the Peer to retry the
Advertise
message (for example if theAdvertise
came on a port that the Mediator determines the Peer will be unable to bind to during the Peer Handshake) then anAdvertiseRetry
message is returned to the Peer, with a reason for the retry request:
{
'type': kMessageTypeAdvertiseRetry,
'payload': {
'reason': [String, reason for requesting a retry]
}
}
Finally, the Peer proves its identity to the Mediator by replying with an
AdvertiseResponse
containing nonce
, the encrypted_nonce
decrypted using
the Peer's private key:
{
'type': kMessageTypeAdvertiseResponse,
'payload': {
'nonce': [Decrypted encrypted_nonce from AdvertiseChallenge]
}
}
The Mediator handles an invalid AdvertiseResponse
by closing the socket.
Once the Peer has proved its identity with the Mediator, the Mediator will attempt to connect it to its specified Peer. This may not be immediate, as the specified Peer may not yet have advertised to the Mediator.
The Peer who registers with the Mediator first is considered to be the Client for the Peer Handshake, while the other Peer is considered to be the Peer.
When both the Client and Peer are registered with the Mediator, the Mediator
sends a PeerDisconnect
message to the Peer:
{
'type': kMessageTypePeerDisconnect,
'payload': {
'port': [uint16_t port number]
}
}
This message lets the Peer know that it should close its connection with the
Mediator and create a new socket bound to port
to listen for a connection
from the Client. port
will in this case be the same port that the Peer used
to communicate with the Mediator. Since the Peer was the active closer of its
socket with the Mediator, the socket's state will be in TIME_WAIT
, and so any
firewall or NAT gateway should keep a port mapping to the Peers host machine
active.
At the same time, the Mediator will send a PeerIdentification
message to
the Client:
{
'type': kMessageTypePeerIdentification,
'payload': {
'ip': [String IP address],
'port': [uint16_t port number]
}
}
In this case, ip
and port
are the IP and port of the Peer as observed by the
Mediator. The Peer will have received a PeerDisconnect
message from the
Mediator and should now be listening on port
. The Client will use the Peer's
IP and port from the PeerIdentification
message to send a PeerChallenge
message to the Peer:
{
'type': kMessageTypePeerChallenge,
'payload': {
'encrypted_nonce': [Nonce encrypted with Client's public key]
}
}
Upon receipt of this message, the Peer verifies its own identity to the Client,
and attempts to verify the identity of the Client by replying with a
PeerChallengeResponse
:
{
'type': kMessageTypePeerChallengeResponse,
'payload': {
'encrypted_nonce': [Nonce encrypted with Peer's public key],
'decrypted_nonce': [Decrypted encrypted_nonce from PeerChallenge]
}
}
Provided that the decrypted_nonce
is correct, the Client will reply with a
PeerResponse
message:
{
'type': kMessageTypePeerResponse,
'payload': {
'decrypted_nonce': [Decrypted encrypted_nonce from PeerChallengeResponse]
}
}
If the Peer does not accept the Clients PeerResponse
, it will close the
socket. Otherwise, it will reply with a PeerAcknowledgement
message:
{
'type': kMessageTypePeerAcknowledgement,
'payload': {}
}
The Peer Handshake step is now complete, as both the Client and Peer are able to communicate P2P.