diff --git a/config/config.go b/config/config.go index 4ccbc509f8..c082cbbf8c 100644 --- a/config/config.go +++ b/config/config.go @@ -150,6 +150,9 @@ func (cfg *Config) makeSwarm() (*swarm.Swarm, error) { if cfg.ResourceManager != nil { opts = append(opts, swarm.WithResourceManager(cfg.ResourceManager)) } + if cfg.MultiaddrResolver != nil { + opts = append(opts, swarm.WithMultiaddrResolver(cfg.MultiaddrResolver)) + } // TODO: Make the swarm implementation configurable. return swarm.NewSwarm(pid, cfg.Peerstore, opts...) } @@ -218,12 +221,13 @@ func (cfg *Config) NewNode() (host.Host, error) { } h, err := bhost.NewHost(swrm, &bhost.HostOpts{ - ConnManager: cfg.ConnManager, - AddrsFactory: cfg.AddrsFactory, - NATManager: cfg.NATManager, - EnablePing: !cfg.DisablePing, - UserAgent: cfg.UserAgent, - MultiaddrResolver: cfg.MultiaddrResolver, + ConnManager: cfg.ConnManager, + AddrsFactory: cfg.AddrsFactory, + NATManager: cfg.NATManager, + EnablePing: !cfg.DisablePing, + UserAgent: cfg.UserAgent, + // TODO + // MultiaddrResolver: cfg.MultiaddrResolver, EnableHolePunching: cfg.EnableHolePunching, HolePunchingOptions: cfg.HolePunchingOptions, EnableRelayService: cfg.EnableRelayService, diff --git a/core/transport/transport.go b/core/transport/transport.go index 379e9d6d4a..ad2ee66496 100644 --- a/core/transport/transport.go +++ b/core/transport/transport.go @@ -77,6 +77,12 @@ type Transport interface { Proxy() bool } +// Resolver can be optionally implemented by transports that want to resolve or transform the +// multiaddr. +type Resolver interface { + Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) +} + // Listener is an interface closely resembling the net.Listener interface. The // only real difference is that Accept() returns Conn's of the type in this // package, and also exposes a Multiaddr method as opposed to a regular Addr diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go index d3642bd810..446ea82f17 100644 --- a/p2p/host/basic/basic_host.go +++ b/p2p/host/basic/basic_host.go @@ -38,10 +38,6 @@ import ( msmux "github.com/multiformats/go-multistream" ) -// The maximum number of address resolution steps we'll perform for a single -// peer (for all addresses). -const maxAddressResolution = 32 - // addrChangeTickrInterval is the interval between two address change ticks. var addrChangeTickrInterval = 5 * time.Second @@ -701,77 +697,16 @@ func (h *BasicHost) Connect(ctx context.Context, pi peer.AddrInfo) error { } } - resolved, err := h.resolveAddrs(ctx, h.Peerstore().PeerInfo(pi.ID)) - if err != nil { - return err - } - h.Peerstore().AddAddrs(pi.ID, resolved, peerstore.TempAddrTTL) + // TODO remove this + // resolved, err := h.resolveAddrs(ctx, h.Peerstore().PeerInfo(pi.ID)) + // if err != nil { + // return err + // } + // h.Peerstore().AddAddrs(pi.ID, resolved, peerstore.TempAddrTTL) return h.dialPeer(ctx, pi.ID) } -func (h *BasicHost) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) { - proto := ma.ProtocolWithCode(ma.P_P2P).Name - p2paddr, err := ma.NewMultiaddr("/" + proto + "/" + pi.ID.Pretty()) - if err != nil { - return nil, err - } - - resolveSteps := 0 - - // Recursively resolve all addrs. - // - // While the toResolve list is non-empty: - // * Pop an address off. - // * If the address is fully resolved, add it to the resolved list. - // * Otherwise, resolve it and add the results to the "to resolve" list. - toResolve := append(([]ma.Multiaddr)(nil), pi.Addrs...) - resolved := make([]ma.Multiaddr, 0, len(pi.Addrs)) - for len(toResolve) > 0 { - // pop the last addr off. - addr := toResolve[len(toResolve)-1] - toResolve = toResolve[:len(toResolve)-1] - - // if it's resolved, add it to the resolved list. - if !madns.Matches(addr) { - resolved = append(resolved, addr) - continue - } - - resolveSteps++ - - // We've resolved too many addresses. We can keep all the fully - // resolved addresses but we'll need to skip the rest. - if resolveSteps >= maxAddressResolution { - log.Warnf( - "peer %s asked us to resolve too many addresses: %s/%s", - pi.ID, - resolveSteps, - maxAddressResolution, - ) - continue - } - - // otherwise, resolve it - reqaddr := addr.Encapsulate(p2paddr) - resaddrs, err := h.maResolver.Resolve(ctx, reqaddr) - if err != nil { - log.Infof("error resolving %s: %s", reqaddr, err) - } - - // add the results to the toResolve list. - for _, res := range resaddrs { - pi, err := peer.AddrInfoFromP2pAddr(res) - if err != nil { - log.Infof("error parsing %s: %s", res, err) - } - toResolve = append(toResolve, pi.Addrs...) - } - } - - return resolved, nil -} - // dialPeer opens a connection to peer, and makes sure to identify // the connection once it has been opened. func (h *BasicHost) dialPeer(ctx context.Context, p peer.ID) error { diff --git a/p2p/net/swarm/dial_test.go b/p2p/net/swarm/dial_test.go index aed15d4ad3..20a5f09ffd 100644 --- a/p2p/net/swarm/dial_test.go +++ b/p2p/net/swarm/dial_test.go @@ -11,12 +11,14 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" testutil "github.com/libp2p/go-libp2p/core/test" + "github.com/libp2p/go-libp2p/p2p/net/swarm" . "github.com/libp2p/go-libp2p/p2p/net/swarm" swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing" "github.com/libp2p/go-libp2p-testing/ci" ma "github.com/multiformats/go-multiaddr" + madns "github.com/multiformats/go-multiaddr-dns" manet "github.com/multiformats/go-multiaddr/net" "github.com/stretchr/testify/require" ) @@ -45,6 +47,44 @@ func TestBasicDialPeer(t *testing.T) { s.Close() } +func TestBasicDialPeerWithResolver(t *testing.T) { + t.Parallel() + + mockResolver := madns.MockResolver{IP: make(map[string][]net.IPAddr)} + ipaddr, err := net.ResolveIPAddr("ip4", "127.0.0.1") + require.NoError(t, err) + mockResolver.IP["example.com"] = []net.IPAddr{*ipaddr} + resolver, err := madns.NewResolver(madns.WithDomainResolver("example.com", &mockResolver)) + require.NoError(t, err) + + swarms := makeSwarms(t, 2, swarmt.WithSwarmOpts([]swarm.Option{swarm.WithMultiaddrResolver(resolver)})) + defer closeSwarms(swarms) + s1 := swarms[0] + s2 := swarms[1] + + // Change the multiaddr from /ip4/127.0.0.1/... to /dns4/example.com/... so + // that the resovler has to resolve this + var s2Addrs []ma.Multiaddr + for _, a := range s2.ListenAddresses() { + _, rest := ma.SplitFunc(a, func(c ma.Component) bool { + return c.Protocol().Code == ma.P_TCP || c.Protocol().Code == ma.P_UDP + }, + ) + if rest != nil { + s2Addrs = append(s2Addrs, ma.StringCast("/dns4/example.com").Encapsulate(rest)) + } + } + + s1.Peerstore().AddAddrs(s2.LocalPeer(), s2Addrs, peerstore.PermanentAddrTTL) + + c, err := s1.DialPeer(context.Background(), s2.LocalPeer()) + require.NoError(t, err) + + s, err := c.NewStream(context.Background()) + require.NoError(t, err) + s.Close() +} + func TestDialWithNoListeners(t *testing.T) { t.Parallel() diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go index 80d67eb979..b27c20a291 100644 --- a/p2p/net/swarm/swarm.go +++ b/p2p/net/swarm/swarm.go @@ -19,6 +19,7 @@ import ( logging "github.com/ipfs/go-log/v2" ma "github.com/multiformats/go-multiaddr" + madns "github.com/multiformats/go-multiaddr-dns" ) const ( @@ -54,6 +55,14 @@ func WithConnectionGater(gater connmgr.ConnectionGater) Option { } } +// WithMultiaddrResolver sets a custom multiaddr.Resolver +func WithMultiaddrResolver(maResolver *madns.Resolver) Option { + return func(s *Swarm) error { + s.maResolver = maResolver + return nil + } +} + // WithMetrics sets a metrics reporter func WithMetrics(reporter metrics.Reporter) Option { return func(s *Swarm) error { @@ -127,6 +136,8 @@ type Swarm struct { m map[int]transport.Transport } + maResolver *madns.Resolver + // stream handlers streamh atomic.Value @@ -153,6 +164,7 @@ func NewSwarm(local peer.ID, peers peerstore.Peerstore, opts ...Option) (*Swarm, ctxCancel: cancel, dialTimeout: defaultDialTimeout, dialTimeoutLocal: defaultDialTimeoutLocal, + maResolver: madns.DefaultResolver, } s.conns.m = make(map[peer.ID][]*Conn) diff --git a/p2p/net/swarm/swarm_dial.go b/p2p/net/swarm/swarm_dial.go index 0e232a1881..4672b0b2da 100644 --- a/p2p/net/swarm/swarm_dial.go +++ b/p2p/net/swarm/swarm_dial.go @@ -13,9 +13,14 @@ import ( "github.com/libp2p/go-libp2p/core/transport" ma "github.com/multiformats/go-multiaddr" + madns "github.com/multiformats/go-multiaddr-dns" manet "github.com/multiformats/go-multiaddr/net" ) +// The maximum number of address resolution steps we'll perform for a single +// peer (for all addresses). +const maxAddressResolution = 32 + // Diagram of dial sync: // // many callers of Dial() synched w. dials many addrs results to callers @@ -292,7 +297,32 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) ([]ma.Multiaddr, er return nil, ErrNoAddresses } - goodAddrs := s.filterKnownUndialables(p, peerAddrs) + peerAddrsAfterTransportResolved := make([]ma.Multiaddr, 0, len(peerAddrs)) + for _, a := range peerAddrs { + tpt := s.TransportForDialing(a) + resolver, ok := tpt.(transport.Resolver) + if ok { + resolvedAddrs, err := resolver.Resolve(ctx, a) + if err != nil { + log.Warnf("Failed to resolve multiaddr %s by transport %v: %v", a, tpt, err) + continue + } + peerAddrsAfterTransportResolved = append(peerAddrsAfterTransportResolved, resolvedAddrs...) + } else { + peerAddrsAfterTransportResolved = append(peerAddrsAfterTransportResolved, a) + } + } + + // Resolve dns or dnsaddrs + resolved, err := s.resolveAddrs(ctx, peer.AddrInfo{ + ID: p, + Addrs: peerAddrsAfterTransportResolved, + }) + if err != nil { + return nil, err + } + + goodAddrs := s.filterKnownUndialables(p, resolved) if forceDirect, _ := network.GetForceDirectDial(ctx); forceDirect { goodAddrs = ma.FilterAddrs(goodAddrs, s.nonProxyAddr) } @@ -301,7 +331,72 @@ func (s *Swarm) addrsForDial(ctx context.Context, p peer.ID) ([]ma.Multiaddr, er return nil, ErrNoGoodAddresses } - return goodAddrs, nil + // Do we store the addrs? + // h.Peerstore().AddAddrs(pi.ID, goodAddrs, peerstore.TempAddrTTL) + + return resolved, nil +} + +func (s *Swarm) resolveAddrs(ctx context.Context, pi peer.AddrInfo) ([]ma.Multiaddr, error) { + proto := ma.ProtocolWithCode(ma.P_P2P).Name + p2paddr, err := ma.NewMultiaddr("/" + proto + "/" + pi.ID.Pretty()) + if err != nil { + return nil, err + } + + resolveSteps := 0 + + // Recursively resolve all addrs. + // + // While the toResolve list is non-empty: + // * Pop an address off. + // * If the address is fully resolved, add it to the resolved list. + // * Otherwise, resolve it and add the results to the "to resolve" list. + toResolve := append(([]ma.Multiaddr)(nil), pi.Addrs...) + resolved := make([]ma.Multiaddr, 0, len(pi.Addrs)) + for len(toResolve) > 0 { + // pop the last addr off. + addr := toResolve[len(toResolve)-1] + toResolve = toResolve[:len(toResolve)-1] + + // if it's resolved, add it to the resolved list. + if !madns.Matches(addr) { + resolved = append(resolved, addr) + continue + } + + resolveSteps++ + + // We've resolved too many addresses. We can keep all the fully + // resolved addresses but we'll need to skip the rest. + if resolveSteps >= maxAddressResolution { + log.Warnf( + "peer %s asked us to resolve too many addresses: %s/%s", + pi.ID, + resolveSteps, + maxAddressResolution, + ) + continue + } + + // otherwise, resolve it + reqaddr := addr.Encapsulate(p2paddr) + resaddrs, err := s.maResolver.Resolve(ctx, reqaddr) + if err != nil { + log.Infof("error resolving %s: %s", reqaddr, err) + } + + // add the results to the toResolve list. + for _, res := range resaddrs { + pi, err := peer.AddrInfoFromP2pAddr(res) + if err != nil { + log.Infof("error parsing %s: %s", res, err) + } + toResolve = append(toResolve, pi.Addrs...) + } + } + + return resolved, nil } func (s *Swarm) dialNextAddr(ctx context.Context, p peer.ID, addr ma.Multiaddr, resch chan dialResult) error { diff --git a/p2p/net/swarm/swarm_dial_test.go b/p2p/net/swarm/swarm_dial_test.go new file mode 100644 index 0000000000..2178f27b51 --- /dev/null +++ b/p2p/net/swarm/swarm_dial_test.go @@ -0,0 +1,61 @@ +package swarm + +import ( + "context" + "crypto/rand" + "net" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/test" + "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "github.com/libp2p/go-libp2p/p2p/transport/websocket" + "github.com/multiformats/go-multiaddr" + madns "github.com/multiformats/go-multiaddr-dns" + "github.com/stretchr/testify/require" +) + +func TestAddrsForDial(t *testing.T) { + mockResolver := madns.MockResolver{IP: make(map[string][]net.IPAddr)} + ipaddr, err := net.ResolveIPAddr("ip4", "1.2.3.4") + if err != nil { + t.Fatal(err) + } + mockResolver.IP["example.com"] = []net.IPAddr{*ipaddr} + + resolver, err := madns.NewResolver(madns.WithDomainResolver("example.com", &mockResolver)) + if err != nil { + t.Fatal(err) + } + + priv, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + id, err := peer.IDFromPrivateKey(priv) + require.NoError(t, err) + + ps, err := pstoremem.NewPeerstore() + require.NoError(t, err) + ps.AddPubKey(id, priv.GetPublic()) + ps.AddPrivKey(id, priv) + t.Cleanup(func() { ps.Close() }) + + tpt, err := websocket.New(nil, network.NullResourceManager) + require.NoError(t, err) + s, err := NewSwarm(id, ps, WithMultiaddrResolver(resolver)) + require.NoError(t, err) + err = s.AddTransport(tpt) + require.NoError(t, err) + + otherPeer := test.RandPeerIDFatal(t) + + ps.AddAddr(otherPeer, multiaddr.StringCast("/dns4/example.com/tcp/1234/wss"), time.Hour) + + ctx := context.Background() + mas, err := s.addrsForDial(ctx, otherPeer) + require.NoError(t, err) + + require.NotZero(t, len(mas)) +} diff --git a/p2p/net/swarm/testing/testing.go b/p2p/net/swarm/testing/testing.go index c3704e5d28..607c0541b0 100644 --- a/p2p/net/swarm/testing/testing.go +++ b/p2p/net/swarm/testing/testing.go @@ -36,6 +36,7 @@ type config struct { connectionGater connmgr.ConnectionGater rcmgr network.ResourceManager sk crypto.PrivKey + swarmOpts []swarm.Option clock } @@ -59,6 +60,12 @@ func WithClock(clock clock) Option { } } +func WithSwarmOpts(swarmOpts []swarm.Option) Option { + return func(_ *testing.T, c *config) { + c.swarmOpts = swarmOpts + } +} + // OptDisableReuseport disables reuseport in this test swarm. var OptDisableReuseport Option = func(_ *testing.T, c *config) { c.disableReuseport = true @@ -144,7 +151,8 @@ func GenSwarm(t *testing.T, opts ...Option) *swarm.Swarm { ps.AddPrivKey(id, priv) t.Cleanup(func() { ps.Close() }) - swarmOpts := []swarm.Option{swarm.WithMetrics(metrics.NewBandwidthCounter())} + swarmOpts := cfg.swarmOpts + swarmOpts = append(swarmOpts, swarm.WithMetrics(metrics.NewBandwidthCounter())) if cfg.connectionGater != nil { swarmOpts = append(swarmOpts, swarm.WithConnectionGater(cfg.connectionGater)) } diff --git a/p2p/transport/websocket/websocket.go b/p2p/transport/websocket/websocket.go index 42c4c618f4..ec5e01ae7c 100644 --- a/p2p/transport/websocket/websocket.go +++ b/p2p/transport/websocket/websocket.go @@ -23,7 +23,7 @@ var WsFmt = mafmt.And(mafmt.TCP, mafmt.Base(ma.P_WS)) // This is _not_ WsFmt because we want the transport to stick to dialing fully // resolved addresses. -var dialMatcher = mafmt.And(mafmt.Or(mafmt.IP, mafmt.DNS), mafmt.Base(ma.P_TCP), mafmt.Or(mafmt.Base(ma.P_WS), mafmt.Base(ma.P_WSS))) +var dialMatcher = mafmt.And(mafmt.Or(mafmt.IP, mafmt.DNS), mafmt.Base(ma.P_TCP), mafmt.Or(mafmt.Base(ma.P_WS), mafmt.And(mafmt.Base(ma.P_TLS), mafmt.Base(ma.P_WS)), mafmt.Base(ma.P_WSS))) func init() { manet.RegisterFromNetAddr(ParseWebsocketNetAddr, "websocket") @@ -100,6 +100,10 @@ func (t *WebsocketTransport) Proxy() bool { return false } +func (t *WebsocketTransport) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multiaddr, error) { + return []ma.Multiaddr{maddr.Decapsulate(ma.StringCast("/wss")).Encapsulate(ma.StringCast("/tls/ws"))}, nil +} + func (t *WebsocketTransport) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (transport.CapableConn, error) { connScope, err := t.rcmgr.OpenConnection(network.DirOutbound, true, raddr) if err != nil {