diff --git a/client/acme.go b/client/acme.go new file mode 100644 index 0000000..d0df3a0 --- /dev/null +++ b/client/acme.go @@ -0,0 +1,248 @@ +package client + +import ( + "context" + "fmt" + "net" + "strings" + "time" + + "github.com/caddyserver/certmagic" + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic" + "github.com/libp2p/go-libp2p/p2p/transport/tcp" + libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" + libp2pws "github.com/libp2p/go-libp2p/p2p/transport/websocket" + libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport" + "github.com/mholt/acmez/v2" + "github.com/mholt/acmez/v2/acme" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + "github.com/multiformats/go-multibase" +) + +var log = logging.Logger("p2p-forge/client") + +type P2PForgeCertMgr struct { + forgeDomain string + forgeRegistrationEndpoint string + cfg *certmagic.Config + h *hostWrapper +} + +type hostWrapper struct { + host.Host +} + +type hostCloseWrapper struct { + host.Host + closeFn func() error +} + +func (h hostCloseWrapper) Close() error { + return h.closeFn() +} + +var libp2pDirectWssComponent = multiaddr.StringCast("/tls/sni/*.libp2p.direct/ws") + +func NewHostWithP2PForge(forgeDomain string, forgeRegistrationEndpoint string, opts ...libp2p.Option) (host.Host, error) { + certMgr := NewP2PForgeCertMgt(forgeDomain, forgeRegistrationEndpoint) + tlsCfg := certMgr.cfg.TLSConfig() + tlsCfg.NextProtos = nil // remove the ACME ALPN + + var h host.Host + var err error + // TODO: Option passing mechanism here isn't respectful of which transports the user wants to support or the addresses they want to listen on + h, err = libp2p.New(libp2p.ChainOptions(libp2p.ChainOptions(opts...), + libp2p.DefaultListenAddrs, + libp2p.ListenAddrStrings([]string{ // TODO: Grab these addresses from a TCP listener and share the ports + fmt.Sprintf("/ip4/0.0.0.0/tcp/0/tls/sni/*.%s/ws", forgeDomain), + fmt.Sprintf("/ip6/::/tcp/0/tls/sni/*.%s/ws", forgeDomain), + }...), + libp2p.Transport(tcp.NewTCPTransport), + libp2p.Transport(libp2pquic.NewTransport), + libp2p.Transport(libp2pws.New, libp2pws.WithTLSConfig(tlsCfg)), + libp2p.Transport(libp2pwebtransport.New), + libp2p.Transport(libp2pwebrtc.New), + libp2p.AddrsFactory(func(multiaddrs []multiaddr.Multiaddr) []multiaddr.Multiaddr { + if h == nil { + return multiaddrs + } + + retAddrs := make([]multiaddr.Multiaddr, len(multiaddrs)) + for i, a := range multiaddrs { + if isRelayAddr(a) || !isPublicAddr(a) { + retAddrs[i] = a + continue + } + + // We expect the address to be of the form: /ipX//tcp//tls/sni/*.libp2p.direct/ws + // We'll then replace the * with the IP address + withoutLibp2pDirectWSS := a.Decapsulate(libp2pDirectWssComponent) + if !withoutLibp2pDirectWSS.Equal(a) { + retAddrs[i] = a + continue + } + + index := 0 + var escapedIPStr string + var ipMaStr string + var tcpPortStr string + multiaddr.ForEach(a, func(c multiaddr.Component) bool { + switch index { + case 0: + switch c.Protocol().Code { + case multiaddr.P_IP4: + ipMaStr = c.String() + ipAddr := c.Value() + escapedIPStr = strings.ReplaceAll(ipAddr, ".", "-") + case multiaddr.P_IP6: + ipMaStr = c.String() + ipAddr := c.Value() + escapedIPStr = strings.ReplaceAll(ipAddr, ":", "-") + if escapedIPStr[0] == '-' { + escapedIPStr = "0" + escapedIPStr + } + if escapedIPStr[len(escapedIPStr)-1] == '-' { + escapedIPStr = escapedIPStr + "0" + } + default: + return false + } + case 1: + if c.Protocol().Code != multiaddr.P_TCP { + return false + } + tcpPortStr = c.Value() + default: + index++ + return false + } + index++ + return true + }) + if index != 2 || escapedIPStr == "" || tcpPortStr == "" { + continue + } + + pidStr := peer.ToCid(h.ID()).Encode(multibase.MustNewEncoder(multibase.Base36)) + + newMaStr := fmt.Sprintf("%s/tcp/%s/tls/sni/%s.%s.%s/w", ipMaStr, tcpPortStr, escapedIPStr, pidStr, forgeDomain) + newMA, err := multiaddr.NewMultiaddr(newMaStr) + if err != nil { + log.Errorf("error creating new multiaddr from %q: %s", newMaStr, err.Error()) + continue + } + retAddrs[i] = newMA + } + return retAddrs + }), + )) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + if err := certMgr.Run(ctx, h); err != nil { + cancel() + return nil, err + } + + w := &hostCloseWrapper{Host: h, closeFn: func() error { + cancel() + err := h.Close() + return err + }} + + return w, nil +} + +func isRelayAddr(a multiaddr.Multiaddr) bool { + found := false + multiaddr.ForEach(a, func(c multiaddr.Component) bool { + found = c.Protocol().Code == multiaddr.P_CIRCUIT + return !found + }) + return found +} + +var publicCIDR6 = "2000::/3" +var public6 *net.IPNet + +func init() { + _, public6, _ = net.ParseCIDR(publicCIDR6) +} + +// isPublicAddr follows the logic of manet.IsPublicAddr, except it uses +// a stricter definition of "public" for ipv6: namely "is it in 2000::/3"? +func isPublicAddr(a multiaddr.Multiaddr) bool { + ip, err := manet.ToIP(a) + if err != nil { + return false + } + if ip.To4() != nil { + return !inAddrRange(ip, manet.Private4) && !inAddrRange(ip, manet.Unroutable4) + } + + return public6.Contains(ip) +} + +func inAddrRange(ip net.IP, ipnets []*net.IPNet) bool { + for _, ipnet := range ipnets { + if ipnet.Contains(ip) { + return true + } + } + + return false +} + +func NewP2PForgeCertMgt(forgeDomain string, forgeRegistrationEndpoint string) *P2PForgeCertMgr { + cfg := certmagic.NewDefault() + cfg.Storage = &certmagic.FileStorage{Path: "foo"} + h := &hostWrapper{} + myACME := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{ // TODO: UX around user passed emails + agreement + CA: certmagic.LetsEncryptStagingCA, // TODO: Switch to real CA by default + Email: "you@yours.com", + Agreed: true, + DNS01Solver: &dns01P2PForgeSolver{forgeRegistrationEndpoint, h}, + }) + cfg.Issuers = []certmagic.Issuer{myACME} + return &P2PForgeCertMgr{forgeDomain, forgeRegistrationEndpoint, cfg, h} +} + +func (m *P2PForgeCertMgr) Run(ctx context.Context, h host.Host) error { + m.h.Host = h + pb36 := peer.ToCid(h.ID()).Encode(multibase.MustNewEncoder(multibase.Base36)) + + if err := m.cfg.ManageAsync(ctx, []string{fmt.Sprintf("*.%s.libp2p.direct", pb36)}); err != nil { + return err + } + return nil +} + +type dns01P2PForgeSolver struct { + forge string + host host.Host +} + +func (d *dns01P2PForgeSolver) Wait(ctx context.Context, challenge acme.Challenge) error { + // TODO: query the authoritative DNS + time.Sleep(time.Second * 5) + return nil +} + +func (d *dns01P2PForgeSolver) Present(ctx context.Context, challenge acme.Challenge) error { + return SendChallenge(ctx, d.forge, d.host.ID(), d.host.Peerstore().PrivKey(d.host.ID()), challenge.DNS01KeyAuthorization(), d.host.Addrs()) +} + +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? + return nil +} + +var _ acmez.Solver = (*dns01P2PForgeSolver)(nil) +var _ acmez.Waiter = (*dns01P2PForgeSolver)(nil) diff --git a/go.mod b/go.mod index ab423be..c0bd18e 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,14 @@ go 1.22 require ( github.com/aws/aws-sdk-go v1.51.25 + github.com/caddyserver/certmagic v0.21.3 github.com/coredns/caddy v1.1.1 github.com/coredns/coredns v1.11.3 github.com/gorilla/mux v1.8.1 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-badger4 v0.1.5 github.com/ipfs/go-ds-dynamodb v0.1.1 + github.com/ipfs/go-log/v2 v2.5.1 github.com/libp2p/go-buffer-pool v0.1.0 github.com/libp2p/go-libp2p v0.36.1 github.com/miekg/dns v1.1.61 @@ -43,6 +45,7 @@ require ( github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect @@ -95,7 +98,6 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 // indirect github.com/ipfs/go-cid v0.4.1 // indirect - github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect @@ -105,6 +107,7 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libdns/libdns v0.2.2 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect @@ -116,6 +119,7 @@ require ( github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mholt/acmez/v2 v2.0.1 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -174,6 +178,7 @@ require ( github.com/stretchr/testify v1.9.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/wlynxg/anet v0.0.3 // indirect + github.com/zeebo/blake3 v0.2.3 // indirect go.etcd.io/etcd/api/v3 v3.5.12 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect go.etcd.io/etcd/client/v3 v3.5.12 // indirect diff --git a/go.sum b/go.sum index c5655e7..920aa81 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= +github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= +github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= +github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -297,6 +301,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= @@ -311,6 +316,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= +github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= @@ -343,6 +350,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k= +github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -562,6 +571,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= +github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A=