How to get a TLS certificate automatically
Interplanetary Shipyard operates a public-good DNS service that will answer Acme DNS-01 Challenges on behalf of any Internet user.
This means that we can use a service such as Let's Encrypt to generate certificates we can use to upgrade our WebSocket transport listeners to a Secure WebSocket version automatically.
The steps for obtaining a TLS certificate are:
- Have a publicly routable listening address
- Claim the
*.<peerID>.libp2p.direct
domain name byPOST
ing a message to register.libp2p.direct - Contact an ACME provider to perform the DNS-01 challenge and generate a certificate
- Add HTTPS listeners to any relevant transports
- Clone this repo then install the dependencies of this example with
npm install
The requirements for using libp2p.direct
are:
- A publicly routable socket address
- A WebSocket or TCP listener
- The identify protocol
ACME services normally have quite restrictive rate limits, so we'll configure a a persistent datastore and a keychain so we can reuse any generated certificates, their private keys and the PeerID it's tied to.
Let's configure the relevant modules:
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { webSockets } from '@libp2p/websockets'
import { createLibp2p } from 'libp2p'
import { autoTLS } from '@libp2p/auto-tls'
import { identify, identifyPush } from '@libp2p/identify'
import { keychain } from '@libp2p/keychain'
import { LevelDatastore } from 'datastore-level'
import { loadOrCreateSelfKey } from '@libp2p/config'
const datastore = new LevelDatastore('./db')
await datastore.open()
const privateKey = await loadOrCreateSelfKey(datastore)
const libp2p = await createLibp2p({
datastore,
privateKey,
addresses: {
listen: [
'/ip4/0.0.0.0/tcp/0/ws',
'/ip6/::/tcp/0/ws'
]
},
transports: [
webSockets()
],
connectionEncrypters: [
noise()
],
streamMuxers: [
yamux()
],
services: {
autoTLS: autoTLS(),
identify: identify(),
identifyPush: identifyPush(),
keychain: keychain()
}
})
Tip
If you want to experiment without hitting ACME service rate limits, you can
set the acmeDirectory
to a staging address, though be aware that any
certificates generated will be self-signed:
const libp2p = await createLibp2p({
// other config
services: {
autoTLS: autoTLS({
acmeDirectory: 'https://acme-staging-v02.api.letsencrypt.org/directory'
}),
// other config
}
})
If we start the node now, we'll notice that nothing happens. The reason is that we don't have a publicly routable address.
If you know you have a publicly routable address, for example if you are
deploying your app on a server or you have manually configured port forwarding
on your router then you can add your transport addresses to the appendAnnounce
config key:
Important
Multiaddrs added to announce
or appendAnnounce
will automatically be
verified as publicly dialable where possible, though note that the domain name
allocated by libp2p.direct
will still need to be verified via AutoNAT or
auto-confirmation - see the next section on confirming dialable addresses for
more
const libp2p = await createLibp2p({
addresses: {
appendAnnounce: [
'/ip4/123.123.123.123/tcp/1234/ws'
],
// other config
},
// other config
})
If you a home user and your router supports configuring port forwarding via UPnP, you can use the @libp2p/upnp-nat module to automatically configure port forwarding for IPv4 and IPv6 networks:
Important
This will only work if your router supports configuring port forwarding via UPnP and also has it enabled.
ISP provided routers sometimes do not and most ship with UPnP disabled by default - please check your router documentation for more information
import { uPnPNAT } from '@libp2p/upnp-nat'
const libp2p = await createLibp2p({
services: {
upnp: uPnPNAT()
// other config
}
})
Designing distributed systems typically involves trying to trust other system components as little as possible. Systems with a lot of trust tend to not be Byztantine fault-tolerant.
So far we've introduced two components that we are implicitly trusting. One is
libp2p.direct
- we trust that it will configure the DNS records to answer the
ACME DNS-01 challenge correctly, and we trust that the external address reported
by our UPnP router is correct.
By default libp2p will not broadcast any public address until it has been confirmed to be dialable.
We can skip this and explicitly trust libp2p.direct
and our router by
auto-confirming the DNS mapping and the public IP address:
import { uPnPNAT } from '@libp2p/upnp-nat'
const libp2p = await createLibp2p({
services: {
autoTLS: autoTLS({
// automatically mark *.<peerID>.libp2p.direct as routable
autoConfirmAddress: true
}),
upnp: uPnPNAT({
// automatically mark any detected socket address as routable
autoConfirmAddress: true
})
// other config
}
})
To not trust these actors and instead require confirmation from multiple peers in different network segments that the addresses are, in fact dialable, we need to configure @libp2p/autonat.
This requires a few more system components. The rough flow here is:
- Acquire network peers that speak the
/libp2p/autonat/1.0.0
protocol - Ask peers from a range of networks to dial us back on a specific address
- Mark the address as reachable/not reachable after enough responses are received
To find network peers we need a peer routing system such as @libp2p/kad-dht (One day we may be able to use a lightweight HTTP alternative but that day is not today).
So far we've only configured a WebSocket listener, but if we use the Amino flavour of KAD-DHT (e.g. the public IPFS network), we have to be aware of the spread of supported transports.
Because the WebSockets transport is not common, we need to add the @libp2p/tcp transport to increase our likelihood of being able to communicate with network peers.
Finally we need to also use the @libp2p/bootstrap module to connect to an initial set of peers that will let us start to fill our routing table and perform queries:
import { autoNAT } from '@libp2p/autonat'
import { bootstrap } from '@libp2p/bootstrap'
import { kadDHT, removePrivateAddressesMapper } from '@libp2p/kad-dht'
import { tcp } from '@libp2p/tcp'
const libp2p = await createLibp2p({
// other config
transports: [
// other config
tcp()
],
services: {
autoNAT: autoNAT(),
aminoDHT: kadDHT({
protocol: '/ipfs/kad/1.0.0',
peerInfoMapper: removePrivateAddressesMapper
}),
bootstrap: bootstrap({
list: [
'/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
'/dnsaddr/va1.bootstrap.libp2p.io/p2p/12D3KooWKnDdG3iXw9eTFijk3EWSunZcFi54Zka4wmtqtt6rPxc8',
'/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ'
]
})
// other config
}
})
If you are running on your own network which has better support for varied
transports you may not need to add @libp2p/tcp
.
Phew, well done making it this far.
If you are happy trusting the IP address assigned by your ISP and router, and
that libp2p.direct
has configured DNS for you correctly, you can run the
working example in ./auto-confirm.js.
If you want to go the full trust-free route, please see the example in ./trust-free.js.
After a little while you should see multiaddr(s) that include the Server name indication tuple:
$ node ./trust-free.js
/ip4/[ip-address]/tcp/[port]/tls/sni/ip-address.base32-peer-id.libp2p.direct/ws/p2p/12D3Foo
- Read the js-libp2p documentation
- Check out the js-libp2p API docs
- Check out the general libp2p documentation for tips, how-tos and more
- Read the libp2p specs
- Ask a question on the js-libp2p discussion board
Licensed under either of
- Apache 2.0, (LICENSE-APACHE / http://www.apache.org/licenses/LICENSE-2.0)
- MIT (LICENSE-MIT / http://opensource.org/licenses/MIT)
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.