Skip to content

Commit 898824c

Browse files
2colorlidelgalarghp-shahi
authored
feat: add AutoTLS example (#3103)
Co-authored-by: Daniel N <[email protected]> Co-authored-by: Marcin Rataj <[email protected]> Co-authored-by: Piotr Galar <[email protected]> Co-authored-by: Prithvi Shahi <[email protected]>
1 parent 6ce2043 commit 898824c

File tree

8 files changed

+382
-148
lines changed

8 files changed

+382
-148
lines changed

.github/workflows/go-check.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ jobs:
1717
go-check:
1818
uses: ipdxco/unified-github-workflows/.github/workflows/[email protected]
1919
with:
20-
go-version: "1.22.x"
20+
go-version: "1.23.x"
2121
go-generate-ignore-protoc-version-comments: true

examples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Let us know if you find any issue or if you want to contribute and add a new tut
77
## Examples and Tutorials
88

99
- [The libp2p 'host'](./libp2p-host)
10+
- [The libp2p 'host' with Secure WebSockets and AutoTLS](./autotls)
1011
- [Building an http proxy with libp2p](./http-proxy)
1112
- [An echo host](./echo)
1213
- [Routed echo host](./routed-echo/)

examples/autotls/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
autotls
2+
p2p-forge-certs/
3+
identity.key

examples/autotls/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# libp2p host with Secure WebSockets and AutoTLS
2+
3+
This example builds on the [libp2p host example](../libp2p-host) example and demonstrates how to use [AutoTLS](https://blog.ipfs.tech/2024-shipyard-improving-ipfs-on-the-web/#autotls-with-libp2p-direct) to automatically generate a wildcard Let's Encrypt TLS certificate unique to the libp2p host (`*.<PeerID>.libp2p.direct`), and use it with [libp2p WebSockets transport over TCP](https://github.com/libp2p/specs/blob/master/websockets/README.md) enabling browsers to directly connect to the libp2p host.
4+
5+
For this example to work, you need to have a public IP address and be publicly reachable. AutoTLS is guarded by connectivity check, and will not ask for a certificate unless your libp2p node emits `event.EvtLocalReachabilityChanged` with `network.ReachabilityPublic`.
6+
7+
## Running the example
8+
9+
From the `go-libp2p/examples` directory run the following:
10+
11+
```sh
12+
cd autotls/
13+
go run .
14+
```

examples/autotls/identity.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/libp2p/go-libp2p/core/crypto"
7+
)
8+
9+
// LoadIdentity reads a private key from the given path and, if it does not
10+
// exist, generates a new one.
11+
func LoadIdentity(keyPath string) (crypto.PrivKey, error) {
12+
if _, err := os.Stat(keyPath); err == nil {
13+
return ReadIdentity(keyPath)
14+
} else if os.IsNotExist(err) {
15+
logger.Infof("Generating peer identity in %s\n", keyPath)
16+
return GenerateIdentity(keyPath)
17+
} else {
18+
return nil, err
19+
}
20+
}
21+
22+
// ReadIdentity reads a private key from the given path.
23+
func ReadIdentity(path string) (crypto.PrivKey, error) {
24+
bytes, err := os.ReadFile(path)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
return crypto.UnmarshalPrivateKey(bytes)
30+
}
31+
32+
// GenerateIdentity writes a new random private key to the given path.
33+
func GenerateIdentity(path string) (crypto.PrivKey, error) {
34+
privk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
bytes, err := crypto.MarshalPrivateKey(privk)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
err = os.WriteFile(path, bytes, 0400)
45+
46+
return privk, err
47+
}

examples/autotls/main.go

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"time"
9+
10+
"github.com/caddyserver/certmagic"
11+
"github.com/ipfs/go-log/v2"
12+
13+
p2pforge "github.com/ipshipyard/p2p-forge/client"
14+
"github.com/libp2p/go-libp2p"
15+
dht "github.com/libp2p/go-libp2p-kad-dht"
16+
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
17+
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
18+
)
19+
20+
var logger = log.Logger("autotls-example")
21+
22+
const userAgent = "go-libp2p/example/autotls"
23+
const identityKeyFile = "identity.key"
24+
25+
func main() {
26+
// Create a background context
27+
ctx := context.Background()
28+
29+
log.SetLogLevel("*", "error")
30+
log.SetLogLevel("autotls-example", "debug") // Set the log level for the example to debug
31+
log.SetLogLevel("basichost", "info") // Set the log level for the basichost package to info
32+
log.SetLogLevel("autotls", "debug") // Set the log level for the autotls-example package to debug
33+
log.SetLogLevel("p2p-forge", "debug") // Set the log level for the p2pforge package to debug
34+
log.SetLogLevel("nat", "debug") // Set the log level for the libp2p nat package to debug
35+
36+
certLoaded := make(chan bool, 1) // Create a channel to signal when the cert is loaded
37+
38+
// use dedicated logger for autotls feature
39+
rawLogger := logger.Desugar()
40+
41+
// p2pforge is the AutoTLS client library.
42+
// The cert manager handles the creation and management of certificate
43+
certManager, err := p2pforge.NewP2PForgeCertMgr(
44+
// Configure CA ACME endpoint
45+
// NOTE:
46+
// This example uses Let's Encrypt staging CA (p2pforge.DefaultCATestEndpoint)
47+
// which will not work correctly in browser, but is useful for initial testing.
48+
// Production should use Let's Encrypt production CA (p2pforge.DefaultCAEndpoint).
49+
p2pforge.WithCAEndpoint(p2pforge.DefaultCATestEndpoint), // test CA endpoint
50+
// TODO: p2pforge.WithCAEndpoint(p2pforge.DefaultCAEndpoint), // production CA endpoint
51+
52+
// Configure where to store certificate
53+
p2pforge.WithCertificateStorage(&certmagic.FileStorage{Path: "p2p-forge-certs"}),
54+
55+
// Configure logger to use
56+
p2pforge.WithLogger(rawLogger.Sugar().Named("autotls")),
57+
58+
// User-Agent to use during DNS-01 ACME challenge
59+
p2pforge.WithUserAgent(userAgent),
60+
61+
// Optional hook called once certificate is ready
62+
p2pforge.WithOnCertLoaded(func() {
63+
certLoaded <- true
64+
}),
65+
)
66+
67+
if err != nil {
68+
panic(err)
69+
}
70+
71+
// Start the cert manager
72+
certManager.Start()
73+
defer certManager.Stop()
74+
75+
// Load or generate a persistent peer identity key
76+
privKey, err := LoadIdentity(identityKeyFile)
77+
if err != nil {
78+
panic(err)
79+
}
80+
81+
opts := []libp2p.Option{
82+
libp2p.Identity(privKey), // Use the loaded identity key
83+
libp2p.DisableRelay(), // Disable relay, since we need a public IP address
84+
libp2p.NATPortMap(), // Attempt to open ports using UPnP for NATed hosts.
85+
86+
libp2p.ListenAddrStrings(
87+
"/ip4/0.0.0.0/tcp/5500", // regular TCP IPv4 connections
88+
"/ip6/::/tcp/5500", // regular TCP IPv6 connections
89+
90+
// Configure Secure WebSockets listeners on the same TCP port
91+
// AutoTLS will automatically generate a certificate for this host
92+
// and use the forge domain (`libp2p.direct`) as the SNI hostname.
93+
fmt.Sprintf("/ip4/0.0.0.0/tcp/5500/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain),
94+
fmt.Sprintf("/ip6/::/tcp/5500/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain),
95+
),
96+
97+
// Configure the TCP transport
98+
libp2p.Transport(tcp.NewTCPTransport),
99+
100+
// Share the same TCP listener between the TCP and WS transports
101+
libp2p.ShareTCPListener(),
102+
103+
// Configure the WS transport with the AutoTLS cert manager
104+
libp2p.Transport(ws.New, ws.WithTLSConfig(certManager.TLSConfig())),
105+
106+
// Configure user agent for libp2p identify protocol (https://github.com/libp2p/specs/blob/master/identify/README.md)
107+
libp2p.UserAgent(userAgent),
108+
109+
// AddrsFactory takes the multiaddrs we're listening on and sets the multiaddrs to advertise to the network.
110+
// We use the AutoTLS address factory so that the `*` in the AutoTLS address string is replaced with the
111+
// actual IP address of the host once detected
112+
libp2p.AddrsFactory(certManager.AddressFactory()),
113+
}
114+
h, err := libp2p.New(opts...)
115+
if err != nil {
116+
panic(err)
117+
}
118+
119+
logger.Info("Host created with PeerID: ", h.ID())
120+
121+
// Bootstrap the DHT to verify our public IPs address with AutoNAT
122+
dhtOpts := []dht.Option{
123+
dht.Mode(dht.ModeClient),
124+
dht.BootstrapPeers(dht.GetDefaultBootstrapPeerAddrInfos()...),
125+
}
126+
dht, err := dht.New(ctx, h, dhtOpts...)
127+
if err != nil {
128+
panic(err)
129+
}
130+
131+
go dht.Bootstrap(ctx)
132+
133+
// Wait for peers to verify public address with AutoNAT
134+
time.Sleep(5 * time.Second)
135+
136+
logger.Info("Addresses: ", h.Addrs())
137+
138+
certManager.ProvideHost(h)
139+
140+
select {
141+
case <-certLoaded:
142+
logger.Info("TLS certificate loaded ")
143+
logger.Info("Addresses: ", h.Addrs())
144+
case <-ctx.Done():
145+
logger.Info("Context done")
146+
}
147+
// Wait for interrupt signal
148+
c := make(chan os.Signal, 1)
149+
signal.Notify(c, os.Interrupt)
150+
<-c
151+
}

0 commit comments

Comments
 (0)