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
This commit is contained in:
Ghanan Gowripalan
2022-06-14 10:35:49 -07:00
committed by gVisor bot
parent 82498d087e
commit fa7c3d9c51
4 changed files with 560 additions and 27 deletions
+6
View File
@@ -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
+2
View File
@@ -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",
+450
View File
@@ -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)
}
})
}
})
}
}
+102 -27
View File
@@ -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
}