ceremonyclient/go-libp2p-kad-dht/dual/dual.go
2024-10-12 11:48:25 -07:00

395 lines
11 KiB
Go

// Package dual provides an implementation of a split or "dual" dht, where two parallel instances
// are maintained for the global internet and the local LAN respectively.
package dual
import (
"context"
"fmt"
"sync"
dht "github.com/libp2p/go-libp2p-kad-dht"
"github.com/libp2p/go-libp2p-kad-dht/internal"
"github.com/libp2p/go-libp2p-routing-helpers/tracing"
"github.com/ipfs/go-cid"
kb "github.com/libp2p/go-libp2p-kbucket"
"github.com/libp2p/go-libp2p-kbucket/peerdiversity"
helper "github.com/libp2p/go-libp2p-routing-helpers"
ci "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/core/routing"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/hashicorp/go-multierror"
)
const tracer = tracing.Tracer("go-libp2p-kad-dht/dual")
const dualName = "Dual"
// DHT implements the routing interface to provide two concrete DHT implementationts for use
// in IPFS that are used to support both global network users and disjoint LAN usecases.
type DHT struct {
WAN *dht.IpfsDHT
LAN *dht.IpfsDHT
}
// LanExtension is used to differentiate local protocol requests from those on the WAN DHT.
const LanExtension protocol.ID = "/lan"
// Assert that IPFS assumptions about interfaces aren't broken. These aren't a
// guarantee, but we can use them to aid refactoring.
var (
_ routing.ContentRouting = (*DHT)(nil)
_ routing.Routing = (*DHT)(nil)
_ routing.PeerRouting = (*DHT)(nil)
_ routing.PubKeyFetcher = (*DHT)(nil)
_ routing.ValueStore = (*DHT)(nil)
)
var (
maxPrefixCountPerCpl = 2
maxPrefixCount = 3
)
type config struct {
wan, lan []dht.Option
}
func (cfg *config) apply(opts ...Option) error {
for i, o := range opts {
if err := o(cfg); err != nil {
return fmt.Errorf("dual dht option %d failed: %w", i, err)
}
}
return nil
}
// Option is an option used to configure the Dual DHT.
type Option func(*config) error
// WanDHTOption applies the given DHT options to the WAN DHT.
func WanDHTOption(opts ...dht.Option) Option {
return func(c *config) error {
c.wan = append(c.wan, opts...)
return nil
}
}
// LanDHTOption applies the given DHT options to the LAN DHT.
func LanDHTOption(opts ...dht.Option) Option {
return func(c *config) error {
c.lan = append(c.lan, opts...)
return nil
}
}
// DHTOption applies the given DHT options to both the WAN and the LAN DHTs.
func DHTOption(opts ...dht.Option) Option {
return func(c *config) error {
c.lan = append(c.lan, opts...)
c.wan = append(c.wan, opts...)
return nil
}
}
// New creates a new DualDHT instance. Options provided are forwarded on to the two concrete
// IpfsDHT internal constructions, modulo additional options used by the Dual DHT to enforce
// the LAN-vs-WAN distinction.
// Note: query or routing table functional options provided as arguments to this function
// will be overriden by this constructor.
func New(ctx context.Context, h host.Host, options ...Option) (*DHT, error) {
var cfg config
err := cfg.apply(
WanDHTOption(
dht.QueryFilter(dht.PublicQueryFilter),
dht.RoutingTableFilter(dht.PublicRoutingTableFilter),
dht.RoutingTablePeerDiversityFilter(dht.NewRTPeerDiversityFilter(h, maxPrefixCountPerCpl, maxPrefixCount)),
// filter out all private addresses
dht.AddressFilter(func(addrs []ma.Multiaddr) []ma.Multiaddr {
return ma.FilterAddrs(addrs, func(a ma.Multiaddr) bool { is, err := manet.IsPublicAddr(a); return is && err == nil })
}),
),
)
if err != nil {
return nil, err
}
err = cfg.apply(
LanDHTOption(
dht.ProtocolExtension(LanExtension),
dht.QueryFilter(dht.PrivateQueryFilter),
dht.RoutingTableFilter(dht.PrivateRoutingTableFilter),
// filter out localhost IP addresses
dht.AddressFilter(func(addrs []ma.Multiaddr) []ma.Multiaddr {
return ma.FilterAddrs(addrs, func(a ma.Multiaddr) bool { return !manet.IsIPLoopback(a) })
}),
),
)
if err != nil {
return nil, err
}
err = cfg.apply(options...)
if err != nil {
return nil, err
}
wan, err := dht.New(ctx, h, cfg.wan...)
if err != nil {
return nil, err
}
// Unless overridden by user supplied options, the LAN DHT should default
// to 'AutoServer' mode.
if wan.Mode() != dht.ModeClient {
cfg.lan = append(cfg.lan, dht.Mode(dht.ModeServer))
}
lan, err := dht.New(ctx, h, cfg.lan...)
if err != nil {
return nil, err
}
impl := DHT{wan, lan}
return &impl, nil
}
// Close closes the DHT context.
func (dht *DHT) Close() error {
return combineErrors(dht.WAN.Close(), dht.LAN.Close())
}
// WANActive returns true when the WAN DHT is active (has peers).
func (dht *DHT) WANActive() bool {
return dht.WAN.RoutingTable().Size() > 0
}
// Provide adds the given cid to the content routing system.
func (dht *DHT) Provide(ctx context.Context, key cid.Cid, announce bool) (err error) {
ctx, end := tracer.Provide(dualName, ctx, key, announce)
defer func() { end(err) }()
if dht.WANActive() {
return dht.WAN.Provide(ctx, key, announce)
}
return dht.LAN.Provide(ctx, key, announce)
}
// GetRoutingTableDiversityStats fetches the Routing Table Diversity Stats.
func (dht *DHT) GetRoutingTableDiversityStats() []peerdiversity.CplDiversityStats {
if dht.WANActive() {
return dht.WAN.GetRoutingTableDiversityStats()
}
return nil
}
// FindProvidersAsync searches for peers who are able to provide a given key
func (dht *DHT) FindProvidersAsync(ctx context.Context, key cid.Cid, count int) (ch <-chan peer.AddrInfo) {
ctx, end := tracer.FindProvidersAsync(dualName, ctx, key, count)
defer func() { ch = end(ch, nil) }()
reqCtx, cancel := context.WithCancel(ctx)
outCh := make(chan peer.AddrInfo)
// Register for and merge query events if we care about them.
subCtx := reqCtx
var evtCh <-chan *routing.QueryEvent
if routing.SubscribesToQueryEvents(ctx) {
subCtx, evtCh = routing.RegisterForQueryEvents(reqCtx)
}
subCtx, span := internal.StartSpan(subCtx, "Dual.worker")
wanCh := dht.WAN.FindProvidersAsync(subCtx, key, count)
lanCh := dht.LAN.FindProvidersAsync(subCtx, key, count)
zeroCount := (count == 0)
go func() {
defer span.End()
defer cancel()
defer close(outCh)
found := make(map[peer.ID]struct{}, count)
var pi peer.AddrInfo
var qEv *routing.QueryEvent
for (zeroCount || count > 0) && (wanCh != nil || lanCh != nil) {
var ok bool
select {
case qEv, ok = <-evtCh:
if !ok {
evtCh = nil
} else if qEv != nil && qEv.Type != routing.QueryError {
routing.PublishQueryEvent(reqCtx, qEv)
}
continue
case pi, ok = <-wanCh:
if !ok {
span.AddEvent("wan finished")
wanCh = nil
continue
}
case pi, ok = <-lanCh:
if !ok {
span.AddEvent("lan finished")
lanCh = nil
continue
}
}
// already found
if _, ok = found[pi.ID]; ok {
continue
}
select {
case outCh <- pi:
found[pi.ID] = struct{}{}
count--
case <-ctx.Done():
return
}
}
if qEv != nil && qEv.Type == routing.QueryError && len(found) == 0 {
routing.PublishQueryEvent(reqCtx, qEv)
}
}()
return outCh
}
// FindPeer searches for a peer with given ID
// Note: with signed peer records, we can change this to short circuit once either DHT returns.
func (dht *DHT) FindPeer(ctx context.Context, pid peer.ID) (pi peer.AddrInfo, err error) {
ctx, end := tracer.FindPeer(dualName, ctx, pid)
defer func() { end(pi, err) }()
var wg sync.WaitGroup
wg.Add(2)
var wanInfo, lanInfo peer.AddrInfo
var wanErr, lanErr error
go func() {
defer wg.Done()
wanInfo, wanErr = dht.WAN.FindPeer(ctx, pid)
}()
go func() {
defer wg.Done()
lanInfo, lanErr = dht.LAN.FindPeer(ctx, pid)
}()
wg.Wait()
// Combine addresses. Try to avoid doing unnecessary work while we're at
// it. Note: We're ignoring the errors for now as many of our DHT
// commands can return both a result and an error.
ai := peer.AddrInfo{ID: pid}
if len(wanInfo.Addrs) == 0 {
ai.Addrs = lanInfo.Addrs
} else if len(lanInfo.Addrs) == 0 {
ai.Addrs = wanInfo.Addrs
} else {
// combine addresses
deduped := make(map[string]ma.Multiaddr, len(wanInfo.Addrs)+len(lanInfo.Addrs))
for _, addr := range wanInfo.Addrs {
deduped[string(addr.Bytes())] = addr
}
for _, addr := range lanInfo.Addrs {
deduped[string(addr.Bytes())] = addr
}
ai.Addrs = make([]ma.Multiaddr, 0, len(deduped))
for _, addr := range deduped {
ai.Addrs = append(ai.Addrs, addr)
}
}
// If one of the commands succeeded, don't return an error.
if wanErr == nil || lanErr == nil {
return ai, nil
}
// Otherwise, return what we have _and_ return the error.
return ai, combineErrors(wanErr, lanErr)
}
func combineErrors(erra, errb error) error {
// if the errors are the same, just return one.
if erra == errb {
return erra
}
// If one of the errors is a kb lookup failure (no peers in routing
// table), return the other.
if erra == kb.ErrLookupFailure {
return errb
} else if errb == kb.ErrLookupFailure {
return erra
}
return multierror.Append(erra, errb).ErrorOrNil()
}
// Bootstrap allows callers to hint to the routing system to get into a
// Boostrapped state and remain there.
func (dht *DHT) Bootstrap(ctx context.Context) (err error) {
ctx, end := tracer.Bootstrap(dualName, ctx)
defer func() { end(err) }()
erra := dht.WAN.Bootstrap(ctx)
errb := dht.LAN.Bootstrap(ctx)
return combineErrors(erra, errb)
}
// PutValue adds value corresponding to given Key.
func (dht *DHT) PutValue(ctx context.Context, key string, val []byte, opts ...routing.Option) (err error) {
ctx, end := tracer.PutValue(dualName, ctx, key, val, opts...)
defer func() { end(err) }()
if dht.WANActive() {
return dht.WAN.PutValue(ctx, key, val, opts...)
}
return dht.LAN.PutValue(ctx, key, val, opts...)
}
// GetValue searches for the value corresponding to given Key.
func (d *DHT) GetValue(ctx context.Context, key string, opts ...routing.Option) (result []byte, err error) {
ctx, end := tracer.GetValue(dualName, ctx, key, opts...)
defer func() { end(result, err) }()
lanCtx, cancelLan := context.WithCancel(ctx)
defer cancelLan()
var (
lanVal []byte
lanErr error
lanWaiter sync.WaitGroup
)
lanWaiter.Add(1)
go func() {
defer lanWaiter.Done()
lanVal, lanErr = d.LAN.GetValue(lanCtx, key, opts...)
}()
wanVal, wanErr := d.WAN.GetValue(ctx, key, opts...)
if wanErr == nil {
cancelLan()
}
lanWaiter.Wait()
if wanErr == nil {
return wanVal, nil
}
if lanErr == nil {
return lanVal, nil
}
return nil, combineErrors(wanErr, lanErr)
}
// SearchValue searches for better values from this value
func (dht *DHT) SearchValue(ctx context.Context, key string, opts ...routing.Option) (ch <-chan []byte, err error) {
ctx, end := tracer.SearchValue(dualName, ctx, key, opts...)
defer func() { ch, err = end(ch, err) }()
p := helper.Parallel{Routers: []routing.Routing{dht.WAN, dht.LAN}, Validator: dht.WAN.Validator}
return p.SearchValue(ctx, key, opts...)
}
// GetPublicKey returns the public key for the given peer.
func (dht *DHT) GetPublicKey(ctx context.Context, pid peer.ID) (ci.PubKey, error) {
p := helper.Parallel{Routers: []routing.Routing{dht.WAN, dht.LAN}, Validator: dht.WAN.Validator}
return p.GetPublicKey(ctx, pid)
}