diff --git a/go.mod b/go.mod index 157942b1c8..2ab8784891 100644 --- a/go.mod +++ b/go.mod @@ -22,12 +22,12 @@ require ( github.com/libp2p/go-libp2p-asn-util v0.1.0 github.com/libp2p/go-libp2p-circuit v0.6.0 github.com/libp2p/go-libp2p-core v0.15.1 - github.com/libp2p/go-libp2p-nat v0.1.0 github.com/libp2p/go-libp2p-peerstore v0.6.0 github.com/libp2p/go-libp2p-resource-manager v0.2.1 github.com/libp2p/go-libp2p-testing v0.9.2 github.com/libp2p/go-mplex v0.7.0 github.com/libp2p/go-msgio v0.2.0 + github.com/libp2p/go-nat v0.1.0 github.com/libp2p/go-netroute v0.2.0 github.com/libp2p/go-reuseport v0.1.0 github.com/libp2p/go-reuseport-transport v0.1.0 @@ -84,7 +84,6 @@ require ( github.com/libp2p/go-libp2p-tls v0.4.1 // indirect github.com/libp2p/go-libp2p-transport-upgrader v0.7.1 // indirect github.com/libp2p/go-libp2p-yamux v0.9.1 // indirect - github.com/libp2p/go-nat v0.1.0 // indirect github.com/libp2p/go-openssl v0.0.7 // indirect github.com/libp2p/go-tcp-transport v0.5.1 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect diff --git a/go.sum b/go.sum index ff34722db0..aba0d03037 100644 --- a/go.sum +++ b/go.sum @@ -344,7 +344,6 @@ github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9 github.com/ipfs/go-log v1.0.4/go.mod h1:oDCg2FkjogeFOhqqb+N39l2RpTNPL6F/StPkB3kPgcs= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= -github.com/ipfs/go-log/v2 v2.0.3/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0= github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= github.com/ipfs/go-log/v2 v2.1.1/go.mod h1:2v2nsGfZsvvAJz13SyFzf9ObaqwHiHxsPLEHntrv9KM= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= @@ -442,8 +441,6 @@ github.com/libp2p/go-libp2p-core v0.15.1 h1:0RY+Mi/ARK9DgG1g9xVQLb8dDaaU8tCePMtG github.com/libp2p/go-libp2p-core v0.15.1/go.mod h1:agSaboYM4hzB1cWekgVReqV5M4g5M+2eNNejV+1EEhs= github.com/libp2p/go-libp2p-mplex v0.4.1/go.mod h1:cmy+3GfqfM1PceHTLL7zQzAAYaryDu6iPSC+CIb094g= github.com/libp2p/go-libp2p-mplex v0.5.0/go.mod h1:eLImPJLkj3iG5t5lq68w3Vm5NAQ5BcKwrrb2VmOYb3M= -github.com/libp2p/go-libp2p-nat v0.1.0 h1:vigUi2MEN+fwghe5ijpScxtbbDz+L/6y8XwlzYOJgSY= -github.com/libp2p/go-libp2p-nat v0.1.0/go.mod h1:DQzAG+QbDYjN1/C3B6vXucLtz3u9rEonLVPtZVzQqks= github.com/libp2p/go-libp2p-peerstore v0.4.0/go.mod h1:rDJUFyzEWPpXpEwywkcTYYzDHlwza8riYMaUzaN6hX0= github.com/libp2p/go-libp2p-peerstore v0.6.0 h1:HJminhQSGISBIRb93N6WK3t6Fa8OOTnHd/VBjL4mY5A= github.com/libp2p/go-libp2p-peerstore v0.6.0/go.mod h1:DGEmKdXrcYpK9Jha3sS7MhqYdInxJy84bIPtSu65bKc= @@ -852,7 +849,6 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go index 5a5f039a58..9073639ba1 100644 --- a/p2p/host/basic/basic_host.go +++ b/p2p/host/basic/basic_host.go @@ -12,6 +12,7 @@ import ( "github.com/libp2p/go-libp2p/p2p/host/autonat" "github.com/libp2p/go-libp2p/p2p/host/pstoremanager" "github.com/libp2p/go-libp2p/p2p/host/relaysvc" + inat "github.com/libp2p/go-libp2p/p2p/net/nat" relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay" "github.com/libp2p/go-libp2p/p2p/protocol/holepunch" "github.com/libp2p/go-libp2p/p2p/protocol/identify" @@ -28,7 +29,6 @@ import ( "github.com/libp2p/go-libp2p-core/record" "github.com/libp2p/go-eventbus" - inat "github.com/libp2p/go-libp2p-nat" "github.com/libp2p/go-netroute" logging "github.com/ipfs/go-log/v2" diff --git a/p2p/host/basic/natmgr.go b/p2p/host/basic/natmgr.go index 8b41bfa820..af1de0ec67 100644 --- a/p2p/host/basic/natmgr.go +++ b/p2p/host/basic/natmgr.go @@ -8,8 +8,10 @@ import ( "sync" "time" + inat "github.com/libp2p/go-libp2p/p2p/net/nat" + "github.com/libp2p/go-libp2p-core/network" - inat "github.com/libp2p/go-libp2p-nat" + ma "github.com/multiformats/go-multiaddr" ) diff --git a/p2p/net/nat/mapping.go b/p2p/net/nat/mapping.go new file mode 100644 index 0000000000..f9b508e4e2 --- /dev/null +++ b/p2p/net/nat/mapping.go @@ -0,0 +1,119 @@ +package nat + +import ( + "fmt" + "net" + "sync" + "time" +) + +// Mapping represents a port mapping in a NAT. +type Mapping interface { + // NAT returns the NAT object this Mapping belongs to. + NAT() *NAT + + // Protocol returns the protocol of this port mapping. This is either + // "tcp" or "udp" as no other protocols are likely to be NAT-supported. + Protocol() string + + // InternalPort returns the internal device port. Mapping will continue to + // try to map InternalPort() to an external facing port. + InternalPort() int + + // ExternalPort returns the external facing port. If the mapping is not + // established, port will be 0 + ExternalPort() int + + // ExternalAddr returns the external facing address. If the mapping is not + // established, addr will be nil, and and ErrNoMapping will be returned. + ExternalAddr() (addr net.Addr, err error) + + // Close closes the port mapping + Close() error +} + +// keeps republishing +type mapping struct { + sync.Mutex // guards all fields + + nat *NAT + proto string + intport int + extport int + + cached net.IP + cacheTime time.Time + cacheLk sync.Mutex +} + +func (m *mapping) NAT() *NAT { + m.Lock() + defer m.Unlock() + return m.nat +} + +func (m *mapping) Protocol() string { + m.Lock() + defer m.Unlock() + return m.proto +} + +func (m *mapping) InternalPort() int { + m.Lock() + defer m.Unlock() + return m.intport +} + +func (m *mapping) ExternalPort() int { + m.Lock() + defer m.Unlock() + return m.extport +} + +func (m *mapping) setExternalPort(p int) { + m.Lock() + defer m.Unlock() + m.extport = p +} + +func (m *mapping) ExternalAddr() (net.Addr, error) { + m.cacheLk.Lock() + defer m.cacheLk.Unlock() + oport := m.ExternalPort() + if oport == 0 { + // dont even try right now. + return nil, ErrNoMapping + } + + if time.Since(m.cacheTime) >= CacheTime { + m.nat.natmu.Lock() + cval, err := m.nat.nat.GetExternalAddress() + m.nat.natmu.Unlock() + + if err != nil { + return nil, err + } + + m.cached = cval + m.cacheTime = time.Now() + } + switch m.Protocol() { + case "tcp": + return &net.TCPAddr{ + IP: m.cached, + Port: oport, + }, nil + case "udp": + return &net.UDPAddr{ + IP: m.cached, + Port: oport, + }, nil + default: + panic(fmt.Sprintf("invalid protocol %q", m.Protocol())) + } +} + +func (m *mapping) Close() error { + m.nat.removeMapping(m) + return nil +} diff --git a/p2p/net/nat/nat.go b/p2p/net/nat/nat.go new file mode 100644 index 0000000000..e2656f8bcc --- /dev/null +++ b/p2p/net/nat/nat.go @@ -0,0 +1,193 @@ +package nat + +import ( + "context" + "errors" + "fmt" + "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.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.Debug("DiscoverGateway address error:", err) + } else { + log.Debug("DiscoverGateway address:", 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.mappingmu.Lock() + nat.closed = true + nat.mappingmu.Unlock() + + nat.ctxCancel() + nat.refCount.Wait() + return nil +} + +// Mappings returns a slice of all NAT mappings +func (nat *NAT) Mappings() []Mapping { + nat.mappingmu.Lock() + maps2 := make([]Mapping, 0, len(nat.mappings)) + for m := range nat.mappings { + maps2 = append(maps2, m) + } + nat.mappingmu.Unlock() + return maps2 +} + +// 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": + default: + return nil, fmt.Errorf("invalid protocol: %s", protocol) + } + + m := &mapping{ + intport: port, + nat: nat, + proto: protocol, + } + + nat.mappingmu.Lock() + if nat.closed { + nat.mappingmu.Unlock() + return nil, errors.New("closed") + } + nat.mappings[m] = struct{}{} + nat.refCount.Add(1) + nat.mappingmu.Unlock() + 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. + nat.establishMapping(m) + return m, nil +} + +func (nat *NAT) removeMapping(m *mapping) { + nat.mappingmu.Lock() + delete(nat.mappings, m) + nat.mappingmu.Unlock() + nat.natmu.Lock() + nat.nat.DeletePortMapping(m.Protocol(), m.InternalPort()) + nat.natmu.Unlock() +} + +func (nat *NAT) refreshMappings(m *mapping) { + defer nat.refCount.Done() + t := time.NewTicker(MappingDuration / 3) + defer t.Stop() + + for { + select { + case <-t.C: + nat.establishMapping(m) + case <-nat.ctx.Done(): + m.Close() + return + } + } +} + +func (nat *NAT) establishMapping(m *mapping) { + oldport := m.ExternalPort() + + log.Debugf("Attempting port map: %s/%d", m.Protocol(), m.InternalPort()) + const comment = "libp2p" + + nat.natmu.Lock() + 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) + } + nat.natmu.Unlock() + + if err != nil || newport == 0 { + m.setExternalPort(0) // clear mapping + // 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 + } + + m.setExternalPort(newport) + log.Debugf("NAT Mapping: %d --> %d (%s)", m.ExternalPort(), m.InternalPort(), m.Protocol()) + if oldport != 0 && newport != oldport { + log.Debugf("failed to renew same port mapping: ch %d -> %d", oldport, newport) + } +}