gitlab.com/jokerrs1/Sia@v1.3.2/modules/host/upnp.go (about)

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