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 }