Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handle networks with multiple NATS #9

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require (
github.com/huin/goupnp v0.0.0-20180415215157-1395d1447324
github.com/jackpal/gateway v1.0.5
github.com/jackpal/go-nat-pmp v1.0.1
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79
golang.org/x/text v0.3.0
github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79 // indirect
golang.org/x/text v0.3.0 // indirect
)
90 changes: 80 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,87 @@ 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)

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