Skip to content

Commit

Permalink
netfilter: Support multiport matching (-m multiport)
Browse files Browse the repository at this point in the history
This set of changes adds:

  - support for `xt_multiport_{,v1}` matchers for matching for
    a range of ports and their inverse, i.e.,:

      ```
      -m multiport [!] --[s|d]ports (PORT,...|PORT:PORT,...)
      ```

  - support for `IP{,6}T_SO_GET_REVISION_MATCH` socket options,
    which allows `iptables` to query for the highest supported
    revision for a given matcher
  • Loading branch information
clickyotomy committed Dec 14, 2024
1 parent 7aa4e8d commit 51d6e38
Show file tree
Hide file tree
Showing 11 changed files with 1,137 additions and 4 deletions.
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

0 comments on commit 51d6e38

Please sign in to comment.