-
Notifications
You must be signed in to change notification settings - Fork 12
NAT Auto Discovery #1
Changes from 13 commits
32e8ab9
70f7dd8
f3d9a24
cc058d6
ef097b5
6efad8f
2aa66e5
ea43bf5
00fb7e7
0377627
7fad996
9af8715
bc41c7a
dcbcfce
9efd0ec
aaaa90e
1562e1b
6d4bc41
fa14117
d16ca79
b1733eb
bb5cad4
cd7a875
7b3981e
cf04a09
7c097ed
5837cc5
56a0966
54fb466
66ca387
3abf9c7
3b679e0
1cba297
dd7c7a9
9ff7df3
0fdf1b0
46d352f
0a4e215
91c209c
00d2fea
8ea9f1b
d9a0d1a
aadb8db
d7f55b0
852f4e0
9c8ee52
b2c65b0
8d2e2ae
9ef3734
6a3a9cb
67bccae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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 | ||
NATStatusPublic | ||
NATStatusPrivate | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about "no nat"? Do we need that state? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I was thinking:
(although we may not need to track the undialable case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's more of "dialable" or not "dialable". |
||
) | ||
|
||
type AutoNAT interface { | ||
Status() NATStatus | ||
PublicAddr() (ma.Multiaddr, error) | ||
} | ||
|
||
type AutoNATState struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hrm, good point. Easy to fix, should I call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed to |
||
ctx context.Context | ||
host host.Host | ||
peers map[peer.ID]struct{} | ||
status NATStatus | ||
addr ma.Multiaddr | ||
mx sync.Mutex | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. peers struct {
sync.Mutex
set map[peer.ID]struct{}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the mutex protects the state/address as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following the mutex hat idiom maybe clearer then. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For testing, it might be useful for it to be configurable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can bump it a bit, say to 15s. Re: testing There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure, will do. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can also reuse the client objects. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. magic number There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can give a name to the incantation. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
} | ||
} |
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() | ||
} |
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) {} |
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, will do.