github.com/line/ostracon@v1.0.10-0.20230328032236-7f20145f065d/p2p/upnp/upnp.go (about) 1 // Taken from taipei-torrent. 2 // Just enough UPnP to be able to forward ports 3 // For more information, see: http://www.upnp-hacks.org/upnp.html 4 package upnp 5 6 // TODO: use syscalls to get actual ourIP, see issue #712 7 8 import ( 9 "bytes" 10 "encoding/xml" 11 "errors" 12 "fmt" 13 "io" 14 "net" 15 "net/http" 16 "strconv" 17 "strings" 18 "time" 19 20 net2 "github.com/line/ostracon/libs/net" 21 ) 22 23 const HttpRequestTimeout = 30 * time.Second 24 25 type upnpNAT struct { 26 serviceURL string 27 ourIP string 28 urnDomain string 29 } 30 31 // protocol is either "udp" or "tcp" 32 type NAT interface { 33 GetExternalAddress() (addr net.IP, err error) 34 AddPortMapping( 35 protocol string, 36 externalPort, 37 internalPort int, 38 description string, 39 timeout int) (mappedExternalPort int, err error) 40 DeletePortMapping(protocol string, externalPort, internalPort int) (err error) 41 } 42 43 func Discover() (nat NAT, err error) { 44 ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") 45 if err != nil { 46 return 47 } 48 conn, err := net.ListenPacket("udp4", ":0") 49 if err != nil { 50 return 51 } 52 socket := conn.(*net.UDPConn) 53 defer socket.Close() 54 55 if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil { 56 return nil, err 57 } 58 59 st := "InternetGatewayDevice:1" 60 61 buf := bytes.NewBufferString( 62 "M-SEARCH * HTTP/1.1\r\n" + 63 "HOST: 239.255.255.250:1900\r\n" + 64 "ST: ssdp:all\r\n" + 65 "MAN: \"ssdp:discover\"\r\n" + 66 "MX: 2\r\n\r\n") 67 message := buf.Bytes() 68 answerBytes := make([]byte, 1024) 69 for i := 0; i < 3; i++ { 70 _, err = socket.WriteToUDP(message, ssdp) 71 if err != nil { 72 return 73 } 74 var n int 75 _, _, err = socket.ReadFromUDP(answerBytes) 76 if err != nil { 77 return 78 } 79 for { 80 n, _, err = socket.ReadFromUDP(answerBytes) 81 if err != nil { 82 break 83 } 84 answer := string(answerBytes[0:n]) 85 if !strings.Contains(answer, st) { 86 continue 87 } 88 // HTTP header field names are case-insensitive. 89 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 90 locString := "\r\nlocation:" 91 answer = strings.ToLower(answer) 92 locIndex := strings.Index(answer, locString) 93 if locIndex < 0 { 94 continue 95 } 96 loc := answer[locIndex+len(locString):] 97 endIndex := strings.Index(loc, "\r\n") 98 if endIndex < 0 { 99 continue 100 } 101 locURL := strings.TrimSpace(loc[0:endIndex]) 102 var serviceURL, urnDomain string 103 serviceURL, urnDomain, err = getServiceURL(locURL) 104 if err != nil { 105 return 106 } 107 var ourIP net.IP 108 ourIP, err = localIPv4() 109 if err != nil { 110 return 111 } 112 nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain} 113 return 114 } 115 } 116 err = errors.New("upnp port discovery failed") 117 return nat, err 118 } 119 120 type Envelope struct { 121 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 122 Soap *SoapBody 123 } 124 type SoapBody struct { 125 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` 126 ExternalIP *ExternalIPAddressResponse 127 } 128 129 type ExternalIPAddressResponse struct { 130 XMLName xml.Name `xml:"GetExternalIPAddressResponse"` 131 IPAddress string `xml:"NewExternalIPAddress"` 132 } 133 134 type ExternalIPAddress struct { 135 XMLName xml.Name `xml:"NewExternalIPAddress"` 136 IP string 137 } 138 139 type Service struct { 140 ServiceType string `xml:"serviceType"` 141 ControlURL string `xml:"controlURL"` 142 } 143 144 type DeviceList struct { 145 Device []Device `xml:"device"` 146 } 147 148 type ServiceList struct { 149 Service []Service `xml:"service"` 150 } 151 152 type Device struct { 153 XMLName xml.Name `xml:"device"` 154 DeviceType string `xml:"deviceType"` 155 DeviceList DeviceList `xml:"deviceList"` 156 ServiceList ServiceList `xml:"serviceList"` 157 } 158 159 type Root struct { 160 Device Device 161 } 162 163 func getChildDevice(d *Device, deviceType string) *Device { 164 dl := d.DeviceList.Device 165 for i := 0; i < len(dl); i++ { 166 if strings.Contains(dl[i].DeviceType, deviceType) { 167 return &dl[i] 168 } 169 } 170 return nil 171 } 172 173 func getChildService(d *Device, serviceType string) *Service { 174 sl := d.ServiceList.Service 175 for i := 0; i < len(sl); i++ { 176 if strings.Contains(sl[i].ServiceType, serviceType) { 177 return &sl[i] 178 } 179 } 180 return nil 181 } 182 183 func localIPv4() (net.IP, error) { 184 tt, err := net.Interfaces() 185 if err != nil { 186 return nil, err 187 } 188 for _, t := range tt { 189 aa, err := t.Addrs() 190 if err != nil { 191 return nil, err 192 } 193 for _, a := range aa { 194 ipnet, ok := a.(*net.IPNet) 195 if !ok { 196 continue 197 } 198 v4 := ipnet.IP.To4() 199 if v4 == nil || v4[0] == 127 { // loopback address 200 continue 201 } 202 return v4, nil 203 } 204 } 205 return nil, errors.New("cannot find local IP address") 206 } 207 208 func getServiceURL(rootURL string) (url, urnDomain string, err error) { 209 r, err := net2.HttpGet(rootURL, HttpRequestTimeout) 210 if err != nil { 211 return 212 } 213 defer r.Body.Close() 214 215 if r.StatusCode >= 400 { 216 err = errors.New(fmt.Sprint(r.StatusCode)) 217 return 218 } 219 var root Root 220 err = xml.NewDecoder(r.Body).Decode(&root) 221 if err != nil { 222 return 223 } 224 a := &root.Device 225 if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") { 226 err = errors.New("no InternetGatewayDevice") 227 return 228 } 229 b := getChildDevice(a, "WANDevice:1") 230 if b == nil { 231 err = errors.New("no WANDevice") 232 return 233 } 234 c := getChildDevice(b, "WANConnectionDevice:1") 235 if c == nil { 236 err = errors.New("no WANConnectionDevice") 237 return 238 } 239 d := getChildService(c, "WANIPConnection:1") 240 if d == nil { 241 // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice, 242 // instead of under WanConnectionDevice 243 d = getChildService(b, "WANIPConnection:1") 244 245 if d == nil { 246 err = errors.New("no WANIPConnection") 247 return 248 } 249 } 250 // Extract the domain name, which isn't always 'schemas-upnp-org' 251 urnDomain = strings.Split(d.ServiceType, ":")[1] 252 url = combineURL(rootURL, d.ControlURL) 253 return url, urnDomain, err 254 } 255 256 func combineURL(rootURL, subURL string) string { 257 protocolEnd := "://" 258 protoEndIndex := strings.Index(rootURL, protocolEnd) 259 a := rootURL[protoEndIndex+len(protocolEnd):] 260 rootIndex := strings.Index(a, "/") 261 return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL 262 } 263 264 func soapRequest(url, function, message, domain string) (r *http.Response, err error) { 265 fullMessage := "<?xml version=\"1.0\" ?>" + 266 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " + 267 "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" + 268 "<s:Body>" + message + "</s:Body></s:Envelope>" 269 270 req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) 271 if err != nil { 272 return nil, err 273 } 274 req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") 275 req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") 276 // req.Header.Set("Transfer-Encoding", "chunked") 277 req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"") 278 req.Header.Set("Connection", "Close") 279 req.Header.Set("Cache-Control", "no-cache") 280 req.Header.Set("Pragma", "no-cache") 281 282 // log.Stderr("soapRequest ", req) 283 284 r, err = net2.HttpRequest(req, HttpRequestTimeout) 285 if err != nil { 286 return nil, err 287 } 288 /*if r.Body != nil { 289 defer r.Body.Close() 290 }*/ 291 292 if r.StatusCode != http.StatusOK { 293 // log.Stderr(function, r.StatusCode) 294 err = errors.New("error " + strconv.Itoa(r.StatusCode) + " for " + function) 295 r = nil 296 return 297 } 298 return r, err 299 } 300 301 type statusInfo struct { 302 externalIPAddress string 303 } 304 305 func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) { 306 307 message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + 308 "</u:GetExternalIPAddress>" 309 310 var response *http.Response 311 response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) 312 if response != nil { 313 defer response.Body.Close() 314 } 315 if err != nil { 316 return 317 } 318 var envelope Envelope 319 data, err := io.ReadAll(response.Body) 320 if err != nil { 321 return 322 } 323 reader := bytes.NewReader(data) 324 err = xml.NewDecoder(reader).Decode(&envelope) 325 if err != nil { 326 return 327 } 328 329 info = statusInfo{envelope.Soap.ExternalIP.IPAddress} 330 331 if err != nil { 332 return 333 } 334 335 return info, err 336 } 337 338 // GetExternalAddress returns an external IP. If GetExternalIPAddress action 339 // fails or IP returned is invalid, GetExternalAddress returns an error. 340 func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { 341 info, err := n.getExternalIPAddress() 342 if err != nil { 343 return 344 } 345 addr = net.ParseIP(info.externalIPAddress) 346 if addr == nil { 347 err = fmt.Errorf("failed to parse IP: %v", info.externalIPAddress) 348 } 349 return 350 } 351 352 func (n *upnpNAT) AddPortMapping( 353 protocol string, 354 externalPort, 355 internalPort int, 356 description string, 357 timeout int) (mappedExternalPort int, err error) { 358 // A single concatenation would break ARM compilation. 359 message := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + 360 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) 361 message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" 362 message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" + 363 "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" + 364 "<NewEnabled>1</NewEnabled><NewPortMappingDescription>" 365 message += description + 366 "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) + 367 "</NewLeaseDuration></u:AddPortMapping>" 368 369 var response *http.Response 370 response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) 371 if response != nil { 372 defer response.Body.Close() 373 } 374 if err != nil { 375 return 376 } 377 378 // TODO: check response to see if the port was forwarded 379 // log.Println(message, response) 380 // JAE: 381 // body, err := ioutil.ReadAll(response.Body) 382 // fmt.Println(string(body), err) 383 mappedExternalPort = externalPort 384 _ = response 385 return mappedExternalPort, err 386 } 387 388 func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { 389 390 message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + 391 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) + 392 "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" + 393 "</u:DeletePortMapping>" 394 395 var response *http.Response 396 response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) 397 if response != nil { 398 defer response.Body.Close() 399 } 400 if err != nil { 401 return 402 } 403 404 // TODO: check response to see if the port was deleted 405 // log.Println(message, response) 406 _ = response 407 return 408 }