mirror of https://github.com/poanetwork/gecko.git
254 lines
6.5 KiB
Go
254 lines
6.5 KiB
Go
// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package nat
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/huin/goupnp"
|
|
"github.com/huin/goupnp/dcps/internetgateway1"
|
|
"github.com/huin/goupnp/dcps/internetgateway2"
|
|
)
|
|
|
|
const (
|
|
soapTimeout = time.Second
|
|
)
|
|
|
|
var (
|
|
errNoGateway = errors.New("Failed to connect to any avaliable gateways")
|
|
)
|
|
|
|
// upnpClient is the interface used by goupnp for their client implementations
|
|
type upnpClient interface {
|
|
// attempts to map connection using the provided protocol from the external
|
|
// port to the internal port for the lease duration.
|
|
AddPortMapping(
|
|
newRemoteHost string,
|
|
newExternalPort uint16,
|
|
newProtocol string,
|
|
newInternalPort uint16,
|
|
newInternalClient string,
|
|
newEnabled bool,
|
|
newPortMappingDescription string,
|
|
newLeaseDuration uint32) error
|
|
|
|
// attempt to remove any mapping from the external port.
|
|
DeletePortMapping(
|
|
newRemoteHost string,
|
|
newExternalPort uint16,
|
|
newProtocol string) error
|
|
|
|
// attempts to return the external IP address, formatted as a string.
|
|
GetExternalIPAddress() (ip string, err error)
|
|
|
|
// 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,
|
|
)
|
|
}
|
|
|
|
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) {
|
|
// attempt to get an address on the router
|
|
deviceAddr, err := net.ResolveUDPAddr("udp4", n.root.URLBase.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
deviceIP := deviceAddr.IP
|
|
|
|
netInterfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// attempt to find one of my ips that the router would know about
|
|
for _, netInterface := range netInterfaces {
|
|
addrs, err := netInterface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if ipNet.Contains(deviceIP) {
|
|
return ipNet.IP, nil
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func gateway1(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)
|
|
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 {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
}
|
|
}
|
|
return nil
|
|
}
|