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

Add helpers for Linux Traffic Control (TC) filter #209

Open
wants to merge 6 commits into
base: main
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
93 changes: 92 additions & 1 deletion drgn/helpers/linux/tc.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,102 @@
"""

import operator
from typing import Iterator

from drgn import NULL, IntegerLike, Object
from drgn.helpers.linux.list import hlist_for_each_entry, list_for_each_entry

__all__ = ("qdisc_lookup",)
__all__ = (
"for_each_tcf_chain",
"for_each_tcf_proto",
"get_tcf_chain_by_index",
"get_tcf_proto_by_prio",
"qdisc_lookup",
)


def for_each_tcf_chain(block: Object) -> Iterator[Object]:
"""
Iterate over all TC filter chains on a block.

This is only supported since Linux v4.13.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with TC at all, but looking at the commits you reference here, it looks like TC filter chains didn't exist at all before 4.13, is that correct? Maybe we can say something like "This is only supported since Linux v4.13, before which TC filter chains didn't exist.".


:param block: ``struct tcf_block *``
:return: Iterator of ``struct tcf_chain *`` objects.
"""
# Before Linux kernel commit 5bc1701881e3 ("net: sched: introduce
# multichain support for filters") (in v4.13), each block contained only
# one chain.
try:
chain_list = block.chain_list.address_of_()
except AttributeError:
# Before Linux kernel commit 2190d1d0944f ("net: sched: introduce
# helpers to work with filter chains") (in v4.13), struct tcf_chain
# didn't exist.
return block.chain
Comment on lines +36 to +45
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like both of these commits went in during v4.13-rc1. I wouldn't bother checking for the intermediate state between those two commits; let's just assume that the kernel is an official tagged release, which I think will simplify this.


for chain in list_for_each_entry("struct tcf_chain", chain_list, "list"):
yield chain
Comment on lines +47 to +48
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can simplify this to:

Suggested change
for chain in list_for_each_entry("struct tcf_chain", chain_list, "list"):
yield chain
return list_for_each_entry("struct tcf_chain", chain_list, "list")



def for_each_tcf_proto(chain: Object) -> Iterator[Object]:
"""
Iterate over all TC filters on a chain.

This is only supported since Linux v4.13.

:param chain: ``struct tcf_chain *``
:return: Iterator of ``struct tcf_proto *`` objects.
"""
# Before Linux kernel commit 2190d1d0944f ("net: sched: introduce helpers
# to work with filter chains") (in v4.13), struct tcf_chain::filter_chain
# didn't exist.
proto = chain.filter_chain

while proto:
yield proto
proto = proto.next


def get_tcf_chain_by_index(block: Object, index: IntegerLike) -> Object:
"""
Get the TC filter chain with the given index number from a block.

This is only supported since Linux v4.13.

:param block: ``struct tcf_block *``
:param index: TC filter chain index number
:return: ``struct tcf_chain *`` (``NULL`` if not found)
"""
index = operator.index(index)

for chain in for_each_tcf_chain(block):
# Before Linux kernel commit 5bc1701881e3 ("net: sched: introduce
# multichain support for filters") (in v4.13), struct tcf_chain::index
# didn't exist.
if chain.index == index:
return chain

return NULL(block.prog_, "struct tcf_chain *")


def get_tcf_proto_by_prio(chain: Object, prio: IntegerLike) -> Object:
"""
Get the TC filter with the given priority from a chain.

This is only supported since Linux v4.13.

:param chain: ``struct tcf_chain *``
:param prio: TC filter priority (preference) number
:return: ``struct tcf_proto *`` (``NULL`` if not found)
"""
prio = operator.index(prio) << 16

for proto in for_each_tcf_proto(chain):
if proto.prio == prio:
return proto

return NULL(chain.prog_, "struct tcf_proto *")


def qdisc_lookup(dev: Object, major: IntegerLike) -> Object:
Expand Down
183 changes: 155 additions & 28 deletions tests/linux_kernel/helpers/test_tc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,65 @@
import string
import unittest

from drgn import Object
from drgn.helpers.linux.fs import path_lookup
from drgn.helpers.linux.net import get_net_ns_by_inode, netdev_get_by_name
from drgn.helpers.linux.tc import qdisc_lookup
from drgn.helpers.linux.tc import (
for_each_tcf_chain,
for_each_tcf_proto,
get_tcf_chain_by_index,
get_tcf_proto_by_prio,
qdisc_lookup,
)
from tests.linux_kernel import LinuxKernelTestCase

try:
from pyroute2 import NetNS
from pyroute2 import NetNS, protocols
from pyroute2.netlink import (
NLM_F_ACK,
NLM_F_CREATE,
NLM_F_EXCL,
NLM_F_REQUEST,
nlmsg,
)
from pyroute2.netlink.exceptions import NetlinkError
from pyroute2.netlink.rtnl import RTM_NEWTFILTER, TC_H_INGRESS, TC_H_ROOT
from pyroute2.netlink.rtnl.tcmsg import cls_u32, plugins

class _tcmsg(nlmsg):
prefix = "TCA_"

fields = (
("family", "B"),
("pad1", "B"),
("pad2", "H"),
("index", "i"),
("handle", "I"),
("parent", "I"),
("info", "I"),
)

# Currently pyroute2 doesn't support TCA_CHAIN. Use this tailored version
# of class tcmsg for now.
nla_map = (
("TCA_UNSPEC", "none"),
("TCA_KIND", "asciiz"),
("TCA_OPTIONS", "get_options"),
("TCA_STATS", "none"),
("TCA_XSTATS", "none"),
("TCA_RATE", "none"),
("TCA_FCNT", "none"),
("TCA_STATS2", "none"),
("TCA_STAB", "none"),
("TCA_PAD", "none"),
("TCA_DUMP_INVISIBLE", "none"),
("TCA_CHAIN", "uint32"),
)

@staticmethod
def get_options(self, *argv, **kwarg):
del self, argv, kwarg
return cls_u32.options

have_pyroute2 = True
except ImportError:
Expand All @@ -34,41 +85,52 @@ def setUpClass(cls):
cls.ns = NetNS(cls.name, flags=os.O_CREAT | os.O_EXCL)
except FileExistsError:
pass
# ip link add dummy0 type dummy
try:
cls.ns.link("add", ifname="dummy0", kind="dummy")
except NetlinkError:
raise unittest.SkipTest(
"kernel does not support dummy interface (CONFIG_DUMMY)"
)
cls.index = cls.ns.link_lookup(ifname="dummy0")[0]
inode = path_lookup(
cls.prog, os.path.realpath(f"/var/run/netns/{cls.name}")
).dentry.d_inode
cls.net = get_net_ns_by_inode(inode)
cls.netdev = netdev_get_by_name(cls.net, "dummy0")

@classmethod
def tearDownClass(cls):
cls.ns.remove()
super().tearDownClass()

def test_qdisc_lookup(self):
try:
self.ns.link("add", ifname="dummy0", kind="dummy")
except NetlinkError:
self.skipTest("kernel does not support dummy interface (CONFIG_DUMMY)")

dummy = self.ns.link_lookup(ifname="dummy0")[0]
def tearDown(self):
for parent in [TC_H_ROOT, TC_H_INGRESS]: # delete all Qdiscs
try:
self.ns.tc("delete", index=self.index, parent=parent)
except NetlinkError:
pass

def test_qdisc_lookup(self):
# tc qdisc add dev dummy0 root handle 1: prio
try:
self.ns.tc(
"add",
kind="prio",
index=dummy,
index=self.index,
handle="1:",
# default TCA_OPTIONS for sch_prio, see [iproute2] tc/q_prio.c:prio_parse_opt()
bands=3,
priomap=[1, 2, 2, 2, 1, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
)
except NetlinkError:
self.ns.link("delete", ifname="dummy0")
self.skipTest(
"kernel does not support Multi Band Priority Queueing (CONFIG_NET_SCH_PRIO)"
)
# tc qdisc add dev dummy0 parent 1:1 handle 10: sfq
try:
self.ns.tc("add", kind="sfq", index=dummy, parent="1:1", handle="10:")
self.ns.tc("add", kind="sfq", index=self.index, parent="1:1", handle="10:")
except NetlinkError:
self.ns.link("delete", ifname="dummy0")
self.skipTest(
"kernel does not support Stochastic Fairness Queueing (CONFIG_NET_SCH_SFQ)"
)
Expand All @@ -77,38 +139,103 @@ def test_qdisc_lookup(self):
self.ns.tc(
"add",
kind="tbf",
index=dummy,
index=self.index,
parent="1:2",
handle="20:",
rate=2500,
burst=1600,
limit=3000,
)
except NetlinkError:
self.ns.link("delete", ifname="dummy0")
self.skipTest(
"kernel does not support Token Bucket Filter (CONFIG_NET_SCH_TBF)"
)
# tc qdisc add dev dummy0 parent 1:3 handle 30: sfq
self.ns.tc("add", kind="sfq", index=dummy, parent="1:3", handle="30:")
self.ns.tc("add", kind="sfq", index=self.index, parent="1:3", handle="30:")
# tc qdisc add dev dummy0 ingress
try:
self.ns.tc("add", kind="ingress", index=dummy)
self.ns.tc("add", kind="ingress", index=self.index)
except NetlinkError:
self.ns.link("delete", ifname="dummy0")
self.skipTest(
"kernel does not support ingress Qdisc (CONFIG_NET_SCH_INGRESS)"
)

inode = path_lookup(
self.prog, os.path.realpath(f"/var/run/netns/{self.name}")
).dentry.d_inode
netdev = netdev_get_by_name(get_net_ns_by_inode(inode), "dummy0")
self.assertEqual(qdisc_lookup(self.netdev, 0x1).ops.id.string_(), b"prio")
self.assertEqual(qdisc_lookup(self.netdev, 0x10).ops.id.string_(), b"sfq")
self.assertEqual(qdisc_lookup(self.netdev, 0x20).ops.id.string_(), b"tbf")
self.assertEqual(qdisc_lookup(self.netdev, 0x30).ops.id.string_(), b"sfq")
self.assertEqual(qdisc_lookup(self.netdev, 0xFFFF).ops.id.string_(), b"ingress")

def _add_u32_filter(self, chain: int, prio: int):
flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL

msg = _tcmsg()
msg["index"] = self.index
msg["parent"] = 0x10000

u32 = plugins["u32"]
kwarg = dict(
protocol=protocols.ETH_P_ALL,
prio=prio,
target=0x10020,
keys=["0x0/0x0+0"],
action="ok",
)
u32.fix_msg(msg, kwarg)
options = u32.get_parameters(kwarg)

msg["attrs"].append(["TCA_KIND", "u32"])
msg["attrs"].append(["TCA_OPTIONS", options])
msg["attrs"].append(["TCA_CHAIN", chain])

return tuple(self.ns.nlm_request(msg, msg_type=RTM_NEWTFILTER, msg_flags=flags))

def test_tcf_chain_and_tcf_proto(self):
# tc qdisc add dev dummy0 root handle 1: htb
try:
self.ns.tc("add", kind="htb", index=self.index, handle="1:")
except NetlinkError:
self.skipTest(
"kernel does not support Hierarchical Token Bucket (CONFIG_NET_SCH_HTB)"
)

qdisc = qdisc_lookup(self.netdev, 0x1)

if not qdisc.prog_.type("struct htb_sched").has_member("block"):
# Before Linux kernel commit 6529eaba33f0 ("net: sched: introduce
# tcf block infractructure") (in v4.13), struct tcf_block didn't
# exist.
self.skipTest("struct tcf_block does not exist")

block = Object(self.prog, "struct htb_sched *", qdisc.privdata.address_).block

if not block.prog_.type("struct tcf_block").has_member("chain_list"):
# Before Linux kernel commit 5bc1701881e3 ("net: sched: introduce
# multichain support for filters") (in v4.13), struct
# tcf_block::chain_list didn't exist.
self.skipTest("kernel does not support multichain for TC filters")

index_list = [0, 1, 2]
prio_list = [10, 20, 30]

try:
for index in index_list:
for prio in prio_list:
self._add_u32_filter(index, prio)
except NetlinkError:
self.skipTest("kernel does not support u32 filter (CONFIG_NET_CLS_U32)")

chains = list(for_each_tcf_chain(block))
self.assertEqual(len(chains), len(index_list))

for index, chain in zip(index_list, chains):
self.assertEqual(chain.index, index)
self.assertEqual(get_tcf_chain_by_index(block, index), chain)

self.assertEqual(qdisc_lookup(netdev, 0x1).ops.id.string_(), b"prio")
self.assertEqual(qdisc_lookup(netdev, 0x10).ops.id.string_(), b"sfq")
self.assertEqual(qdisc_lookup(netdev, 0x20).ops.id.string_(), b"tbf")
self.assertEqual(qdisc_lookup(netdev, 0x30).ops.id.string_(), b"sfq")
self.assertEqual(qdisc_lookup(netdev, 0xFFFF).ops.id.string_(), b"ingress")
filters = list(for_each_tcf_proto(chain))
self.assertEqual(len(filters), len(prio_list))

self.ns.link("delete", ifname="dummy0")
for prio, filter in zip(prio_list, filters):
self.assertEqual(filter.ops.kind.string_(), b"u32")
self.assertEqual(filter.prio, prio << 16)
self.assertEqual(get_tcf_proto_by_prio(chain, prio), filter)