p2p/nat: new package for port mapping stuff
I have verified that UPnP and NAT-PMP work against an older version of the MiniUPnP daemon running on pfSense. This code is kind of hard to test automatically.
This commit is contained in:
		
							parent
							
								
									4242b05462
								
							
						
					
					
						commit
						1543833ca0
					
				
							
								
								
									
										235
									
								
								p2p/nat/nat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								p2p/nat/nat.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,235 @@ | ||||
| // 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/jackpal/go-nat-pmp" | ||||
| ) | ||||
| 
 | ||||
| var log = logger.NewLogger("P2P NAT") | ||||
| 
 | ||||
| // 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() | ||||
| 		log.Debugf("Deleting port mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m) | ||||
| 		m.DeleteMapping(protocol, extport, intport) | ||||
| 	}() | ||||
| 	log.Debugf("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 { | ||||
| 		log.Errorf("mapping error: %v\n", err) | ||||
| 	} | ||||
| 	for { | ||||
| 		select { | ||||
| 		case _, ok := <-c: | ||||
| 			if !ok { | ||||
| 				return | ||||
| 			} | ||||
| 		case <-refresh.C: | ||||
| 			log.DebugDetailf("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 { | ||||
| 				log.Errorf("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 | ||||
| } | ||||
							
								
								
									
										115
									
								
								p2p/nat/natpmp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								p2p/nat/natpmp.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | ||||
| package nat | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/jackpal/go-nat-pmp" | ||||
| ) | ||||
| 
 | ||||
| // natPMPClient adapts the NAT-PMP protocol implementation so it conforms to
 | ||||
| // the common interface.
 | ||||
| type pmp struct { | ||||
| 	gw net.IP | ||||
| 	c  *natpmp.Client | ||||
| } | ||||
| 
 | ||||
| func (n *pmp) String() string { | ||||
| 	return fmt.Sprintf("NAT-PMP(%v)", n.gw) | ||||
| } | ||||
| 
 | ||||
| func (n *pmp) ExternalIP() (net.IP, error) { | ||||
| 	response, err := n.c.GetExternalAddress() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return response.ExternalIPAddress[:], nil | ||||
| } | ||||
| 
 | ||||
| func (n *pmp) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error { | ||||
| 	if lifetime <= 0 { | ||||
| 		return fmt.Errorf("lifetime must not be <= 0") | ||||
| 	} | ||||
| 	// Note order of port arguments is switched between our
 | ||||
| 	// AddMapping and the client's AddPortMapping.
 | ||||
| 	_, err := n.c.AddPortMapping(strings.ToLower(protocol), intport, extport, int(lifetime/time.Second)) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (n *pmp) DeleteMapping(protocol string, extport, intport 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.c.AddPortMapping(strings.ToLower(protocol), intport, 0, 0) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func discoverPMP() Interface { | ||||
| 	// run external address lookups on all potential gateways
 | ||||
| 	gws := potentialGateways() | ||||
| 	found := make(chan *pmp, len(gws)) | ||||
| 	for i := range gws { | ||||
| 		gw := gws[i] | ||||
| 		go func() { | ||||
| 			c := natpmp.NewClient(gw) | ||||
| 			if _, err := c.GetExternalAddress(); err != nil { | ||||
| 				found <- nil | ||||
| 			} else { | ||||
| 				found <- &pmp{gw, c} | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| 	// return the one that responds first.
 | ||||
| 	// discovery needs to be quick, so we stop caring about
 | ||||
| 	// any responses after a very short timeout.
 | ||||
| 	timeout := time.NewTimer(1 * time.Second) | ||||
| 	defer timeout.Stop() | ||||
| 	for _ = range gws { | ||||
| 		select { | ||||
| 		case c := <-found: | ||||
| 			if c != nil { | ||||
| 				return c | ||||
| 			} | ||||
| 		case <-timeout.C: | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	// LAN IP ranges
 | ||||
| 	_, lan10, _  = net.ParseCIDR("10.0.0.0/8") | ||||
| 	_, lan176, _ = net.ParseCIDR("172.16.0.0/12") | ||||
| 	_, lan192, _ = net.ParseCIDR("192.168.0.0/16") | ||||
| ) | ||||
| 
 | ||||
| // TODO: improve this. We currently assume that (on most networks)
 | ||||
| // the router is X.X.X.1 in a local LAN range.
 | ||||
| func potentialGateways() (gws []net.IP) { | ||||
| 	ifaces, err := net.Interfaces() | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	for _, iface := range ifaces { | ||||
| 		ifaddrs, err := iface.Addrs() | ||||
| 		if err != nil { | ||||
| 			return gws | ||||
| 		} | ||||
| 		for _, addr := range ifaddrs { | ||||
| 			switch x := addr.(type) { | ||||
| 			case *net.IPNet: | ||||
| 				if lan10.Contains(x.IP) || lan176.Contains(x.IP) || lan192.Contains(x.IP) { | ||||
| 					ip := x.IP.Mask(x.Mask).To4() | ||||
| 					if ip != nil { | ||||
| 						ip[3] = ip[3] | 0x01 | ||||
| 						gws = append(gws, ip) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return gws | ||||
| } | ||||
							
								
								
									
										149
									
								
								p2p/nat/natupnp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								p2p/nat/natupnp.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,149 @@ | ||||
| package nat | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/fjl/goupnp" | ||||
| 	"github.com/fjl/goupnp/dcps/internetgateway1" | ||||
| 	"github.com/fjl/goupnp/dcps/internetgateway2" | ||||
| ) | ||||
| 
 | ||||
| type upnp struct { | ||||
| 	dev     *goupnp.RootDevice | ||||
| 	service string | ||||
| 	client  upnpClient | ||||
| } | ||||
| 
 | ||||
| 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) ExternalIP() (addr net.IP, err error) { | ||||
| 	ipString, err := n.client.GetExternalIPAddress() | ||||
| 	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) | ||||
| 	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 { | ||||
| 			switch x := addr.(type) { | ||||
| 			case *net.IPNet: | ||||
| 				if 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.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol)) | ||||
| } | ||||
| 
 | ||||
| func (n *upnp) String() string { | ||||
| 	return "UPNP " + n.service | ||||
| } | ||||
| 
 | ||||
| // 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(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp { | ||||
| 		switch sc.Service.ServiceType { | ||||
| 		case internetgateway1.URN_WANIPConnection_1: | ||||
| 			return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{sc}} | ||||
| 		case internetgateway1.URN_WANPPPConnection_1: | ||||
| 			return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{sc}} | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	// IGDv2
 | ||||
| 	go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp { | ||||
| 		switch sc.Service.ServiceType { | ||||
| 		case internetgateway2.URN_WANIPConnection_1: | ||||
| 			return &upnp{dev, "IGDv2-IP1", &internetgateway2.WANIPConnection1{sc}} | ||||
| 		case internetgateway2.URN_WANIPConnection_2: | ||||
| 			return &upnp{dev, "IGDv2-IP2", &internetgateway2.WANIPConnection2{sc}} | ||||
| 		case internetgateway2.URN_WANPPPConnection_1: | ||||
| 			return &upnp{dev, "IGDv2-PPP1", &internetgateway2.WANPPPConnection1{sc}} | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	for i := 0; i < cap(found); i++ { | ||||
| 		if c := <-found; c != nil { | ||||
| 			return c | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) { | ||||
| 	devs, err := goupnp.DiscoverDevices(target) | ||||
| 	if err != 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{service.NewSOAPClient(), devs[i].Root, service} | ||||
| 			upnp := matcher(devs[i].Root, sc) | ||||
| 			if upnp == nil { | ||||
| 				return | ||||
| 			} | ||||
| 			// check whether port mapping is enabled
 | ||||
| 			if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat { | ||||
| 				return | ||||
| 			} | ||||
| 			out <- upnp | ||||
| 			found = true | ||||
| 		}) | ||||
| 	} | ||||
| 	if !found { | ||||
| 		out <- nil | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user