From 3f503ffc7f85287fc3716afb704f90a1a4e7b21b Mon Sep 17 00:00:00 2001 From: obscuren Date: Sun, 2 Feb 2014 19:22:39 +0100 Subject: [PATCH] Implemented support for UPnP --- ethereum.go | 62 +++++++++++++++ nat.go | 12 +++ natpmp.go | 54 +++++++++++++ upnp.go => natupnp.go | 179 +++++++++++++----------------------------- 4 files changed, 182 insertions(+), 125 deletions(-) create mode 100644 nat.go create mode 100644 natpmp.go rename upnp.go => natupnp.go (56%) diff --git a/ethereum.go b/ethereum.go index ae0f2eed0..83243e23c 100644 --- a/ethereum.go +++ b/ethereum.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/ethwire-go" "log" "net" + "strconv" "sync" "sync/atomic" "time" @@ -29,6 +30,7 @@ const ( type Ethereum struct { // Channel for shutting down the ethereum shutdownChan chan bool + quit chan bool // DB interface //db *ethdb.LDBDatabase db *ethdb.MemDatabase @@ -48,6 +50,8 @@ type Ethereum struct { // Capabilities for outgoing peers serverCaps Caps + + nat NAT } func New(caps Caps) (*Ethereum, error) { @@ -57,15 +61,30 @@ func New(caps Caps) (*Ethereum, error) { return nil, err } + /* + gateway := net.ParseIP("192.168.192.1") + nat := NewNatPMP(gateway) + port, err := nat.AddPortMapping("tcp", 30303, 30303, "", 60) + log.Println(port, err) + */ + + nat, err := Discover() + if err != nil { + log.Println("UPnP failed", err) + return nil, err + } + ethutil.Config.Db = db nonce, _ := ethutil.RandomUint64() ethereum := &Ethereum{ shutdownChan: make(chan bool), + quit: make(chan bool), db: db, peers: list.New(), Nonce: nonce, serverCaps: caps, + nat: nat, } ethereum.TxPool = ethchain.NewTxPool() ethereum.TxPool.Speaker = ethereum @@ -217,6 +236,8 @@ func (s *Ethereum) Start() { go s.peerHandler(ln) } + go s.upnpUpdateThread() + // Start the reaping processes go s.ReapDeadPeerHandler() @@ -245,6 +266,8 @@ func (s *Ethereum) Stop() { p.Stop() }) + close(s.quit) + s.shutdownChan <- true s.TxPool.Stop() @@ -254,3 +277,42 @@ func (s *Ethereum) Stop() { func (s *Ethereum) WaitForShutdown() { <-s.shutdownChan } + +func (s *Ethereum) upnpUpdateThread() { + // Go off immediately to prevent code duplication, thereafter we renew + // lease every 15 minutes. + timer := time.NewTimer(0 * time.Second) + lport, _ := strconv.ParseInt("30303", 10, 16) + first := true +out: + for { + select { + case <-timer.C: + listenPort, err := s.nat.AddPortMapping("TCP", int(lport), int(lport), "eth listen port", 20*60) + if err != nil { + log.Println("can't add UPnP port mapping:", err) + break out + } + if first && err == nil { + externalip, err := s.nat.GetExternalAddress() + if err != nil { + log.Println("UPnP can't get external address:", err) + continue out + } + log.Println("Successfully bound via UPnP to", externalip, listenPort) + first = false + } + timer.Reset(time.Minute * 15) + case <-s.quit: + break out + } + } + + timer.Stop() + + if err := s.nat.DeletePortMapping("TCP", int(lport), int(lport)); err != nil { + log.Println("unable to remove UPnP port mapping:", err) + } else { + log.Println("succesfully disestablished UPnP port mapping") + } +} diff --git a/nat.go b/nat.go new file mode 100644 index 000000000..999308eb2 --- /dev/null +++ b/nat.go @@ -0,0 +1,12 @@ +package eth + +import ( + "net" +) + +// protocol is either "udp" or "tcp" +type NAT interface { + GetExternalAddress() (addr net.IP, err error) + AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) + DeletePortMapping(protocol string, externalPort, internalPort int) (err error) +} diff --git a/natpmp.go b/natpmp.go new file mode 100644 index 000000000..9a1fb652b --- /dev/null +++ b/natpmp.go @@ -0,0 +1,54 @@ +package eth + +import ( + natpmp "code.google.com/p/go-nat-pmp" + "fmt" + "net" +) + +// Adapt the NAT-PMP protocol to the NAT interface + +// TODO: +// + Register for changes to the external address. +// + Re-register port mapping when router reboots. +// + A mechanism for keeping a port mapping registered. + +type natPMPClient struct { + client *natpmp.Client +} + +func NewNatPMP(gateway net.IP) (nat NAT) { + return &natPMPClient{natpmp.NewClient(gateway)} +} + +func (n *natPMPClient) GetExternalAddress() (addr net.IP, err error) { + response, err := n.client.GetExternalAddress() + if err != nil { + return + } + ip := response.ExternalIPAddress + addr = net.IPv4(ip[0], ip[1], ip[2], ip[3]) + return +} + +func (n *natPMPClient) AddPortMapping(protocol string, externalPort, internalPort int, + description string, timeout int) (mappedExternalPort int, err error) { + if timeout <= 0 { + err = fmt.Errorf("timeout must not be <= 0") + return + } + // Note order of port arguments is switched between our AddPortMapping and the client's AddPortMapping. + response, err := n.client.AddPortMapping(protocol, internalPort, externalPort, timeout) + if err != nil { + return + } + mappedExternalPort = int(response.MappedExternalPort) + return +} + +func (n *natPMPClient) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { + // To destroy a mapping, send an add-port with + // an internalPort of the internal port to destroy, an external port of zero and a time of zero. + _, err = n.client.AddPortMapping(protocol, internalPort, 0, 0) + return +} diff --git a/upnp.go b/natupnp.go similarity index 56% rename from upnp.go rename to natupnp.go index c84ed04f8..e4072d0dd 100644 --- a/upnp.go +++ b/natupnp.go @@ -1,34 +1,5 @@ package eth -// Upnp code taken from Taipei Torrent license is below: -// Copyright (c) 2010 Jack Palevich. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - // Just enough UPnP to be able to forward ports // @@ -44,27 +15,11 @@ import ( "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 externalport to - // internal port with description lasting for timeout. - AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) - // Remove a previously added port mapping from externalport to - // internal port. - DeletePortMapping(protocol string, externalPort, internalPort int) (err error) -} - type upnpNAT struct { serviceURL string ourIP 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 { @@ -77,7 +32,7 @@ func Discover() (nat NAT, err error) { socket := conn.(*net.UDPConn) defer socket.Close() - err = socket.SetDeadline(time.Now().Add(3 * time.Second)) + err = socket.SetDeadline(time.Now().Add(10 * time.Second)) if err != nil { return } @@ -134,7 +89,7 @@ func Discover() (nat NAT, err error) { nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP} return } - err = errors.New("UPnP port discovery failed") + err = errors.New("UPnP port discovery failed.") return } @@ -190,39 +145,38 @@ type root struct { 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 d.DeviceList.Device[i].DeviceType == deviceType { - return &d.DeviceList.Device[i] + dl := d.DeviceList.Device + for i := 0; i < len(dl); i++ { + if dl[i].DeviceType == deviceType { + return &dl[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 d.ServiceList.Service[i].ServiceType == serviceType { - return &d.ServiceList.Service[i] + sl := d.ServiceList.Service + for i := 0; i < len(sl); i++ { + if sl[i].ServiceType == serviceType { + return &sl[i] } } return nil } -// getOurIP returns a best guess at what the local IP is. func getOurIP() (ip string, err error) { hostname, err := os.Hostname() if err != nil { return } - return net.LookupCNAME(hostname) + p, err := net.LookupIP(hostname) + if err != nil && len(p) > 0 { + return + } + return p[0].String(), 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 string, err error) { r, err := http.Get(rootURL) if err != nil { @@ -235,34 +189,34 @@ func getServiceURL(rootURL string) (url string, err error) { } var root root err = xml.NewDecoder(r.Body).Decode(&root) + if err != nil { return } a := &root.Device if a.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" { - err = errors.New("no InternetGatewayDevice") + err = errors.New("No InternetGatewayDevice") return } b := getChildDevice(a, "urn:schemas-upnp-org:device:WANDevice:1") if b == nil { - err = errors.New("no WANDevice") + err = errors.New("No WANDevice") return } c := getChildDevice(b, "urn:schemas-upnp-org:device:WANConnectionDevice:1") if c == nil { - err = errors.New("no WANConnectionDevice") + err = errors.New("No WANConnectionDevice") return } d := getChildService(c, "urn:schemas-upnp-org:service:WANIPConnection:1") if d == nil { - err = errors.New("no WANIPConnection") + err = errors.New("No WANIPConnection") return } url = combineURL(rootURL, d.ControlURL) return } -// combineURL appends subURL onto rootURL. func combineURL(rootURL, subURL string) string { protocolEnd := "://" protoEndIndex := strings.Index(rootURL, protocolEnd) @@ -271,24 +225,7 @@ func combineURL(rootURL, subURL string) string { 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 string) (replyXML []byte, err error) { +func soapRequest(url, function, message string) (r *http.Response, err error) { fullMessage := "" + "\r\n" + "" + message + "" @@ -305,10 +242,10 @@ func soapRequest(url, function, message string) (replyXML []byte, err error) { 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 - } + // log.Stderr("soapRequest ", req) + //fmt.Println(fullMessage) + + r, err = http.DefaultClient.Do(req) if r.Body != nil { defer r.Body.Close() } @@ -319,45 +256,39 @@ func soapRequest(url, function, message string) (replyXML []byte, err error) { r = nil return } - var reply soapEnvelope - err = xml.NewDecoder(r.Body).Decode(&reply) + return +} + +type statusInfo struct { + externalIpAddress string +} + +func (n *upnpNAT) getStatusInfo() (info statusInfo, err error) { + + message := "\r\n" + + "" + + var response *http.Response + response, err = soapRequest(n.serviceURL, "GetStatusInfo", message) if err != nil { - return nil, err + return } - return reply.Body.Data, nil + + // TODO: Write a soap reply parser. It has to eat the Body and envelope tags... + + response.Body.Close() + return } -// 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) + info, err := n.getStatusInfo() if err != nil { - return nil, err + return } - - 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 + addr = net.ParseIP(info.externalIpAddress) + return } -// 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, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { // A single concatenation would break ARM compilation. message := "\r\n" + @@ -370,22 +301,19 @@ func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int "" + strconv.Itoa(timeout) + "" - response, err := soapRequest(n.serviceURL, "AddPortMapping", message) + var response *http.Response + response, err = soapRequest(n.serviceURL, "AddPortMapping", message) 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. + // log.Println(message, response) mappedExternalPort = externalPort _ = response return } -// AddPortMapping 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" + @@ -393,7 +321,8 @@ func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort "" + protocol + "" + "" - response, err := soapRequest(n.serviceURL, "DeletePortMapping", message) + var response *http.Response + response, err = soapRequest(n.serviceURL, "DeletePortMapping", message) if err != nil { return }