Skip to content

Commit 3e1eccd

Browse files
authored
docs: add auto-tls example (#194)
Adds an example showing how to configure auto-tls and includes discussion of some of the trade-offs and descisions you have to make.
1 parent 79d19a0 commit 3e1eccd

File tree

10 files changed

+568
-0
lines changed

10 files changed

+568
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ playwright-report
1414
.parcel-cache
1515
.envrc
1616
.tool-versions
17+
db
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ⚠️ IMPORTANT ⚠️
2+
3+
# Please do not create a Pull Request for this repository
4+
5+
The contents of this repository are automatically synced from the parent [js-libp2p Examples Project](https://github.com/libp2p/js-libp2p-examples) so any changes made to the standalone repository will be lost after the next sync.
6+
7+
Please open a PR against [js-libp2p Examples](https://github.com/libp2p/js-libp2p-examples) instead.
8+
9+
## Contributing
10+
11+
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
12+
13+
1. Fork the [js-libp2p Examples Project](https://github.com/libp2p/js-libp2p-examples)
14+
2. Create your Feature Branch (`git checkout -b feature/amazing-example`)
15+
3. Commit your Changes (`git commit -a -m 'feat: add some amazing example'`)
16+
4. Push to the Branch (`git push origin feature/amazing-example`)
17+
5. Open a Pull Request
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: pull
2+
3+
on:
4+
workflow_dispatch
5+
6+
jobs:
7+
sync:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v2
11+
- name: Pull from another repository
12+
uses: ipfs-examples/actions-pull-directory-from-repo@main
13+
with:
14+
source-repo: libp2p/js-libp2p-examples
15+
source-folder-path: examples/${{ github.event.repository.name }}
16+
source-branch: main
17+
target-branch: main
18+
git-username: github-actions
19+
git-email: [email protected]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
This project is dual licensed under MIT and Apache-2.0.
2+
3+
MIT: https://www.opensource.org/licenses/mit
4+
Apache-2.0: https://www.apache.org/licenses/license-2.0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
2+
3+
http://www.apache.org/licenses/LICENSE-2.0
4+
5+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
The MIT License (MIT)
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# @libp2p/example-auto-tls
2+
3+
[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
4+
[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io)
5+
[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-examples.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-examples)
6+
[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p-examples/ci.yml?branch=main\&style=flat-square)](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

Comments
 (0)