Skip to content

Commit

Permalink
feat: AutoTLS.AutoWSS
Browse files Browse the repository at this point in the history
This adds AutoTLS.AutoWSS flag that is set to true by default.

It will check if Addresses.Swarm contain explicit /ws listener,
and if not found, it will append one per every /tcp listener

This way existing TCP ports are reused without any extra configuration,
but we don't break user's who have custom / explicit /ws listener
already.

I also moved logger around, to include Addresses.Swarm inspection
results in `autotls` logger.
  • Loading branch information
lidel committed Dec 4, 2024
1 parent 1831a3d commit 9b887ea
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 18 deletions.
6 changes: 5 additions & 1 deletion config/autotls.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import p2pforge "github.com/ipshipyard/p2p-forge/client"
// for obtaining a domain and TLS certificate to improve connectivity for web
// browser clients. More: https://github.com/ipshipyard/p2p-forge#readme
type AutoTLS struct {
// Enables the p2p-forge feature
// Enables the p2p-forge feature and all related features.
Enabled Flag `json:",omitempty"`

// Optional, controls if Kubo should add /tls/sni/.../ws listener to every /tcp port if no explicit /ws is defined in Addresses.Swarm
AutoWSS Flag `json:",omitempty"`

// Optional override of the parent domain that will be used
DomainSuffix *OptionalString `json:",omitempty"`

Expand All @@ -27,4 +30,5 @@ const (
DefaultDomainSuffix = p2pforge.DefaultForgeDomain
DefaultRegistrationEndpoint = p2pforge.DefaultForgeEndpoint
DefaultCAEndpoint = p2pforge.DefaultCAEndpoint
DefaultAutoWSS = true // requires AutoTLS.Enabled
)
47 changes: 44 additions & 3 deletions core/node/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -115,6 +116,8 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
enableRelayService := cfg.Swarm.RelayService.Enabled.WithDefault(enableRelayTransport)
enableRelayClient := cfg.Swarm.RelayClient.Enabled.WithDefault(enableRelayTransport)
enableAutoTLS := cfg.AutoTLS.Enabled.WithDefault(config.DefaultAutoTLSEnabled)
enableAutoWSS := cfg.AutoTLS.AutoWSS.WithDefault(config.DefaultAutoWSS)
atlsLog := log.Logger("autotls")

// Log error when relay subsystem could not be initialized due to missing dependency
if !enableRelayTransport {
Expand All @@ -125,21 +128,59 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
logger.Fatal("Failed to enable `Swarm.RelayClient`, it requires `Swarm.Transports.Network.Relay` to be true.")
}
}

if enableAutoTLS {
if !cfg.Swarm.Transports.Network.TCP.WithDefault(true) {
logger.Fatal("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.TCP to be true as well.")
}
if !cfg.Swarm.Transports.Network.Websocket.WithDefault(true) {
logger.Fatal("Invalid configuration: AutoTLS.Enabled=true requires Swarm.Transports.Network.Websocket to be true as well.")
}

// AutoTLS for Secure WebSockets: ensure WSS listeners are in place (manual or automatic)
wssWildcard := fmt.Sprintf("/tls/sni/*.%s/ws", cfg.AutoTLS.DomainSuffix.WithDefault(config.DefaultDomainSuffix))
wssWildcardPresent := false
customWsPresent := false
customWsRegex := regexp.MustCompile(`/wss?$`)
tcpRegex := regexp.MustCompile(`/tcp/\d+$`)

// inspect listeners defined in config at Addresses.Swarm
var tcpListeners []string
for _, listener := range cfg.Addresses.Swarm {
// detect if user manually added /tls/sni/.../ws listener matching AutoTLS.DomainSuffix
if strings.Contains(listener, wssWildcard) {
atlsLog.Infof("found compatible wildcard listener in Addresses.Swarm. AutoTLS will be used on %s", listener)
wssWildcardPresent = true
break
}
// detect if user manually added own /ws or /wss listener that is
// not related to AutoTLS feature
if customWsRegex.MatchString(listener) {
atlsLog.Infof("found custom /ws listener set by user in Addresses.Swarm. AutoTLS will not be used on %s.", listener)
customWsPresent = true
break
}
// else, remember /tcp listeners that can be reused for /tls/sni/../ws
if tcpRegex.MatchString(listener) {
tcpListeners = append(tcpListeners, listener)
}
}
if !wssWildcardPresent {
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))

// Append AutoTLS's wildcard listener
// if no manual /ws listener was set by the user
if enableAutoWSS && !wssWildcardPresent && !customWsPresent {
if len(tcpListeners) == 0 {
logger.Fatal("Invalid configuration: AutoTLS.AutoWSS=true requires at least one /tcp listener present in Addresses.Swarm, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls")
}
for _, tcpListener := range tcpListeners {
wssListener := tcpListener + wssWildcard
cfg.Addresses.Swarm = append(cfg.Addresses.Swarm, wssListener)
atlsLog.Infof("appended AutoWSS listener: %s", wssListener)
}
}

if !wssWildcardPresent && !enableAutoWSS {
logger.Fatal(fmt.Sprintf("Invalid configuration: AutoTLS.Enabled=true requires a /tcp listener ending with %q to be present in Addresses.Swarm or AutoTLS.AutoWSS=true, see https://github.com/ipfs/kubo/blob/master/docs/config.md#autotls", wssWildcard))
}
}

Expand All @@ -152,7 +193,7 @@ func LibP2P(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part

// Services (resource management)
fx.Provide(libp2p.ResourceManager(bcfg.Repo.Path(), cfg.Swarm, userResourceOverrides)),
maybeProvide(libp2p.P2PForgeCertMgr(bcfg.Repo.Path(), cfg.AutoTLS), enableAutoTLS),
maybeProvide(libp2p.P2PForgeCertMgr(bcfg.Repo.Path(), cfg.AutoTLS, atlsLog), enableAutoTLS),
maybeInvoke(libp2p.StartP2PAutoTLS, enableAutoTLS),
fx.Provide(libp2p.AddrFilters(cfg.Swarm.AddrFilters)),
fx.Provide(libp2p.AddrsFactory(cfg.Addresses.Announce, cfg.Addresses.AppendAnnounce, cfg.Addresses.NoAnnounce)),
Expand Down
11 changes: 5 additions & 6 deletions core/node/libp2p/addrs.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,20 @@ func ListenOn(addresses []string) interface{} {
}
}

func P2PForgeCertMgr(repoPath string, cfg config.AutoTLS) interface{} {
func P2PForgeCertMgr(repoPath string, cfg config.AutoTLS, atlsLog *logging.ZapEventLogger) interface{} {
return func() (*p2pforge.P2PForgeCertMgr, error) {
storagePath := filepath.Join(repoPath, "p2p-forge-certs")

forgeLogger := logging.Logger("autotls").Desugar()

// TODO: this should not be necessary, but we do it to help tracking
// down any race conditions causing
// https://github.com/ipshipyard/p2p-forge/issues/8
certmagic.Default.Logger = forgeLogger.Named("default_fixme")
certmagic.DefaultACME.Logger = forgeLogger.Named("default_acme_client_fixme")
rawLogger := atlsLog.Desugar()
certmagic.Default.Logger = rawLogger.Named("default_fixme")
certmagic.DefaultACME.Logger = rawLogger.Named("default_acme_client_fixme")

certStorage := &certmagic.FileStorage{Path: storagePath}
certMgr, err := p2pforge.NewP2PForgeCertMgr(
p2pforge.WithLogger(forgeLogger.Sugar()),
p2pforge.WithLogger(rawLogger.Sugar()),
p2pforge.WithForgeDomain(cfg.DomainSuffix.WithDefault(config.DefaultDomainSuffix)),
p2pforge.WithForgeRegistrationEndpoint(cfg.RegistrationEndpoint.WithDefault(config.DefaultRegistrationEndpoint)),
p2pforge.WithCAEndpoint(cfg.CAEndpoint.WithDefault(config.DefaultCAEndpoint)),
Expand Down
26 changes: 18 additions & 8 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ config file at runtime.
- [`AutoNAT.Throttle.Interval`](#autonatthrottleinterval)
- [`AutoTLS`](#autotls)
- [`AutoTLS.Enabled`](#autotlsenabled)
- [`AutoTLS.AutoWSS`](#autotlsautowss)
- [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix)
- [`AutoTLS.RegistrationEndpoint`](#autotlsregistrationendpoint)
- [`AutoTLS.RegistrationToken`](#autotlsregistrationtoken)
Expand Down Expand Up @@ -492,30 +493,39 @@ Type: `object`
> Feel free to enable it and [report issues](https://github.com/ipfs/kubo/issues/new/choose) if you want to help with testing.
> Track progress in [kubo#10560](https://github.com/ipfs/kubo/issues/10560).
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/4001/tls/sni/*.libp2p.direct/ws` and `/ip6/::/tcp/4001/tls/sni/*.libp2p.direct/ws`.
Enables AutoTLS feature to get DNS+TLS for [libp2p Secure WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) on `/tcp` port.

If `.../tls/sni/*.libp2p.direct/ws` [multiaddr] is present in [`Addresses.Swarm`](#addressesswarm)
If `AutoTLS.AutoWSS` is `true`, or `/tcp/../tls/sni/*.libp2p.direct/ws` [multiaddr] is present in [`Addresses.Swarm`](#addressesswarm)
with SNI segment ending with [`AutoTLS.DomainSuffix`](#autotlsdomainsuffix),
Kubo will obtain and set up a trusted PKI TLS certificate for it, making it dialable from web browser's [Secure Contexts](https://w3c.github.io/webappsec-secure-contexts/).
Kubo will obtain and set up a trusted PKI TLS certificate for `*.peerid.libp2p.direct`, making it dialable from web browser's [Secure Contexts](https://w3c.github.io/webappsec-secure-contexts/).

> [!TIP]
> - Most users don't need custom `/ws` config in `Addresses.Swarm`. Try running this with `AutoTLS.AutoWSS=true`: it will reuse preexisting catch-all `/tcp` ports that were already forwarded/safelisted on your firewall.
> - Debugging can be enabled by setting environment variable `GOLOG_LOG_LEVEL="error,autotls=debug,p2p-forge/client=debug"`. Less noisy `GOLOG_LOG_LEVEL="error,autotls=info` may be informative enough.
> - Certificates are stored in `$IPFS_PATH/p2p-forge-certs`. Removing directory and restarting daemon will trigger certificate rotation.
> [!IMPORTANT]
> Caveats:
> - Requires your Kubo node to be publicly dialable.
> - If you want to test this with a node that is behind a NAT and uses manual port forwarding or UPnP (`Swarm.DisableNatPortMap=false`),
> - If you want to test this with a node that is behind a NAT and uses manual TCP port forwarding or UPnP (`Swarm.DisableNatPortMap=false`), use `AutoTLS.AutoWSS=true`, or manually
> add catch-all `/ip4/0.0.0.0/tcp/4001/tls/sni/*.libp2p.direct/ws` and `/ip6/::/tcp/4001/tls/sni/*.libp2p.direct/ws` to [`Addresses.Swarm`](#addressesswarm)
> and **wait 5-15 minutes** for libp2p node to set up and learn about own public addresses via [AutoNAT](#autonat).
> - If your node is fresh and just started, the [p2p-forge] client may produce and log ERRORs during this time, but once a publicly dialable addresses are set up, a subsequent retry should be successful.
> - The TLS certificate is used only for [libp2p WebSocket](https://github.com/libp2p/specs/blob/master/websockets/README.md) connections.
> - 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).
> [!TIP]
> - Debugging can be enabled by setting environment variable `GOLOG_LOG_LEVEL="error,autotls=debug,p2p-forge/client=debug"`
> - Certificates are stored in `$IPFS_PATH/p2p-forge-certs`. Removing directory and restarting daemon will trigger certificate rotation.
Default: `false`

Type: `flag`

### `AutoTLS.AutoWSS`

Optional. Controls if Kubo should add `/tls/sni/*.libp2p.direct/ws` listener to every pre-existing `/tcp` port IFF no explicit `/ws` is defined in [`Addresses.Swarm`](#addressesswarm) already.

Default: `true` (active only if `AutoTLS.Enabled` is `true` as well)

Type: `flag`

### `AutoTLS.DomainSuffix`

Optional override of the parent domain suffix that will be used in DNS+TLS+WebSockets multiaddrs generated by [p2p-forge] client.
Expand Down

0 comments on commit 9b887ea

Please sign in to comment.