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 13 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}
142 changes: 142 additions & 0 deletions autonat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package autonat

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

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

type NATStatus int

const (
NATStatusUnknown = iota
Copy link
Member

Choose a reason for hiding this comment

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

I would give those constants explicit type of NATStatus.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, will do.

NATStatusPublic
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.

)

type AutoNAT interface {
Status() NATStatus
PublicAddr() (ma.Multiaddr, error)
}

type AutoNATState struct {

Choose a reason for hiding this comment

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

the naming here is a bit weird. Its not clear from reading that AutoNATState is an implementation of AutoNAT

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hrm, good point. Easy to fix, should I call it AutoNATImpl? boring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like AmbientAutoNAT:

  • it conveys that it is an AutoNAT instance
  • it conveys that it is ambient, meaning that the user doesn't have anything to do other than creating an instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed to AmbientAutoNAT in 1562e1b

ctx context.Context
host host.Host
peers map[peer.ID]struct{}
status NATStatus
addr ma.Multiaddr
mx sync.Mutex
Copy link
Member

Choose a reason for hiding this comment

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

We started using anonymous/nested struct pattern to describe what is protected by mutex.
I am not 100% sure if the peers map is the only thing but if it is it would look:

peers struct {
    sync.Mutex
    set map[peer.ID]struct{}
}

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@vyzo vyzo May 9, 2018

Choose a reason for hiding this comment

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

the mutex protects the state/address as well.

Choose a reason for hiding this comment

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

Following the mutex hat idiom maybe clearer then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mutex hat in cd7a875

}

func NewAutoNAT(ctx context.Context, h host.Host) AutoNAT {
as := &AutoNATState{
ctx: ctx,
host: h,
peers: make(map[peer.ID]struct{}),
status: NATStatusUnknown,
}

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

return as
}

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

func (as *AutoNATState) 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 *AutoNATState) background() {
// wait a bit for the node to come online and establish some connections
// before starting autodetection
time.Sleep(10 * time.Second)
Copy link
Member

Choose a reason for hiding this comment

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

10s might be a bit short here. Automatic port forwarding has about 10s timeout.
From another hand, if it is timing out we won't get and forwarding either way.

Copy link
Member

Choose a reason for hiding this comment

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

For testing, it might be useful for it to be configurable.

Copy link
Contributor Author

@vyzo vyzo May 7, 2018

Choose a reason for hiding this comment

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

We can bump it a bit, say to 15s.

Re: testing
I don't think it will be useful, as we won't be able to unit-test this aspect with go test.
I plan to run some test programs and try it in the wild.

Copy link
Contributor Author

@vyzo vyzo May 7, 2018

Choose a reason for hiding this comment

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

Then again we probably do want to test the autodiscovery without waiting for this time, so maybe we can make it a variable.

We can revisit when I have the test suite ready.

for {
as.autodetect()
select {
case <-time.After(15 * time.Minute):
case <-as.ctx.Done():
return
}
}
}

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

as.mx.Lock()

Choose a reason for hiding this comment

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

probably want to put this locked section into a separate method so we can use defers nicely

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in d16ca79

peers := make([]peer.ID, 0, len(as.peers))
for p := range as.peers {
if len(as.host.Network().ConnsToPeer(p)) > 0 {
peers = append(peers, p)
}
}

if len(peers) == 0 {
// we don't have any open connections, try any autonat peer that we know about
for p := range as.peers {
peers = append(peers, p)
}
}

as.mx.Unlock()

shufflePeers(peers)

for _, p := range peers {
cli := NewAutoNATClient(as.host, p)

Choose a reason for hiding this comment

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

creating a single use thingy here feels a bit weird. Maybe just make the dial method take the host and peer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can also reuse the client objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

on the other hand it's a very simple object, with no state. Not sure it's worth the trouble to cache it.

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 might make sense to make it take a peer so that we can reuse the object for all peers in the interaction; no need to hold it across function calls i think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in bb5cad4

The client object is reused for dialing all peers as needed.

ctx, cancel := context.WithTimeout(as.ctx, 60*time.Second)

Choose a reason for hiding this comment

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

magic number

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can give a name to the incantation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

named it and made it a variable so that we can unit test; fa14117

a, err := cli.Dial(ctx)
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.mx.Unlock()
return

case IsDialError(err):
log.Debugf("NAT status is private; dial error through %s: %s", p.Pretty(), err.Error())
as.mx.Lock()
as.status = NATStatusPrivate
as.mx.Unlock()
return

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

as.mx.Lock()
as.status = NATStatusUnknown
as.mx.Unlock()
}

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

import (
"context"
"fmt"

pb "github.com/libp2p/go-libp2p-autonat/pb"

ggio "github.com/gogo/protobuf/io"
host "github.com/libp2p/go-libp2p-host"
inet "github.com/libp2p/go-libp2p-net"
peer "github.com/libp2p/go-libp2p-peer"
pstore "github.com/libp2p/go-libp2p-peerstore"
ma "github.com/multiformats/go-multiaddr"
)

type AutoNATClient interface {
Dial(ctx context.Context) (ma.Multiaddr, error)
}

type AutoNATError struct {
Status pb.Message_ResponseStatus
Text string
}

func NewAutoNATClient(h host.Host, p peer.ID) AutoNATClient {
return &client{h: h, p: p}
}

type client struct {
h host.Host
p peer.ID
}

func (c *client) Dial(ctx context.Context) (ma.Multiaddr, error) {
s, err := c.h.NewStream(ctx, c.p, AutoNATProto)
if err != nil {
return nil, err
}
defer s.Close()

r := ggio.NewDelimitedReader(s, inet.MessageSizeMax)
w := ggio.NewDelimitedWriter(s)

req := newDialMessage(pstore.PeerInfo{ID: c.h.ID(), Addrs: c.h.Addrs()})
err = w.WriteMsg(req)
if err != nil {
return nil, err
}

var res pb.Message
err = r.ReadMsg(&res)
if err != nil {
return nil, err
}

if res.GetType() != pb.Message_DIAL_RESPONSE {
return nil, fmt.Errorf("Unexpected response: %s", res.GetType().String())
}

status := res.GetDialResponse().GetStatus()
switch status {
case pb.Message_OK:
addr := res.GetDialResponse().GetAddr()
return ma.NewMultiaddrBytes(addr)

default:
return nil, AutoNATError{Status: status, Text: res.GetDialResponse().GetStatusText()}
}
}

func (e AutoNATError) Error() string {
return fmt.Sprintf("AutoNAT error: %s (%s)", e.Text, e.Status.String())
}

func (e AutoNATError) IsDialError() bool {
return e.Status == pb.Message_E_DIAL_ERROR
}

func (e AutoNATError) IsDialRefused() bool {
return e.Status == pb.Message_E_DIAL_REFUSED
}

func IsDialError(e error) bool {
ae, ok := e.(AutoNATError)
return ok && ae.IsDialError()
}

func IsDialRefused(e error) bool {
ae, ok := e.(AutoNATError)
return ok && ae.IsDialRefused()
}
31 changes: 31 additions & 0 deletions notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package autonat

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

var _ inet.Notifiee = (*AutoNATState)(nil)

func (as *AutoNATState) Listen(net inet.Network, a ma.Multiaddr) {}
func (as *AutoNATState) ListenClose(net inet.Network, a ma.Multiaddr) {}
func (as *AutoNATState) OpenedStream(net inet.Network, s inet.Stream) {}
func (as *AutoNATState) ClosedStream(net inet.Network, s inet.Stream) {}

func (as *AutoNATState) Connected(net inet.Network, c inet.Conn) {
go func(p peer.ID) {
s, err := as.host.NewStream(as.ctx, p, AutoNATProto)
if err != nil {
return
}
s.Close()

log.Infof("Discovered AutoNAT peer %s", p.Pretty())
as.mx.Lock()
as.peers[p] = struct{}{}
as.mx.Unlock()
}(c.RemotePeer())
}

func (as *AutoNATState) Disconnected(net inet.Network, c inet.Conn) {}