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

Commit

Permalink
Merge pull request #1 from libp2p/implementation
Browse files Browse the repository at this point in the history
NAT Auto Discovery
  • Loading branch information
vyzo committed Oct 16, 2018
2 parents c851a30 + 67bccae commit 35a0832
Show file tree
Hide file tree
Showing 15 changed files with 2,172 additions and 0 deletions.
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# go-libp2p-discovery

[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io)
[![](https://img.shields.io/badge/project-libp2p-blue.svg?style=flat-square)](http://libp2p.io/)
[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)

> Ambient NAT discovery
This package provides an ambient NAT autodiscovery service.
It allows peers to figure out their NAT dialability situation by using test dial backs through peers providing the AutoNAT service.

## Documenation

See https://godoc.org/github.com/libp2p/go-libp2p-discovery.

## Contribute

Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/go-libp2p-discovery/issues)!

This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).

## License

MIT
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
}
207 changes: 207 additions & 0 deletions autonat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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
// dial through known autonat peers. initial state.
NATStatusUnknown NATStatus = iota
// NAT status is publicly dialable
NATStatusPublic
// NAT status is private network
NATStatusPrivate
)

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 {
ctx context.Context
host host.Host

mx sync.Mutex
peers map[peer.ID]struct{}
status NATStatus
addr ma.Multiaddr
// Reflects the confidence on of the NATStatus being private, as a single
// dialback may fail for reasons unrelated to NAT.
// If it is <3, then multiple autoNAT peers may be contacted for dialback
// If only a single autoNAT peer is known, then the confidence increases
// for each failure until it reaches 3.
confidence int
}

// 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]
}
}

0 comments on commit 35a0832

Please sign in to comment.