// Copyright 2015 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // The go-ethereum library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . package nat import ( "errors" "fmt" "net" "strings" "sync" "time" "github.com/huin/goupnp" "github.com/huin/goupnp/dcps/internetgateway1" "github.com/huin/goupnp/dcps/internetgateway2" ) const ( soapRequestTimeout = 3 * time.Second rateLimit = 200 * time.Millisecond ) type upnp struct { dev *goupnp.RootDevice service string client upnpClient mu sync.Mutex lastReqTime time.Time } type upnpClient interface { GetExternalIPAddress() (string, error) AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error DeletePortMapping(string, uint16, string) error GetNATRSIPStatus() (sip bool, nat bool, err error) } func (n *upnp) natEnabled() bool { var ok bool var err error n.withRateLimit(func() error { _, ok, err = n.client.GetNATRSIPStatus() return err }) return err == nil && ok } func (n *upnp) ExternalIP() (addr net.IP, err error) { var ipString string n.withRateLimit(func() error { ipString, err = n.client.GetExternalIPAddress() return err }) if err != nil { return nil, err } ip := net.ParseIP(ipString) if ip == nil { return nil, errors.New("bad IP in response") } return ip, nil } func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error { ip, err := n.internalAddress() if err != nil { return nil } protocol = strings.ToUpper(protocol) lifetimeS := uint32(lifetime / time.Second) n.DeleteMapping(protocol, extport, intport) return n.withRateLimit(func() error { return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS) }) } func (n *upnp) internalAddress() (net.IP, error) { devaddr, err := net.ResolveUDPAddr("udp4", n.dev.URLBase.Host) if err != nil { return nil, err } ifaces, err := net.Interfaces() if err != nil { return nil, err } for _, iface := range ifaces { addrs, err := iface.Addrs() if err != nil { return nil, err } for _, addr := range addrs { if x, ok := addr.(*net.IPNet); ok && x.Contains(devaddr.IP) { return x.IP, nil } } } return nil, fmt.Errorf("could not find local address in same net as %v", devaddr) } func (n *upnp) DeleteMapping(protocol string, extport, intport int) error { return n.withRateLimit(func() error { return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol)) }) } func (n *upnp) String() string { return "UPNP " + n.service } func (n *upnp) withRateLimit(fn func() error) error { n.mu.Lock() defer n.mu.Unlock() lastreq := time.Since(n.lastReqTime) if lastreq < rateLimit { time.Sleep(rateLimit - lastreq) } err := fn() n.lastReqTime = time.Now() return err } // discoverUPnP searches for Internet Gateway Devices // and returns the first one it can find on the local network. func discoverUPnP() Interface { found := make(chan *upnp, 2) // IGDv1 go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(sc goupnp.ServiceClient) *upnp { switch sc.Service.ServiceType { case internetgateway1.URN_WANIPConnection_1: return &upnp{service: "IGDv1-IP1", client: &internetgateway1.WANIPConnection1{ServiceClient: sc}} case internetgateway1.URN_WANPPPConnection_1: return &upnp{service: "IGDv1-PPP1", client: &internetgateway1.WANPPPConnection1{ServiceClient: sc}} } return nil }) // IGDv2 go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(sc goupnp.ServiceClient) *upnp { switch sc.Service.ServiceType { case internetgateway2.URN_WANIPConnection_1: return &upnp{service: "IGDv2-IP1", client: &internetgateway2.WANIPConnection1{ServiceClient: sc}} case internetgateway2.URN_WANIPConnection_2: return &upnp{service: "IGDv2-IP2", client: &internetgateway2.WANIPConnection2{ServiceClient: sc}} case internetgateway2.URN_WANPPPConnection_1: return &upnp{service: "IGDv2-PPP1", client: &internetgateway2.WANPPPConnection1{ServiceClient: sc}} } return nil }) for i := 0; i < cap(found); i++ { if c := <-found; c != nil { return c } } return nil } // finds devices matching the given target and calls matcher for all // advertised services of each device. The first non-nil service found // is sent into out. If no service matched, nil is sent. func discover(out chan<- *upnp, target string, matcher func(goupnp.ServiceClient) *upnp) { devs, err := goupnp.DiscoverDevices(target) if err != nil { out <- nil return } found := false for i := 0; i < len(devs) && !found; i++ { if devs[i].Root == nil { continue } devs[i].Root.Device.VisitServices(func(service *goupnp.Service) { if found { return } // check for a matching IGD service sc := goupnp.ServiceClient{ SOAPClient: service.NewSOAPClient(), RootDevice: devs[i].Root, Location: devs[i].Location, Service: service, } sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout upnp := matcher(sc) if upnp == nil { return } upnp.dev = devs[i].Root // check whether port mapping is enabled if upnp.natEnabled() { out <- upnp found = true } }) } if !found { out <- nil } }