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 }