From 58b9bdfc21e792c5d529ec9f4ab0b2f2cd1ee082 Mon Sep 17 00:00:00 2001 From: Arthur Sfez Date: Wed, 29 Dec 2021 12:31:04 -0800 Subject: [PATCH] Use protocol-specific options for TTL/HopLimit The new HopLimit matches the IPV6_UNICAST_HOPS socket option. Updates #6389 PiperOrigin-RevId: 418831844 --- pkg/sentry/socket/netstack/netstack.go | 36 +++- pkg/tcpip/tcpip.go | 29 ++- .../transport/internal/network/endpoint.go | 53 +++-- pkg/tcpip/transport/tcp/accept.go | 6 +- pkg/tcpip/transport/tcp/connect.go | 18 +- pkg/tcpip/transport/tcp/endpoint.go | 43 +++- pkg/tcpip/transport/tcp/forwarder.go | 2 +- pkg/tcpip/transport/tcp/protocol.go | 13 +- pkg/tcpip/transport/tcp/tcp_test.go | 21 +- pkg/tcpip/transport/udp/udp_test.go | 53 +++-- test/syscalls/BUILD | 5 + test/syscalls/linux/BUILD | 16 ++ test/syscalls/linux/socket_ipv6_unbound.cc | 201 ++++++++++++++++++ 13 files changed, 427 insertions(+), 69 deletions(-) create mode 100644 test/syscalls/linux/socket_ipv6_unbound.cc diff --git a/pkg/sentry/socket/netstack/netstack.go b/pkg/sentry/socket/netstack/netstack.go index ee9195169..59d2e5e4b 100644 --- a/pkg/sentry/socket/netstack/netstack.go +++ b/pkg/sentry/socket/netstack/netstack.go @@ -1351,6 +1351,26 @@ func getSockOptIPv6(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name v := primitive.Int32(boolToInt32(ep.SocketOptions().GetV6Only())) return &v, nil + case linux.IPV6_UNICAST_HOPS: + if outLen < sizeOfInt32 { + return nil, syserr.ErrInvalidArgument + } + + v, err := ep.GetSockOptInt(tcpip.IPv6HopLimitOption) + if err != nil { + return nil, syserr.TranslateNetstackError(err) + } + + // Fill in the default value, if needed. + vP := primitive.Int32(v) + if vP == -1 { + // TODO(https://github.com/google/gvisor/issues/6973): Retrieve the + // configured DefaultTTLOption of the IPv6 protocol. + vP = DefaultTTL + } + + return &vP, nil + case linux.IPV6_PATHMTU: t.Kernel().EmitUnimplementedEvent(t) @@ -1499,7 +1519,7 @@ func getSockOptIP(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name in return nil, syserr.ErrInvalidArgument } - v, err := ep.GetSockOptInt(tcpip.TTLOption) + v, err := ep.GetSockOptInt(tcpip.IPv4TTLOption) if err != nil { return nil, syserr.TranslateNetstackError(err) } @@ -1507,6 +1527,8 @@ func getSockOptIP(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name in // Fill in the default value, if needed. vP := primitive.Int32(v) if vP == 0 { + // TODO(https://github.com/google/gvisor/issues/6973): Retrieve the + // configured DefaultTTLOption of the IPv4 protocol. vP = DefaultTTL } @@ -2200,6 +2222,16 @@ func setSockOptIPv6(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name ep.SocketOptions().SetIPv6ReceivePacketInfo(v != 0) return nil + case linux.IPV6_UNICAST_HOPS: + if len(optVal) < sizeOfInt32 { + return syserr.ErrInvalidArgument + } + v := int32(hostarch.ByteOrder.Uint32(optVal)) + if v < -1 || v > 255 { + return syserr.ErrInvalidArgument + } + return syserr.TranslateNetstackError(ep.SetSockOptInt(tcpip.IPv6HopLimitOption, int(v))) + case linux.IPV6_TCLASS: if len(optVal) < sizeOfInt32 { return syserr.ErrInvalidArgument @@ -2410,7 +2442,7 @@ func setSockOptIP(t *kernel.Task, s socket.SocketOps, ep commonEndpoint, name in } else if v < 1 || v > 255 { return syserr.ErrInvalidArgument } - return syserr.TranslateNetstackError(ep.SetSockOptInt(tcpip.TTLOption, int(v))) + return syserr.TranslateNetstackError(ep.SetSockOptInt(tcpip.IPv4TTLOption, int(v))) case linux.IP_TOS: if len(optVal) == 0 { diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index dd30a6519..697bad7ee 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -731,12 +731,19 @@ const ( // number of unread bytes in the output buffer should be returned. SendQueueSizeOption - // TTLOption is used by SetSockOptInt/GetSockOptInt to control the - // default TTL/hop limit value for unicast messages. The default is - // protocol specific. + // IPv4TTLOption is used by SetSockOptInt/GetSockOptInt to control the default + // TTL value for unicast messages. // - // A zero value indicates the default. - TTLOption + // The default is configured by DefaultTTLOption. A UseDefaultIPv4TTL value + // configures the endpoint to use the default. + IPv4TTLOption + + // IPv6HopLimitOption is used by SetSockOptInt/GetSockOptInt to control the + // default hop limit value for unicast messages. + // + // The default is configured by DefaultTTLOption. A UseDefaultIPv6HopLimit + // value configures the endpoint to use the default. + IPv6HopLimitOption // TCPSynCountOption is used by SetSockOptInt/GetSockOptInt to specify // the number of SYN retransmits that TCP should send before aborting @@ -752,6 +759,18 @@ const ( TCPWindowClampOption ) +const ( + // UseDefaultIPv4TTL is the IPv4TTLOption value that configures an endpoint to + // use the default ttl currently configured by the IPv4 protocol (see + // DefaultTTLOption). + UseDefaultIPv4TTL = 0 + + // UseDefaultIPv6HopLimit is the IPv6HopLimitOption value that configures an + // endpoint to use the default hop limit currently configured by the IPv6 + // protocol (see DefaultTTLOption). + UseDefaultIPv6HopLimit = -1 +) + const ( // PMTUDiscoveryWant is a setting of the MTUDiscoverOption to use // per-route settings. diff --git a/pkg/tcpip/transport/internal/network/endpoint.go b/pkg/tcpip/transport/internal/network/endpoint.go index fb31e5104..55f518a86 100644 --- a/pkg/tcpip/transport/internal/network/endpoint.go +++ b/pkg/tcpip/transport/internal/network/endpoint.go @@ -53,9 +53,10 @@ type Endpoint struct { connectedRoute *stack.Route `state:"manual"` // +checklocks:mu multicastMemberships map[multicastMembership]struct{} - // TODO(https://gvisor.dev/issue/6389): Use different fields for IPv4/IPv6. // +checklocks:mu - ttl uint8 + ipv4TTL uint8 + // +checklocks:mu + ipv6HopLimit int16 // TODO(https://gvisor.dev/issue/6389): Use different fields for IPv4/IPv6. // +checklocks:mu multicastTTL uint8 @@ -131,6 +132,8 @@ func (e *Endpoint) Init(s *stack.Stack, netProto tcpip.NetworkProtocolNumber, tr TransProto: transProto, }, effectiveNetProto: netProto, + ipv4TTL: tcpip.UseDefaultIPv4TTL, + ipv6HopLimit: tcpip.UseDefaultIPv6HopLimit, // Linux defaults to TTL=1. multicastTTL: 1, multicastMemberships: make(map[multicastMembership]struct{}), @@ -191,16 +194,27 @@ func (e *Endpoint) SetOwner(owner tcpip.PacketOwner) { e.owner = owner } -func calculateTTL(route *stack.Route, ttl uint8, multicastTTL uint8) uint8 { - if header.IsV4MulticastAddress(route.RemoteAddress()) || header.IsV6MulticastAddress(route.RemoteAddress()) { - return multicastTTL +// +checklocksread:e.mu +func (e *Endpoint) calculateTTL(route *stack.Route) uint8 { + remoteAddress := route.RemoteAddress() + if header.IsV4MulticastAddress(remoteAddress) || header.IsV6MulticastAddress(remoteAddress) { + return e.multicastTTL } - if ttl == 0 { - return route.DefaultTTL() + switch netProto := route.NetProto(); netProto { + case header.IPv4ProtocolNumber: + if e.ipv4TTL == 0 { + return route.DefaultTTL() + } + return e.ipv4TTL + case header.IPv6ProtocolNumber: + if e.ipv6HopLimit == -1 { + return route.DefaultTTL() + } + return uint8(e.ipv6HopLimit) + default: + panic(fmt.Sprintf("invalid protocol number = %d", netProto)) } - - return ttl } // WriteContext holds the context for a write. @@ -327,7 +341,7 @@ func (e *Endpoint) AcquireContextForWrite(opts tcpip.WriteOptions) (WriteContext return WriteContext{ transProto: e.transProto, route: route, - ttl: calculateTTL(route, e.ttl, e.multicastTTL), + ttl: e.calculateTTL(route), tos: tos, owner: e.owner, }, nil @@ -602,9 +616,14 @@ func (e *Endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error { e.multicastTTL = uint8(v) e.mu.Unlock() - case tcpip.TTLOption: + case tcpip.IPv4TTLOption: e.mu.Lock() - e.ttl = uint8(v) + e.ipv4TTL = uint8(v) + e.mu.Unlock() + + case tcpip.IPv6HopLimitOption: + e.mu.Lock() + e.ipv6HopLimit = int16(v) e.mu.Unlock() case tcpip.IPv4TOSOption: @@ -634,9 +653,15 @@ func (e *Endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) { e.mu.Unlock() return v, nil - case tcpip.TTLOption: + case tcpip.IPv4TTLOption: e.mu.Lock() - v := int(e.ttl) + v := int(e.ipv4TTL) + e.mu.Unlock() + return v, nil + + case tcpip.IPv6HopLimitOption: + e.mu.Lock() + v := int(e.ipv6HopLimit) e.mu.Unlock() return v, nil diff --git a/pkg/tcpip/transport/tcp/accept.go b/pkg/tcpip/transport/tcp/accept.go index d0f68b72c..262e49310 100644 --- a/pkg/tcpip/transport/tcp/accept.go +++ b/pkg/tcpip/transport/tcp/accept.go @@ -435,7 +435,7 @@ func (e *endpoint) handleListenSegment(ctx *listenContext, s *segment) tcpip.Err // RFC 793 section 3.4 page 35 (figure 12) outlines that a RST // must be sent in response to a SYN-ACK while in the listen // state to prevent completing a handshake from an old SYN. - return replyWithReset(e.stack, s, e.sendTOS, e.ttl) + return replyWithReset(e.stack, s, e.sendTOS, e.ipv4TTL, e.ipv6HopLimit) } switch { @@ -569,7 +569,7 @@ 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: e.ttl, + ttl: calculateTTL(route, e.ipv4TTL, e.ipv6HopLimit), tos: e.sendTOS, flags: header.TCPFlagSyn | header.TCPFlagAck, seq: cookie, @@ -616,7 +616,7 @@ func (e *endpoint) handleListenSegment(ctx *listenContext, s *segment) tcpip.Err // The only time we should reach here when a connection // was opened and closed really quickly and a delayed // ACK was received from the sender. - return replyWithReset(e.stack, s, e.sendTOS, e.ttl) + return replyWithReset(e.stack, s, e.sendTOS, e.ipv4TTL, e.ipv6HopLimit) } // Keep hold of acceptMu until the new endpoint is in the accept queue (or diff --git a/pkg/tcpip/transport/tcp/connect.go b/pkg/tcpip/transport/tcp/connect.go index ae454dbfb..06f56367b 100644 --- a/pkg/tcpip/transport/tcp/connect.go +++ b/pkg/tcpip/transport/tcp/connect.go @@ -283,7 +283,7 @@ func (h *handshake) synSentState(s *segment) tcpip.Error { // but resend our own SYN and wait for it to be acknowledged in the // SYN-RCVD state. h.state = handshakeSynRcvd - ttl := h.ep.ttl + ttl := calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit) amss := h.ep.amss h.ep.setEndpointState(StateSynRecv) synOpts := header.TCPSynOptions{ @@ -366,7 +366,7 @@ func (h *handshake) synRcvdState(s *segment) tcpip.Error { } h.ep.sendSynTCP(h.ep.route, tcpFields{ id: h.ep.TransportEndpointInfo.ID, - ttl: h.ep.ttl, + ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), tos: h.ep.sendTOS, flags: h.flags, seq: h.iss, @@ -508,7 +508,7 @@ func (h *handshake) start() { h.sendSYNOpts = synOpts h.ep.sendSynTCP(h.ep.route, tcpFields{ id: h.ep.TransportEndpointInfo.ID, - ttl: h.ep.ttl, + ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), tos: h.ep.sendTOS, flags: h.flags, seq: h.iss, @@ -556,7 +556,7 @@ func (h *handshake) complete() tcpip.Error { if h.active || !h.acked || h.deferAccept != 0 && h.ep.stack.Clock().NowMonotonic().Sub(h.startTime) > h.deferAccept { h.ep.sendSynTCP(h.ep.route, tcpFields{ id: h.ep.TransportEndpointInfo.ID, - ttl: h.ep.ttl, + ttl: calculateTTL(h.ep.route, h.ep.ipv4TTL, h.ep.ipv6HopLimit), tos: h.ep.sendTOS, flags: h.flags, seq: h.iss, @@ -847,9 +847,6 @@ func sendTCPBatch(r *stack.Route, tf tcpFields, data buffer.VectorisedView, gso buildTCPHdr(r, tf, pkt, gso) tf.seq = tf.seq.Add(seqnum.Size(packetSize)) pkt.GSOOptions = gso - if tf.ttl == 0 { - tf.ttl = r.DefaultTTL() - } if err := r.WritePacket(stack.NetworkHeaderParams{Protocol: ProtocolNumber, TTL: tf.ttl, TOS: tf.tos}, pkt); err != nil { r.Stats().TCP.SegmentSendErrors.Increment() pkt.DecRef() @@ -883,9 +880,6 @@ func sendTCP(r *stack.Route, tf tcpFields, data buffer.VectorisedView, gso stack pkt.Owner = owner buildTCPHdr(r, tf, pkt, gso) - if tf.ttl == 0 { - tf.ttl = r.DefaultTTL() - } if err := r.WritePacket(stack.NetworkHeaderParams{Protocol: ProtocolNumber, TTL: tf.ttl, TOS: tf.tos}, pkt); err != nil { r.Stats().TCP.SegmentSendErrors.Increment() return err @@ -945,7 +939,7 @@ func (e *endpoint) sendRaw(data buffer.VectorisedView, flags header.TCPFlags, se options := e.makeOptions(sackBlocks) err := e.sendTCP(e.route, tcpFields{ id: e.TransportEndpointInfo.ID, - ttl: e.ttl, + ttl: calculateTTL(e.route, e.ipv4TTL, e.ipv6HopLimit), tos: e.sendTOS, flags: flags, seq: seq, @@ -1049,7 +1043,7 @@ func (e *endpoint) tryDeliverSegmentFromClosedEndpoint(s *segment) { ) } if ep == nil { - replyWithReset(e.stack, s, stack.DefaultTOS, 0 /* ttl */) + replyWithReset(e.stack, s, stack.DefaultTOS, tcpip.UseDefaultIPv4TTL, tcpip.UseDefaultIPv6HopLimit) s.decRef() return } diff --git a/pkg/tcpip/transport/tcp/endpoint.go b/pkg/tcpip/transport/tcp/endpoint.go index 4ceeb2e47..bbf6b5f1b 100644 --- a/pkg/tcpip/transport/tcp/endpoint.go +++ b/pkg/tcpip/transport/tcp/endpoint.go @@ -436,7 +436,8 @@ type endpoint struct { isRegistered bool `state:"manual"` boundNICID tcpip.NICID route *stack.Route `state:"manual"` - ttl uint8 + ipv4TTL uint8 + ipv6HopLimit int16 isConnectNotified bool // h stores a reference to the current handshake state if the endpoint is in @@ -783,6 +784,25 @@ func (e *endpoint) recentTimestamp() uint32 { return e.RecentTS } +// TODO(gvisor.dev/issue/6974): Remove once tcp endpoints are composed with a +// network.Endpoint, which also defines this function. +func calculateTTL(route *stack.Route, ipv4TTL uint8, ipv6HopLimit int16) uint8 { + switch netProto := route.NetProto(); netProto { + case header.IPv4ProtocolNumber: + if ipv4TTL == tcpip.UseDefaultIPv4TTL { + return route.DefaultTTL() + } + return ipv4TTL + case header.IPv6ProtocolNumber: + if ipv6HopLimit == tcpip.UseDefaultIPv6HopLimit { + return route.DefaultTTL() + } + return uint8(ipv6HopLimit) + default: + panic(fmt.Sprintf("invalid protocol number = %d", netProto)) + } +} + // keepalive is a synchronization wrapper used to appease stateify. See the // comment in endpoint, where it is used. // @@ -818,6 +838,8 @@ func newEndpoint(s *stack.Stack, protocol *protocol, netProto tcpip.NetworkProto count: DefaultKeepaliveCount, }, uniqueID: s.UniqueID(), + ipv4TTL: tcpip.UseDefaultIPv4TTL, + ipv6HopLimit: tcpip.UseDefaultIPv6HopLimit, txHash: s.Rand().Uint32(), windowClamp: DefaultReceiveBufferSize, maxSynRetries: DefaultSynRetries, @@ -1775,9 +1797,14 @@ func (e *endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error { return &tcpip.ErrNotSupported{} } - case tcpip.TTLOption: + case tcpip.IPv4TTLOption: e.LockUser() - e.ttl = uint8(v) + e.ipv4TTL = uint8(v) + e.UnlockUser() + + case tcpip.IPv6HopLimitOption: + e.LockUser() + e.ipv6HopLimit = int16(v) e.UnlockUser() case tcpip.TCPSynCountOption: @@ -1960,9 +1987,15 @@ func (e *endpoint) GetSockOptInt(opt tcpip.SockOptInt) (int, tcpip.Error) { case tcpip.ReceiveQueueSizeOption: return e.readyReceiveSize() - case tcpip.TTLOption: + case tcpip.IPv4TTLOption: e.LockUser() - v := int(e.ttl) + v := int(e.ipv4TTL) + e.UnlockUser() + return v, nil + + case tcpip.IPv6HopLimitOption: + e.LockUser() + v := int(e.ipv6HopLimit) e.UnlockUser() return v, nil diff --git a/pkg/tcpip/transport/tcp/forwarder.go b/pkg/tcpip/transport/tcp/forwarder.go index 128ef09e3..e50b359a2 100644 --- a/pkg/tcpip/transport/tcp/forwarder.go +++ b/pkg/tcpip/transport/tcp/forwarder.go @@ -132,7 +132,7 @@ func (r *ForwarderRequest) Complete(sendReset bool) { r.forwarder.mu.Unlock() if sendReset { - replyWithReset(r.forwarder.stack, r.segment, stack.DefaultTOS, 0 /* ttl */) + replyWithReset(r.forwarder.stack, r.segment, stack.DefaultTOS, tcpip.UseDefaultIPv4TTL, tcpip.UseDefaultIPv6HopLimit) } // Release all resources. diff --git a/pkg/tcpip/transport/tcp/protocol.go b/pkg/tcpip/transport/tcp/protocol.go index f122ea009..8d3ab0dc9 100644 --- a/pkg/tcpip/transport/tcp/protocol.go +++ b/pkg/tcpip/transport/tcp/protocol.go @@ -165,7 +165,7 @@ func (p *protocol) HandleUnknownDestinationPacket(id stack.TransportEndpointID, } if !s.flags.Contains(header.TCPFlagRst) { - replyWithReset(p.stack, s, stack.DefaultTOS, 0) + replyWithReset(p.stack, s, stack.DefaultTOS, tcpip.UseDefaultIPv4TTL, tcpip.UseDefaultIPv6HopLimit) } return stack.UnknownDestinationPacketHandled @@ -191,14 +191,17 @@ func (p *protocol) tsOffset(src, dst tcpip.Address) tcp.TSOffset { // replyWithReset replies to the given segment with a reset segment. // -// If the passed TTL is 0, then the route's default TTL will be used. -func replyWithReset(st *stack.Stack, s *segment, tos, ttl uint8) tcpip.Error { +// If the relevant TTL has its reset value (0 for ipv4TTL, -1 for ipv6HopLimit), +// then the route's default TTL will be used. +func replyWithReset(st *stack.Stack, s *segment, tos, ipv4TTL uint8, ipv6HopLimit int16) tcpip.Error { route, err := st.FindRoute(s.nicID, s.dstAddr, s.srcAddr, s.netProto, false /* multicastLoop */) if err != nil { return err } defer route.Release() + ttl := calculateTTL(route, ipv4TTL, ipv6HopLimit) + // Get the seqnum from the packet if the ack flag is set. seq := seqnum.Value(0) ack := seqnum.Value(0) @@ -221,10 +224,6 @@ func replyWithReset(st *stack.Stack, s *segment, tos, ttl uint8) tcpip.Error { ack = s.sequenceNumber.Add(s.logicalLen()) } - if ttl == 0 { - ttl = route.DefaultTTL() - } - return sendTCP(route, tcpFields{ id: s.id, ttl: ttl, diff --git a/pkg/tcpip/transport/tcp/tcp_test.go b/pkg/tcpip/transport/tcp/tcp_test.go index 0b88480ef..668fe0078 100644 --- a/pkg/tcpip/transport/tcp/tcp_test.go +++ b/pkg/tcpip/transport/tcp/tcp_test.go @@ -3463,12 +3463,14 @@ func TestDefaultTTL(t *testing.T) { func TestSetTTL(t *testing.T) { for _, test := range []struct { - name string - protoNum tcpip.NetworkProtocolNumber - addr tcpip.Address + name string + protoNum tcpip.NetworkProtocolNumber + addr tcpip.Address + relevantOpt tcpip.SockOptInt + irrelevantOpt tcpip.SockOptInt }{ - {"ipv4", ipv4.ProtocolNumber, context.TestAddr}, - {"ipv6", ipv6.ProtocolNumber, context.TestV6Addr}, + {"ipv4", ipv4.ProtocolNumber, context.TestAddr, tcpip.IPv4TTLOption, tcpip.IPv6HopLimitOption}, + {"ipv6", ipv6.ProtocolNumber, context.TestV6Addr, tcpip.IPv6HopLimitOption, tcpip.IPv4TTLOption}, } { t.Run(fmt.Sprint(test.name), func(t *testing.T) { for _, wantTTL := range []uint8{1, 2, 50, 64, 128, 254, 255} { @@ -3482,8 +3484,13 @@ func TestSetTTL(t *testing.T) { t.Fatalf("NewEndpoint failed: %s", err) } - if err := c.EP.SetSockOptInt(tcpip.TTLOption, int(wantTTL)); err != nil { - t.Fatalf("SetSockOptInt(TTLOption, %d) failed: %s", wantTTL, err) + if err := c.EP.SetSockOptInt(test.relevantOpt, int(wantTTL)); err != nil { + t.Fatalf("SetSockOptInt(%d, %d) failed: %s", test.relevantOpt, wantTTL, err) + } + // Set a different ttl/hoplimit for the unused protocol, showing that + // it does not affect the other protocol. + if err := c.EP.SetSockOptInt(test.irrelevantOpt, int(wantTTL+1)); err != nil { + t.Fatalf("SetSockOptInt(%d, %d) failed: %s", test.irrelevantOpt, wantTTL, err) } { diff --git a/pkg/tcpip/transport/udp/udp_test.go b/pkg/tcpip/transport/udp/udp_test.go index d4dd72534..d7f97586c 100644 --- a/pkg/tcpip/transport/udp/udp_test.go +++ b/pkg/tcpip/transport/udp/udp_test.go @@ -287,13 +287,6 @@ func (flow testFlow) isReverseMulticast() bool { } } -func (flow testFlow) ttlOption() tcpip.SockOptInt { - if flow.isMulticast() { - return tcpip.MulticastTTLOption - } - return tcpip.TTLOption -} - type testContext struct { t *testing.T linkEP *channel.Endpoint @@ -1615,7 +1608,7 @@ func (*testInterface) Enabled() bool { return true } -func TestNonMulticastDefaultTTL(t *testing.T) { +func TestDefaultTTL(t *testing.T) { for _, flow := range []testFlow{unicastV4, unicastV4in6, unicastV6, unicastV6Only, broadcast, broadcastIn6} { t.Run(fmt.Sprintf("flow:%s", flow), func(t *testing.T) { c := newDualTestContext(t, defaultMTU) @@ -1642,8 +1635,8 @@ func TestNonMulticastDefaultTTL(t *testing.T) { } } -func TestSetTTL(t *testing.T) { - for _, flow := range []testFlow{unicastV4, unicastV4in6, unicastV6, unicastV6Only, multicastV4, multicastV4in6, multicastV6, broadcast, broadcastIn6} { +func TestNonMulticastDefaultTTL(t *testing.T) { + for _, flow := range []testFlow{unicastV4, unicastV4in6, unicastV6, unicastV6Only, broadcast, broadcastIn6} { t.Run(fmt.Sprintf("flow:%s", flow), func(t *testing.T) { for _, wantTTL := range []uint8{1, 2, 50, 64, 128, 254, 255} { t.Run(fmt.Sprintf("TTL:%d", wantTTL), func(t *testing.T) { @@ -1652,9 +1645,43 @@ func TestSetTTL(t *testing.T) { c.createEndpointForFlow(flow) - opt := flow.ttlOption() - if err := c.ep.SetSockOptInt(opt, int(wantTTL)); err != nil { - c.t.Fatalf("SetSockOptInt(%d, %d) failed: %s", opt, wantTTL, err) + var relevantOpt tcpip.SockOptInt + var irrelevantOpt tcpip.SockOptInt + if flow.isV4() { + relevantOpt = tcpip.IPv4TTLOption + irrelevantOpt = tcpip.IPv6HopLimitOption + } else { + relevantOpt = tcpip.IPv6HopLimitOption + irrelevantOpt = tcpip.IPv4TTLOption + } + if err := c.ep.SetSockOptInt(relevantOpt, int(wantTTL)); err != nil { + c.t.Fatalf("SetSockOptInt(%d, %d) failed: %s", relevantOpt, wantTTL, err) + } + // Set a different ttl/hoplimit for the unused protocol, showing that + // it does not affect the other protocol. + if err := c.ep.SetSockOptInt(irrelevantOpt, int(wantTTL+1)); err != nil { + c.t.Fatalf("SetSockOptInt(%d, %d) failed: %s", irrelevantOpt, wantTTL, err) + } + + testWrite(c, flow, checker.TTL(wantTTL)) + }) + } + }) + } +} + +func TestSetMulticastTTL(t *testing.T) { + for _, flow := range []testFlow{multicastV4, multicastV4in6, multicastV6} { + t.Run(fmt.Sprintf("flow:%s", flow), func(t *testing.T) { + for _, wantTTL := range []uint8{1, 2, 50, 64, 128, 254, 255} { + t.Run(fmt.Sprintf("TTL:%d", wantTTL), func(t *testing.T) { + c := newDualTestContext(t, defaultMTU) + defer c.cleanup() + + c.createEndpointForFlow(flow) + + if err := c.ep.SetSockOptInt(tcpip.MulticastTTLOption, int(wantTTL)); err != nil { + c.t.Fatalf("SetSockOptInt failed: %s", err) } testWrite(c, flow, checker.TTL(wantTTL)) diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 4a73cc6d3..7b6bbb3a6 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -766,6 +766,11 @@ syscall_test( test = "//test/syscalls/linux:socket_ip_unbound_test", ) +syscall_test( + shard_count = more_shards, + test = "//test/syscalls/linux:socket_ipv6_unbound_test", +) + syscall_test( test = "//test/syscalls/linux:socket_ip_unbound_netlink_test", ) diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 22552cf01..6721c89c0 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -3058,6 +3058,22 @@ cc_binary( ], ) +cc_binary( + name = "socket_ipv6_unbound_test", + testonly = 1, + srcs = [ + "socket_ipv6_unbound.cc", + ], + linkstatic = 1, + deps = [ + ":ip_socket_test_util", + "//test/util:socket_util", + gtest, + "//test/util:test_main", + "//test/util:test_util", + ], +) + cc_binary( name = "socket_ipv6_udp_unbound_loopback_test", testonly = 1, diff --git a/test/syscalls/linux/socket_ipv6_unbound.cc b/test/syscalls/linux/socket_ipv6_unbound.cc new file mode 100644 index 000000000..5ac0072f2 --- /dev/null +++ b/test/syscalls/linux/socket_ipv6_unbound.cc @@ -0,0 +1,201 @@ +// Copyright 2021 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 +#ifdef __linux__ +#include +#endif // __linux__ +#include +#include + +#include "gtest/gtest.h" +#include "test/syscalls/linux/ip_socket_test_util.h" +#include "test/util/socket_util.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { +namespace { + +constexpr int kDefaultHopLimit = 64; + +using ::testing::ValuesIn; +using IPv6UnboundSocketTest = SimpleSocketTest; + +TEST_P(IPv6UnboundSocketTest, HopLimitDefault) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + int get = -1; + socklen_t get_sz = sizeof(get); + ASSERT_THAT( + getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get, &get_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get_sz, sizeof(get)); + EXPECT_EQ(get, kDefaultHopLimit); +} + +TEST_P(IPv6UnboundSocketTest, SetHopLimit) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + int get1 = -1; + socklen_t get1_sz = sizeof(get1); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get1, + &get1_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get1_sz, sizeof(get1)); + EXPECT_EQ(get1, kDefaultHopLimit); + + const int set = (get1 % 255) + 1; + ASSERT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &set, + sizeof(set)), + SyscallSucceedsWithValue(0)); + + int get2 = -1; + socklen_t get2_sz = sizeof(get2); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get2, + &get2_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get2_sz, sizeof(get2)); + EXPECT_EQ(get2, set); +} + +TEST_P(IPv6UnboundSocketTest, ResetHopLimitToDefault) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + int get1 = -1; + socklen_t get1_sz = sizeof(get1); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get1, + &get1_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get1_sz, sizeof(get1)); + EXPECT_EQ(get1, kDefaultHopLimit); + + const int set = (get1 % 255) + 1; + ASSERT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &set, + sizeof(set)), + SyscallSucceedsWithValue(0)); + + constexpr int kUseDefaultHopLimit = -1; + ASSERT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, + &kUseDefaultHopLimit, sizeof(kUseDefaultHopLimit)), + SyscallSucceedsWithValue(0)); + + int get2 = -1; + socklen_t get2_sz = sizeof(get2); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get2, + &get2_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get2_sz, sizeof(get2)); + EXPECT_EQ(get2, get1); +} + +TEST_P(IPv6UnboundSocketTest, ZeroHopLimit) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + constexpr int kZero = 0; + ASSERT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kZero, + sizeof(kZero)), + SyscallSucceedsWithValue(0)); + + int get = -1; + socklen_t get_sz = sizeof(get); + ASSERT_THAT( + getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get, &get_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get, kZero); + EXPECT_EQ(get_sz, sizeof(get)); +} + +TEST_P(IPv6UnboundSocketTest, InvalidLargeHopLimit) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + constexpr int kInvalidLarge = 256; + EXPECT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, + &kInvalidLarge, sizeof(kInvalidLarge)), + SyscallFailsWithErrno(EINVAL)); +} + +TEST_P(IPv6UnboundSocketTest, InvalidNegativeHopLimit) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + constexpr int kInvalidNegative = -2; + EXPECT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, + &kInvalidNegative, sizeof(kInvalidNegative)), + SyscallFailsWithErrno(EINVAL)); +} + +TEST_P(IPv6UnboundSocketTest, SetTtlDoesNotAffectHopLimit) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + int get = -1; + socklen_t get_sz = sizeof(get); + ASSERT_THAT( + getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get, &get_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get_sz, sizeof(get)); + + const int set = (get % 255) + 1; + ASSERT_THAT(setsockopt(socket->get(), IPPROTO_IP, IP_TTL, &set, sizeof(set)), + SyscallSucceedsWithValue(0)); + + int get2 = -1; + socklen_t get2_sz = sizeof(get2); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &get2, + &get2_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get2_sz, sizeof(get2)); + EXPECT_EQ(get2, get); +} + +TEST_P(IPv6UnboundSocketTest, SetHopLimitDoesNotAffectTtl) { + std::unique_ptr socket = + ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + + int get = -1; + socklen_t get_sz = sizeof(get); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IP, IP_TTL, &get, &get_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get_sz, sizeof(get)); + + const int set = (get % 255) + 1; + ASSERT_THAT(setsockopt(socket->get(), IPPROTO_IPV6, IPV6_UNICAST_HOPS, &set, + sizeof(set)), + SyscallSucceedsWithValue(0)); + + int get2 = -1; + socklen_t get2_sz = sizeof(get2); + ASSERT_THAT(getsockopt(socket->get(), IPPROTO_IP, IP_TTL, &get2, &get2_sz), + SyscallSucceedsWithValue(0)); + ASSERT_EQ(get2_sz, sizeof(get2)); + EXPECT_EQ(get2, get); +} + +INSTANTIATE_TEST_SUITE_P( + IPv6UnboundSockets, IPv6UnboundSocketTest, + ValuesIn(VecCat( + ApplyVec(IPv6UDPUnboundSocket, + std::vector{0, SOCK_NONBLOCK}), + ApplyVec(IPv6TCPUnboundSocket, + std::vector{0, SOCK_NONBLOCK})))); + +} // namespace +} // namespace testing +} // namespace gvisor