@@ -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.
8793type 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
0 commit comments