Skip to content
This repository has been archived by the owner on May 11, 2022. It is now read-only.

NAT Auto Discovery #1

Merged
merged 51 commits into from
Oct 16, 2018
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
32e8ab9
protobuf
vyzo May 5, 2018
70f7dd8
basic client
vyzo May 5, 2018
f3d9a24
add E_BAD_REQUEST to protobuf
vyzo May 6, 2018
cc058d6
service implementation
vyzo May 6, 2018
ef097b5
NAT autodetection
vyzo May 6, 2018
6efad8f
remove left-over design notes
vyzo May 6, 2018
2aa66e5
don't delete autonat peers on disconnect, just mark them as disconnected
vyzo May 6, 2018
ea43bf5
we only track autonat peers
vyzo May 6, 2018
00fb7e7
fix autonat peer tracking
vyzo May 6, 2018
0377627
add TODO in service about skipping private network addresses
vyzo May 6, 2018
7fad996
add network notifee
vyzo May 6, 2018
9af8715
don't try to dial private network addresses
vyzo May 6, 2018
bc41c7a
add localhost to private addr ranges
vyzo May 6, 2018
dcbcfce
no need to select; it's a one shot sync
vyzo May 7, 2018
9efd0ec
typed NATStatus constants
vyzo May 7, 2018
aaaa90e
bump initial autodiscovery delay to 15s
vyzo May 7, 2018
1562e1b
AutoNATState is AmbientAutoNAT
vyzo May 8, 2018
6d4bc41
variables for background delays
vyzo May 8, 2018
fa14117
named magic number incantations
vyzo May 9, 2018
d16ca79
refactor getPeers for locked scope
vyzo May 9, 2018
b1733eb
don't throw away read errors; log them.
vyzo May 9, 2018
bb5cad4
simplify autonat client itnerface
vyzo May 9, 2018
cd7a875
mutex hat
vyzo May 11, 2018
7b3981e
docstrings and another mutex hat.
vyzo May 11, 2018
cf04a09
improve docstring for NewAutoNAT
vyzo May 11, 2018
7c097ed
improve NATStatusUknown docstring
vyzo May 11, 2018
5837cc5
fix typo
vyzo May 12, 2018
56a0966
update gx deps
vyzo Sep 7, 2018
54fb466
regenerate protobuf
vyzo Sep 7, 2018
66ca387
svc: construct dialer host without listen addrs
vyzo Sep 8, 2018
3abf9c7
accept libp2p options for the dialer constructor in NewAutoNATService
vyzo Sep 8, 2018
3b679e0
make service dialback timeout configurable; useful for tests
vyzo Sep 8, 2018
1cba297
basic service tests
vyzo Sep 8, 2018
dd7c7a9
Makefile and travis build file
vyzo Sep 8, 2018
9ff7df3
test for ambient autonat functionality
vyzo Sep 8, 2018
0fdf1b0
address review comments
vyzo Sep 29, 2018
46d352f
use the protocol list by identify, don't emit chatter on every connec…
vyzo Sep 29, 2018
0a4e215
add observed address to the dialback set
vyzo Oct 3, 2018
91c209c
ensure test hosts are only on loopback
vyzo Oct 3, 2018
00d2fea
add /libp2p prefix in protocol string
vyzo Oct 3, 2018
8ea9f1b
configurable throttle for service rate limiter
vyzo Oct 3, 2018
d9a0d1a
call AutoNATService.peers something else (reqs)
vyzo Oct 3, 2018
aadb8db
use more peer dial errors for increased confidence in NATPrivate state
vyzo Oct 4, 2018
d7f55b0
use more peers if less than 3 are connected
vyzo Oct 4, 2018
852f4e0
adjust AutoNATRetryInterval in autonat tests
vyzo Oct 4, 2018
9c8ee52
increase identify delay to 500ms
vyzo Oct 12, 2018
b2c65b0
jenkins file
vyzo Oct 12, 2018
8d2e2ae
add README
vyzo Oct 12, 2018
9ef3734
reduce depgraph to just go-libp2p, update to 6.0.19
vyzo Oct 12, 2018
6a3a9cb
Add configurable Identify delay, with default value of 5 secs
vyzo Oct 16, 2018
67bccae
add docstring for confidence
vyzo Oct 16, 2018
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
22 changes: 22 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
os:
- linux

sudo: false

language: go

go:
- 1.9.x

install:
- make deps

script:
- bash <(curl -s https://raw.githubusercontent.com/ipfs/ci-helpers/master/travis-ci/run-standard-tests.sh)

cache:
directories:
- $GOPATH/src/gx

notifications:
email: false
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
gx:
go get -u github.com/whyrusleeping/gx
go get -u github.com/whyrusleeping/gx-go

deps: gx
gx --verbose install --global
gx-go rewrite
70 changes: 70 additions & 0 deletions addr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package autonat

import (
"net"

ma "github.com/multiformats/go-multiaddr"
)

var private4, private6 []*net.IPNet
var privateCIDR4 = []string{
// localhost
"127.0.0.0/8",
// private networks
"10.0.0.0/8",
"100.64.0.0/10",
"172.16.0.0/12",
"192.168.0.0/16",
// link local
"169.254.0.0/16",
}
var privateCIDR6 = []string{
// localhost
"::1/128",
// ULA reserved
"fc00::/7",
// link local
"fe80::/10",
}

func init() {
private4 = parsePrivateCIDR(privateCIDR4)
private6 = parsePrivateCIDR(privateCIDR6)
}

func parsePrivateCIDR(cidrs []string) []*net.IPNet {
ipnets := make([]*net.IPNet, len(cidrs))
for i, cidr := range cidrs {
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
ipnets[i] = ipnet
}
return ipnets
}

func isPublicAddr(a ma.Multiaddr) bool {
ip, err := a.ValueForProtocol(ma.P_IP4)
if err == nil {
return !inAddrRange(ip, private4)
}

ip, err = a.ValueForProtocol(ma.P_IP6)
if err == nil {
return !inAddrRange(ip, private6)
}

return false
}

func inAddrRange(s string, ipnets []*net.IPNet) bool {
ip := net.ParseIP(s)
for _, ipnet := range ipnets {
if ipnet.Contains(ip) {
return true
}
}

return false
}
202 changes: 202 additions & 0 deletions autonat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package autonat

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

host "github.com/libp2p/go-libp2p-host"
inet "github.com/libp2p/go-libp2p-net"
peer "github.com/libp2p/go-libp2p-peer"
ma "github.com/multiformats/go-multiaddr"
)

// NATStatus is the state of NAT as detected by the ambient service.
type NATStatus int

const (
// NAT status is unknown; this means that the ambient service has not been
// able to decide the presence of NAT in the most recent attempt to test
magik6k marked this conversation as resolved.
Show resolved Hide resolved
// dial through known autonat peers. initial state.
NATStatusUnknown NATStatus = iota
// NAT status is publicly dialable
NATStatusPublic
// NAT status is private network
NATStatusPrivate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about "no nat"? Do we need that state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does that state mean though? We have Uknown and Public -- no nat is equivalent to public.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I was thinking:

  • NatStatusPrivate -> Behind a nat and undiablable.
  • NatStatusPublic -> Behind a nat and dialable.

(although we may not need to track the undialable case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more of "dialable" or not "dialable".
Do we gain anything by knowing that there is no NAT whatsoever?
Note that the inference might be hard to make.

)

var (
AutoNATBootDelay = 15 * time.Second
AutoNATRetryInterval = 90 * time.Second
AutoNATRefreshInterval = 15 * time.Minute
AutoNATRequestTimeout = 60 * time.Second
)

// AutoNAT is the interface for ambient NAT autodiscovery
type AutoNAT interface {
// Status returns the current NAT status
Status() NATStatus
// PublicAddr returns the public dial address when NAT status is public and an
// error otherwise
PublicAddr() (ma.Multiaddr, error)
}

// AmbientAutoNAT is the implementation of ambient NAT autodiscovery
type AmbientAutoNAT struct {
magik6k marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context
host host.Host

mx sync.Mutex
peers map[peer.ID]struct{}
status NATStatus
addr ma.Multiaddr
confidence int
magik6k marked this conversation as resolved.
Show resolved Hide resolved
}

// NewAutoNAT creates a new ambient NAT autodiscovery instance attached to a host
func NewAutoNAT(ctx context.Context, h host.Host) AutoNAT {
as := &AmbientAutoNAT{
ctx: ctx,
host: h,
peers: make(map[peer.ID]struct{}),
status: NATStatusUnknown,
}

h.Network().Notify(as)
go as.background()

return as
}

func (as *AmbientAutoNAT) Status() NATStatus {
return as.status
}

func (as *AmbientAutoNAT) PublicAddr() (ma.Multiaddr, error) {
as.mx.Lock()
defer as.mx.Unlock()

if as.status != NATStatusPublic {
return nil, errors.New("NAT Status is not public")
}

return as.addr, nil
}

func (as *AmbientAutoNAT) background() {
// wait a bit for the node to come online and establish some connections
// before starting autodetection
select {
case <-time.After(AutoNATBootDelay):
case <-as.ctx.Done():
return
}

for {
as.autodetect()

delay := AutoNATRefreshInterval
if as.status == NATStatusUnknown {
delay = AutoNATRetryInterval
}

select {
case <-time.After(delay):
case <-as.ctx.Done():
return
}
}
}

func (as *AmbientAutoNAT) autodetect() {
peers := as.getPeers()

if len(peers) == 0 {
log.Debugf("skipping NAT auto detection; no autonat peers")
return
}

cli := NewAutoNATClient(as.host)
failures := 0

for _, p := range peers {
ctx, cancel := context.WithTimeout(as.ctx, AutoNATRequestTimeout)
a, err := cli.DialBack(ctx, p)
cancel()

switch {
case err == nil:
log.Debugf("NAT status is public; address through %s: %s", p.Pretty(), a.String())
as.mx.Lock()
as.addr = a
as.status = NATStatusPublic
as.confidence = 0
as.mx.Unlock()
return

case IsDialError(err):
log.Debugf("dial error through %s: %s", p.Pretty(), err.Error())
failures++
if failures >= 3 || as.confidence >= 3 { // 3 times is enemy action
log.Debugf("NAT status is private")
as.mx.Lock()
as.status = NATStatusPrivate
as.confidence = 3
as.mx.Unlock()
return
}

default:
log.Debugf("Error dialing through %s: %s", p.Pretty(), err.Error())
}
}

as.mx.Lock()
if failures > 0 {
as.status = NATStatusPrivate
as.confidence++
log.Debugf("NAT status is private")
} else {
as.status = NATStatusUnknown
as.confidence = 0
log.Debugf("NAT status is unknown")
}
as.mx.Unlock()
}

func (as *AmbientAutoNAT) getPeers() []peer.ID {
as.mx.Lock()
defer as.mx.Unlock()

if len(as.peers) == 0 {
return nil
}

var connected, others []peer.ID

for p := range as.peers {
if as.host.Network().Connectedness(p) == inet.Connected {
connected = append(connected, p)
} else {
others = append(others, p)
}
}

shufflePeers(connected)

if len(connected) < 3 {
shufflePeers(others)
return append(connected, others...)
} else {
return connected
}
}

func shufflePeers(peers []peer.ID) {
for i := range peers {
j := rand.Intn(i + 1)
peers[i], peers[j] = peers[j], peers[i]
}
}
90 changes: 90 additions & 0 deletions autonat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package autonat

import (
"context"
"net"
"testing"
"time"

libp2p "github.com/libp2p/go-libp2p"
host "github.com/libp2p/go-libp2p-host"
)

func makeAutoNAT(ctx context.Context, t *testing.T) (host.Host, AutoNAT) {
h, err := libp2p.New(ctx, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
if err != nil {
t.Fatal(err)
}

a := NewAutoNAT(ctx, h)

return h, a
}

// Note: these tests assume the host has only private inet addresses!
func TestAutoNATPrivate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

save1 := AutoNATBootDelay
AutoNATBootDelay = 1 * time.Second
save2 := AutoNATRefreshInterval
AutoNATRefreshInterval = 1 * time.Second
save3 := AutoNATRetryInterval
AutoNATRetryInterval = 1 * time.Second

hs, _ := makeAutoNATService(ctx, t)
hc, an := makeAutoNAT(ctx, t)

status := an.Status()
if status != NATStatusUnknown {
t.Fatalf("unexpected NAT status: %d", status)
}

connect(t, hs, hc)
time.Sleep(2 * time.Second)

status = an.Status()
if status != NATStatusPrivate {
t.Fatalf("unexpected NAT status: %d", status)
}

AutoNATBootDelay = save1
AutoNATRefreshInterval = save2
AutoNATRetryInterval = save3
}

func TestAutoNATPublic(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

save1 := AutoNATBootDelay
AutoNATBootDelay = 1 * time.Second
save2 := AutoNATRefreshInterval
AutoNATRefreshInterval = 1 * time.Second
save3 := AutoNATRetryInterval
AutoNATRetryInterval = 1 * time.Second
save4 := private4
private4 = []*net.IPNet{}

hs, _ := makeAutoNATService(ctx, t)
hc, an := makeAutoNAT(ctx, t)

status := an.Status()
if status != NATStatusUnknown {
t.Fatalf("unexpected NAT status: %d", status)
}

connect(t, hs, hc)
time.Sleep(2 * time.Second)

status = an.Status()
if status != NATStatusPublic {
t.Fatalf("unexpected NAT status: %d", status)
}

AutoNATBootDelay = save1
AutoNATRefreshInterval = save2
AutoNATRetryInterval = save3
private4 = save4
}
Loading