github.com/nebulouslabs/sia@v1.3.7/modules/host/upnp.go (about)

     1  package host
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/NebulousLabs/Sia/build"
    15  	"github.com/NebulousLabs/Sia/modules"
    16  
    17  	"github.com/NebulousLabs/go-upnp"
    18  )
    19  
    20  // managedLearnHostname discovers the external IP of the Host. If the host's
    21  // net address is blank and the host's auto address appears to have changed,
    22  // the host will make an announcement on the blockchain.
    23  func (h *Host) managedLearnHostname() {
    24  	if build.Release == "testing" {
    25  		return
    26  	}
    27  
    28  	// Fetch a group of host vars that will be used to dictate the logic of the
    29  	// function.
    30  	h.mu.RLock()
    31  	netAddr := h.settings.NetAddress
    32  	hostPort := h.port
    33  	hostAutoAddress := h.autoAddress
    34  	hostAnnounced := h.announced
    35  	hostAcceptingContracts := h.settings.AcceptingContracts
    36  	hostContractCount := h.financialMetrics.ContractCount
    37  	h.mu.RUnlock()
    38  
    39  	// If the settings indicate that an address has been manually set, there is
    40  	// no reason to learn the hostname.
    41  	if netAddr != "" {
    42  		return
    43  	}
    44  	h.log.Println("No manually set net address. Scanning to automatically determine address.")
    45  
    46  	// try UPnP first, then fallback to myexternalip.com
    47  	ctx, cancel := context.WithCancel(context.Background())
    48  	defer cancel()
    49  	go func() {
    50  		select {
    51  		case <-h.tg.StopChan():
    52  			cancel()
    53  		case <-ctx.Done():
    54  		}
    55  	}()
    56  	var hostname string
    57  	d, err := upnp.DiscoverCtx(ctx)
    58  	if err == nil {
    59  		hostname, err = d.ExternalIP()
    60  	}
    61  	if err != nil {
    62  		hostname, err = myExternalIP()
    63  	}
    64  	if err != nil {
    65  		h.log.Println("WARN: failed to discover external IP")
    66  		return
    67  	}
    68  
    69  	autoAddress := modules.NetAddress(net.JoinHostPort(hostname, hostPort))
    70  	if err := autoAddress.IsValid(); err != nil {
    71  		h.log.Printf("WARN: discovered hostname %q is invalid: %v", autoAddress, err)
    72  		return
    73  	}
    74  	if autoAddress == hostAutoAddress && hostAnnounced {
    75  		// Nothing to do - the auto address has not changed and the previous
    76  		// annoucement was successful.
    77  		return
    78  	}
    79  
    80  	h.mu.Lock()
    81  	h.autoAddress = autoAddress
    82  	err = h.saveSync()
    83  	h.mu.Unlock()
    84  	if err != nil {
    85  		h.log.Println(err)
    86  	}
    87  
    88  	// Announce the host, but only if the host is either accepting contracts or
    89  	// has a storage obligation. If the host is not accepting contracts and has
    90  	// no open contracts, there is no reason to notify anyone that the host's
    91  	// address has changed.
    92  	if hostAcceptingContracts || hostContractCount > 0 {
    93  		h.log.Println("Host external IP address changed from", hostAutoAddress, "to", autoAddress, "- performing host announcement.")
    94  		err = h.managedAnnounce(autoAddress)
    95  		if err != nil {
    96  			// Set h.announced to false, as the address has changed yet the
    97  			// renewed annoucement has failed.
    98  			h.mu.Lock()
    99  			h.announced = false
   100  			h.mu.Unlock()
   101  			h.log.Println("unable to announce address after upnp-detected address change:", err)
   102  		}
   103  	}
   104  }
   105  
   106  // managedForwardPort adds a port mapping to the router.
   107  func (h *Host) managedForwardPort(port string) error {
   108  	if build.Release == "testing" {
   109  		// Add a blocking placeholder where testing is able to mock behaviors
   110  		// such as a port forward action that blocks for 10 seconds before
   111  		// completing.
   112  		if h.dependencies.Disrupt("managedForwardPort") {
   113  			return nil
   114  		}
   115  
   116  		// Port forwarding functions are frequently unavailable during testing,
   117  		// and the long blocking can be highly disruptive. Under normal
   118  		// scenarios, return without complaint, and without running the
   119  		// port-forward logic.
   120  		return nil
   121  	}
   122  
   123  	// If the port is invalid, there is no need to perform any of the other
   124  	// tasks.
   125  	portInt, err := strconv.Atoi(port)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	ctx, cancel := context.WithCancel(context.Background())
   131  	defer cancel()
   132  	go func() {
   133  		select {
   134  		case <-h.tg.StopChan():
   135  			cancel()
   136  		case <-ctx.Done():
   137  		}
   138  	}()
   139  	d, err := upnp.DiscoverCtx(ctx)
   140  	if err != nil {
   141  		h.log.Printf("WARN: could not automatically forward port %s: %v", port, err)
   142  		return err
   143  	}
   144  	err = d.Forward(uint16(portInt), "Sia Host")
   145  	if err != nil {
   146  		h.log.Printf("WARN: could not automatically forward port %s: %v", port, err)
   147  		return err
   148  	}
   149  
   150  	h.log.Println("INFO: successfully forwarded port", port)
   151  	return nil
   152  }
   153  
   154  // managedClearPort removes a port mapping from the router.
   155  func (h *Host) managedClearPort() error {
   156  	if build.Release == "testing" {
   157  		// Allow testing to force an error to be returned here.
   158  		if h.dependencies.Disrupt("managedClearPort return error") {
   159  			return errors.New("Mocked managedClearPortErr")
   160  		}
   161  		return nil
   162  	}
   163  
   164  	// If the port is invalid, there is no need to perform any of the other
   165  	// tasks.
   166  	h.mu.RLock()
   167  	port := h.port
   168  	h.mu.RUnlock()
   169  	portInt, err := strconv.Atoi(port)
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	ctx, cancel := context.WithCancel(context.Background())
   175  	defer cancel()
   176  	go func() {
   177  		select {
   178  		case <-h.tg.StopChan():
   179  			cancel()
   180  		case <-ctx.Done():
   181  		}
   182  	}()
   183  	d, err := upnp.DiscoverCtx(ctx)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	err = d.Clear(uint16(portInt))
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	h.log.Println("INFO: successfully unforwarded port", port)
   193  	return nil
   194  }
   195  
   196  // myExternalIP discovers the host's external IP by querying a centralized
   197  // service, http://myexternalip.com.
   198  func myExternalIP() (string, error) {
   199  	// timeout after 10 seconds
   200  	client := http.Client{Timeout: time.Duration(10 * time.Second)}
   201  	resp, err := client.Get("http://myexternalip.com/raw")
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  	defer resp.Body.Close()
   206  	if resp.StatusCode != http.StatusOK {
   207  		errResp, _ := ioutil.ReadAll(resp.Body)
   208  		return "", errors.New(string(errResp))
   209  	}
   210  	buf, err := ioutil.ReadAll(io.LimitReader(resp.Body, 64))
   211  	if err != nil {
   212  		return "", err
   213  	}
   214  	if len(buf) == 0 {
   215  		return "", errors.New("myexternalip.com returned a 0 length IP address")
   216  	}
   217  	// trim newline
   218  	return strings.TrimSpace(string(buf)), nil
   219  }