diff --git a/.gx/lastpubver b/.gx/lastpubver new file mode 100644 index 0000000..efd6364 --- /dev/null +++ b/.gx/lastpubver @@ -0,0 +1 @@ +1.0.0: QmNtEW59XTrreF9U4NAjYUmixEJ56HnfiJCj1p97bFnah2 diff --git a/README.md b/README.md index 57ef719..31c8758 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ It allows peers to figure out their NAT dialability situation by using test dial ## Documenation -See https://godoc.org/github.com/libp2p/go-libp2p-discovery. +See https://godoc.org/github.com/libp2p/go-libp2p-autonat ## Contribute diff --git a/addr.go b/addr.go deleted file mode 100644 index 4e078e3..0000000 --- a/addr.go +++ /dev/null @@ -1,70 +0,0 @@ -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 -} diff --git a/autonat.go b/autonat.go index 1eace01..0aa987c 100644 --- a/autonat.go +++ b/autonat.go @@ -48,6 +48,8 @@ type AmbientAutoNAT struct { ctx context.Context host host.Host + getAddrs GetAddrs + mx sync.Mutex peers map[peer.ID]struct{} status NATStatus @@ -61,12 +63,18 @@ type AmbientAutoNAT struct { } // NewAutoNAT creates a new ambient NAT autodiscovery instance attached to a host -func NewAutoNAT(ctx context.Context, h host.Host) AutoNAT { +// If getAddrs is nil, h.Addrs will be used +func NewAutoNAT(ctx context.Context, h host.Host, getAddrs GetAddrs) AutoNAT { + if getAddrs == nil { + getAddrs = h.Addrs + } + as := &AmbientAutoNAT{ - ctx: ctx, - host: h, - peers: make(map[peer.ID]struct{}), - status: NATStatusUnknown, + ctx: ctx, + host: h, + getAddrs: getAddrs, + peers: make(map[peer.ID]struct{}), + status: NATStatusUnknown, } h.Network().Notify(as) @@ -123,7 +131,7 @@ func (as *AmbientAutoNAT) autodetect() { return } - cli := NewAutoNATClient(as.host) + cli := NewAutoNATClient(as.host, as.getAddrs) failures := 0 for _, p := range peers { diff --git a/autonat_test.go b/autonat_test.go index 6d31d46..aa8d879 100644 --- a/autonat_test.go +++ b/autonat_test.go @@ -2,12 +2,18 @@ package autonat import ( "context" - "net" "testing" "time" - libp2p "github.com/libp2p/go-libp2p" + pb "github.com/libp2p/go-libp2p-autonat/pb" + + ggio "github.com/gogo/protobuf/io" + bhost "github.com/libp2p/go-libp2p-blankhost" host "github.com/libp2p/go-libp2p-host" + inet "github.com/libp2p/go-libp2p-net" + pstore "github.com/libp2p/go-libp2p-peerstore" + swarmt "github.com/libp2p/go-libp2p-swarm/testing" + ma "github.com/multiformats/go-multiaddr" ) func init() { @@ -17,24 +23,76 @@ func init() { AutoNATIdentifyDelay = 100 * time.Millisecond } -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) +// these are mock service implementations for testing +func makeAutoNATServicePrivate(ctx context.Context, t *testing.T) host.Host { + h := bhost.NewBlankHost(swarmt.GenSwarm(t, ctx)) + h.SetStreamHandler(AutoNATProto, sayAutoNATPrivate) + return h +} + +func makeAutoNATServicePublic(ctx context.Context, t *testing.T) host.Host { + h := bhost.NewBlankHost(swarmt.GenSwarm(t, ctx)) + h.SetStreamHandler(AutoNATProto, sayAutoNATPublic) + return h +} + +func sayAutoNATPrivate(s inet.Stream) { + defer s.Close() + w := ggio.NewDelimitedWriter(s) + res := pb.Message{ + Type: pb.Message_DIAL_RESPONSE.Enum(), + DialResponse: newDialResponseError(pb.Message_E_DIAL_ERROR, "no dialable addresses"), } + w.WriteMsg(&res) +} + +func sayAutoNATPublic(s inet.Stream) { + defer s.Close() + w := ggio.NewDelimitedWriter(s) + res := pb.Message{ + Type: pb.Message_DIAL_RESPONSE.Enum(), + DialResponse: newDialResponseOK(s.Conn().RemoteMultiaddr()), + } + w.WriteMsg(&res) +} - a := NewAutoNAT(ctx, h) +func newDialResponseOK(addr ma.Multiaddr) *pb.Message_DialResponse { + dr := new(pb.Message_DialResponse) + dr.Status = pb.Message_OK.Enum() + dr.Addr = addr.Bytes() + return dr +} + +func newDialResponseError(status pb.Message_ResponseStatus, text string) *pb.Message_DialResponse { + dr := new(pb.Message_DialResponse) + dr.Status = status.Enum() + dr.StatusText = &text + return dr +} + +func makeAutoNAT(ctx context.Context, t *testing.T, ash host.Host) (host.Host, AutoNAT) { + h := bhost.NewBlankHost(swarmt.GenSwarm(t, ctx)) + a := NewAutoNAT(ctx, h, nil) + a.(*AmbientAutoNAT).peers[ash.ID()] = struct{}{} return h, a } -// Note: these tests assume the host has only private inet addresses! +func connect(t *testing.T, a, b host.Host) { + pinfo := pstore.PeerInfo{ID: a.ID(), Addrs: a.Addrs()} + err := b.Connect(context.Background(), pinfo) + if err != nil { + t.Fatal(err) + } +} + +// tests func TestAutoNATPrivate(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - hs, _ := makeAutoNATService(ctx, t) - hc, an := makeAutoNAT(ctx, t) + hs := makeAutoNATServicePrivate(ctx, t) + hc, an := makeAutoNAT(ctx, t, hs) status := an.Status() if status != NATStatusUnknown { @@ -54,11 +112,8 @@ func TestAutoNATPublic(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - save := private4 - private4 = []*net.IPNet{} - - hs, _ := makeAutoNATService(ctx, t) - hc, an := makeAutoNAT(ctx, t) + hs := makeAutoNATServicePublic(ctx, t) + hc, an := makeAutoNAT(ctx, t, hs) status := an.Status() if status != NATStatusUnknown { @@ -72,6 +127,4 @@ func TestAutoNATPublic(t *testing.T) { if status != NATStatusPublic { t.Fatalf("unexpected NAT status: %d", status) } - - private4 = save } diff --git a/client.go b/client.go index 468bc68..0fd665d 100644 --- a/client.go +++ b/client.go @@ -27,13 +27,21 @@ type AutoNATError struct { Text string } +// GetAddrs is a function that returns the addresses to dial back +type GetAddrs func() []ma.Multiaddr + // NewAutoNATClient creates a fresh instance of an AutoNATClient -func NewAutoNATClient(h host.Host) AutoNATClient { - return &client{h: h} +// If getAddrs is nil, h.Addrs will be used +func NewAutoNATClient(h host.Host, getAddrs GetAddrs) AutoNATClient { + if getAddrs == nil { + getAddrs = h.Addrs + } + return &client{h: h, getAddrs: getAddrs} } type client struct { - h host.Host + h host.Host + getAddrs GetAddrs } func (c *client) DialBack(ctx context.Context, p peer.ID) (ma.Multiaddr, error) { @@ -46,7 +54,7 @@ func (c *client) DialBack(ctx context.Context, p peer.ID) (ma.Multiaddr, error) r := ggio.NewDelimitedReader(s, inet.MessageSizeMax) w := ggio.NewDelimitedWriter(s) - req := newDialMessage(pstore.PeerInfo{ID: c.h.ID(), Addrs: c.h.Addrs()}) + req := newDialMessage(pstore.PeerInfo{ID: c.h.ID(), Addrs: c.getAddrs()}) err = w.WriteMsg(req) if err != nil { return nil, err diff --git a/package.json b/package.json index dc59c8b..0a627aa 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,56 @@ "gxDependencies": [ { "author": "whyrusleeping", - "hash": "QmPL3AKtiaQyYpchZceXBZhZ3MSnoGqJvLZrc7fzDTTQdJ", - "name": "go-libp2p", - "version": "6.0.19" + "hash": "QmYWXhT9R7Q5wuBXAkdKqd8aYZwijB7KvWSZFY7SGfnoaw", + "name": "go-libp2p-host", + "version": "3.0.14" + }, + { + "author": "whyrusleeping", + "hash": "QmaNMjwCgftJwZHVUJU24ySvPMFeEnHzVnnmCpNsKZguTB", + "name": "go-libp2p-net", + "version": "3.0.14" + }, + { + "author": "whyrusleeping", + "hash": "QmTRhk7cgjUf2gfQ3p2M9KPECNZEW9XUrmHcFCgog4cPgB", + "name": "go-libp2p-peer", + "version": "2.4.0" + }, + { + "author": "whyrusleeping", + "hash": "QmatE2nxsAaK96jxMFBPMtVJEsaMrcJ21UYBHpXTkEM95r", + "name": "go-libp2p-peerstore", + "version": "2.0.5" + }, + { + "hash": "QmZChCsSt8DctjceaL56Eibc29CVQq4dGKRXC5JRZ6Ppae", + "name": "go-log", + "version": "1.5.7" + }, + { + "author": "whyrusleeping", + "hash": "QmTexoZajNmyWoxHMt2gCu9TAzfhV22LGrf4h7vmZXkWre", + "name": "go-libp2p-blankhost", + "version": "0.3.14" + }, + { + "author": "whyrusleeping", + "hash": "QmQ23JpVSdm2G3e2x4TFNSApeZ16rsw7unM42s5TENgBep", + "name": "go-libp2p-swarm", + "version": "3.0.20" + }, + { + "author": "whyrusleeping", + "hash": "QmdxUuburamoF6zF9qjeQC4WYcWGbWuRmdLacMEsW8ioD8", + "name": "gogo-protobuf", + "version": "0.0.0" + }, + { + "author": "multiformats", + "hash": "QmYmsdtJ3HsodkePE3eU3TsCaP2YvPZJ4LoXnNkDE5Tpt7", + "name": "go-multiaddr", + "version": "1.3.0" } ], "gxVersion": "0.12.1", @@ -17,6 +64,6 @@ "license": "", "name": "go-libp2p-autonat", "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", - "version": "0.0.0" + "version": "1.0.0" } diff --git a/proto.go b/proto.go index c9768cb..2dbb8eb 100644 --- a/proto.go +++ b/proto.go @@ -5,7 +5,6 @@ import ( logging "github.com/ipfs/go-log" pstore "github.com/libp2p/go-libp2p-peerstore" - ma "github.com/multiformats/go-multiaddr" ) const AutoNATProto = "/libp2p/autonat/1.0.0" @@ -25,17 +24,3 @@ func newDialMessage(pi pstore.PeerInfo) *pb.Message { return msg } - -func newDialResponseOK(addr ma.Multiaddr) *pb.Message_DialResponse { - dr := new(pb.Message_DialResponse) - dr.Status = pb.Message_OK.Enum() - dr.Addr = addr.Bytes() - return dr -} - -func newDialResponseError(status pb.Message_ResponseStatus, text string) *pb.Message_DialResponse { - dr := new(pb.Message_DialResponse) - dr.Status = status.Enum() - dr.StatusText = &text - return dr -} diff --git a/svc.go b/svc.go deleted file mode 100644 index 4d395d2..0000000 --- a/svc.go +++ /dev/null @@ -1,214 +0,0 @@ -package autonat - -import ( - "context" - "sync" - "time" - - pb "github.com/libp2p/go-libp2p-autonat/pb" - - ggio "github.com/gogo/protobuf/io" - libp2p "github.com/libp2p/go-libp2p" - 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" -) - -const P_CIRCUIT = 290 - -var ( - AutoNATServiceDialTimeout = 42 * time.Second - AutoNATServiceResetInterval = 1 * time.Minute - - AutoNATServiceThrottle = 3 -) - -// AutoNATService provides NAT autodetection services to other peers -type AutoNATService struct { - ctx context.Context - dialer host.Host - - // rate limiter - mx sync.Mutex - reqs map[peer.ID]int -} - -// NewAutoNATService creates a new AutoNATService instance attached to a host -func NewAutoNATService(ctx context.Context, h host.Host, opts ...libp2p.Option) (*AutoNATService, error) { - opts = append(opts, libp2p.NoListenAddrs) - dialer, err := libp2p.New(ctx, opts...) - if err != nil { - return nil, err - } - - as := &AutoNATService{ - ctx: ctx, - dialer: dialer, - reqs: make(map[peer.ID]int), - } - h.SetStreamHandler(AutoNATProto, as.handleStream) - - go as.resetRateLimiter() - - return as, nil -} - -func (as *AutoNATService) handleStream(s inet.Stream) { - defer s.Close() - - pid := s.Conn().RemotePeer() - log.Debugf("New stream from %s", pid.Pretty()) - - r := ggio.NewDelimitedReader(s, inet.MessageSizeMax) - w := ggio.NewDelimitedWriter(s) - - var req pb.Message - var res pb.Message - - err := r.ReadMsg(&req) - if err != nil { - log.Debugf("Error reading message from %s: %s", pid.Pretty(), err.Error()) - s.Reset() - return - } - - t := req.GetType() - if t != pb.Message_DIAL { - log.Debugf("Unexpected message from %s: %s (%d)", pid.Pretty(), t.String(), t) - s.Reset() - return - } - - dr := as.handleDial(pid, s.Conn().RemoteMultiaddr(), req.GetDial().GetPeer()) - res.Type = pb.Message_DIAL_RESPONSE.Enum() - res.DialResponse = dr - - err = w.WriteMsg(&res) - if err != nil { - log.Debugf("Error writing response to %s: %s", pid.Pretty(), err.Error()) - s.Reset() - return - } -} - -func (as *AutoNATService) handleDial(p peer.ID, obsaddr ma.Multiaddr, mpi *pb.Message_PeerInfo) *pb.Message_DialResponse { - if mpi == nil { - return newDialResponseError(pb.Message_E_BAD_REQUEST, "missing peer info") - } - - mpid := mpi.GetId() - if mpid != nil { - mp, err := peer.IDFromBytes(mpid) - if err != nil { - return newDialResponseError(pb.Message_E_BAD_REQUEST, "bad peer id") - } - - if mp != p { - return newDialResponseError(pb.Message_E_BAD_REQUEST, "peer id mismatch") - } - } - - addrs := make([]ma.Multiaddr, 0) - seen := make(map[string]struct{}) - - // add observed addr to the list of addresses to dial - if !as.skipDial(obsaddr) { - addrs = append(addrs, obsaddr) - seen[obsaddr.String()] = struct{}{} - } - - for _, maddr := range mpi.GetAddrs() { - addr, err := ma.NewMultiaddrBytes(maddr) - if err != nil { - log.Debugf("Error parsing multiaddr: %s", err.Error()) - continue - } - - if as.skipDial(addr) { - continue - } - - str := addr.String() - _, ok := seen[str] - if ok { - continue - } - - addrs = append(addrs, addr) - seen[str] = struct{}{} - } - - if len(addrs) == 0 { - return newDialResponseError(pb.Message_E_DIAL_ERROR, "no dialable addresses") - } - - return as.doDial(pstore.PeerInfo{ID: p, Addrs: addrs}) -} - -func (as *AutoNATService) skipDial(addr ma.Multiaddr) bool { - // skip relay addresses - _, err := addr.ValueForProtocol(P_CIRCUIT) - if err == nil { - return true - } - - // skip private network (unroutable) addresses - if !isPublicAddr(addr) { - return true - } - - return false -} - -func (as *AutoNATService) doDial(pi pstore.PeerInfo) *pb.Message_DialResponse { - // rate limit check - as.mx.Lock() - count := as.reqs[pi.ID] - if count >= AutoNATServiceThrottle { - as.mx.Unlock() - return newDialResponseError(pb.Message_E_DIAL_REFUSED, "too many dials") - } - as.reqs[pi.ID] = count + 1 - as.mx.Unlock() - - ctx, cancel := context.WithTimeout(as.ctx, AutoNATServiceDialTimeout) - defer cancel() - - err := as.dialer.Connect(ctx, pi) - if err != nil { - log.Debugf("error dialing %s: %s", pi.ID.Pretty(), err.Error()) - // wait for the context to timeout to avoid leaking timing information - // this renders the service ineffective as a port scanner - <-ctx.Done() - return newDialResponseError(pb.Message_E_DIAL_ERROR, "dial failed") - } - - conns := as.dialer.Network().ConnsToPeer(pi.ID) - if len(conns) == 0 { - log.Errorf("supposedly connected to %s, but no connection to peer", pi.ID.Pretty()) - return newDialResponseError(pb.Message_E_INTERNAL_ERROR, "internal service error") - } - - ra := conns[0].RemoteMultiaddr() - as.dialer.Network().ClosePeer(pi.ID) - return newDialResponseOK(ra) -} - -func (as *AutoNATService) resetRateLimiter() { - ticker := time.NewTicker(AutoNATServiceResetInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - as.mx.Lock() - as.reqs = make(map[peer.ID]int) - as.mx.Unlock() - - case <-as.ctx.Done(): - return - } - } -} diff --git a/svc_test.go b/svc_test.go deleted file mode 100644 index cfa4068..0000000 --- a/svc_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package autonat - -import ( - "context" - "net" - "testing" - "time" - - libp2p "github.com/libp2p/go-libp2p" - host "github.com/libp2p/go-libp2p-host" - pstore "github.com/libp2p/go-libp2p-peerstore" -) - -func makeAutoNATService(ctx context.Context, t *testing.T) (host.Host, *AutoNATService) { - h, err := libp2p.New(ctx, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) - if err != nil { - t.Fatal(err) - } - - as, err := NewAutoNATService(ctx, h) - if err != nil { - t.Fatal(err) - } - - return h, as -} - -func makeAutoNATClient(ctx context.Context, t *testing.T) (host.Host, AutoNATClient) { - h, err := libp2p.New(ctx, libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) - if err != nil { - t.Fatal(err) - } - - cli := NewAutoNATClient(h) - return h, cli -} - -func connect(t *testing.T, a, b host.Host) { - pinfo := pstore.PeerInfo{ID: a.ID(), Addrs: a.Addrs()} - err := b.Connect(context.Background(), pinfo) - if err != nil { - t.Fatal(err) - } -} - -// Note: these tests assume that the host has only private inet addresses! -func TestAutoNATServiceDialError(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - save := AutoNATServiceDialTimeout - AutoNATServiceDialTimeout = 1 * time.Second - - hs, _ := makeAutoNATService(ctx, t) - hc, ac := makeAutoNATClient(ctx, t) - connect(t, hs, hc) - - _, err := ac.DialBack(ctx, hs.ID()) - if err == nil { - t.Fatal("Dial back succeeded unexpectedly!") - } - - if !IsDialError(err) { - t.Fatal(err) - } - - AutoNATServiceDialTimeout = save -} - -func TestAutoNATServiceDialSuccess(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - save := private4 - private4 = []*net.IPNet{} - - hs, _ := makeAutoNATService(ctx, t) - hc, ac := makeAutoNATClient(ctx, t) - connect(t, hs, hc) - - _, err := ac.DialBack(ctx, hs.ID()) - if err != nil { - t.Fatalf("Dial back failed: %s", err.Error()) - } - - private4 = save -} - -func TestAutoNATServiceDialRateLimiter(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - save1 := AutoNATServiceDialTimeout - AutoNATServiceDialTimeout = 1 * time.Second - save2 := AutoNATServiceResetInterval - AutoNATServiceResetInterval = 1 * time.Second - save3 := AutoNATServiceThrottle - AutoNATServiceThrottle = 1 - save4 := private4 - private4 = []*net.IPNet{} - - hs, _ := makeAutoNATService(ctx, t) - hc, ac := makeAutoNATClient(ctx, t) - connect(t, hs, hc) - - _, err := ac.DialBack(ctx, hs.ID()) - if err != nil { - t.Fatal(err) - } - - _, err = ac.DialBack(ctx, hs.ID()) - if err == nil { - t.Fatal("Dial back succeeded unexpectedly!") - } - - if !IsDialRefused(err) { - t.Fatal(err) - } - - time.Sleep(2 * time.Second) - - _, err = ac.DialBack(ctx, hs.ID()) - if err != nil { - t.Fatal(err) - } - - AutoNATServiceDialTimeout = save1 - AutoNATServiceResetInterval = save2 - AutoNATServiceThrottle = save3 - private4 = save4 -}