diff --git a/client/acme.go b/client/acme.go index 5f7d6ca..ac2a195 100644 --- a/client/acme.go +++ b/client/acme.go @@ -39,6 +39,7 @@ type P2PForgeCertMgr struct { cfg *certmagic.Config log *zap.SugaredLogger allowPrivateForgeAddresses bool + produceShortAddrs bool hasCert bool // tracking if we've received a certificate certCheckMx sync.RWMutex @@ -85,6 +86,7 @@ type P2PForgeCertMgrConfig struct { onCertLoaded func() log *zap.SugaredLogger allowPrivateForgeAddresses bool + produceShortAddrs bool } type P2PForgeCertMgrOptions func(*P2PForgeCertMgrConfig) error @@ -184,6 +186,23 @@ func WithAllowPrivateForgeAddrs() P2PForgeCertMgrOptions { } } +// WithShortForgeAddrs controls if final addresses produced by p2p-forge addr +// factory are short and start with /dnsX or are longer and the DNS name is +// fully resolved into /ipX /sni components. +// +// Using /dnsX may be beneficial when interop with older libp2p clients is +// required, or when shorter addresses are preferred. +// +// Example multiaddr formats: +// - When true: /dnsX/../tcp//tls/ws +// - When false: /ipX//tcp//tls/sni/../ws +func WithShortForgeAddrs(produceShortAddrs bool) P2PForgeCertMgrOptions { + return func(config *P2PForgeCertMgrConfig) error { + config.produceShortAddrs = produceShortAddrs + return nil + } +} + func WithLogger(log *zap.SugaredLogger) P2PForgeCertMgrOptions { return func(config *P2PForgeCertMgrConfig) error { config.log = log @@ -301,6 +320,7 @@ func NewP2PForgeCertMgr(opts ...P2PForgeCertMgrOptions) (*P2PForgeCertMgr, error cfg: certCfg, log: mgrCfg.log, allowPrivateForgeAddresses: mgrCfg.allowPrivateForgeAddresses, + produceShortAddrs: mgrCfg.produceShortAddrs, } certCfg.OnEvent = func(ctx context.Context, event string, data map[string]any) error { @@ -417,7 +437,8 @@ func (m *P2PForgeCertMgr) TLSConfig() *tls.Config { } func (m *P2PForgeCertMgr) AddrStrings() []string { - return []string{fmt.Sprintf("/ip4/0.0.0.0/tcp/0/tls/sni/*.%s/ws", m.forgeDomain), + return []string{ + fmt.Sprintf("/ip4/0.0.0.0/tcp/0/tls/sni/*.%s/ws", m.forgeDomain), fmt.Sprintf("/ip6/::/tcp/0/tls/sni/*.%s/ws", m.forgeDomain), } } @@ -429,7 +450,7 @@ func (m *P2PForgeCertMgr) AddressFactory() config.AddrsFactory { tlsCfg := m.cfg.TLSConfig() tlsCfg.NextProtos = []string{"h2", "http/1.1"} // remove the ACME ALPN and set the HTTP 1.1 and 2 ALPNs - return m.createAddrsFactory(m.allowPrivateForgeAddresses) + return m.createAddrsFactory(m.allowPrivateForgeAddresses, m.produceShortAddrs) } // localCertExists returns true if a certificate matching passed name is already present in certmagic.Storage @@ -448,8 +469,8 @@ func certName(id peer.ID, suffixDomain string) string { return fmt.Sprintf("*.%s.%s", pb36, suffixDomain) } -func (m *P2PForgeCertMgr) createAddrsFactory(allowPrivateForgeAddrs bool) config.AddrsFactory { - var p2pForgeWssComponent = multiaddr.StringCast(fmt.Sprintf("/tls/sni/*.%s/ws", m.forgeDomain)) +func (m *P2PForgeCertMgr) createAddrsFactory(allowPrivateForgeAddrs bool, produceShortAddrs bool) config.AddrsFactory { + p2pForgeWssComponent := multiaddr.StringCast(fmt.Sprintf("/tls/sni/*.%s/ws", m.forgeDomain)) return func(multiaddrs []multiaddr.Multiaddr) []multiaddr.Multiaddr { var skipForgeAddrs bool @@ -462,7 +483,7 @@ func (m *P2PForgeCertMgr) createAddrsFactory(allowPrivateForgeAddrs bool) config } m.certCheckMx.RUnlock() - return addrFactoryFn(skipForgeAddrs, func() peer.ID { return m.hostFn().ID() }, m.forgeDomain, allowPrivateForgeAddrs, p2pForgeWssComponent, multiaddrs, m.log) + return addrFactoryFn(skipForgeAddrs, func() peer.ID { return m.hostFn().ID() }, m.forgeDomain, allowPrivateForgeAddrs, produceShortAddrs, p2pForgeWssComponent, multiaddrs, m.log) } } @@ -526,14 +547,16 @@ func (d *dns01P2PForgeSolver) Present(ctx context.Context, challenge acme.Challe } func (d *dns01P2PForgeSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error { - //TODO: Should we implement this, or is doing delete and Last-Writer-Wins enough? + // TODO: Should we implement this, or is doing delete and Last-Writer-Wins enough? return nil } -var _ acmez.Solver = (*dns01P2PForgeSolver)(nil) -var _ acmez.Waiter = (*dns01P2PForgeSolver)(nil) +var ( + _ acmez.Solver = (*dns01P2PForgeSolver)(nil) + _ acmez.Waiter = (*dns01P2PForgeSolver)(nil) +) -func addrFactoryFn(skipForgeAddrs bool, peerIDFn func() peer.ID, forgeDomain string, allowPrivateForgeAddrs bool, p2pForgeWssComponent multiaddr.Multiaddr, multiaddrs []multiaddr.Multiaddr, log *zap.SugaredLogger) []multiaddr.Multiaddr { +func addrFactoryFn(skipForgeAddrs bool, peerIDFn func() peer.ID, forgeDomain string, allowPrivateForgeAddrs bool, produceShortAddrs bool, p2pForgeWssComponent multiaddr.Multiaddr, multiaddrs []multiaddr.Multiaddr, log *zap.SugaredLogger) []multiaddr.Multiaddr { retAddrs := make([]multiaddr.Multiaddr, 0, len(multiaddrs)) for _, a := range multiaddrs { if isRelayAddr(a) { @@ -551,6 +574,7 @@ func addrFactoryFn(skipForgeAddrs bool, peerIDFn func() peer.ID, forgeDomain str index := 0 var escapedIPStr string + var ipVersion string var ipMaStr string var tcpPortStr string multiaddr.ForEach(withoutForgeWSS, func(c multiaddr.Component) bool { @@ -558,10 +582,12 @@ func addrFactoryFn(skipForgeAddrs bool, peerIDFn func() peer.ID, forgeDomain str case 0: switch c.Protocol().Code { case multiaddr.P_IP4: + ipVersion = "4" ipMaStr = c.String() ipAddr := c.Value() escapedIPStr = strings.ReplaceAll(ipAddr, ".", "-") case multiaddr.P_IP6: + ipVersion = "6" ipMaStr = c.String() ipAddr := c.Value() escapedIPStr = strings.ReplaceAll(ipAddr, ":", "-") @@ -601,9 +627,14 @@ func addrFactoryFn(skipForgeAddrs bool, peerIDFn func() peer.ID, forgeDomain str continue } - pidStr := peer.ToCid(peerIDFn()).Encode(multibase.MustNewEncoder(multibase.Base36)) + b36PidStr := peer.ToCid(peerIDFn()).Encode(multibase.MustNewEncoder(multibase.Base36)) - newMaStr := fmt.Sprintf("%s/tcp/%s/tls/sni/%s.%s.%s/ws", ipMaStr, tcpPortStr, escapedIPStr, pidStr, forgeDomain) + var newMaStr string + if produceShortAddrs { + newMaStr = fmt.Sprintf("/dns%s/%s.%s.%s/tcp/%s/tls/ws", ipVersion, escapedIPStr, b36PidStr, forgeDomain, tcpPortStr) + } else { + newMaStr = fmt.Sprintf("%s/tcp/%s/tls/sni/%s.%s.%s/ws", ipMaStr, tcpPortStr, escapedIPStr, b36PidStr, forgeDomain) + } newMA, err := multiaddr.NewMultiaddr(newMaStr) if err != nil { log.Errorf("error creating new multiaddr from %q: %s", newMaStr, err.Error()) diff --git a/e2e_test.go b/e2e_test.go index 1f59268..4a57443 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -484,139 +484,177 @@ func TestIPv6Lookup(t *testing.T) { } func TestLibp2pACMEE2E(t *testing.T) { - db := pebbleDB.NewMemoryStore() - logger := log.New(os.Stdout, "", 0) - ca := pebbleCA.New(logger, db, "", 0, 1, 0) - va := pebbleVA.New(logger, 0, 0, false, dnsServerAddress, db) - - wfeImpl := pebbleWFE.New(logger, db, va, ca, false, false, 3, 5) - muxHandler := wfeImpl.Handler() - - acmeHTTPListener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) + isValidResolvedForgeAddr := func(addr string) bool { + return strings.Contains(addr, "libp2p.direct/ws") } - defer acmeHTTPListener.Close() - - // Generate the self-signed certificate and private key - certPEM, privPEM, err := generateSelfSignedCert("127.0.0.1") - if err != nil { - log.Fatalf("Failed to generate self-signed certificate: %v", err) + isValidShortForgeAddr := func(addr string) bool { + return strings.Contains(addr, "libp2p.direct/tcp/") && strings.Contains(addr, "/tls/ws") } - // Load the certificate and key into tls.Certificate - cert, err := tls.X509KeyPair(certPEM, privPEM) - if err != nil { - log.Fatalf("Failed to load key pair: %v", err) + tests := []struct { + name string + clientOpts []client.P2PForgeCertMgrOptions + isValidForgeAddr func(addr string) bool + }{ + { + name: "default opts", + clientOpts: []client.P2PForgeCertMgrOptions{}, + isValidForgeAddr: isValidResolvedForgeAddr, + }, + { + name: "explicit WithShortForgeAddrs(true)", + clientOpts: []client.P2PForgeCertMgrOptions{client.WithShortForgeAddrs(true)}, + isValidForgeAddr: isValidShortForgeAddr, + }, + { + name: "explicit WithShortForgeAddrs(false)", + clientOpts: []client.P2PForgeCertMgrOptions{client.WithShortForgeAddrs(false)}, + isValidForgeAddr: isValidResolvedForgeAddr, + }, } - // Create a TLS configuration with the certificate - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { - // Wrap the listener with TLS - acmeHTTPListener = tls.NewListener(acmeHTTPListener, tlsConfig) + db := pebbleDB.NewMemoryStore() + logger := log.New(os.Stdout, "", 0) + ca := pebbleCA.New(logger, db, "", 0, 1, 0) + va := pebbleVA.New(logger, 0, 0, false, dnsServerAddress, db) - go func() { - http.Serve(acmeHTTPListener, muxHandler) - }() + wfeImpl := pebbleWFE.New(logger, db, va, ca, false, false, 3, 5) + muxHandler := wfeImpl.Handler() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + acmeHTTPListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer acmeHTTPListener.Close() - cas := x509.NewCertPool() - cas.AppendCertsFromPEM(certPEM) - - acmeEndpoint := fmt.Sprintf("https://%s%s", acmeHTTPListener.Addr(), pebbleWFE.DirectoryPath) - certLoaded := make(chan bool, 1) - - certMgr, err := client.NewP2PForgeCertMgr( - client.WithForgeDomain(forge), client.WithForgeRegistrationEndpoint(fmt.Sprintf("http://127.0.0.1:%d", httpPort)), client.WithCAEndpoint(acmeEndpoint), client.WithTrustedRoots(cas), - client.WithModifiedForgeRequest(func(req *http.Request) error { - req.Host = forgeRegistration - req.Header.Set(authForgeHeader, authToken) - return nil - }), - client.WithAllowPrivateForgeAddrs(), - client.WithOnCertLoaded(func() { - certLoaded <- true - })) - if err != nil { - t.Fatal(err) - } - certMgr.Start() - defer certMgr.Stop() - - h, err := libp2p.New(libp2p.ChainOptions( - libp2p.DefaultListenAddrs, - libp2p.Transport(tcp.NewTCPTransport), - libp2p.Transport(libp2pquic.NewTransport), - libp2p.Transport(libp2pwebtransport.New), - libp2p.Transport(libp2pwebrtc.New), - - libp2p.ListenAddrStrings( - certMgr.AddrStrings()..., // TODO reuse tcp port for ws - ), - libp2p.Transport(libp2pws.New, libp2pws.WithTLSConfig(certMgr.TLSConfig())), - libp2p.AddrsFactory(certMgr.AddressFactory()), - )) - if err != nil { - t.Fatal(err) - } - certMgr.ProvideHost(h) + // Generate the self-signed certificate and private key + certPEM, privPEM, err := generateSelfSignedCert("127.0.0.1") + if err != nil { + log.Fatalf("Failed to generate self-signed certificate: %v", err) + } - cp := x509.NewCertPool() - cp.AddCert(ca.GetRootCert(0).Cert) - tlsCfgWithTestCA := &tls.Config{RootCAs: cp} + // Load the certificate and key into tls.Certificate + cert, err := tls.X509KeyPair(certPEM, privPEM) + if err != nil { + log.Fatalf("Failed to load key pair: %v", err) + } - localDnsResolver, err := madns.NewResolver(madns.WithDefaultResolver(&net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: time.Second * 5, // Set a timeout for the connection + // Create a TLS configuration with the certificate + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, } - return d.DialContext(ctx, network, dnsServerAddress) - }, - })) - if err != nil { - t.Fatal(err) - } - customResolver, err := madns.NewResolver(madns.WithDomainResolver("libp2p.direct.", localDnsResolver)) - if err != nil { - t.Fatal(err) - } - h2, err := libp2p.New(libp2p.Transport(libp2pws.New, libp2pws.WithTLSClientConfig(tlsCfgWithTestCA)), - libp2p.MultiaddrResolver(swarm.ResolverFromMaDNS{Resolver: customResolver})) - if err != nil { - t.Fatal(err) - } + // Wrap the listener with TLS + acmeHTTPListener = tls.NewListener(acmeHTTPListener, tlsConfig) - select { - case <-certLoaded: - case <-time.After(time.Second * 30): - t.Fatal("timed out waiting for certificate") - } + go func() { + http.Serve(acmeHTTPListener, muxHandler) + }() - var dialAddr multiaddr.Multiaddr - hAddrs := h.Addrs() - for _, addr := range hAddrs { - as := addr.String() - if strings.Contains(as, "p2p-circuit") { - continue - } - if strings.Contains(as, "libp2p.direct/ws") { - dialAddr = addr - break - } - } - if dialAddr == nil { - t.Fatalf("no valid wss addresses: %v", hAddrs) - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - if err := h2.Connect(ctx, peer.AddrInfo{ID: h.ID(), Addrs: []multiaddr.Multiaddr{dialAddr}}); err != nil { - t.Fatal(err) + cas := x509.NewCertPool() + cas.AppendCertsFromPEM(certPEM) + + acmeEndpoint := fmt.Sprintf("https://%s%s", acmeHTTPListener.Addr(), pebbleWFE.DirectoryPath) + certLoaded := make(chan bool, 1) + + clientOpts := append([]client.P2PForgeCertMgrOptions{ + client.WithForgeDomain(forge), client.WithForgeRegistrationEndpoint(fmt.Sprintf("http://127.0.0.1:%d", httpPort)), client.WithCAEndpoint(acmeEndpoint), client.WithTrustedRoots(cas), + client.WithModifiedForgeRequest(func(req *http.Request) error { + req.Host = forgeRegistration + req.Header.Set(authForgeHeader, authToken) + return nil + }), + client.WithAllowPrivateForgeAddrs(), + client.WithOnCertLoaded(func() { + certLoaded <- true + }), + }, tt.clientOpts...) + + certMgr, err := client.NewP2PForgeCertMgr(clientOpts...) + if err != nil { + t.Fatal(err) + } + certMgr.Start() + defer certMgr.Stop() + + h, err := libp2p.New(libp2p.ChainOptions( + libp2p.DefaultListenAddrs, + libp2p.Transport(tcp.NewTCPTransport), + libp2p.Transport(libp2pquic.NewTransport), + libp2p.Transport(libp2pwebtransport.New), + libp2p.Transport(libp2pwebrtc.New), + + libp2p.ListenAddrStrings( + certMgr.AddrStrings()..., // TODO reuse tcp port for ws + ), + libp2p.Transport(libp2pws.New, libp2pws.WithTLSConfig(certMgr.TLSConfig())), + libp2p.AddrsFactory(certMgr.AddressFactory()), + )) + if err != nil { + t.Fatal(err) + } + certMgr.ProvideHost(h) + + cp := x509.NewCertPool() + cp.AddCert(ca.GetRootCert(0).Cert) + tlsCfgWithTestCA := &tls.Config{RootCAs: cp} + + localDnsResolver, err := madns.NewResolver(madns.WithDefaultResolver(&net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Second * 5, // Set a timeout for the connection + } + return d.DialContext(ctx, network, dnsServerAddress) + }, + })) + if err != nil { + t.Fatal(err) + } + customResolver, err := madns.NewResolver(madns.WithDomainResolver("libp2p.direct.", localDnsResolver)) + if err != nil { + t.Fatal(err) + } + + h2, err := libp2p.New(libp2p.Transport(libp2pws.New, libp2pws.WithTLSClientConfig(tlsCfgWithTestCA)), + libp2p.MultiaddrResolver(swarm.ResolverFromMaDNS{Resolver: customResolver})) + if err != nil { + t.Fatal(err) + } + + select { + case <-certLoaded: + case <-time.After(time.Second * 30): + t.Fatal("timed out waiting for certificate") + } + + var dialAddr multiaddr.Multiaddr + hAddrs := h.Addrs() + for _, addr := range hAddrs { + as := addr.String() + if strings.Contains(as, "p2p-circuit") { + continue + } + if tt.isValidForgeAddr(as) { + dialAddr = addr + break + } + } + if dialAddr == nil { + t.Fatalf("no valid wss addresses: %v", hAddrs) + } + + if err := h2.Connect(ctx, peer.AddrInfo{ID: h.ID(), Addrs: []multiaddr.Multiaddr{dialAddr}}); err != nil { + t.Fatal(err) + } + + }) } }