gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/gateway/upnp.go (about)

     1  package gateway
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"gitlab.com/NebulousLabs/errors"
    15  	"gitlab.com/NebulousLabs/go-upnp"
    16  
    17  	"gitlab.com/SiaPrime/SiaPrime/build"
    18  	"gitlab.com/SiaPrime/SiaPrime/modules"
    19  )
    20  
    21  // myExternalIP discovers the gateway's external IP by querying a centralized
    22  // service, http://myexternalip.com.
    23  func myExternalIP() (string, error) {
    24  	// timeout after 10 seconds
    25  	client := http.Client{Timeout: time.Duration(10 * time.Second)}
    26  	resp, err := client.Get("http://myexternalip.com/raw")
    27  	if err != nil {
    28  		return "", err
    29  	}
    30  	defer resp.Body.Close()
    31  	if resp.StatusCode != http.StatusOK {
    32  		errResp, _ := ioutil.ReadAll(resp.Body)
    33  		return "", errors.New(string(errResp))
    34  	}
    35  	buf, err := ioutil.ReadAll(io.LimitReader(resp.Body, 64))
    36  	if err != nil {
    37  		return "", err
    38  	}
    39  	if len(buf) == 0 {
    40  		return "", errors.New("myexternalip.com returned a 0 length IP address")
    41  	}
    42  	// trim newline
    43  	return strings.TrimSpace(string(buf)), nil
    44  }
    45  
    46  // managedLearnHostname tries to discover the external ip of the machine. If
    47  // discovering the address failed or if it is invalid, an error is returned.
    48  func (g *Gateway) managedLearnHostname(cancel <-chan struct{}) (net.IP, error) {
    49  	// create ctx to cancel upnp discovery during shutdown
    50  	ctx, ctxCancel := context.WithTimeout(context.Background(), timeoutIPDiscovery)
    51  	defer ctxCancel()
    52  	go func() {
    53  		select {
    54  		case <-cancel:
    55  			ctxCancel()
    56  		case <-g.threads.StopChan():
    57  			ctxCancel()
    58  		case <-ctx.Done():
    59  		}
    60  	}()
    61  
    62  	// try UPnP first, then fallback to myexternalip.com and peer-to-peer
    63  	// discovery.
    64  	var host string
    65  	d, err := upnp.Load(g.persist.RouterURL)
    66  	if err != nil {
    67  		d, err = upnp.DiscoverCtx(ctx)
    68  	}
    69  	if err == nil {
    70  		g.mu.Lock()
    71  		g.persist.RouterURL = d.Location()
    72  		if err = g.saveSync(); err != nil {
    73  			g.log.Panicln("WARN: could not save the gateway:", err)
    74  		}
    75  		g.mu.Unlock()
    76  		host, err = d.ExternalIP()
    77  	}
    78  	if err != nil {
    79  		host, err = g.managedIPFromPeers(ctx.Done())
    80  	}
    81  	if !build.DEBUG && err != nil {
    82  		host, err = myExternalIP()
    83  	}
    84  	if err != nil {
    85  		return nil, errors.AddContext(err, "failed to discover external IP")
    86  	}
    87  	ip := net.ParseIP(host)
    88  	if ip == nil {
    89  		return nil, fmt.Errorf("%v is not a valid IP", host)
    90  	}
    91  	return ip, nil
    92  }
    93  
    94  // threadedLearnHostname discovers the external IP of the Gateway regularly.
    95  func (g *Gateway) threadedLearnHostname() {
    96  	if err := g.threads.Add(); err != nil {
    97  		return
    98  	}
    99  	defer g.threads.Done()
   100  
   101  	if build.Release == "testing" {
   102  		return
   103  	}
   104  
   105  	for {
   106  		host, err := g.managedLearnHostname(nil)
   107  		if err != nil {
   108  			g.log.Println("WARN: failed to discover external IP:", err)
   109  		}
   110  		// If we were unable to discover our IP we try again later.
   111  		if err != nil {
   112  			if !g.managedSleep(rediscoverIPIntervalFailure) {
   113  				return // shutdown interrupted sleep
   114  			}
   115  			continue
   116  		}
   117  
   118  		g.mu.RLock()
   119  		addr := modules.NetAddress(net.JoinHostPort(host.String(), g.port))
   120  		g.mu.RUnlock()
   121  		if err := addr.IsValid(); err != nil {
   122  			g.log.Printf("WARN: discovered hostname %q is invalid: %v", addr, err)
   123  			if !g.managedSleep(rediscoverIPIntervalFailure) {
   124  				return // shutdown interrupted sleep
   125  			}
   126  			continue
   127  		}
   128  
   129  		g.mu.Lock()
   130  		g.myAddr = addr
   131  		g.mu.Unlock()
   132  
   133  		g.log.Println("INFO: our address is", addr)
   134  
   135  		// Rediscover the IP later in case it changed.
   136  		if !g.managedSleep(rediscoverIPIntervalSuccess) {
   137  			return // shutdown interrupted sleep
   138  		}
   139  	}
   140  }
   141  
   142  // managedForwardPort adds a port mapping to the router.
   143  func (g *Gateway) managedForwardPort(port string) error {
   144  	if build.Release == "testing" {
   145  		// Port forwarding functions are frequently unavailable during testing,
   146  		// and the long blocking can be highly disruptive. Under normal
   147  		// scenarios, return without complaint, and without running the
   148  		// port-forward logic.
   149  		return nil
   150  	}
   151  
   152  	// If the port is invalid, there is no need to perform any of the other
   153  	// tasks.
   154  	portInt, err := strconv.Atoi(port)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	// Create a context to stop UPnP discovery in case of a shutdown.
   160  	ctx, cancel := context.WithCancel(context.Background())
   161  	defer cancel()
   162  	go func() {
   163  		select {
   164  		case <-g.threads.StopChan():
   165  			cancel()
   166  		case <-ctx.Done():
   167  		}
   168  	}()
   169  
   170  	// Look for UPnP-enabled devices
   171  	d, err := upnp.DiscoverCtx(ctx)
   172  	if err != nil {
   173  		err = fmt.Errorf("WARN: could not automatically forward port %s: no UPnP-enabled devices found: %v", port, err)
   174  		return err
   175  	}
   176  
   177  	// Forward port
   178  	err = d.Forward(uint16(portInt), "Sia RPC")
   179  	if err != nil {
   180  		err = fmt.Errorf("WARN: could not automatically forward port %s: %v", port, err)
   181  		return err
   182  	}
   183  
   184  	// Establish port-clearing at shutdown.
   185  	g.threads.AfterStop(func() {
   186  		g.managedClearPort(port)
   187  	})
   188  	return nil
   189  }
   190  
   191  // managedClearPort removes a port mapping from the router.
   192  func (g *Gateway) managedClearPort(port string) {
   193  	if build.Release == "testing" {
   194  		return
   195  	}
   196  
   197  	ctx, cancel := context.WithCancel(context.Background())
   198  	defer cancel()
   199  	go func() {
   200  		select {
   201  		case <-g.threads.StopChan():
   202  			cancel()
   203  		case <-ctx.Done():
   204  		}
   205  	}()
   206  	d, err := upnp.DiscoverCtx(ctx)
   207  	if err != nil {
   208  		return
   209  	}
   210  
   211  	portInt, _ := strconv.Atoi(port)
   212  	err = d.Clear(uint16(portInt))
   213  	if err != nil {
   214  		g.log.Printf("WARN: could not automatically unforward port %s: %v", port, err)
   215  		return
   216  	}
   217  
   218  	g.log.Println("INFO: successfully unforwarded port", port)
   219  }
   220  
   221  // threadedForwardPort forwards a port and logs potential errors.
   222  func (g *Gateway) threadedForwardPort(port string) {
   223  	if err := g.threads.Add(); err != nil {
   224  		return
   225  	}
   226  	defer g.threads.Done()
   227  
   228  	if err := g.managedForwardPort(port); err != nil {
   229  		g.log.Debugf("WARN: %v", err)
   230  	}
   231  	g.log.Println("INFO: successfully forwarded port", port)
   232  	return
   233  }