Skip to content

Commit eb03b35

Browse files
committed
net/udprelay: replace VNI pool with selection algorithm (tailscale#17868)
This reduces memory usage when tailscaled is acting as a peer relay. Updates tailscale#17801 Signed-off-by: Jordan Whited <jordan@tailscale.com> (cherry picked from commit f4f9dd7)
1 parent 771a9d2 commit eb03b35

File tree

2 files changed

+56
-12
lines changed

2 files changed

+56
-12
lines changed

net/udprelay/server.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,17 @@ type Server struct {
7777
addrPorts []netip.AddrPort // the ip:port pairs returned as candidate endpoints
7878
closed bool
7979
lamportID uint64
80-
vniPool []uint32 // the pool of available VNIs
80+
nextVNI uint32
8181
byVNI map[uint32]*serverEndpoint
8282
byDisco map[key.SortedPairOfDiscoPublic]*serverEndpoint
8383
}
8484

85+
const (
86+
minVNI = uint32(1)
87+
maxVNI = uint32(1<<24 - 1)
88+
totalPossibleVNI = maxVNI - minVNI + 1
89+
)
90+
8591
// serverEndpoint contains Server-internal [endpoint.ServerEndpoint] state.
8692
// serverEndpoint methods are not thread-safe.
8793
type serverEndpoint struct {
@@ -281,15 +287,10 @@ func NewServer(logf logger.Logf, port int, overrideAddrs []netip.Addr) (s *Serve
281287
steadyStateLifetime: defaultSteadyStateLifetime,
282288
closeCh: make(chan struct{}),
283289
byDisco: make(map[key.SortedPairOfDiscoPublic]*serverEndpoint),
290+
nextVNI: minVNI,
284291
byVNI: make(map[uint32]*serverEndpoint),
285292
}
286293
s.discoPublic = s.disco.Public()
287-
// TODO: instead of allocating 10s of MBs for the full pool, allocate
288-
// smaller chunks and increase as needed
289-
s.vniPool = make([]uint32, 0, 1<<24-1)
290-
for i := 1; i < 1<<24; i++ {
291-
s.vniPool = append(s.vniPool, uint32(i))
292-
}
293294

294295
// TODO(creachadair): Find a way to plumb this in during initialization.
295296
// As-written, messages published here will not be seen by other components
@@ -557,7 +558,6 @@ func (s *Server) Close() error {
557558
defer s.mu.Unlock()
558559
clear(s.byVNI)
559560
clear(s.byDisco)
560-
s.vniPool = nil
561561
s.closed = true
562562
s.bus.Close()
563563
})
@@ -579,7 +579,6 @@ func (s *Server) endpointGCLoop() {
579579
if v.isExpired(now, s.bindLifetime, s.steadyStateLifetime) {
580580
delete(s.byDisco, k)
581581
delete(s.byVNI, v.vni)
582-
s.vniPool = append(s.vniPool, v.vni)
583582
}
584583
}
585584
}
@@ -714,6 +713,27 @@ func (e ErrServerNotReady) Error() string {
714713
return fmt.Sprintf("server not ready, retry after %v", e.RetryAfter)
715714
}
716715

716+
// getNextVNILocked returns the next available VNI. It implements the
717+
// "Traditional BSD Port Selection Algorithm" from RFC6056. This algorithm does
718+
// not attempt to obfuscate the selection, i.e. the selection is predictable.
719+
// For now, we favor simplicity and reducing VNI re-use over more complex
720+
// ephemeral port (VNI) selection algorithms.
721+
func (s *Server) getNextVNILocked() (uint32, error) {
722+
for i := uint32(0); i < totalPossibleVNI; i++ {
723+
vni := s.nextVNI
724+
if vni == maxVNI {
725+
s.nextVNI = minVNI
726+
} else {
727+
s.nextVNI++
728+
}
729+
_, ok := s.byVNI[vni]
730+
if !ok {
731+
return vni, nil
732+
}
733+
}
734+
return 0, errors.New("VNI pool exhausted")
735+
}
736+
717737
// AllocateEndpoint allocates an [endpoint.ServerEndpoint] for the provided pair
718738
// of [key.DiscoPublic]'s. If an allocation already exists for discoA and discoB
719739
// it is returned without modification/reallocation. AllocateEndpoint returns
@@ -762,19 +782,20 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.Serv
762782
}, nil
763783
}
764784

765-
if len(s.vniPool) == 0 {
766-
return endpoint.ServerEndpoint{}, errors.New("VNI pool exhausted")
785+
vni, err := s.getNextVNILocked()
786+
if err != nil {
787+
return endpoint.ServerEndpoint{}, err
767788
}
768789

769790
s.lamportID++
770791
e = &serverEndpoint{
771792
discoPubKeys: pair,
772793
lamportID: s.lamportID,
773794
allocatedAt: time.Now(),
795+
vni: vni,
774796
}
775797
e.discoSharedSecrets[0] = s.disco.Shared(e.discoPubKeys.Get()[0])
776798
e.discoSharedSecrets[1] = s.disco.Shared(e.discoPubKeys.Get()[1])
777-
e.vni, s.vniPool = s.vniPool[0], s.vniPool[1:]
778799

779800
s.byDisco[pair] = e
780801
s.byVNI[e.vni] = e

net/udprelay/server_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111
"time"
1212

13+
qt "github.com/frankban/quicktest"
1314
"github.com/google/go-cmp/cmp"
1415
"github.com/google/go-cmp/cmp/cmpopts"
1516
"go4.org/mem"
@@ -319,3 +320,25 @@ func TestServer(t *testing.T) {
319320
})
320321
}
321322
}
323+
324+
func TestServer_getNextVNILocked(t *testing.T) {
325+
t.Parallel()
326+
c := qt.New(t)
327+
s := &Server{
328+
nextVNI: minVNI,
329+
byVNI: make(map[uint32]*serverEndpoint),
330+
}
331+
for i := uint64(0); i < uint64(totalPossibleVNI); i++ {
332+
vni, err := s.getNextVNILocked()
333+
if err != nil { // using quicktest here triples test time
334+
t.Fatal(err)
335+
}
336+
s.byVNI[vni] = nil
337+
}
338+
c.Assert(s.nextVNI, qt.Equals, minVNI)
339+
_, err := s.getNextVNILocked()
340+
c.Assert(err, qt.IsNotNil)
341+
delete(s.byVNI, minVNI)
342+
_, err = s.getNextVNILocked()
343+
c.Assert(err, qt.IsNil)
344+
}

0 commit comments

Comments
 (0)