218 lines
5.0 KiB
Go
218 lines
5.0 KiB
Go
|
package autonat
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/libp2p/go-libp2p"
|
||
|
"github.com/libp2p/go-libp2p-core/helpers"
|
||
|
"github.com/libp2p/go-libp2p-core/host"
|
||
|
"github.com/libp2p/go-libp2p-core/network"
|
||
|
"github.com/libp2p/go-libp2p-core/peer"
|
||
|
|
||
|
pb "github.com/libp2p/go-libp2p-autonat/pb"
|
||
|
|
||
|
ggio "github.com/gogo/protobuf/io"
|
||
|
autonat "github.com/libp2p/go-libp2p-autonat"
|
||
|
ma "github.com/multiformats/go-multiaddr"
|
||
|
manet "github.com/multiformats/go-multiaddr-net"
|
||
|
)
|
||
|
|
||
|
const P_CIRCUIT = 290
|
||
|
|
||
|
var (
|
||
|
AutoNATServiceDialTimeout = 15 * time.Second
|
||
|
AutoNATServiceResetInterval = 1 * time.Minute
|
||
|
|
||
|
AutoNATServiceThrottle = 3
|
||
|
)
|
||
|
|
||
|
// AutoNATService provides NAT autodetection services to other peers
|
||
|
type AutoNATService struct {
|
||
|
ctx context.Context
|
||
|
dialer host.Host
|
||
|
|
||
|
// rate limiter
|
||
|
mx sync.Mutex
|
||
|
reqs map[peer.ID]int
|
||
|
}
|
||
|
|
||
|
// NewAutoNATService creates a new AutoNATService instance attached to a host
|
||
|
func NewAutoNATService(ctx context.Context, h host.Host, opts ...libp2p.Option) (*AutoNATService, error) {
|
||
|
opts = append(opts, libp2p.NoListenAddrs)
|
||
|
dialer, err := libp2p.New(ctx, opts...)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
as := &AutoNATService{
|
||
|
ctx: ctx,
|
||
|
dialer: dialer,
|
||
|
reqs: make(map[peer.ID]int),
|
||
|
}
|
||
|
h.SetStreamHandler(autonat.AutoNATProto, as.handleStream)
|
||
|
|
||
|
go as.resetRateLimiter()
|
||
|
|
||
|
return as, nil
|
||
|
}
|
||
|
|
||
|
func (as *AutoNATService) handleStream(s network.Stream) {
|
||
|
defer helpers.FullClose(s)
|
||
|
|
||
|
pid := s.Conn().RemotePeer()
|
||
|
log.Debugf("New stream from %s", pid.Pretty())
|
||
|
|
||
|
r := ggio.NewDelimitedReader(s, network.MessageSizeMax)
|
||
|
w := ggio.NewDelimitedWriter(s)
|
||
|
|
||
|
var req pb.Message
|
||
|
var res pb.Message
|
||
|
|
||
|
err := r.ReadMsg(&req)
|
||
|
if err != nil {
|
||
|
log.Debugf("Error reading message from %s: %s", pid.Pretty(), err.Error())
|
||
|
s.Reset()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
t := req.GetType()
|
||
|
if t != pb.Message_DIAL {
|
||
|
log.Debugf("Unexpected message from %s: %s (%d)", pid.Pretty(), t.String(), t)
|
||
|
s.Reset()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
dr := as.handleDial(pid, s.Conn().RemoteMultiaddr(), req.GetDial().GetPeer())
|
||
|
res.Type = pb.Message_DIAL_RESPONSE.Enum()
|
||
|
res.DialResponse = dr
|
||
|
|
||
|
err = w.WriteMsg(&res)
|
||
|
if err != nil {
|
||
|
log.Debugf("Error writing response to %s: %s", pid.Pretty(), err.Error())
|
||
|
s.Reset()
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (as *AutoNATService) handleDial(p peer.ID, obsaddr ma.Multiaddr, mpi *pb.Message_PeerInfo) *pb.Message_DialResponse {
|
||
|
if mpi == nil {
|
||
|
return newDialResponseError(pb.Message_E_BAD_REQUEST, "missing peer info")
|
||
|
}
|
||
|
|
||
|
mpid := mpi.GetId()
|
||
|
if mpid != nil {
|
||
|
mp, err := peer.IDFromBytes(mpid)
|
||
|
if err != nil {
|
||
|
return newDialResponseError(pb.Message_E_BAD_REQUEST, "bad peer id")
|
||
|
}
|
||
|
|
||
|
if mp != p {
|
||
|
return newDialResponseError(pb.Message_E_BAD_REQUEST, "peer id mismatch")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
addrs := make([]ma.Multiaddr, 0)
|
||
|
seen := make(map[string]struct{})
|
||
|
|
||
|
// add observed addr to the list of addresses to dial
|
||
|
if !as.skipDial(obsaddr) {
|
||
|
addrs = append(addrs, obsaddr)
|
||
|
seen[obsaddr.String()] = struct{}{}
|
||
|
}
|
||
|
|
||
|
for _, maddr := range mpi.GetAddrs() {
|
||
|
addr, err := ma.NewMultiaddrBytes(maddr)
|
||
|
if err != nil {
|
||
|
log.Debugf("Error parsing multiaddr: %s", err.Error())
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if as.skipDial(addr) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
str := addr.String()
|
||
|
_, ok := seen[str]
|
||
|
if ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
addrs = append(addrs, addr)
|
||
|
seen[str] = struct{}{}
|
||
|
}
|
||
|
|
||
|
if len(addrs) == 0 {
|
||
|
return newDialResponseError(pb.Message_E_DIAL_ERROR, "no dialable addresses")
|
||
|
}
|
||
|
|
||
|
return as.doDial(peer.AddrInfo{ID: p, Addrs: addrs})
|
||
|
}
|
||
|
|
||
|
func (as *AutoNATService) skipDial(addr ma.Multiaddr) bool {
|
||
|
// skip relay addresses
|
||
|
_, err := addr.ValueForProtocol(P_CIRCUIT)
|
||
|
if err == nil {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// skip private network (unroutable) addresses
|
||
|
if !manet.IsPublicAddr(addr) {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (as *AutoNATService) doDial(pi peer.AddrInfo) *pb.Message_DialResponse {
|
||
|
// rate limit check
|
||
|
as.mx.Lock()
|
||
|
count := as.reqs[pi.ID]
|
||
|
if count >= AutoNATServiceThrottle {
|
||
|
as.mx.Unlock()
|
||
|
return newDialResponseError(pb.Message_E_DIAL_REFUSED, "too many dials")
|
||
|
}
|
||
|
as.reqs[pi.ID] = count + 1
|
||
|
as.mx.Unlock()
|
||
|
|
||
|
ctx, cancel := context.WithTimeout(as.ctx, AutoNATServiceDialTimeout)
|
||
|
defer cancel()
|
||
|
|
||
|
err := as.dialer.Connect(ctx, pi)
|
||
|
if err != nil {
|
||
|
log.Debugf("error dialing %s: %s", pi.ID.Pretty(), err.Error())
|
||
|
// wait for the context to timeout to avoid leaking timing information
|
||
|
// this renders the service ineffective as a port scanner
|
||
|
<-ctx.Done()
|
||
|
return newDialResponseError(pb.Message_E_DIAL_ERROR, "dial failed")
|
||
|
}
|
||
|
|
||
|
conns := as.dialer.Network().ConnsToPeer(pi.ID)
|
||
|
if len(conns) == 0 {
|
||
|
log.Errorf("supposedly connected to %s, but no connection to peer", pi.ID.Pretty())
|
||
|
return newDialResponseError(pb.Message_E_INTERNAL_ERROR, "internal service error")
|
||
|
}
|
||
|
|
||
|
ra := conns[0].RemoteMultiaddr()
|
||
|
as.dialer.Network().ClosePeer(pi.ID)
|
||
|
return newDialResponseOK(ra)
|
||
|
}
|
||
|
|
||
|
func (as *AutoNATService) resetRateLimiter() {
|
||
|
ticker := time.NewTicker(AutoNATServiceResetInterval)
|
||
|
defer ticker.Stop()
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-ticker.C:
|
||
|
as.mx.Lock()
|
||
|
as.reqs = make(map[peer.ID]int)
|
||
|
as.mx.Unlock()
|
||
|
|
||
|
case <-as.ctx.Done():
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|