Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/tcpip/stack/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ go_test(
"neighbor_entry_test.go",
"nic_test.go",
"packet_buffer_test.go",
"save_restore_test.go",
],
library = ":stack",
deps = [
Expand Down
14 changes: 10 additions & 4 deletions pkg/tcpip/stack/conntrack.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,16 @@ func (cn *conn) update(pkt *PacketBuffer, reply bool) {
//
// +stateify savable
type ConnTrack struct {
// seed is a one-time random value initialized at stack startup
// and is used in the calculation of hash keys for the list of buckets.
// It is immutable.
seed uint32
// seed is a one-time random value used in conntrack bucket index hashing.
//
// The field is tagged state:"nosave" because the hash seed is
// regeneratable at restore time. The Stack-level afterLoad in
// save_restore.go redraws this field from secureRNG. Persisting the
// pre-checkpoint seed would extend the brute-force window across save
// boundaries and freeze the bucket layout in a way that is observable
// from the checkpoint state stream once IPTables S/R lands
// (gvisor.dev/issue/4595).
seed uint32 `state:"nosave"`

// clock provides timing used to determine conntrack reapings.
clock tcpip.Clock
Expand Down
9 changes: 9 additions & 0 deletions pkg/tcpip/stack/save_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,13 @@ func (s *Stack) beforeSave() {
func (s *Stack) afterLoad(context.Context) {
s.insecureRNG = rand.New(rand.NewSource(time.Now().UnixNano()))
s.secureRNG = cryptorand.RNGFrom(cryptorand.Reader)
// TODO(gvisor.dev/issue/4595): When Stack.tables becomes savable and
// ConnTrack flows into checkpoint state, the seed must be redrawn here
// AND the entries in s.tables.connections.buckets must be rehashed
// under the new seed. bucket_index = jenkins.Sum32(seed) % len(buckets)
// couples the seed value to bucket layout; redrawing seed without
// rehashing leaves restored entries unreachable by Lookup. The
// state:"nosave" tag on ConnTrack.seed in conntrack.go is a safety
// contract: it prevents accidental serialization of the pre-restore
// seed while the rehash strategy is pending design.
}
37 changes: 37 additions & 0 deletions pkg/tcpip/stack/save_restore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package stack

import (
"reflect"
"testing"
)

// TestConnTrackSeedHasNosaveTag is a structural assertion that the seed
// field on ConnTrack carries the state:"nosave" tag. Removing the tag
// would let the bucket hash seed flow into checkpoint state once
// IPTables S/R lands (gvisor.dev/issue/4595). The seed must not be
// persisted across save/restore; see the TODO in save_restore.go.
func TestConnTrackSeedHasNosaveTag(t *testing.T) {
var ct ConnTrack
typ := reflect.TypeOf(ct)
f, ok := typ.FieldByName("seed")
if !ok {
t.Fatalf("field seed not found on ConnTrack")
}
if got, want := f.Tag.Get("state"), "nosave"; got != want {
t.Errorf("ConnTrack.seed state-tag = %q, want %q", got, want)
}
}