Implement RTM_NEWROUTE in netstack to create/replace a route.

PiperOrigin-RevId: 650814137
This commit is contained in:
Jing Chen
2024-07-09 18:04:19 -07:00
committed by gVisor bot
parent 16ebae768c
commit 35309c96c0
13 changed files with 403 additions and 1 deletions
+3
View File
@@ -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()
+7
View File
@@ -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() {}
+6
View File
@@ -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() {}
@@ -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
}
@@ -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)
}
}
}
@@ -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:
+116
View File
@@ -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
+19
View File
@@ -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]
+7 -1
View File
@@ -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.
+9
View File
@@ -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,
)
+6
View File
@@ -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"],
+33
View File
@@ -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"
@@ -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<const struct rtmsg*>(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);