|
| 1 | +# @libp2p/example-auto-tls |
| 2 | + |
| 3 | +[](http://libp2p.io/) |
| 4 | +[](https://discuss.libp2p.io) |
| 5 | +[](https://codecov.io/gh/libp2p/js-libp2p-examples) |
| 6 | +[](https://github.com/libp2p/js-libp2p-examples/actions/workflows/ci.yml?query=branch%3Amain) |
| 7 | + |
| 8 | +> How to get a TLS certificate automatically |
| 9 | +
|
| 10 | +[Interplanetary Shipyard](https://ipshipyard.com/) operates a public-good DNS |
| 11 | +service that will answer [Acme DNS-01 Challenges](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) |
| 12 | +on behalf of any Internet user. |
| 13 | + |
| 14 | +This means that we can use a service such as [Let's Encrypt](https://letsencrypt.org/) |
| 15 | +to generate certificates we can use to upgrade our WebSocket transport listeners |
| 16 | +to a Secure WebSocket version automatically. |
| 17 | + |
| 18 | +## AutoTLS flow |
| 19 | + |
| 20 | +The steps for obtaining a TLS certificate are: |
| 21 | + |
| 22 | +1. Have a publicly routable listening address |
| 23 | +2. Claim the `*.<peerID>.libp2p.direct` domain name by `POST`ing a message to [register.libp2p.direct](https://github.com/ipshipyard/p2p-forge?tab=readme-ov-file#submitting-challenge-records) |
| 24 | +3. Contact an ACME provider to perform the [DNS-01](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) challenge and generate a certificate |
| 25 | +4. Add HTTPS listeners to any relevant transports |
| 26 | + |
| 27 | + |
| 28 | +## Setup |
| 29 | + |
| 30 | +1. Clone this repo then install the dependencies of this example with `npm install` |
| 31 | + |
| 32 | +## Instructions |
| 33 | + |
| 34 | +The requirements for using `libp2p.direct` are: |
| 35 | + |
| 36 | +1. A publicly routable socket address |
| 37 | +2. A WebSocket or TCP listener |
| 38 | +3. The [identify](https://www.npmjs.com/package/@libp2p/identify) protocol |
| 39 | + |
| 40 | +ACME services normally have quite restrictive rate limits, so we'll configure a |
| 41 | +a persistent datastore and a keychain so we can reuse any generated |
| 42 | +certificates, their private keys and the PeerID it's tied to. |
| 43 | + |
| 44 | +Let's configure the relevant modules: |
| 45 | + |
| 46 | +```js |
| 47 | +import { noise } from '@chainsafe/libp2p-noise' |
| 48 | +import { yamux } from '@chainsafe/libp2p-yamux' |
| 49 | +import { webSockets } from '@libp2p/websockets' |
| 50 | +import { createLibp2p } from 'libp2p' |
| 51 | +import { autoTLS } from '@libp2p/auto-tls' |
| 52 | +import { identify, identifyPush } from '@libp2p/identify' |
| 53 | +import { keychain } from '@libp2p/keychain' |
| 54 | +import { LevelDatastore } from 'datastore-level' |
| 55 | +import { loadOrCreateSelfKey } from '@libp2p/config' |
| 56 | + |
| 57 | +const datastore = new LevelDatastore('./db') |
| 58 | +await datastore.open() |
| 59 | + |
| 60 | +const privateKey = await loadOrCreateSelfKey(datastore) |
| 61 | + |
| 62 | +const libp2p = await createLibp2p({ |
| 63 | + datastore, |
| 64 | + privateKey, |
| 65 | + addresses: { |
| 66 | + listen: [ |
| 67 | + '/ip4/0.0.0.0/tcp/0/ws', |
| 68 | + '/ip6/::/tcp/0/ws' |
| 69 | + ] |
| 70 | + }, |
| 71 | + transports: [ |
| 72 | + webSockets() |
| 73 | + ], |
| 74 | + connectionEncrypters: [ |
| 75 | + noise() |
| 76 | + ], |
| 77 | + streamMuxers: [ |
| 78 | + yamux() |
| 79 | + ], |
| 80 | + services: { |
| 81 | + autoTLS: autoTLS(), |
| 82 | + identify: identify(), |
| 83 | + identifyPush: identifyPush(), |
| 84 | + keychain: keychain() |
| 85 | + } |
| 86 | +}) |
| 87 | +``` |
| 88 | + |
| 89 | +> [!TIP] |
| 90 | +> If you want to experiment without hitting ACME service rate limits, you can |
| 91 | +> set the `acmeDirectory` to a staging address, though be aware that any |
| 92 | +> certificates generated will be self-signed: |
| 93 | +> |
| 94 | +> ```TypeScript |
| 95 | +> const libp2p = await createLibp2p({ |
| 96 | +> // other config |
| 97 | +> services: { |
| 98 | +> autoTLS: autoTLS({ |
| 99 | +> acmeDirectory: 'https://acme-staging-v02.api.letsencrypt.org/directory' |
| 100 | +> }), |
| 101 | +> // other config |
| 102 | +> } |
| 103 | +> }) |
| 104 | +> ``` |
| 105 | +
|
| 106 | +## Getting a publicly routable address |
| 107 | +
|
| 108 | +If we start the node now, we'll notice that nothing happens. The reason is that |
| 109 | +we don't have a publicly routable address. |
| 110 | +
|
| 111 | +### Manual configuration |
| 112 | +
|
| 113 | +If you know you have a publicly routable address, for example if you are |
| 114 | +deploying your app on a server or you have manually configured port forwarding |
| 115 | +on your router then you can add your transport addresses to the `appendAnnounce` |
| 116 | +config key: |
| 117 | +
|
| 118 | +> [!IMPORTANT] |
| 119 | +> Multiaddrs added to `announce` or `appendAnnounce` will automatically be |
| 120 | +> verified as publicly dialable where possible, though note that the domain name |
| 121 | +> allocated by `libp2p.direct` will still need to be verified via AutoNAT or |
| 122 | +> auto-confirmation - see the next section on confirming dialable addresses for |
| 123 | +> more |
| 124 | +
|
| 125 | +```js |
| 126 | +const libp2p = await createLibp2p({ |
| 127 | + addresses: { |
| 128 | + appendAnnounce: [ |
| 129 | + '/ip4/123.123.123.123/tcp/1234/ws' |
| 130 | + ], |
| 131 | + // other config |
| 132 | + }, |
| 133 | + // other config |
| 134 | +}) |
| 135 | +``` |
| 136 | +
|
| 137 | +### Automatic configuration via UPnP |
| 138 | + |
| 139 | +If you a home user and your router supports configuring port forwarding via |
| 140 | +[UPnP](https://en.wikipedia.org/wiki/Universal_Plug_and_Play), you can use the |
| 141 | +[@libp2p/upnp-nat](https://www.npmjs.com/package/@libp2p/upnp-nat) module to |
| 142 | +automatically configure port forwarding for IPv4 and IPv6 networks: |
| 143 | + |
| 144 | +> [!IMPORTANT] |
| 145 | +> This will only work if your router supports configuring port forwarding via |
| 146 | +> UPnP and also has it enabled. |
| 147 | +> |
| 148 | +> ISP provided routers sometimes do not and most ship with UPnP disabled by |
| 149 | +> default - please check your router documentation for more information |
| 150 | +
|
| 151 | +```TypeScript |
| 152 | +import { uPnPNAT } from '@libp2p/upnp-nat' |
| 153 | + |
| 154 | +const libp2p = await createLibp2p({ |
| 155 | + services: { |
| 156 | + upnp: uPnPNAT() |
| 157 | + // other config |
| 158 | + } |
| 159 | +}) |
| 160 | +``` |
| 161 | + |
| 162 | +### Confirming dialable addresses |
| 163 | + |
| 164 | +Designing distributed systems typically involves trying to trust other system |
| 165 | +components as little as possible. Systems with a lot of trust tend to not be |
| 166 | +[Byztantine fault](https://en.wikipedia.org/wiki/Byzantine_fault)-tolerant. |
| 167 | + |
| 168 | +So far we've introduced two components that we are implicitly trusting. One is |
| 169 | +`libp2p.direct` - we trust that it will configure the DNS records to answer the |
| 170 | +ACME DNS-01 challenge correctly, and we trust that the external address reported |
| 171 | +by our UPnP router is correct. |
| 172 | + |
| 173 | +By default libp2p will not broadcast any public address until it has been |
| 174 | +confirmed to be dialable. |
| 175 | + |
| 176 | +We can skip this and explicitly trust `libp2p.direct` and our router by |
| 177 | +auto-confirming the DNS mapping and the public IP address: |
| 178 | + |
| 179 | +```TypeScript |
| 180 | +import { uPnPNAT } from '@libp2p/upnp-nat' |
| 181 | + |
| 182 | +const libp2p = await createLibp2p({ |
| 183 | + services: { |
| 184 | + autoTLS: autoTLS({ |
| 185 | + // automatically mark *.<peerID>.libp2p.direct as routable |
| 186 | + autoConfirmAddress: true |
| 187 | + }), |
| 188 | + upnp: uPnPNAT({ |
| 189 | + // automatically mark any detected socket address as routable |
| 190 | + autoConfirmAddress: true |
| 191 | + }) |
| 192 | + // other config |
| 193 | + } |
| 194 | +}) |
| 195 | +``` |
| 196 | + |
| 197 | +To not trust these actors and instead require confirmation from multiple peers |
| 198 | +in different network segments that the addresses are, in fact dialable, we need |
| 199 | +to configure [@libp2p/autonat](https://www.npmjs.com/package/@libp2p/autonat). |
| 200 | + |
| 201 | +#### AutoNAT |
| 202 | + |
| 203 | +This requires a few more system components. The rough flow here is: |
| 204 | + |
| 205 | +1. Acquire network peers that speak the `/libp2p/autonat/1.0.0` protocol |
| 206 | +2. Ask peers from a range of networks to dial us back on a specific address |
| 207 | +3. Mark the address as reachable/not reachable after enough responses are received |
| 208 | + |
| 209 | +To find network peers we need a peer routing system such as [@libp2p/kad-dht](https://www.npmjs.com/package/@libp2p/kad-dht) (One day we may be able to use a [lightweight HTTP alternative](https://github.com/ipfs/specs/pull/476) |
| 210 | +but that day is not today). |
| 211 | + |
| 212 | +So far we've only configured a WebSocket listener, but if we use the Amino |
| 213 | +flavour of KAD-DHT (e.g. the public IPFS network), we have to be aware of |
| 214 | +[the spread of supported transports](https://probelab.io/ipfs/amino/#dht-transport-distribution). |
| 215 | + |
| 216 | +Because the WebSockets transport is not common, we need to add the [@libp2p/tcp](https://www.npmjs.com/package/@libp2p/tcp) transport to increase our likelihood of being able to |
| 217 | +communicate with network peers. |
| 218 | + |
| 219 | +Finally we need to also use the [@libp2p/bootstrap](https://www.npmjs.com/package/@libp2p/bootstrap) |
| 220 | +module to connect to an initial set of peers that will let us start to fill our |
| 221 | +routing table and perform queries: |
| 222 | + |
| 223 | +```js |
| 224 | +import { autoNAT } from '@libp2p/autonat' |
| 225 | +import { bootstrap } from '@libp2p/bootstrap' |
| 226 | +import { kadDHT, removePrivateAddressesMapper } from '@libp2p/kad-dht' |
| 227 | +import { tcp } from '@libp2p/tcp' |
| 228 | + |
| 229 | +const libp2p = await createLibp2p({ |
| 230 | + // other config |
| 231 | + transports: [ |
| 232 | + // other config |
| 233 | + tcp() |
| 234 | + ], |
| 235 | + services: { |
| 236 | + autoNAT: autoNAT(), |
| 237 | + aminoDHT: kadDHT({ |
| 238 | + protocol: '/ipfs/kad/1.0.0', |
| 239 | + peerInfoMapper: removePrivateAddressesMapper |
| 240 | + }), |
| 241 | + bootstrap: bootstrap({ |
| 242 | + list: [ |
| 243 | + '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', |
| 244 | + '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', |
| 245 | + '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt', |
| 246 | + '/dnsaddr/va1.bootstrap.libp2p.io/p2p/12D3KooWKnDdG3iXw9eTFijk3EWSunZcFi54Zka4wmtqtt6rPxc8', |
| 247 | + '/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ' |
| 248 | + ] |
| 249 | + }) |
| 250 | + // other config |
| 251 | + } |
| 252 | +}) |
| 253 | +``` |
| 254 | + |
| 255 | +If you are running on your own network which has better support for varied |
| 256 | +transports you may not need to add `@libp2p/tcp`. |
| 257 | + |
| 258 | +## Putting it all together |
| 259 | + |
| 260 | +Phew, well done making it this far. |
| 261 | + |
| 262 | +If you are happy trusting the IP address assigned by your ISP and router, and |
| 263 | +that `libp2p.direct` has configured DNS for you correctly, you can run the |
| 264 | +working example in [./auto-confirm.js](./auto-confirm.js). |
| 265 | + |
| 266 | +If you want to go the full trust-free route, please see the example in |
| 267 | +[./trust-free.js](./trust-free.js). |
| 268 | + |
| 269 | +After a little while you should see multiaddr(s) that include the |
| 270 | +[Server name indication](https://en.wikipedia.org/wiki/Server_Name_Indication) |
| 271 | +tuple: |
| 272 | + |
| 273 | +```console |
| 274 | +$ node ./trust-free.js |
| 275 | +/ip4/[ip-address]/tcp/[port]/tls/sni/ip-address.base32-peer-id.libp2p.direct/ws/p2p/12D3Foo |
| 276 | +``` |
| 277 | + |
| 278 | +## Need help? |
| 279 | + |
| 280 | +- Read the [js-libp2p documentation](https://github.com/libp2p/js-libp2p/tree/main/doc) |
| 281 | +- Check out the [js-libp2p API docs](https://libp2p.github.io/js-libp2p/) |
| 282 | +- Check out the [general libp2p documentation](https://docs.libp2p.io) for tips, how-tos and more |
| 283 | +- Read the [libp2p specs](https://github.com/libp2p/specs) |
| 284 | +- Ask a question on the [js-libp2p discussion board](https://github.com/libp2p/js-libp2p/discussions) |
| 285 | + |
| 286 | +## License |
| 287 | + |
| 288 | +Licensed under either of |
| 289 | + |
| 290 | +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>) |
| 291 | +- MIT ([LICENSE-MIT](LICENSE-MIT) / <http://opensource.org/licenses/MIT>) |
| 292 | + |
| 293 | +## Contribution |
| 294 | + |
| 295 | +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. |
0 commit comments