diff --git a/p2p/nat/nat.go b/p2p/nat/nat.go
new file mode 100644
index 0000000000000000000000000000000000000000..12d355ba18ee74db209ff75d340334d02aa553aa
--- /dev/null
+++ b/p2p/nat/nat.go
@@ -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
+}
diff --git a/p2p/nat/natpmp.go b/p2p/nat/natpmp.go
new file mode 100644
index 0000000000000000000000000000000000000000..f249c60732cefb832f977c44dcad09ae7e100b31
--- /dev/null
+++ b/p2p/nat/natpmp.go
@@ -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
+}
diff --git a/p2p/nat/natupnp.go b/p2p/nat/natupnp.go
new file mode 100644
index 0000000000000000000000000000000000000000..25cbc0d71156102d0062b226d764c79cc3c64788
--- /dev/null
+++ b/p2p/nat/natupnp.go
@@ -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
+	}
+}