Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add AutoTLS example #3103

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ jobs:
go-check:
uses: ipdxco/unified-github-workflows/.github/workflows/[email protected]
with:
go-version: "1.22.x"
go-version: "1.23.x"
go-generate-ignore-protoc-version-comments: true
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Let us know if you find any issue or if you want to contribute and add a new tut
## Examples and Tutorials

- [The libp2p 'host'](./libp2p-host)
- [The libp2p 'host' with Secure WebSockets and AutoTLS](./autotls)
- [Building an http proxy with libp2p](./http-proxy)
- [An echo host](./echo)
- [Routed echo host](./routed-echo/)
Expand Down
3 changes: 3 additions & 0 deletions examples/autotls/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
autotls
p2p-forge-certs/
identity.key
14 changes: 14 additions & 0 deletions examples/autotls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# libp2p host with Secure WebSockets and AutoTLS

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.

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`.

## Running the example

From the `go-libp2p/examples` directory run the following:

```sh
cd autotls/
go run .
```
49 changes: 49 additions & 0 deletions examples/autotls/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

// Borrowed from https://github.com/libp2p/go-libp2p-relay-daemon/blob/master/identity.go

import (
"os"

"github.com/libp2p/go-libp2p/core/crypto"
)

// LoadIdentity reads a private key from the given path and, if it does not
// exist, generates a new one.
func LoadIdentity(keyPath string) (crypto.PrivKey, error) {
if _, err := os.Stat(keyPath); err == nil {
return ReadIdentity(keyPath)
} else if os.IsNotExist(err) {
logger.Infof("Generating peer identity in %s\n", keyPath)
return GenerateIdentity(keyPath)
} else {
return nil, err
}
}

// ReadIdentity reads a private key from the given path.
func ReadIdentity(path string) (crypto.PrivKey, error) {
bytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}

return crypto.UnmarshalPrivateKey(bytes)
}

// GenerateIdentity writes a new random private key to the given path.
func GenerateIdentity(path string) (crypto.PrivKey, error) {
privk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
if err != nil {
return nil, err
}

bytes, err := crypto.MarshalPrivateKey(privk)
if err != nil {
return nil, err
}

err = os.WriteFile(path, bytes, 0400)

return privk, err
}
153 changes: 153 additions & 0 deletions examples/autotls/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"context"
"fmt"
"os"
"os/signal"
"time"

"github.com/caddyserver/certmagic"
"github.com/ipfs/go-log/v2"

p2pforge "github.com/ipshipyard/p2p-forge/client"
"github.com/libp2p/go-libp2p"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
)

var logger = log.Logger("autotls-example")

const userAgent = "go-libp2p/example/autotls"
const identityKeyFile = "identity.key"

func main() {
// Create a background context
ctx := context.Background()

log.SetLogLevel("*", "error")
log.SetLogLevel("autotls-example", "debug") // Set the log level for the example to debug
log.SetLogLevel("basichost", "info") // Set the log level for the basichost package to info
log.SetLogLevel("autotls", "debug") // Set the log level for the autotls-example package to debug
log.SetLogLevel("p2p-forge", "debug") // Set the log level for the p2pforge package to debug
log.SetLogLevel("nat", "debug") // Set the log level for the libp2p nat package to debug

certLoaded := make(chan bool, 1) // Create a channel to signal when the cert is loaded

// TODO: this should not be necessary once https://github.com/ipshipyard/p2p-forge/issues/8 is resolved
rawLogger := logger.Desugar()
certmagic.Default.Logger = rawLogger.Named("default_fixme")
certmagic.DefaultACME.Logger = rawLogger.Named("default_acme_client_fixme")

// p2pforge is the AutoTLS client library.
// The cert manager handles the creation and management of certificate
certManager, err := p2pforge.NewP2PForgeCertMgr(
// Configure CA ACME endpoint
// NOTE:
// This example uses Let's Encrypt staging CA (p2pforge.DefaultCATestEndpoint)
// which will not work correctly in browser, but is useful for initial testing.
// Production should use Let's Encrypt production CA (p2pforge.DefaultCAEndpoint).
p2pforge.WithCAEndpoint(p2pforge.DefaultCATestEndpoint), // test CA endpoint
// TODO: p2pforge.WithCAEndpoint(p2pforge.DefaultCAEndpoint), // production CA endpoint
2color marked this conversation as resolved.
Show resolved Hide resolved

// Configure where to store certificate
p2pforge.WithCertificateStorage(&certmagic.FileStorage{Path: "p2p-forge-certs"}),
2color marked this conversation as resolved.
Show resolved Hide resolved

// Configure logger to use
p2pforge.WithLogger(rawLogger.Sugar().Named("autotls")),
2color marked this conversation as resolved.
Show resolved Hide resolved

// User-Agent to use during DNS-01 ACME challenge
p2pforge.WithUserAgent(userAgent),

// Optional hook called once certificate is ready
p2pforge.WithOnCertLoaded(func() {
certLoaded <- true
}),
)

if err != nil {
panic(err)
}

// Start the cert manager
certManager.Start()
defer certManager.Stop()

// Load or generate a persistent peer identity key
privKey, err := LoadIdentity(identityKeyFile)
if err != nil {
panic(err)
}

opts := []libp2p.Option{
libp2p.Identity(privKey), // Use the loaded identity key
libp2p.DisableRelay(), // Disable relay, since we need a public IP address
libp2p.NATPortMap(), // Attempt to open ports using UPnP for NATed hosts.

libp2p.ListenAddrStrings(
// Configure default catch-all listeners for TCP and UDP
2color marked this conversation as resolved.
Show resolved Hide resolved
"/ip4/0.0.0.0/tcp/5500", // regular TCP IPv4 connections
"/ip6/::/tcp/5500", // regular TCP IPv6 connections

// Configure Secure WebSockets listeners on the same TCP port
// AutoTLS will automatically generate a certificate for this host
// and use the forge domain (`libp2p.direct`) as the SNI hostname.
fmt.Sprintf("/ip4/0.0.0.0/tcp/5500/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain),
fmt.Sprintf("/ip6/::/tcp/5500/tls/sni/*.%s/ws", p2pforge.DefaultForgeDomain),
2color marked this conversation as resolved.
Show resolved Hide resolved
),

// Configure the TCP transport
libp2p.Transport(tcp.NewTCPTransport),

// Share the same TCP listener between the TCP and WS transports
libp2p.ShareTCPListener(),

// Configure the WS transport with the AutoTLS cert manager
libp2p.Transport(ws.New, ws.WithTLSConfig(certManager.TLSConfig())),

// Configure user agent for libp2p identify protocol (https://github.com/libp2p/specs/blob/master/identify/README.md)
libp2p.UserAgent(userAgent),

// AddrsFactory takes the multiaddrs we're listening on and sets the multiaddrs to advertise to the network.
// We use the AutoTLS address factory so that the `*` in the AutoTLS address string is replaced with the
// actual IP address of the host once detected
libp2p.AddrsFactory(certManager.AddressFactory()),
}
h, err := libp2p.New(opts...)
if err != nil {
panic(err)
}

logger.Info("Host created with PeerID: ", h.ID())

// Bootstrap the DHT to verify our public IPs address with AutoNAT
dhtOpts := []dht.Option{
dht.Mode(dht.ModeClient),
dht.BootstrapPeers(dht.GetDefaultBootstrapPeerAddrInfos()...),
}
dht, err := dht.New(ctx, h, dhtOpts...)
if err != nil {
panic(err)
}

go dht.Bootstrap(ctx)

time.Sleep(5 * time.Second)

logger.Info("Addresses: ", h.Addrs())

certManager.ProvideHost(h)

select {
case <-certLoaded:
logger.Info("TLS certificate loaded ")
logger.Info("Addresses: ", h.Addrs())
case <-ctx.Done():
logger.Info("Context done")
}
// Wait for interrupt signal
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
Loading
Loading