You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
4.7 KiB

package nat
import (
// ErrNoMapping signals no mapping exists for an address
var ErrNoMapping = errors.New("mapping not established")
// MappingDuration is a default port mapping duration.
// Port mappings are renewed every (MappingDuration / 3)
const MappingDuration = time.Second * 60
// CacheTime is the time a mapping will cache an external address for
const CacheTime = time.Second * 15
// 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 := nat.DiscoverGateway(ctx)
if err != nil {
return nil, err
// Log the device addr.
addr, err := natInstance.GetDeviceAddress()
if err != nil {
log.Printf("DiscoverGateway address error: %+v\n", err)
} else {
log.Printf("DiscoverGateway address: %s\n", addr)
return newNAT(natInstance), 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
refCount sync.WaitGroup
ctx context.Context
ctxCancel context.CancelFunc
mappingmu sync.RWMutex // guards mappings
closed bool
mappings map[*mapping]struct{}
func newNAT(realNAT nat.NAT) *NAT {
ctx, cancel := context.WithCancel(context.Background())
return &NAT{
nat: realNAT,
mappings: make(map[*mapping]struct{}),
ctx: ctx,
ctxCancel: cancel,
// Close shuts down all port mappings. NAT can no longer be used.
func (nat *NAT) Close() error {
nat.closed = true
return nil
// expose the underlying GetExternalAddress which should return the public internet address of the router
func (nat *NAT) GetExternalAddress() (net.IP, error) {
return nat.nat.GetExternalAddress()
// NewMapping attempts to construct a mapping on protocol and internal port
// It will also periodically renew the mapping until the returned Mapping
// -- or its parent NAT -- is Closed.
// May not succeed, and mappings may change over time;
// NAT devices may not respect our port requests, and even lie.
// Clients should not store the mapped results, but rather always
// poll our object for the latest mappings.
func (nat *NAT) NewMapping(protocol string, port int) (Mapping, error) {
if nat == nil {
return nil, fmt.Errorf("no nat available")
switch protocol {
case "tcp", "udp":
return nil, fmt.Errorf("invalid protocol: %s", protocol)
m := &mapping{
intport: port,
nat: nat,
proto: protocol,
if nat.closed {
return nil, errors.New("closed")
nat.mappings[m] = struct{}{}
go nat.refreshMappings(m)
// 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.
return m, nil
func (nat *NAT) removeMapping(m *mapping) {
delete(nat.mappings, m)
nat.nat.DeletePortMapping(m.Protocol(), m.InternalPort())
func (nat *NAT) refreshMappings(m *mapping) {
defer nat.refCount.Done()
t := time.NewTicker(MappingDuration / 3)
defer t.Stop()
for {
select {
case <-t.C:
case <-nat.ctx.Done():
func (nat *NAT) establishMapping(m *mapping) {
oldport := m.ExternalPort()
//log.Debugf("Attempting port map: %s/%d", m.Protocol(), m.InternalPort())
const comment = "libp2p"
newport, err := nat.nat.AddPortMapping(m.Protocol(), m.InternalPort(), comment, MappingDuration)
if err != nil {
// Some hardware does not support mappings with timeout, so try that
newport, err = nat.nat.AddPortMapping(m.Protocol(), m.InternalPort(), comment, 0)
if err != nil || newport == 0 {
m.setExternalPort(0) // clear mapping
// TODO: log.Event
if err != nil {
log.Printf("failed to establish port mapping: %s\n", err)
} else {
log.Printf("failed to establish port mapping: newport = 0")
// we do not close if the mapping failed,
// because it may work again next time.
//log.Debugf("NAT Mapping: %d --> %d (%s)", m.ExternalPort(), m.InternalPort(), m.Protocol())
if oldport != 0 && newport != oldport {
log.Printf("failed to renew same port mapping: ch %d -> %d", oldport, newport)