From e79833b852e0fe409a8380a8e9d3802fedb5c1e6 Mon Sep 17 00:00:00 2001 From: Eduard Zateev Date: Sat, 24 Oct 2026 10:25:43 +0200 Subject: [PATCH 1/2] virtualnetwork: configure TCP time-wait timeout Signed-off-by: Eduard Zateev --- cmd/gvproxy/config_test.go | 2 + pkg/types/configuration.go | 3 ++ pkg/virtualnetwork/virtualnetwork.go | 27 +++++++++++ pkg/virtualnetwork/virtualnetwork_test.go | 58 +++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 pkg/virtualnetwork/virtualnetwork_test.go diff --git a/cmd/gvproxy/config_test.go b/cmd/gvproxy/config_test.go index 2fcbf3ac9..bd8289be9 100644 --- a/cmd/gvproxy/config_test.go +++ b/cmd/gvproxy/config_test.go @@ -724,6 +724,7 @@ interfaces: InputConfig: `stack: tcpMaxInFlight: 500 tcpConnectTimeout: 10 + tcpTimeWaitTimeout: 1 `, ResultConfig: `log-level: info stack: @@ -739,6 +740,7 @@ stack: - 192.168.127.254 tcpMaxInFlight: 500 tcpConnectTimeout: 10 + tcpTimeWaitTimeout: 1 `, }, } diff --git a/pkg/types/configuration.go b/pkg/types/configuration.go index 613b32a14..1fd0f4692 100644 --- a/pkg/types/configuration.go +++ b/pkg/types/configuration.go @@ -65,6 +65,9 @@ type Configuration struct { // Timeout in seconds for outbound TCP connection attempts (default: 30) TCPConnectTimeout int `yaml:"tcpConnectTimeout,omitempty"` + + // Timeout in seconds for TCP connections in TIME-WAIT (default: 60) + TCPTimeWaitTimeout int `yaml:"tcpTimeWaitTimeout,omitempty"` } type Protocol string diff --git a/pkg/virtualnetwork/virtualnetwork.go b/pkg/virtualnetwork/virtualnetwork.go index b2b446144..74d195e28 100644 --- a/pkg/virtualnetwork/virtualnetwork.go +++ b/pkg/virtualnetwork/virtualnetwork.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/containers/gvisor-tap-vsock/pkg/notification" "github.com/containers/gvisor-tap-vsock/pkg/services/forwarder" @@ -33,6 +34,16 @@ type VirtualNetwork struct { apiToken string // Bearer token for API authentication } +func parseTCPTimeWaitTimeout(timeoutSeconds int) (time.Duration, error) { + if timeoutSeconds < 0 { + return 0, errors.New("TCP time-wait timeout must not be negative") + } + if int64(timeoutSeconds) > (1<<63-1)/int64(time.Second) { + return 0, errors.New("TCP time-wait timeout is too large") + } + return time.Duration(timeoutSeconds) * time.Second, nil +} + func (n *VirtualNetwork) SetNotificationSender(notificationSender *notification.NotificationSender) { n.networkSwitch.SetNotificationSender(notificationSender) } @@ -42,6 +53,10 @@ func (n *VirtualNetwork) SetAPIToken(token string) { } func New(configuration *types.Configuration) (*VirtualNetwork, error) { + if _, err := parseTCPTimeWaitTimeout(configuration.TCPTimeWaitTimeout); err != nil { + return nil, err + } + var endpoint stack.LinkEndpoint ipPool, err := tap.NewIPPool(configuration.Subnet) @@ -125,6 +140,11 @@ func (n *VirtualNetwork) BytesReceived() uint64 { } func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint) (*stack.Stack, error) { + timeWaitTimeout, err := parseTCPTimeWaitTimeout(configuration.TCPTimeWaitTimeout) + if err != nil { + return nil, err + } + s := stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ ipv4.NewProtocol, @@ -137,6 +157,13 @@ func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint }, }) + if timeWaitTimeout > 0 { + tcpTimeWaitTimeout := tcpip.TCPTimeWaitTimeoutOption(timeWaitTimeout) + if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTimeWaitTimeout); err != nil { + return nil, fmt.Errorf("cannot set TCP time-wait timeout: %s", err) + } + } + if err := s.CreateNIC(1, endpoint); err != nil { return nil, errors.New(err.String()) } diff --git a/pkg/virtualnetwork/virtualnetwork_test.go b/pkg/virtualnetwork/virtualnetwork_test.go new file mode 100644 index 000000000..a930a9dd1 --- /dev/null +++ b/pkg/virtualnetwork/virtualnetwork_test.go @@ -0,0 +1,58 @@ +package virtualnetwork + +import ( + "strconv" + "testing" + "time" + + "github.com/containers/gvisor-tap-vsock/pkg/types" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/link/loopback" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" +) + +func TestCreateStackTCPTimeWaitTimeout(t *testing.T) { + type testCase struct { + name string + timeout int + want time.Duration + wantErr bool + } + tests := []testCase{ + {name: "default", want: tcp.DefaultTCPTimeWaitTimeout}, + {name: "configured", timeout: 1, want: time.Second}, + {name: "negative", timeout: -1, wantErr: true}, + } + if strconv.IntSize == 64 { + timeoutTooLarge := int64((1<<63-1)/int64(time.Second)) + 1 + tests = append(tests, testCase{name: "too large", timeout: int(timeoutTooLarge), wantErr: true}) + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + configuration := &types.Configuration{ + GatewayIP: "192.168.127.1", + Subnet: "192.168.127.0/24", + TCPTimeWaitTimeout: test.timeout, + } + s, err := createStack(configuration, loopback.New()) + if test.wantErr { + if err == nil { + t.Fatal("createStack succeeded, want error") + } + return + } + if err != nil { + t.Fatalf("createStack: %v", err) + } + + var got tcpip.TCPTimeWaitTimeoutOption + if err := s.TransportProtocolOption(tcp.ProtocolNumber, &got); err != nil { + t.Fatalf("TransportProtocolOption: %v", err) + } + if got != tcpip.TCPTimeWaitTimeoutOption(test.want) { + t.Fatalf("TCP time-wait timeout = %s, want %s", time.Duration(got), test.want) + } + }) + } +} From dbd1e15f46991d4a9d26665f6bdb9d5fba6027f2 Mon Sep 17 00:00:00 2001 From: Eduard Zateev Date: Sat, 7 Nov 2026 22:42:19 -0500 Subject: [PATCH 2/2] virtualnetwork: address TCP time-wait review feedback Signed-off-by: Eduard Zateev --- cmd/gvproxy/config.go | 13 ++++ cmd/gvproxy/config_test.go | 29 ++++++++ pkg/types/configuration.go | 2 +- pkg/virtualnetwork/virtualnetwork.go | 24 +++---- pkg/virtualnetwork/virtualnetwork_test.go | 86 ++++++++++++++++++++--- 5 files changed, 132 insertions(+), 22 deletions(-) diff --git a/cmd/gvproxy/config.go b/cmd/gvproxy/config.go index 16cbad470..eb632d0bf 100644 --- a/cmd/gvproxy/config.go +++ b/cmd/gvproxy/config.go @@ -10,6 +10,7 @@ import ( "os" "runtime" "slices" + "strconv" "strings" "github.com/containers/gvisor-tap-vsock/pkg/transport" @@ -24,6 +25,7 @@ const ( ) type GvproxyArgs struct { + tcpTimeWaitTimeout *int config string endpoints arrayFlags debug bool @@ -121,6 +123,14 @@ func GvproxyArgParse(flagSet *flag.FlagSet, args *GvproxyArgs, argv []string) (* flagSet.Var(&args.endpoints, "listen", "control endpoint") flagSet.BoolVar(&args.debug, "debug", false, "Print debug info") flagSet.StringVar(&args.pcapFile, "pcap", "", "Capture network traffic to a pcap file") + flagSet.Func("tcp-time-wait-timeout", "TCP TIME-WAIT timeout in seconds (default: 60)", func(value string) error { + timeout, err := strconv.Atoi(value) + if err != nil { + return err + } + args.tcpTimeWaitTimeout = &timeout + return nil + }) flagSet.IntVar(&args.mtu, "mtu", 0, "Set the MTU (default: 1500)") flagSet.IntVar(&args.sshPort, "ssh-port", 2222, "Port to access the guest virtual machine. Must be between 1024 and 65535") flagSet.StringVar(&args.subnet, "subnet", "192.168.127.0/24", "Subnet address in CIDR format, e.g. 192.168.127.0/24") @@ -269,6 +279,9 @@ func GvproxyConfigure(config *GvproxyConfig, args *GvproxyArgs, version string) // Default vpnkit mac addresses enabled only for the default mode // Patch config with CLI args + if args.tcpTimeWaitTimeout != nil { + config.Stack.TCPTimeWaitTimeout = args.tcpTimeWaitTimeout + } if args.qemuSocket != "" { config.Interfaces.Qemu = args.qemuSocket } diff --git a/cmd/gvproxy/config_test.go b/cmd/gvproxy/config_test.go index bd8289be9..eb8666477 100644 --- a/cmd/gvproxy/config_test.go +++ b/cmd/gvproxy/config_test.go @@ -745,3 +745,32 @@ stack: }, } } + +func TestTCPTimeWaitCLI(t *testing.T) { + for _, tc := range []struct { + name string + argv []string + yaml string + want *int + }{ + {name: "omitted", want: nil}, + {name: "yaml", yaml: "stack:\n tcpTimeWaitTimeout: 5\n", want: ptrInt(5)}, + {name: "yaml zero", yaml: "stack:\n tcpTimeWaitTimeout: 0\n", want: ptrInt(0)}, + {name: "explicit zero overrides yaml", argv: []string{"-tcp-time-wait-timeout=0"}, yaml: "stack:\n tcpTimeWaitTimeout: 5\n", want: ptrInt(0)}, + {name: "positive", argv: []string{"-tcp-time-wait-timeout=2"}, want: ptrInt(2)}, + } { + t.Run(tc.name, func(t *testing.T) { + var args GvproxyArgs + flags := flag.NewFlagSet("test", flag.ContinueOnError) + _, err := GvproxyArgParse(flags, &args, tc.argv) + require.NoError(t, err) + var config GvproxyConfig + require.NoError(t, yaml.Unmarshal([]byte(tc.yaml), &config)) + _, err = GvproxyConfigure(&config, &args, "test") + require.NoError(t, err) + assert.Equal(t, tc.want, config.Stack.TCPTimeWaitTimeout) + }) + } +} + +func ptrInt(n int) *int { return &n } diff --git a/pkg/types/configuration.go b/pkg/types/configuration.go index 1fd0f4692..2a9dc59fa 100644 --- a/pkg/types/configuration.go +++ b/pkg/types/configuration.go @@ -67,7 +67,7 @@ type Configuration struct { TCPConnectTimeout int `yaml:"tcpConnectTimeout,omitempty"` // Timeout in seconds for TCP connections in TIME-WAIT (default: 60) - TCPTimeWaitTimeout int `yaml:"tcpTimeWaitTimeout,omitempty"` + TCPTimeWaitTimeout *int `yaml:"tcpTimeWaitTimeout,omitempty"` } type Protocol string diff --git a/pkg/virtualnetwork/virtualnetwork.go b/pkg/virtualnetwork/virtualnetwork.go index 74d195e28..772b5cf63 100644 --- a/pkg/virtualnetwork/virtualnetwork.go +++ b/pkg/virtualnetwork/virtualnetwork.go @@ -38,7 +38,7 @@ func parseTCPTimeWaitTimeout(timeoutSeconds int) (time.Duration, error) { if timeoutSeconds < 0 { return 0, errors.New("TCP time-wait timeout must not be negative") } - if int64(timeoutSeconds) > (1<<63-1)/int64(time.Second) { + if int64(timeoutSeconds) > math.MaxInt64/int64(time.Second) { return 0, errors.New("TCP time-wait timeout is too large") } return time.Duration(timeoutSeconds) * time.Second, nil @@ -53,8 +53,13 @@ func (n *VirtualNetwork) SetAPIToken(token string) { } func New(configuration *types.Configuration) (*VirtualNetwork, error) { - if _, err := parseTCPTimeWaitTimeout(configuration.TCPTimeWaitTimeout); err != nil { - return nil, err + var timeWaitTimeout *time.Duration + if configuration.TCPTimeWaitTimeout != nil { + timeout, err := parseTCPTimeWaitTimeout(*configuration.TCPTimeWaitTimeout) + if err != nil { + return nil, err + } + timeWaitTimeout = &timeout } var endpoint stack.LinkEndpoint @@ -97,7 +102,7 @@ func New(configuration *types.Configuration) (*VirtualNetwork, error) { } else { endpoint = tapEndpoint } - stack, err := createStack(configuration, endpoint) + stack, err := createStack(configuration, endpoint, timeWaitTimeout) if err != nil { return nil, fmt.Errorf("cannot create network stack: %w", err) } @@ -139,12 +144,7 @@ func (n *VirtualNetwork) BytesReceived() uint64 { return n.networkSwitch.Received } -func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint) (*stack.Stack, error) { - timeWaitTimeout, err := parseTCPTimeWaitTimeout(configuration.TCPTimeWaitTimeout) - if err != nil { - return nil, err - } - +func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint, timeWaitTimeout *time.Duration) (*stack.Stack, error) { s := stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ ipv4.NewProtocol, @@ -157,8 +157,8 @@ func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint }, }) - if timeWaitTimeout > 0 { - tcpTimeWaitTimeout := tcpip.TCPTimeWaitTimeoutOption(timeWaitTimeout) + if timeWaitTimeout != nil { + tcpTimeWaitTimeout := tcpip.TCPTimeWaitTimeoutOption(*timeWaitTimeout) if err := s.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTimeWaitTimeout); err != nil { return nil, fmt.Errorf("cannot set TCP time-wait timeout: %s", err) } diff --git a/pkg/virtualnetwork/virtualnetwork_test.go b/pkg/virtualnetwork/virtualnetwork_test.go index a930a9dd1..aa347594e 100644 --- a/pkg/virtualnetwork/virtualnetwork_test.go +++ b/pkg/virtualnetwork/virtualnetwork_test.go @@ -1,6 +1,8 @@ package virtualnetwork import ( + "os" + "path/filepath" "strconv" "testing" "time" @@ -14,18 +16,19 @@ import ( func TestCreateStackTCPTimeWaitTimeout(t *testing.T) { type testCase struct { name string - timeout int + timeout *int want time.Duration wantErr bool } tests := []testCase{ {name: "default", want: tcp.DefaultTCPTimeWaitTimeout}, - {name: "configured", timeout: 1, want: time.Second}, - {name: "negative", timeout: -1, wantErr: true}, + {name: "zero", timeout: intPtr(0), want: 0}, + {name: "configured", timeout: intPtr(1), want: time.Second}, + {name: "negative", timeout: intPtr(-1), wantErr: true}, } if strconv.IntSize == 64 { timeoutTooLarge := int64((1<<63-1)/int64(time.Second)) + 1 - tests = append(tests, testCase{name: "too large", timeout: int(timeoutTooLarge), wantErr: true}) + tests = append(tests, testCase{name: "too large", timeout: intPtr(int(timeoutTooLarge)), wantErr: true}) } for _, test := range tests { @@ -35,13 +38,21 @@ func TestCreateStackTCPTimeWaitTimeout(t *testing.T) { Subnet: "192.168.127.0/24", TCPTimeWaitTimeout: test.timeout, } - s, err := createStack(configuration, loopback.New()) - if test.wantErr { - if err == nil { - t.Fatal("createStack succeeded, want error") + var timeout *time.Duration + if test.timeout != nil { + parsed, err := parseTCPTimeWaitTimeout(*test.timeout) + if test.wantErr { + if err == nil { + t.Fatal("expected invalid timeout error") + } + return } - return + if err != nil { + t.Fatal(err) + } + timeout = &parsed } + s, err := createStack(configuration, loopback.New(), timeout) if err != nil { t.Fatalf("createStack: %v", err) } @@ -56,3 +67,60 @@ func TestCreateStackTCPTimeWaitTimeout(t *testing.T) { }) } } + +func intPtr(n int) *int { return &n } + +func TestNewTCPTimeWaitTimeout(t *testing.T) { + for _, tc := range []struct { + name string + timeout *int + want time.Duration + }{ + {name: "default", want: tcp.DefaultTCPTimeWaitTimeout}, + {name: "zero", timeout: intPtr(0), want: 0}, + {name: "configured", timeout: intPtr(2), want: 2 * time.Second}, + } { + t.Run(tc.name, func(t *testing.T) { + n, err := New(&types.Configuration{ + Subnet: "192.168.127.0/24", GatewayIP: "192.168.127.1", + GatewayMacAddress: "5a:94:ef:e4:0c:dd", MTU: 1500, + TCPTimeWaitTimeout: tc.timeout, + }) + if err != nil { + t.Fatal(err) + } + defer n.Close() + defer n.stack.Close() + var got tcpip.TCPTimeWaitTimeoutOption + if err := n.stack.TransportProtocolOption(tcp.ProtocolNumber, &got); err != nil { + t.Fatal(err) + } + if time.Duration(got) != tc.want { + t.Fatalf("timeout = %v, want %v", time.Duration(got), tc.want) + } + }) + } +} + +func TestInvalidTCPTimeWaitPreservesCapture(t *testing.T) { + timeouts := []int{-1} + if strconv.IntSize == 64 { + timeouts = append(timeouts, int(int64((1<<63-1)/int64(time.Second))+1)) + } + for _, timeout := range timeouts { + t.Run(strconv.Itoa(timeout), func(t *testing.T) { + path := filepath.Join(t.TempDir(), "capture.pcap") + if err := os.WriteFile(path, []byte("existing"), 0600); err != nil { + t.Fatal(err) + } + _, err := New(&types.Configuration{TCPTimeWaitTimeout: intPtr(timeout), CaptureFile: path}) + if err == nil { + t.Fatal("expected invalid timeout error") + } + data, err := os.ReadFile(path) + if err != nil || string(data) != "existing" { + t.Fatalf("capture changed: %q, %v", data, err) + } + }) + } +}