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