github.com/MetalBlockchain/metalgo@v1.11.9/nat/nat.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package nat
     5  
     6  import (
     7  	"net/netip"
     8  	"sync"
     9  	"time"
    10  
    11  	"go.uber.org/zap"
    12  
    13  	"github.com/MetalBlockchain/metalgo/utils"
    14  	"github.com/MetalBlockchain/metalgo/utils/logging"
    15  )
    16  
    17  const (
    18  	mapTimeout        = 30 * time.Minute
    19  	maxRefreshRetries = 3
    20  )
    21  
    22  // Router describes the functionality that a network device must support to be
    23  // able to open ports to an external IP.
    24  type Router interface {
    25  	// True iff this router supports NAT
    26  	SupportsNAT() bool
    27  	// Map external port [extPort] to internal port [intPort] for [duration]
    28  	MapPort(intPort, extPort uint16, desc string, duration time.Duration) error
    29  	// Undo a port mapping
    30  	UnmapPort(intPort, extPort uint16) error
    31  	// Return our external IP
    32  	ExternalIP() (netip.Addr, error)
    33  }
    34  
    35  // GetRouter returns a router on the current network.
    36  func GetRouter() Router {
    37  	if r := getUPnPRouter(); r != nil {
    38  		return r
    39  	}
    40  	if r := getPMPRouter(); r != nil {
    41  		return r
    42  	}
    43  
    44  	return NewNoRouter()
    45  }
    46  
    47  // Mapper attempts to open a set of ports on a router
    48  type Mapper struct {
    49  	log    logging.Logger
    50  	r      Router
    51  	closer chan struct{}
    52  	wg     sync.WaitGroup
    53  }
    54  
    55  // NewPortMapper returns an initialized mapper
    56  func NewPortMapper(log logging.Logger, r Router) *Mapper {
    57  	return &Mapper{
    58  		log:    log,
    59  		r:      r,
    60  		closer: make(chan struct{}),
    61  	}
    62  }
    63  
    64  // Map external port [extPort] (exposed to the internet) to internal port [intPort] (where our process is listening)
    65  // and set [ip]. Does this every [updateTime]. [ip] may be nil.
    66  func (m *Mapper) Map(
    67  	intPort uint16,
    68  	extPort uint16,
    69  	desc string,
    70  	ip *utils.Atomic[netip.AddrPort],
    71  	updateTime time.Duration,
    72  ) {
    73  	if !m.r.SupportsNAT() {
    74  		return
    75  	}
    76  
    77  	// we attempt a port map, and log an Error if it fails.
    78  	err := m.retryMapPort(intPort, extPort, desc, mapTimeout)
    79  	if err != nil {
    80  		m.log.Error("NAT traversal failed",
    81  			zap.Uint16("externalPort", extPort),
    82  			zap.Uint16("internalPort", intPort),
    83  			zap.Error(err),
    84  		)
    85  	} else {
    86  		m.log.Info("NAT traversal successful",
    87  			zap.Uint16("externalPort", extPort),
    88  			zap.Uint16("internalPort", intPort),
    89  		)
    90  	}
    91  
    92  	m.wg.Add(1)
    93  	go m.keepPortMapping(intPort, extPort, desc, ip, updateTime)
    94  }
    95  
    96  // Retry port map up to maxRefreshRetries with a 1 second delay
    97  func (m *Mapper) retryMapPort(intPort, extPort uint16, desc string, timeout time.Duration) error {
    98  	var err error
    99  	for retryCnt := 0; retryCnt < maxRefreshRetries; retryCnt++ {
   100  		err = m.r.MapPort(intPort, extPort, desc, timeout)
   101  		if err == nil {
   102  			return nil
   103  		}
   104  
   105  		// log a message, sleep a second and retry.
   106  		m.log.Warn("renewing port mapping failed",
   107  			zap.Int("attempt", retryCnt+1),
   108  			zap.Uint16("externalPort", extPort),
   109  			zap.Uint16("internalPort", intPort),
   110  			zap.Error(err),
   111  		)
   112  		time.Sleep(1 * time.Second)
   113  	}
   114  	return err
   115  }
   116  
   117  // keepPortMapping runs in the background to keep a port mapped. It renews the mapping from [extPort]
   118  // to [intPort]] every [updateTime]. Updates [ip] every [updateTime].
   119  func (m *Mapper) keepPortMapping(
   120  	intPort uint16,
   121  	extPort uint16,
   122  	desc string,
   123  	ip *utils.Atomic[netip.AddrPort],
   124  	updateTime time.Duration,
   125  ) {
   126  	updateTimer := time.NewTimer(updateTime)
   127  
   128  	defer func(extPort uint16) {
   129  		updateTimer.Stop()
   130  
   131  		m.log.Debug("unmapping port",
   132  			zap.Uint16("externalPort", extPort),
   133  		)
   134  
   135  		if err := m.r.UnmapPort(intPort, extPort); err != nil {
   136  			m.log.Debug("error unmapping port",
   137  				zap.Uint16("externalPort", extPort),
   138  				zap.Uint16("internalPort", intPort),
   139  				zap.Error(err),
   140  			)
   141  		}
   142  
   143  		m.wg.Done()
   144  	}(extPort)
   145  
   146  	for {
   147  		select {
   148  		case <-updateTimer.C:
   149  			err := m.retryMapPort(intPort, extPort, desc, mapTimeout)
   150  			if err != nil {
   151  				m.log.Warn("renew NAT traversal failed",
   152  					zap.Uint16("externalPort", extPort),
   153  					zap.Uint16("internalPort", intPort),
   154  					zap.Error(err),
   155  				)
   156  			}
   157  			m.updateIP(ip)
   158  			updateTimer.Reset(updateTime)
   159  		case <-m.closer:
   160  			return
   161  		}
   162  	}
   163  }
   164  
   165  func (m *Mapper) updateIP(ip *utils.Atomic[netip.AddrPort]) {
   166  	if ip == nil {
   167  		return
   168  	}
   169  	newAddr, err := m.r.ExternalIP()
   170  	if err != nil {
   171  		m.log.Error("failed to get external IP",
   172  			zap.Error(err),
   173  		)
   174  		return
   175  	}
   176  	oldAddrPort := ip.Get()
   177  	oldAddr := oldAddrPort.Addr()
   178  	if newAddr != oldAddr {
   179  		port := oldAddrPort.Port()
   180  		ip.Set(netip.AddrPortFrom(newAddr, port))
   181  		m.log.Info("external IP updated",
   182  			zap.Stringer("oldIP", oldAddr),
   183  			zap.Stringer("newIP", newAddr),
   184  		)
   185  	}
   186  }
   187  
   188  // UnmapAllPorts stops mapping all ports from this mapper and attempts to unmap
   189  // them.
   190  func (m *Mapper) UnmapAllPorts() {
   191  	close(m.closer)
   192  	m.wg.Wait()
   193  	m.log.Info("Unmapped all ports")
   194  }