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
13 changes: 13 additions & 0 deletions cmd/gvproxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"runtime"
"slices"
"strconv"
"strings"

"github.com/containers/gvisor-tap-vsock/pkg/transport"
Expand All @@ -24,6 +25,7 @@ const (
)

type GvproxyArgs struct {
tcpTimeWaitTimeout *int
config string
endpoints arrayFlags
debug bool
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Expand Down
31 changes: 31 additions & 0 deletions cmd/gvproxy/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ interfaces:
InputConfig: `stack:
tcpMaxInFlight: 500
tcpConnectTimeout: 10
tcpTimeWaitTimeout: 1
`,
ResultConfig: `log-level: info
stack:
Expand All @@ -739,7 +740,37 @@ stack:
- 192.168.127.254
tcpMaxInFlight: 500
tcpConnectTimeout: 10
tcpTimeWaitTimeout: 1
`,
},
}
}

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 }
3 changes: 3 additions & 0 deletions pkg/types/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions pkg/virtualnetwork/virtualnetwork.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) > math.MaxInt64/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)
}
Expand All @@ -42,6 +53,15 @@ func (n *VirtualNetwork) SetAPIToken(token string) {
}

func New(configuration *types.Configuration) (*VirtualNetwork, error) {
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

ipPool, err := tap.NewIPPool(configuration.Subnet)
Expand Down Expand Up @@ -82,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)
}
Expand Down Expand Up @@ -124,7 +144,7 @@ func (n *VirtualNetwork) BytesReceived() uint64 {
return n.networkSwitch.Received
}

func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint) (*stack.Stack, error) {
func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint, timeWaitTimeout *time.Duration) (*stack.Stack, error) {
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
Expand All @@ -137,6 +157,13 @@ func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint
},
})

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)
}
}

if err := s.CreateNIC(1, endpoint); err != nil {
return nil, errors.New(err.String())
}
Expand Down
126 changes: 126 additions & 0 deletions pkg/virtualnetwork/virtualnetwork_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package virtualnetwork

import (
"os"
"path/filepath"
"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: "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: intPtr(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,
}
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
}
if err != nil {
t.Fatal(err)
}
timeout = &parsed
}
s, err := createStack(configuration, loopback.New(), timeout)
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)
}
})
}
}

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)
}
})
}
}
Loading