Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

netfilter: Support multiport matching (-m multiport) #11298

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions pkg/abi/linux/netfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,3 +727,63 @@ const (
// packets do not have an associated socket.
XT_OWNER_SOCKET = 1 << 2
)

// XT_MULTI_PORTS is the maximum number of ports that the
// multiport match can handle.
const XT_MULTI_PORTS = 15

// Flags in XTMultiport{,V1}.Flags; values from "enum xt_multiport_flags"
// in "include/uapi/linux/netfilter/xt_multiport.h".
const (
XT_MULTIPORT_SOURCE uint8 = 0x0 // Match against source ports.
XT_MULTIPORT_DESTINATION uint8 = 0x1 // Match against destination ports.
XT_MULTIPORT_EITHER uint8 = 0x2 // Match against either ports.
)

// XTMultiport holds data for matching packets against a set
// of ports. It corresponds to "struct xt_multiport" defined
// in "include/uapi/linux/netfilter/xt_multiport.h".
//
// +marshal
type XTMultiport struct {
// Flags indicates whether the match applies to
// source ports, destination ports, or either, as
// defined by "enum xt_multiport_flags".
Flags uint8

// Count is the number of ports in the "Ports"
// slice that the match will check. It must be
// between 1 and "XT_MULTI_PORTS" (inclusive).
Count uint8

// Ports is the set of ports that will be matched.
// Only the first "Count" entries are considered.
Ports [XT_MULTI_PORTS]uint16
}

// XTMultiportV1 holds data for matching packets against a set
// of ports. It corresponds to "struct xt_multiport_v1" defined
// in "include/uapi/linux/netfilter/xt_multiport.h".
//
// +marshal
type XTMultiportV1 struct {
// Fields same as "XTMultiport".
Flags uint8
Count uint8
Ports [XT_MULTI_PORTS]uint16

// Pflags is an array of port-specific flags. Each entry
// in "Pflags" corresponds to the port at the same index
// in "Ports".
Pflags [XT_MULTI_PORTS]uint8

// Invert is a flag that, if nonzero, indicates
// that the match result should be inverted.
Invert uint8
}

// SizeOfXTMultiport is the size of XTMultiport (in bytes).
const SizeOfXTMultiport = 2 + (XT_MULTI_PORTS * 2)

// SizeOfXTMultiportV1 is the size of XTMultiportV1 (in bytes).
const SizeOfXTMultiportV1 = SizeOfXTMultiport + XT_MULTI_PORTS + 1
2 changes: 2 additions & 0 deletions pkg/abi/linux/netfilter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func TestSizes(t *testing.T) {
{IP6TReplace{}, SizeOfIP6TReplace},
{IP6TEntry{}, SizeOfIP6TEntry},
{IP6TIP{}, SizeOfIP6TIP},
{XTMultiport{}, SizeOfXTMultiport},
{XTMultiportV1{}, SizeOfXTMultiportV1},
}

for _, tc := range testCases {
Expand Down
2 changes: 2 additions & 0 deletions pkg/sentry/socket/netfilter/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ go_library(
"extensions.go",
"ipv4.go",
"ipv6.go",
"multiport_matcher.go",
"multiport_matcher_v1.go",
"netfilter.go",
"owner_matcher.go",
"owner_matcher_v1.go",
Expand Down
21 changes: 20 additions & 1 deletion pkg/sentry/socket/netfilter/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func marshalEntryMatch(name string, data []byte) []byte {
return buf
}

func unmarshalMatcher(mapper IDMapper, match linux.XTEntryMatch, filter stack.IPHeaderFilter, buf []byte) (stack.Matcher, error) {
func unmarshalMatcher(mapper IDMapper, match *linux.XTEntryMatch, filter stack.IPHeaderFilter, buf []byte) (stack.Matcher, error) {
key := matchKey{
name: match.Name.String(),
revision: match.Revision,
Expand All @@ -117,6 +117,25 @@ func unmarshalMatcher(mapper IDMapper, match linux.XTEntryMatch, filter stack.IP
return matchMaker.unmarshal(mapper, buf, filter)
}

// matchMakerRevision returns the maximum supported version of the
// matcher with "name" up to "rev" and whether any such matcher
// with that name exists.
func matchMakerRevision(name string, rev uint8) (uint8, bool) {
var found bool
var ret uint8

for matcher := range matchMakers {
if name == matcher.name {
found = true
if matcher.revision > ret {
ret = matcher.revision
}
}
}

return ret, found
}

// targetMaker knows how to (un)marshal a target. Once registered,
// marshalTarget and unmarshalTarget can be used.
type targetMaker interface {
Expand Down
242 changes: 242 additions & 0 deletions pkg/sentry/socket/netfilter/multiport_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// 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.

package netfilter

import (
"fmt"

"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/marshal"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)

const (
matcherNameMultiport string = "multiport"
matcherRevMultiport uint8 = 0
matcherPfxMultiport string = (matcherNameMultiport + ".0")
)

// multiportMarshaler handles marshalling and
// unmarshalling of "xt_multiport" matchers.
type multiportMarshaler struct{}

// multiportMatcher represents a multiport matcher
// with source and/or destination ports.
type multiportMatcher struct {
flags uint8 // Port match flag (source/destination/either).
count uint8 // Number of ports.
ports []uint16 // List of ports to match against.
}

// init registers the "multiportMarshaler" with the matcher registry.
func init() {
registerMatchMaker(multiportMarshaler{})
}

// name returns the name of the marshaler.
func (multiportMarshaler) name() string {
return matcherNameMultiport
}

// revision returns the revision number of the marshaler.
func (multiportMarshaler) revision() uint8 {
return matcherRevMultiport
}

// marshal converts a matcher into its binary representation.
func (multiportMarshaler) marshal(mr matcher) []byte {
m := mr.(*multiportMatcher)
var xtmp linux.XTMultiport

nflog("%s: marshal: XTMultiport: %+v", matcherPfxMultiport, m)

// Set the match criteria flag.
xtmp.Flags = m.flags

// Set the count of ports and populate the "Ports" slice.
xtmp.Count = uint8(len(m.ports))

// Truncate the "ports" slice to the maximum allowed
// by "XT_MULTI_PORTS" to prevent out-of-bounds writes.
if xtmp.Count > linux.XT_MULTI_PORTS {
xtmp.Count = linux.XT_MULTI_PORTS
}

// Copy over the ports.
for i := uint8(0); i < xtmp.Count; i++ {
xtmp.Ports[i] = m.ports[i]
}

// Marshal the XTMultiport structure into binary format.
return marshalEntryMatch(matcherNameMultiport, marshal.Marshal(&xtmp))
}

// unmarshal converts binary data into a multiportMatcher instance.
func (multiportMarshaler) unmarshal(_ IDMapper, buf []byte, filter stack.IPHeaderFilter) (stack.Matcher, error) {
var matchData linux.XTMultiport

nflog("%s: raw: XTMultiport: %+v", matcherPfxMultiport, buf)

// Check if the buffer has enough data for XTMultiport.
if len(buf) < linux.SizeOfXTMultiport {
return nil, fmt.Errorf(
"%s: insufficient data, got %d, want: >= %d",
matcherPfxMultiport,
len(buf),
linux.SizeOfXTMultiport,
)
}

// Unmarshal the buffer into the XTMultiport structure.
matchData.UnmarshalUnsafe(buf)
nflog("%s: parsed XTMultiport: %+v", matcherPfxMultiport, matchData)

// Validate the port count.
if matchData.Count == 0 || matchData.Count > linux.XT_MULTI_PORTS {
return nil, fmt.Errorf(
"%s: invalid port count, got %d, want: [1, %d]",
matcherPfxMultiport, matchData.Count, linux.XT_MULTI_PORTS,
)
}

// Extract the list of ports from the match data.
ports := make([]uint16, matchData.Count)
for i := 0; i < int(matchData.Count); i++ {
ports[i] = matchData.Ports[i]
}

// Initialize "multiportMatcher" with the extracted ports.
matcher := &multiportMatcher{
flags: matchData.Flags,
count: matchData.Count,
ports: ports,
}

return matcher, nil
}

// name returns the name of the matcher.
func (multiportMatcher) name() string {
return matcherNameMultiport
}

// revision returns the revision number of the matcher.
func (multiportMatcher) revision() uint8 {
return matcherRevMultiport
}

// Match determines if the packet matches any of the specified ports
// and returns true if a match is found. The second boolean returned
// indicates whether the packet should be "hot" dropped, or processed
// with other matchers.
func (m *multiportMatcher) Match(hook stack.Hook, pkt *stack.PacketBuffer, _, _ string) (bool, bool) {
// Extract source and destination ports from the packet.
srcPort, dstPort, ok := extractPorts(pkt)
// The packet does not contain valid transport
// headers or uses an unsupported protocol.
if !ok {
return false, true
}

// Iterate through the list of ports to check for a match based on
// the specified match criteria: source, destination or either.
for i := uint8(0); i < m.count; i++ {
if exactPortMatch(m.flags, srcPort, dstPort, m.ports[i]) {
return true, false
}
}

// No match.
return false, false
}

// extractTransportHeaderPorts is a helper routine that extracts
// the source and destination ports from the provided transport
// header based on the specified transport protocol. It supports
// TCP and UDP protocols and returns the source port, destination
// port, and a boolean indicating whether the extraction was
// successful. If the protocol is unsupported or the transport
// header is too short, it returns (0, 0, false).
func extractTransportHeaderPorts(hdr []byte, prot tcpip.TransportProtocolNumber) (uint16, uint16, bool) {
switch prot {
case header.TCPProtocolNumber:
// Ensure the TCP header has the minimum required length.
if len(hdr) < header.TCPMinimumSize {
return 0, 0, false
}
// Extract and return the source and destination ports.
tcpHdr := header.TCP(hdr)
return tcpHdr.SourcePort(), tcpHdr.DestinationPort(), true

case header.UDPProtocolNumber:
// Similar to TCP.
if len(hdr) < header.UDPMinimumSize {
return 0, 0, false
}
udpHdr := header.UDP(hdr)
return udpHdr.SourcePort(), udpHdr.DestinationPort(), true

default:
// Unsupported transport protocol; cannot extract ports.
return 0, 0, false
}
}

// extractPorts extracts the source and destination ports from the given
// packet buffer. It supports both IPv4 and IPv6 packets and handles TCP
// and UDP transport protocols. It returns the source port, destination
// port, and a boolean indicating success. If the packet does not contain
// enough data or uses an unsupported protocol, it returns (0, 0, false).
func extractPorts(pkt *stack.PacketBuffer) (uint16, uint16, bool) {
// Retrieve the transport header (TCP/UDP) from the packet buffer.
transportHdr := pkt.TransportHeader().Slice()

// Determine the network protocol.
switch pkt.NetworkProtocolNumber {
case header.IPv4ProtocolNumber:
// Extract the IPv4 header from the network header
// slice, then the transport protocol from it.
ipv4 := header.IPv4(pkt.NetworkHeader().Slice())
prot := ipv4.TransportProtocol()
return extractTransportHeaderPorts(transportHdr, prot)

case header.IPv6ProtocolNumber:
// Similar to IPv4.
ipv6 := header.IPv6(pkt.NetworkHeader().Slice())
prot := ipv6.TransportProtocol()
return extractTransportHeaderPorts(transportHdr, prot)

default:
// Unsupported network protocol; cannot extract ports.
return 0, 0, false
}
}

// exactPortMatch return true if "srcPort" or "dstPort" are the
// same as "matchPort" depending on the matching criteria specified
// in "flags".
func exactPortMatch(flags uint8, srcPort, dstPort, matchPort uint16) bool {
switch flags {
case linux.XT_MULTIPORT_SOURCE:
return srcPort == matchPort
case linux.XT_MULTIPORT_DESTINATION:
return dstPort == matchPort
case linux.XT_MULTIPORT_EITHER:
return (srcPort == matchPort) || (dstPort == matchPort)
}
return false
}
Loading
Loading