diff --git a/pkg/tcpip/checker/checker.go b/pkg/tcpip/checker/checker.go index 24c2c3e6b..52a9a19fb 100644 --- a/pkg/tcpip/checker/checker.go +++ b/pkg/tcpip/checker/checker.go @@ -292,32 +292,54 @@ func ReceiveTClass(want uint32) ControlMessagesChecker { return func(t *testing.T, cm tcpip.ControlMessages) { t.Helper() if !cm.HasTClass { - t.Errorf("got cm.HasTClass = %t, want = true", cm.HasTClass) + t.Error("got cm.HasTClass = false, want = true") } else if got := cm.TClass; got != want { t.Errorf("got cm.TClass = %d, want %d", got, want) } } } +// NoTClassReceived creates a checker that checks the absence of the TCLASS +// field in ControlMessages. +func NoTClassReceived() ControlMessagesChecker { + return func(t *testing.T, cm tcpip.ControlMessages) { + t.Helper() + if cm.HasTClass { + t.Error("got cm.HasTClass = true, want = false") + } + } +} + // ReceiveTOS creates a checker that checks the TOS field in ControlMessages. func ReceiveTOS(want uint8) ControlMessagesChecker { return func(t *testing.T, cm tcpip.ControlMessages) { t.Helper() if !cm.HasTOS { - t.Errorf("got cm.HasTOS = %t, want = true", cm.HasTOS) + t.Error("got cm.HasTOS = false, want = true") } else if got := cm.TOS; got != want { t.Errorf("got cm.TOS = %d, want %d", got, want) } } } +// NoTOSReceived creates a checker that checks the absence of the TOS field in +// ControlMessages. +func NoTOSReceived() ControlMessagesChecker { + return func(t *testing.T, cm tcpip.ControlMessages) { + t.Helper() + if cm.HasTOS { + t.Error("got cm.HasTOS = true, want = false") + } + } +} + // ReceiveIPPacketInfo creates a checker that checks the PacketInfo field in // ControlMessages. func ReceiveIPPacketInfo(want tcpip.IPPacketInfo) ControlMessagesChecker { return func(t *testing.T, cm tcpip.ControlMessages) { t.Helper() if !cm.HasIPPacketInfo { - t.Errorf("got cm.HasIPPacketInfo = %t, want = true", cm.HasIPPacketInfo) + t.Error("got cm.HasIPPacketInfo = false, want = true") } else if diff := cmp.Diff(want, cm.PacketInfo); diff != "" { t.Errorf("IPPacketInfo mismatch (-want +got):\n%s", diff) } @@ -330,7 +352,7 @@ func ReceiveIPv6PacketInfo(want tcpip.IPv6PacketInfo) ControlMessagesChecker { return func(t *testing.T, cm tcpip.ControlMessages) { t.Helper() if !cm.HasIPv6PacketInfo { - t.Errorf("got cm.HasIPv6PacketInfo = %t, want = true", cm.HasIPv6PacketInfo) + t.Error("got cm.HasIPv6PacketInfo = false, want = true") } else if diff := cmp.Diff(want, cm.IPv6PacketInfo); diff != "" { t.Errorf("IPv6PacketInfo mismatch (-want +got):\n%s", diff) } @@ -343,7 +365,7 @@ func ReceiveOriginalDstAddr(want tcpip.FullAddress) ControlMessagesChecker { return func(t *testing.T, cm tcpip.ControlMessages) { t.Helper() if !cm.HasOriginalDstAddress { - t.Errorf("got cm.HasOriginalDstAddress = %t, want = true", cm.HasOriginalDstAddress) + t.Error("got cm.HasOriginalDstAddress = false, want = true") } else if diff := cmp.Diff(want, cm.OriginalDstAddress); diff != "" { t.Errorf("OriginalDstAddress mismatch (-want +got):\n%s", diff) } diff --git a/pkg/tcpip/network/ipv6/icmp.go b/pkg/tcpip/network/ipv6/icmp.go index a74c61517..280055f05 100644 --- a/pkg/tcpip/network/ipv6/icmp.go +++ b/pkg/tcpip/network/ipv6/icmp.go @@ -689,10 +689,14 @@ func (e *endpoint) handleICMP(pkt *stack.PacketBuffer, hasFragmentHeader bool, r PayloadCsum: dataRange.Checksum(), PayloadLen: dataRange.Size(), })) + replyTClass, _ := iph.TOS() if err := r.WritePacket(stack.NetworkHeaderParams{ Protocol: header.ICMPv6ProtocolNumber, TTL: r.DefaultTTL(), - TOS: stack.DefaultTOS, + // Even though RFC 4443 does not mention anything about it, Linux uses the + // TrafficClass of the received echo request when replying. + // https://github.com/torvalds/linux/blob/0280e3c58f9/net/ipv6/icmp.c#L797 + TOS: replyTClass, }, replyPkt); err != nil { sent.dropped.Increment() return diff --git a/pkg/tcpip/transport/icmp/BUILD b/pkg/tcpip/transport/icmp/BUILD index 177912cd7..5affd5459 100644 --- a/pkg/tcpip/transport/icmp/BUILD +++ b/pkg/tcpip/transport/icmp/BUILD @@ -59,6 +59,7 @@ go_test( "//pkg/tcpip/network/ipv4", "//pkg/tcpip/stack", "//pkg/tcpip/testutil", + "//pkg/tcpip/transport/testing/context", "//pkg/waiter", ], ) diff --git a/pkg/tcpip/transport/icmp/endpoint.go b/pkg/tcpip/transport/icmp/endpoint.go index acdc56b32..8f1422e5b 100644 --- a/pkg/tcpip/transport/icmp/endpoint.go +++ b/pkg/tcpip/transport/icmp/endpoint.go @@ -37,6 +37,10 @@ type icmpPacket struct { senderAddress tcpip.FullAddress data buffer.VectorisedView `state:".(buffer.VectorisedView)"` receivedAt time.Time `state:".(int64)"` + + // tosOrTClass stores either the Type of Service for IPv4 or the Traffic Class + // for IPv6. + tosOrTClass uint8 } // endpoint represents an ICMP endpoint. This struct serves as the interface @@ -177,12 +181,32 @@ func (e *endpoint) Read(dst io.Writer, opts tcpip.ReadOptions) (tcpip.ReadResult e.rcvMu.Unlock() + // Control Messages + // TODO(https://gvisor.dev/issue/7012): Share control message code with other + // network endpoints. + cm := tcpip.ControlMessages{ + HasTimestamp: true, + Timestamp: p.receivedAt, + } + switch netProto := e.net.NetProto(); netProto { + case header.IPv4ProtocolNumber: + if e.ops.GetReceiveTOS() { + cm.HasTOS = true + cm.TOS = p.tosOrTClass + } + case header.IPv6ProtocolNumber: + if e.ops.GetReceiveTClass() { + cm.HasTClass = true + // Although TClass is an 8-bit value it's read in the CMsg as a uint32. + cm.TClass = uint32(p.tosOrTClass) + } + default: + panic(fmt.Sprintf("unrecognized network protocol = %d", netProto)) + } + res := tcpip.ReadResult{ - Total: p.data.Size(), - ControlMessages: tcpip.ControlMessages{ - HasTimestamp: true, - Timestamp: p.receivedAt, - }, + Total: p.data.Size(), + ControlMessages: cm, } if opts.NeedRemoteAddr { res.RemoteAddr = p.senderAddress @@ -680,6 +704,9 @@ func (e *endpoint) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketB }, } + // Save any useful information from the network header to the packet. + packet.tosOrTClass, _ = pkt.Network().TOS() + // ICMP socket's data includes ICMP header. packet.data = pkt.TransportHeader().View().ToVectorisedView() packet.data.Append(pkt.Data().ExtractVV()) diff --git a/pkg/tcpip/transport/icmp/icmp_test.go b/pkg/tcpip/transport/icmp/icmp_test.go index ec058b5c2..c88c88d97 100644 --- a/pkg/tcpip/transport/icmp/icmp_test.go +++ b/pkg/tcpip/transport/icmp/icmp_test.go @@ -30,6 +30,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/testutil" "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/testing/context" "gvisor.dev/gvisor/pkg/waiter" ) @@ -42,6 +43,8 @@ var ( remoteV4Addr = testutil.MustParse4("10.0.0.3") ) +const testTOS = 0x80 + func addNICWithDefaultRoute(t *testing.T, s *stack.Stack, id tcpip.NICID, name string, addrV4 tcpip.Address) *channel.Endpoint { t.Helper() @@ -80,18 +83,6 @@ func writePayload(buf []byte) { } } -func newICMPv4EchoRequest(payloadSize uint32) buffer.View { - buf := buffer.NewView(header.ICMPv4MinimumSize + int(payloadSize)) - writePayload(buf[header.ICMPv4MinimumSize:]) - - icmp := header.ICMPv4(buf) - icmp.SetType(header.ICMPv4Echo) - // No need to set the checksum; it is reset by the socket before the packet - // is sent. - - return buf -} - // TestWriteUnboundWithBindToDevice exercises writing to an unbound ICMP socket // when SO_BINDTODEVICE is set to the non-default NIC for that subnet. // @@ -117,10 +108,22 @@ func TestWriteUnboundWithBindToDevice(t *testing.T) { echoPayloadSize := defaultEP.MTU() - header.IPv4MinimumSize - header.ICMPv4MinimumSize + newICMPv4EchoRequest := func() buffer.View { + buf := buffer.NewView(header.ICMPv4MinimumSize + int(echoPayloadSize)) + writePayload(buf[header.ICMPv4MinimumSize:]) + + icmp := header.ICMPv4(buf) + icmp.SetType(header.ICMPv4Echo) + // No need to set the checksum; it is reset by the socket before the packet + // is sent. + + return buf + } + // Send a packet without SO_BINDTODEVICE. This verifies that the first NIC // to be added is the default NIC to send packets when not explicitly bound. { - buf := newICMPv4EchoRequest(echoPayloadSize) + buf := newICMPv4EchoRequest() r := buf.Reader() n, err := socket.Write(&r, tcpip.WriteOptions{ To: &tcpip.FullAddress{Addr: remoteV4Addr}, @@ -162,7 +165,7 @@ func TestWriteUnboundWithBindToDevice(t *testing.T) { // Use SO_BINDTODEVICE to send over the alternate NIC by default. socket.SocketOptions().SetBindToDevice(2) - buf := newICMPv4EchoRequest(echoPayloadSize) + buf := newICMPv4EchoRequest() r := buf.Reader() n, err := socket.Write(&r, tcpip.WriteOptions{ To: &tcpip.FullAddress{Addr: remoteV4Addr}, @@ -204,7 +207,7 @@ func TestWriteUnboundWithBindToDevice(t *testing.T) { { socket.SocketOptions().SetBindToDevice(0) - buf := newICMPv4EchoRequest(echoPayloadSize) + buf := newICMPv4EchoRequest() r := buf.Reader() n, err := socket.Write(&r, tcpip.WriteOptions{ To: &tcpip.FullAddress{Addr: remoteV4Addr}, @@ -241,6 +244,155 @@ func TestWriteUnboundWithBindToDevice(t *testing.T) { } } +func buildV4EchoReplyPacket(payload []byte, h context.Header4Tuple) (buffer.View, buffer.View) { + const ttl = 65 + // Allocate a buffer for data and headers. + buf := buffer.NewView(header.IPv4MinimumSize + header.ICMPv4MinimumSize + len(payload)) + payloadStart := len(buf) - len(payload) + copy(buf[payloadStart:], payload) + + // Initialize the IP header. + ip := header.IPv4(buf) + ip.Encode(&header.IPv4Fields{ + TOS: testTOS, + TotalLength: uint16(len(buf)), + TTL: ttl, + Protocol: uint8(icmp.ProtocolNumber4), + SrcAddr: h.Src.Addr, + DstAddr: h.Dst.Addr, + }) + ip.SetChecksum(^ip.CalculateChecksum()) + + // Initialize the ICMP header. + icmp := header.ICMPv4(buf[header.IPv4MinimumSize:]) + icmp.SetType(header.ICMPv4EchoReply) + icmp.SetCode(header.ICMPv4UnusedCode) + icmp.SetIdent(h.Dst.Port) + icmp.SetChecksum(^header.Checksum(icmp, 0)) + + return buf, buffer.View(icmp) +} + +func buildV6EchoReplyPacket(payload []byte, h context.Header4Tuple) (buffer.View, buffer.View) { + const hoplimit = 65 + // Allocate a buffer for data and headers. + buf := buffer.NewView(header.IPv6MinimumSize + header.ICMPv6EchoMinimumSize + len(payload)) + payloadStart := len(buf) - len(payload) + copy(buf[payloadStart:], payload) + + // Initialize the IP header. + ip := header.IPv6(buf) + ip.Encode(&header.IPv6Fields{ + TrafficClass: testTOS, + PayloadLength: uint16(header.ICMPv6EchoMinimumSize + len(payload)), + TransportProtocol: icmp.ProtocolNumber6, + HopLimit: hoplimit, + SrcAddr: h.Src.Addr, + DstAddr: h.Dst.Addr, + }) + + // Initialize the ICMPv6 header. + icmpv6 := header.ICMPv6(buf[header.IPv6MinimumSize:]) + icmpv6.SetType(header.ICMPv6EchoReply) + icmpv6.SetCode(header.ICMPv6UnusedCode) + icmpv6.SetIdent(h.Dst.Port) + icmpv6.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ + Header: icmpv6[:header.ICMPv6EchoMinimumSize], + Src: h.Src.Addr, + Dst: h.Dst.Addr, + PayloadCsum: header.Checksum(payload, 0), + PayloadLen: len(payload), + })) + + return buf, buffer.View(icmpv6) +} + +// buildEchoReplyPacket builds an ICMPv4 or ICMPv6 echo reply packet, and +// returns the full packet and the ICMP portion of the packet. +func buildEchoReplyPacket(payload []byte, flow context.TestFlow) (buffer.View, buffer.View) { + h := flow.MakeHeader4Tuple(context.Incoming) + if flow.IsV4() { + return buildV4EchoReplyPacket(payload, h) + } + return buildV6EchoReplyPacket(payload, h) +} + +func TestReceiveControlMessages(t *testing.T) { + var payload = [...]byte{0, 1, 2, 3, 4, 5} + + for _, test := range []struct { + name string + optionProtocol tcpip.NetworkProtocolNumber + getReceiveOption func(tcpip.Endpoint) bool + setReceiveOption func(tcpip.Endpoint, bool) + presenceChecker checker.ControlMessagesChecker + absenceChecker checker.ControlMessagesChecker + }{ + { + name: "TOS", + optionProtocol: header.IPv4ProtocolNumber, + getReceiveOption: func(ep tcpip.Endpoint) bool { return ep.SocketOptions().GetReceiveTOS() }, + setReceiveOption: func(ep tcpip.Endpoint, value bool) { ep.SocketOptions().SetReceiveTOS(value) }, + presenceChecker: checker.ReceiveTOS(testTOS), + absenceChecker: checker.NoTOSReceived(), + }, + { + name: "TClass", + optionProtocol: header.IPv6ProtocolNumber, + getReceiveOption: func(ep tcpip.Endpoint) bool { return ep.SocketOptions().GetReceiveTClass() }, + setReceiveOption: func(ep tcpip.Endpoint, value bool) { ep.SocketOptions().SetReceiveTClass(value) }, + presenceChecker: checker.ReceiveTClass(testTOS), + absenceChecker: checker.NoTClassReceived(), + }, + } { + t.Run(test.name, func(t *testing.T) { + for _, flow := range []context.TestFlow{context.UnicastV4, context.UnicastV6, context.UnicastV6Only, context.MulticastV4, context.MulticastV6, context.MulticastV6Only, context.Broadcast} { + t.Run(flow.String(), func(t *testing.T) { + c := context.New(t, []stack.TransportProtocolFactory{icmp.NewProtocol4, icmp.NewProtocol6}) + defer c.Cleanup() + + icmpProto := func() tcpip.TransportProtocolNumber { + if flow.IsV4() { + return icmp.ProtocolNumber4 + } + return icmp.ProtocolNumber6 + }() + + c.CreateEndpointForFlow(flow, icmpProto) + if err := c.EP.Bind(tcpip.FullAddress{Port: context.StackPort}); err != nil { + c.T.Fatalf("Bind failed: %s", err) + } + if flow.IsMulticast() { + netProto := flow.NetProto() + addr := flow.GetMulticastAddr() + if err := c.Stack.JoinGroup(netProto, context.NICID, addr); err != nil { + c.T.Fatalf("JoinGroup(%d, %d, %s): %s", netProto, context.NICID, addr, err) + } + } + + buf, icmp := buildEchoReplyPacket(payload[:], flow) + + if test.getReceiveOption(c.EP) { + t.Fatal("got getReceiveOption() = true, want = false") + } + + test.setReceiveOption(c.EP, true) + if !test.getReceiveOption(c.EP) { + t.Fatal("got getReceiveOption() = false, want = true") + } + + c.InjectPacket(flow.NetProto(), buf) + if flow.NetProto() == test.optionProtocol { + c.ReadFromEndpointExpectSuccess(icmp, flow, test.presenceChecker) + } else { + c.ReadFromEndpointExpectSuccess(icmp, flow, test.absenceChecker) + } + }) + } + }) + } +} + func TestMain(m *testing.M) { refs.SetLeakMode(refs.LeaksPanic) code := m.Run() diff --git a/pkg/tcpip/transport/raw/BUILD b/pkg/tcpip/transport/raw/BUILD index 10b0c35fb..9185926f3 100644 --- a/pkg/tcpip/transport/raw/BUILD +++ b/pkg/tcpip/transport/raw/BUILD @@ -1,4 +1,4 @@ -load("//tools:defs.bzl", "go_library") +load("//tools:defs.bzl", "go_library", "go_test") load("//tools/go_generics:defs.bzl", "go_template_instance") package(licenses = ["notice"]) @@ -40,3 +40,19 @@ go_library( "//pkg/waiter", ], ) + +go_test( + name = "raw_x_test", + size = "small", + srcs = ["raw_test.go"], + deps = [ + "//pkg/refs", + "//pkg/refsvfs2", + "//pkg/tcpip", + "//pkg/tcpip/checker", + "//pkg/tcpip/header", + "//pkg/tcpip/stack", + "//pkg/tcpip/transport/testing/context", + "//pkg/tcpip/transport/udp", + ], +) diff --git a/pkg/tcpip/transport/raw/endpoint.go b/pkg/tcpip/transport/raw/endpoint.go index 5b902b3f9..2ff25366c 100644 --- a/pkg/tcpip/transport/raw/endpoint.go +++ b/pkg/tcpip/transport/raw/endpoint.go @@ -50,6 +50,10 @@ type rawPacket struct { // senderAddr is the network address of the sender. senderAddr tcpip.FullAddress packetInfo tcpip.IPPacketInfo + + // tosOrTClass stores either the Type of Service for IPv4 or the Traffic Class + // for IPv6. + tosOrTClass uint8 } // endpoint is the raw socket implementation of tcpip.Endpoint. It is legal to @@ -222,26 +226,32 @@ func (e *endpoint) Read(dst io.Writer, opts tcpip.ReadOptions) (tcpip.ReadResult e.rcvMu.Unlock() - res := tcpip.ReadResult{ - Total: pkt.data.Size(), - ControlMessages: tcpip.ControlMessages{ - HasTimestamp: true, - Timestamp: pkt.receivedAt, - }, - } - if opts.NeedRemoteAddr { - res.RemoteAddr = pkt.senderAddr + // Control Messages + // TODO(https://gvisor.dev/issue/7012): Share control message code with other + // network endpoints. + cm := tcpip.ControlMessages{ + HasTimestamp: true, + Timestamp: pkt.receivedAt, } switch netProto := e.net.NetProto(); netProto { case header.IPv4ProtocolNumber: + if e.ops.GetReceiveTOS() { + cm.HasTOS = true + cm.TOS = pkt.tosOrTClass + } if e.ops.GetReceivePacketInfo() { - res.ControlMessages.HasIPPacketInfo = true - res.ControlMessages.PacketInfo = pkt.packetInfo + cm.HasIPPacketInfo = true + cm.PacketInfo = pkt.packetInfo } case header.IPv6ProtocolNumber: + if e.ops.GetReceiveTClass() { + cm.HasTClass = true + // Although TClass is an 8-bit value it's read in the CMsg as a uint32. + cm.TClass = uint32(pkt.tosOrTClass) + } if e.ops.GetIPv6ReceivePacketInfo() { - res.ControlMessages.HasIPv6PacketInfo = true - res.ControlMessages.IPv6PacketInfo = tcpip.IPv6PacketInfo{ + cm.HasIPv6PacketInfo = true + cm.IPv6PacketInfo = tcpip.IPv6PacketInfo{ NIC: pkt.packetInfo.NIC, Addr: pkt.packetInfo.DestinationAddr, } @@ -250,6 +260,14 @@ func (e *endpoint) Read(dst io.Writer, opts tcpip.ReadOptions) (tcpip.ReadResult panic(fmt.Sprintf("unrecognized network protocol = %d", netProto)) } + res := tcpip.ReadResult{ + Total: pkt.data.Size(), + ControlMessages: cm, + } + if opts.NeedRemoteAddr { + res.RemoteAddr = pkt.senderAddr + } + n, err := pkt.data.ReadTo(dst, opts.Peek) if n == 0 && err != nil { return res, &tcpip.ErrBadBuffer{} @@ -604,6 +622,9 @@ func (e *endpoint) HandlePacket(pkt *stack.PacketBuffer) { }, } + // Save any useful information from the network header to the packet. + packet.tosOrTClass, _ = pkt.Network().TOS() + // Raw IPv4 endpoints return the IP header, but IPv6 endpoints do not. // We copy headers' underlying bytes because pkt.*Header may point to // the middle of a slice, and another struct may point to the "outer" diff --git a/pkg/tcpip/transport/raw/raw_test.go b/pkg/tcpip/transport/raw/raw_test.go new file mode 100644 index 000000000..92bb3ed77 --- /dev/null +++ b/pkg/tcpip/transport/raw/raw_test.go @@ -0,0 +1,116 @@ +// Copyright 2022 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 raw_test + +import ( + "os" + "testing" + + "gvisor.dev/gvisor/pkg/refs" + "gvisor.dev/gvisor/pkg/refsvfs2" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/checker" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/testing/context" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +const ( + testTOS = 0x80 + testTTL = 65 +) + +func TestReceiveControlMessage(t *testing.T) { + var payload = [...]byte{0, 1, 2, 3, 4, 5} + + for _, test := range []struct { + name string + optionProtocol tcpip.NetworkProtocolNumber + getReceiveOption func(tcpip.Endpoint) bool + setReceiveOption func(tcpip.Endpoint, bool) + presenceChecker checker.ControlMessagesChecker + absenceChecker checker.ControlMessagesChecker + }{ + { + name: "TOS", + optionProtocol: header.IPv4ProtocolNumber, + getReceiveOption: func(ep tcpip.Endpoint) bool { return ep.SocketOptions().GetReceiveTOS() }, + setReceiveOption: func(ep tcpip.Endpoint, value bool) { ep.SocketOptions().SetReceiveTOS(value) }, + presenceChecker: checker.ReceiveTOS(testTOS), + absenceChecker: checker.NoTOSReceived(), + }, + { + name: "TClass", + optionProtocol: header.IPv6ProtocolNumber, + getReceiveOption: func(ep tcpip.Endpoint) bool { return ep.SocketOptions().GetReceiveTClass() }, + setReceiveOption: func(ep tcpip.Endpoint, value bool) { ep.SocketOptions().SetReceiveTClass(value) }, + presenceChecker: checker.ReceiveTClass(testTOS), + absenceChecker: checker.NoTClassReceived(), + }, + } { + t.Run(test.name, func(t *testing.T) { + for _, flow := range []context.TestFlow{context.UnicastV4, context.UnicastV6, context.UnicastV6Only, context.MulticastV4, context.MulticastV6, context.MulticastV6Only, context.Broadcast} { + t.Run(flow.String(), func(t *testing.T) { + + c := context.New(t, []stack.TransportProtocolFactory{udp.NewProtocol}) + defer c.Cleanup() + + c.CreateRawEndpointForFlow(flow, header.UDPProtocolNumber) + if err := c.EP.Bind(tcpip.FullAddress{Port: context.StackPort}); err != nil { + c.T.Fatalf("Bind failed: %s", err) + } + if flow.IsMulticast() { + netProto := flow.NetProto() + addr := flow.GetMulticastAddr() + if err := c.Stack.JoinGroup(netProto, context.NICID, addr); err != nil { + c.T.Fatalf("JoinGroup(%d, %d, %s): %s", netProto, context.NICID, addr, err) + } + } + + buf := context.BuildUDPPacket(payload[:], flow, context.Incoming, testTOS, testTTL, false) + expectedReadData := buf + if flow.IsV6() { + // Raw IPv6 endpoints do not return the network header. + expectedReadData = expectedReadData[header.IPv6MinimumSize:] + } + + if test.getReceiveOption(c.EP) { + t.Fatal("got getReceiveOption() = true, want = false") + } + + test.setReceiveOption(c.EP, true) + if !test.getReceiveOption(c.EP) { + t.Fatal("got getReceiveOption() = false, want = true") + } + + c.InjectPacket(flow.NetProto(), buf) + if flow.NetProto() == test.optionProtocol { + c.ReadFromEndpointExpectSuccess(expectedReadData, flow, test.presenceChecker) + } else { + c.ReadFromEndpointExpectSuccess(expectedReadData, flow, test.absenceChecker) + } + }) + } + }) + } +} + +func TestMain(m *testing.M) { + refs.SetLeakMode(refs.LeaksPanic) + code := m.Run() + refsvfs2.DoLeakCheck() + os.Exit(code) +} diff --git a/pkg/tcpip/transport/testing/context/BUILD b/pkg/tcpip/transport/testing/context/BUILD index 25a7120f5..e38384d66 100644 --- a/pkg/tcpip/transport/testing/context/BUILD +++ b/pkg/tcpip/transport/testing/context/BUILD @@ -23,6 +23,8 @@ go_library( "//pkg/tcpip/network/ipv4", "//pkg/tcpip/network/ipv6", "//pkg/tcpip/stack", + "//pkg/tcpip/transport/raw", + "//pkg/tcpip/transport/udp", "//pkg/waiter", "@com_github_google_go_cmp//cmp:go_default_library", "@org_golang_x_time//rate:go_default_library", diff --git a/pkg/tcpip/transport/testing/context/context.go b/pkg/tcpip/transport/testing/context/context.go index 037abe237..bece02311 100644 --- a/pkg/tcpip/transport/testing/context/context.go +++ b/pkg/tcpip/transport/testing/context/context.go @@ -32,6 +32,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/raw" "gvisor.dev/gvisor/pkg/waiter" ) @@ -96,6 +97,7 @@ func NewWithOptions(t *testing.T, transportProtocols []stack.TransportProtocolFa TransportProtocols: transportProtocols, HandleLocal: options.HandleLocal, Clock: &faketime.NullClock{}, + RawFactory: &raw.EndpointFactory{}, } s := stack.New(stackOptions) @@ -178,6 +180,30 @@ func (c *Context) CreateEndpointForFlow(flow TestFlow, transport tcpip.Transport } } +// CreateRawEndpoint creates the Context's Endpoint. +func (c *Context) CreateRawEndpoint(network tcpip.NetworkProtocolNumber, transport tcpip.TransportProtocolNumber) { + c.T.Helper() + + var err tcpip.Error + c.EP, err = c.Stack.NewRawEndpoint(transport, network, &c.WQ, true /* associated */) + if err != nil { + c.T.Fatal("c.Stack.NewRawEndpoint failed: ", err) + } +} + +// CreateRawEndpointForFlow creates the Context's Endpoint and configured it +// according to the given TestFlow. +func (c *Context) CreateRawEndpointForFlow(flow TestFlow, transport tcpip.TransportProtocolNumber) { + c.T.Helper() + + c.CreateRawEndpoint(flow.SockProto(), transport) + if flow.isV6Only() { + c.EP.SocketOptions().SetV6Only(true) + } else if flow.isBroadcast() { + c.EP.SocketOptions().SetBroadcast(true) + } +} + // CheckEndpointWriteStats checks that the write statistic related to the given // error has been incremented as expected. func (c *Context) CheckEndpointWriteStats(incr uint64, want tcpip.TransportEndpointStats, err tcpip.Error) { diff --git a/pkg/tcpip/transport/testing/context/flow.go b/pkg/tcpip/transport/testing/context/flow.go index 4ca3562b8..96fd10e0f 100644 --- a/pkg/tcpip/transport/testing/context/flow.go +++ b/pkg/tcpip/transport/testing/context/flow.go @@ -19,10 +19,12 @@ import ( "testing" "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/buffer" "gvisor.dev/gvisor/pkg/tcpip/checker" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" ) const ( @@ -338,3 +340,104 @@ func (flow TestFlow) isReverseMulticast() bool { return false } } + +// BuildV4UDPPacket builds an IPv4 UDP packet. +func BuildV4UDPPacket(payload []byte, h Header4Tuple, tos, ttl uint8, badChecksum bool) buffer.View { + // Allocate a buffer for data and headers. + buf := buffer.NewView(header.UDPMinimumSize + header.IPv4MinimumSize + len(payload)) + payloadStart := len(buf) - len(payload) + copy(buf[payloadStart:], payload) + + // Initialize the IP header. + ip := header.IPv4(buf) + ip.Encode(&header.IPv4Fields{ + TOS: tos, + TotalLength: uint16(len(buf)), + TTL: ttl, + Protocol: uint8(udp.ProtocolNumber), + SrcAddr: h.Src.Addr, + DstAddr: h.Dst.Addr, + }) + ip.SetChecksum(^ip.CalculateChecksum()) + + // Initialize the UDP header. + u := header.UDP(buf[header.IPv4MinimumSize:]) + u.Encode(&header.UDPFields{ + SrcPort: h.Src.Port, + DstPort: h.Dst.Port, + Length: uint16(header.UDPMinimumSize + len(payload)), + }) + + // Calculate the UDP pseudo-header checksum. + xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, h.Src.Addr, h.Dst.Addr, uint16(len(u))) + + // Calculate the UDP checksum and set it. + xsum = header.Checksum(payload, xsum) + u.SetChecksum(^u.CalculateChecksum(xsum)) + + if badChecksum { + // Invalidate the UDP header checksum field, taking care to avoid overflow + // to zero, which would disable checksum validation. + for { + u.SetChecksum(u.Checksum() + 1) + if u.Checksum() != 0 { + break + } + } + } + + return buf +} + +// BuildV6UDPPacket builds an IPv6 UDP packet. +func BuildV6UDPPacket(payload []byte, h Header4Tuple, tclass, hoplimit uint8, badChecksum bool) buffer.View { + // Allocate a buffer for data and headers. + buf := buffer.NewView(header.UDPMinimumSize + header.IPv6MinimumSize + len(payload)) + payloadStart := len(buf) - len(payload) + copy(buf[payloadStart:], payload) + + // Initialize the IP header. + ip := header.IPv6(buf) + ip.Encode(&header.IPv6Fields{ + TrafficClass: tclass, + PayloadLength: uint16(header.UDPMinimumSize + len(payload)), + TransportProtocol: udp.ProtocolNumber, + HopLimit: hoplimit, + SrcAddr: h.Src.Addr, + DstAddr: h.Dst.Addr, + }) + + // Initialize the UDP header. + u := header.UDP(buf[header.IPv6MinimumSize:]) + u.Encode(&header.UDPFields{ + SrcPort: h.Src.Port, + DstPort: h.Dst.Port, + Length: uint16(header.UDPMinimumSize + len(payload)), + }) + + // Calculate the UDP pseudo-header checksum. + xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, h.Src.Addr, h.Dst.Addr, uint16(len(u))) + + // Calculate the UDP checksum and set it. + xsum = header.Checksum(payload, xsum) + u.SetChecksum(^u.CalculateChecksum(xsum)) + + if badChecksum { + // Invalidate the UDP header checksum field (Unlike IPv4, zero is a valid + // checksum value for IPv6 so no need to avoid it). + u := header.UDP(buf[header.IPv6MinimumSize:]) + u.SetChecksum(u.Checksum() + 1) + } + + return buf +} + +// BuildUDPPacket builds an IPv4 or IPv6 UDP packet, depending on the specified +// TestFlow. +func BuildUDPPacket(payload []byte, flow TestFlow, direction PacketDirection, tosOrTclass, ttlOrHopLimit uint8, badChecksum bool) buffer.View { + h := flow.MakeHeader4Tuple(direction) + if flow.IsV4() { + return BuildV4UDPPacket(payload, h, tosOrTclass, ttlOrHopLimit, badChecksum) + } + return BuildV6UDPPacket(payload, h, tosOrTclass, ttlOrHopLimit, badChecksum) +} diff --git a/pkg/tcpip/transport/udp/endpoint.go b/pkg/tcpip/transport/udp/endpoint.go index 7b17e9c7f..b005b1dac 100644 --- a/pkg/tcpip/transport/udp/endpoint.go +++ b/pkg/tcpip/transport/udp/endpoint.go @@ -39,8 +39,9 @@ type udpPacket struct { packetInfo tcpip.IPPacketInfo pkt *stack.PacketBuffer receivedAt time.Time `state:".(int64)"` - // tos stores either the receiveTOS or receiveTClass value. - tos uint8 + // tosOrTClass stores either the Type of Service for IPv4 or the Traffic Class + // for IPv6. + tosOrTClass uint8 } // endpoint represents a UDP endpoint. This struct serves as the interface @@ -233,18 +234,18 @@ func (e *endpoint) Read(dst io.Writer, opts tcpip.ReadOptions) (tcpip.ReadResult e.rcvMu.Unlock() // Control Messages + // TODO(https://gvisor.dev/issue/7012): Share control message code with other + // network endpoints. cm := tcpip.ControlMessages{ HasTimestamp: true, Timestamp: p.receivedAt, } - switch p.netProto { case header.IPv4ProtocolNumber: if e.ops.GetReceiveTOS() { cm.HasTOS = true - cm.TOS = p.tos + cm.TOS = p.tosOrTClass } - if e.ops.GetReceivePacketInfo() { cm.HasIPPacketInfo = true cm.PacketInfo = p.packetInfo @@ -253,9 +254,8 @@ func (e *endpoint) Read(dst io.Writer, opts tcpip.ReadOptions) (tcpip.ReadResult if e.ops.GetReceiveTClass() { cm.HasTClass = true // Although TClass is an 8-bit value it's read in the CMsg as a uint32. - cm.TClass = uint32(p.tos) + cm.TClass = uint32(p.tosOrTClass) } - if e.ops.GetIPv6ReceivePacketInfo() { cm.HasIPv6PacketInfo = true cm.IPv6PacketInfo = tcpip.IPv6PacketInfo{ @@ -926,12 +926,7 @@ func (e *endpoint) HandlePacket(id stack.TransportEndpointID, pkt *stack.PacketB e.rcvBufSize += pkt.Data().Size() // Save any useful information from the network header to the packet. - switch pkt.NetworkProtocolNumber { - case header.IPv4ProtocolNumber: - packet.tos, _ = header.IPv4(pkt.NetworkHeader().View()).TOS() - case header.IPv6ProtocolNumber: - packet.tos, _ = header.IPv6(pkt.NetworkHeader().View()).TOS() - } + packet.tosOrTClass, _ = pkt.Network().TOS() // TODO(gvisor.dev/issue/3556): r.LocalAddress may be a multicast or broadcast // address. packetInfo.LocalAddr should hold a unicast address that can be diff --git a/pkg/tcpip/transport/udp/udp_test.go b/pkg/tcpip/transport/udp/udp_test.go index 2ded2cb5d..3c5aabac7 100644 --- a/pkg/tcpip/transport/udp/udp_test.go +++ b/pkg/tcpip/transport/udp/udp_test.go @@ -44,6 +44,7 @@ import ( const ( testTOS = 0x80 + testTTL = 65 arbitraryPayloadSize = 30 ) @@ -57,115 +58,18 @@ func newRandomPayload(size int) []byte { return b } -func buildV4Packet(payload []byte, h context.Header4Tuple, badChecksum bool) buffer.View { - // Allocate a buffer for data and headers. - buf := buffer.NewView(header.UDPMinimumSize + header.IPv4MinimumSize + len(payload)) - payloadStart := len(buf) - len(payload) - copy(buf[payloadStart:], payload) - - // Initialize the IP header. - ip := header.IPv4(buf) - ip.Encode(&header.IPv4Fields{ - TOS: testTOS, - TotalLength: uint16(len(buf)), - TTL: 65, - Protocol: uint8(udp.ProtocolNumber), - SrcAddr: h.Src.Addr, - DstAddr: h.Dst.Addr, - }) - ip.SetChecksum(^ip.CalculateChecksum()) - - // Initialize the UDP header. - u := header.UDP(buf[header.IPv4MinimumSize:]) - u.Encode(&header.UDPFields{ - SrcPort: h.Src.Port, - DstPort: h.Dst.Port, - Length: uint16(header.UDPMinimumSize + len(payload)), - }) - - // Calculate the UDP pseudo-header checksum. - xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, h.Src.Addr, h.Dst.Addr, uint16(len(u))) - - // Calculate the UDP checksum and set it. - xsum = header.Checksum(payload, xsum) - u.SetChecksum(^u.CalculateChecksum(xsum)) - - if badChecksum { - // Invalidate the UDP header checksum field, taking care to avoid overflow - // to zero, which would disable checksum validation. - for { - u.SetChecksum(u.Checksum() + 1) - if u.Checksum() != 0 { - break - } - } - } - - return buf -} - -func buildV6Packet(payload []byte, h context.Header4Tuple, badChecksum bool) buffer.View { - // Allocate a buffer for data and headers. - buf := buffer.NewView(header.UDPMinimumSize + header.IPv6MinimumSize + len(payload)) - payloadStart := len(buf) - len(payload) - copy(buf[payloadStart:], payload) - - // Initialize the IP header. - ip := header.IPv6(buf) - ip.Encode(&header.IPv6Fields{ - TrafficClass: testTOS, - PayloadLength: uint16(header.UDPMinimumSize + len(payload)), - TransportProtocol: udp.ProtocolNumber, - HopLimit: 65, - SrcAddr: h.Src.Addr, - DstAddr: h.Dst.Addr, - }) - - // Initialize the UDP header. - u := header.UDP(buf[header.IPv6MinimumSize:]) - u.Encode(&header.UDPFields{ - SrcPort: h.Src.Port, - DstPort: h.Dst.Port, - Length: uint16(header.UDPMinimumSize + len(payload)), - }) - - // Calculate the UDP pseudo-header checksum. - xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, h.Src.Addr, h.Dst.Addr, uint16(len(u))) - - // Calculate the UDP checksum and set it. - xsum = header.Checksum(payload, xsum) - u.SetChecksum(^u.CalculateChecksum(xsum)) - - if badChecksum { - // Invalidate the UDP header checksum field (Unlike IPv4, zero is a valid - // checksum value for IPv6 so no need to avoid it). - u := header.UDP(buf[header.IPv6MinimumSize:]) - u.SetChecksum(u.Checksum() + 1) - } - - return buf -} - -func buildPacket(payload []byte, flow context.TestFlow, direction context.PacketDirection, badChecksum bool) buffer.View { - h := flow.MakeHeader4Tuple(direction) - if flow.IsV4() { - return buildV4Packet(payload, h, badChecksum) - } - return buildV6Packet(payload, h, badChecksum) -} - func testRead(c *context.Context, flow context.TestFlow, checkers ...checker.ControlMessagesChecker) { c.T.Helper() payload := newRandomPayload(arbitraryPayloadSize) - c.InjectPacket(flow.NetProto(), buildPacket(payload, flow, context.Incoming, false)) + c.InjectPacket(flow.NetProto(), context.BuildUDPPacket(payload, flow, context.Incoming, testTOS, testTTL, false)) c.ReadFromEndpointExpectSuccess(payload, flow, checkers...) } func testFailingRead(c *context.Context, flow context.TestFlow, expectReadError bool) { c.T.Helper() - c.InjectPacket(flow.NetProto(), buildPacket(newRandomPayload(arbitraryPayloadSize), flow, context.Incoming, false)) + c.InjectPacket(flow.NetProto(), context.BuildUDPPacket(newRandomPayload(arbitraryPayloadSize), flow, context.Incoming, testTOS, testTTL, false)) if expectReadError { c.ReadFromEndpointExpectError() } else { @@ -312,7 +216,7 @@ func TestV4ReadOnV6(t *testing.T) { } payload := newRandomPayload(arbitraryPayloadSize) - buf := buildPacket(payload, context.UnicastV4in6, context.Incoming, false) + buf := context.BuildUDPPacket(payload, context.UnicastV4in6, context.Incoming, testTOS, testTTL, false) c.InjectPacket(header.IPv4ProtocolNumber, buf) c.ReadFromEndpointExpectSuccess(payload, context.UnicastV4in6) } @@ -390,7 +294,7 @@ func TestV4ReadSelfSource(t *testing.T) { payload := newRandomPayload(arbitraryPayloadSize) h := context.UnicastV4.MakeHeader4Tuple(context.Incoming) h.Src = h.Dst - c.InjectPacket(header.IPv4ProtocolNumber, buildV4Packet(payload, h, false)) + c.InjectPacket(header.IPv4ProtocolNumber, context.BuildV4UDPPacket(payload, h, testTOS, testTTL, false)) if got := c.Stack.Stats().IP.InvalidSourceAddressesReceived.Value(); got != tt.wantInvalidSource { t.Errorf("c.Stack.Stats().IP.InvalidSourceAddressesReceived got %d, want %d", got, tt.wantInvalidSource) @@ -1341,83 +1245,71 @@ func TestSetTClass(t *testing.T) { } } -func TestReceiveTosTClass(t *testing.T) { - const RcvTOSOpt = "ReceiveTosOption" - const RcvTClassOpt = "ReceiveTClassOption" - - testCases := []struct { - name string - tests []context.TestFlow +func TestReceiveControlMessage(t *testing.T) { + for _, test := range []struct { + name string + optionProtocol tcpip.NetworkProtocolNumber + getReceiveOption func(tcpip.Endpoint) bool + setReceiveOption func(tcpip.Endpoint, bool) + presenceChecker checker.ControlMessagesChecker + absenceChecker checker.ControlMessagesChecker }{ { - name: RcvTOSOpt, - tests: v4PacketFlows[:], + name: "TOS", + optionProtocol: header.IPv4ProtocolNumber, + getReceiveOption: func(ep tcpip.Endpoint) bool { return ep.SocketOptions().GetReceiveTOS() }, + setReceiveOption: func(ep tcpip.Endpoint, value bool) { ep.SocketOptions().SetReceiveTOS(value) }, + presenceChecker: checker.ReceiveTOS(testTOS), + absenceChecker: checker.NoTOSReceived(), }, { - name: RcvTClassOpt, - tests: v6PacketFlows[:], + name: "TClass", + optionProtocol: header.IPv6ProtocolNumber, + getReceiveOption: func(ep tcpip.Endpoint) bool { return ep.SocketOptions().GetReceiveTClass() }, + setReceiveOption: func(ep tcpip.Endpoint, value bool) { ep.SocketOptions().SetReceiveTClass(value) }, + presenceChecker: checker.ReceiveTClass(testTOS), + absenceChecker: checker.NoTClassReceived(), }, - } - for _, testCase := range testCases { - for _, flow := range testCase.tests { - t.Run(fmt.Sprintf("%s:flow:%s", testCase.name, flow), func(t *testing.T) { - c := context.New(t, []stack.TransportProtocolFactory{udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4}) - defer c.Cleanup() + } { + t.Run(test.name, func(t *testing.T) { + for _, flow := range []context.TestFlow{context.UnicastV4, context.UnicastV6, context.UnicastV6Only, context.MulticastV4, context.MulticastV6, context.MulticastV6Only, context.Broadcast} { + t.Run(flow.String(), func(t *testing.T) { + c := context.New(t, []stack.TransportProtocolFactory{udp.NewProtocol}) + defer c.Cleanup() - c.CreateEndpointForFlow(flow, udp.ProtocolNumber) - name := testCase.name - - if flow.IsMulticast() { - netProto := flow.NetProto() - addr := flow.GetMulticastAddr() - if err := c.Stack.JoinGroup(netProto, context.NICID, addr); err != nil { - c.T.Fatalf("JoinGroup(%d, %d, %s): %s", netProto, context.NICID, addr, err) + c.CreateEndpointForFlow(flow, udp.ProtocolNumber) + if err := c.EP.Bind(tcpip.FullAddress{Port: context.StackPort}); err != nil { + c.T.Fatalf("Bind failed: %s", err) + } + if flow.IsMulticast() { + netProto := flow.NetProto() + addr := flow.GetMulticastAddr() + if err := c.Stack.JoinGroup(netProto, context.NICID, addr); err != nil { + c.T.Fatalf("JoinGroup(%d, %d, %s): %s", netProto, context.NICID, addr, err) + } } - } - var optionGetter func() bool - var optionSetter func(bool) - switch name { - case RcvTOSOpt: - optionGetter = c.EP.SocketOptions().GetReceiveTOS - optionSetter = c.EP.SocketOptions().SetReceiveTOS - case RcvTClassOpt: - optionGetter = c.EP.SocketOptions().GetReceiveTClass - optionSetter = c.EP.SocketOptions().SetReceiveTClass - default: - t.Fatalf("unkown test variant: %s", name) - } + payload := newRandomPayload(arbitraryPayloadSize) + buf := context.BuildUDPPacket(payload, flow, context.Incoming, testTOS, testTTL, false) - // Verify that setting and reading the option works. - v := optionGetter() - // Test for expected default value. - if v != false { - c.T.Errorf("got GetSockOptBool(%s) = %t, want = %t", name, v, false) - } + if test.getReceiveOption(c.EP) { + t.Fatal("got getReceiveOption() = true, want = false") + } - const want = true - optionSetter(want) + test.setReceiveOption(c.EP, true) + if !test.getReceiveOption(c.EP) { + t.Fatal("got getReceiveOption() = false, want = true") + } - got := optionGetter() - if got != want { - c.T.Errorf("got GetSockOptBool(%s) = %t, want = %t", name, got, want) - } - - // Verify that the correct received TOS or TClass is handed through as - // ancillary data to the ControlMessages struct. - if err := c.EP.Bind(tcpip.FullAddress{Port: context.StackPort}); err != nil { - c.T.Fatalf("Bind failed: %s", err) - } - switch name { - case RcvTClassOpt: - testRead(c, flow, checker.ReceiveTClass(testTOS)) - case RcvTOSOpt: - testRead(c, flow, checker.ReceiveTOS(testTOS)) - default: - t.Fatalf("unknown test variant: %s", name) - } - }) - } + c.InjectPacket(flow.NetProto(), buf) + if flow.NetProto() == test.optionProtocol { + c.ReadFromEndpointExpectSuccess(payload, flow, test.presenceChecker) + } else { + c.ReadFromEndpointExpectSuccess(payload, flow, test.absenceChecker) + } + }) + } + }) } } @@ -1518,7 +1410,7 @@ func TestV4UnknownDestination(t *testing.T) { payloadSize += header.IPv4MinimumProcessableDatagramSize } payload := newRandomPayload(payloadSize) - c.InjectPacket(tc.flow.NetProto(), buildPacket(payload, tc.flow, context.Incoming, tc.badChecksum)) + c.InjectPacket(tc.flow.NetProto(), context.BuildUDPPacket(payload, tc.flow, context.Incoming, testTOS, testTTL, tc.badChecksum)) if tc.badChecksum { checksumErrors++ if got, want := c.Stack.Stats().UDP.ChecksumErrors.Value(), checksumErrors; got != want { @@ -1612,7 +1504,7 @@ func TestV6UnknownDestination(t *testing.T) { payloadSize += header.IPv6MinimumMTU } payload := newRandomPayload(payloadSize) - c.InjectPacket(tc.flow.NetProto(), buildPacket(payload, tc.flow, context.Incoming, tc.badChecksum)) + c.InjectPacket(tc.flow.NetProto(), context.BuildUDPPacket(payload, tc.flow, context.Incoming, testTOS, testTTL, tc.badChecksum)) if tc.badChecksum { checksumErrors++ if got, want := c.Stack.Stats().UDP.ChecksumErrors.Value(), checksumErrors; got != want { @@ -1678,7 +1570,7 @@ func TestIncrementMalformedPacketsReceived(t *testing.T) { payload := newRandomPayload(arbitraryPayloadSize) h := context.UnicastV6.MakeHeader4Tuple(context.Incoming) - buf := buildV6Packet(payload, h, false) + buf := context.BuildV6UDPPacket(payload, h, testTOS, testTTL, false) // Invalidate the UDP header length field. u := header.UDP(buf[header.IPv6MinimumSize:]) @@ -1717,7 +1609,7 @@ func TestShortHeader(t *testing.T) { TrafficClass: testTOS, PayloadLength: uint16(udpSize), TransportProtocol: udp.ProtocolNumber, - HopLimit: 65, + HopLimit: testTTL, SrcAddr: h.Src.Addr, DstAddr: h.Dst.Addr, }) @@ -1757,7 +1649,7 @@ func TestBadChecksumErrors(t *testing.T) { c.T.Fatalf("Bind failed: %s", err) } - c.InjectPacket(flow.NetProto(), buildPacket(newRandomPayload(arbitraryPayloadSize), flow, context.Incoming, true)) + c.InjectPacket(flow.NetProto(), context.BuildUDPPacket(newRandomPayload(arbitraryPayloadSize), flow, context.Incoming, testTOS, testTTL, true)) const want = 1 if got := c.Stack.Stats().UDP.ChecksumErrors.Value(); got != want { @@ -1784,7 +1676,7 @@ func TestPayloadModifiedV4(t *testing.T) { payload := newRandomPayload(arbitraryPayloadSize) h := context.UnicastV4.MakeHeader4Tuple(context.Incoming) - buf := buildV4Packet(payload, h, false) + buf := context.BuildV4UDPPacket(payload, h, testTOS, testTTL, false) // Modify the payload so that the checksum value in the UDP header will be // incorrect. buf[len(buf)-1]++ @@ -1813,7 +1705,7 @@ func TestPayloadModifiedV6(t *testing.T) { payload := newRandomPayload(arbitraryPayloadSize) h := context.UnicastV6.MakeHeader4Tuple(context.Incoming) - buf := buildV6Packet(payload, h, false) + buf := context.BuildV6UDPPacket(payload, h, testTOS, testTTL, false) // Modify the payload so that the checksum value in the UDP header will be // incorrect. buf[len(buf)-1]++ @@ -1842,7 +1734,7 @@ func TestChecksumZeroV4(t *testing.T) { payload := newRandomPayload(arbitraryPayloadSize) h := context.UnicastV4.MakeHeader4Tuple(context.Incoming) - buf := buildV4Packet(payload, h, false) + buf := context.BuildV4UDPPacket(payload, h, testTOS, testTTL, false) // Set the checksum field in the UDP header to zero. u := header.UDP(buf[header.IPv4MinimumSize:]) u.SetChecksum(0) @@ -1871,7 +1763,7 @@ func TestChecksumZeroV6(t *testing.T) { payload := newRandomPayload(arbitraryPayloadSize) h := context.UnicastV6.MakeHeader4Tuple(context.Incoming) - buf := buildV6Packet(payload, h, false) + buf := context.BuildV6UDPPacket(payload, h, testTOS, testTTL, false) // Set the checksum field in the UDP header to zero. u := header.UDP(buf[header.IPv6MinimumSize:]) u.SetChecksum(0) diff --git a/test/syscalls/linux/ip_socket_test_util.cc b/test/syscalls/linux/ip_socket_test_util.cc index d18e616d0..6aac04610 100644 --- a/test/syscalls/linux/ip_socket_test_util.cc +++ b/test/syscalls/linux/ip_socket_test_util.cc @@ -246,5 +246,45 @@ std::string GetAddrStr(const sockaddr* a) { } } +namespace { +template +void RecvCmsg(int sock, int cmsg_level, int cmsg_type, char buf[], + size_t* buf_size, T* out_cmsg_value) { + struct iovec iov = { + iov.iov_base = buf, + iov.iov_len = *buf_size, + }; + // Add an extra byte to confirm we only read what we expected. + char control[CMSG_SPACE(sizeof(*out_cmsg_value)) + 1]; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = control, + .msg_controllen = sizeof(control), + }; + + ASSERT_THAT(*buf_size = RetryEINTR(recvmsg)(sock, &msg, /*flags=*/0), + SyscallSucceeds()); + ASSERT_EQ(msg.msg_controllen, CMSG_SPACE(sizeof(*out_cmsg_value))); + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); + ASSERT_NE(cmsg, nullptr); + ASSERT_EQ(cmsg->cmsg_len, CMSG_LEN(sizeof(*out_cmsg_value))); + ASSERT_EQ(cmsg->cmsg_level, cmsg_level); + ASSERT_EQ(cmsg->cmsg_type, cmsg_type); + + std::copy_n(CMSG_DATA(cmsg), sizeof(*out_cmsg_value), + reinterpret_cast(out_cmsg_value)); +} +} // namespace + +void RecvTOS(int sock, char buf[], size_t* buf_size, uint8_t* out_tos) { + RecvCmsg(sock, SOL_IP, IP_TOS, buf, buf_size, out_tos); +} + +void RecvTClass(int sock, char buf[], size_t* buf_size, int* out_tclass) { + RecvCmsg(sock, SOL_IPV6, IPV6_TCLASS, buf, buf_size, out_tclass); +} + } // namespace testing } // namespace gvisor diff --git a/test/syscalls/linux/ip_socket_test_util.h b/test/syscalls/linux/ip_socket_test_util.h index 957006e25..ba370575d 100644 --- a/test/syscalls/linux/ip_socket_test_util.h +++ b/test/syscalls/linux/ip_socket_test_util.h @@ -125,6 +125,17 @@ std::string GetAddr6Str(const in6_addr* a); // string. std::string GetAddrStr(const sockaddr* a); +// RecvTOS attempts to read buf_size bytes into buf, and then update buf_size +// with the numbers of bytes actually read. It expects the IP_TOS cmsg to be +// received. The buffer must already be allocated with at least buf_size size. +void RecvTOS(int sock, char buf[], size_t* buf_size, uint8_t* out_tos); + +// RecvTClass attempts to read buf_size bytes into buf, and then update buf_size +// with the numbers of bytes actually read. It expects the IPV6_TCLASS cmsg to +// be received. The buffer must already be allocated with at least buf_size +// size. +void RecvTClass(int sock, char buf[], size_t* buf_size, int* out_tclass); + } // namespace testing } // namespace gvisor diff --git a/test/syscalls/linux/ping_socket.cc b/test/syscalls/linux/ping_socket.cc index 025c9568f..285b44dc4 100644 --- a/test/syscalls/linux/ping_socket.cc +++ b/test/syscalls/linux/ping_socket.cc @@ -13,6 +13,7 @@ // limitations under the License. #include +#include #include #include #include @@ -80,6 +81,116 @@ TEST(PingSocket, ICMPPortExhaustion) { } } +TEST(PingSocket, ReceiveTOS) { + PosixErrorOr result = + Socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); + if (!result.ok()) { + int errno_value = result.error().errno_value(); + ASSERT_EQ(errno_value, EACCES) << strerror(errno_value); + GTEST_SKIP() << "ping socket not supported"; + } + FileDescriptor& ping = result.ValueOrDie(); + + const sockaddr_in kAddr = { + .sin_family = AF_INET, + .sin_addr = {.s_addr = htonl(INADDR_LOOPBACK)}, + }; + ASSERT_THAT(bind(ping.get(), reinterpret_cast(&kAddr), + sizeof(kAddr)), + SyscallSucceeds()); + + constexpr int kArbitraryTOS = 42; + ASSERT_THAT(setsockopt(ping.get(), IPPROTO_IP, IP_TOS, &kArbitraryTOS, + sizeof(kArbitraryTOS)), + SyscallSucceeds()); + + constexpr icmphdr kSendIcmp = { + .type = ICMP_ECHO, + }; + ASSERT_THAT(sendto(ping.get(), &kSendIcmp, sizeof(kSendIcmp), 0, + reinterpret_cast(&kAddr), sizeof(kAddr)), + SyscallSucceedsWithValue(sizeof(kSendIcmp))); + + // Register to receive TOS. + constexpr int kOne = 1; + ASSERT_THAT( + setsockopt(ping.get(), IPPROTO_IP, IP_RECVTOS, &kOne, sizeof(kOne)), + SyscallSucceeds()); + + struct { + icmphdr icmp; + + // Add an extra byte to confirm we did not read unexpected bytes. + char unused; + } ABSL_ATTRIBUTE_PACKED recv_buf; + size_t recv_buf_len = sizeof(recv_buf); + uint8_t received_tos; + ASSERT_NO_FATAL_FAILURE(RecvTOS(ping.get(), + reinterpret_cast(&recv_buf), + &recv_buf_len, &received_tos)); + ASSERT_EQ(recv_buf_len, sizeof(icmphdr)); + + EXPECT_EQ(recv_buf.icmp.type, ICMP_ECHOREPLY); + EXPECT_EQ(recv_buf.icmp.code, 0); + + EXPECT_EQ(received_tos, kArbitraryTOS); +} + +TEST(PingSocket, ReceiveTClass) { + PosixErrorOr result = + Socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); + if (!result.ok()) { + int errno_value = result.error().errno_value(); + ASSERT_EQ(errno_value, EACCES) << strerror(errno_value); + GTEST_SKIP() << "ping socket not supported"; + } + FileDescriptor& ping = result.ValueOrDie(); + + const sockaddr_in6 kAddr = { + .sin6_family = AF_INET6, + .sin6_addr = in6addr_loopback, + }; + ASSERT_THAT(bind(ping.get(), reinterpret_cast(&kAddr), + sizeof(kAddr)), + SyscallSucceeds()); + + constexpr int kArbitraryTClass = 42; + ASSERT_THAT(setsockopt(ping.get(), IPPROTO_IPV6, IPV6_TCLASS, + &kArbitraryTClass, sizeof(kArbitraryTClass)), + SyscallSucceeds()); + + constexpr icmp6_hdr kSendIcmp = { + .icmp6_type = ICMP6_ECHO_REQUEST, + }; + ASSERT_THAT(sendto(ping.get(), &kSendIcmp, sizeof(kSendIcmp), 0, + reinterpret_cast(&kAddr), sizeof(kAddr)), + SyscallSucceedsWithValue(sizeof(kSendIcmp))); + + // Register to receive TCLASS. + constexpr int kOne = 1; + ASSERT_THAT(setsockopt(ping.get(), IPPROTO_IPV6, IPV6_RECVTCLASS, &kOne, + sizeof(kOne)), + SyscallSucceeds()); + + struct { + icmp6_hdr icmpv6; + + // Add an extra byte to confirm we did not read unexpected bytes. + char unused; + } ABSL_ATTRIBUTE_PACKED recv_buf; + size_t recv_buf_len = sizeof(recv_buf); + int received_tclass; + ASSERT_NO_FATAL_FAILURE(RecvTClass(ping.get(), + reinterpret_cast(&recv_buf), + &recv_buf_len, &received_tclass)); + ASSERT_EQ(recv_buf_len, sizeof(kSendIcmp)); + + EXPECT_EQ(recv_buf.icmpv6.icmp6_type, ICMP6_ECHO_REPLY); + EXPECT_EQ(recv_buf.icmpv6.icmp6_code, 0); + + EXPECT_EQ(received_tclass, kArbitraryTClass); +} + struct BindTestCase { TestAddress bind_to; int want = 0; diff --git a/test/syscalls/linux/raw_socket.cc b/test/syscalls/linux/raw_socket.cc index 8b21b1dc9..82c805cb7 100644 --- a/test/syscalls/linux/raw_socket.cc +++ b/test/syscalls/linux/raw_socket.cc @@ -1186,6 +1186,124 @@ TEST(RawSocketTest, ReceiveIPv6PacketInfo) { EXPECT_THAT(CMSG_NXTHDR(&recv_msg, cmsg), IsNull()); } +TEST(RawSocketTest, ReceiveTOS) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveRawIPSocketCapability())); + + FileDescriptor raw = + ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_RAW, IPPROTO_UDP)); + + const sockaddr_in kAddr = { + .sin_family = AF_INET, + .sin_addr = {.s_addr = htonl(INADDR_LOOPBACK)}, + }; + ASSERT_THAT( + bind(raw.get(), reinterpret_cast(&kAddr), sizeof(kAddr)), + SyscallSucceeds()); + + constexpr int kArbitraryTOS = 42; + ASSERT_THAT(setsockopt(raw.get(), IPPROTO_IP, IP_TOS, &kArbitraryTOS, + sizeof(kArbitraryTOS)), + SyscallSucceeds()); + + constexpr char kSendBuf[] = "malformed UDP"; + ASSERT_THAT(sendto(raw.get(), kSendBuf, sizeof(kSendBuf), 0 /* flags */, + reinterpret_cast(&kAddr), sizeof(kAddr)), + SyscallSucceedsWithValue(sizeof(kSendBuf))); + + // Register to receive TOS. + constexpr int kOne = 1; + ASSERT_THAT( + setsockopt(raw.get(), IPPROTO_IP, IP_RECVTOS, &kOne, sizeof(kOne)), + SyscallSucceeds()); + + struct { + iphdr ip; + char data[sizeof(kSendBuf)]; + + // Extra space in the receive buffer should be unused. + char unused_space; + } ABSL_ATTRIBUTE_PACKED recv_buf; + uint8_t recv_tos; + size_t recv_buf_len = sizeof(recv_buf); + ASSERT_NO_FATAL_FAILURE(RecvTOS(raw.get(), reinterpret_cast(&recv_buf), + &recv_buf_len, &recv_tos)); + ASSERT_EQ(recv_buf_len, sizeof(iphdr) + sizeof(kSendBuf)); + + EXPECT_EQ(recv_buf.ip.version, static_cast(IPVERSION)); + // IHL holds the number of header bytes in 4 byte units. + EXPECT_EQ(recv_buf.ip.ihl, sizeof(iphdr) / 4); + EXPECT_EQ(ntohs(recv_buf.ip.tot_len), sizeof(iphdr) + sizeof(kSendBuf)); + EXPECT_EQ(recv_buf.ip.protocol, IPPROTO_UDP); + EXPECT_EQ(ntohl(recv_buf.ip.saddr), INADDR_LOOPBACK); + EXPECT_EQ(ntohl(recv_buf.ip.daddr), INADDR_LOOPBACK); + + EXPECT_EQ(memcmp(kSendBuf, &recv_buf.data, sizeof(kSendBuf)), 0); + + if (const char* val = getenv("TOS_TCLASS_EXPECT_DEFAULT"); + val != nullptr && strcmp(val, "1") == 0) { + // TODO(https://issuetracker.google.com/issues/217448626): As of writing, it + // seems like at least one Linux environment does not allow setting a custom + // TOS. In this case, we expect the default instead of the TOS that was set + // above. + EXPECT_EQ(recv_buf.ip.tos, 0u); + EXPECT_EQ(recv_tos, 0u); + } else { + EXPECT_EQ(recv_buf.ip.tos, static_cast(kArbitraryTOS)); + EXPECT_EQ(recv_tos, kArbitraryTOS); + } +} + +TEST(RawSocketTest, ReceiveTClass) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveRawIPSocketCapability())); + + FileDescriptor raw = + ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET6, SOCK_RAW, IPPROTO_UDP)); + + const sockaddr_in6 kAddr = { + .sin6_family = AF_INET6, + .sin6_addr = in6addr_loopback, + }; + ASSERT_THAT( + bind(raw.get(), reinterpret_cast(&kAddr), sizeof(kAddr)), + SyscallSucceeds()); + + constexpr int kArbitraryTClass = 42; + ASSERT_THAT(setsockopt(raw.get(), IPPROTO_IPV6, IPV6_TCLASS, + &kArbitraryTClass, sizeof(kArbitraryTClass)), + SyscallSucceeds()); + + constexpr char send_buf[] = "malformed UDP"; + ASSERT_THAT(sendto(raw.get(), send_buf, sizeof(send_buf), 0 /* flags */, + reinterpret_cast(&kAddr), sizeof(kAddr)), + SyscallSucceedsWithValue(sizeof(send_buf))); + + // Register to receive TClass. + constexpr int kOne = 1; + ASSERT_THAT( + setsockopt(raw.get(), IPPROTO_IPV6, IPV6_RECVTCLASS, &kOne, sizeof(kOne)), + SyscallSucceeds()); + + char recv_buf[sizeof(send_buf) + 1]; + size_t recv_buf_len = sizeof(recv_buf); + int recv_tclass; + ASSERT_NO_FATAL_FAILURE( + RecvTClass(raw.get(), recv_buf, &recv_buf_len, &recv_tclass)); + ASSERT_EQ(recv_buf_len, sizeof(send_buf)); + + EXPECT_EQ(memcmp(send_buf, recv_buf, sizeof(send_buf)), 0); + + if (const char* val = getenv("TOS_TCLASS_EXPECT_DEFAULT"); + val != nullptr && strcmp(val, "1") == 0) { + // TODO(https://issuetracker.google.com/issues/217448626): As of writing, it + // seems like at least one Linux environment does not allow setting a custom + // TCLASS. In this case, we expect the default instead of the TCLASS that + // was set above. + EXPECT_EQ(recv_tclass, 0); + } else { + EXPECT_EQ(recv_tclass, kArbitraryTClass); + } +} + TEST(RawSocketTest, SetIPv6ChecksumError_MultipleOf2) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveRawIPSocketCapability()));