Skip to content

Commit

Permalink
holepunch: add test for nat traversal
Browse files Browse the repository at this point in the history
  • Loading branch information
sukunrt committed Jul 26, 2023
1 parent 1edbe87 commit 4bb87b6
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 3 deletions.
211 changes: 211 additions & 0 deletions p2p/protocol/holepunch/holepunch_test.go
Expand Up @@ -2,7 +2,9 @@ package holepunch_test

import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"testing"
"time"
Expand All @@ -13,12 +15,18 @@ import (
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/libp2p/go-libp2p/p2p/host/autonat"
"github.com/libp2p/go-libp2p/p2p/host/autorelay"
basichost "github.com/libp2p/go-libp2p/p2p/host/basic"
swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
holepunch_pb "github.com/libp2p/go-libp2p/p2p/protocol/holepunch/pb"
"github.com/libp2p/go-libp2p/p2p/protocol/identify"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
"tailscale.com/tstest/natlab"

"github.com/libp2p/go-msgio/pbio"
ma "github.com/multiformats/go-multiaddr"
Expand Down Expand Up @@ -511,3 +519,206 @@ func mkHostWithHolePunchSvc(t *testing.T, opts ...holepunch.Option) (host.Host,
require.NoError(t, err)
return h, hps
}

type TestHost struct {
H host.Host
Addr ma.Multiaddr
}

func TestNATHost(t *testing.T) {
natFactory := NewNatHostFactory()
var public []TestHost
for i := 0; i < 10; i++ {
h, addr := natFactory.NewPublicHost(t)
public = append(public, TestHost{H: h, Addr: addr})
}
relay, addr := natFactory.NewPublicHost(t)
natHost := natFactory.NewNATHost(t, peer.AddrInfo{ID: relay.ID(), Addrs: []ma.Multiaddr{addr}})

h := natHost.H
for i := 0; i < 5; i++ {
err := h.Connect(context.Background(), peer.AddrInfo{ID: public[i].H.ID(), Addrs: []ma.Multiaddr{public[i].Addr}})
if err != nil {
t.Fatal(err)
}
}

require.Eventually(t, func() bool {
return len(ma.FilterAddrs(natHost.idService.OwnObservedAddrs(), manet.IsPublicAddr)) > 0
}, 5*time.Second, 100*time.Millisecond)

h2 := public[5]
for i := 0; i < 5; i++ {
err := h2.H.Connect(context.Background(), peer.AddrInfo{ID: public[i].H.ID(), Addrs: []ma.Multiaddr{public[i].Addr}})
if err != nil {
t.Fatal(err)
}
}

natHost2 := natFactory.NewNATHost(t, peer.AddrInfo{ID: relay.ID(), Addrs: []ma.Multiaddr{addr}})
h3 := natHost2.H
for i := 0; i < 5; i++ {
err := h3.Connect(context.Background(), peer.AddrInfo{ID: public[i].H.ID(), Addrs: []ma.Multiaddr{public[i].Addr}})
if err != nil {
t.Fatal(err)
}
}
require.Eventually(t, func() bool {
return len(ma.FilterAddrs(natHost2.idService.OwnObservedAddrs(), manet.IsPublicAddr)) > 0
}, 5*time.Second, 100*time.Millisecond)

time.Sleep(10 * time.Second)

var relayAddr ma.Multiaddr
for _, addr := range h.Addrs() {
if _, err := addr.ValueForProtocol(ma.P_CIRCUIT); err == nil {
relayAddr = addr
break
}
}
if relayAddr == nil {
t.Fatal("no relay reservations obtained")
}

err := h2.H.Connect(context.Background(), peer.AddrInfo{ID: h.ID(), Addrs: []ma.Multiaddr{relayAddr}})
if err != nil {
t.Fatal(err)
}

require.Eventually(t, func() bool {
conns := h.Network().ConnsToPeer(h2.H.ID())
for _, c := range conns {
if _, err := c.RemoteMultiaddr().ValueForProtocol(ma.P_CIRCUIT); err != nil {
return true
}
}
return false
}, 5*time.Second, 100*time.Millisecond)

err = h3.Connect(context.Background(), peer.AddrInfo{ID: h.ID(), Addrs: []ma.Multiaddr{relayAddr}})
if err != nil {
t.Fatal(err)
}

require.Eventually(t, func() bool {
conns := h.Network().ConnsToPeer(h3.ID())
for _, c := range conns {
if _, err := c.RemoteMultiaddr().ValueForProtocol(ma.P_CIRCUIT); err != nil {
return true
}
}
return false
}, 5*time.Second, 100*time.Millisecond)
}

type NATHostFactory struct {
inet *natlab.Network
hostCnt int
nextPort int
}

type NATHost struct {
H host.Host
idService identify.IDService
}

func NewNatHostFactory() *NATHostFactory {
return &NATHostFactory{inet: &natlab.Network{Prefix4: netip.MustParsePrefix("1.0.0.0/8")}, nextPort: 2000}
}

func (n *NATHostFactory) NewNATHost(t *testing.T, relayInfo peer.AddrInfo) NATHost {
hostID := n.hostCnt
n.hostCnt++
nat := &natlab.Machine{Name: fmt.Sprintf("NAT-%d", hostID)}
lan := &natlab.Network{Name: fmt.Sprintf("LAN-%d", hostID), Prefix4: netip.MustParsePrefix("192.168.0.0/24")}
natWAN := nat.Attach("wan", n.inet)
natLAN := nat.Attach(fmt.Sprintf("LAN-%d", hostID), lan)
lan.SetDefaultGateway(natLAN)
nat.PacketHandler = &natlab.SNAT44{
Machine: nat,
ExternalInterface: natWAN,
Firewall: &natlab.Firewall{
TrustedInterface: natLAN,
},
}

m := &natlab.Machine{Name: fmt.Sprintf("host-%d", hostID), PacketHandler: &natlab.Firewall{}}
m.Attach("eth0", lan)
port := n.nextPort
n.nextPort++
swarm := swarmt.GenSwarm(
t,
swarmt.OptDisableTCP,
swarmt.OptQUICListenAddress(ma.StringCast(fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", port))),
swarmt.OptUDPTransport(swarmt.UDPTransport(m)),
)
upgrader := swarmt.GenUpgrader(t, swarm, nil)
host, err := basichost.NewHost(swarm, &basichost.HostOpts{EnableRelayService: false, EnableHolePunching: true})
if err != nil {
t.Fatal(err)
}
err = client.AddTransport(host, upgrader)
if err != nil {
t.Fatal(err)
}
autonat, err := autonat.New(host, autonat.WithReachability(network.ReachabilityPrivate))
if err != nil {
t.Fatal(err)
}
host.SetAutoNat(autonat)
idService := host.IDService()

var ar *autorelay.AutoRelay
if relayInfo.ID != "" {
ar, err = autorelay.NewAutoRelay(host, autorelay.WithStaticRelays([]peer.AddrInfo{relayInfo}))
if err != nil {
t.Fatal(err)
}
ar.Start()
}
if err != nil {
t.Fatal(err)
}
if ar != nil {
arhost := autorelay.NewAutoRelayHost(host, ar)
arhost.Start()
idService.Start()
return NATHost{H: arhost, idService: idService}
}
host.Start()
return NATHost{H: host, idService: idService}
}

func (n *NATHostFactory) NewPublicHost(t *testing.T) (host.Host, ma.Multiaddr) {
hostID := n.hostCnt
n.hostCnt++
m := &natlab.Machine{Name: fmt.Sprintf("host-%d", hostID)}
mif := m.Attach("eth0", n.inet)
port := n.nextPort
n.nextPort++
swarm := swarmt.GenSwarm(
t,
swarmt.OptDisableTCP,
swarmt.OptQUICListenAddress(ma.StringCast(fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", port))),
swarmt.OptUDPTransport(swarmt.UDPTransport(m)))
host, err := basichost.NewHost(swarm, &basichost.HostOpts{
EnableRelayService: true,
})
if err != nil {
t.Fatal(err)
}
upgrader := swarmt.GenUpgrader(t, swarm, nil)
err = client.AddTransport(host, upgrader)
if err != nil {
t.Fatal(err)
}

autonat, err := autonat.New(host, autonat.WithReachability(network.ReachabilityPublic))
if err != nil {
t.Fatal(err)
}
host.SetAutoNat(autonat)

host.Start()
return host, ma.StringCast(fmt.Sprintf("/ip4/%s/udp/%d/quic-v1", mif.V4().String(), port))
}
3 changes: 0 additions & 3 deletions p2p/protocol/holepunch/holepuncher.go
Expand Up @@ -129,7 +129,6 @@ func (hp *holePuncher) directConnect(rp peer.ID) error {
}

log.Debugw("got inbound proxy conn", "peer", rp)

// hole punch
for i := 1; i <= maxRetries; i++ {
addrs, obsAddrs, rtt, err := hp.initiateHolePunch(rp)
Expand Down Expand Up @@ -263,14 +262,12 @@ type netNotifiee holePuncher

func (nn *netNotifiee) Connected(_ network.Network, conn network.Conn) {
hs := (*holePuncher)(nn)

// Hole punch if it's an inbound proxy connection.
// If we already have a direct connection with the remote peer, this will be a no-op.
if conn.Stat().Direction == network.DirInbound && isRelayAddress(conn.RemoteMultiaddr()) {
hs.refCount.Add(1)
go func() {
defer hs.refCount.Done()

select {
// waiting for Identify here will allow us to access the peer's public and observed addresses
// that we can dial to for a hole punch.
Expand Down

0 comments on commit 4bb87b6

Please sign in to comment.