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

base limits on reservations issued #132

Merged
merged 9 commits into from
Aug 5, 2021
135 changes: 135 additions & 0 deletions v2/relay/constraints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package relay

import (
crand "crypto/rand"
"encoding/binary"
"errors"
"math/rand"
"sync"
"time"

asnutil "github.com/libp2p/go-libp2p-asn-util"
"github.com/libp2p/go-libp2p-core/peer"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)

var validity = 30 * time.Minute

var (
errTooManyReservations = errors.New("too many reservations")
errTooManyReservationsForPeer = errors.New("too many reservations for peer")
errTooManyReservationsForIP = errors.New("too many peers for IP address")
errTooManyReservationsForASN = errors.New("too many peers for ASN")
)

// constraints implements various reservation constraints
type constraints struct {
rc *Resources

mutex sync.Mutex
rand rand.Rand
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved
total []time.Time
peers map[peer.ID][]time.Time
ips map[string][]time.Time
asns map[string][]time.Time
}

// newConstraints creates a new constraints object.
// The methods are *not* thread-safe; an external lock must be held if synchronization
// is required.
func newConstraints(rc *Resources) *constraints {
b := make([]byte, 8)
if _, err := crand.Read(b); err != nil {
panic("failed to read from crypto/rand")
}
random := rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(b))))

return &constraints{
rc: rc,
rand: *random,
peers: make(map[peer.ID][]time.Time),
ips: make(map[string][]time.Time),
asns: make(map[string][]time.Time),
}
}

// AddReservation adds a reservation for a given peer with a given multiaddr.
// If adding this reservation violates IP constraints, an error is returned.
func (c *constraints) AddReservation(p peer.ID, a ma.Multiaddr) error {
c.mutex.Lock()
defer c.mutex.Unlock()

now := time.Now()
c.cleanup(now)

if len(c.total) >= c.rc.MaxReservations {
return errTooManyReservations
}

ip, err := manet.ToIP(a)
if err != nil {
return errors.New("no IP address associated with peer")
}

peerReservations := c.peers[p]
if len(peerReservations) >= c.rc.MaxReservationsPerPeer {
return errTooManyReservationsForPeer
}

ipReservations := c.ips[ip.String()]
if len(ipReservations) >= c.rc.MaxReservationsPerIP {
return errTooManyReservationsForIP
}

var asnReservations []time.Time
var asn string
if ip.To4() == nil {
asn, _ = asnutil.Store.AsnForIPv6(ip)
if asn != "" {
asnReservations = c.asns[asn]
if len(asnReservations) >= c.rc.MaxReservationsPerASN {
return errTooManyReservationsForASN
}
}
}

expiry := now.Add(validity)
c.total = append(c.total, expiry)

peerReservations = append(peerReservations, expiry)
c.peers[p] = peerReservations

ipReservations = append(ipReservations, expiry)
c.ips[ip.String()] = ipReservations

if asn != "" {
asnReservations = append(asnReservations, expiry)
c.asns[asn] = asnReservations
}
return nil
}

func (c *constraints) cleanupList(l []time.Time, now time.Time) []time.Time {
var index int
for i, t := range l {
if t.After(now) {
break
}
index = i + 1
}
return l[index:]
}

func (c *constraints) cleanup(now time.Time) {
c.total = c.cleanupList(c.total, now)
for k, peerReservations := range c.peers {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small optimization: we can return early when nothing has expired (when c.total wasn't updated).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right, but I'm not sure if it's worth it. This seems like an easy place to introduce non-obvious bugs in the future.

c.peers[k] = c.cleanupList(peerReservations, now)
}
for k, ipReservations := range c.ips {
c.ips[k] = c.cleanupList(ipReservations, now)
}
for k, asnReservations := range c.asns {
c.asns[k] = c.cleanupList(asnReservations, now)
}
}
142 changes: 142 additions & 0 deletions v2/relay/constraints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package relay

import (
"crypto/rand"
"fmt"
"math"
"net"
"testing"
"time"

"github.com/libp2p/go-libp2p-core/test"
ma "github.com/multiformats/go-multiaddr"
)

func randomIPv4Addr(t *testing.T) ma.Multiaddr {
t.Helper()
b := make([]byte, 4)
rand.Read(b)
addr, err := ma.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/1234", net.IP(b)))
if err != nil {
t.Fatal(err)
}
return addr
}

func TestConstraints(t *testing.T) {
infResources := func() *Resources {
return &Resources{
MaxReservations: math.MaxInt32,
MaxReservationsPerPeer: math.MaxInt32,
MaxReservationsPerIP: math.MaxInt32,
MaxReservationsPerASN: math.MaxInt32,
}
}
const limit = 7

t.Run("total reservations", func(t *testing.T) {
res := infResources()
res.MaxReservations = limit
c := newConstraints(res)
for i := 0; i < limit; i++ {
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
t.Fatal(err)
}
}
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != errTooManyReservations {
t.Fatalf("expected to run into total reservation limit, got %v", err)
}
})

t.Run("reservations per peer", func(t *testing.T) {
p := test.RandPeerIDFatal(t)
res := infResources()
res.MaxReservationsPerPeer = limit
c := newConstraints(res)
for i := 0; i < limit; i++ {
if err := c.AddReservation(p, randomIPv4Addr(t)); err != nil {
t.Fatal(err)
}
}
if err := c.AddReservation(p, randomIPv4Addr(t)); err != errTooManyReservationsForPeer {
t.Fatalf("expected to run into total reservation limit, got %v", err)
}
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
t.Fatalf("expected reservation for different peer to be possible, got %v", err)
}
})

t.Run("reservations per IP", func(t *testing.T) {
ip := randomIPv4Addr(t)
res := infResources()
res.MaxReservationsPerIP = limit
c := newConstraints(res)
for i := 0; i < limit; i++ {
if err := c.AddReservation(test.RandPeerIDFatal(t), ip); err != nil {
t.Fatal(err)
}
}
if err := c.AddReservation(test.RandPeerIDFatal(t), ip); err != errTooManyReservationsForIP {
t.Fatalf("expected to run into total reservation limit, got %v", err)
}
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
t.Fatalf("expected reservation for different IP to be possible, got %v", err)
}
})

t.Run("reservations per ASN", func(t *testing.T) {
getAddr := func(t *testing.T, ip net.IP) ma.Multiaddr {
t.Helper()
addr, err := ma.NewMultiaddr(fmt.Sprintf("/ip6/%s/tcp/1234", ip))
if err != nil {
t.Fatal(err)
}
return addr
}

res := infResources()
res.MaxReservationsPerASN = limit
c := newConstraints(res)
const ipv6Prefix = "2a03:2880:f003:c07:face:b00c::"
for i := 0; i < limit; i++ {
addr := getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, i+1)))
if err := c.AddReservation(test.RandPeerIDFatal(t), addr); err != nil {
t.Fatal(err)
}
}
if err := c.AddReservation(test.RandPeerIDFatal(t), getAddr(t, net.ParseIP(fmt.Sprintf("%s%d", ipv6Prefix, 42)))); err != errTooManyReservationsForASN {
t.Fatalf("expected to run into total reservation limit, got %v", err)
}
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
t.Fatalf("expected reservation for different IP to be possible, got %v", err)
}
})
}

func TestConstraintsCleanup(t *testing.T) {
origValidity := validity
defer func() { validity = origValidity }()
validity = 500 * time.Millisecond

const limit = 7
res := &Resources{
MaxReservations: limit,
MaxReservationsPerPeer: math.MaxInt32,
MaxReservationsPerIP: math.MaxInt32,
MaxReservationsPerASN: math.MaxInt32,
}
c := newConstraints(res)
for i := 0; i < limit; i++ {
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
t.Fatal(err)
}
}
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != errTooManyReservations {
t.Fatalf("expected to run into total reservation limit, got %v", err)
}

time.Sleep(validity + time.Millisecond)
if err := c.AddReservation(test.RandPeerIDFatal(t), randomIPv4Addr(t)); err != nil {
t.Fatalf("expected old reservations to have been garbage collected, %v", err)
}
}
110 changes: 0 additions & 110 deletions v2/relay/ipcs.go

This file was deleted.

Loading