forked from mirror/go-ethereum
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.poc8
parent
4242b05462
commit
1543833ca0
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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