diff --git a/upnp/README.md b/upnp/README.md new file mode 100644 index 0000000..fe5be4e --- /dev/null +++ b/upnp/README.md @@ -0,0 +1,5 @@ +# UPnP + +There are 2 reference implementations which both are based on 'Taipei Torrent'. This UPnP code is a fork mostly from the btcd one with some changes forked from tendermint. +* https://github.com/btcsuite/btcd/blob/master/upnp.go +* https://github.com/tendermint/tendermint/tree/master/p2p/upnp diff --git a/upnp/UPnP.go b/upnp/UPnP.go new file mode 100644 index 0000000..32238bb --- /dev/null +++ b/upnp/UPnP.go @@ -0,0 +1,373 @@ +/* +File Name: UPnP.go +Copyright: 2021 Peernet s.r.o. +Author: Peter Kleissner +*/ + +package upnp + +import ( + "bytes" + "encoding/xml" + "errors" + "net" + "net/http" + "strconv" + "strings" + "time" +) + +// NAT is an interface representing a NAT traversal options for example UPNP or +// NAT-PMP. It provides methods to query and manipulate this traversal to allow +// access to services. +type NAT interface { + // Get the external address from outside the NAT. + GetExternalAddress() (addr net.IP, err error) + // Add a port mapping for protocol ("udp" or "tcp") from external port to internal port with description lasting for timeout. + AddPortMapping(protocol string, internalIP net.IP, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) + // Remove a previously added port mapping from external port to internal port. + DeletePortMapping(protocol string, externalPort, internalPort int) (err error) +} + +type upnpNAT struct { + serviceURL string + urnDomain string +} + +// Discover searches the local network for a UPnP router returning a NAT +// for the network if so, nil if not. +func Discover() (nat NAT, err error) { + ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") + if err != nil { + return + } + conn, err := net.ListenPacket("udp4", ":0") + if err != nil { + return + } + socket := conn.(*net.UDPConn) + defer socket.Close() + + err = socket.SetDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return + } + + st := "InternetGatewayDevice:1" + + buf := bytes.NewBufferString( + "M-SEARCH * HTTP/1.1\r\n" + + "HOST: 239.255.255.250:1900\r\n" + + "ST: ssdp:all\r\n" + + "MAN: \"ssdp:discover\"\r\n" + + "MX: 2\r\n\r\n") + message := buf.Bytes() + answerBytes := make([]byte, 1024) + for i := 0; i < 3; i++ { + _, err = socket.WriteToUDP(message, ssdp) + if err != nil { + return + } + var n int + _, _, err = socket.ReadFromUDP(answerBytes) + if err != nil { + return + } + for { + n, _, err = socket.ReadFromUDP(answerBytes) + if err != nil { + break + } + answer := string(answerBytes[0:n]) + if !strings.Contains(answer, st) { + continue + } + // HTTP header field names are case-insensitive. + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + locString := "\r\nlocation:" + answer = strings.ToLower(answer) + locIndex := strings.Index(answer, locString) + if locIndex < 0 { + continue + } + loc := answer[locIndex+len(locString):] + endIndex := strings.Index(loc, "\r\n") + if endIndex < 0 { + continue + } + locURL := strings.TrimSpace(loc[0:endIndex]) + var serviceURL, urnDomain string + serviceURL, urnDomain, err = getServiceURL(locURL) + if err != nil { + return + } + nat = &upnpNAT{serviceURL: serviceURL, urnDomain: urnDomain} + return + } + } + err = errors.New("UPnP port discovery failed") + return +} + +// service represents the Service type in an UPnP xml description. +// Only the parts we care about are present and thus the xml may have more +// fields than present in the structure. +type service struct { + ServiceType string `xml:"serviceType"` + ControlURL string `xml:"controlURL"` +} + +// deviceList represents the deviceList type in an UPnP xml description. +// Only the parts we care about are present and thus the xml may have more +// fields than present in the structure. +type deviceList struct { + XMLName xml.Name `xml:"deviceList"` + Device []device `xml:"device"` +} + +// serviceList represents the serviceList type in an UPnP xml description. +// Only the parts we care about are present and thus the xml may have more +// fields than present in the structure. +type serviceList struct { + XMLName xml.Name `xml:"serviceList"` + Service []service `xml:"service"` +} + +// device represents the device type in an UPnP xml description. +// Only the parts we care about are present and thus the xml may have more +// fields than present in the structure. +type device struct { + XMLName xml.Name `xml:"device"` + DeviceType string `xml:"deviceType"` + DeviceList deviceList `xml:"deviceList"` + ServiceList serviceList `xml:"serviceList"` +} + +// specVersion represents the specVersion in a UPnP xml description. +// Only the parts we care about are present and thus the xml may have more +// fields than present in the structure. +type specVersion struct { + XMLName xml.Name `xml:"specVersion"` + Major int `xml:"major"` + Minor int `xml:"minor"` +} + +// root represents the Root document for a UPnP xml description. +// Only the parts we care about are present and thus the xml may have more +// fields than present in the structure. +type root struct { + XMLName xml.Name `xml:"root"` + SpecVersion specVersion + Device device +} + +// getChildDevice searches the children of device for a device with the given +// type. +func getChildDevice(d *device, deviceType string) *device { + for i := range d.DeviceList.Device { + if strings.Contains(d.DeviceList.Device[i].DeviceType, deviceType) { + return &d.DeviceList.Device[i] + } + } + return nil +} + +// getChildDevice searches the service list of device for a service with the +// given type. +func getChildService(d *device, serviceType string) *service { + for i := range d.ServiceList.Service { + if strings.Contains(d.ServiceList.Service[i].ServiceType, serviceType) { + return &d.ServiceList.Service[i] + } + } + return nil +} + +// getServiceURL parses the xml description at the given root url to find the +// url for the WANIPConnection service to be used for port forwarding. +func getServiceURL(rootURL string) (url, urnDomain string, err error) { + r, err := http.Get(rootURL) + if err != nil { + return + } + defer r.Body.Close() + + if r.StatusCode >= 400 { + err = errors.New("Unexpected status code " + strconv.Itoa(r.StatusCode)) + return + } + var root root + err = xml.NewDecoder(r.Body).Decode(&root) + if err != nil { + return + } + a := &root.Device + if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") { + err = errors.New("no InternetGatewayDevice") + return + } + b := getChildDevice(a, "WANDevice:1") + if b == nil { + err = errors.New("no WANDevice") + return + } + c := getChildDevice(b, "WANConnectionDevice:1") + if c == nil { + err = errors.New("no WANConnectionDevice") + return + } + d := getChildService(c, "WANIPConnection:1") + if d == nil { + // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice, + // instead of under WanConnectionDevice + d = getChildService(b, "WANIPConnection:1") + + if d == nil { + err = errors.New("no WANIPConnection") + return + } + } + // Extract the domain name, which isn't always 'schemas-upnp-org' + urnDomain = strings.Split(d.ServiceType, ":")[1] + url = combineURL(rootURL, d.ControlURL) + return url, urnDomain, err +} + +// combineURL appends subURL onto rootURL. +func combineURL(rootURL, subURL string) string { + protocolEnd := "://" + protoEndIndex := strings.Index(rootURL, protocolEnd) + a := rootURL[protoEndIndex+len(protocolEnd):] + rootIndex := strings.Index(a, "/") + return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL +} + +// soapBody represents the element in a SOAP reply. +// fields we don't care about are elided. +type soapBody struct { + XMLName xml.Name `xml:"Body"` + Data []byte `xml:",innerxml"` +} + +// soapEnvelope represents the element in a SOAP reply. +// fields we don't care about are elided. +type soapEnvelope struct { + XMLName xml.Name `xml:"Envelope"` + Body soapBody `xml:"Body"` +} + +// soapRequests performs a soap request with the given parameters and returns +// the xml replied stripped of the soap headers. in the case that the request is +// unsuccessful the an error is returned. +func soapRequest(url, function, message, domain string) (replyXML []byte, err error) { + fullMessage := "" + + "\r\n" + + "" + message + "" + + req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") + req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") + req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"") + req.Header.Set("Connection", "Close") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + + r, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + if r.Body != nil { + defer r.Body.Close() + } + + if r.StatusCode >= 400 { + err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) + r = nil + return + } + var reply soapEnvelope + err = xml.NewDecoder(r.Body).Decode(&reply) + if err != nil { + return nil, err + } + return reply.Body.Data, nil +} + +// getExternalIPAddressResponse represents the XML response to a +// GetExternalIPAddress SOAP request. +type getExternalIPAddressResponse struct { + XMLName xml.Name `xml:"GetExternalIPAddressResponse"` + ExternalIPAddress string `xml:"NewExternalIPAddress"` +} + +// GetExternalAddress implements the NAT interface by fetching the external IP +// from the UPnP router. +func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { + message := "\r\n" + response, err := soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) + if err != nil { + return nil, err + } + + var reply getExternalIPAddressResponse + err = xml.Unmarshal(response, &reply) + if err != nil { + return nil, err + } + + addr = net.ParseIP(reply.ExternalIPAddress) + if addr == nil { + return nil, errors.New("unable to parse ip address") + } + return addr, nil +} + +// AddPortMapping implements the NAT interface by setting up a port forwarding +// from the UPnP router to the local machine with the given ports and protocol. +func (n *upnpNAT) AddPortMapping(protocol string, internalIP net.IP, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { + // A single concatenation would break ARM compilation. + message := "\r\n" + + "" + strconv.Itoa(externalPort) + message += "" + strings.ToUpper(protocol) + "" + message += "" + strconv.Itoa(internalPort) + "" + + "" + internalIP.String() + "" + + "1" + message += description + + "" + strconv.Itoa(timeout) + + "" + + response, err := soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) + if err != nil { + return + } + + // TODO: check response to see if the port was forwarded + // If the port was not wildcard we don't get an reply with the port in it. Not sure about wildcard yet. miniupnpc just checks for error codes here. + mappedExternalPort = externalPort + _ = response + + return mappedExternalPort, err +} + +// DeletePortMapping implements the NAT interface by removing up a port forwarding +// from the UPnP router to the local machine with the given ports and. +func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { + + message := "\r\n" + + "" + strconv.Itoa(externalPort) + + "" + strings.ToUpper(protocol) + "" + + "" + + response, err := soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) + if err != nil { + return + } + + // TODO: check response to see if the port was deleted + // log.Println(message, response) + _ = response + return +}