Skip to content

Commit 3134fd2

Browse files
aschmahmannlidel
andauthored
feat(AutoTLS): opt-in WSS certs from p2p-forge at libp2p.direct (#10521)
Co-authored-by: Marcin Rataj <lidel@lidel.org>
1 parent ecb81c9 commit 3134fd2

File tree

13 files changed

+598
-52
lines changed

13 files changed

+598
-52
lines changed

config/autotls.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package config
2+
3+
import p2pforge "github.com/ipshipyard/p2p-forge/client"
4+
5+
// AutoTLS includes optional configuration of p2p-forge client of service
6+
// for obtaining a domain and TLS certificate to improve connectivity for web
7+
// browser clients. More: https://github.com/ipshipyard/p2p-forge#readme
8+
type AutoTLS struct {
9+
// Enables the p2p-forge feature
10+
Enabled Flag `json:",omitempty"`
11+
12+
// Optional override of the parent domain that will be used
13+
DomainSuffix *OptionalString `json:",omitempty"`
14+
15+
// Optional override of HTTP API that acts as ACME DNS-01 Challenge broker
16+
RegistrationEndpoint *OptionalString `json:",omitempty"`
17+
18+
// Optional Authorization token, used with private/test instances of p2p-forge
19+
RegistrationToken *OptionalString `json:",omitempty"`
20+
21+
// Optional override of CA ACME API used by p2p-forge system
22+
CAEndpoint *OptionalString `json:",omitempty"`
23+
}
24+
25+
const (
26+
DefaultAutoTLSEnabled = false // experimental, opt-in for now (https://github.com/ipfs/kubo/pull/10521)
27+
DefaultDomainSuffix = p2pforge.DefaultForgeDomain
28+
DefaultRegistrationEndpoint = p2pforge.DefaultForgeEndpoint
29+
DefaultCAEndpoint = p2pforge.DefaultCAEndpoint
30+
)

config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Config struct {
2626
API API // local node's API settings
2727
Swarm SwarmConfig
2828
AutoNAT AutoNATConfig
29+
AutoTLS AutoTLS
2930
Pubsub PubsubConfig
3031
Peering Peering
3132
DNS DNS

core/node/groups.go

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"strings"
78
"time"
89

910
"github.com/dustin/go-humanize"
@@ -113,6 +114,7 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
113114
enableRelayTransport := cfg.Swarm.Transports.Network.Relay.WithDefault(true) // nolint
114115
enableRelayService := cfg.Swarm.RelayService.Enabled.WithDefault(enableRelayTransport)
115116
enableRelayClient := cfg.Swarm.RelayClient.Enabled.WithDefault(enableRelayTransport)
117+
enableAutoTLS := cfg.AutoTLS.Enabled.WithDefault(config.DefaultAutoTLSEnabled)
116118

117119
// Log error when relay subsystem could not be initialized due to missing dependency
118120
if !enableRelayTransport {
@@ -123,6 +125,23 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
123125
logger.Fatal("Failed to enable `Swarm.RelayClient`, it requires `Swarm.Transports.Network.Relay` to be true.")
124126
}
125127
}
128+
if enableAutoTLS {
129+
if !cfg.Swarm.Transports.Network.Websocket.WithDefault(true) {
130+
logger.Fatal("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.Websocket to be true as well.")
131+
}
132+
133+
wssWildcard := fmt.Sprintf("/tls/sni/*.%s/ws", cfg.AutoTLS.DomainSuffix.WithDefault(config.DefaultDomainSuffix))
134+
wssWildcardPresent := false
135+
for _, listener := range cfg.Addresses.Swarm {
136+
if strings.Contains(listener, wssWildcard) {
137+
wssWildcardPresent = true
138+
break
139+
}
140+
}
141+
if !wssWildcardPresent {
142+
logger.Fatal(fmt.Sprintf("Invalid configuration: AutoTLS.Enabled=true requires a catch-all Addresses.Swarm listener ending with %q to be present, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls", wssWildcard))
143+
}
144+
}
126145

127146
// Gather all the options
128147
opts := fx.Options(
@@ -133,6 +152,8 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
133152

134153
// Services (resource management)
135154
fx.Provide(libp2p.ResourceManager(bcfg.Repo.Path(), cfg.Swarm, userResourceOverrides)),
155+
maybeProvide(libp2p.P2PForgeCertMgr(cfg.AutoTLS), enableAutoTLS),
156+
maybeInvoke(libp2p.StartP2PAutoTLS, enableAutoTLS),
136157
fx.Provide(libp2p.AddrFilters(cfg.Swarm.AddrFilters)),
137158
fx.Provide(libp2p.AddrsFactory(cfg.Addresses.Announce, cfg.Addresses.AppendAnnounce, cfg.Addresses.NoAnnounce)),
138159
fx.Provide(libp2p.SmuxTransport(cfg.Swarm.Transports)),

core/node/libp2p/addrs.go

+66-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
package libp2p
22

33
import (
4+
"context"
45
"fmt"
6+
"os"
57

8+
logging "github.com/ipfs/go-log"
9+
version "github.com/ipfs/kubo"
10+
"github.com/ipfs/kubo/config"
11+
p2pforge "github.com/ipshipyard/p2p-forge/client"
612
"github.com/libp2p/go-libp2p"
13+
"github.com/libp2p/go-libp2p/core/host"
714
p2pbhost "github.com/libp2p/go-libp2p/p2p/host/basic"
815
ma "github.com/multiformats/go-multiaddr"
916
mamask "github.com/whyrusleeping/multiaddr-filter"
17+
18+
"github.com/caddyserver/certmagic"
19+
"go.uber.org/fx"
1020
)
1121

1222
func AddrFilters(filters []string) func() (*ma.Filters, Libp2pOpts, error) {
@@ -87,12 +97,26 @@ func makeAddrsFactory(announce []string, appendAnnouce []string, noAnnounce []st
8797
}, nil
8898
}
8999

90-
func AddrsFactory(announce []string, appendAnnouce []string, noAnnounce []string) func() (opts Libp2pOpts, err error) {
91-
return func() (opts Libp2pOpts, err error) {
92-
addrsFactory, err := makeAddrsFactory(announce, appendAnnouce, noAnnounce)
100+
func AddrsFactory(announce []string, appendAnnouce []string, noAnnounce []string) interface{} {
101+
return func(params struct {
102+
fx.In
103+
ForgeMgr *p2pforge.P2PForgeCertMgr `optional:"true"`
104+
},
105+
) (opts Libp2pOpts, err error) {
106+
var addrsFactory p2pbhost.AddrsFactory
107+
announceAddrsFactory, err := makeAddrsFactory(announce, appendAnnouce, noAnnounce)
93108
if err != nil {
94109
return opts, err
95110
}
111+
if params.ForgeMgr == nil {
112+
addrsFactory = announceAddrsFactory
113+
} else {
114+
addrsFactory = func(multiaddrs []ma.Multiaddr) []ma.Multiaddr {
115+
forgeProcessing := params.ForgeMgr.AddressFactory()(multiaddrs)
116+
annouceProcessing := announceAddrsFactory(forgeProcessing)
117+
return annouceProcessing
118+
}
119+
}
96120
opts.Opts = append(opts.Opts, libp2p.AddrsFactory(addrsFactory))
97121
return
98122
}
@@ -107,3 +131,42 @@ func ListenOn(addresses []string) interface{} {
107131
}
108132
}
109133
}
134+
135+
func P2PForgeCertMgr(cfg config.AutoTLS) interface{} {
136+
return func() (*p2pforge.P2PForgeCertMgr, error) {
137+
storagePath, err := config.Path("", "p2p-forge-certs")
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
forgeLogger := logging.Logger("autotls").Desugar()
143+
certStorage := &certmagic.FileStorage{Path: storagePath}
144+
certMgr, err := p2pforge.NewP2PForgeCertMgr(
145+
p2pforge.WithLogger(forgeLogger.Sugar()),
146+
p2pforge.WithForgeDomain(cfg.DomainSuffix.WithDefault(config.DefaultDomainSuffix)),
147+
p2pforge.WithForgeRegistrationEndpoint(cfg.RegistrationEndpoint.WithDefault(config.DefaultRegistrationEndpoint)),
148+
p2pforge.WithCAEndpoint(cfg.CAEndpoint.WithDefault(config.DefaultCAEndpoint)),
149+
p2pforge.WithForgeAuth(cfg.RegistrationToken.WithDefault(os.Getenv(p2pforge.ForgeAuthEnv))),
150+
p2pforge.WithUserAgent(version.GetUserAgentVersion()),
151+
p2pforge.WithCertificateStorage(certStorage),
152+
)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
return certMgr, nil
158+
}
159+
}
160+
161+
func StartP2PAutoTLS(lc fx.Lifecycle, certMgr *p2pforge.P2PForgeCertMgr, h host.Host) {
162+
lc.Append(fx.Hook{
163+
OnStart: func(ctx context.Context) error {
164+
certMgr.ProvideHost(h)
165+
return certMgr.Start()
166+
},
167+
OnStop: func(ctx context.Context) error {
168+
certMgr.Stop()
169+
return nil
170+
},
171+
})
172+
}

core/node/libp2p/transport.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package libp2p
22

33
import (
44
"fmt"
5-
65
"github.com/ipfs/kubo/config"
6+
"github.com/ipshipyard/p2p-forge/client"
77
"github.com/libp2p/go-libp2p"
88
"github.com/libp2p/go-libp2p/core/metrics"
99
quic "github.com/libp2p/go-libp2p/p2p/transport/quic"
@@ -16,20 +16,25 @@ import (
1616
)
1717

1818
func Transports(tptConfig config.Transports) interface{} {
19-
return func(pnet struct {
19+
return func(params struct {
2020
fx.In
21-
Fprint PNetFingerprint `optional:"true"`
21+
Fprint PNetFingerprint `optional:"true"`
22+
ForgeMgr *client.P2PForgeCertMgr `optional:"true"`
2223
},
2324
) (opts Libp2pOpts, err error) {
24-
privateNetworkEnabled := pnet.Fprint != nil
25+
privateNetworkEnabled := params.Fprint != nil
2526

2627
if tptConfig.Network.TCP.WithDefault(true) {
2728
// TODO(9290): Make WithMetrics configurable
2829
opts.Opts = append(opts.Opts, libp2p.Transport(tcp.NewTCPTransport, tcp.WithMetrics()))
2930
}
3031

3132
if tptConfig.Network.Websocket.WithDefault(true) {
32-
opts.Opts = append(opts.Opts, libp2p.Transport(websocket.New))
33+
if params.ForgeMgr == nil {
34+
opts.Opts = append(opts.Opts, libp2p.Transport(websocket.New))
35+
} else {
36+
opts.Opts = append(opts.Opts, libp2p.Transport(websocket.New, websocket.WithTLSConfig(params.ForgeMgr.TLSConfig())))
37+
}
3338
}
3439

3540
if tptConfig.Network.QUIC.WithDefault(!privateNetworkEnabled) {

docs/changelogs/v0.32.md

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
# Kubo changelog v0.32
22

3-
- [v0.31.0](#v0320)
3+
- [v0.32.0](#v0310)
44

55
## v0.32.0
66

77
- [Overview](#overview)
88
- [🔦 Highlights](#-highlights)
9-
- [go-libp2p updates](#go-libp2p-updated)
10-
- [update boxo](#update-boxo)
9+
- [🎯 AutoTLS: Automatic Certificates for libp2p WebSockets via `libp2p.direct`](#-autotls-automatic-certificates-for-libp2p-websockets-via-libp2pdirect)
10+
- [📦️ Boxo and go-libp2p updates](#-boxo-and-go-libp2p-updates)
1111
- [📝 Changelog](#-changelog)
1212
- [👨‍👩‍👧‍👦 Contributors](#-contributors)
1313

1414
### Overview
1515

1616
### 🔦 Highlights
1717

18+
#### 🎯 AutoTLS: Automatic Certificates for libp2p WebSockets via `libp2p.direct`
1819

19-
#### go-libp2p updates
20+
This release introduces an experimental feature that significantly improves how browsers can connect to Kubo node.
21+
Opt-in configuration allows Kubo nodes to obtain CA-signed TLS certificates for [libp2p Secure WebSocket (WSS)](https://github.com/libp2p/specs/blob/master/websockets/README.md) connections automatically.
2022

23+
See [`AutoTLS`](https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls) configuration for details how to enable it. We appreciate you testing and providing an early feedback in [kubo#10560](https://github.com/ipfs/kubo/issues/10560).
24+
25+
#### 📦️ Boxo and go-libp2p updates
26+
27+
- update `boxo` to [v0.24.2](https://github.com/ipfs/boxo/releases/tag/v0.24.2). This includes a number of fixes and bitswap improvements.
2128
- update `go-libp2p` to [v0.37.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.37.0)
2229
- This update required removal of `Swarm.RelayService.MaxReservationsPerPeer` configuration option from Kubo. If you had it set, remove it from your configuration file.
2330
- update `go-libp2p-kad-dht` to [v0.27.0](https://github.com/libp2p/go-libp2p-kad-dht/releases/tag/v0.27.0)
2431
- update `go-libp2p-pubsub` to [v0.12.0](https://github.com/libp2p/go-libp2p-pubsub/releases/tag/v0.12.0)
2532

26-
#### Update Boxo
27-
28-
Update boxo to [v0.24.2](https://github.com/ipfs/boxo/releases/tag/v0.24.2). This includes a number of fixes and bitswap improvements.
29-
3033
### 📝 Changelog
3134

3235
### 👨‍👩‍👧‍👦 Contributors

docs/config.md

+110-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ config file at runtime.
2727
- [`AutoNAT.Throttle.GlobalLimit`](#autonatthrottlegloballimit)
2828
- [`AutoNAT.Throttle.PeerLimit`](#autonatthrottlepeerlimit)
2929
- [`AutoNAT.Throttle.Interval`](#autonatthrottleinterval)
30+
- [`AutoTLS`](#autotls)
31+
- [`AutoTLS.Enabled`](#autotlsenabled)
32+
- [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix)
33+
- [`AutoTLS.RegistrationEndpoint`](#autotlsregistrationendpoint)
34+
- [`AutoTLS.RegistrationToken`](#autotlsregistrationtoken)
35+
- [`AutoTLS.CAEndpoint`](#autotlscaendpoint)
3036
- [`Bootstrap`](#bootstrap)
3137
- [`Datastore`](#datastore)
3238
- [`Datastore.StorageMax`](#datastorestoragemax)
@@ -449,6 +455,109 @@ Default: 1 Minute
449455

450456
Type: `duration` (when `0`/unset, the default value is used)
451457

458+
## `AutoTLS`
459+
460+
> [!CAUTION]
461+
> This is an **EXPERIMENTAL** opt-in feature and should not be used in production yet.
462+
> Feel free to enable it and [report issues](https://github.com/ipfs/kubo/issues/new/choose) if you want to help with testing.
463+
> Track progress in [kubo#10560](https://github.com/ipfs/kubo/issues/10560).
464+
465+
AutoTLS feature enables publicly reachable Kubo nodes (those dialable from the public
466+
internet) to automatically obtain a wildcard TLS certificate for a DNS name
467+
unique to their PeerID at `*.[PeerID].libp2p.direct`. This enables direct
468+
libp2p connections and retrieval of IPFS content from browsers [Secure Context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)
469+
using transports such as [Secure WebSockets](https://github.com/libp2p/specs/blob/master/websockets/README.md),
470+
without requiring user to do any manual domain registration and ceritficate configuration.
471+
472+
Under the hood, [p2p-forge] client uses public utility service at `libp2p.direct` as an [ACME DNS-01 Challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
473+
broker enabling peer to obtain a wildcard TLS certificate tied to public key of their [PeerID](https://docs.libp2p.io/concepts/fundamentals/peers/#peer-id).
474+
475+
By default, the certificates are requested from Let's Encrypt. Origin and rationale for this project can be found in [community.letsencrypt.org discussion](https://community.letsencrypt.org/t/feedback-on-raising-certificates-per-registered-domain-to-enable-peer-to-peer-networking/223003).
476+
477+
> [!NOTE]
478+
> Public good DNS and [p2p-forge] infrastructure at `libp2p.direct` is run by the team at [Interplanetary Shipyard](https://ipshipyard.com).
479+
>
480+
> <a href="https://ipshipyard.com/"><img src="https://github.com/user-attachments/assets/39ed3504-bb71-47f6-9bf8-cb9a1698f272" /></a>
481+
482+
[p2p-forge]: https://github.com/ipshipyard/p2p-forge
483+
484+
Default: `{}`
485+
486+
Type: `object`
487+
488+
### `AutoTLS.Enabled`
489+
490+
> [!CAUTION]
491+
> This is an **EXPERIMENTAL** opt-in feature and should not be used in production yet.
492+
> Feel free to enable it and [report issues](https://github.com/ipfs/kubo/issues/new/choose) if you want to help with testing.
493+
> Track progress in [kubo#10560](https://github.com/ipfs/kubo/issues/10560).
494+
495+
Enables AutoTLS feature to get DNS+TLS for [libp2p Secure WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) listeners defined in [`Addresses.Swarm`](#addressesswarm), such as `/ip4/0.0.0.0/tcp/4002/tls/sni/*.libp2p.direct/ws` and `/ip6/::/tcp/4002/tls/sni/*.libp2p.direct/ws`.
496+
497+
If `.../tls/sni/*.libp2p.direct/ws` [multiaddr] is present in [`Addresses.Swarm`](#addressesswarm)
498+
with SNI segment ending with [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix),
499+
Kubo will obtain and set up a trusted PKI TLS certificate for it, making it diallable from web browser's [Secure Contexts](https://w3c.github.io/webappsec-secure-contexts/).
500+
501+
> [!IMPORTANT]
502+
> Caveats:
503+
> - Requires your Kubo node to be publicly diallable.
504+
> - If you want to test this with a node that is behind a NAT and uses manual port forwarding or UPnP (`Swarm.DisableNatPortMap=false`),
505+
> add catch-all `/ip4/0.0.0.0/tcp/4002/tls/sni/*.libp2p.direct/ws` and `/ip6/::/tcp/4002/tls/sni/*.libp2p.direct/ws` to [`Addresses.Swarm`](#addressesswarm)
506+
> and **wait 5-15 minutes** for libp2p node to set up and learn about own public addresses via [AutoNAT](#autonat).
507+
> - If your node is fresh and just started, the [p2p-forge] client may produce and log ERRORs during this time, but once a publicly diallable addresses are set up, a subsequent retry should be successful.
508+
> - Requires manually updating [`Addresses.Swarm`](#addressesswarm) and opening a new port
509+
> - A separate port has to be used instead of `4001` because we wait for TCP port sharing ([go-libp2p#2984](https://github.com/libp2p/go-libp2p/issues/2684)) to be implemented.
510+
> - If you use manual port forwarding, make sure incoming connections to this additional port are allowed the same way `4001` ones already are.
511+
> - The TLS certificate is used only for [libp2p WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) connections.
512+
> - Right now, this is NOT used for hosting a [Gateway](#gateway) over HTTPS (that use case still requires manual TLS setup on reverse proxy, and your own domain).
513+
514+
> [!TIP]
515+
> Debugging can be enabled by setting environment variable `GOLOG_LOG_LEVEL="error,autotls=debug,p2p-forge/client=debug"`
516+
517+
Default: `false`
518+
519+
Type: `flag`
520+
521+
### `AutoTLS.DomainSuffix`
522+
523+
Optional override of the parent domain suffix that will be used in DNS+TLS+WebSockets multiaddrs generated by [p2p-forge] client.
524+
Do not change this unless you self-host [p2p-forge].
525+
526+
Default: `libp2p.direct` (public good run by [Interplanetary Shipyard](https://ipshipyard.com))
527+
528+
Type: `optionalString`
529+
530+
### `AutoTLS.RegistrationEndpoint`
531+
532+
Optional override of [p2p-forge] HTTP registration API.
533+
Do not change this unless you self-host [p2p-forge].
534+
535+
> [!IMPORTANT]
536+
> The default endpoint performs [libp2p Peer ID Authentication over HTTP](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md)
537+
> (prooving ownership of PeerID), probes if your Kubo node can correctly answer to a [libp2p Identify](https://github.com/libp2p/specs/tree/master/identify) query.
538+
> This ensures only a correctly configured, publicly diallable Kubo can initiate [ACME DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) for `peerid.libp2p.direct`.
539+
540+
Default: `https://registration.libp2p.direct` (public good run by [Interplanetary Shipyard](https://ipshipyard.com))
541+
542+
Type: `optionalString`
543+
544+
### `AutoTLS.RegistrationToken`
545+
546+
Optional value for `Forge-Authorization` token sent with request to `RegistrationEndpoint`
547+
(useful for private/self-hosted/test instances of [p2p-forge], unset by default).
548+
549+
Default: `""`
550+
551+
Type: `optionalString`
552+
553+
### `AutoTLS.CAEndpoint`
554+
555+
Optional override of CA ACME API used by [p2p-forge] system.
556+
557+
Default: [certmagic.LetsEncryptProductionCA](https://pkg.go.dev/github.com/caddyserver/certmagic#pkg-constants) (see [community.letsencrypt.org discussion](https://community.letsencrypt.org/t/feedback-on-raising-certificates-per-registered-domain-to-enable-peer-to-peer-networking/223003))
558+
559+
Type: `optionalString`
560+
452561
## `Bootstrap`
453562

454563
Bootstrap is an array of [multiaddrs][multiaddr] of trusted nodes that your node connects to, to fetch other nodes of the network on startup.
@@ -1835,7 +1944,7 @@ Type: `optionalInteger`
18351944

18361945
#### `Swarm.RelayService.MaxReservationsPerPeer`
18371946

1838-
**REMOVED in kubo 0.32 due to removal from go-libp2p v0.37**
1947+
**REMOVED in kubo 0.32 due to [go-libp2p#2974](https://github.com/libp2p/go-libp2p/pull/2974)**
18391948

18401949
#### `Swarm.RelayService.MaxReservationsPerIP`
18411950

0 commit comments

Comments
 (0)