Thread (3 messages) 3 messages, 1 author, 14d ago
DORMANTno replies

[RFC PATCH net-next 2/2] selftests/net: selftest for AF_LLC jumbo frames

From: David 'equinox' Lamparter <hidden>
Date: 2026-07-13 12:46:00
Subsystem: kernel selftest framework, networking [general], the rest · Maintainers: Shuah Khan, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds

This selftest just checks that AF_LLC correctly in- & output frames
below and above the jumbo frame threshold (1500 bytes).  It more or less
also doubles as basic AF_LLC test.

Unfortunately AF_LLC can't be used in a netns, so this test must run as
root in the initial netns.

Signed-off-by: David 'equinox' Lamparter <redacted>
---
 MAINTAINERS                              |   1 +
 tools/testing/selftests/net/Makefile     |   1 +
 tools/testing/selftests/net/llc_jumbo.py | 337 +++++++++++++++++++++++
 3 files changed, 339 insertions(+)
 create mode 100644 tools/testing/selftests/net/llc_jumbo.py
diff --git a/MAINTAINERS b/MAINTAINERS
index f3218abefd0c..8ca5afa39734 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15057,6 +15057,7 @@ F:	include/linux/llc.h
 F:	include/net/llc*
 F:	include/uapi/linux/llc.h
 F:	net/llc/
+F:	tools/testing/selftests/net/llc_jumbo.py
 
 LM73 HARDWARE MONITOR DRIVER
 M:	Guillaume Ligneul <guillaume.ligneul@gmail.com>
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 708d960ae07d..4b3d5491909b 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -57,6 +57,7 @@ TEST_PROGS := \
 	l2_tos_ttl_inherit.sh \
 	l2tp.sh \
 	link_netns.py \
+	llc_jumbo.py \
 	lwt_dst_cache_ref_loop.sh \
 	macvlan_mcast_shared_mac.sh \
 	msg_zerocopy.sh \
diff --git a/tools/testing/selftests/net/llc_jumbo.py b/tools/testing/selftests/net/llc_jumbo.py
new file mode 100644
index 000000000000..51c23ea6f812
--- /dev/null
+++ b/tools/testing/selftests/net/llc_jumbo.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Copyright (C) 2026 David 'equinox' Lamparter
+
+Linux kernel self-test for AF_LLC sockets, specifically jumbo frame (802.1AC)
+support.
+"""
+
+from __future__ import annotations
+
+import os
+import signal
+import struct
+import binascii
+import functools
+import errno
+import ctypes
+from typing import Callable
+
+from socket import socket, htons, SOCK_DGRAM, SOCK_RAW, AF_PACKET, ETH_P_ALL
+
+from lib.py import ksft_run, ksft_exit, ksft_eq, KsftSkipEx
+from lib.py import ip
+
+# from lib.py import NetNS, NetNSEnter
+
+
+ETH_P_8021AC = 0x8870
+AF_LLC = 26
+
+
+class ContextSkip:
+    """
+    convert exception from context manager into KsftSkipEx with message
+    """
+
+    def __init__(self, ctx, message):
+        self._ctx = ctx
+        self._message = message
+
+    def __enter__(self):
+        try:
+            return self._ctx.__enter__()
+        except Exception as e:
+            raise KsftSkipEx(self._message + "[" + repr(e) + "]") from e
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self._ctx.__exit__(exc_type, exc_value, traceback)
+
+
+# Python's socket.bind() has no clue about AF_LLC
+c_macaddr = ctypes.c_ubyte * 6
+
+
+class sockaddr_llc(ctypes.Structure):
+    _fields_ = [
+        ("sllc_family", ctypes.c_ushort),
+        ("sllc_arphrd", ctypes.c_ushort),
+        ("sllc_test", ctypes.c_ubyte),
+        ("sllc_xid", ctypes.c_ubyte),
+        ("sllc_ua", ctypes.c_ubyte),
+        ("sllc_sap", ctypes.c_ubyte),
+        ("sllc_mac", c_macaddr),
+        ("pad", ctypes.c_ubyte * 2),
+    ]
+
+    @classmethod
+    def make(
+        cls,
+        sllc_family=AF_LLC,
+        sllc_arphrd=0,
+        sllc_test=0,
+        sllc_xid=0,
+        sllc_ua=0,
+        sllc_sap=0,
+        sllc_mac: None | str | bytes = None,
+    ) -> sockaddr_llc:
+        if sllc_mac is None:
+            _sllc_mac = c_macaddr(0, 0, 0, 0, 0, 0)
+        elif isinstance(sllc_mac, str):
+            _sllc_mac = c_macaddr(*(int(b, 16) for b in sllc_mac.split(":")))
+        else:
+            _sllc_mac = c_macaddr(*(b for b in sllc_mac))
+        return cls(
+            sllc_family, sllc_arphrd, sllc_test, sllc_xid, sllc_ua, sllc_sap, _sllc_mac
+        )
+
+
+assert len(bytes(sockaddr_llc())) == 16
+
+
+libc = ctypes.CDLL(None, use_errno=True)
+libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(sockaddr_llc), ctypes.c_int)
+libc.bind.restype = ctypes.c_int
+libc.sendto.argtypes = (
+    ctypes.c_int,
+    ctypes.c_voidp,
+    ctypes.c_size_t,
+    ctypes.c_int,
+    ctypes.POINTER(sockaddr_llc),
+    ctypes.c_int,
+)
+libc.sendto.restype = ctypes.c_int
+
+
+def llc_bind(fd: socket, addr: sockaddr_llc) -> None:
+    ret = libc.bind(fd.fileno(), addr, len(bytes(addr)))
+    if ret:
+        err = ctypes.get_errno()
+        raise OSError(err, errno.errorcode.get(err, str(err)))
+
+
+def llc_sendto(fd: socket, addr: sockaddr_llc, data: bytes, flags=0) -> None:
+    ret = libc.sendto(fd.fileno(), data, len(data), flags, addr, len(bytes(addr)))
+    if ret < 0:
+        err = ctypes.get_errno()
+        raise OSError(err, errno.errorcode.get(err, str(err)))
+    return ret
+
+
+def wrap_common_setup(testfn: Callable[[socket, socket], None]) -> Callable[[], None]:
+    """
+    common setup for all LLC tests
+
+    (create netns, create AF_LLC + AF_PACKET sockets)
+    """
+
+    def inner1() -> None:
+        if os.path.exists("/sys/class/net/testveth0"):
+            raise KsftSkipEx("already have a testveth0 netdev")
+        if os.path.exists("/sys/class/net/testveth1"):
+            raise KsftSkipEx("already have a testveth1 netdev")
+
+        ip(
+            "link add name testveth0 address 02:00:00:00:00:00 "
+            + "type veth peer name testveth1 address 02:11:11:11:11:11"
+        )
+        with open(
+            "/proc/sys/net/ipv6/conf/testveth0/disable_ipv6", "w", encoding="ASCII"
+        ) as fd:
+            fd.write("1\n")
+        with open(
+            "/proc/sys/net/ipv6/conf/testveth1/disable_ipv6", "w", encoding="ASCII"
+        ) as fd:
+            fd.write("1\n")
+        ip("link set testveth0 mtu 9000 up")
+        ip("link set testveth1 mtu 9000 up")
+
+        try:
+            with ContextSkip(
+                socket(AF_LLC, SOCK_DGRAM, 0), "AF_LLC not enabled in kernel?"
+            ) as sock0_llc:
+                llc_bind(
+                    sock0_llc,
+                    sockaddr_llc.make(sllc_sap=0xFE, sllc_mac="02:00:00:00:00:00"),
+                )
+
+                with ContextSkip(
+                    socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)),
+                    "AF_PACKET not enabled in kernel?",
+                ) as sock1_pkt:
+                    sock1_pkt.bind(("testveth1", ETH_P_ALL))
+
+                    signal.alarm(10)
+                    testfn(sock0_llc, sock1_pkt)
+
+        finally:
+            signal.alarm(0)
+            ip("link del testveth0")
+
+    @functools.wraps(testfn)
+    def inner() -> None:
+        if os.getuid() == 0:
+            inner1()
+        else:
+            raise KsftSkipEx(
+                "this test requires root since AF_LLC has no netns support (yet?)"
+            )
+            # with NetNS() as ns1:
+            #    with NetNSEnter(str(ns1)):
+            #        inner1()
+
+    return inner
+
+
+def pkt_check(fd: socket, expectlen: int, data: None | bytes = None):
+    # ignore non-LLC packets
+    # (since we can't run in netns, random junk may show up)
+    proto_len = -1
+    while not (0 <= proto_len < 1536 or proto_len == 0x8870):
+        pkt = fd.recv(4096)
+        proto_len = struct.unpack(">H", pkt[12:14])[0]
+
+    assert binascii.b2a_hex(pkt[0:6]) == b"021111111111"
+    assert binascii.b2a_hex(pkt[6:12]) == b"020000000000"
+
+    if expectlen <= 1497:
+        ksft_eq(proto_len, expectlen + 3)
+    else:
+        ksft_eq(proto_len, ETH_P_8021AC, "802.1AC ethertype (0x8870)")
+
+    if data is None:
+        data = b"\00" * expectlen
+    else:
+        data = (data + b"\00" * expectlen)[:expectlen]
+
+    ksft_eq(binascii.b2a_hex(pkt[14:17]).decode("ASCII").upper(), "FEFE03")
+    ksft_eq(pkt[17:], data)
+
+
+def test_llc_tx(sock0_llc: socket, sock1_pkt: socket) -> None:
+    llc_sendto(
+        sock0_llc,
+        sockaddr_llc.make(sllc_sap=0xFE, sllc_mac="02:11:11:11:11:11"),
+        b"\x00" * 1400,
+    )
+    pkt_check(sock1_pkt, 1400)
+
+    llc_sendto(
+        sock0_llc,
+        sockaddr_llc.make(sllc_sap=0xFE, sllc_mac="02:11:11:11:11:11"),
+        b"\x00" * 1497,
+    )
+    pkt_check(sock1_pkt, 1497)
+
+
+def test_llc_tx_jumbo(sock0_llc: socket, sock1_pkt: socket) -> None:
+    llc_sendto(
+        sock0_llc,
+        sockaddr_llc.make(sllc_sap=0xFE, sllc_mac="02:11:11:11:11:11"),
+        b"\x00" * 1498,
+    )
+    pkt_check(sock1_pkt, 1498)
+
+    llc_sendto(
+        sock0_llc,
+        sockaddr_llc.make(sllc_sap=0xFE, sllc_mac="02:11:11:11:11:11"),
+        b"\x00" * 4000,
+    )
+    pkt_check(sock1_pkt, 4000)
+
+
+machdr = binascii.a2b_hex("020000000000" + "021111111111")
+
+
+def test_llc_rx(sock0_llc: socket, sock1_pkt: socket) -> None:
+    sock1_pkt.send(
+        machdr + struct.pack(">H", 1403) + binascii.a2b_hex("FEFE03") + b"\x00" * 1400
+    )
+    rxdata = sock0_llc.recv(4096)
+    ksft_eq(len(rxdata), 1400)
+    ksft_eq(rxdata, b"\x00" * 1400)
+
+    sock1_pkt.send(
+        machdr + struct.pack(">H", 1500) + binascii.a2b_hex("FEFE03") + b"\x00" * 1497
+    )
+    rxdata = sock0_llc.recv(4096)
+    ksft_eq(len(rxdata), 1497)
+    ksft_eq(rxdata, b"\x00" * 1497)
+
+
+def test_llc_rx_jumbo(sock0_llc: socket, sock1_pkt: socket) -> None:
+    sock1_pkt.send(
+        machdr
+        + struct.pack(">H", ETH_P_8021AC)
+        + binascii.a2b_hex("FEFE03")
+        + b"\x00" * 1498
+    )
+    rxdata = sock0_llc.recv(4096)
+    ksft_eq(len(rxdata), 1498)
+    ksft_eq(rxdata, b"\x00" * 1498)
+
+    sock1_pkt.send(
+        machdr
+        + struct.pack(">H", ETH_P_8021AC)
+        + binascii.a2b_hex("FEFE03")
+        + b"\x00" * 4000
+    )
+    rxdata = sock0_llc.recv(4096)
+    ksft_eq(len(rxdata), 4000)
+    ksft_eq(rxdata, b"\x00" * 4000)
+
+
+def test_llc_rx_reject_smuggling(sock0_llc: socket, sock1_pkt: socket) -> None:
+    """
+    make sure smaller packets can't be smuggled in using 0x8870 ethertype
+    """
+    # these tests use a 2nd packet to check first one was dropped, relying on ordering
+
+    sock1_pkt.send(
+        machdr
+        + struct.pack(">H", ETH_P_8021AC)
+        + binascii.a2b_hex("FEFE03")
+        + b"\x00" * 1497
+    )
+    sock1_pkt.send(
+        machdr + struct.pack(">H", 131) + binascii.a2b_hex("FEFE03") + b"\x00" * 128
+    )
+    rxdata = sock0_llc.recv(4096)
+    ksft_eq(len(rxdata), 128)
+    ksft_eq(rxdata, b"\x00" * 128)
+
+    sock1_pkt.send(
+        machdr
+        + struct.pack(">H", ETH_P_8021AC)
+        + binascii.a2b_hex("FEFE03")
+        + b"\x00" * 250
+    )
+    sock1_pkt.send(
+        machdr + struct.pack(">H", 131) + binascii.a2b_hex("FEFE03") + b"\x00" * 128
+    )
+    rxdata = sock0_llc.recv(4096)
+    ksft_eq(len(rxdata), 128)
+    ksft_eq(rxdata, b"\x00" * 128)
+
+
+def main() -> None:
+    ksft_run(
+        [
+            wrap_common_setup(test_llc_tx),
+            wrap_common_setup(test_llc_tx_jumbo),
+            wrap_common_setup(test_llc_rx),
+            wrap_common_setup(test_llc_rx_jumbo),
+            wrap_common_setup(test_llc_rx_reject_smuggling),
+        ]
+    )
+    ksft_exit()
+
+
+def sigalrm(sig, frame):
+    raise TimeoutError("SIGALRM")
+
+
+if __name__ == "__main__":
+    signal.signal(signal.SIGALRM, sigalrm)
+    main()
-- 
2.53.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help