diff --git a/go.mod b/go.mod index 5afd406..ea394f4 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/AppsFlyer/go-sundheit v0.2.0 github.com/allegro/bigcache v1.2.1 // indirect github.com/aristanetworks/goarista v0.0.0-20200520141224-0f14e646773f // indirect - github.com/ava-labs/coreth v0.2.5 // Added manually; don't delete + github.com/ava-labs/coreth v0.2.5 // indirect; Added manually; don't delete github.com/ava-labs/go-ethereum v1.9.3 // indirect github.com/deckarep/golang-set v1.7.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1 v1.0.3 + github.com/decred/dcrd/dcrec/secp256k1 v1.0.3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200526030155-0c6c7ca85d3b github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/elastic/gosigar v0.10.5 // indirect @@ -29,12 +29,11 @@ require ( github.com/karalabe/usb v0.0.0-20191104083709-911d15fe12a9 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/mitchellh/go-homedir v1.1.0 - github.com/mr-tron/base58 v1.1.3 + github.com/mr-tron/base58 v1.2.0 github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d github.com/olekukonko/tablewriter v0.0.4 // indirect github.com/pborman/uuid v1.2.0 // indirect github.com/prometheus/client_golang v1.6.0 - github.com/prometheus/common v0.9.1 github.com/prometheus/tsdb v0.10.0 // indirect github.com/rjeczalik/notify v0.9.2 // indirect github.com/rs/cors v1.7.0 diff --git a/go.sum b/go.sum index fb197cd..d165208 100644 --- a/go.sum +++ b/go.sum @@ -43,7 +43,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= github.com/decred/dcrd v1.3.0 h1:EEXm7BdiROfazDtuFsOu9mfotnyy00bgCuVwUqaszFo= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= github.com/decred/dcrd/dcrec/secp256k1 v1.0.3 h1:u4XpHqlscRolxPxt2YHrFBDVZYY1AK+KMV02H1r+HmU= @@ -69,6 +71,7 @@ github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= @@ -82,6 +85,7 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -103,6 +107,7 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= @@ -136,6 +141,7 @@ github.com/jackpal/gateway v1.0.6/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -149,8 +155,10 @@ github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= @@ -171,6 +179,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= @@ -321,6 +331,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -351,6 +362,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/main/main.go b/main/main.go index 43fe1b9..71de46f 100644 --- a/main/main.go +++ b/main/main.go @@ -40,10 +40,6 @@ func main() { defer log.StopOnPanic() defer Config.DB.Close() - if Config.StakingIP.IsZero() { - log.Warn("NAT traversal has failed. It will be able to connect to less nodes.") - } - // Track if sybil control is enforced if !Config.EnableStaking && Config.EnableP2PTLS { log.Warn("Staking is disabled. Sybil control is not enforced.") @@ -68,12 +64,16 @@ func main() { log.Debug("assertions are enabled. This may slow down execution") } - mapper := nat.NewDefaultMapper(log, Config.Nat, nat.TCP, "gecko") + mapper := nat.NewPortMapper(log, Config.Nat) defer mapper.UnmapAllPorts() - mapper.MapPort(Config.StakingIP.Port, Config.StakingIP.Port) // Open staking port - if Config.HTTPHost != "127.0.0.1" && Config.HTTPHost != "localhost" { // Open HTTP port iff HTTP server not listening on localhost - mapper.MapPort(Config.HTTPPort, Config.HTTPPort) + Config.StakingIP.Port = mapper.Map("TCP", Config.StakingLocalPort, "gecko-staking") // Open staking port + if Config.HTTPHost != "127.0.0.1" && Config.HTTPHost != "localhost" { // Open HTTP port iff HTTP server not listening on localhost + mapper.Map("TCP", Config.HTTPPort, "gecko-http") + } + + if Config.StakingIP.IsZero() { + log.Warn("NAT traversal has failed. The node will be able to connect to less nodes.") } node := node.Node{} diff --git a/main/params.go b/main/params.go index 53e7b01..03b8459 100644 --- a/main/params.go +++ b/main/params.go @@ -284,12 +284,12 @@ func init() { Config.DB = memdb.New() } - Config.Nat = nat.NewRouter() + Config.Nat = nat.GetRouter() var ip net.IP // If public IP is not specified, get it using shell command dig if *consensusIP == "" { - ip, err = Config.Nat.IP() + ip, err = Config.Nat.ExternalIP() if err != nil { ip = net.IPv4zero // Couldn't get my IP...set to 0.0.0.0 } @@ -306,6 +306,7 @@ func init() { IP: ip, Port: uint16(*consensusPort), } + Config.StakingLocalPort = uint16(*consensusPort) defaultBootstrapIPs, defaultBootstrapIDs := GetDefaultBootstraps(networkID, 5) diff --git a/nat/mapper.go b/nat/mapper.go deleted file mode 100644 index 3beaedd..0000000 --- a/nat/mapper.go +++ /dev/null @@ -1,143 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package nat - -import ( - "sync" - "time" - - "github.com/ava-labs/gecko/utils/logging" - "github.com/ava-labs/gecko/utils/wrappers" -) - -const ( - defaultMappingTimeout = 30 * time.Minute - defaultMappingUpdateInterval = 3 * defaultMappingTimeout / 4 -) - -// Mapper maps port -type Mapper interface { - MapPort(newInternalPort, newExternalPort uint16) error - UnmapAllPorts() error -} - -type mapper struct { - log logging.Logger - router Router - networkProtocol NetworkProtocol - mappingNames string - mappingTimeout time.Duration - mappingUpdateInterval time.Duration - - closer chan struct{} - wg sync.WaitGroup - errLock sync.Mutex - errs wrappers.Errs -} - -// NewMapper returns a new mapper that can map ports on a router -func NewMapper( - log logging.Logger, - router Router, - networkProtocol NetworkProtocol, - mappingNames string, - mappingTimeout time.Duration, - mappingUpdateInterval time.Duration, -) Mapper { - return &mapper{ - log: log, - router: router, - networkProtocol: networkProtocol, - mappingNames: mappingNames, - mappingTimeout: mappingTimeout, - mappingUpdateInterval: mappingUpdateInterval, - closer: make(chan struct{}), - } -} - -// NewDefaultMapper returns a new mapper that can map ports on a router with -// default settings -func NewDefaultMapper( - log logging.Logger, - router Router, - networkProtocol NetworkProtocol, - mappingNames string, -) Mapper { - return NewMapper( - log, - router, - networkProtocol, - mappingNames, - defaultMappingTimeout, // uses the default value - defaultMappingUpdateInterval, // uses the default value - ) -} - -// MapPort maps a local port to a port on the router until UnmapAllPorts is -// called. -func (m *mapper) MapPort(newInternalPort, newExternalPort uint16) error { - m.wg.Add(1) - go m.mapPort(newInternalPort, newExternalPort) - return nil -} - -func (m *mapper) mapPort(newInternalPort, newExternalPort uint16) { - // duration is set to 0 here so that the select case will execute - // immediately - updateTimer := time.NewTimer(0) - defer func() { - updateTimer.Stop() - - m.errLock.Lock() - m.errs.Add(m.router.UnmapPort( - m.networkProtocol, - newInternalPort, - newExternalPort)) - m.errLock.Unlock() - - m.log.Debug("Unmapped external port %d to internal port %d", - newExternalPort, - newInternalPort) - - m.wg.Done() - }() - - for { - select { - case <-updateTimer.C: - err := m.router.MapPort( - m.networkProtocol, - newInternalPort, - newExternalPort, - m.mappingNames, - m.mappingTimeout) - - if err != nil { - m.errLock.Lock() - m.errs.Add(err) - m.errLock.Unlock() - - m.log.Debug("Failed to add mapping from external port %d to internal port %d due to %s", - newExternalPort, - newInternalPort, - err) - } else { - m.log.Debug("Mapped external port %d to internal port %d", - newExternalPort, - newInternalPort) - } - - // remap the port in m.mappingUpdateInterval - updateTimer.Reset(m.mappingUpdateInterval) - case _, _ = <-m.closer: - return // only return when all ports are unmapped - } - } -} - -func (m *mapper) UnmapAllPorts() error { - close(m.closer) - m.wg.Wait() - return m.errs.Err -} diff --git a/nat/nat.go b/nat/nat.go new file mode 100644 index 0000000..8c351c7 --- /dev/null +++ b/nat/nat.go @@ -0,0 +1,134 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package nat + +import ( + "net" + "sync" + "time" + + "github.com/ava-labs/gecko/utils/logging" +) + +const ( + mapTimeout = 30 * time.Second + mapUpdateTimeout = mapTimeout / 2 + maxRetries = 20 +) + +// Router describes the functionality that a network device must support to be +// able to open ports to an external IP. +type Router interface { + MapPort(protocol string, intPort, extPort uint16, desc string, duration time.Duration) error + UnmapPort(protocol string, intPort, extPort uint16) error + ExternalIP() (net.IP, error) + GetPortMappingEntry(extPort uint16, protocol string) ( + InternalIP string, + InternalPort uint16, + Description string, + err error, + ) +} + +// GetRouter returns a router on the current network. +func GetRouter() Router { + if r := getUPnPRouter(); r != nil { + return r + } + if r := getPMPRouter(); r != nil { + return r + } + + return NewNoRouter() +} + +// Mapper attempts to open a set of ports on a router +type Mapper struct { + log logging.Logger + r Router + closer chan struct{} + wg sync.WaitGroup +} + +// NewPortMapper returns an initialized mapper +func NewPortMapper(log logging.Logger, r Router) Mapper { + return Mapper{ + log: log, + r: r, + closer: make(chan struct{}), + } +} + +// Map sets up port mapping using given protocol, internal and external ports +// and returns the final port mapped. It returns 0 if mapping failed after the +// maximun number of retries +func (dev *Mapper) Map(protocol string, intPort uint16, desc string) uint16 { + mappedPort := make(chan uint16) + + go dev.keepPortMapping(mappedPort, protocol, intPort, desc) + + return <-mappedPort +} + +// keepPortMapping runs in the background to keep a port mapped. It renews the +// the port mapping in mapUpdateTimeout. +func (dev *Mapper) keepPortMapping(mappedPort chan<- uint16, protocol string, + intPort uint16, desc string) { + updateTimer := time.NewTimer(mapUpdateTimeout) + + for i := 0; i <= maxRetries; i++ { + extPort := intPort + uint16(i) + if intaddr, intPort, desc, err := dev.r.GetPortMappingEntry(extPort, protocol); err == nil { + dev.log.Debug("Port %d is taken by %s:%d: %s, retry with the next port", + extPort, intaddr, intPort, desc) + } else if err := dev.r.MapPort(protocol, intPort, extPort, desc, mapTimeout); err != nil { + dev.log.Debug("Map port failed. Protocol %s Internal %d External %d. %s", + protocol, intPort, extPort, err) + } else { + dev.log.Info("Mapped Protocol %s Internal %d External %d.", protocol, + intPort, extPort) + + dev.wg.Add(1) + + mappedPort <- extPort + + defer func(extPort uint16) { + updateTimer.Stop() + + dev.log.Debug("Unmap protocol %s external port %d", protocol, extPort) + dev.r.UnmapPort(protocol, intPort, extPort) + + dev.wg.Done() + }(extPort) + + for { + select { + case <-updateTimer.C: + if err := dev.r.MapPort(protocol, intPort, extPort, desc, mapTimeout); err != nil { + dev.log.Error("Renewing port mapping from external port %d to internal port %d failed with %s", + intPort, extPort, err) + } else { + dev.log.Info("Renewed port mapping from external port %d to internal port %d.", + intPort, extPort) + } + + updateTimer.Reset(mapUpdateTimeout) + case _, _ = <-dev.closer: + return + } + } + } + } + + dev.log.Debug("Unable to map port %d~%d", intPort, intPort+maxRetries) + mappedPort <- 0 +} + +// UnmapAllPorts stops mapping all ports from this mapper and attempts to unmap +// them. +func (dev *Mapper) UnmapAllPorts() { + close(dev.closer) + dev.wg.Wait() + dev.log.Info("Unmapped all ports") +} diff --git a/nat/no_router.go b/nat/no_router.go index edb86b6..0971b46 100644 --- a/nat/no_router.go +++ b/nat/no_router.go @@ -4,25 +4,57 @@ package nat import ( - "errors" + "fmt" "net" "time" ) -var ( - errNoRouter = errors.New("no nat enabled router was discovered") -) +const googleDNSServer = "8.8.8.8:80" -type noRouter struct{} - -func (noRouter) MapPort(_ NetworkProtocol, _, _ uint16, _ string, _ time.Duration) error { - return errNoRouter +type noRouter struct { + ip net.IP } -func (noRouter) UnmapPort(_ NetworkProtocol, _, _ uint16) error { - return errNoRouter +func (noRouter) MapPort(_ string, intPort, extPort uint16, _ string, _ time.Duration) error { + if intPort != extPort { + return fmt.Errorf("cannot map port %d to %d", intPort, extPort) + } + return nil } -func (noRouter) IP() (net.IP, error) { - return nil, errNoRouter +func (noRouter) UnmapPort(string, uint16, uint16) error { + return nil +} + +func (r noRouter) ExternalIP() (net.IP, error) { + return r.ip, nil +} + +func (noRouter) GetPortMappingEntry(uint16, string) (string, uint16, string, error) { + return "", 0, "", nil +} + +func getOutboundIP() (net.IP, error) { + conn, err := net.Dial("udp", googleDNSServer) + if err != nil { + return nil, err + } + + if udpAddr, ok := conn.LocalAddr().(*net.UDPAddr); ok { + return udpAddr.IP, conn.Close() + } + + conn.Close() + return nil, fmt.Errorf("getting outbound IP failed") +} + +// NewNoRouter returns a router that assumes the network is public +func NewNoRouter() Router { + ip, err := getOutboundIP() + if err != nil { + return nil + } + return &noRouter{ + ip: ip, + } } diff --git a/nat/pmp.go b/nat/pmp.go index 311375d..ce40362 100644 --- a/nat/pmp.go +++ b/nat/pmp.go @@ -4,11 +4,13 @@ package nat import ( + "fmt" "net" "time" "github.com/jackpal/gateway" - "github.com/jackpal/go-nat-pmp" + + natpmp "github.com/jackpal/go-nat-pmp" ) var ( @@ -17,12 +19,12 @@ var ( // natPMPClient adapts the NAT-PMP protocol implementation so it conforms to // the common interface. -type pmpClient struct { +type pmpRouter struct { client *natpmp.Client } -func (pmp *pmpClient) MapPort( - networkProtocol NetworkProtocol, +func (pmp *pmpRouter) MapPort( + networkProtocol string, newInternalPort uint16, newExternalPort uint16, mappingName string, @@ -37,8 +39,8 @@ func (pmp *pmpClient) MapPort( return err } -func (pmp *pmpClient) UnmapPort( - networkProtocol NetworkProtocol, +func (pmp *pmpRouter) UnmapPort( + networkProtocol string, internalPort uint16, _ uint16) error { protocol := string(networkProtocol) @@ -48,7 +50,7 @@ func (pmp *pmpClient) UnmapPort( return err } -func (pmp *pmpClient) IP() (net.IP, error) { +func (pmp *pmpRouter) ExternalIP() (net.IP, error) { response, err := pmp.client.GetExternalAddress() if err != nil { return nil, err @@ -56,14 +58,20 @@ func (pmp *pmpClient) IP() (net.IP, error) { return response.ExternalIPAddress[:], nil } -func getPMPRouter() Router { +// go-nat-pmp does not support port mapping entry query +func (pmp *pmpRouter) GetPortMappingEntry(externalPort uint16, protocol string) ( + string, uint16, string, error) { + return "", 0, "", fmt.Errorf("port mapping entry not found") +} + +func getPMPRouter() *pmpRouter { gatewayIP, err := gateway.DiscoverGateway() if err != nil { return nil } - pmp := &pmpClient{natpmp.NewClientWithTimeout(gatewayIP, pmpClientTimeout)} - if _, err := pmp.IP(); err != nil { + pmp := &pmpRouter{natpmp.NewClientWithTimeout(gatewayIP, pmpClientTimeout)} + if _, err := pmp.ExternalIP(); err != nil { return nil } diff --git a/nat/router.go b/nat/router.go deleted file mode 100644 index 11b58f9..0000000 --- a/nat/router.go +++ /dev/null @@ -1,65 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package nat performs network address translation and provides helpers for -// routing ports. -package nat - -import ( - "net" - "time" -) - -// NetworkProtocol is a protocol that will be used through a port -type NetworkProtocol string - -// Available protocol -const ( - TCP NetworkProtocol = "TCP" - UDP NetworkProtocol = "UDP" -) - -// Router provides a standard NAT router functions. Specifically, allowing the -// fetching of public IPs and port forwarding to this computer. -type Router interface { - // mapPort creates a mapping between a port on the local computer to an - // external port on the router. - // - // The mappingName is something displayed on the router, so it is included - // for completeness. - MapPort( - networkProtocol NetworkProtocol, - newInternalPort uint16, - newExternalPort uint16, - mappingName string, - mappingDuration time.Duration) error - - // UnmapPort clears a mapping that was previous made by a call to MapPort - UnmapPort( - networkProtocol NetworkProtocol, - internalPort uint16, - externalPort uint16) error - - // Returns the routers IP address on the network the router considers - // external - IP() (net.IP, error) -} - -// NewRouter returns a new router discovered on the local network -func NewRouter() Router { - routers := make(chan Router) - // Because getting a router can take a noticeable amount of time to error, - // we run these requests in parallel - go func() { - routers <- getUPnPRouter() - }() - go func() { - routers <- getPMPRouter() - }() - for i := 0; i < 2; i++ { - if router := <-routers; router != nil { - return router - } - } - return noRouter{} -} diff --git a/nat/upnp.go b/nat/upnp.go index e60cd6e..3191bc8 100644 --- a/nat/upnp.go +++ b/nat/upnp.go @@ -4,7 +4,6 @@ package nat import ( - "errors" "fmt" "net" "time" @@ -15,11 +14,7 @@ import ( ) const ( - soapTimeout = time.Second -) - -var ( - errNoGateway = errors.New("Failed to connect to any avaliable gateways") + soapRequestTimeout = 3 * time.Second ) // upnpClient is the interface used by goupnp for their client implementations @@ -47,69 +42,30 @@ type upnpClient interface { // returns if there is rsip available, nat enabled, or an unexpected error. GetNATRSIPStatus() (newRSIPAvailable bool, natEnabled bool, err error) -} -type upnpRouter struct { - root *goupnp.RootDevice - client upnpClient -} - -func (n *upnpRouter) MapPort( - networkProtocol NetworkProtocol, - newInternalPort uint16, - newExternalPort uint16, - mappingName string, - mappingDuration time.Duration, -) error { - ip, err := n.localAddress() - if err != nil { - return err - } - - protocol := string(networkProtocol) - // goupnp uses seconds to denote their lifetime - lifetime := uint32(mappingDuration / time.Second) - - // UnmapPort's error is intentionally dropped, because the mapping may not - // exist. - n.UnmapPort(networkProtocol, newInternalPort, newExternalPort) - - return n.client.AddPortMapping( - "", // newRemoteHost isn't used to limit the mapping to a host - newExternalPort, - protocol, - newInternalPort, - ip.String(), // newInternalClient is the client traffic should be sent to - true, // newEnabled enables port mappings - mappingName, - lifetime, + // attempts to get port mapping information give a external port and protocol + GetSpecificPortMappingEntry( + NewRemoteHost string, + NewExternalPort uint16, + NewProtocol string, + ) ( + NewInternalPort uint16, + NewInternalClient string, + NewEnabled bool, + NewPortMappingDescription string, + NewLeaseDuration uint32, + err error, ) } -func (n *upnpRouter) UnmapPort(networkProtocol NetworkProtocol, _, externalPort uint16) error { - protocol := string(networkProtocol) - return n.client.DeletePortMapping( - "", // newRemoteHost isn't used to limit the mapping to a host - externalPort, - protocol) +type upnpRouter struct { + dev *goupnp.RootDevice + client upnpClient } -func (n *upnpRouter) IP() (net.IP, error) { - ipStr, err := n.client.GetExternalIPAddress() - if err != nil { - return nil, err - } - - ip := net.ParseIP(ipStr) - if ip == nil { - return nil, fmt.Errorf("invalid IP %s", ipStr) - } - return ip, nil -} - -func (n *upnpRouter) localAddress() (net.IP, error) { +func (r *upnpRouter) localIP() (net.IP, error) { // attempt to get an address on the router - deviceAddr, err := net.ResolveUDPAddr("udp4", n.root.URLBase.Host) + deviceAddr, err := net.ResolveUDPAddr("udp", r.dev.URLBase.Host) if err != nil { return nil, err } @@ -120,7 +76,7 @@ func (n *upnpRouter) localAddress() (net.IP, error) { return nil, err } - // attempt to find one of my ips that the router would know about + // attempt to find one of my IPs that matches router's record for _, netInterface := range netInterfaces { addrs, err := netInterface.Addrs() if err != nil { @@ -128,9 +84,6 @@ func (n *upnpRouter) localAddress() (net.IP, error) { } for _, addr := range addrs { - // this is pretty janky, but it seems to be the best way to get the - // ip mask and properly check if the ip references the device we are - // connected to ipNet, ok := addr.(*net.IPNet) if !ok { continue @@ -144,110 +97,119 @@ func (n *upnpRouter) localAddress() (net.IP, error) { return nil, fmt.Errorf("couldn't find the local address in the same network as %s", deviceIP) } -// getUPnPRouter searches for all Gateway Devices that have avaliable -// connections in the goupnp library and returns the first connection it can -// find. -func getUPnPRouter() Router { - routers := make(chan *upnpRouter) - // Because DiscoverDevices takes a noticeable amount of time to error, we - // run these requests in parallel - go func() { - routers <- connectToGateway(internetgateway1.URN_WANConnectionDevice_1, gateway1) - }() - go func() { - routers <- connectToGateway(internetgateway2.URN_WANConnectionDevice_2, gateway2) - }() - for i := 0; i < 2; i++ { - if router := <-routers; router != nil { - return router - } +func (r *upnpRouter) ExternalIP() (net.IP, error) { + str, err := r.client.GetExternalIPAddress() + if err != nil { + return nil, err } - return nil + + ip := net.ParseIP(str) + if ip == nil { + return nil, fmt.Errorf("invalid IP %s", str) + } + return ip, nil } -func gateway1(client goupnp.ServiceClient) upnpClient { +func (r *upnpRouter) MapPort(protocol string, intPort, extPort uint16, + desc string, duration time.Duration) error { + ip, err := r.localIP() + if err != nil { + return nil + } + lifetime := uint32(duration / time.Second) + + return r.client.AddPortMapping("", extPort, protocol, intPort, + ip.String(), true, desc, lifetime) +} + +func (r *upnpRouter) UnmapPort(protocol string, _, extPort uint16) error { + return r.client.DeletePortMapping("", extPort, protocol) +} + +func (r *upnpRouter) GetPortMappingEntry(extPort uint16, protocol string) (string, uint16, string, error) { + intPort, intAddr, _, desc, _, err := r.client.GetSpecificPortMappingEntry("", extPort, protocol) + return intAddr, intPort, desc, err +} + +// create UPnP SOAP service client with URN +func getUPnPClient(client goupnp.ServiceClient) upnpClient { switch client.Service.ServiceType { case internetgateway1.URN_WANIPConnection_1: return &internetgateway1.WANIPConnection1{ServiceClient: client} case internetgateway1.URN_WANPPPConnection_1: return &internetgateway1.WANPPPConnection1{ServiceClient: client} - default: - return nil - } -} - -func gateway2(client goupnp.ServiceClient) upnpClient { - switch client.Service.ServiceType { - case internetgateway2.URN_WANIPConnection_1: - return &internetgateway2.WANIPConnection1{ServiceClient: client} case internetgateway2.URN_WANIPConnection_2: return &internetgateway2.WANIPConnection2{ServiceClient: client} - case internetgateway2.URN_WANPPPConnection_1: - return &internetgateway2.WANPPPConnection1{ServiceClient: client} default: return nil } } -func connectToGateway(deviceType string, toClient func(goupnp.ServiceClient) upnpClient) *upnpRouter { - devs, err := goupnp.DiscoverDevices(deviceType) +// discover() tries to find gateway device +func discover(target string) *upnpRouter { + devs, err := goupnp.DiscoverDevices(target) if err != nil { return nil } - // we are iterating over all the network devices, acting a possible roots - for i := range devs { - dev := &devs[i] - if dev.Root == nil { + + router := make(chan *upnpRouter) + for i := 0; i < len(devs); i++ { + if devs[i].Root == nil { continue } + go func(dev *goupnp.MaybeRootDevice) { + var r *upnpRouter = nil + dev.Root.Device.VisitServices(func(service *goupnp.Service) { + c := goupnp.ServiceClient{ + SOAPClient: service.NewSOAPClient(), + RootDevice: dev.Root, + Location: dev.Location, + Service: service, + } + c.SOAPClient.HTTPClient.Timeout = soapRequestTimeout + client := getUPnPClient(c) + if client == nil { + return + } + if _, nat, err := client.GetNATRSIPStatus(); err != nil || !nat { + return + } + r = &upnpRouter{dev.Root, client} + }) + router <- r + }(&devs[i]) + } - // the root device may be a router, so attempt to connect to that - rootDevice := &dev.Root.Device - if upnp := getRouter(dev, rootDevice, toClient); upnp != nil { - return upnp - } - - // attempt to connect to any sub devices - devices := rootDevice.Devices - for i := range devices { - if upnp := getRouter(dev, &devices[i], toClient); upnp != nil { - return upnp - } + for i := 0; i < len(devs); i++ { + if r := <-router; r != nil { + return r } } + return nil } -func getRouter(rootDevice *goupnp.MaybeRootDevice, device *goupnp.Device, toClient func(goupnp.ServiceClient) upnpClient) *upnpRouter { - for i := range device.Services { - service := &device.Services[i] +// getUPnPRouter searches for internet gateway using both Device Control Protocol +// and returns the first one it can find. It returns nil if no UPnP gateway is found +func getUPnPRouter() *upnpRouter { + targets := []string{ + internetgateway1.URN_WANConnectionDevice_1, + internetgateway2.URN_WANConnectionDevice_2, + } - soapClient := service.NewSOAPClient() - // make sure the client times out if needed - soapClient.HTTPClient.Timeout = soapTimeout + routers := make(chan *upnpRouter) - // attempt to create a client connection - serviceClient := goupnp.ServiceClient{ - SOAPClient: soapClient, - RootDevice: rootDevice.Root, - Location: rootDevice.Location, - Service: service, - } - client := toClient(serviceClient) - if client == nil { - continue - } + for _, urn := range targets { + go func(urn string) { + routers <- discover(urn) + }(urn) + } - // check whether port mapping is enabled - if _, nat, err := client.GetNATRSIPStatus(); err != nil || !nat { - continue - } - - // we found a router! - return &upnpRouter{ - root: rootDevice.Root, - client: client, + for i := 0; i < len(targets); i++ { + if r := <-routers; r != nil { + return r } } + return nil } diff --git a/node/config.go b/node/config.go index b8754e9..e578fc4 100644 --- a/node/config.go +++ b/node/config.go @@ -33,11 +33,12 @@ type Config struct { DB database.Database // Staking configuration - StakingIP utils.IPDesc - EnableP2PTLS bool - EnableStaking bool - StakingKeyFile string - StakingCertFile string + StakingIP utils.IPDesc + StakingLocalPort uint16 + EnableP2PTLS bool + EnableStaking bool + StakingKeyFile string + StakingCertFile string // Bootstrapping configuration BootstrapPeers []*Peer diff --git a/node/node.go b/node/node.go index dbc58a8..bfeccc8 100644 --- a/node/node.go +++ b/node/node.go @@ -113,7 +113,7 @@ type Node struct { */ func (n *Node) initNetworking() error { - listener, err := net.Listen(TCP, n.Config.StakingIP.PortString()) + listener, err := net.Listen(TCP, fmt.Sprintf(":%d", n.Config.StakingLocalPort)) if err != nil { return err }