From 943d7b398fc871282f7d14b0e16171ac0d3fbd2e Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Wed, 6 Mar 2019 09:58:32 -0800 Subject: [PATCH 1/3] expose all discovered NATs This change makes it possible to configure all discovered NATs, not just the first one found. --- nat.go | 65 +++++++++++++++++++++++++++++++++++++++++++++--------- natpmp.go | 40 +++++++++++++++++++++++---------- upnp.go | 66 +++++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 131 insertions(+), 40 deletions(-) diff --git a/nat.go b/nat.go index eb574d6..4505df6 100644 --- a/nat.go +++ b/nat.go @@ -2,6 +2,7 @@ package nat import ( + "context" "errors" "math" "math/rand" @@ -34,20 +35,64 @@ 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() + nat := <-DiscoverNATs(ctx) + if nat == nil { return nil, ErrNoNATFound } + return nat, nil } func randomPort() int { diff --git a/natpmp.go b/natpmp.go index 395a5dd..a31a499 100644 --- a/natpmp.go +++ b/natpmp.go @@ -1,6 +1,7 @@ package nat import ( + "context" "net" "time" @@ -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 { diff --git a/upnp.go b/upnp.go index caad350..ccfeb14 100644 --- a/upnp.go +++ b/upnp.go @@ -1,6 +1,7 @@ package nat import ( + "context" "net" "net/url" "strings" @@ -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) @@ -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{ @@ -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: @@ -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(): + } } } @@ -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) @@ -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{ @@ -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: @@ -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: @@ -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(): + } } } @@ -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 @@ -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{ @@ -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: @@ -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(): + } } } From c8594db014d29a36fe16880224818429f2a867c2 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Wed, 6 Mar 2019 10:44:40 -0800 Subject: [PATCH 2/3] try to pick the best NAT when multiple NATs are discovered --- nat.go | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/nat.go b/nat.go index 4505df6..727ae2a 100644 --- a/nat.go +++ b/nat.go @@ -8,6 +8,8 @@ import ( "math/rand" "net" "time" + + "github.com/jackpal/gateway" ) var ErrNoExternalAddress = errors.New("no external address") @@ -88,11 +90,37 @@ func DiscoverNATs(ctx context.Context) <-chan NAT { func DiscoverGateway() (NAT, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - nat := <-DiscoverNATs(ctx) - if nat == nil { + + 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 nat, nil + return bestNAT, nil } func randomPort() int { From 6994e2ef7381334fba3225af417319204e681857 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 3 May 2019 10:27:04 -0700 Subject: [PATCH 3/3] don't mask context --- go.mod | 5 +++-- nat.go | 3 --- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 73255a0..bf529f3 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/nat.go b/nat.go index 727ae2a..b34180e 100644 --- a/nat.go +++ b/nat.go @@ -44,9 +44,6 @@ func DiscoverNATs(ctx context.Context) <-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)