LogoPear Docs
How ToConnect to peers

Connect two peers by key with HyperDHT

Connect client and server peers by public key using HyperDHT hole punching.

HyperDHT helps clients connect to a server peer with a known public key. HyperDHT uses a series of holepunching techniques to establish direct connections between the peers, even if they're located on home networks with tricky NATs.

In the HyperDHT, peers are identified by a public key, not by an IP address. The public key is looked up in a decentralized hash table, which maps the key to an IP address and port. This means users can connect to each other irrespective of their location, even if they move between different networks.

HyperDHT's holepunching will fail if both the client peer and the server peer are on randomizing NATs, in which case the connection must be relayed through a third peer. HyperDHT does not do any relaying by default.

For example, Keet implements its relaying system wherein other call participants can serve as relays -- the more participants in the call, the stronger overall connectivity becomes.

Use the HyperDHT to create a basic CLI chat app where a client peer connects to a server peer by public key. The example consists of two applications: client-app and server-app.

This guide is a terminal-only walkthrough. For the desktop equivalent — the same peer connection wired into an Electron + Bare worker shell — follow the Getting Started path, which builds a pear:// chat app on top of the hello-pear-electron template.

Create the server app

The server-app creates a key pair, starts a server listening on it, and logs the public key. Copy that key — the client uses it to connect.

Create the server project

Create the server-app project with the following commands:

mkdir server-app
cd server-app
npm init -y
npm pkg set type="module"
npm install hyperdht b4a bare-process

Add the server logic

Alter server-app/index.js to the following.

The server starts a DHT node (L5) and generates a key pair that serves as its identity in the DHT (L8), then derives a hex string name from the public key to print and share (L9). dht.createServer registers a connection handler (L11–L13) that logs each connecting peer, wires incoming data to the console, and forwards local stdin to the connection (L15–L19). Finally, server.listen(keyPair) announces the key on the DHT and logs the public key for the client to copy (L22–L24):

server-app/index.js
import DHT from 'hyperdht'
import b4a from 'b4a'
import process from 'bare-process'

const dht = new DHT()

// This keypair is the peer identifier in the DHT
const keyPair = DHT.keyPair()
const name = b4a.toString(keyPair.publicKey, 'hex')

const server = dht.createServer(conn => {
  const peer = b4a.toString(conn.remotePublicKey, 'hex')
  console.log('* got a connection from:', peer, '*')

  conn.on('data', data => console.log(`${peer}: ${data}`))
  process.stdin.on('data', d => {
    console.log(`${name}: ${d}`)
    conn.write(d)
  })
})

server.listen(keyPair).then(() => {
  console.log('listening on:', name)
})

// Unannounce the public key before exiting the process
// (Not strictly required, but it helps avoid DHT pollution.)
process.once('SIGINT', () => server.close().then(() => process.exit(0)))

Run the server

To run the server-app, move one directory up and run the following command:

bare server-app

Create the client app

Create the client project

In another terminal create the client-app project with the following commands:

mkdir client-app
cd client-app
npm init -y
npm pkg set type="module"
npm install hyperdht b4a bare-process

Add the client logic

Alter client-app/index.js to the following.

The client reads the server's public key from the command-line argument and fails fast if it is missing (L6–L7), then decodes it from hex (L10). It starts its own DHT node (L12–L13) and calls dht.connect(publicKey) to begin hole punching toward the server (L15). The open handler logs the connected peer once the link is established (L16–L19), the data handler prints incoming messages (L21–L24), and local stdin is written to the connection (L26–L29):

client-app/index.js

import DHT from 'hyperdht'
import b4a from 'b4a'
import process from 'bare-process'

const key = Bare.argv[2]
if (!key) throw new Error('provide a key')

console.log('Connecting to:', key)
const publicKey = b4a.from(key, 'hex')

const dht = new DHT()
const name = b4a.toString(dht.defaultKeyPair.publicKey, 'hex')

const conn = dht.connect(publicKey)
conn.once('open', () => {
  const peer = b4a.toString(conn.remotePublicKey, 'hex')
  console.log('* got a connection from:', peer, '*')
})

conn.on('data', data => {
  const peer = b4a.toString(conn.remotePublicKey, 'hex')
  console.log(`${peer}: ${data}`)
})

process.stdin.on('data', d => {
  console.log(`${name}: ${d}`)
  conn.write(d)
})

Run the chat client

To run the client-app, move one directory up and run the following command:

bare client-app <SUPPLY KEY HERE>

The client-app will spin up a client, and the public key copied earlier must be supplied as a command line argument for connecting to the server. The client process will log got connection into the console when it connects to the server.

Once it's connected, try typing in both terminals.

See also

On this page