-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
base limits on reservations issued (#132)
* 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
1 parent
d9a0316
commit 39cf03b
Showing
6 changed files
with
279 additions
and
197 deletions.
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.