Skip to content

Commit

Permalink
base limits on reservations issued (#132)
Browse files Browse the repository at this point in the history
* base limits on reservations issued

* switch default reservation limits per peer and per IP

* don't export the constructor for relay.constraints

* panic when reading from crypto/rand fails

* optimize IP-based reservation lookup

* use lists instead of maps to save reservations

* save expiry timestamp in reservations

* use slices instead of linked lists for reservations

* remove unused rand in constraints
  • Loading branch information
marten-seemann committed Aug 5, 2021
1 parent 7a72c33 commit c67489d
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 197 deletions.
124 changes: 124 additions & 0 deletions p2p/host/circuitv2/relay/constraints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package relay

import (
"errors"
"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
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 {
return &constraints{
rc: rc,
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 {
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 p2p/host/circuitv2/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 p2p/host/circuitv2/relay/ipcs.go

This file was deleted.

Loading

0 comments on commit c67489d

Please sign in to comment.