nat: introduce new NAT traversal package with UPnP/NAT-PMP implementations

In this commit, we introduce a new package `nat`. This package is
responsible for handling the different techniques for NAT traversals.
Specifically, we have implemented UPnP and NAT-PMP support. This will
allow users to easily advertise their nodes to the network as long as
their devices are behind a single NAT. Devices behind multiple NATs are
not supported.
This commit is contained in:
Wilmer Paulino 2018-04-19 23:34:47 -04:00
parent 8651b4a422
commit 377e770db4
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
3 changed files with 287 additions and 0 deletions

117
nat/pmp.go Normal file

@ -0,0 +1,117 @@
package nat
import (
"fmt"
"net"
"sync"
"time"
"github.com/jackpal/gateway"
natpmp "github.com/jackpal/go-nat-pmp"
)
// Compile-time check to ensure PMP implements the Traversal interface.
var _ Traversal = (*PMP)(nil)
// PMP is a concrete implementation of the Traversal interface that uses the
// NAT-PMP technique.
type PMP struct {
client *natpmp.Client
forwardedPortsMtx sync.Mutex
forwardedPorts map[uint16]struct{}
}
// DiscoverPMP attempts to scan the local network for a NAT-PMP enabled device
// within the given timeout.
func DiscoverPMP(timeout time.Duration) (*PMP, error) {
// Retrieve the gateway IP address of the local network.
gatewayIP, err := gateway.DiscoverGateway()
if err != nil {
return nil, err
}
pmp := &PMP{
client: natpmp.NewClientWithTimeout(gatewayIP, timeout),
forwardedPorts: make(map[uint16]struct{}),
}
// We'll then attempt to retrieve the external IP address of this
// device to ensure it is not behind multiple NATs.
if _, err := pmp.ExternalIP(); err != nil {
return nil, err
}
return pmp, nil
}
// ExternalIP returns the external IP address of the NAT-PMP enabled device.
func (p *PMP) ExternalIP() (net.IP, error) {
res, err := p.client.GetExternalAddress()
if err != nil {
return nil, err
}
ip := net.IP(res.ExternalIPAddress[:])
if isPrivateIP(ip) {
return nil, ErrMultipleNAT
}
return ip, nil
}
// AddPortMapping enables port forwarding for the given port.
func (p *PMP) AddPortMapping(port uint16) error {
p.forwardedPortsMtx.Lock()
defer p.forwardedPortsMtx.Unlock()
if _, exists := p.forwardedPorts[port]; exists {
return nil
}
_, err := p.client.AddPortMapping("tcp", int(port), int(port), 0)
if err != nil {
return err
}
p.forwardedPorts[port] = struct{}{}
return nil
}
// DeletePortMapping disables port forwarding for the given port.
func (p *PMP) DeletePortMapping(port uint16) error {
p.forwardedPortsMtx.Lock()
defer p.forwardedPortsMtx.Unlock()
if _, exists := p.forwardedPorts[port]; !exists {
return fmt.Errorf("port %d is not being forwarded", port)
}
_, err := p.client.AddPortMapping("tcp", int(port), 0, 0)
if err != nil {
return err
}
delete(p.forwardedPorts, port)
return nil
}
// ForwardedPorts returns a list of ports currently being forwarded.
func (p *PMP) ForwardedPorts() []uint16 {
p.forwardedPortsMtx.Lock()
defer p.forwardedPortsMtx.Unlock()
ports := make([]uint16, 0, len(p.forwardedPorts))
for port := range p.forwardedPorts {
ports = append(ports, port)
}
return ports
}
// Name returns the name of the specific NAT traversal technique used.
func (p *PMP) Name() string {
return "NAT-PMP"
}

58
nat/traversal.go Normal file

@ -0,0 +1,58 @@
package nat
import (
"errors"
"net"
)
var (
// private24BitBlock contains the set of private IPv4 addresses within
// the 10.0.0.0/8 adddress space.
private24BitBlock *net.IPNet
// private20BitBlock contains the set of private IPv4 addresses within
// the 172.16.0.0/12 address space.
private20BitBlock *net.IPNet
// private16BitBlock contains the set of private IPv4 addresses within
// the 192.168.0.0/16 address space.
private16BitBlock *net.IPNet
// ErrMultipleNAT is an error returned when multiple NATs have been
// detected.
ErrMultipleNAT = errors.New("multiple NATs detected")
)
func init() {
_, private24BitBlock, _ = net.ParseCIDR("10.0.0.0/8")
_, private20BitBlock, _ = net.ParseCIDR("172.16.0.0/12")
_, private16BitBlock, _ = net.ParseCIDR("192.168.0.0/16")
}
// Traversal is an interface that brings together the different NAT traversal
// techniques.
type Traversal interface {
// ExternalIP returns the external IP address.
ExternalIP() (net.IP, error)
// AddPortMapping adds a port mapping for the given port between the
// private and public addresses.
AddPortMapping(port uint16) error
// DeletePortMapping deletes a port mapping for the given port between
// the private and public addresses.
DeletePortMapping(port uint16) error
// ForwardedPorts returns the ports currently being forwarded using NAT
// traversal.
ForwardedPorts() []uint16
// Name returns the name of the specific NAT traversal technique used.
Name() string
}
// isPrivateIP determines if the IP is private.
func isPrivateIP(ip net.IP) bool {
return private24BitBlock.Contains(ip) ||
private20BitBlock.Contains(ip) || private16BitBlock.Contains(ip)
}

112
nat/upnp.go Normal file

@ -0,0 +1,112 @@
package nat
import (
"context"
"fmt"
"net"
"sync"
upnp "github.com/NebulousLabs/go-upnp"
)
// Compile-time check to ensure UPnP implements the Traversal interface.
var _ Traversal = (*UPnP)(nil)
// UPnP is a concrete implementation of the Traversal interface that uses the
// UPnP technique.
type UPnP struct {
device *upnp.IGD
forwardedPortsMtx sync.Mutex
forwardedPorts map[uint16]struct{}
}
// DiscoverUPnP scans the local network for a UPnP enabled device.
func DiscoverUPnP(ctx context.Context) (*UPnP, error) {
// Scan the local network for a UPnP-enabled device.
device, err := upnp.DiscoverCtx(ctx)
if err != nil {
return nil, err
}
u := &UPnP{
device: device,
forwardedPorts: make(map[uint16]struct{}),
}
// We'll then attempt to retrieve the external IP address of this
// device to ensure it is not behind multiple NATs.
if _, err := u.ExternalIP(); err != nil {
return nil, err
}
return u, nil
}
// ExternalIP returns the external IP address of the UPnP enabled device.
func (u *UPnP) ExternalIP() (net.IP, error) {
ip, err := u.device.ExternalIP()
if err != nil {
return nil, err
}
if isPrivateIP(net.ParseIP(ip)) {
return nil, ErrMultipleNAT
}
return net.ParseIP(ip), nil
}
// AddPortMapping enables port forwarding for the given port.
func (u *UPnP) AddPortMapping(port uint16) error {
u.forwardedPortsMtx.Lock()
defer u.forwardedPortsMtx.Unlock()
if _, exists := u.forwardedPorts[port]; exists {
return nil
}
if err := u.device.Forward(port, ""); err != nil {
return err
}
u.forwardedPorts[port] = struct{}{}
return nil
}
// DeletePortMapping disables port forwarding for the given port.
func (u *UPnP) DeletePortMapping(port uint16) error {
u.forwardedPortsMtx.Lock()
defer u.forwardedPortsMtx.Unlock()
if _, exists := u.forwardedPorts[port]; !exists {
return fmt.Errorf("port %d is not being forwarded", port)
}
if err := u.device.Clear(port); err != nil {
return err
}
delete(u.forwardedPorts, port)
return nil
}
// ForwardedPorts returns a list of ports currently being forwarded.
func (u *UPnP) ForwardedPorts() []uint16 {
u.forwardedPortsMtx.Lock()
defer u.forwardedPortsMtx.Unlock()
ports := make([]uint16, 0, len(u.forwardedPorts))
for port := range u.forwardedPorts {
ports = append(ports, port)
}
return ports
}
// Name returns the name of the specific NAT traversal technique used.
func (u *UPnP) Name() string {
return "UPnP"
}