From 7232b29c86269108f73d1b915d440af4fa1bb427 Mon Sep 17 00:00:00 2001 From: Alexander Cueva Date: Fri, 6 Jun 2025 13:27:05 -0700 Subject: [PATCH] Stub for netlink netfilter sockets. This creates the basic file and structure needed to process ONE netlink message, though implementation details are omitted for now. Future changes will work with filling out the processing of different netlink_netfilter message types (i.e NFT_MSG_NEWTABLE), eventually processing batch messages (NFNL_MSG_BATCH_BEGIN and NFNL_MSG_BATCH_END). PiperOrigin-RevId: 768196902 --- pkg/abi/linux/BUILD | 1 + pkg/abi/linux/netlink_netfilter.go | 95 +++++++++++++++++++ pkg/abi/linux/nf_tables.go | 43 +++++++++ pkg/sentry/socket/netlink/netfilter/BUILD | 20 ++++ .../socket/netlink/netfilter/protocol.go | 76 +++++++++++++++ runsc/boot/BUILD | 1 + runsc/boot/loader.go | 1 + test/syscalls/BUILD | 5 + test/syscalls/linux/BUILD | 17 ++++ .../linux/socket_netlink_netfilter.cc | 95 +++++++++++++++++++ 10 files changed, 354 insertions(+) create mode 100644 pkg/abi/linux/netlink_netfilter.go create mode 100644 pkg/sentry/socket/netlink/netfilter/BUILD create mode 100644 pkg/sentry/socket/netlink/netfilter/protocol.go create mode 100644 test/syscalls/linux/socket_netlink_netfilter.cc diff --git a/pkg/abi/linux/BUILD b/pkg/abi/linux/BUILD index b34f6fe9e5..755432453d 100644 --- a/pkg/abi/linux/BUILD +++ b/pkg/abi/linux/BUILD @@ -56,6 +56,7 @@ go_library( "netfilter_ipv4.go", "netfilter_ipv6.go", "netlink.go", + "netlink_netfilter.go", "netlink_route.go", "nf_tables.go", "poll.go", diff --git a/pkg/abi/linux/netlink_netfilter.go b/pkg/abi/linux/netlink_netfilter.go new file mode 100644 index 0000000000..3fb95baf93 --- /dev/null +++ b/pkg/abi/linux/netlink_netfilter.go @@ -0,0 +1,95 @@ +// Copyright 2025 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. + +package linux + +// Group describes Netlink Netfilter groups, from uapi/linux/netfilter/nfnetlink.h. +// Users bind to specific groups to receive processing logs from those groups. +type Group uint16 + +// Netlink Netfilter groups. +const ( + NFNLGPR_NONE Group = iota + NFNLGRP_CONNTRACK_NEW + NFNLGRP_CONNTRACK_UPDATE + NFNLGRP_CONNTRACK_DESTROY + NFNLGRP_CONNTRACK_EXP_NEW + NFNLGRP_CONNTRACK_EXP_UPDATE + NFNLGRP_CONNTRACK_EXP_DESTROY + NFNLGRP_NFTABLES + NFNLGRP_ACCT_QUOTA + NFNLGRP_NFTRACE + __NFNLGRP_MAX + NFNLGRP_MAX = __NFNLGRP_MAX - 1 +) + +// NetFilterGenMsg describes the netlink netfilter genmsg message, from uapi/linux/netfilter/nfnetlink.h. +type NetFilterGenMsg struct { + Family uint8 + Version uint8 + ResourceID uint16 +} + +// SizeOfNetfilterGenMsg is the size of the netlink netfilter genmsg message. +const SizeOfNetfilterGenMsg = 4 + +// NFNETLINK_V0 is the default version of the netlink netfilter. +const NFNETLINK_V0 = 0 + +// SubsysID describes Netlink Netfilter subsystem IDs, from uapi/linux/netfilter/nfnetlink.h. +type SubsysID uint16 + +// Netlink Netfilter subsystem IDs. +const ( + NFNL_SUBSYS_NONE SubsysID = iota + NFNL_SUBSYS_CTNETLINK + NFNL_SUBSYS_CTNETLINK_EXP + NFNL_SUBSYS_QUEUE + NFNL_SUBSYS_ULOG + NFNL_SUBSYS_OSF + NFNL_SUBSYS_IPSET + NFNL_SUBSYS_ACCT + NFNL_SUBSYS_CTNETLINK_TIMEOUT + NFNL_SUBSYS_CTHELPER + NFNL_SUBSYS_NFTABLES + NFNL_SUBSYS_NFT_COMPAT + NFNL_SUBSYS_HOOK +) + +// NetFilterSubsysID returns the Netfilter Subsystem ID from the netlink message header. +func (hdr *NetlinkMessageHeader) NetFilterSubsysID() SubsysID { + return SubsysID((hdr.Type & 0xff00) >> 8) +} + +// NetFilterMsgType returns the Netfilter Message Type from the netlink message header. +func (hdr *NetlinkMessageHeader) NetFilterMsgType() NfTableMsgType { + return NfTableMsgType(hdr.Type & 0x00ff) +} + +// Reserved control Netlink Netfilter messages, from uapi/linux/netfilter/nfnetlink.h. +const ( + NFNL_MSG_BATCH_BEGIN = NLMSG_MIN_TYPE + NFNL_MSG_BATCH_END = NLMSG_MIN_TYPE + 1 +) + +// NetlinkBatchAttr describes Netlink Netfilter batch attributes, from uapi/linux/netfilter/nfnetlink.h. +type NetlinkBatchAttr uint16 + +// Netlink Netfilter batch attributes. +const ( + NFNL_BATCH_UNSPEC NetlinkBatchAttr = iota + NFNL_BATCH_GENID + __NFNL_BATCH_MAX + NFNL_BATCH_MAX = __NFNL_BATCH_MAX - 1 +) diff --git a/pkg/abi/linux/nf_tables.go b/pkg/abi/linux/nf_tables.go index a60d4a0d64..17c14bdc77 100644 --- a/pkg/abi/linux/nf_tables.go +++ b/pkg/abi/linux/nf_tables.go @@ -81,6 +81,49 @@ const ( NFT_RETURN int32 = -5 ) +// NfTableMsgType values map to operations within the nftables api. +// These correspond to values in include/uapi/linux/netfilter/nf_tables.h. +type NfTableMsgType uint16 + +// Netlink Netfilter table message types. +const ( + NFT_MSG_NEWTABLE NfTableMsgType = iota + NFT_MSG_GETTABLE + NFT_MSG_DELTABLE + NFT_MSG_NEWCHAIN + NFT_MSG_GETCHAIN + NFT_MSG_DELCHAIN + NFT_MSG_NEWRULE + NFT_MSG_GETRULE + NFT_MSG_DELRULE + NFT_MSG_NEWSET + NFT_MSG_GETSET + NFT_MSG_DELSET + NFT_MSG_NEWSETELEM + NFT_MSG_GETSETELEM + NFT_MSG_DELSETELEM + NFT_MSG_NEWGEN + NFT_MSG_GETGEN + NFT_MSG_TRACE + NFT_MSG_NEWOBJ + NFT_MSG_GETOBJ + NFT_MSG_DELOBJ + NFT_MSG_GETOBJ_RESET + NFT_MSG_NEWFLOWTABLE + NFT_MSG_GETFLOWTABLE + NFT_MSG_DELFLOWTABLE + NFT_MSG_GETRULE_RESET + NFT_MSG_DESTROYTABLE + NFT_MSG_DESTROYCHAIN + NFT_MSG_DESTROYRULE + NFT_MSG_DESTROYSET + NFT_MSG_DESTROYSETELEM + NFT_MSG_DESTROYOBJ + NFT_MSG_DESTROYFLOWTABLE + NFT_MSG_GETSETELEM_RESET + NFT_MSG_MAX +) + // Nf table relational operators. // Used by the nft comparison operation to compare values in registers. // These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. diff --git a/pkg/sentry/socket/netlink/netfilter/BUILD b/pkg/sentry/socket/netlink/netfilter/BUILD new file mode 100644 index 0000000000..2be43f5cc3 --- /dev/null +++ b/pkg/sentry/socket/netlink/netfilter/BUILD @@ -0,0 +1,20 @@ +load("//tools:defs.bzl", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "netfilter", + srcs = ["protocol.go"], + visibility = ["//pkg/sentry:internal"], + deps = [ + "//pkg/abi/linux", + "//pkg/context", + "//pkg/sentry/kernel", + "//pkg/sentry/socket/netlink", + "//pkg/sentry/socket/netlink/nlmsg", + "//pkg/syserr", + ], +) diff --git a/pkg/sentry/socket/netlink/netfilter/protocol.go b/pkg/sentry/socket/netlink/netfilter/protocol.go new file mode 100644 index 0000000000..53c40b9ed8 --- /dev/null +++ b/pkg/sentry/socket/netlink/netfilter/protocol.go @@ -0,0 +1,76 @@ +// Copyright 2025 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. + +// Package netfilter provides a NETLINK_NETFILTER socket protocol. +package netfilter + +import ( + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/socket/netlink" + "gvisor.dev/gvisor/pkg/sentry/socket/netlink/nlmsg" + "gvisor.dev/gvisor/pkg/syserr" +) + +// Protocol implements netlink.Protocol. +// +// +stateify savable +type Protocol struct{} + +var _ netlink.Protocol = (*Protocol)(nil) + +// NewProtocol creates a NETLINK_NETFILTER netlink.Protocol. +func NewProtocol(t *kernel.Task) (netlink.Protocol, *syserr.Error) { + return &Protocol{}, nil +} + +// Protocol implements netlink.Protocol.Protocol. +func (p *Protocol) Protocol() int { + return linux.NETLINK_NETFILTER +} + +// CanSend implements netlink.Protocol.CanSend. +// Netfilter sockets should be able to send responses back, namely +// if a user wants to see the current state of the netfilter tables. +func (p *Protocol) CanSend() bool { + return true +} + +// ProcessMessage implements netlink.Protocol.ProcessMessage. +func (p *Protocol) ProcessMessage(ctx context.Context, s *netlink.Socket, msg *nlmsg.Message, ms *nlmsg.MessageSet) *syserr.Error { + hdr := msg.Header() + + // Netlink message payloads must be of at least the size of the genmsg. Return early if it is not, + // from linux/net/netfilter/nfnetlink.c. + if netLinkMessagePayloadSize(&hdr) < linux.SizeOfNetfilterGenMsg { + return nil + } + + msgType := hdr.NetFilterMsgType() + // TODO: b/421437663 - Match the message type and call the appropriate Nftables function. + switch msgType { + default: + return syserr.ErrInvalidArgument + } +} + +// init registers the NETLINK_NETFILTER provider. +func init() { + netlink.RegisterProvider(linux.NETLINK_NETFILTER, NewProtocol) +} + +func netLinkMessagePayloadSize(h *linux.NetlinkMessageHeader) int { + return int(h.Length) - linux.NetlinkMessageHeaderSize +} diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index ebd6bd8734..30d6c7e5e5 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -94,6 +94,7 @@ go_library( "//pkg/sentry/socket/hostinet", "//pkg/sentry/socket/netfilter", "//pkg/sentry/socket/netlink", + "//pkg/sentry/socket/netlink/netfilter", "//pkg/sentry/socket/netlink/route", "//pkg/sentry/socket/netlink/uevent", "//pkg/sentry/socket/netstack", diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 0d6eacf254..bc8df90bd9 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -90,6 +90,7 @@ import ( // Include other supported socket providers. _ "gvisor.dev/gvisor/pkg/sentry/socket/netlink" + _ "gvisor.dev/gvisor/pkg/sentry/socket/netlink/netfilter" _ "gvisor.dev/gvisor/pkg/sentry/socket/netlink/route" _ "gvisor.dev/gvisor/pkg/sentry/socket/netlink/uevent" _ "gvisor.dev/gvisor/pkg/sentry/socket/unix" diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 91f3f3d143..130c068dd4 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -937,6 +937,11 @@ syscall_test( test = "//test/syscalls/linux:socket_netlink_route_test", ) +syscall_test( + add_hostinet = True, + test = "//test/syscalls/linux:socket_netlink_netfilter_test", +) + syscall_test( add_hostinet = True, test = "//test/syscalls/linux:socket_netlink_uevent_test", diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 2bfe0bf005..14259293a6 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -3495,6 +3495,23 @@ cc_binary( ], ) +cc_binary( + name = "socket_netlink_netfilter_test", + testonly = 1, + srcs = ["socket_netlink_netfilter.cc"], + linkstatic = 1, + malloc = "//test/util:errno_safe_allocator", + deps = select_gtest() + [ + ":socket_netlink_util", + "//test/util:file_descriptor", + "//test/util:posix_error", + "//test/util:socket_util", + "//test/util:test_main", + "//test/util:test_util", + "@com_google_absl//absl/strings:str_format", + ], +) + cc_binary( name = "socket_netlink_uevent_test", testonly = 1, diff --git a/test/syscalls/linux/socket_netlink_netfilter.cc b/test/syscalls/linux/socket_netlink_netfilter.cc new file mode 100644 index 0000000000..0211e950fa --- /dev/null +++ b/test/syscalls/linux/socket_netlink_netfilter.cc @@ -0,0 +1,95 @@ +// Copyright 2025 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. + +#include +#include +#include + +#include +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/strings/str_format.h" +#include "test/syscalls/linux/socket_netlink_util.h" +#include "test/util/file_descriptor.h" +#include "test/util/posix_error.h" +#include "test/util/socket_util.h" +#include "test/util/test_util.h" + +// Tests for NETLINK_NETFILTER sockets. + +namespace gvisor { +namespace testing { + +namespace { + +using SockOptTest = ::testing::TestWithParam< + std::tuple, std::string>>; + +TEST_P(SockOptTest, GetSockOpt) { + int sockopt = std::get<0>(GetParam()); + auto verifier = std::get<1>(GetParam()); + std::string verifier_description = std::get<2>(GetParam()); + + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE( + Socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER)); + + int res; + socklen_t len = sizeof(res); + + EXPECT_THAT(getsockopt(fd.get(), SOL_SOCKET, sockopt, &res, &len), + SyscallSucceeds()); + + EXPECT_EQ(len, sizeof(res)); + EXPECT_TRUE(verifier(res)) << absl::StrFormat( + "getsockopt(%d, SOL_SOCKET, %d, &res, &len) => res=%d was unexpected, " + "expected %s", + fd.get(), sockopt, res, verifier_description); +} + +std::function IsPositive() { + return [](int val) { return val > 0; }; +} + +std::function IsEqual(int target) { + return [target](int val) { return val == target; }; +} + +INSTANTIATE_TEST_SUITE_P( + NetlinkNetfilterTest, SockOptTest, + ::testing::Values( + std::make_tuple(SO_SNDBUF, IsPositive(), "positive send buffer size"), + std::make_tuple(SO_RCVBUF, IsPositive(), + "positive receive buffer size"), + std::make_tuple(SO_TYPE, IsEqual(SOCK_RAW), + absl::StrFormat("SOCK_RAW (%d)", SOCK_RAW)), + std::make_tuple(SO_DOMAIN, IsEqual(AF_NETLINK), + absl::StrFormat("AF_NETLINK (%d)", AF_NETLINK)), + std::make_tuple(SO_PROTOCOL, IsEqual(NETLINK_NETFILTER), + absl::StrFormat("NETLINK_NETFILTER (%d)", + NETLINK_NETFILTER)), + std::make_tuple(SO_PASSCRED, IsEqual(0), "0"))); + +// Netlink sockets must be SOCK_DGRAM or SOCK_RAW. +TEST(NetlinkNetfilterTest, CanCreateSocket) { + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NetlinkBoundSocket(NETLINK_NETFILTER)); + EXPECT_THAT(fd.get(), SyscallSucceeds()); +} +} // namespace + +} // namespace testing +} // namespace gvisor