mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-29 11:15:50 +00:00
This changes the new prototype PEX reactor to resolve peer address URLs into IP/port PEX addresses itself. Branched off of #5974. I've spent some time thinking about address handling in the P2P stack. We currently use `PeerAddress` URLs everywhere, except for two places: when dialing a peer, and when exchanging addresses via PEX. We had two options: 1. Resolve addresses to endpoints inside `PeerManager`. This would introduce a lot of added complexity: we would have to track connection statistics per endpoint, have goroutines that asynchronously resolve and refresh these endpoints, deal with resolve scheduling before dialing (which is trickier than it sounds since it involves multiple goroutines in the peer manager and router and messes with peer rating order), handle IP address visibility issues, and so on. 2. Resolve addresses to endpoints (IP/port) only where they're used: when dialing, and in PEX. Everywhere else we use URLs. I went with 2, because this significantly simplifies the handling of hostname resolution, and because I really think the PEX reactor should migrate to exchanging URLs instead of IP/port numbers anyway -- this allows operators to use DNS names for validators (and can easily migrate them to new IPs and/or load balance requests), and also allows different protocols (e.g. QUIC and `MemoryTransport`). Happy to discuss this.
227 lines
6.7 KiB
Go
227 lines
6.7 KiB
Go
package pex
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
"github.com/tendermint/tendermint/libs/service"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
protop2p "github.com/tendermint/tendermint/proto/tendermint/p2p"
|
|
)
|
|
|
|
var (
|
|
_ service.Service = (*ReactorV2)(nil)
|
|
_ p2p.Wrapper = (*protop2p.PexMessage)(nil)
|
|
)
|
|
|
|
const (
|
|
maxAddresses uint16 = 100
|
|
resolveTimeout = 3 * time.Second
|
|
)
|
|
|
|
// ReactorV2 is a PEX reactor for the new P2P stack. The legacy reactor
|
|
// is Reactor.
|
|
//
|
|
// FIXME: Rename this when Reactor is removed, and consider moving to p2p/.
|
|
type ReactorV2 struct {
|
|
service.BaseService
|
|
|
|
peerManager *p2p.PeerManager
|
|
pexCh *p2p.Channel
|
|
peerUpdates *p2p.PeerUpdatesCh
|
|
closeCh chan struct{}
|
|
}
|
|
|
|
// NewReactor returns a reference to a new reactor.
|
|
func NewReactorV2(
|
|
logger log.Logger,
|
|
peerManager *p2p.PeerManager,
|
|
pexCh *p2p.Channel,
|
|
peerUpdates *p2p.PeerUpdatesCh,
|
|
) *ReactorV2 {
|
|
r := &ReactorV2{
|
|
peerManager: peerManager,
|
|
pexCh: pexCh,
|
|
peerUpdates: peerUpdates,
|
|
closeCh: make(chan struct{}),
|
|
}
|
|
|
|
r.BaseService = *service.NewBaseService(logger, "PEX", r)
|
|
return r
|
|
}
|
|
|
|
// OnStart starts separate go routines for each p2p Channel and listens for
|
|
// envelopes on each. In addition, it also listens for peer updates and handles
|
|
// messages on that p2p channel accordingly. The caller must be sure to execute
|
|
// OnStop to ensure the outbound p2p Channels are closed.
|
|
func (r *ReactorV2) OnStart() error {
|
|
go r.processPexCh()
|
|
go r.processPeerUpdates()
|
|
return nil
|
|
}
|
|
|
|
// OnStop stops the reactor by signaling to all spawned goroutines to exit and
|
|
// blocking until they all exit.
|
|
func (r *ReactorV2) OnStop() {
|
|
// Close closeCh to signal to all spawned goroutines to gracefully exit. All
|
|
// p2p Channels should execute Close().
|
|
close(r.closeCh)
|
|
|
|
// Wait for all p2p Channels to be closed before returning. This ensures we
|
|
// can easily reason about synchronization of all p2p Channels and ensure no
|
|
// panics will occur.
|
|
<-r.pexCh.Done()
|
|
<-r.peerUpdates.Done()
|
|
}
|
|
|
|
// handlePexMessage handles envelopes sent from peers on the PexChannel.
|
|
func (r *ReactorV2) handlePexMessage(envelope p2p.Envelope) error {
|
|
logger := r.Logger.With("peer", envelope.From)
|
|
|
|
// FIXME: We may want to add DoS protection here, by rate limiting peers and
|
|
// only processing addresses we actually requested.
|
|
switch msg := envelope.Message.(type) {
|
|
case *protop2p.PexRequest:
|
|
pexAddresses := r.resolve(r.peerManager.Advertise(envelope.From, maxAddresses), maxAddresses)
|
|
r.pexCh.Out() <- p2p.Envelope{
|
|
To: envelope.From,
|
|
Message: &protop2p.PexResponse{Addresses: pexAddresses},
|
|
}
|
|
|
|
case *protop2p.PexResponse:
|
|
for _, pexAddress := range msg.Addresses {
|
|
peerAddress, err := p2p.ParsePeerAddress(
|
|
fmt.Sprintf("%s@%s:%d", pexAddress.ID, pexAddress.IP, pexAddress.Port))
|
|
if err != nil {
|
|
logger.Debug("invalid PEX address", "address", pexAddress, "err", err)
|
|
continue
|
|
}
|
|
if err = r.peerManager.Add(peerAddress); err != nil {
|
|
logger.Debug("failed to register PEX address", "address", peerAddress, "err", err)
|
|
}
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("received unknown message: %T", msg)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// resolve resolves a set of peer addresses into PEX addresses.
|
|
//
|
|
// FIXME: This is necessary because the current PEX protocol only supports
|
|
// IP/port pairs, while the P2P stack uses PeerAddress URLs. The PEX protocol
|
|
// should really use URLs too, to exchange DNS names instead of IPs and allow
|
|
// different transport protocols (e.g. QUIC and MemoryTransport).
|
|
//
|
|
// FIXME: We may want to cache and parallelize this, but for now we'll just rely
|
|
// on the operating system to cache it for us.
|
|
func (r *ReactorV2) resolve(addresses []p2p.PeerAddress, limit uint16) []protop2p.PexAddress {
|
|
pexAddresses := make([]protop2p.PexAddress, 0, len(addresses))
|
|
for _, address := range addresses {
|
|
ctx, cancel := context.WithTimeout(context.Background(), resolveTimeout)
|
|
endpoints, err := address.Resolve(ctx)
|
|
cancel()
|
|
if err != nil {
|
|
r.Logger.Debug("failed to resolve address", "address", address, "err", err)
|
|
continue
|
|
}
|
|
for _, endpoint := range endpoints {
|
|
if len(pexAddresses) >= int(limit) {
|
|
return pexAddresses
|
|
|
|
} else if endpoint.IP != nil {
|
|
// PEX currently only supports IP-networked transports (as
|
|
// opposed to e.g. p2p.MemoryTransport).
|
|
pexAddresses = append(pexAddresses, protop2p.PexAddress{
|
|
ID: string(endpoint.PeerID),
|
|
IP: endpoint.IP.String(),
|
|
Port: uint32(endpoint.Port),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return pexAddresses
|
|
}
|
|
|
|
// handleMessage handles an Envelope sent from a peer on a specific p2p Channel.
|
|
// It will handle errors and any possible panics gracefully. A caller can handle
|
|
// any error returned by sending a PeerError on the respective channel.
|
|
func (r *ReactorV2) handleMessage(chID p2p.ChannelID, envelope p2p.Envelope) (err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("panic in processing message: %v", e)
|
|
}
|
|
}()
|
|
|
|
r.Logger.Debug("received message", "peer", envelope.From)
|
|
|
|
switch chID {
|
|
case p2p.ChannelID(PexChannel):
|
|
err = r.handlePexMessage(envelope)
|
|
|
|
default:
|
|
err = fmt.Errorf("unknown channel ID (%d) for envelope (%v)", chID, envelope)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// processPexCh implements a blocking event loop where we listen for p2p
|
|
// Envelope messages from the pexCh.
|
|
func (r *ReactorV2) processPexCh() {
|
|
defer r.pexCh.Close()
|
|
|
|
for {
|
|
select {
|
|
case envelope := <-r.pexCh.In():
|
|
if err := r.handleMessage(r.pexCh.ID(), envelope); err != nil {
|
|
r.Logger.Error("failed to process message", "ch_id", r.pexCh.ID(), "envelope", envelope, "err", err)
|
|
r.pexCh.Error() <- p2p.PeerError{
|
|
PeerID: envelope.From,
|
|
Err: err,
|
|
Severity: p2p.PeerErrorSeverityLow,
|
|
}
|
|
}
|
|
|
|
case <-r.closeCh:
|
|
r.Logger.Debug("stopped listening on PEX channel; closing...")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// processPeerUpdate processes a PeerUpdate. For added peers, PeerStatusUp, we
|
|
// send a request for addresses.
|
|
func (r *ReactorV2) processPeerUpdate(peerUpdate p2p.PeerUpdate) {
|
|
r.Logger.Debug("received peer update", "peer", peerUpdate.PeerID, "status", peerUpdate.Status)
|
|
|
|
if peerUpdate.Status == p2p.PeerStatusUp {
|
|
r.pexCh.Out() <- p2p.Envelope{
|
|
To: peerUpdate.PeerID,
|
|
Message: &protop2p.PexRequest{},
|
|
}
|
|
}
|
|
}
|
|
|
|
// processPeerUpdates initiates a blocking process where we listen for and handle
|
|
// PeerUpdate messages. When the reactor is stopped, we will catch the signal and
|
|
// close the p2p PeerUpdatesCh gracefully.
|
|
func (r *ReactorV2) processPeerUpdates() {
|
|
defer r.peerUpdates.Close()
|
|
|
|
for {
|
|
select {
|
|
case peerUpdate := <-r.peerUpdates.Updates():
|
|
r.processPeerUpdate(peerUpdate)
|
|
|
|
case <-r.closeCh:
|
|
r.Logger.Debug("stopped listening on peer updates channel; closing...")
|
|
return
|
|
}
|
|
}
|
|
}
|