235 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			235 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Package nat provides access to common port mapping protocols.
 | 
						|
package nat
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"net"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/ethereum/go-ethereum/logger"
 | 
						|
	"github.com/ethereum/go-ethereum/logger/glog"
 | 
						|
	"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        = 20 * time.Minute
 | 
						|
	mapUpdateInterval = 15 * 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) {
 | 
						|
	refresh := time.NewTimer(mapUpdateInterval)
 | 
						|
	defer func() {
 | 
						|
		refresh.Stop()
 | 
						|
		glog.V(logger.Debug).Infof("Deleting port mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
 | 
						|
		m.DeleteMapping(protocol, extport, intport)
 | 
						|
	}()
 | 
						|
	glog.V(logger.Debug).Infof("add mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
 | 
						|
	if err := m.AddMapping(protocol, intport, extport, name, mapTimeout); err != nil {
 | 
						|
		glog.V(logger.Error).Infof("mapping error: %v\n", err)
 | 
						|
	}
 | 
						|
	for {
 | 
						|
		select {
 | 
						|
		case _, ok := <-c:
 | 
						|
			if !ok {
 | 
						|
				return
 | 
						|
			}
 | 
						|
		case <-refresh.C:
 | 
						|
			glog.V(logger.Detail).Infof("refresh mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
 | 
						|
			if err := m.AddMapping(protocol, intport, extport, name, mapTimeout); err != nil {
 | 
						|
				glog.V(logger.Error).Infof("mapping error: %v\n", err)
 | 
						|
			}
 | 
						|
			refresh.Reset(mapUpdateInterval)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// 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.
 | 
						|
func ExtIP(ip net.IP) Interface {
 | 
						|
	if ip == nil {
 | 
						|
		panic("IP must not be nil")
 | 
						|
	}
 | 
						|
	return extIP(ip)
 | 
						|
}
 | 
						|
 | 
						|
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
 | 
						|
	done <-chan Interface
 | 
						|
 | 
						|
	mu    sync.Mutex
 | 
						|
	found Interface
 | 
						|
}
 | 
						|
 | 
						|
func startautodisc(what string, doit func() Interface) Interface {
 | 
						|
	// TODO: monitor network configuration and rerun doit when it changes.
 | 
						|
	done := make(chan Interface)
 | 
						|
	ad := &autodisc{what: what, done: done}
 | 
						|
	go func() { done <- doit(); close(done) }()
 | 
						|
	return ad
 | 
						|
}
 | 
						|
 | 
						|
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()
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (n *autodisc) wait() error {
 | 
						|
	n.mu.Lock()
 | 
						|
	found := n.found
 | 
						|
	n.mu.Unlock()
 | 
						|
	if found != nil {
 | 
						|
		// already discovered
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if found = <-n.done; found == nil {
 | 
						|
		return errors.New("no devices discovered")
 | 
						|
	}
 | 
						|
	n.mu.Lock()
 | 
						|
	n.found = found
 | 
						|
	n.mu.Unlock()
 | 
						|
	return nil
 | 
						|
}
 |