Skip to content

Commit

Permalink
Merge pull request #598 from libp2p/feat/moar-relays
Browse files Browse the repository at this point in the history
autorelay: curtail addrsplosion
  • Loading branch information
Stebalien authored Apr 20, 2019
2 parents b0f4a6f + 8d073ce commit 9fc5c96
Show file tree
Hide file tree
Showing 5 changed files with 419 additions and 88 deletions.
167 changes: 167 additions & 0 deletions p2p/host/relay/addrsplosion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package relay

import (
"encoding/binary"

circuit "github.com/libp2p/go-libp2p-circuit"
ma "github.com/multiformats/go-multiaddr"
dns "github.com/multiformats/go-multiaddr-dns"
manet "github.com/multiformats/go-multiaddr-net"
)

// This function cleans up a relay's address set to remove private addresses and curtail
// addrsplosion.
func cleanupAddressSet(addrs []ma.Multiaddr) []ma.Multiaddr {
var public, private []ma.Multiaddr

for _, a := range addrs {
if isRelayAddr(a) {
continue
}

if manet.IsPublicAddr(a) || isDNSAddr(a) {
public = append(public, a)
continue
}

// discard unroutable addrs
if manet.IsPrivateAddr(a) {
private = append(private, a)
}
}

if !hasAddrsplosion(public) {
return public
}

return sanitizeAddrsplodedSet(public, private)
}

func isRelayAddr(a ma.Multiaddr) bool {
isRelay := false

ma.ForEach(a, func(c ma.Component) bool {
switch c.Protocol().Code {
case circuit.P_CIRCUIT:
isRelay = true
return false
default:
return true
}
})

return isRelay
}

func isDNSAddr(a ma.Multiaddr) bool {
if first, _ := ma.SplitFirst(a); first != nil {
switch first.Protocol().Code {
case dns.P_DNS4, dns.P_DNS6, dns.P_DNSADDR:
return true
}
}
return false
}

// we have addrsplosion if for some protocol we advertise multiple ports on
// the same base address.
func hasAddrsplosion(addrs []ma.Multiaddr) bool {
aset := make(map[string]int)

for _, a := range addrs {
key, port := addrKeyAndPort(a)
xport, ok := aset[key]
if ok && port != xport {
return true
}
aset[key] = port
}

return false
}

func addrKeyAndPort(a ma.Multiaddr) (string, int) {
var (
key string
port int
)

ma.ForEach(a, func(c ma.Component) bool {
switch c.Protocol().Code {
case ma.P_TCP, ma.P_UDP:
port = int(binary.BigEndian.Uint16(c.RawValue()))
key += "/" + c.Protocol().Name
default:
val := c.Value()
if val == "" {
val = c.Protocol().Name
}
key += "/" + val
}
return true
})

return key, port
}

// clean up addrsplosion
// the following heuristic is used:
// - for each base address/protocol combination, if there are multiple ports advertised then
// only accept the default port if present.
// - If the default port is not present, we check for non-standard ports by tracking
// private port bindings if present.
// - If there is no default or private port binding, then we can't infer the correct
// port and give up and return all addrs (for that base address)
func sanitizeAddrsplodedSet(public, private []ma.Multiaddr) []ma.Multiaddr {
type portAndAddr struct {
addr ma.Multiaddr
port int
}

privports := make(map[int]struct{})
pubaddrs := make(map[string][]portAndAddr)

for _, a := range private {
_, port := addrKeyAndPort(a)
privports[port] = struct{}{}
}

for _, a := range public {
key, port := addrKeyAndPort(a)
pubaddrs[key] = append(pubaddrs[key], portAndAddr{addr: a, port: port})
}

var result []ma.Multiaddr
for _, pas := range pubaddrs {
if len(pas) == 1 {
// it's not addrsploded
result = append(result, pas[0].addr)
continue
}

haveAddr := false
for _, pa := range pas {
if _, ok := privports[pa.port]; ok {
// it matches a privately bound port, use it
result = append(result, pa.addr)
haveAddr = true
continue
}

if pa.port == 4001 || pa.port == 4002 {
// it's a default port, use it
result = append(result, pa.addr)
haveAddr = true
}
}

if !haveAddr {
// we weren't able to select a port; bite the bullet and use them all
for _, pa := range pas {
result = append(result, pa.addr)
}
}
}

return result
}
122 changes: 122 additions & 0 deletions p2p/host/relay/addrsplosion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package relay

import (
"testing"

ma "github.com/multiformats/go-multiaddr"
_ "github.com/multiformats/go-multiaddr-dns"
)

func TestCleanupAddrs(t *testing.T) {
// test with no addrsplosion
addrs := makeAddrList(
"/ip4/127.0.0.1/tcp/4001",
"/ip4/127.0.0.1/udp/4002/quic",
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/udp/4002/quic",
"/dnsaddr/somedomain.com/tcp/4002/ws",
)
clean := makeAddrList(
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/udp/4002/quic",
"/dnsaddr/somedomain.com/tcp/4002/ws",
)

r := cleanupAddressSet(addrs)
if !sameAddrs(clean, r) {
t.Fatal("cleaned up set doesn't match expected")
}

// test with default port addrspolosion
addrs = makeAddrList(
"/ip4/127.0.0.1/tcp/4001",
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/tcp/33333",
"/ip4/1.2.3.4/tcp/33334",
"/ip4/1.2.3.4/tcp/33335",
"/ip4/1.2.3.4/udp/4002/quic",
)
clean = makeAddrList(
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/udp/4002/quic",
)
r = cleanupAddressSet(addrs)
if !sameAddrs(clean, r) {
t.Fatal("cleaned up set doesn't match expected")
}

// test with default port addrsplosion but no private addrs
addrs = makeAddrList(
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/tcp/33333",
"/ip4/1.2.3.4/tcp/33334",
"/ip4/1.2.3.4/tcp/33335",
"/ip4/1.2.3.4/udp/4002/quic",
)
clean = makeAddrList(
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/udp/4002/quic",
)
r = cleanupAddressSet(addrs)
if !sameAddrs(clean, r) {
t.Fatal("cleaned up set doesn't match expected")
}

// test with non-standard port addrsplosion
addrs = makeAddrList(
"/ip4/127.0.0.1/tcp/12345",
"/ip4/1.2.3.4/tcp/12345",
"/ip4/1.2.3.4/tcp/33333",
"/ip4/1.2.3.4/tcp/33334",
"/ip4/1.2.3.4/tcp/33335",
)
clean = makeAddrList(
"/ip4/1.2.3.4/tcp/12345",
)
r = cleanupAddressSet(addrs)
if !sameAddrs(clean, r) {
t.Fatal("cleaned up set doesn't match expected")
}

// test with a squeaky clean address set
addrs = makeAddrList(
"/ip4/1.2.3.4/tcp/4001",
"/ip4/1.2.3.4/udp/4001/quic",
)
clean = addrs
r = cleanupAddressSet(addrs)
if !sameAddrs(clean, r) {
t.Fatal("cleaned up set doesn't match expected")
}
}

func makeAddrList(strs ...string) []ma.Multiaddr {
result := make([]ma.Multiaddr, 0, len(strs))
for _, s := range strs {
a := ma.StringCast(s)
result = append(result, a)
}
return result
}

func sameAddrs(as, bs []ma.Multiaddr) bool {
if len(as) != len(bs) {
return false
}

for _, a := range as {
if !findAddr(a, bs) {
return false
}
}
return true
}

func findAddr(a ma.Multiaddr, bs []ma.Multiaddr) bool {
for _, b := range bs {
if a.Equal(b) {
return true
}
}
return false
}
Loading

0 comments on commit 9fc5c96

Please sign in to comment.