diff --git a/go.mod b/go.mod index 4636c8c..c6f4e79 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ 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 diff --git a/go.sum b/go.sum index d79e9a8..2f989e1 100644 --- a/go.sum +++ b/go.sum @@ -165,6 +165,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= diff --git a/main/main.go b/main/main.go index 98cb581..b49d320 100644 --- a/main/main.go +++ b/main/main.go @@ -68,11 +68,11 @@ func main() { log.Debug("assertions are enabled. This may slow down execution") } - mapper := nat.NewDefaultMapper(log, Config.Nat, nat.TCP, "gecko") - defer mapper.UnmapAllPorts() + router := nat.NewRouter(log, Config.Nat) + defer router.UnmapAllPorts() - mapper.MapPort(Config.StakingIP.Port, Config.StakingIP.Port) - mapper.MapPort(Config.HTTPPort, Config.HTTPPort) + router.Map("TCP", Config.StakingIP.Port, Config.StakingIP.Port, "gecko") + router.Map("TCP", Config.HTTPPort, Config.HTTPPort, "gecko http") node := node.Node{} diff --git a/main/params.go b/main/params.go index 6dcad06..86eca34 100644 --- a/main/params.go +++ b/main/params.go @@ -281,12 +281,12 @@ func init() { Config.DB = memdb.New() } - Config.Nat = nat.NewRouter() + Config.Nat = nat.GetNATRouter() 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 } 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..1c3c2fa --- /dev/null +++ b/nat/nat.go @@ -0,0 +1,94 @@ +package nat + +import ( + "net" + "sync" + "time" + + "github.com/ava-labs/gecko/utils/logging" + "github.com/ava-labs/gecko/utils/wrappers" +) + +const ( + mapTimeout = 30 * time.Minute + mapUpdate = mapTimeout / 2 +) + +type NATRouter interface { + MapPort(protocol string, intport, extport uint16, desc string, duration time.Duration) error + UnmapPort(protocol string, extport uint16) error + ExternalIP() (net.IP, error) +} + +func GetNATRouter() NATRouter { + //TODO other protocol + return getUPnPRouter() +} + +type Router struct { + log logging.Logger + r NATRouter + closer chan struct{} + wg sync.WaitGroup + errLock sync.Mutex + errs wrappers.Errs +} + +func NewRouter(log logging.Logger, r NATRouter) Router { + return Router{ + log: log, + r: r, + closer: make(chan struct{}), + } +} + +func (dev *Router) Map(protocol string, intport, extport uint16, desc string) { + dev.wg.Add(1) + go dev.mapPort(protocol, intport, extport, desc) +} + +func (dev *Router) mapPort(protocol string, intport, extport uint16, desc string) { + updater := time.NewTimer(mapUpdate) + defer func() { + updater.Stop() + + dev.log.Info("Unmap protocol %s external port %d", protocol, extport) + dev.errLock.Lock() + dev.errs.Add(dev.r.UnmapPort(protocol, extport)) + dev.errLock.Unlock() + + dev.wg.Done() + }() + + if err := dev.r.MapPort(protocol, intport, extport, desc, mapTimeout); err != nil { + dev.log.Warn("Map port failed. Protocol %s Internal %d External %d. %s", + protocol, intport, extport, err) + dev.errLock.Lock() + dev.errs.Add(err) + dev.errLock.Unlock() + } else { + dev.log.Info("Mapped Protocol %s Internal %d External %d. %s", protocol, + intport, extport) + } + + for { + select { + case <-updater.C: + if err := dev.r.MapPort(protocol, intport, extport, desc, mapTimeout); err != nil { + dev.log.Warn("Renew port mapping failed. Protocol %s Internal %d External %d. %s", + protocol, intport, extport, err) + } else { + dev.log.Info("Renew port mapping Protocol %s Internal %d External %d. %s", protocol, + intport, extport) + } + + updater.Reset(mapUpdate) + } + } +} + +func (dev *Router) UnmapAllPorts() error { + close(dev.closer) + dev.wg.Wait() + return dev.errs.Err +} diff --git a/nat/no_router.go b/nat/no_router.go deleted file mode 100644 index edb86b6..0000000 --- a/nat/no_router.go +++ /dev/null @@ -1,28 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package nat - -import ( - "errors" - "net" - "time" -) - -var ( - errNoRouter = errors.New("no nat enabled router was discovered") -) - -type noRouter struct{} - -func (noRouter) MapPort(_ NetworkProtocol, _, _ uint16, _ string, _ time.Duration) error { - return errNoRouter -} - -func (noRouter) UnmapPort(_ NetworkProtocol, _, _ uint16) error { - return errNoRouter -} - -func (noRouter) IP() (net.IP, error) { - return nil, errNoRouter -} diff --git a/nat/pmp.go b/nat/pmp.go deleted file mode 100644 index 311375d..0000000 --- a/nat/pmp.go +++ /dev/null @@ -1,71 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package nat - -import ( - "net" - "time" - - "github.com/jackpal/gateway" - "github.com/jackpal/go-nat-pmp" -) - -var ( - pmpClientTimeout = 500 * time.Millisecond -) - -// natPMPClient adapts the NAT-PMP protocol implementation so it conforms to -// the common interface. -type pmpClient struct { - client *natpmp.Client -} - -func (pmp *pmpClient) MapPort( - networkProtocol NetworkProtocol, - newInternalPort uint16, - newExternalPort uint16, - mappingName string, - mappingDuration time.Duration) error { - protocol := string(networkProtocol) - internalPort := int(newInternalPort) - externalPort := int(newExternalPort) - // go-nat-pmp uses seconds to denote their lifetime - lifetime := int(mappingDuration / time.Second) - - _, err := pmp.client.AddPortMapping(protocol, internalPort, externalPort, lifetime) - return err -} - -func (pmp *pmpClient) UnmapPort( - networkProtocol NetworkProtocol, - internalPort uint16, - _ uint16) error { - protocol := string(networkProtocol) - internalPortInt := int(internalPort) - - _, err := pmp.client.AddPortMapping(protocol, internalPortInt, 0, 0) - return err -} - -func (pmp *pmpClient) IP() (net.IP, error) { - response, err := pmp.client.GetExternalAddress() - if err != nil { - return nil, err - } - return response.ExternalIPAddress[:], nil -} - -func getPMPRouter() Router { - gatewayIP, err := gateway.DiscoverGateway() - if err != nil { - return nil - } - - pmp := &pmpClient{natpmp.NewClientWithTimeout(gatewayIP, pmpClientTimeout)} - if _, err := pmp.IP(); err != nil { - return nil - } - - return pmp -} 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..7f62760 100644 --- a/nat/upnp.go +++ b/nat/upnp.go @@ -1,10 +1,6 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - package nat import ( - "errors" "fmt" "net" "time" @@ -14,13 +10,7 @@ import ( "github.com/huin/goupnp/dcps/internetgateway2" ) -const ( - soapTimeout = time.Second -) - -var ( - errNoGateway = errors.New("Failed to connect to any avaliable gateways") -) +const soapRequestTimeout = 3 * time.Second // upnpClient is the interface used by goupnp for their client implementations type upnpClient interface { @@ -50,66 +40,13 @@ type upnpClient interface { } type upnpRouter struct { - root *goupnp.RootDevice + dev *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, - ) -} - -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) -} - -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("udp4", r.dev.URLBase.Host) if err != nil { return nil, err } @@ -142,27 +79,34 @@ 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 (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) + r.UnmapPort(protocol, extport) + 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 gateway1(client goupnp.ServiceClient) upnpClient { @@ -175,7 +119,6 @@ func gateway1(client goupnp.ServiceClient) upnpClient { return nil } } - func gateway2(client goupnp.ServiceClient) upnpClient { switch client.Service.ServiceType { case internetgateway2.URN_WANIPConnection_1: @@ -189,65 +132,69 @@ func gateway2(client goupnp.ServiceClient) upnpClient { } } -func connectToGateway(deviceType string, toClient func(goupnp.ServiceClient) upnpClient) *upnpRouter { - devs, err := goupnp.DiscoverDevices(deviceType) +func getUpnpClient(client goupnp.ServiceClient) upnpClient { + c := gateway1(client) + if c != nil { + return c + } + return gateway2(client) +} + +func getRootDevice(dev *goupnp.MaybeRootDevice) *upnpRouter { + var router *upnpRouter + 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 + } + router = &upnpRouter{dev.Root, client} + if router == nil { + return + } + if _, nat, err := router.client.GetNATRSIPStatus(); err != nil || !nat { + router = nil + return + } + }) + return router +} + +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 { + for i := 0; i < len(devs); i++ { + if devs[i].Root == nil { continue } - - // 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 - } + u := getRootDevice(&devs[i]) + if u != nil { + return u } } 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] - - soapClient := service.NewSOAPClient() - // make sure the client times out if needed - soapClient.HTTPClient.Timeout = soapTimeout - - // 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 - } - - // 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, - } +func getUPnPRouter() *upnpRouter { + r := discover(internetgateway1.URN_WANConnectionDevice_1) + if r != nil { + return r } - return nil + return discover(internetgateway2.URN_WANConnectionDevice_2) +} + +func GetUPnP() *upnpRouter { + r := discover(internetgateway1.URN_WANConnectionDevice_1) + if r != nil { + return r + } + return discover(internetgateway2.URN_WANConnectionDevice_2) } diff --git a/node/config.go b/node/config.go index 2504276..aaadb7a 100644 --- a/node/config.go +++ b/node/config.go @@ -15,7 +15,7 @@ import ( // Config contains all of the configurations of an Ava node. type Config struct { // protocol to use for opening the network interface - Nat nat.Router + Nat nat.NATRouter // ID of the network this node should connect to NetworkID uint32