github.com/MetalBlockchain/metalgo@v1.11.9/nat/upnp.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package nat
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"net"
    10  	"net/netip"
    11  	"time"
    12  
    13  	"github.com/huin/goupnp"
    14  	"github.com/huin/goupnp/dcps/internetgateway1"
    15  	"github.com/huin/goupnp/dcps/internetgateway2"
    16  
    17  	"github.com/MetalBlockchain/metalgo/utils/ips"
    18  )
    19  
    20  const (
    21  	// upnpProtocol is intentionally uppercase and should not be confused with
    22  	// pmpProtocol.
    23  	// See:
    24  	// - https://github.com/huin/goupnp/blob/v1.0.3/dcps/internetgateway1/internetgateway1.go#L2361
    25  	// - https://github.com/huin/goupnp/blob/v1.0.3/dcps/internetgateway1/internetgateway1.go#L3618
    26  	// - https://github.com/huin/goupnp/blob/v1.0.3/dcps/internetgateway2/internetgateway2.go#L3919
    27  	upnpProtocol       = "TCP"
    28  	soapRequestTimeout = 10 * time.Second
    29  )
    30  
    31  var _ Router = (*upnpRouter)(nil)
    32  
    33  // upnpClient is the interface used by goupnp for their client implementations
    34  type upnpClient interface {
    35  	// attempts to map connection using the provided protocol from the external
    36  	// port to the internal port for the lease duration.
    37  	AddPortMapping(
    38  		newRemoteHost string,
    39  		newExternalPort uint16,
    40  		newProtocol string,
    41  		newInternalPort uint16,
    42  		newInternalClient string,
    43  		newEnabled bool,
    44  		newPortMappingDescription string,
    45  		newLeaseDuration uint32) error
    46  
    47  	// attempt to remove any mapping from the external port.
    48  	DeletePortMapping(
    49  		newRemoteHost string,
    50  		newExternalPort uint16,
    51  		newProtocol string) error
    52  
    53  	// attempts to return the external IP address, formatted as a string.
    54  	GetExternalIPAddress() (ip string, err error)
    55  
    56  	// returns if there is rsip available, nat enabled, or an unexpected error.
    57  	GetNATRSIPStatus() (newRSIPAvailable bool, natEnabled bool, err error)
    58  
    59  	// attempts to get port mapping information give a external port and protocol
    60  	GetSpecificPortMappingEntry(
    61  		NewRemoteHost string,
    62  		NewExternalPort uint16,
    63  		NewProtocol string,
    64  	) (
    65  		NewInternalPort uint16,
    66  		NewInternalClient string,
    67  		NewEnabled bool,
    68  		NewPortMappingDescription string,
    69  		NewLeaseDuration uint32,
    70  		err error,
    71  	)
    72  }
    73  
    74  type upnpRouter struct {
    75  	dev    *goupnp.RootDevice
    76  	client upnpClient
    77  }
    78  
    79  func (*upnpRouter) SupportsNAT() bool {
    80  	return true
    81  }
    82  
    83  func (r *upnpRouter) localIP() (net.IP, error) {
    84  	// attempt to get an address on the router
    85  	deviceAddr, err := net.ResolveUDPAddr("udp", r.dev.URLBase.Host)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	deviceIP := deviceAddr.IP
    90  
    91  	netInterfaces, err := net.Interfaces()
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	// attempt to find one of my IPs that matches router's record
    97  	for _, netInterface := range netInterfaces {
    98  		addrs, err := netInterface.Addrs()
    99  		if err != nil {
   100  			continue
   101  		}
   102  
   103  		for _, addr := range addrs {
   104  			ipNet, ok := addr.(*net.IPNet)
   105  			if !ok {
   106  				continue
   107  			}
   108  
   109  			if ipNet.Contains(deviceIP) {
   110  				return ipNet.IP, nil
   111  			}
   112  		}
   113  	}
   114  	return nil, fmt.Errorf("couldn't find the local address in the same network as %s", deviceIP)
   115  }
   116  
   117  func (r *upnpRouter) ExternalIP() (netip.Addr, error) {
   118  	str, err := r.client.GetExternalIPAddress()
   119  	if err != nil {
   120  		return netip.Addr{}, err
   121  	}
   122  	return ips.ParseAddr(str)
   123  }
   124  
   125  func (r *upnpRouter) MapPort(
   126  	intPort,
   127  	extPort uint16,
   128  	desc string,
   129  	duration time.Duration,
   130  ) error {
   131  	ip, err := r.localIP()
   132  	if err != nil {
   133  		return err
   134  	}
   135  	lifetime := duration.Seconds()
   136  	if lifetime < 0 || lifetime > math.MaxUint32 {
   137  		return errInvalidLifetime
   138  	}
   139  
   140  	return r.client.AddPortMapping("", extPort, upnpProtocol, intPort,
   141  		ip.String(), true, desc, uint32(lifetime))
   142  }
   143  
   144  func (r *upnpRouter) UnmapPort(_, extPort uint16) error {
   145  	return r.client.DeletePortMapping("", extPort, upnpProtocol)
   146  }
   147  
   148  // create UPnP SOAP service client with URN
   149  func getUPnPClient(client goupnp.ServiceClient) upnpClient {
   150  	switch client.Service.ServiceType {
   151  	case internetgateway1.URN_WANIPConnection_1:
   152  		return &internetgateway1.WANIPConnection1{ServiceClient: client}
   153  	case internetgateway1.URN_WANPPPConnection_1:
   154  		return &internetgateway1.WANPPPConnection1{ServiceClient: client}
   155  	case internetgateway2.URN_WANIPConnection_2:
   156  		return &internetgateway2.WANIPConnection2{ServiceClient: client}
   157  	default:
   158  		return nil
   159  	}
   160  }
   161  
   162  // discover() tries to find gateway device
   163  func discover(target string) *upnpRouter {
   164  	devs, err := goupnp.DiscoverDevices(target)
   165  	if err != nil {
   166  		return nil
   167  	}
   168  
   169  	router := make(chan *upnpRouter)
   170  	for i := 0; i < len(devs); i++ {
   171  		if devs[i].Root == nil {
   172  			continue
   173  		}
   174  		go func(dev *goupnp.MaybeRootDevice) {
   175  			var r *upnpRouter
   176  			dev.Root.Device.VisitServices(func(service *goupnp.Service) {
   177  				c := goupnp.ServiceClient{
   178  					SOAPClient: service.NewSOAPClient(),
   179  					RootDevice: dev.Root,
   180  					Location:   dev.Location,
   181  					Service:    service,
   182  				}
   183  				c.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
   184  				client := getUPnPClient(c)
   185  				if client == nil {
   186  					return
   187  				}
   188  				if _, nat, err := client.GetNATRSIPStatus(); err != nil || !nat {
   189  					return
   190  				}
   191  				newRouter := &upnpRouter{
   192  					dev:    dev.Root,
   193  					client: client,
   194  				}
   195  				if _, err := newRouter.localIP(); err != nil {
   196  					return
   197  				}
   198  				r = newRouter
   199  			})
   200  			router <- r
   201  		}(&devs[i])
   202  	}
   203  
   204  	for i := 0; i < len(devs); i++ {
   205  		if r := <-router; r != nil {
   206  			return r
   207  		}
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  // getUPnPRouter searches for internet gateway using both Device Control Protocol
   214  // and returns the first one it can find. It returns nil if no UPnP gateway is found
   215  func getUPnPRouter() *upnpRouter {
   216  	targets := []string{
   217  		internetgateway1.URN_WANConnectionDevice_1,
   218  		internetgateway2.URN_WANConnectionDevice_2,
   219  	}
   220  
   221  	routers := make(chan *upnpRouter)
   222  
   223  	for _, urn := range targets {
   224  		go func(urn string) {
   225  			routers <- discover(urn)
   226  		}(urn)
   227  	}
   228  
   229  	for i := 0; i < len(targets); i++ {
   230  		if r := <-routers; r != nil {
   231  			return r
   232  		}
   233  	}
   234  
   235  	return nil
   236  }