From fa7c3d9c5142a3b9e05bf64a2e60624e5e6ff5cf Mon Sep 17 00:00:00 2001 From: Ghanan Gowripalan Date: Tue, 14 Jun 2022 10:32:49 -0700 Subject: [PATCH] Support specifying local IPv6 adddress/interface ...for outgoing packets. This is the first step in supporting IPV6_PKTINFO in the send path. Note that the implementation enforces strong host semantics as gVisor currently follows the strong host model. This change does not support IPV6_PKTINFO in the send path from Linux applications (via CMSGS) yet as there is no need. The bindings to enable the IPV6_PKTINFO CMSG in the sendpath can be done in a followup. Bug: https://fxbug.dev/102222 PiperOrigin-RevId: 454898862 --- pkg/tcpip/tcpip.go | 6 + pkg/tcpip/transport/BUILD | 2 + pkg/tcpip/transport/datagram_test.go | 450 ++++++++++++++++++ .../transport/internal/network/endpoint.go | 129 +++-- 4 files changed, 560 insertions(+), 27 deletions(-) diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index 81013e522..dd3236add 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -436,6 +436,12 @@ type SendableControlMessages struct { // HopLimit is the IPv6 Hop Limit of the associated packet. HopLimit uint8 + + // HasIPv6PacketInfo indicates whether IPv6PacketInfo is set. + HasIPv6PacketInfo bool + + // IPv6PacketInfo holds interface and address data on an incoming packet. + IPv6PacketInfo IPv6PacketInfo } // ReceivableControlMessages contains socket control messages that can be diff --git a/pkg/tcpip/transport/BUILD b/pkg/tcpip/transport/BUILD index 889317964..33e6557b5 100644 --- a/pkg/tcpip/transport/BUILD +++ b/pkg/tcpip/transport/BUILD @@ -19,7 +19,9 @@ go_test( deps = [ ":transport", "//pkg/tcpip", + "//pkg/tcpip/checker", "//pkg/tcpip/header", + "//pkg/tcpip/link/channel", "//pkg/tcpip/link/loopback", "//pkg/tcpip/network/ipv4", "//pkg/tcpip/network/ipv6", diff --git a/pkg/tcpip/transport/datagram_test.go b/pkg/tcpip/transport/datagram_test.go index b80912102..9618b7447 100644 --- a/pkg/tcpip/transport/datagram_test.go +++ b/pkg/tcpip/transport/datagram_test.go @@ -23,7 +23,9 @@ import ( "github.com/google/go-cmp/cmp" "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/checker" "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" "gvisor.dev/gvisor/pkg/tcpip/link/loopback" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" @@ -664,3 +666,451 @@ func TestMulticastLoop(t *testing.T) { }) } } + +func TestIPv6PacketInfo(t *testing.T) { + const ( + nicID1 = 1 + nicID2 = 2 + port = 12345 + ) + + type localNICAddr struct { + nicID tcpip.NICID + addr tcpip.AddressWithPrefix + } + + type testCase struct { + name string + boundNICID tcpip.NICID + bindAddr tcpip.FullAddress + connectAddr tcpip.FullAddress + toAddr tcpip.FullAddress + pktInfo tcpip.IPv6PacketInfo + + expectedErr tcpip.Error + expectedLocalAddr tcpip.Address + expectedRemoteAddr tcpip.Address + } + + ipv6Addr1 := testutil.MustParse6("1::1") + ipv6Addr2 := testutil.MustParse6("1::2") + ipv6RemoteAddr1 := testutil.MustParse6("2::1") + ipv6RemoteAddr2 := testutil.MustParse6("2::2") + + localAddrs := []localNICAddr{ + { + nicID: nicID1, + addr: ipv6Addr1.WithPrefix(), + }, + { + nicID: nicID2, + addr: ipv6Addr2.WithPrefix(), + }, + } + + tests := []testCase{ + // Bind and SendTo + { + name: "Bind wildcard & SendTo with packet info NIC", + bindAddr: tcpip.FullAddress{ + Addr: "", + Port: port, + }, + toAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + { + name: "BindToDevice & Bind wildcard & SendTo with packet info NIC not matching", + boundNICID: nicID2, + bindAddr: tcpip.FullAddress{ + Addr: "", + Port: port, + }, + toAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + { + name: "Bind wildcard and NIC & SendTo with packet info NIC matching", + bindAddr: tcpip.FullAddress{ + NIC: nicID1, + Addr: "", + Port: port, + }, + toAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + { + name: "Bind wildcard and NIC & SendTo with packet info NIC not matching", + bindAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: "", + Port: port, + }, + toAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + { + name: "Bind specified & SendTo with packet info NIC not matching bound addr", + bindAddr: tcpip.FullAddress{ + Addr: ipv6Addr2, + Port: port, + }, + toAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrBadLocalAddress{}, + }, + { + name: "Bind specified and NIC & SendTo with packet info NIC not matching but local addr specified", + bindAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: ipv6Addr2, + Port: port, + }, + toAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + Addr: ipv6Addr1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + + // Bind and Connect + { + name: "Bind wildcard & Connect then Send with packet info NIC", + bindAddr: tcpip.FullAddress{ + Addr: "", + Port: port, + }, + connectAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + { + name: "Bind wildcard and NIC & Connect then Send with packet info NIC matching", + bindAddr: tcpip.FullAddress{ + NIC: nicID1, + Addr: "", + Port: port, + }, + connectAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + { + name: "Bind wildcard and NIC & Connect then Send with packet info NIC not matching", + bindAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: "", + Port: port, + }, + connectAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + { + name: "Bind wildcard & Connect with NIC then Send with packet info NIC matching", + bindAddr: tcpip.FullAddress{ + Addr: "", + Port: port, + }, + connectAddr: tcpip.FullAddress{ + NIC: nicID1, + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + { + name: "Bind wildcard & Connect with NIC then Send with packet info NIC not matching", + bindAddr: tcpip.FullAddress{ + Addr: "", + Port: port, + }, + connectAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + { + name: "Bind specified & Connect then Send with packet info NIC not matching but local addr specified", + bindAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: ipv6Addr2, + Port: port, + }, + connectAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + Addr: ipv6Addr1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + + // Connect + { + name: "Connect with NIC then Send with packet info NIC matching", + connectAddr: tcpip.FullAddress{ + NIC: nicID1, + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + { + // Because NIC2 is preferred over NIC1 for route selection, we pick a + // local address on NIC2. Since the pktinfo does not specify a local + // address but requests the packet to be sent out through NIC1 we fail + // with err bad local address because NIC2's local address is not + // available on NIC1. + name: "Connect then Send with packet info NIC not matching", + connectAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrBadLocalAddress{}, + }, + { + name: "BindToDevice & Connect then Send with packet info NIC matching", + boundNICID: nicID2, + connectAddr: tcpip.FullAddress{ + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + { + name: "Connect then Send with packet info NIC not matching", + connectAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + NIC: nicID1, + }, + expectedErr: &tcpip.ErrNoRoute{}, + }, + + // Connect and SendTo + { + name: "Connect with NIC then SendTo with different NIC with packet info NIC matching SendTo NIC", + connectAddr: tcpip.FullAddress{ + NIC: nicID2, + Addr: ipv6RemoteAddr2, + Port: port, + }, + toAddr: tcpip.FullAddress{ + NIC: nicID1, + Addr: ipv6RemoteAddr1, + Port: port, + }, + pktInfo: tcpip.IPv6PacketInfo{ + Addr: ipv6Addr1, + NIC: nicID1, + }, + expectedLocalAddr: ipv6Addr1, + expectedRemoteAddr: ipv6RemoteAddr1, + }, + } + + for _, transProto := range []struct { + name string + createEndpoint func(*stack.Stack, *waiter.Queue) (tcpip.Endpoint, error) + }{ + { + name: "UDP", + createEndpoint: func(s *stack.Stack, wq *waiter.Queue) (tcpip.Endpoint, error) { + ep, err := s.NewEndpoint(udp.ProtocolNumber, header.IPv6ProtocolNumber, wq) + if err != nil { + return nil, fmt.Errorf("s.NewEndpoint(%d, %d, _) failed: %s", udp.ProtocolNumber, header.IPv6ProtocolNumber, err) + } + return ep, nil + }, + }, + { + name: "RAW", + createEndpoint: func(s *stack.Stack, wq *waiter.Queue) (tcpip.Endpoint, error) { + ep, err := s.NewRawEndpoint(udp.ProtocolNumber, header.IPv6ProtocolNumber, wq, true /* associated */) + if err != nil { + return nil, fmt.Errorf("s.NewRawEndpoint(%d, %d, _, true) failed: %s", udp.ProtocolNumber, header.IPv6ProtocolNumber, err) + } + return ep, nil + }, + }, + } { + t.Run(transProto.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocol, ipv4.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol}, + RawFactory: &raw.EndpointFactory{}, + }) + e1 := channel.New(1, header.IPv6MinimumMTU, "") + if err := s.CreateNIC(nicID1, e1); err != nil { + t.Fatalf("s.CreateNIC(%d, _) failed: %s", nicID1, err) + } + e2 := channel.New(1, header.IPv6MinimumMTU, "") + if err := s.CreateNIC(nicID2, e2); err != nil { + t.Fatalf("s.CreateNIC(%d, _) failed: %s", nicID2, err) + } + + for _, localAddr := range localAddrs { + addr := tcpip.ProtocolAddress{ + Protocol: header.IPv6ProtocolNumber, + AddressWithPrefix: localAddr.addr, + } + if err := s.AddProtocolAddress(localAddr.nicID, addr, stack.AddressProperties{}); err != nil { + t.Fatalf("AddProtocolAddress(%d, %#v, {}): %s", localAddr.nicID, addr, err) + } + } + s.SetRouteTable([]tcpip.Route{ + // NIC2 before NIC1 to let NIC2 have preference. + { + Destination: header.IPv6EmptySubnet, + NIC: nicID2, + }, + { + Destination: header.IPv6EmptySubnet, + NIC: nicID1, + }, + }) + + var wq waiter.Queue + ep, err := transProto.createEndpoint(s, &wq) + if err != nil { + t.Fatalf("transProto.createEndpoint(_) failed: %s", err) + } + defer ep.Close() + + if err := ep.SocketOptions().SetBindToDevice(int32(test.boundNICID)); err != nil { + t.Fatalf("ep.SocketOptions().SetBindToDevice(int32(%d)): %s", test.boundNICID, err) + } + + if test.bindAddr != (tcpip.FullAddress{}) { + if err := ep.Bind(test.bindAddr); err != nil { + t.Fatalf("ep.Bind(%#v): %s", test.bindAddr, err) + } + } + + if test.connectAddr != (tcpip.FullAddress{}) { + if err := ep.Connect(test.connectAddr); err != nil { + t.Fatalf("ep.Connect(%#v): %s", test.connectAddr, err) + } + } + + buf := [...]byte{1, 2, 3, 4} + var r bytes.Reader + r.Reset(buf[:]) + opts := tcpip.WriteOptions{ + ControlMessages: tcpip.SendableControlMessages{ + HasIPv6PacketInfo: true, + IPv6PacketInfo: test.pktInfo, + }, + } + if test.toAddr != (tcpip.FullAddress{}) { + opts.To = &test.toAddr + } + + if n, err := ep.Write(&r, opts); !cmp.Equal(test.expectedErr, err) { + t.Fatalf("got Write(_, %#v) = %s, want = %s", opts, err, test.expectedErr) + } else if test.expectedErr != nil { + return + } else if want := int64(len(buf)); n != want { + t.Fatalf("got Write(_, %#v) = %d, want = %d", opts, n, want) + } + + { + p := e1.Read() + if p == nil { + t.Fatal("packet didn't arrive at ep1") + } + + checker.IPv6(t, stack.PayloadSince(p.NetworkHeader()), + checker.SrcAddr(test.expectedLocalAddr), + checker.DstAddr(test.expectedRemoteAddr), + ) + } + + if p := e2.Read(); p != nil { + t.Errorf("unexpected packet from ep2 = %#v", p) + } + }) + } + }) + } +} diff --git a/pkg/tcpip/transport/internal/network/endpoint.go b/pkg/tcpip/transport/internal/network/endpoint.go index b70080b1c..724114ba0 100644 --- a/pkg/tcpip/transport/internal/network/endpoint.go +++ b/pkg/tcpip/transport/internal/network/endpoint.go @@ -392,40 +392,113 @@ func (e *Endpoint) AcquireContextForWrite(opts tcpip.WriteOptions) (WriteContext return WriteContext{}, &tcpip.ErrClosedForSend{} } + ipv6PktInfoValid := e.effectiveNetProto == header.IPv6ProtocolNumber && opts.ControlMessages.HasIPv6PacketInfo + route := e.connectedRoute - if opts.To == nil { + to := opts.To + info := e.Info() + switch { + case to == nil: // If the user doesn't specify a destination, they should have // connected to another address. if e.State() != transport.DatagramEndpointStateConnected { return WriteContext{}, &tcpip.ErrDestinationRequired{} } - route.Acquire() - } else { + if !ipv6PktInfoValid { + route.Acquire() + break + } + + // We are connected and the caller did not specify the destination but + // we have an IPv6 packet info structure which may change our local + // interface/address used to send the packet so we need to construct + // a new route instead of using the connected route. + // + // Contruct a destination matching the remote the endpoint is connected + // to. + to = &tcpip.FullAddress{ + // RegisterNICID is set when the endpoint is connected. It is usually + // only set for link-local addresses or multicast addresses if the + // multicast interface was specified (see e.multicastNICID, + // e.connectRouteRLocked and e.ConnectAndThen). + NIC: info.RegisterNICID, + Addr: info.ID.RemoteAddress, + } + fallthrough + default: // Reject destination address if it goes through a different // NIC than the endpoint was bound to. - nicID := opts.To.NIC + nicID := to.NIC if nicID == 0 { nicID = tcpip.NICID(e.ops.GetBindToDevice()) } - info := e.Info() - if info.BindNICID != 0 { - if nicID != 0 && nicID != info.BindNICID { - return WriteContext{}, &tcpip.ErrNoRoute{} + + var localAddr tcpip.Address + if ipv6PktInfoValid { + // Uphold strong-host semantics since (as of writing) the stack follows + // the strong host model. + + pktInfoNICID := opts.ControlMessages.IPv6PacketInfo.NIC + pktInfoAddr := opts.ControlMessages.IPv6PacketInfo.Addr + + if pktInfoNICID != 0 { + // If we are bound to an interface or specified the destination + // interface (usually when using link-local addresses), make sure the + // interface matches the specified local interface. + if nicID != 0 && nicID != pktInfoNICID { + return WriteContext{}, &tcpip.ErrNoRoute{} + } + + // If a local address is not specified, then we need to make sure the + // bound address belongs to the specified local interface. + if len(pktInfoAddr) == 0 { + // If the bound interface is different from the specified local + // interface, the bound address obviously does not belong to the + // specified local interface. + // + // The bound interface is usually only set for link-local addresses. + if info.BindNICID != 0 && info.BindNICID != pktInfoNICID { + return WriteContext{}, &tcpip.ErrNoRoute{} + } + if len(info.ID.LocalAddress) != 0 && e.stack.CheckLocalAddress(pktInfoNICID, header.IPv6ProtocolNumber, info.ID.LocalAddress) == 0 { + return WriteContext{}, &tcpip.ErrBadLocalAddress{} + } + } + + nicID = pktInfoNICID } - nicID = info.BindNICID - } - if nicID == 0 { - nicID = info.RegisterNICID + if len(pktInfoAddr) != 0 { + // The local address must belong to the stack. If an outgoing interface + // is specified as a result of binding the endpoint to a device, or + // specifying the outgoing interface in the destination address/pkt info + // structure, the address must belong to that interface. + if e.stack.CheckLocalAddress(nicID, header.IPv6ProtocolNumber, pktInfoAddr) == 0 { + return WriteContext{}, &tcpip.ErrBadLocalAddress{} + } + + localAddr = pktInfoAddr + } + } else { + if info.BindNICID != 0 { + if nicID != 0 && nicID != info.BindNICID { + return WriteContext{}, &tcpip.ErrNoRoute{} + } + + nicID = info.BindNICID + } + if nicID == 0 { + nicID = info.RegisterNICID + } } - dst, netProto, err := e.checkV4Mapped(*opts.To) + dst, netProto, err := e.checkV4Mapped(*to) if err != nil { return WriteContext{}, err } - route, _, err = e.connectRouteRLocked(nicID, dst, netProto) + route, _, err = e.connectRouteRLocked(nicID, localAddr, dst, netProto) if err != nil { return WriteContext{}, err } @@ -496,19 +569,21 @@ func (e *Endpoint) Disconnect() { // specified address is a multicast address. // // +checklocksread:e.mu -func (e *Endpoint) connectRouteRLocked(nicID tcpip.NICID, addr tcpip.FullAddress, netProto tcpip.NetworkProtocolNumber) (*stack.Route, tcpip.NICID, tcpip.Error) { - localAddr := e.Info().ID.LocalAddress - if e.isBroadcastOrMulticast(nicID, netProto, localAddr) { - // A packet can only originate from a unicast address (i.e., an interface). - localAddr = "" - } - - if header.IsV4MulticastAddress(addr.Addr) || header.IsV6MulticastAddress(addr.Addr) { - if nicID == 0 { - nicID = e.multicastNICID +func (e *Endpoint) connectRouteRLocked(nicID tcpip.NICID, localAddr tcpip.Address, addr tcpip.FullAddress, netProto tcpip.NetworkProtocolNumber) (*stack.Route, tcpip.NICID, tcpip.Error) { + if len(localAddr) == 0 { + localAddr = e.Info().ID.LocalAddress + if e.isBroadcastOrMulticast(nicID, netProto, localAddr) { + // A packet can only originate from a unicast address (i.e., an interface). + localAddr = "" } - if localAddr == "" && nicID == 0 { - localAddr = e.multicastAddr + + if header.IsV4MulticastAddress(addr.Addr) || header.IsV6MulticastAddress(addr.Addr) { + if nicID == 0 { + nicID = e.multicastNICID + } + if localAddr == "" && nicID == 0 { + localAddr = e.multicastAddr + } } } @@ -563,7 +638,7 @@ func (e *Endpoint) ConnectAndThen(addr tcpip.FullAddress, f func(netProto tcpip. return err } - r, nicID, err := e.connectRouteRLocked(nicID, addr, netProto) + r, nicID, err := e.connectRouteRLocked(nicID, "", addr, netProto) if err != nil { return err }