1543833ca0
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.
116 lines
2.7 KiB
Go
116 lines
2.7 KiB
Go
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
|
|
}
|