diff --git a/pkg/sentry/inet/inet.go b/pkg/sentry/inet/inet.go index c1a5d668b..350c8ce3b 100644 --- a/pkg/sentry/inet/inet.go +++ b/pkg/sentry/inet/inet.go @@ -85,6 +85,9 @@ type Stack interface { // RouteTable returns the network stack's route table. RouteTable() []Route + // NewRoute adds the given route to the network stack's route table. + NewRoute(ctx context.Context, msg *nlmsg.Message) *syserr.Error + // Pause pauses the network stack before save. Pause() diff --git a/pkg/sentry/inet/test_stack.go b/pkg/sentry/inet/test_stack.go index b8e88510a..d5d7031f5 100644 --- a/pkg/sentry/inet/test_stack.go +++ b/pkg/sentry/inet/test_stack.go @@ -26,6 +26,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" ) +var _ Stack = (*TestStack)(nil) + // TestStack is a dummy implementation of Stack for tests. type TestStack struct { InterfacesMap map[int32]Interface @@ -157,6 +159,11 @@ func (s *TestStack) RouteTable() []Route { return s.RouteList } +// NewRoute implements Stack. +func (s *TestStack) NewRoute(ctx context.Context, msg *nlmsg.Message) *syserr.Error { + return syserr.ErrNotPermitted +} + // Pause implements Stack. func (s *TestStack) Pause() {} diff --git a/pkg/sentry/socket/hostinet/stack.go b/pkg/sentry/socket/hostinet/stack.go index 8e6a5121d..7b0fea80c 100644 --- a/pkg/sentry/socket/hostinet/stack.go +++ b/pkg/sentry/socket/hostinet/stack.go @@ -382,6 +382,12 @@ func (s *Stack) RouteTable() []inet.Route { return append([]inet.Route(nil), routes...) } +// NewRoute implements inet.Stack.NewRoute. +func (*Stack) NewRoute(context.Context, *nlmsg.Message) *syserr.Error { + // TODO(b/343524351): implements RTM_NEWROUTE for hostinet. + return syserr.ErrNotSupported +} + // Pause implements inet.Stack.Pause. func (*Stack) Pause() {} diff --git a/pkg/sentry/socket/netlink/nlmsg/message.go b/pkg/sentry/socket/netlink/nlmsg/message.go index fa1ec6f5e..60375e3fe 100644 --- a/pkg/sentry/socket/netlink/nlmsg/message.go +++ b/pkg/sentry/socket/netlink/nlmsg/message.go @@ -322,3 +322,14 @@ func (v *BytesView) Uint32() (uint32, bool) { val.UnmarshalBytes(attr) return uint32(val), true } + +// Int32 converts the raw attribute value to int32. +func (v *BytesView) Int32() (int32, bool) { + attr := []byte(*v) + val := primitive.Int32(0) + if len(attr) != val.SizeBytes() { + return 0, false + } + val.UnmarshalBytes(attr) + return int32(val), true +} diff --git a/pkg/sentry/socket/netlink/nlmsg/message_test.go b/pkg/sentry/socket/netlink/nlmsg/message_test.go index cc1cffe00..500326797 100644 --- a/pkg/sentry/socket/netlink/nlmsg/message_test.go +++ b/pkg/sentry/socket/netlink/nlmsg/message_test.go @@ -301,3 +301,75 @@ func TestAttrView(t *testing.T) { } } } + +type bytesViewTest[T any] struct { + desc string + input nlmsg.BytesView + ok bool + value T +} + +func TestBytesView(t *testing.T) { + tests := []any{ + bytesViewTest[string]{ + desc: "Convert BytesView to string", + input: nlmsg.BytesView([]byte("hello world")), + ok: true, + value: "hello world", + }, + bytesViewTest[uint32]{ + desc: "Convert BytesView to uint32", + input: nlmsg.BytesView([]byte{7, 0, 0, 0}), + ok: true, + value: 7, + }, + bytesViewTest[uint32]{ + desc: "Failed to convert BytesView to uint32", + input: nlmsg.BytesView([]byte{7, 0}), + ok: false, + value: 0, + }, + bytesViewTest[int32]{ + desc: "Convert BytesView to int32", + input: nlmsg.BytesView([]byte{8, 0, 0, 0}), + ok: true, + value: 8, + }, + bytesViewTest[int32]{ + desc: "Failed convert BytesView to int32", + input: nlmsg.BytesView([]byte{8}), + ok: false, + value: 0, + }, + } + for _, test := range tests { + switch test.(type) { + case bytesViewTest[string]: + tst := test.(bytesViewTest[string]) + value := tst.input.String() + if value != tst.value { + t.Errorf("%v: BytesView.String() got %v, want %v", tst.desc, value, tst.value) + } + case bytesViewTest[uint32]: + tst := test.(bytesViewTest[uint32]) + value, ok := tst.input.Uint32() + if ok != tst.ok { + t.Errorf("%v: BytesView.Uint32() got ok = %v, want %v", tst.desc, ok, tst.ok) + } + if ok && value != tst.value { + t.Errorf("%v: BytesView.Uint32() got %v, want %v", tst.desc, value, tst.value) + } + case bytesViewTest[int32]: + tst := test.(bytesViewTest[int32]) + value, ok := tst.input.Int32() + if ok != tst.ok { + t.Errorf("%v: BytesView.Int32() got ok = %v, want %v", tst.desc, ok, tst.ok) + } + if ok && value != tst.value { + t.Errorf("%v: BytesView.Int32() got %v, want %v", tst.desc, value, tst.value) + } + default: + t.Errorf("BytesView %T not support", t) + } + } +} diff --git a/pkg/sentry/socket/netlink/route/protocol.go b/pkg/sentry/socket/netlink/route/protocol.go index 597f711e1..1fe9602ee 100644 --- a/pkg/sentry/socket/netlink/route/protocol.go +++ b/pkg/sentry/socket/netlink/route/protocol.go @@ -397,6 +397,20 @@ func parseForDestination(msg *nlmsg.Message) ([]byte, *syserr.Error) { return nil, syserr.ErrInvalidArgument } +// newRoute handles RTM_NEWROUTE requests. +func (p *Protocol) newRoute(ctx context.Context, s *netlink.Socket, msg *nlmsg.Message, ms *nlmsg.MessageSet) *syserr.Error { + stack := s.Stack() + if stack == nil { + // No network routes. + return syserr.ErrProtocolNotSupported + } + + if msg.Header().Flags&linux.NLM_F_REQUEST != linux.NLM_F_REQUEST { + return syserr.ErrProtocolNotSupported + } + return stack.NewRoute(ctx, msg) +} + // dumpRoutes handles RTM_GETROUTE requests. func (p *Protocol) dumpRoutes(ctx context.Context, s *netlink.Socket, msg *nlmsg.Message, ms *nlmsg.MessageSet) *syserr.Error { // RTM_GETROUTE dump requests need not contain anything more than the @@ -617,6 +631,8 @@ func (p *Protocol) ProcessMessage(ctx context.Context, s *netlink.Socket, msg *n case linux.RTM_SETLINK: // RTM_NEWLINK is backward compatible to RTM_SETLINK. return p.setLink(ctx, s, msg, ms) + case linux.RTM_NEWROUTE: + return p.newRoute(ctx, s, msg, ms) case linux.RTM_GETROUTE: return p.dumpRoutes(ctx, s, msg, ms) case linux.RTM_NEWADDR: diff --git a/pkg/sentry/socket/netstack/stack.go b/pkg/sentry/socket/netstack/stack.go index 83cd186db..0071ddd34 100644 --- a/pkg/sentry/socket/netstack/stack.go +++ b/pkg/sentry/socket/netstack/stack.go @@ -742,6 +742,122 @@ func (s *Stack) RouteTable() []inet.Route { return routeTable } +// NewRoute implements inet.Stack.NewRoute. +func (s *Stack) NewRoute(ctx context.Context, msg *nlmsg.Message) *syserr.Error { + var routeMsg linux.RouteMessage + attrs, ok := msg.GetData(&routeMsg) + if !ok { + return syserr.ErrInvalidArgument + } + + route := inet.Route{ + Family: routeMsg.Family, + DstLen: routeMsg.DstLen, + SrcLen: routeMsg.SrcLen, + TOS: routeMsg.TOS, + Table: routeMsg.Table, + Protocol: routeMsg.Protocol, + Scope: routeMsg.Scope, + Type: routeMsg.Type, + Flags: routeMsg.Flags, + } + + for !attrs.Empty() { + ahdr, value, rest, ok := attrs.ParseFirst() + if !ok { + return syserr.ErrInvalidArgument + } + attrs = rest + + switch ahdr.Type { + case linux.RTA_DST: + if len(value) < 1 { + return syserr.ErrInvalidArgument + } + route.DstAddr = value + case linux.RTA_SRC: + if len(value) < 1 { + return syserr.ErrInvalidArgument + } + route.SrcAddr = value + case linux.RTA_OIF: + oif := nlmsg.BytesView(value) + outputInterface, ok := oif.Int32() + if !ok { + return syserr.ErrInvalidArgument + } + if _, exist := s.Interfaces()[outputInterface]; !exist { + return syserr.ErrNoDevice + } + route.OutputInterface = outputInterface + case linux.RTA_GATEWAY: + if len(value) < 1 { + return syserr.ErrInvalidArgument + } + route.GatewayAddr = value + case linux.RTA_PRIORITY: + default: + ctx.Warningf("Unknown attribute: %v", ahdr.Type) + return syserr.ErrNotSupported + } + } + + var dest tcpip.Subnet + // When no destination address is provided, the new route might be the default route. + if route.DstAddr == nil { + if route.GatewayAddr == nil { + return syserr.ErrInvalidArgument + } + switch len(route.GatewayAddr) { + case header.IPv4AddressSize: + subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(tcpip.IPv4Zero), tcpip.MaskFromBytes(tcpip.IPv4Zero)) + if err != nil { + return syserr.ErrInvalidArgument + } + dest = subnet + case header.IPv6AddressSize: + subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(tcpip.IPv6Zero), tcpip.MaskFromBytes(tcpip.IPv6Zero)) + if err != nil { + return syserr.ErrInvalidArgument + } + dest = subnet + default: + return syserr.ErrInvalidArgument + } + } else { + dest = tcpip.AddressWithPrefix{ + Address: tcpip.AddrFromSlice(route.DstAddr), + PrefixLen: int(route.DstLen)}.Subnet() + } + + localRoute := tcpip.Route{ + Destination: dest, + Gateway: tcpip.AddrFromSlice(route.GatewayAddr), + NIC: tcpip.NICID(route.OutputInterface), + } + if len(route.SrcAddr) != 0 { + localRoute.SourceHint = tcpip.AddrFromSlice(route.SrcAddr) + } + found := false + for _, rt := range s.Stack.GetRouteTable() { + if localRoute.Equal(rt) { + found = true + break + } + } + flags := msg.Header().Flags + switch { + case !found && flags&linux.NLM_F_CREATE == linux.NLM_F_CREATE: + s.Stack.AddRoute(localRoute) + case found && flags&linux.NLM_F_REPLACE != linux.NLM_F_REPLACE: + return syserr.ErrExists + } + if flags&linux.NLM_F_REPLACE == linux.NLM_F_REPLACE { + s.Stack.ReplaceRoute(localRoute) + } + return nil +} + // IPTables returns the stack's iptables. func (s *Stack) IPTables() (*stack.IPTables, error) { return s.Stack.IPTables(), nil diff --git a/pkg/tcpip/stack/stack.go b/pkg/tcpip/stack/stack.go index 2a9c16245..7dc7cd357 100644 --- a/pkg/tcpip/stack/stack.go +++ b/pkg/tcpip/stack/stack.go @@ -784,6 +784,11 @@ func (s *Stack) RemoveRoutes(match func(tcpip.Route) bool) { s.routeMu.Lock() defer s.routeMu.Unlock() + s.removeRoutesLocked(match) +} + +// +checklocks:s.routeMu +func (s *Stack) removeRoutesLocked(match func(tcpip.Route) bool) { for route := s.routeTable.Front(); route != nil; { next := route.Next() if match(*route) { @@ -793,6 +798,20 @@ func (s *Stack) RemoveRoutes(match func(tcpip.Route) bool) { } } +// ReplaceRoute replaces the route in the routing table which matchse +// the lookup key for the routing table. If there is no match, the given +// route will still be added to the routing table. +// The lookup key consists of destination, ToS, scope and output interface. +func (s *Stack) ReplaceRoute(route tcpip.Route) { + s.routeMu.Lock() + defer s.routeMu.Unlock() + + s.removeRoutesLocked(func(rt tcpip.Route) bool { + return rt.Equal(route) + }) + s.addRouteLocked(&route) +} + // NewEndpoint creates a new transport layer endpoint of the given protocol. func (s *Stack) NewEndpoint(transport tcpip.TransportProtocolNumber, network tcpip.NetworkProtocolNumber, waiterQueue *waiter.Queue) (tcpip.Endpoint, tcpip.Error) { t, ok := s.transportProtocols[transport] diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index 29c421e86..b89481733 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -60,6 +60,12 @@ const ( LinkAddressSize = 6 ) +// Known IP address. +var ( + IPv4Zero = []byte{0, 0, 0, 0} + IPv6Zero = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +) + // Errors related to Subnet var ( errSubnetLengthMismatch = errors.New("subnet length of address and mask differ") @@ -1553,7 +1559,7 @@ func (r Route) String() string { // Equal returns true if the given Route is equal to this Route. func (r Route) Equal(to Route) bool { // NOTE: This relies on the fact that r.Destination == to.Destination - return r.Destination.Equal(to.Destination) && r.Gateway == to.Gateway && r.NIC == to.NIC + return r.Destination.Equal(to.Destination) && r.NIC == to.NIC } // TransportProtocolNumber is the number of a transport protocol. diff --git a/test/rtnetlink/BUILD b/test/rtnetlink/BUILD index 0bef62bf2..9e86988ed 100644 --- a/test/rtnetlink/BUILD +++ b/test/rtnetlink/BUILD @@ -31,3 +31,12 @@ syscall_test( test = "//test/rtnetlink/linux:setlink_test", use_tmpfs = True, ) + +syscall_test( + size = "small", + container = True, + overlay = True, + save = False, + test = "//test/rtnetlink/linux:route_test", + use_tmpfs = True, +) diff --git a/test/rtnetlink/linux/BUILD b/test/rtnetlink/linux/BUILD index 0ac32febe..02f58db4b 100644 --- a/test/rtnetlink/linux/BUILD +++ b/test/rtnetlink/linux/BUILD @@ -25,6 +25,12 @@ sh_binary( deps = [":rtnetlink_test"], ) +sh_binary( + name = "route_test", + srcs = ["route_test.sh"], + deps = [":rtnetlink_test"], +) + sh_binary( name = "bridge_test", srcs = ["bridge_test.sh"], diff --git a/test/rtnetlink/linux/route_test.sh b/test/rtnetlink/linux/route_test.sh new file mode 100755 index 000000000..9cd22391a --- /dev/null +++ b/test/rtnetlink/linux/route_test.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Copyright 2024 The gVisor Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -xeo pipefail +source "$(dirname "$0")/rtnetlink_test.sh" + +# Create a new default route and a new route with a address. +ip netns add test +ip link add name veth1 type veth peer name eth0 netns test +ip netns exec test ip link set up dev lo +ip netns exec test ip link set up dev eth0 +ip netns exec test ip addr add 192.168.11.2/24 dev eth0 +ip netns exec test ip r add default via 192.168.11.1 dev eth0 +ip netns exec test ip r list | grep "default via 192.168.11.1 dev eth0" +ip netns exec test ip r add 192.168.146.48/28 dev eth0 +ip netns exec test ip r list | grep "192.168.146.48/28 dev eth0" + +# Replace the routes. +ip netns exec test ip r replace default via 192.168.11.2 dev eth0 +ip netns exec test ip r list | grep "default via 192.168.11.2 dev eth0" diff --git a/test/syscalls/linux/socket_netlink_route.cc b/test/syscalls/linux/socket_netlink_route.cc index 12139577d..0b4eaeb1f 100644 --- a/test/syscalls/linux/socket_netlink_route.cc +++ b/test/syscalls/linux/socket_netlink_route.cc @@ -1097,6 +1097,104 @@ INSTANTIATE_TEST_SUITE_P(NetlinkRouteIpv4AndIpv6Tests, NetlinkRouteIpInvariantTest, ::testing::Values(AF_INET, AF_INET6)); +TEST_P(NetlinkRouteIpInvariantTest, NewRoute) { + // CAP_NET_ADMIN is required to modify the routing table. + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_ADMIN))); + SKIP_IF(!IsRunningOnGvisor()); + SKIP_IF(IsRunningWithHostinet()); + // Routes are not savable. + DisableSave ds; + + const std::string dst_v4_address = "192.0.2.0"; + const std::string dst_v6_address = "2001:db8::"; + + // Based on the test parameter, build an IPv4 or IPv6 destination subnet. + int family = GetParam(); + void* dst = nullptr; + int dst_len; + int prefixlen; + switch (family) { + case AF_INET: + struct in_addr dst_v4; + ASSERT_EQ(inet_pton(family, dst_v4_address.c_str(), &dst_v4), 1); + prefixlen = 24; + dst = &dst_v4; + dst_len = sizeof(dst_v4); + break; + case AF_INET6: + struct in6_addr dst_v6; + ASSERT_EQ(inet_pton(family, dst_v6_address.c_str(), &dst_v6), 1); + prefixlen = 64; + dst = &dst_v6; + dst_len = sizeof(dst_v6); + break; + default: + FAIL() << "address family must be AF_INET or AF_INET6"; + } + + Link loopback_link = ASSERT_NO_ERRNO_AND_VALUE(LoopbackLink()); + + ASSERT_NO_ERRNO( + AddUnicastRoute(loopback_link.index, family, prefixlen, dst, dst_len)); + + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NetlinkBoundSocket(NETLINK_ROUTE)); + + struct request { + struct nlmsghdr hdr; + struct rtmsg rtm; + }; + + struct request req = {}; + req.hdr.nlmsg_len = sizeof(req); + req.hdr.nlmsg_type = RTM_GETROUTE; + req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + req.hdr.nlmsg_seq = kSeq; + req.rtm.rtm_family = AF_UNSPEC; + + bool routeDstFound = false; + ASSERT_NO_ERRNO(NetlinkRequestResponse( + fd, &req, sizeof(req), + [&](const struct nlmsghdr* hdr) { + // Validate the reponse to RTM_GETROUTE + NLM_F_DUMP. + EXPECT_THAT(hdr->nlmsg_type, AnyOf(Eq(RTM_NEWROUTE), Eq(NLMSG_DONE))); + // The test should not proceed if it's not a RTM_NEWROUTE message. + if (hdr->nlmsg_type != RTM_NEWROUTE) { + return; + } + const struct rtmsg* msg = + reinterpret_cast(NLMSG_DATA(hdr)); + int len = RTM_PAYLOAD(hdr); + for (struct rtattr* attr = RTM_RTA(msg); RTA_OK(attr, len); + attr = RTA_NEXT(attr, len)) { + if (attr->rta_type == RTA_DST) { + char v4_address[INET_ADDRSTRLEN] = {}; + char v6_address[INET6_ADDRSTRLEN] = {}; + switch (family) { + case AF_INET: + inet_ntop(AF_INET, RTA_DATA(attr), v4_address, + sizeof(v4_address)); + if (strcmp(v4_address, dst_v4_address.c_str())) { + routeDstFound = true; + return; + } + break; + case AF_INET6: + inet_ntop(AF_INET6, RTA_DATA(attr), v6_address, + sizeof(v6_address)); + if (strcmp(v6_address, dst_v6_address.c_str())) { + routeDstFound = true; + return; + } + break; + } + } + } + }, + false)); + EXPECT_TRUE(routeDstFound); +} + TEST_P(NetlinkRouteIpInvariantTest, AddAndRemoveRoute) { // Gvisor does not support `RTM_NEWROUTE` or `RTM_DELROUTE`. SKIP_IF(IsRunningOnGvisor() && GvisorPlatform() != Platform::kStarnix);