Skip to content

Commit

Permalink
Merge pull request #5 from Stebalien/multi-nat
Browse files Browse the repository at this point in the history
Multi nat
  • Loading branch information
Stebalien authored Mar 12, 2019
2 parents 5b3e757 + c8594db commit 2c696e8
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 40 deletions.
93 changes: 83 additions & 10 deletions nat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
package nat

import (
"context"
"errors"
"math"
"math/rand"
"net"
"time"

"github.com/jackpal/gateway"
)

var ErrNoExternalAddress = errors.New("no external address")
Expand Down Expand Up @@ -34,20 +37,90 @@ type NAT interface {
DeletePortMapping(protocol string, internalPort int) (err error)
}

// DiscoverNATs returns all NATs discovered in the network.
func DiscoverNATs(ctx context.Context) <-chan NAT {
nats := make(chan NAT)

go func() {
defer close(nats)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

upnpIg1 := discoverUPNP_IG1(ctx)
upnpIg2 := discoverUPNP_IG2(ctx)
natpmp := discoverNATPMP(ctx)
upnpGenIGDev := discoverUPNP_GenIGDev(ctx)
for upnpIg1 != nil || upnpIg2 != nil || natpmp != nil {
var (
nat NAT
ok bool
)
select {
case nat, ok = <-upnpIg1:
if !ok {
upnpIg1 = nil
}
case nat, ok = <-upnpIg2:
if !ok {
upnpIg2 = nil
}
case nat, ok = <-upnpGenIGDev:
if !ok {
upnpGenIGDev = nil
}
case nat, ok = <-natpmp:
if !ok {
natpmp = nil
}
}
if ok {
select {
case nats <- nat:
case <-ctx.Done():
return
}
}
}
}()
return nats
}

// DiscoverGateway attempts to find a gateway device.
func DiscoverGateway() (NAT, error) {
select {
case nat := <-discoverUPNP_IG1():
return nat, nil
case nat := <-discoverUPNP_IG2():
return nat, nil
case nat := <-discoverUPNP_GenIGDev():
return nat, nil
case nat := <-discoverNATPMP():
return nat, nil
case <-time.After(10 * time.Second):
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var nats []NAT
for nat := range DiscoverNATs(ctx) {
nats = append(nats, nat)
}
switch len(nats) {
case 0:
return nil, ErrNoNATFound
case 1:
return nats[0], nil
}
gw, _ := gateway.DiscoverGateway()
bestNAT := nats[0]
natGw, _ := bestNAT.GetDeviceAddress()
bestNATIsGw := gw != nil && natGw.Equal(gw)
// 1. Prefer gateways discovered _last_. This is an OK heuristic for
// discovering the most-upstream (furthest) NAT.
// 2. Prefer gateways that actually match our known gateway address.
// Some relays like to claim to be NATs even if they aren't.
for _, nat := range nats[1:] {
natGw, _ := nat.GetDeviceAddress()
natIsGw := gw != nil && natGw.Equal(gw)

if bestNATIsGw && !natIsGw {
continue
}

bestNATIsGw = natIsGw
bestNAT = nat
}
return bestNAT, nil
}

func randomPort() int {
Expand Down
40 changes: 29 additions & 11 deletions natpmp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nat

import (
"context"
"net"
"time"

Expand All @@ -12,25 +13,42 @@ var (
_ NAT = (*natpmpNAT)(nil)
)

func discoverNATPMP() <-chan NAT {
func discoverNATPMP(ctx context.Context) <-chan NAT {
res := make(chan NAT, 1)

ip, err := gateway.DiscoverGateway()
if err == nil {
go discoverNATPMPWithAddr(res, ip)
go func() {
defer close(res)
// Unfortunately, we can't actually _stop_ the natpmp
// library. However, we can at least close _our_ channel
// and walk away.
select {
case client, ok := <-discoverNATPMPWithAddr(ip):
if ok {
res <- &natpmpNAT{client, ip, make(map[int]int)}
}
case <-ctx.Done():
}
}()
} else {
close(res)
}

return res
}

func discoverNATPMPWithAddr(c chan NAT, ip net.IP) {
client := natpmp.NewClient(ip)
_, err := client.GetExternalAddress()
if err != nil {
return
}

c <- &natpmpNAT{client, ip, make(map[int]int)}
func discoverNATPMPWithAddr(ip net.IP) <-chan *natpmp.Client {
res := make(chan *natpmp.Client, 1)
go func() {
defer close(res)
client := natpmp.NewClient(ip)
_, err := client.GetExternalAddress()
if err != nil {
return
}
res <- client
}()
return res
}

type natpmpNAT struct {
Expand Down
66 changes: 47 additions & 19 deletions upnp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nat

import (
"context"
"net"
"net/url"
"strings"
Expand All @@ -17,9 +18,10 @@ var (
_ NAT = (*upnp_NAT)(nil)
)

func discoverUPNP_IG1() <-chan NAT {
res := make(chan NAT, 1)
func discoverUPNP_IG1(ctx context.Context) <-chan NAT {
res := make(chan NAT)
go func() {
defer close(res)

// find devices
devs, err := goupnp.DiscoverDevices(internetgateway1.URN_WANConnectionDevice_1)
Expand All @@ -33,6 +35,9 @@ func discoverUPNP_IG1() <-chan NAT {
}

dev.Root.Device.VisitServices(func(srv *goupnp.Service) {
if ctx.Err() != nil {
return
}
switch srv.ServiceType {
case internetgateway1.URN_WANIPConnection_1:
client := &internetgateway1.WANIPConnection1{ServiceClient: goupnp.ServiceClient{
Expand All @@ -42,8 +47,10 @@ func discoverUPNP_IG1() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-IP1)", dev.Root}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-IP1)", dev.Root}:
case <-ctx.Done():
}
}

case internetgateway1.URN_WANPPPConnection_1:
Expand All @@ -54,8 +61,10 @@ func discoverUPNP_IG1() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-PPP1)", dev.Root}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-PPP1)", dev.Root}:
case <-ctx.Done():
}
}

}
Expand All @@ -66,9 +75,10 @@ func discoverUPNP_IG1() <-chan NAT {
return res
}

func discoverUPNP_IG2() <-chan NAT {
res := make(chan NAT, 1)
func discoverUPNP_IG2(ctx context.Context) <-chan NAT {
res := make(chan NAT)
go func() {
defer close(res)

// find devices
devs, err := goupnp.DiscoverDevices(internetgateway2.URN_WANConnectionDevice_2)
Expand All @@ -82,6 +92,9 @@ func discoverUPNP_IG2() <-chan NAT {
}

dev.Root.Device.VisitServices(func(srv *goupnp.Service) {
if ctx.Err() != nil {
return
}
switch srv.ServiceType {
case internetgateway2.URN_WANIPConnection_1:
client := &internetgateway2.WANIPConnection1{ServiceClient: goupnp.ServiceClient{
Expand All @@ -91,8 +104,10 @@ func discoverUPNP_IG2() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-IP1)", dev.Root}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-IP1)", dev.Root}:
case <-ctx.Done():
}
}

case internetgateway2.URN_WANIPConnection_2:
Expand All @@ -103,8 +118,10 @@ func discoverUPNP_IG2() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-IP2)", dev.Root}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-IP2)", dev.Root}:
case <-ctx.Done():
}
}

case internetgateway2.URN_WANPPPConnection_1:
Expand All @@ -115,8 +132,10 @@ func discoverUPNP_IG2() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-PPP1)", dev.Root}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-PPP1)", dev.Root}:
case <-ctx.Done():
}
}

}
Expand All @@ -127,9 +146,11 @@ func discoverUPNP_IG2() <-chan NAT {
return res
}

func discoverUPNP_GenIGDev() <-chan NAT {
func discoverUPNP_GenIGDev(ctx context.Context) <-chan NAT {
res := make(chan NAT, 1)
go func() {
defer close(res)

DeviceList, err := ssdp.Search(ssdp.All, 5, "")
if err != nil {
return
Expand All @@ -152,6 +173,9 @@ func discoverUPNP_GenIGDev() <-chan NAT {
}

RootDevice.Device.VisitServices(func(srv *goupnp.Service) {
if ctx.Err() != nil {
return
}
switch srv.ServiceType {
case internetgateway1.URN_WANIPConnection_1:
client := &internetgateway1.WANIPConnection1{ServiceClient: goupnp.ServiceClient{
Expand All @@ -161,8 +185,10 @@ func discoverUPNP_GenIGDev() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-IP1)", RootDevice}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-IP1)", RootDevice}:
case <-ctx.Done():
}
}

case internetgateway1.URN_WANPPPConnection_1:
Expand All @@ -173,8 +199,10 @@ func discoverUPNP_GenIGDev() <-chan NAT {
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-PPP1)", RootDevice}
return
select {
case res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-PPP1)", RootDevice}:
case <-ctx.Done():
}
}

}
Expand Down

0 comments on commit 2c696e8

Please sign in to comment.