diff --git a/pkg/sentry/socket/netstack/BUILD b/pkg/sentry/socket/netstack/BUILD index ce61fe80d..f801bb501 100644 --- a/pkg/sentry/socket/netstack/BUILD +++ b/pkg/sentry/socket/netstack/BUILD @@ -12,6 +12,7 @@ go_library( "netstack_state.go", "provider.go", "save_restore.go", + "socketopt_custom.go", "stack.go", "tun.go", ], diff --git a/pkg/sentry/socket/netstack/netstack.go b/pkg/sentry/socket/netstack/netstack.go index 9c319009a..808fe1d2d 100644 --- a/pkg/sentry/socket/netstack/netstack.go +++ b/pkg/sentry/socket/netstack/netstack.go @@ -1107,6 +1107,10 @@ func getSockOptSocket(t *kernel.Task, s socket.Socket, ep commonEndpoint, family v := primitive.Int32(ep.SocketOptions().GetRcvlowat()) return &v, nil + default: + if v, err, handled := getSockOptSocketCustom(t, s, ep, name, outLen); handled { + return v, err + } } return nil, syserr.ErrProtocolNotAvailable } @@ -2028,6 +2032,10 @@ func setSockOptSocket(t *kernel.Task, s socket.Socket, ep commonEndpoint, name i v := hostarch.ByteOrder.Uint32(optVal) ep.SocketOptions().SetRcvlowat(int32(v)) return nil + default: + if err, handled := setSockOptSocketCustom(t, s, ep, name, optVal); handled { + return err + } } return nil diff --git a/pkg/sentry/socket/netstack/socketopt_custom.go b/pkg/sentry/socket/netstack/socketopt_custom.go new file mode 100644 index 000000000..41a5cd391 --- /dev/null +++ b/pkg/sentry/socket/netstack/socketopt_custom.go @@ -0,0 +1,39 @@ +// Copyright 2024 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. + +//go:build !false +// +build !false + +package netstack + +import ( + "gvisor.dev/gvisor/pkg/marshal" + "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/socket" + "gvisor.dev/gvisor/pkg/syserr" +) + +// setSockOptSocketCustom handles SetSockOpt options not handled by setSockOptSocket. +// It returns a bool indicating whether the option was handled in addition to +// return values from setSockOptSocket. +func setSockOptSocketCustom(t *kernel.Task, s socket.Socket, ep commonEndpoint, name int, optVal []byte) (*syserr.Error, bool) { + return nil, false +} + +// getSockOptSocketCustom handles GetSockOpt options not handled by getSockOptSocket. +// It returns a bool indicating whether the option was handled in addition to +// return values from getSockOptSocket. +func getSockOptSocketCustom(t *kernel.Task, s socket.Socket, ep commonEndpoint, name int, outLen int) (marshal.Marshallable, *syserr.Error, bool) { + return nil, nil, false +} diff --git a/pkg/tcpip/socketops.go b/pkg/tcpip/socketops.go index b8196912f..668cd4951 100644 --- a/pkg/tcpip/socketops.go +++ b/pkg/tcpip/socketops.go @@ -263,6 +263,10 @@ type SocketOptions struct { // rcvlowat specifies the minimum number of bytes which should be // received to indicate the socket as readable. rcvlowat atomicbitops.Int32 + + // experimentOptionValue is the value set for the IP option experiment header + // if it is not zero. + experimentOptionValue atomicbitops.Uint32 } // InitHandler initializes the handler. This must be called before using the @@ -539,6 +543,17 @@ func (so *SocketOptions) SetLinger(linger LingerOption) { so.mu.Unlock() } +// GetExperimentOptionValue gets value for the experiment IP option header. +func (so *SocketOptions) GetExperimentOptionValue() uint16 { + v := so.experimentOptionValue.Load() + return uint16(v) +} + +// SetExperimentOptionValue sets the value for the experiment IP option header. +func (so *SocketOptions) SetExperimentOptionValue(v uint16) { + so.experimentOptionValue.Store(uint32(v)) +} + // SockErrOrigin represents the constants for error origin. type SockErrOrigin uint8 diff --git a/pkg/tcpip/stack/registration.go b/pkg/tcpip/stack/registration.go index 78b2162c3..ee5952f9b 100644 --- a/pkg/tcpip/stack/registration.go +++ b/pkg/tcpip/stack/registration.go @@ -322,6 +322,10 @@ type NetworkHeaderParams struct { // DF indicates whether the DF bit should be set. DF bool + + // ExperimentOptionValue is a 16 bit value that is set for the IP experiment + // option headers if it is not zero. + ExperimentOptionValue uint16 } // GroupAddressableEndpoint is an endpoint that supports group addressing. diff --git a/pkg/tcpip/transport/internal/network/endpoint.go b/pkg/tcpip/transport/internal/network/endpoint.go index 9b77ae36d..3adb6b7fc 100644 --- a/pkg/tcpip/transport/internal/network/endpoint.go +++ b/pkg/tcpip/transport/internal/network/endpoint.go @@ -345,9 +345,10 @@ func (c *WriteContext) WritePacket(pkt *stack.PacketBuffer, headerIncluded bool) } err := c.route.WritePacket(stack.NetworkHeaderParams{ - Protocol: c.e.transProto, - TTL: c.ttl, - TOS: c.tos, + Protocol: c.e.transProto, + TTL: c.ttl, + TOS: c.tos, + ExperimentOptionValue: c.e.ops.GetExperimentOptionValue(), }, pkt) if _, ok := err.(*tcpip.ErrNoBufferSpace); ok { diff --git a/pkg/tcpip/transport/tcp/accept.go b/pkg/tcpip/transport/tcp/accept.go index adcfdcfd5..3f1c3ce03 100644 --- a/pkg/tcpip/transport/tcp/accept.go +++ b/pkg/tcpip/transport/tcp/accept.go @@ -526,13 +526,14 @@ func (e *Endpoint) handleListenSegment(ctx *listenContext, s *segment) tcpip.Err } cookie := ctx.createCookie(s.id, s.sequenceNumber, encodeMSS(opts.MSS)) fields := tcpFields{ - id: s.id, - ttl: calculateTTL(route, e.ipv4TTL, e.ipv6HopLimit), - tos: e.sendTOS, - flags: header.TCPFlagSyn | header.TCPFlagAck, - seq: cookie, - ack: s.sequenceNumber + 1, - rcvWnd: ctx.rcvWnd, + id: s.id, + ttl: calculateTTL(route, e.ipv4TTL, e.ipv6HopLimit), + tos: e.sendTOS, + flags: header.TCPFlagSyn | header.TCPFlagAck, + seq: cookie, + ack: s.sequenceNumber + 1, + rcvWnd: ctx.rcvWnd, + expOptVal: e.SocketOptions().GetExperimentOptionValue(), } if err := e.sendSynTCP(route, fields, synOpts); err != nil { return err diff --git a/pkg/tcpip/transport/tcp/connect.go b/pkg/tcpip/transport/tcp/connect.go index eada8450b..3aaa7158d 100644 --- a/pkg/tcpip/transport/tcp/connect.go +++ b/pkg/tcpip/transport/tcp/connect.go @@ -364,13 +364,14 @@ func (h *handshake) synSentState(s *segment) tcpip.Error { ttl = h.ep.route.DefaultTTL() } h.ep.sendSynTCP(h.ep.route, tcpFields{ - id: h.ep.TransportEndpointInfo.ID, - ttl: ttl, - tos: h.ep.sendTOS, - flags: h.flags, - seq: h.iss, - ack: h.ackNum, - rcvWnd: h.rcvWnd, + id: h.ep.TransportEndpointInfo.ID, + ttl: ttl, + tos: h.ep.sendTOS, + flags: h.flags, + seq: h.iss, + ack: h.ackNum, + rcvWnd: h.rcvWnd, + expOptVal: h.ep.SocketOptions().GetExperimentOptionValue(), }, synOpts) return nil } @@ -450,13 +451,14 @@ func (h *handshake) synRcvdState(s *segment) tcpip.Error { MSS: h.ep.amss, } h.ep.sendSynTCP(h.ep.route, tcpFields{ - id: h.ep.TransportEndpointInfo.ID, - ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), - tos: h.ep.sendTOS, - flags: h.flags, - seq: h.iss, - ack: h.ackNum, - rcvWnd: h.rcvWnd, + id: h.ep.TransportEndpointInfo.ID, + ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), + tos: h.ep.sendTOS, + flags: h.flags, + seq: h.iss, + ack: h.ackNum, + rcvWnd: h.rcvWnd, + expOptVal: h.ep.SocketOptions().GetExperimentOptionValue(), }, synOpts) return nil } @@ -587,13 +589,14 @@ func (h *handshake) start() { h.sendSYNOpts = synOpts h.ep.sendSynTCP(h.ep.route, tcpFields{ - id: h.ep.TransportEndpointInfo.ID, - ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), - tos: h.ep.sendTOS, - flags: h.flags, - seq: h.iss, - ack: h.ackNum, - rcvWnd: h.rcvWnd, + id: h.ep.TransportEndpointInfo.ID, + ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), + tos: h.ep.sendTOS, + flags: h.flags, + seq: h.iss, + ack: h.ackNum, + rcvWnd: h.rcvWnd, + expOptVal: h.ep.SocketOptions().GetExperimentOptionValue(), }, synOpts) } @@ -623,13 +626,14 @@ func (h *handshake) retransmitHandlerLocked() tcpip.Error { // retransmitted on their own). if h.active || !h.acked || h.deferAccept != 0 && e.stack.Clock().NowMonotonic().Sub(h.startTime) > h.deferAccept { e.sendSynTCP(e.route, tcpFields{ - id: e.TransportEndpointInfo.ID, - ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit), - tos: e.sendTOS, - flags: h.flags, - seq: h.iss, - ack: h.ackNum, - rcvWnd: h.rcvWnd, + id: e.TransportEndpointInfo.ID, + ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit), + tos: e.sendTOS, + flags: h.flags, + seq: h.iss, + ack: h.ackNum, + rcvWnd: h.rcvWnd, + expOptVal: e.SocketOptions().GetExperimentOptionValue(), }, h.sendSYNOpts) // If we have ever retransmitted the SYN-ACK or // SYN segment, we should only measure RTT if @@ -800,16 +804,17 @@ func makeSynOptions(opts header.TCPSynOptions) []byte { // tcpFields is a struct to carry different parameters required by the // send*TCP variant functions below. type tcpFields struct { - id stack.TransportEndpointID - ttl uint8 - tos uint8 - flags header.TCPFlags - seq seqnum.Value - ack seqnum.Value - rcvWnd seqnum.Size - opts []byte - txHash uint32 - df bool + id stack.TransportEndpointID + ttl uint8 + tos uint8 + flags header.TCPFlags + seq seqnum.Value + ack seqnum.Value + rcvWnd seqnum.Size + opts []byte + txHash uint32 + df bool + expOptVal uint16 } func (e *Endpoint) sendSynTCP(r *stack.Route, tf tcpFields, opts header.TCPSynOptions) tcpip.Error { @@ -897,7 +902,13 @@ func sendTCPBatch(r *stack.Route, tf tcpFields, pkt *stack.PacketBuffer, gso sta buildTCPHdr(r, tf, pkt, gso) tf.seq = tf.seq.Add(seqnum.Size(packetSize)) pkt.GSOOptions = gso - if err := r.WritePacket(stack.NetworkHeaderParams{Protocol: ProtocolNumber, TTL: tf.ttl, TOS: tf.tos, DF: tf.df}, pkt); err != nil { + if err := r.WritePacket(stack.NetworkHeaderParams{ + Protocol: ProtocolNumber, + TTL: tf.ttl, + TOS: tf.tos, + DF: tf.df, + ExperimentOptionValue: tf.expOptVal, + }, pkt); err != nil { r.Stats().TCP.SegmentSendErrors.Increment() if shouldSplitPacket { pkt.DecRef() @@ -929,7 +940,13 @@ func sendTCP(r *stack.Route, tf tcpFields, pkt *stack.PacketBuffer, gso stack.GS pkt.Owner = owner buildTCPHdr(r, tf, pkt, gso) - if err := r.WritePacket(stack.NetworkHeaderParams{Protocol: ProtocolNumber, TTL: tf.ttl, TOS: tf.tos, DF: tf.df}, pkt); err != nil { + if err := r.WritePacket(stack.NetworkHeaderParams{ + Protocol: ProtocolNumber, + TTL: tf.ttl, + TOS: tf.tos, + DF: tf.df, + ExperimentOptionValue: tf.expOptVal, + }, pkt); err != nil { r.Stats().TCP.SegmentSendErrors.Increment() return err } diff --git a/pkg/tcpip/transport/tcp/protocol.go b/pkg/tcpip/transport/tcp/protocol.go index fa552a665..0e1acb6b4 100644 --- a/pkg/tcpip/transport/tcp/protocol.go +++ b/pkg/tcpip/transport/tcp/protocol.go @@ -235,14 +235,19 @@ func replyWithReset(st *stack.Stack, s *segment, tos, ipv4TTL uint8, ipv6HopLimi p := stack.NewPacketBuffer(stack.PacketBufferOptions{ReserveHeaderBytes: header.TCPMinimumSize + int(route.MaxHeaderLength())}) defer p.DecRef() + var expOptVal uint16 + if s.ep != nil { + expOptVal = s.ep.SocketOptions().GetExperimentOptionValue() + } return sendTCP(route, tcpFields{ - id: s.id, - ttl: ttl, - tos: tos, - flags: flags, - seq: seq, - ack: ack, - rcvWnd: 0, + id: s.id, + ttl: ttl, + tos: tos, + flags: flags, + seq: seq, + ack: ack, + rcvWnd: 0, + expOptVal: expOptVal, }, p, stack.GSO{}, nil /* PacketOwner */) } diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index e9eba54a9..bdf7f6042 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -1263,3 +1263,8 @@ syscall_test( save = False, test = "//test/syscalls/linux:close_range_test", ) + +syscall_test( + save = False, + test = "//test/syscalls/linux:socketopt_test", +) diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 0280bbc27..771b590ce 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -2557,6 +2557,22 @@ cc_library( alwayslink = 1, ) +cc_binary( + name = "socketopt_test", + testonly = 1, + srcs = ["socketopt_test.cc"], + linkstatic = 1, + deps = select_gtest() + [ + "//test/util:capability_util", + "//test/util:file_descriptor", + "//test/util:posix_error", + "//test/util:socket_util", + "//test/util:test_main", + "//test/util:test_util", + "@com_google_absl//absl/strings:str_format", + ], +) + cc_binary( name = "socket_stress_test", testonly = 1, diff --git a/test/syscalls/linux/socketopt_test.cc b/test/syscalls/linux/socketopt_test.cc new file mode 100644 index 000000000..ff0434c68 --- /dev/null +++ b/test/syscalls/linux/socketopt_test.cc @@ -0,0 +1,23 @@ +// Copyright 2024 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. + +#include +#include +#include + +namespace gvisor { +namespace testing { +namespace {} // namespace +} // namespace testing +} // namespace gvisor diff --git a/tools/bazeldefs/tags.bzl b/tools/bazeldefs/tags.bzl index 1fe6987eb..f0825b364 100644 --- a/tools/bazeldefs/tags.bzl +++ b/tools/bazeldefs/tags.bzl @@ -51,6 +51,7 @@ generic = [ "_unsafe", "_opts", "_testonly", + "_custom", ] # State explosion? Sure. This is approximately: