This adds a lock around requests because some routers can't handle concurrent requests. Requests are also rate-limited. The Map function request a new mapping exactly when the map timeout occurs instead of 5 minutes earlier. This should prevent duplicate mappings.
		
			
				
	
	
		
			239 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			239 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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 <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| // Package nat provides access to common network port mapping protocols.
 | |
| package nat
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/ethereum/go-ethereum/log"
 | |
| 	natpmp "github.com/jackpal/go-nat-pmp"
 | |
| )
 | |
| 
 | |
| // An implementation of nat.Interface can map local ports to ports
 | |
| // accessible from the Internet.
 | |
| type Interface interface {
 | |
| 	// These methods manage a mapping between a port on the local
 | |
| 	// machine to a port that can be connected to from the internet.
 | |
| 	//
 | |
| 	// protocol is "UDP" or "TCP". Some implementations allow setting
 | |
| 	// a display name for the mapping. The mapping may be removed by
 | |
| 	// the gateway when its lifetime ends.
 | |
| 	AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error
 | |
| 	DeleteMapping(protocol string, extport, intport int) error
 | |
| 
 | |
| 	// This method should return the external (Internet-facing)
 | |
| 	// address of the gateway device.
 | |
| 	ExternalIP() (net.IP, error)
 | |
| 
 | |
| 	// Should return name of the method. This is used for logging.
 | |
| 	String() string
 | |
| }
 | |
| 
 | |
| // Parse parses a NAT interface description.
 | |
| // The following formats are currently accepted.
 | |
| // Note that mechanism names are not case-sensitive.
 | |
| //
 | |
| //     "" or "none"         return nil
 | |
| //     "extip:77.12.33.4"   will assume the local machine is reachable on the given IP
 | |
| //     "any"                uses the first auto-detected mechanism
 | |
| //     "upnp"               uses the Universal Plug and Play protocol
 | |
| //     "pmp"                uses NAT-PMP with an auto-detected gateway address
 | |
| //     "pmp:192.168.0.1"    uses NAT-PMP with the given gateway address
 | |
| func Parse(spec string) (Interface, error) {
 | |
| 	var (
 | |
| 		parts = strings.SplitN(spec, ":", 2)
 | |
| 		mech  = strings.ToLower(parts[0])
 | |
| 		ip    net.IP
 | |
| 	)
 | |
| 	if len(parts) > 1 {
 | |
| 		ip = net.ParseIP(parts[1])
 | |
| 		if ip == nil {
 | |
| 			return nil, errors.New("invalid IP address")
 | |
| 		}
 | |
| 	}
 | |
| 	switch mech {
 | |
| 	case "", "none", "off":
 | |
| 		return nil, nil
 | |
| 	case "any", "auto", "on":
 | |
| 		return Any(), nil
 | |
| 	case "extip", "ip":
 | |
| 		if ip == nil {
 | |
| 			return nil, errors.New("missing IP address")
 | |
| 		}
 | |
| 		return ExtIP(ip), nil
 | |
| 	case "upnp":
 | |
| 		return UPnP(), nil
 | |
| 	case "pmp", "natpmp", "nat-pmp":
 | |
| 		return PMP(ip), nil
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("unknown mechanism %q", parts[0])
 | |
| 	}
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	mapTimeout = 10 * time.Minute
 | |
| )
 | |
| 
 | |
| // Map adds a port mapping on m and keeps it alive until c is closed.
 | |
| // This function is typically invoked in its own goroutine.
 | |
| func Map(m Interface, c <-chan struct{}, protocol string, extport, intport int, name string) {
 | |
| 	log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m)
 | |
| 	refresh := time.NewTimer(mapTimeout)
 | |
| 	defer func() {
 | |
| 		refresh.Stop()
 | |
| 		log.Debug("Deleting port mapping")
 | |
| 		m.DeleteMapping(protocol, extport, intport)
 | |
| 	}()
 | |
| 	if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
 | |
| 		log.Debug("Couldn't add port mapping", "err", err)
 | |
| 	} else {
 | |
| 		log.Info("Mapped network port")
 | |
| 	}
 | |
| 	for {
 | |
| 		select {
 | |
| 		case _, ok := <-c:
 | |
| 			if !ok {
 | |
| 				return
 | |
| 			}
 | |
| 		case <-refresh.C:
 | |
| 			log.Trace("Refreshing port mapping")
 | |
| 			if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
 | |
| 				log.Debug("Couldn't add port mapping", "err", err)
 | |
| 			}
 | |
| 			refresh.Reset(mapTimeout)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ExtIP assumes that the local machine is reachable on the given
 | |
| // external IP address, and that any required ports were mapped manually.
 | |
| // Mapping operations will not return an error but won't actually do anything.
 | |
| type ExtIP net.IP
 | |
| 
 | |
| func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
 | |
| func (n ExtIP) String() string              { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }
 | |
| 
 | |
| // These do nothing.
 | |
| 
 | |
| func (ExtIP) AddMapping(string, int, int, string, time.Duration) error { return nil }
 | |
| func (ExtIP) DeleteMapping(string, int, int) error                     { return nil }
 | |
| 
 | |
| // Any returns a port mapper that tries to discover any supported
 | |
| // mechanism on the local network.
 | |
| func Any() Interface {
 | |
| 	// TODO: attempt to discover whether the local machine has an
 | |
| 	// Internet-class address. Return ExtIP in this case.
 | |
| 	return startautodisc("UPnP or NAT-PMP", func() Interface {
 | |
| 		found := make(chan Interface, 2)
 | |
| 		go func() { found <- discoverUPnP() }()
 | |
| 		go func() { found <- discoverPMP() }()
 | |
| 		for i := 0; i < cap(found); i++ {
 | |
| 			if c := <-found; c != nil {
 | |
| 				return c
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // UPnP returns a port mapper that uses UPnP. It will attempt to
 | |
| // discover the address of your router using UDP broadcasts.
 | |
| func UPnP() Interface {
 | |
| 	return startautodisc("UPnP", discoverUPnP)
 | |
| }
 | |
| 
 | |
| // PMP returns a port mapper that uses NAT-PMP. The provided gateway
 | |
| // address should be the IP of your router. If the given gateway
 | |
| // address is nil, PMP will attempt to auto-discover the router.
 | |
| func PMP(gateway net.IP) Interface {
 | |
| 	if gateway != nil {
 | |
| 		return &pmp{gw: gateway, c: natpmp.NewClient(gateway)}
 | |
| 	}
 | |
| 	return startautodisc("NAT-PMP", discoverPMP)
 | |
| }
 | |
| 
 | |
| // autodisc represents a port mapping mechanism that is still being
 | |
| // auto-discovered. Calls to the Interface methods on this type will
 | |
| // wait until the discovery is done and then call the method on the
 | |
| // discovered mechanism.
 | |
| //
 | |
| // This type is useful because discovery can take a while but we
 | |
| // want return an Interface value from UPnP, PMP and Auto immediately.
 | |
| type autodisc struct {
 | |
| 	what string // type of interface being autodiscovered
 | |
| 	once sync.Once
 | |
| 	doit func() Interface
 | |
| 
 | |
| 	mu    sync.Mutex
 | |
| 	found Interface
 | |
| }
 | |
| 
 | |
| func startautodisc(what string, doit func() Interface) Interface {
 | |
| 	// TODO: monitor network configuration and rerun doit when it changes.
 | |
| 	return &autodisc{what: what, doit: doit}
 | |
| }
 | |
| 
 | |
| func (n *autodisc) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error {
 | |
| 	if err := n.wait(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return n.found.AddMapping(protocol, extport, intport, name, lifetime)
 | |
| }
 | |
| 
 | |
| func (n *autodisc) DeleteMapping(protocol string, extport, intport int) error {
 | |
| 	if err := n.wait(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return n.found.DeleteMapping(protocol, extport, intport)
 | |
| }
 | |
| 
 | |
| func (n *autodisc) ExternalIP() (net.IP, error) {
 | |
| 	if err := n.wait(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return n.found.ExternalIP()
 | |
| }
 | |
| 
 | |
| func (n *autodisc) String() string {
 | |
| 	n.mu.Lock()
 | |
| 	defer n.mu.Unlock()
 | |
| 	if n.found == nil {
 | |
| 		return n.what
 | |
| 	} else {
 | |
| 		return n.found.String()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // wait blocks until auto-discovery has been performed.
 | |
| func (n *autodisc) wait() error {
 | |
| 	n.once.Do(func() {
 | |
| 		n.mu.Lock()
 | |
| 		n.found = n.doit()
 | |
| 		n.mu.Unlock()
 | |
| 	})
 | |
| 	if n.found == nil {
 | |
| 		return fmt.Errorf("no %s router discovered", n.what)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 |