This repository was archived by the owner on Sep 9, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
base limits on reservations issued #132
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8706bab
base limits on reservations issued
marten-seemann 168a600
switch default reservation limits per peer and per IP
marten-seemann 2dbf65a
don't export the constructor for relay.constraints
marten-seemann 88d9aab
panic when reading from crypto/rand fails
marten-seemann e1f7b86
optimize IP-based reservation lookup
marten-seemann 6fe8142
use lists instead of maps to save reservations
marten-seemann eff0cbe
save expiry timestamp in reservations
marten-seemann 54c4a93
use slices instead of linked lists for reservations
marten-seemann 0ebfb5e
remove unused rand in constraints
marten-seemann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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).There was a problem hiding this comment.
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.