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 }