mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Implement RTM_NEWROUTE in netstack to create/replace a route.
PiperOrigin-RevId: 650814137
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Executable
+33
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user