mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-27 17:15:18 +00:00
263 lines
6.9 KiB
Go
263 lines
6.9 KiB
Go
package nat
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"sync"
|
|
"time"
|
|
|
|
logging "github.com/ipfs/go-log/v2"
|
|
|
|
"github.com/libp2p/go-nat"
|
|
)
|
|
|
|
// ErrNoMapping signals no mapping exists for an address
|
|
var ErrNoMapping = errors.New("mapping not established")
|
|
|
|
var log = logging.Logger("nat")
|
|
|
|
// MappingDuration is a default port mapping duration.
|
|
// Port mappings are renewed every (MappingDuration / 3)
|
|
const MappingDuration = time.Minute
|
|
|
|
// CacheTime is the time a mapping will cache an external address for
|
|
const CacheTime = 15 * time.Second
|
|
|
|
type entry struct {
|
|
protocol string
|
|
port int
|
|
}
|
|
|
|
// so we can mock it in tests
|
|
var discoverGateway = nat.DiscoverGateway
|
|
|
|
// DiscoverNAT looks for a NAT device in the network and returns an object that can manage port mappings.
|
|
func DiscoverNAT(ctx context.Context) (*NAT, error) {
|
|
natInstance, err := discoverGateway(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var extAddr netip.Addr
|
|
extIP, err := natInstance.GetExternalAddress()
|
|
if err == nil {
|
|
extAddr, _ = netip.AddrFromSlice(extIP)
|
|
}
|
|
|
|
// Log the device addr.
|
|
addr, err := natInstance.GetDeviceAddress()
|
|
if err != nil {
|
|
log.Debug("DiscoverGateway address error:", err)
|
|
} else {
|
|
log.Debug("DiscoverGateway address:", addr)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
nat := &NAT{
|
|
nat: natInstance,
|
|
extAddr: extAddr,
|
|
mappings: make(map[entry]int),
|
|
ctx: ctx,
|
|
ctxCancel: cancel,
|
|
}
|
|
nat.refCount.Add(1)
|
|
go func() {
|
|
nat.background()
|
|
nat.refCount.Done()
|
|
}()
|
|
return nat, nil
|
|
}
|
|
|
|
// NAT is an object that manages address port mappings in
|
|
// NATs (Network Address Translators). It is a long-running
|
|
// service that will periodically renew port mappings,
|
|
// and keep an up-to-date list of all the external addresses.
|
|
type NAT struct {
|
|
natmu sync.Mutex
|
|
nat nat.NAT
|
|
// External IP of the NAT. Will be renewed periodically (every CacheTime).
|
|
extAddr netip.Addr
|
|
|
|
refCount sync.WaitGroup
|
|
ctx context.Context
|
|
ctxCancel context.CancelFunc
|
|
|
|
mappingmu sync.RWMutex // guards mappings
|
|
closed bool
|
|
mappings map[entry]int
|
|
}
|
|
|
|
// Close shuts down all port mappings. NAT can no longer be used.
|
|
func (nat *NAT) Close() error {
|
|
nat.mappingmu.Lock()
|
|
nat.closed = true
|
|
nat.mappingmu.Unlock()
|
|
|
|
nat.ctxCancel()
|
|
nat.refCount.Wait()
|
|
return nil
|
|
}
|
|
|
|
func (nat *NAT) GetMapping(protocol string, port int) (addr netip.AddrPort, found bool) {
|
|
nat.mappingmu.Lock()
|
|
|
|
if !nat.extAddr.IsValid() {
|
|
nat.mappingmu.Unlock()
|
|
return netip.AddrPort{}, false
|
|
}
|
|
extPort, found := nat.mappings[entry{protocol: protocol, port: port}]
|
|
if !found {
|
|
nat.mappingmu.Unlock()
|
|
return netip.AddrPort{}, false
|
|
}
|
|
nat.mappingmu.Unlock()
|
|
return netip.AddrPortFrom(nat.extAddr, uint16(extPort)), true
|
|
}
|
|
|
|
// AddMapping attempts to construct a mapping on protocol and internal port.
|
|
// It blocks until a mapping was established. Once added, it periodically renews the mapping.
|
|
//
|
|
// May not succeed, and mappings may change over time;
|
|
// NAT devices may not respect our port requests, and even lie.
|
|
func (nat *NAT) AddMapping(ctx context.Context, protocol string, port int) error {
|
|
switch protocol {
|
|
case "tcp", "udp":
|
|
default:
|
|
return fmt.Errorf("invalid protocol: %s", protocol)
|
|
}
|
|
|
|
nat.mappingmu.Lock()
|
|
|
|
if nat.closed {
|
|
nat.mappingmu.Unlock()
|
|
return errors.New("closed")
|
|
}
|
|
|
|
// do it once synchronously, so first mapping is done right away, and before exiting,
|
|
// allowing users -- in the optimistic case -- to use results right after.
|
|
extPort := nat.establishMapping(ctx, protocol, port)
|
|
nat.mappings[entry{protocol: protocol, port: port}] = extPort
|
|
nat.mappingmu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// RemoveMapping removes a port mapping.
|
|
// It blocks until the NAT has removed the mapping.
|
|
func (nat *NAT) RemoveMapping(ctx context.Context, protocol string, port int) error {
|
|
nat.mappingmu.Lock()
|
|
|
|
switch protocol {
|
|
case "tcp", "udp":
|
|
e := entry{protocol: protocol, port: port}
|
|
if _, ok := nat.mappings[e]; ok {
|
|
delete(nat.mappings, e)
|
|
nat.mappingmu.Unlock()
|
|
return nat.nat.DeletePortMapping(ctx, protocol, port)
|
|
}
|
|
nat.mappingmu.Unlock()
|
|
return errors.New("unknown mapping")
|
|
default:
|
|
nat.mappingmu.Unlock()
|
|
return fmt.Errorf("invalid protocol: %s", protocol)
|
|
}
|
|
}
|
|
|
|
func (nat *NAT) background() {
|
|
const mappingUpdate = MappingDuration / 3
|
|
|
|
now := time.Now()
|
|
nextMappingUpdate := now.Add(mappingUpdate)
|
|
nextAddrUpdate := now.Add(CacheTime)
|
|
|
|
t := time.NewTimer(minTime(nextMappingUpdate, nextAddrUpdate).Sub(now)) // don't use a ticker here. We don't know how long establishing the mappings takes.
|
|
|
|
var in []entry
|
|
var out []int // port numbers
|
|
for {
|
|
select {
|
|
case now := <-t.C:
|
|
if now.After(nextMappingUpdate) {
|
|
in = in[:0]
|
|
out = out[:0]
|
|
nat.mappingmu.Lock()
|
|
for e := range nat.mappings {
|
|
in = append(in, e)
|
|
}
|
|
nat.mappingmu.Unlock()
|
|
// Establishing the mapping involves network requests.
|
|
// Don't hold the mutex, just save the ports.
|
|
for _, e := range in {
|
|
out = append(out, nat.establishMapping(nat.ctx, e.protocol, e.port))
|
|
}
|
|
nat.mappingmu.Lock()
|
|
for i, p := range in {
|
|
if _, ok := nat.mappings[p]; !ok {
|
|
continue // entry might have been deleted
|
|
}
|
|
nat.mappings[p] = out[i]
|
|
}
|
|
nat.mappingmu.Unlock()
|
|
nextMappingUpdate = time.Now().Add(mappingUpdate)
|
|
}
|
|
if now.After(nextAddrUpdate) {
|
|
var extAddr netip.Addr
|
|
extIP, err := nat.nat.GetExternalAddress()
|
|
if err == nil {
|
|
extAddr, _ = netip.AddrFromSlice(extIP)
|
|
}
|
|
nat.extAddr = extAddr
|
|
nextAddrUpdate = time.Now().Add(CacheTime)
|
|
}
|
|
t.Reset(time.Until(minTime(nextAddrUpdate, nextMappingUpdate)))
|
|
case <-nat.ctx.Done():
|
|
nat.mappingmu.Lock()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
for e := range nat.mappings {
|
|
delete(nat.mappings, e)
|
|
nat.nat.DeletePortMapping(ctx, e.protocol, e.port)
|
|
}
|
|
nat.mappingmu.Unlock()
|
|
t.Stop()
|
|
cancel()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (nat *NAT) establishMapping(ctx context.Context, protocol string, internalPort int) (externalPort int) {
|
|
log.Debugf("Attempting port map: %s/%d", protocol, internalPort)
|
|
const comment = "libp2p"
|
|
|
|
nat.natmu.Lock()
|
|
var err error
|
|
externalPort, err = nat.nat.AddPortMapping(ctx, protocol, internalPort, comment, MappingDuration)
|
|
if err != nil {
|
|
// Some hardware does not support mappings with timeout, so try that
|
|
externalPort, err = nat.nat.AddPortMapping(ctx, protocol, internalPort, comment, 0)
|
|
}
|
|
nat.natmu.Unlock()
|
|
|
|
if err != nil || externalPort == 0 {
|
|
// TODO: log.Event
|
|
if err != nil {
|
|
log.Warnf("failed to establish port mapping: %s", err)
|
|
} else {
|
|
log.Warnf("failed to establish port mapping: newport = 0")
|
|
}
|
|
// we do not close if the mapping failed,
|
|
// because it may work again next time.
|
|
return 0
|
|
}
|
|
|
|
log.Debugf("NAT Mapping: %d --> %d (%s)", externalPort, internalPort, protocol)
|
|
return externalPort
|
|
}
|
|
|
|
func minTime(a, b time.Time) time.Time {
|
|
if a.Before(b) {
|
|
return a
|
|
}
|
|
return b
|
|
}
|