github.com/btcsuite/btcd@v0.24.0/upnp.go (about) 1 package main 2 3 // Upnp code taken from Taipei Torrent license is below: 4 // Copyright (c) 2010 Jack Palevich. All rights reserved. 5 // 6 // Redistribution and use in source and binary forms, with or without 7 // modification, are permitted provided that the following conditions are 8 // met: 9 // 10 // * Redistributions of source code must retain the above copyright 11 // notice, this list of conditions and the following disclaimer. 12 // * Redistributions in binary form must reproduce the above 13 // copyright notice, this list of conditions and the following disclaimer 14 // in the documentation and/or other materials provided with the 15 // distribution. 16 // * Neither the name of Google Inc. nor the names of its 17 // contributors may be used to endorse or promote products derived from 18 // this software without specific prior written permission. 19 // 20 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 32 // Just enough UPnP to be able to forward ports 33 // 34 35 import ( 36 "bytes" 37 "encoding/xml" 38 "errors" 39 "fmt" 40 "net" 41 "net/http" 42 "os" 43 "strconv" 44 "strings" 45 "time" 46 ) 47 48 // NAT is an interface representing a NAT traversal options for example UPNP or 49 // NAT-PMP. It provides methods to query and manipulate this traversal to allow 50 // access to services. 51 type NAT interface { 52 // Get the external address from outside the NAT. 53 GetExternalAddress() (addr net.IP, err error) 54 // Add a port mapping for protocol ("udp" or "tcp") from external port to 55 // internal port with description lasting for timeout. 56 AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) 57 // Remove a previously added port mapping from external port to 58 // internal port. 59 DeletePortMapping(protocol string, externalPort, internalPort int) (err error) 60 } 61 62 type upnpNAT struct { 63 serviceURL string 64 ourIP string 65 } 66 67 // Discover searches the local network for a UPnP router returning a NAT 68 // for the network if so, nil if not. 69 func Discover() (nat NAT, err error) { 70 ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") 71 if err != nil { 72 return 73 } 74 conn, err := net.ListenPacket("udp4", ":0") 75 if err != nil { 76 return 77 } 78 socket := conn.(*net.UDPConn) 79 defer socket.Close() 80 81 err = socket.SetDeadline(time.Now().Add(3 * time.Second)) 82 if err != nil { 83 return 84 } 85 86 st := "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" 87 buf := bytes.NewBufferString( 88 "M-SEARCH * HTTP/1.1\r\n" + 89 "HOST: 239.255.255.250:1900\r\n" + 90 st + 91 "MAN: \"ssdp:discover\"\r\n" + 92 "MX: 2\r\n\r\n") 93 message := buf.Bytes() 94 answerBytes := make([]byte, 1024) 95 for i := 0; i < 3; i++ { 96 _, err = socket.WriteToUDP(message, ssdp) 97 if err != nil { 98 return 99 } 100 var n int 101 n, _, err = socket.ReadFromUDP(answerBytes) 102 if err != nil { 103 continue 104 // socket.Close() 105 // return 106 } 107 answer := string(answerBytes[0:n]) 108 if !strings.Contains(answer, "\r\n"+st) { 109 continue 110 } 111 // HTTP header field names are case-insensitive. 112 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 113 locString := "\r\nlocation: " 114 locIndex := strings.Index(strings.ToLower(answer), locString) 115 if locIndex < 0 { 116 continue 117 } 118 loc := answer[locIndex+len(locString):] 119 endIndex := strings.Index(loc, "\r\n") 120 if endIndex < 0 { 121 continue 122 } 123 locURL := loc[0:endIndex] 124 var serviceURL string 125 serviceURL, err = getServiceURL(locURL) 126 if err != nil { 127 return 128 } 129 var ourIP string 130 ourIP, err = getOurIP() 131 if err != nil { 132 return 133 } 134 nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP} 135 return 136 } 137 err = errors.New("UPnP port discovery failed") 138 return 139 } 140 141 // service represents the Service type in an UPnP xml description. 142 // Only the parts we care about are present and thus the xml may have more 143 // fields than present in the structure. 144 type service struct { 145 ServiceType string `xml:"serviceType"` 146 ControlURL string `xml:"controlURL"` 147 } 148 149 // deviceList represents the deviceList type in an UPnP xml description. 150 // Only the parts we care about are present and thus the xml may have more 151 // fields than present in the structure. 152 type deviceList struct { 153 XMLName xml.Name `xml:"deviceList"` 154 Device []device `xml:"device"` 155 } 156 157 // serviceList represents the serviceList type in an UPnP xml description. 158 // Only the parts we care about are present and thus the xml may have more 159 // fields than present in the structure. 160 type serviceList struct { 161 XMLName xml.Name `xml:"serviceList"` 162 Service []service `xml:"service"` 163 } 164 165 // device represents the device type in an UPnP xml description. 166 // Only the parts we care about are present and thus the xml may have more 167 // fields than present in the structure. 168 type device struct { 169 XMLName xml.Name `xml:"device"` 170 DeviceType string `xml:"deviceType"` 171 DeviceList deviceList `xml:"deviceList"` 172 ServiceList serviceList `xml:"serviceList"` 173 } 174 175 // specVersion represents the specVersion in a UPnP xml description. 176 // Only the parts we care about are present and thus the xml may have more 177 // fields than present in the structure. 178 type specVersion struct { 179 XMLName xml.Name `xml:"specVersion"` 180 Major int `xml:"major"` 181 Minor int `xml:"minor"` 182 } 183 184 // root represents the Root document for a UPnP xml description. 185 // Only the parts we care about are present and thus the xml may have more 186 // fields than present in the structure. 187 type root struct { 188 XMLName xml.Name `xml:"root"` 189 SpecVersion specVersion 190 Device device 191 } 192 193 // getChildDevice searches the children of device for a device with the given 194 // type. 195 func getChildDevice(d *device, deviceType string) *device { 196 for i := range d.DeviceList.Device { 197 if d.DeviceList.Device[i].DeviceType == deviceType { 198 return &d.DeviceList.Device[i] 199 } 200 } 201 return nil 202 } 203 204 // getChildDevice searches the service list of device for a service with the 205 // given type. 206 func getChildService(d *device, serviceType string) *service { 207 for i := range d.ServiceList.Service { 208 if d.ServiceList.Service[i].ServiceType == serviceType { 209 return &d.ServiceList.Service[i] 210 } 211 } 212 return nil 213 } 214 215 // getOurIP returns a best guess at what the local IP is. 216 func getOurIP() (ip string, err error) { 217 hostname, err := os.Hostname() 218 if err != nil { 219 return 220 } 221 return net.LookupCNAME(hostname) 222 } 223 224 // getServiceURL parses the xml description at the given root url to find the 225 // url for the WANIPConnection service to be used for port forwarding. 226 func getServiceURL(rootURL string) (url string, err error) { 227 r, err := http.Get(rootURL) 228 if err != nil { 229 return 230 } 231 defer r.Body.Close() 232 if r.StatusCode >= 400 { 233 err = errors.New(fmt.Sprint(r.StatusCode)) 234 return 235 } 236 var root root 237 err = xml.NewDecoder(r.Body).Decode(&root) 238 if err != nil { 239 return 240 } 241 a := &root.Device 242 if a.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" { 243 err = errors.New("no InternetGatewayDevice") 244 return 245 } 246 b := getChildDevice(a, "urn:schemas-upnp-org:device:WANDevice:1") 247 if b == nil { 248 err = errors.New("no WANDevice") 249 return 250 } 251 c := getChildDevice(b, "urn:schemas-upnp-org:device:WANConnectionDevice:1") 252 if c == nil { 253 err = errors.New("no WANConnectionDevice") 254 return 255 } 256 d := getChildService(c, "urn:schemas-upnp-org:service:WANIPConnection:1") 257 if d == nil { 258 err = errors.New("no WANIPConnection") 259 return 260 } 261 url = combineURL(rootURL, d.ControlURL) 262 return 263 } 264 265 // combineURL appends subURL onto rootURL. 266 func combineURL(rootURL, subURL string) string { 267 protocolEnd := "://" 268 protoEndIndex := strings.Index(rootURL, protocolEnd) 269 a := rootURL[protoEndIndex+len(protocolEnd):] 270 rootIndex := strings.Index(a, "/") 271 return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL 272 } 273 274 // soapBody represents the <s:Body> element in a SOAP reply. 275 // fields we don't care about are elided. 276 type soapBody struct { 277 XMLName xml.Name `xml:"Body"` 278 Data []byte `xml:",innerxml"` 279 } 280 281 // soapEnvelope represents the <s:Envelope> element in a SOAP reply. 282 // fields we don't care about are elided. 283 type soapEnvelope struct { 284 XMLName xml.Name `xml:"Envelope"` 285 Body soapBody `xml:"Body"` 286 } 287 288 // soapRequests performs a soap request with the given parameters and returns 289 // the xml replied stripped of the soap headers. in the case that the request is 290 // unsuccessful the an error is returned. 291 func soapRequest(url, function, message string) (replyXML []byte, err error) { 292 fullMessage := "<?xml version=\"1.0\" ?>" + 293 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" + 294 "<s:Body>" + message + "</s:Body></s:Envelope>" 295 296 req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) 297 if err != nil { 298 return nil, err 299 } 300 req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") 301 req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") 302 req.Header.Set("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#"+function+"\"") 303 req.Header.Set("Connection", "Close") 304 req.Header.Set("Cache-Control", "no-cache") 305 req.Header.Set("Pragma", "no-cache") 306 307 r, err := http.DefaultClient.Do(req) 308 if err != nil { 309 return nil, err 310 } 311 if r.Body != nil { 312 defer r.Body.Close() 313 } 314 315 if r.StatusCode >= 400 { 316 err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) 317 r = nil 318 return 319 } 320 var reply soapEnvelope 321 err = xml.NewDecoder(r.Body).Decode(&reply) 322 if err != nil { 323 return nil, err 324 } 325 return reply.Body.Data, nil 326 } 327 328 // getExternalIPAddressResponse represents the XML response to a 329 // GetExternalIPAddress SOAP request. 330 type getExternalIPAddressResponse struct { 331 XMLName xml.Name `xml:"GetExternalIPAddressResponse"` 332 ExternalIPAddress string `xml:"NewExternalIPAddress"` 333 } 334 335 // GetExternalAddress implements the NAT interface by fetching the external IP 336 // from the UPnP router. 337 func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { 338 message := "<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>\r\n" 339 response, err := soapRequest(n.serviceURL, "GetExternalIPAddress", message) 340 if err != nil { 341 return nil, err 342 } 343 344 var reply getExternalIPAddressResponse 345 err = xml.Unmarshal(response, &reply) 346 if err != nil { 347 return nil, err 348 } 349 350 addr = net.ParseIP(reply.ExternalIPAddress) 351 if addr == nil { 352 return nil, errors.New("unable to parse ip address") 353 } 354 return addr, nil 355 } 356 357 // AddPortMapping implements the NAT interface by setting up a port forwarding 358 // from the UPnP router to the local machine with the given ports and protocol. 359 func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { 360 // A single concatenation would break ARM compilation. 361 message := "<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" + 362 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) 363 message += "</NewExternalPort><NewProtocol>" + strings.ToUpper(protocol) + "</NewProtocol>" 364 message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" + 365 "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" + 366 "<NewEnabled>1</NewEnabled><NewPortMappingDescription>" 367 message += description + 368 "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) + 369 "</NewLeaseDuration></u:AddPortMapping>" 370 371 response, err := soapRequest(n.serviceURL, "AddPortMapping", message) 372 if err != nil { 373 return 374 } 375 376 // TODO: check response to see if the port was forwarded 377 // If the port was not wildcard we don't get an reply with the port in 378 // it. Not sure about wildcard yet. miniupnpc just checks for error 379 // codes here. 380 mappedExternalPort = externalPort 381 _ = response 382 return 383 } 384 385 // DeletePortMapping implements the NAT interface by removing up a port forwarding 386 // from the UPnP router to the local machine with the given ports and. 387 func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { 388 389 message := "<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" + 390 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) + 391 "</NewExternalPort><NewProtocol>" + strings.ToUpper(protocol) + "</NewProtocol>" + 392 "</u:DeletePortMapping>" 393 394 response, err := soapRequest(n.serviceURL, "DeletePortMapping", message) 395 if err != nil { 396 return 397 } 398 399 // TODO: check response to see if the port was deleted 400 // log.Println(message, response) 401 _ = response 402 return 403 }