Skip to content

Commit

Permalink
cleanup nat addr handling; add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sukunrt committed Dec 3, 2024
1 parent e8dba37 commit e3367f1
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 83 deletions.
173 changes: 90 additions & 83 deletions p2p/host/basic/address_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ type addressService struct {
ctx context.Context
ctxCancel context.CancelFunc

wg sync.WaitGroup
updateLocalIPv4Backoff backoff.ExpBackoff
updateLocalIPv6Backoff backoff.ExpBackoff
wg sync.WaitGroup

ifaceAddrs *interfaceAddrsCache
}
Expand Down Expand Up @@ -226,91 +224,15 @@ func (a *addressService) AllAddrs() []ma.Multiaddr {
return nil
}

filteredIfaceAddrs := a.ifaceAddrs.Filtered()

// Iterate over all _unresolved_ listen addresses, resolving our primary
// interface only to avoid advertising too many addresses.
finalAddrs := make([]ma.Multiaddr, 0, 8)
if resolved, err := manet.ResolveUnspecifiedAddresses(listenAddrs, filteredIfaceAddrs); err != nil {
// This can happen if we're listening on no addrs, or listening
// on IPv6 addrs, but only have IPv4 interface addrs.
log.Debugw("failed to resolve listen addrs", "error", err)
} else {
finalAddrs = append(finalAddrs, resolved...)
}

finalAddrs = ma.Unique(finalAddrs)
finalAddrs = a.appendInterfaceAddrs(finalAddrs, listenAddrs)

// use nat mappings if we have them
if a.natmgr != nil && a.natmgr.HasDiscoveredNAT() {
// We have successfully mapped ports on our NAT. Use those
// instead of observed addresses (mostly).
// Next, apply this mapping to our addresses.
for _, listen := range listenAddrs {
extMaddr := a.natmgr.GetMapping(listen)
if extMaddr == nil {
// not mapped
continue
}

// if the router reported a sane address
if !manet.IsIPUnspecified(extMaddr) {
// Add in the mapped addr.
finalAddrs = append(finalAddrs, extMaddr)
} else {
log.Warn("NAT device reported an unspecified IP as it's external address")
}

// Did the router give us a routable public addr?
if manet.IsPublicAddr(extMaddr) {
// well done
continue
}

// No.
// in case the router gives us a wrong address or we're behind a double-NAT.
// also add observed addresses
resolved, err := manet.ResolveUnspecifiedAddress(listen, a.ifaceAddrs.All())
if err != nil {
// This can happen if we try to resolve /ip6/::/...
// without any IPv6 interface addresses.
continue
}

for _, addr := range resolved {
// Now, check if we have any observed addresses that
// differ from the one reported by the router. Routers
// don't always give the most accurate information.
observed := a.ids.ObservedAddrsFor(addr)

if len(observed) == 0 {
continue
}

// Drop the IP from the external maddr
_, extMaddrNoIP := ma.SplitFirst(extMaddr)

for _, obsMaddr := range observed {
// Extract a public observed addr.
ip, _ := ma.SplitFirst(obsMaddr)
if ip == nil || !manet.IsPublicAddr(ip) {
continue
}

finalAddrs = append(finalAddrs, ma.Join(ip, extMaddrNoIP))
}
}
}
} else {
var observedAddrs []ma.Multiaddr
if a.ids != nil {
observedAddrs = a.ids.OwnObservedAddrs()
}
finalAddrs = append(finalAddrs, observedAddrs...)
}
finalAddrs = a.appendNATAddrs(finalAddrs, listenAddrs)
finalAddrs = ma.Unique(finalAddrs)

// Remove /p2p-circuit addresses from the list.
// The p2p-circuit tranport listener reports its address as just /p2p-circuit
// The p2p-circuit transport listener reports its address as just /p2p-circuit
// This is useless for dialing. Users need to manage their circuit addresses themselves,
// or use AutoRelay.
finalAddrs = slices.DeleteFunc(finalAddrs, func(a ma.Multiaddr) bool {
Expand All @@ -322,6 +244,35 @@ func (a *addressService) AllAddrs() []ma.Multiaddr {
return finalAddrs
}

func (a *addressService) appendInterfaceAddrs(result []ma.Multiaddr, listenAddrs []ma.Multiaddr) []ma.Multiaddr {
// resolving any unspecified listen addressees to use only the primary
// interface to avoid advertising too many addresses.
if resolved, err := manet.ResolveUnspecifiedAddresses(listenAddrs, a.ifaceAddrs.Filtered()); err != nil {
log.Warnw("failed to resolve listen addrs", "error", err)
} else {
result = append(result, resolved...)
}
result = ma.Unique(result)
return result
}

func (a *addressService) appendNATAddrs(result []ma.Multiaddr, listenAddrs []ma.Multiaddr) []ma.Multiaddr {
ifaceAddrs := a.ifaceAddrs.All()
// use nat mappings if we have them
if a.natmgr != nil && a.natmgr.HasDiscoveredNAT() {
// we have a NAT device
for _, listen := range listenAddrs {
extMaddr := a.natmgr.GetMapping(listen)
result = appendValidNATAddrs(result, listen, extMaddr, a.ids.ObservedAddrsFor, ifaceAddrs)
}
} else {
if a.ids != nil {
result = append(result, a.ids.OwnObservedAddrs()...)
}
}
return result
}

func (a *addressService) addCertHashes(addrs []ma.Multiaddr) []ma.Multiaddr {
// This is a temporary workaround/hack that fixes #2233. Once we have a
// proper address pipeline, rework this. See the issue for more context.
Expand Down Expand Up @@ -536,3 +487,59 @@ func (i *interfaceAddrsCache) updateUnlocked() {
}
}
}

func getAllPossibleLocalAddrs(listenAddr ma.Multiaddr, ifaceAddrs []ma.Multiaddr) []ma.Multiaddr {
// If the nat mapping fails, use the observed addrs
resolved, err := manet.ResolveUnspecifiedAddress(listenAddr, ifaceAddrs)
if err != nil {
log.Warnf("failed to resolve listen addr %s, %s: %s", listenAddr, ifaceAddrs, err)
return nil
}
return append(resolved, listenAddr)
}

// appendValidNATAddrs adds the NAT-ed addresses to the result. If the NAT device doesn't provide
// us with a public IP address, we use the observed addresses.
func appendValidNATAddrs(result []ma.Multiaddr, listenAddr ma.Multiaddr, natMapping ma.Multiaddr,
obsAddrsFunc func(ma.Multiaddr) []ma.Multiaddr,
ifaceAddrs []ma.Multiaddr) []ma.Multiaddr {
if natMapping == nil {
allAddrs := getAllPossibleLocalAddrs(listenAddr, ifaceAddrs)
for _, a := range allAddrs {
result = append(result, obsAddrsFunc(a)...)
}
return result
}

// if the router reported a sane address, use it.
if !manet.IsIPUnspecified(natMapping) {
result = append(result, natMapping)
} else {
log.Warn("NAT device reported an unspecified IP as it's external address")
}

// If the router gave us a public address, use it and ignore observed addresses
if manet.IsPublicAddr(natMapping) {
return result
}

// Router gave us a private IP; maybe we're behind a CGNAT.
// See if we have a public IP from observed addresses.
_, extMaddrNoIP := ma.SplitFirst(natMapping)
if extMaddrNoIP == nil {
return result
}

allAddrs := getAllPossibleLocalAddrs(listenAddr, ifaceAddrs)
for _, addr := range allAddrs {
for _, obsMaddr := range obsAddrsFunc(addr) {
// Extract a public observed addr.
ip, _ := ma.SplitFirst(obsMaddr)
if ip == nil || !manet.IsPublicAddr(ip) {
continue
}
result = append(result, ma.Join(ip, extMaddrNoIP))
}
}
return result
}
98 changes: 98 additions & 0 deletions p2p/host/basic/address_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package basichost

import (
"testing"

ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/stretchr/testify/require"
)

func TestAppendNATAddrs(t *testing.T) {
if1, if2 := ma.StringCast("/ip4/192.168.0.100"), ma.StringCast("/ip4/1.1.1.1")
ifaceAddrs := []ma.Multiaddr{if1, if2}
tcpListenAddr, udpListenAddr := ma.StringCast("/ip4/0.0.0.0/tcp/1"), ma.StringCast("/ip4/0.0.0.0/udp/2/quic-v1")
cases := []struct {
Name string
Listen ma.Multiaddr
Nat ma.Multiaddr
ObsAddrFunc func(ma.Multiaddr) []ma.Multiaddr
Expected []ma.Multiaddr
}{
{
Name: "nat map success",
// nat mapping success, obsaddress ignored
Listen: ma.StringCast("/ip4/0.0.0.0/udp/1/quic-v1"),
Nat: ma.StringCast("/ip4/1.1.1.1/udp/10/quic-v1"),
ObsAddrFunc: func(m ma.Multiaddr) []ma.Multiaddr {
return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/udp/100/quic-v1")}
},
Expected: []ma.Multiaddr{ma.StringCast("/ip4/1.1.1.1/udp/10/quic-v1")},
},
{
Name: "nat map failure",
//nat mapping fails, obs addresses added
Listen: ma.StringCast("/ip4/0.0.0.0/tcp/1"),
Nat: nil,
ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
ip, _ := ma.SplitFirst(a)
if ip == nil {
return nil
}
if ip.Equal(if1) {
return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/100")}
} else {
return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/tcp/100")}
}
},
Expected: []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/100"), ma.StringCast("/ip4/3.3.3.3/tcp/100")},
},
{
Name: "nat map success but CGNAT",
//nat addr added, obs address added with nat provided port
Listen: tcpListenAddr,
Nat: ma.StringCast("/ip4/100.100.1.1/tcp/100"),
ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
ip, _ := ma.SplitFirst(a)
if ip == nil {
return nil
}
if ip.Equal(if1) {
return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/20")}
} else {
return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/tcp/30")}
}
},
Expected: []ma.Multiaddr{
ma.StringCast("/ip4/100.100.1.1/tcp/100"),
ma.StringCast("/ip4/2.2.2.2/tcp/100"),
ma.StringCast("/ip4/3.3.3.3/tcp/100"),
},
},
{
Name: "uses unspecified address for obs address",
// observed address manager should be queries with both specified and unspecified addresses
// udp observed addresses are mapped to unspecified addresses
Listen: udpListenAddr,
Nat: nil,
ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
if manet.IsIPUnspecified(a) {
return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/udp/20/quic-v1")}
}
return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/udp/20/quic-v1")}
},
Expected: []ma.Multiaddr{
ma.StringCast("/ip4/2.2.2.2/udp/20/quic-v1"),
ma.StringCast("/ip4/3.3.3.3/udp/20/quic-v1"),
},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
res := appendValidNATAddrs(nil,
tc.Listen, tc.Nat, tc.ObsAddrFunc, ifaceAddrs)
res = ma.Unique(res)
require.ElementsMatch(t, tc.Expected, res, "%s\n%s", tc.Expected, res)
})
}
}

0 comments on commit e3367f1

Please sign in to comment.