From 2b80e79f72484b090b52d2aa0361b472216c2c78 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 14 May 2026 18:05:42 +0200 Subject: [PATCH 1/3] Add support for VLAN 802.1q tagging/filtering --- .github/workflows/stm32h563-m33mu.yml | 46 + Makefile | 43 +- config.h | 23 + src/port/stm32h563/Makefile | 24 + src/port/stm32h563/config.h | 23 + src/port/stm32h563/main.c | 39 + src/test/unit/unit.c | 37 + src/test/unit/unit_shared.c | 5 + src/test/unit/unit_tests_vlan.c | 1109 +++++++++++++++++++++++++ src/wolfip.c | 246 +++++- wolfip.h | 40 + 11 files changed, 1611 insertions(+), 24 deletions(-) create mode 100644 src/test/unit/unit_tests_vlan.c diff --git a/.github/workflows/stm32h563-m33mu.yml b/.github/workflows/stm32h563-m33mu.yml index ad1c9f01..21fc294b 100644 --- a/.github/workflows/stm32h563-m33mu.yml +++ b/.github/workflows/stm32h563-m33mu.yml @@ -449,3 +449,49 @@ jobs: if [ -f /tmp/tcpdump.pid ]; then sudo kill "$(cat /tmp/tcpdump.pid)" 2>/dev/null || true fi + + stm32h563_m33mu_vlan: + runs-on: ubuntu-latest + timeout-minutes: 20 + container: + image: ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.2 + options: --privileged + + steps: + - uses: actions/checkout@v4 + + - name: Install host tools + run: | + set -euo pipefail + apt-get update + # iproute2: 'ip' command (tap, vlan link-add) + # tcpdump: packet capture on tap0 + # tshark: filter/parse the pcap for VID + direction assertions + # sudo: the integration script wraps privileged ops with sudo + apt-get install -y sudo iproute2 tcpdump tshark + + - name: Run VLAN integration test (TCP echo over 802.1Q) + timeout-minutes: 15 + env: + VLAN_VID: "100" + VLAN_PCP: "0" + M33MU_TIMEOUT: "60" + run: | + set -euo pipefail + # The script builds the firmware with ENABLE_VLAN=1, sets up tap0 + # + tap0.${VLAN_VID}, boots m33mu, probes the TCP echo service on + # port 7 over the VLAN, and asserts via tshark that 802.1Q frames + # flowed in both directions on VID=${VLAN_VID}. + bash tools/scripts/debug-m33mu-vlan-local.sh + + - name: Upload VLAN artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: vlan-debug-artifacts + path: | + /tmp/m33mu-vlan.log + /tmp/m33mu-vlan.pcap + /tmp/m33mu-vlan-tshark.txt + /tmp/m33mu-vlan-echo.txt + if-no-files-found: ignore diff --git a/Makefile b/Makefile index 4307b1c5..bbb5489f 100644 --- a/Makefile +++ b/Makefile @@ -447,7 +447,8 @@ UNIT_TEST_SRCS:=src/test/unit/unit.c \ src/test/unit/unit_tests_dhcp_edges.c \ src/test/unit/unit_tests_ip_arp_recv.c \ src/test/unit/unit_tests_dns_edges.c \ - src/test/unit/unit_tests_misc_edges.c + src/test/unit/unit_tests_misc_edges.c \ + src/test/unit/unit_tests_vlan.c unit: build/test/unit @@ -461,6 +462,9 @@ build/test/unit: $(UNIT_TEST_SRCS) unit-multicast: CFLAGS+=-DIP_MULTICAST unit-multicast: clean-unit unit +unit-vlan: CFLAGS+=-DWOLFIP_VLAN=1 -DWOLFIP_MAX_INTERFACES=6 +unit-vlan: clean-unit unit + ESP_UNIT_CHECK_CFLAGS := $(CHECK_PKG_CFLAGS) ifeq ($(UNAME_S),Darwin) ifneq ($(CHECK_PREFIX),) @@ -523,6 +527,8 @@ COV_UNIT:=$(COV_DIR)/unit COV_UNIT_O:=$(COV_DIR)/unit.o COV_MCAST_UNIT:=$(COV_DIR)/unit-multicast COV_MCAST_UNIT_O:=$(COV_DIR)/unit-multicast.o +COV_VLAN_UNIT:=$(COV_DIR)/unit-vlan +COV_VLAN_UNIT_O:=$(COV_DIR)/unit-vlan.o $(COV_UNIT_O): $(UNIT_TEST_SRCS) @mkdir -p $(COV_DIR) @@ -594,6 +600,39 @@ cov-multicast: unit-multicast $(COV_MCAST_UNIT) --html-details -o build/coverage/multicast.html @$(OPEN_CMD) build/coverage/multicast.html +$(COV_VLAN_UNIT_O): $(UNIT_TEST_SRCS) + @mkdir -p $(COV_DIR) + @echo "[CC] unit.c (vlan coverage)" + @$(CC) $(UNIT_CFLAGS) $(CFLAGS) -DWOLFIP_VLAN=1 -DWOLFIP_MAX_INTERFACES=6 --coverage -c src/test/unit/unit.c -o $(COV_VLAN_UNIT_O) + +$(COV_VLAN_UNIT): LDFLAGS+=--coverage $(UNIT_LIBS) +$(COV_VLAN_UNIT): $(COV_VLAN_UNIT_O) + @echo "[LD] $@" + @$(CC) $(COV_VLAN_UNIT_O) -o $(COV_VLAN_UNIT) $(UNIT_LDFLAGS) $(LDFLAGS) + +cov-vlan: unit-vlan $(COV_VLAN_UNIT) + @echo "[RUN] unit vlan (coverage)" + @rm -f $(COV_DIR)/*.gcda + @$(COV_VLAN_UNIT) + @echo "[COV] gcovr vlan html" + @mkdir -p build/coverage + @gcovr -r . --exclude "src/test/unit/.*" \ + --gcov-ignore-errors=no_working_dir_found \ + --merge-mode-functions=merge-use-line-min \ + --html-details -o build/coverage/vlan.html + @$(OPEN_CMD) build/coverage/vlan.html + +autocov-vlan: unit-vlan $(COV_VLAN_UNIT) + @echo "[RUN] unit vlan (coverage)" + @rm -f $(COV_DIR)/*.gcda + @$(COV_VLAN_UNIT) + @echo "[COV] gcovr vlan html" + @mkdir -p build/coverage + @gcovr -r . --exclude "src/test/unit/.*" \ + --gcov-ignore-errors=no_working_dir_found \ + --merge-mode-functions=merge-use-line-min \ + --html-details -o build/coverage/vlan.html + # Install dynamic library to re-link linux applications # install: @@ -692,7 +731,7 @@ build/test/test-wolfguard-interop: src/test/test_wolfguard_interop.c src/port/po clean-test-wolfguard-interop: @rm -f build/test/test-wolfguard-interop build/test/test_wolfguard_interop.o build/test/linux_tun.o -.PHONY: clean all static cppcheck cov autocov autocov-multicast cov-multicast unit-multicast unit-asan unit-ubsan unit-leaksan clean-unit \ +.PHONY: clean all static cppcheck cov autocov autocov-multicast cov-multicast unit-multicast unit-vlan cov-vlan autocov-vlan unit-asan unit-ubsan unit-leaksan clean-unit \ unit-esp-asan unit-esp-ubsan unit-esp-leaksan clean-unit-esp \ unit-wolfguard unit-wolfguard-asan unit-wolfguard-ubsan clean-unit-wolfguard \ test-wolfguard-loopback test-wolfguard-loopback-asan test-wolfguard-loopback-ubsan \ diff --git a/config.h b/config.h index 6e803095..55b45548 100644 --- a/config.h +++ b/config.h @@ -78,6 +78,29 @@ #error "WOLFIP_ENABLE_LOOPBACK requires WOLFIP_MAX_INTERFACES > 1" #endif +/* 802.1Q VLAN support. Off by default; when off, all VLAN code is removed + * by the preprocessor and behavior/ABI of the stack is unchanged. + * + * WOLFIP_VLAN_MAX is a hard cap on the number of *simultaneously live* + * VLAN sub-interfaces. The capacity must fit alongside the physical + * interface and, when loopback is enabled, also the loopback slot. */ +#ifndef WOLFIP_VLAN +#define WOLFIP_VLAN 0 +#endif +#ifndef WOLFIP_VLAN_MAX +#define WOLFIP_VLAN_MAX 4 +#endif +#if WOLFIP_VLAN +#if WOLFIP_ENABLE_LOOPBACK +#define WOLFIP_VLAN_RESERVED_SLOTS 2 /* loopback + 1 physical */ +#else +#define WOLFIP_VLAN_RESERVED_SLOTS 1 /* 1 physical */ +#endif +#if (WOLFIP_MAX_INTERFACES < (WOLFIP_VLAN_RESERVED_SLOTS + WOLFIP_VLAN_MAX)) +#error "WOLFIP_VLAN requires WOLFIP_MAX_INTERFACES >= 1 (physical) + (WOLFIP_ENABLE_LOOPBACK ? 1 : 0) + WOLFIP_VLAN_MAX" +#endif +#endif + /* Linux test configuration */ #define WOLFIP_IP "10.10.10.2" #define HOST_STACK_IP "10.10.10.1" diff --git a/src/port/stm32h563/Makefile b/src/port/stm32h563/Makefile index ca4ea2f2..bfd557a4 100644 --- a/src/port/stm32h563/Makefile +++ b/src/port/stm32h563/Makefile @@ -35,6 +35,18 @@ ENABLE_MQTT_BROKER ?= 0 # wolfBoot update partition. TZEN=0 only. ENABLE_TFTP ?= 0 +# 802.1Q VLAN sub-interface support. Set ENABLE_VLAN=1 to enable +# WOLFIP_VLAN at compile time and run all DHCP/TFTP/etc. traffic over +# a VLAN sub-interface with VID = VLAN_VID. Static IP configuration on +# the sub-interface is selected via VLAN_IP / VLAN_MASK / VLAN_GW; the +# physical interface stays untagged with no IP. +ENABLE_VLAN ?= 0 +VLAN_VID ?= 100 +VLAN_PCP ?= 0 +VLAN_IP ?= 10.10.100.2 +VLAN_MASK ?= 255.255.255.0 +VLAN_GW ?= 10.10.100.1 + # FreeRTOS integration: set FREERTOS=1 to run the HTTPS server from a # FreeRTOS task using the blocking BSD socket wrapper layer. FREERTOS ?= 0 @@ -360,6 +372,18 @@ SRCS += $(ROOT)/src/tftp/wolftftp.c endif # ENABLE_TFTP +# ----------------------------------------------------------------------------- +# 802.1Q VLAN +# ----------------------------------------------------------------------------- +ifeq ($(ENABLE_VLAN),1) +CFLAGS += -DENABLE_VLAN -DWOLFIP_VLAN=1 +# Need room for 1 loopback + 1 physical + at least 1 VLAN sub-iface. +# wolfIP default WOLFIP_MAX_INTERFACES is 2; bump to 6 to leave headroom. +CFLAGS += -DWOLFIP_MAX_INTERFACES=6 +CFLAGS += -DVLAN_VID=$(VLAN_VID) -DVLAN_PCP=$(VLAN_PCP) +CFLAGS += -DVLAN_IP=\"$(VLAN_IP)\" -DVLAN_MASK=\"$(VLAN_MASK)\" -DVLAN_GW=\"$(VLAN_GW)\" +endif + # ----------------------------------------------------------------------------- # Build rules # ----------------------------------------------------------------------------- diff --git a/src/port/stm32h563/config.h b/src/port/stm32h563/config.h index 2b2e294c..d287c821 100644 --- a/src/port/stm32h563/config.h +++ b/src/port/stm32h563/config.h @@ -52,6 +52,29 @@ #define WOLFIP_ENABLE_DHCP 1 #endif +/* 802.1Q VLAN sub-interface support. Off by default; enable on the make + * command line with ENABLE_VLAN=1 (the Makefile then passes -DWOLFIP_VLAN=1 + * and bumps WOLFIP_MAX_INTERFACES so the sub-interface fits). + * + * The capacity check accounts for 1 physical + (loopback ? 1 : 0) + + * WOLFIP_VLAN_MAX sub-interface slots. */ +#ifndef WOLFIP_VLAN +#define WOLFIP_VLAN 0 +#endif +#ifndef WOLFIP_VLAN_MAX +#define WOLFIP_VLAN_MAX 4 +#endif +#if WOLFIP_VLAN +#if WOLFIP_ENABLE_LOOPBACK +#define WOLFIP_VLAN_RESERVED_SLOTS 2 +#else +#define WOLFIP_VLAN_RESERVED_SLOTS 1 +#endif +#if (WOLFIP_MAX_INTERFACES < (WOLFIP_VLAN_RESERVED_SLOTS + WOLFIP_VLAN_MAX)) +#error "WOLFIP_VLAN requires WOLFIP_MAX_INTERFACES >= 1 (physical) + (WOLFIP_ENABLE_LOOPBACK ? 1 : 0) + WOLFIP_VLAN_MAX" +#endif +#endif + /* Static IP fallback (used when DHCP is disabled or times out) */ #define WOLFIP_IP "192.168.12.11" #define WOLFIP_NETMASK "255.255.255.0" diff --git a/src/port/stm32h563/main.c b/src/port/stm32h563/main.c index 79534278..c700526f 100644 --- a/src/port/stm32h563/main.c +++ b/src/port/stm32h563/main.c @@ -886,6 +886,44 @@ int main(void) uart_puts("\n"); } +#ifdef ENABLE_VLAN + /* 802.1Q VLAN sub-interface: create a logical interface on top of the + * physical (untagged) interface and run all traffic through it. The + * physical interface stays without an IP; sockets bound on the VLAN + * IP will automatically tag outgoing frames and accept incoming frames + * matching the configured VID. */ + { + unsigned int vlan_idx = 0; + ip4 vip = atoip4(VLAN_IP); + ip4 vnm = atoip4(VLAN_MASK); + ip4 vgw = atoip4(VLAN_GW); + int v_ret; + + uart_puts("Creating VLAN sub-interface VID="); + uart_putdec((uint32_t)(VLAN_VID)); + uart_puts(" PCP="); + uart_putdec((uint32_t)(VLAN_PCP)); + uart_puts(" on physical if 0\n"); + v_ret = wolfIP_vlan_create(IPStack, 0, (uint16_t)(VLAN_VID), + (uint8_t)(VLAN_PCP), 0, &vlan_idx); + if (v_ret < 0) { + uart_puts(" ERROR: wolfIP_vlan_create failed (-"); + uart_putdec((uint32_t)(-v_ret)); + uart_puts(")\n"); + } else { + uart_puts(" VLAN sub-iface at idx "); + uart_putdec((uint32_t)vlan_idx); + uart_puts("\n IP: "); + uart_putip4(vip); + uart_puts("\n Mask: "); + uart_putip4(vnm); + uart_puts("\n GW: "); + uart_putip4(vgw); + uart_puts("\n"); + wolfIP_ipconfig_set_ex(IPStack, vlan_idx, vip, vnm, vgw); + } + } +#else /* ENABLE_VLAN */ #ifdef DHCP { uint32_t dhcp_start_tick; @@ -958,6 +996,7 @@ int main(void) wolfIP_ipconfig_set(IPStack, ip, nm, gw); } #endif +#endif /* ENABLE_VLAN */ #ifdef WOLFIP_USE_FREERTOS uart_puts("Starting FreeRTOS BSD socket layer...\n"); diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index f17ac2e6..604476c5 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -36,6 +36,7 @@ #include "unit_tests_ip_arp_recv.c" #include "unit_tests_dns_edges.c" #include "unit_tests_misc_edges.c" +#include "unit_tests_vlan.c" Suite *wolf_suite(void) { @@ -1481,6 +1482,42 @@ Suite *wolf_suite(void) #endif /* WOLFIP_PACKET_SOCKETS */ tcase_add_test(tc_core, test_bind_port_in_use_different_ips_no_collision); +#if WOLFIP_VLAN + /* --- unit_tests_vlan.c (30 tests for 802.1Q support) --- */ + tcase_add_test(tc_proto, test_vlan_api_create_basic); + tcase_add_test(tc_proto, test_vlan_api_create_vid_max_ok); + tcase_add_test(tc_proto, test_vlan_api_create_vid_4095_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_vid_above_max_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_pcp_above_7_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_dei_above_1_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_duplicate_vid_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_same_vid_two_parents_ok); + tcase_add_test(tc_proto, test_vlan_api_create_parent_not_physical_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_exhausts_max); + tcase_add_test(tc_proto, test_vlan_api_create_uninitialized_parent_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_loopback_parent_rejected); + tcase_add_test(tc_proto, test_vlan_api_create_null_args_rejected); + tcase_add_test(tc_proto, test_vlan_api_delete_basic); + tcase_add_test(tc_proto, test_vlan_api_delete_physical_rejected); + tcase_add_test(tc_proto, test_vlan_api_delete_bad_ifidx_rejected); + tcase_add_test(tc_proto, test_vlan_api_get_null_args_rejected); + tcase_add_test(tc_proto, test_vlan_tx_tag_inserted); + tcase_add_test(tc_proto, test_vlan_tx_pcp_and_dei_encoded); + tcase_add_test(tc_proto, test_vlan_tx_vid_zero_priority_tag); + tcase_add_test(tc_proto, test_vlan_tx_vid_4094_encoded); + tcase_add_test(tc_proto, test_vlan_tx_oversize_rejected); + tcase_add_test(tc_proto, test_vlan_tx_runt_rejected); + tcase_add_test(tc_proto, test_vlan_rx_tagged_match_delivered); + tcase_add_test(tc_proto, test_vlan_rx_tagged_mismatch_dropped); + tcase_add_test(tc_proto, test_vlan_rx_untagged_on_physical_ok); + tcase_add_test(tc_proto, test_vlan_rx_runt_tagged_dropped); + tcase_add_test(tc_proto, test_vlan_rx_multiple_subs_correct_dispatch); + tcase_add_test(tc_proto, test_vlan_rx_delete_then_dropped); + tcase_add_test(tc_proto, test_vlan_rx_dei_bit_accepted); + tcase_add_test(tc_proto, test_vlan_rx_tagged_arp_processed); + tcase_add_test(tc_proto, test_vlan_mtu_inherited); +#endif /* WOLFIP_VLAN */ + suite_add_tcase(s, tc_core); suite_add_tcase(s, tc_utils); suite_add_tcase(s, tc_proto); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index 928a59d0..21ddd742 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -29,7 +29,12 @@ #undef WOLFIP_PACKET_SOCKETS #define WOLFIP_PACKET_SOCKETS 1 #undef WOLFIP_MAX_INTERFACES +#if WOLFIP_VLAN +/* Need room for 1 loopback + 1 physical + multiple VLAN sub-ifaces. */ +#define WOLFIP_MAX_INTERFACES 6 +#else #define WOLFIP_MAX_INTERFACES 3 +#endif #undef WOLFIP_ENABLE_LOOPBACK #define WOLFIP_ENABLE_LOOPBACK 1 #undef WOLFIP_ENABLE_FORWARDING diff --git a/src/test/unit/unit_tests_vlan.c b/src/test/unit/unit_tests_vlan.c new file mode 100644 index 00000000..28a01378 --- /dev/null +++ b/src/test/unit/unit_tests_vlan.c @@ -0,0 +1,1109 @@ +/* unit_tests_vlan.c + * + * Copyright (C) 2024 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#if WOLFIP_VLAN + +/* ========================================================================= + * Environment note + * ========================================================================= + * When WOLFIP_VLAN=1, unit_shared.c bumps WOLFIP_MAX_INTERFACES to 6 so that + * a loopback + a physical parent + WOLFIP_VLAN_MAX (= 4) simultaneously live + * VLAN sub-interfaces fit. Slot layout after wolfIP_init() + mock_link_init(): + * slot 0 (TEST_LOOPBACK_IF): loopback (poll/send set by wolfIP_init) + * slot 1 (TEST_PRIMARY_IF): mock physical (poll/send set by mock_link_init) + * slots 2..5: free (poll=NULL, send=NULL, vlan_active=0); + * the first four wolfIP_vlan_create() calls land + * here in slot-reuse order. + * test_vlan_api_create_exhausts_max exercises the WOLFIP_VLAN_MAX cap by + * filling all four free slots before expecting -WOLFIP_EINVAL. + */ + +/* ========================================================================= + * Local helpers + * ========================================================================= */ + +/* Base IPs used throughout */ +#define VLAN_PHYS_IP 0x0A0A0A02U /* 10.10.10.2 — physical parent */ +#define VLAN_SUB100_IP 0x0A0A6402U /* 10.10.100.2 — VID 100 sub-iface */ +#define VLAN_SUB200_IP 0x0A0AC802U /* 10.10.200.2 — VID 200 sub-iface */ +#define VLAN_REMOTE_IP 0x0A0A6401U /* 10.10.100.1 — remote peer on VID 100 */ + +/* MAC used by the synthetic "remote" sender in injected frames */ +static const uint8_t vlan_remote_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xF1}; + +/* Initialise the stack with only the primary physical interface. + * TEST_SECOND_IF remains free and can be claimed by wolfIP_vlan_create. */ +static void setup_vlan_stack(struct wolfIP *s) +{ + wolfIP_init(s); + mock_link_init(s); + wolfIP_ipconfig_set_ex(s, TEST_PRIMARY_IF, VLAN_PHYS_IP, 0xFFFFFF00U, 0); + last_frame_sent_size = 0; +} + +/* Compute the ICMP checksum over 'len' bytes starting at icmp->type. + * Mirrors icmp_checksum() in wolfip.c. */ +static uint16_t vlan_icmp_checksum(struct wolfIP_icmp_packet *icmp, uint16_t len) +{ + uint32_t sum = 0; + uint32_t i; + const uint8_t *ptr = (const uint8_t *)(&icmp->type); + uint16_t word; + + for (i = 0; i < (uint32_t)(len & ~1u); i += 2) { + memcpy(&word, ptr + i, sizeof(word)); + sum += ee16(word); + } + if (len & 0x01u) { + uint16_t spare = (uint16_t)((uint16_t)ptr[len - 1] << 8); + sum += spare; + } + while (sum >> 16) + sum = (sum & 0xffff) + (sum >> 16); + return (uint16_t)(~sum & 0xFFFF); +} + +/* Build a minimal ICMP echo-request frame (untagged) addressed to dst_ip + * from the vlan_remote_mac/remote_ip pair, using the ll->mac as eth dst. + * buf must be at least ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_HEADER_LEN bytes. */ +static uint32_t build_icmp_echo_request(uint8_t *buf, uint32_t bufsz, + const uint8_t *eth_dst_mac, + ip4 src_ip, ip4 dst_ip) +{ + struct wolfIP_icmp_packet *icmp; + uint32_t frame_len; + uint16_t ip_len; + + frame_len = (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_HEADER_LEN); + if (bufsz < frame_len) + return 0; + memset(buf, 0, frame_len); + + /* Ethernet header */ + memcpy(buf, eth_dst_mac, 6); /* dst */ + memcpy(buf + 6, vlan_remote_mac, 6); /* src */ + buf[12] = 0x08; buf[13] = 0x00; /* ethertype = IPv4 */ + + icmp = (struct wolfIP_icmp_packet *)buf; + ip_len = (uint16_t)(IP_HEADER_LEN + ICMP_HEADER_LEN); + + /* IP header */ + icmp->ip.ver_ihl = 0x45; + icmp->ip.tos = 0; + icmp->ip.len = ee16(ip_len); + icmp->ip.id = 0; + icmp->ip.flags_fo = 0; + icmp->ip.ttl = 64; + icmp->ip.proto = 0x01; /* ICMP */ + icmp->ip.csum = 0; + icmp->ip.src = ee32(src_ip); + icmp->ip.dst = ee32(dst_ip); + iphdr_set_checksum(&icmp->ip); + + /* ICMP echo request */ + icmp->type = 8; /* ICMP_ECHO_REQUEST */ + icmp->code = 0; + icmp->csum = 0; + memset(icmp->unused, 0, 4); /* id=0, seq=0 */ + icmp->csum = ee16(vlan_icmp_checksum(icmp, ICMP_HEADER_LEN)); + + return frame_len; +} + +/* Build a tagged ICMP echo-request frame by inserting a 4-byte 802.1Q tag + * into an already-built untagged frame. + * dst_buf must be at least src_len + 4 bytes. + * Returns new length (src_len + 4). */ +static uint32_t insert_vlan_tag(uint8_t *dst_buf, uint32_t dst_bufsz, + const uint8_t *src_buf, uint32_t src_len, + uint16_t vid, uint8_t pcp, uint8_t dei) +{ + uint16_t tpid, tci; + uint32_t tagged_len; + + tagged_len = src_len + 4u; + if (dst_bufsz < tagged_len || src_len < (uint32_t)ETH_HEADER_LEN) + return 0; + + /* Copy dst+src MAC (12 bytes) */ + memcpy(dst_buf, src_buf, 12); + /* TPID = 0x8100 big-endian */ + tpid = ee16(0x8100u); + memcpy(dst_buf + 12, &tpid, 2); + /* TCI: pcp(3) | dei(1) | vid(12) */ + tci = ee16((uint16_t)(((uint16_t)(pcp & 0x7u) << 13) + | ((uint16_t)(dei & 0x1u) << 12) + | (vid & 0x0FFFu))); + memcpy(dst_buf + 14, &tci, 2); + /* Remaining payload (inner ethertype + IP + ...) */ + memcpy(dst_buf + 16, src_buf + 12, src_len - 12); + return tagged_len; +} + +/* Inject a tagged ICMP echo request onto the parent physical interface and + * return the frame length sent (from last_frame_sent_size after the call). + * Returns 0 if wolfIP_recv_on produced no reply. */ +static uint32_t inject_tagged_icmp_echo(struct wolfIP *s, unsigned int parent_idx, + const uint8_t *parent_mac, + ip4 src_ip, ip4 dst_ip, + uint16_t vid, uint8_t pcp, uint8_t dei) +{ + uint8_t plain[ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_HEADER_LEN]; + uint8_t tagged[ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_HEADER_LEN + 4]; + uint32_t plain_len, tagged_len; + + plain_len = build_icmp_echo_request(plain, sizeof(plain), + parent_mac, src_ip, dst_ip); + if (!plain_len) + return 0; + tagged_len = insert_vlan_tag(tagged, sizeof(tagged), plain, plain_len, + vid, pcp, dei); + if (!tagged_len) + return 0; + + last_frame_sent_size = 0; + wolfIP_recv_on(s, parent_idx, tagged, tagged_len); + return last_frame_sent_size; +} + +/* ========================================================================= + * 1. API edge tests + * ========================================================================= */ + +START_TEST(test_vlan_api_create_basic) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + unsigned int got_parent = 0; + uint16_t got_vid = 0; + uint8_t got_pcp = 0xFF, got_dei = 0xFF; + int ret; + + setup_vlan_stack(&s); + + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 3, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + ck_assert_uint_ne(sub_idx, 0xFFFFFFFFu); + ck_assert_uint_ne(sub_idx, TEST_PRIMARY_IF); + + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, &got_vid, &got_pcp, &got_dei); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(got_parent, TEST_PRIMARY_IF); + ck_assert_uint_eq(got_vid, 100); + ck_assert_uint_eq(got_pcp, 3); + ck_assert_uint_eq(got_dei, 0); +} +END_TEST + +START_TEST(test_vlan_api_create_vid_max_ok) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 4094, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + ck_assert_uint_ne(sub_idx, 0xFFFFFFFFu); +} +END_TEST + +START_TEST(test_vlan_api_create_vid_4095_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 4095, 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_create_vid_above_max_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 0xFFFF, 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_create_pcp_above_7_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 8, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_create_dei_above_1_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 2, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_create_duplicate_vid_rejected) +{ + struct wolfIP s; + unsigned int sub_idx1 = 0xFFFFFFFFu, sub_idx2 = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx1); + ck_assert_int_eq(ret, 0); + + /* Same VID on same parent must be rejected */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 1, 0, &sub_idx2); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +/* test_vlan_api_create_same_vid_two_parents_ok: + * The mock harness exposes one physical parent (TEST_PRIMARY_IF), so we + * exercise the per-parent VID-uniqueness check via slot reuse: create VID=100 + * on the primary, delete it, then re-create VID=100 on the same primary. + * This confirms the duplicate check is scoped per-parent (a deleted slot + * frees up the VID) and that slot reuse works. A full two-physical-parents + * variant would require a second mock physical interface, which the current + * harness does not provide. */ +START_TEST(test_vlan_api_create_same_vid_two_parents_ok) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + + /* Create VID=100 on primary */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Delete it */ + ret = wolfIP_vlan_delete(&s, sub_idx); + ck_assert_int_eq(ret, 0); + + /* Slot is reused: re-creating VID=100 on the same parent succeeds */ + sub_idx = 0xFFFFFFFFu; + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + ck_assert_uint_ne(sub_idx, 0xFFFFFFFFu); +} +END_TEST + +START_TEST(test_vlan_api_create_parent_not_physical_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + unsigned int child_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + + /* Create a sub-interface */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Attempting to create a child of that sub-interface must be rejected */ + ret = wolfIP_vlan_create(&s, sub_idx, 200, 0, 0, &child_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +/* test_vlan_api_create_exhausts_max: + * Create WOLFIP_VLAN_MAX sub-interfaces, then expect the next create to fail + * with -WOLFIP_EINVAL. This exercises the cap on simultaneously-live VLANs. */ +START_TEST(test_vlan_api_create_exhausts_max) +{ + struct wolfIP s; + unsigned int sub_idx; + int ret; + unsigned int i; + + setup_vlan_stack(&s); + + /* Fill up to WOLFIP_VLAN_MAX VLAN slots; each must succeed. */ + for (i = 0; i < WOLFIP_VLAN_MAX; i++) { + sub_idx = 0xFFFFFFFFu; + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, + (uint16_t)(100u + i), 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + ck_assert_uint_ne(sub_idx, 0xFFFFFFFFu); + } + + /* The next create must fail — VLAN_MAX is reached (or no free slot). */ + sub_idx = 0xFFFFFFFFu; + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, + (uint16_t)(100u + WOLFIP_VLAN_MAX), + 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +/* The parent slot must be an initialized Ethernet device. An empty + * pre-allocated ll_dev[] slot (send=NULL, poll=NULL, zero MAC) is not a + * real link, so creating a VLAN on it would produce a sub-interface that + * could never transmit. */ +START_TEST(test_vlan_api_create_uninitialized_parent_rejected) +{ + struct wolfIP s; + struct wolfIP_ll_dev *empty; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + + /* TEST_SECOND_IF (= 2 with loopback) is left untouched by setup_vlan_stack: + * its send/poll are NULL after wolfIP_init. Sanity-check, then try to + * use it as a parent. */ + empty = wolfIP_getdev_ex(&s, TEST_SECOND_IF); + ck_assert_ptr_nonnull(empty); + ck_assert_ptr_null(empty->send); + ck_assert_ptr_null(empty->poll); + + ret = wolfIP_vlan_create(&s, TEST_SECOND_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +/* VLAN is an IEEE 802.3 (Ethernet) feature; non-ethernet interfaces such as + * the loopback (struct wolfIP_ll_dev.non_ethernet == 1) cannot carry tagged + * frames. The API must reject such parents. */ +START_TEST(test_vlan_api_create_loopback_parent_rejected) +{ +#if WOLFIP_ENABLE_LOOPBACK + struct wolfIP s; + struct wolfIP_ll_dev *loop; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + + loop = wolfIP_getdev_ex(&s, TEST_LOOPBACK_IF); + ck_assert_ptr_nonnull(loop); + /* wolfIP_init configures the loopback slot with non_ethernet=1 and a + * loopback send callback; sanity-check before exercising the path. */ + ck_assert_uint_eq((unsigned)loop->non_ethernet, 1u); + ck_assert_ptr_nonnull(loop->send); + + ret = wolfIP_vlan_create(&s, TEST_LOOPBACK_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +#endif +} +END_TEST + +START_TEST(test_vlan_api_create_null_args_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + + /* Null stack */ + ret = wolfIP_vlan_create(NULL, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + /* Null out_if_idx */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, NULL); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + /* Bad parent index (out of range) */ + ret = wolfIP_vlan_create(&s, 0xFFFFFFFFu, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_delete_basic) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + unsigned int sub_idx2 = 0xFFFFFFFFu; + unsigned int got_parent; + uint16_t got_vid; + uint8_t got_pcp, got_dei; + int ret; + + setup_vlan_stack(&s); + + /* Create VID=100 */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Delete it */ + ret = wolfIP_vlan_delete(&s, sub_idx); + ck_assert_int_eq(ret, 0); + + /* wolfIP_vlan_get must now fail on the deleted index */ + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, &got_vid, &got_pcp, &got_dei); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + /* Re-creating the same VID must succeed (slot reuse) */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx2); + ck_assert_int_eq(ret, 0); +} +END_TEST + +START_TEST(test_vlan_api_delete_physical_rejected) +{ + struct wolfIP s; + int ret; + + setup_vlan_stack(&s); + + /* Physical interfaces do not have vlan_active set; delete must fail */ + ret = wolfIP_vlan_delete(&s, TEST_PRIMARY_IF); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_delete_bad_ifidx_rejected) +{ + struct wolfIP s; + int ret; + + setup_vlan_stack(&s); + + /* Out-of-range index */ + ret = wolfIP_vlan_delete(&s, 0xFFFFFFFFu); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + /* Null stack */ + ret = wolfIP_vlan_delete(NULL, TEST_PRIMARY_IF); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_vlan_api_get_null_args_rejected) +{ + struct wolfIP s; + unsigned int sub_idx = 0xFFFFFFFFu; + unsigned int got_parent; + uint16_t got_vid; + uint8_t got_pcp, got_dei; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Null stack */ + ret = wolfIP_vlan_get(NULL, sub_idx, &got_parent, &got_vid, &got_pcp, &got_dei); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + /* Null output pointers */ + ret = wolfIP_vlan_get(&s, sub_idx, NULL, &got_vid, &got_pcp, &got_dei); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, NULL, &got_pcp, &got_dei); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, &got_vid, NULL, &got_dei); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, &got_vid, &got_pcp, NULL); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); +} +END_TEST + +/* ========================================================================= + * 2. TX tagging tests + * ========================================================================= */ + +/* Build a minimal untagged Ethernet frame (60 bytes) with inner ethertype + * 0x0800 and a dummy payload, then send it via the sub-interface index. */ +START_TEST(test_vlan_tx_tag_inserted) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t buf[60]; + uint16_t tci_on_wire; + uint16_t tci_expected; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Give the parent a mock_send function so wolfIP_ll_send_frame works */ + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + phys->mtu = LINK_MTU; + + /* Build a 60-byte frame: dst+src MAC, ethertype 0x0800, zero payload */ + memset(buf, 0, sizeof(buf)); + memcpy(buf, phys->mac, 6); /* dst */ + memcpy(buf + 6, vlan_remote_mac, 6); /* src */ + buf[12] = 0x08; buf[13] = 0x00; /* inner ethertype */ + buf[14] = 0xDE; buf[15] = 0xAD; /* start of payload */ + + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, sizeof(buf)); + ck_assert_int_ge(ret, 0); + + /* Tagged frame = original 60 bytes + 4-byte tag = 64 bytes */ + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 64u); + + /* Bytes [12..13] must be TPID = 0x81 0x00 */ + ck_assert_uint_eq(last_frame_sent[12], 0x81u); + ck_assert_uint_eq(last_frame_sent[13], 0x00u); + + /* Bytes [14..15] = TCI; VID=100 (0x64), PCP=0, DEI=0 */ + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + tci_expected = ee16(100u); /* big-endian 0x0064 */ + ck_assert_uint_eq(tci_on_wire, tci_expected); + + /* Bytes [16..17] must be original inner ethertype 0x0800 */ + ck_assert_uint_eq(last_frame_sent[16], 0x08u); + ck_assert_uint_eq(last_frame_sent[17], 0x00u); + + /* Payload byte [18] == original buf[14] = 0xDE */ + ck_assert_uint_eq(last_frame_sent[18], 0xDEu); +} +END_TEST + +START_TEST(test_vlan_tx_pcp_and_dei_encoded) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t buf[60]; + uint16_t tci_on_wire; + uint16_t tci_expected; + int ret; + + setup_vlan_stack(&s); + /* PCP=7, DEI=1, VID=100: TCI = (7<<13)|(1<<12)|100 = 0xF064 */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 7, 1, &sub_idx); + ck_assert_int_eq(ret, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + phys->mtu = LINK_MTU; + + memset(buf, 0, sizeof(buf)); + memcpy(buf, phys->mac, 6); + memcpy(buf + 6, vlan_remote_mac, 6); + buf[12] = 0x08; buf[13] = 0x00; + + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, sizeof(buf)); + ck_assert_int_ge(ret, 0); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 64u); + + /* TCI on wire (big-endian): 0xF064 */ + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + tci_expected = ee16(0xF064u); + ck_assert_uint_eq(tci_on_wire, tci_expected); +} +END_TEST + +START_TEST(test_vlan_tx_vid_zero_priority_tag) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t buf[60]; + uint16_t tci_on_wire, tci_expected; + int ret; + + setup_vlan_stack(&s); + /* VID=0, PCP=5, DEI=0: TCI = (5<<13)|0 = 0xA000 */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 0, 5, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + phys->mtu = LINK_MTU; + + memset(buf, 0, sizeof(buf)); + memcpy(buf, phys->mac, 6); + memcpy(buf + 6, vlan_remote_mac, 6); + buf[12] = 0x08; buf[13] = 0x00; + + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, sizeof(buf)); + ck_assert_int_ge(ret, 0); + + /* TCI low 12 bits == 0 (VID=0), PCP=5 in high 3 bits */ + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + tci_expected = ee16(0xA000u); + ck_assert_uint_eq(tci_on_wire, tci_expected); +} +END_TEST + +START_TEST(test_vlan_tx_vid_4094_encoded) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t buf[60]; + uint16_t tci_on_wire, tci_expected; + int ret; + + setup_vlan_stack(&s); + /* VID=4094 (0xFFE), PCP=0, DEI=0: TCI low 12 bits = 0xFFE */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 4094, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + phys->mtu = LINK_MTU; + + memset(buf, 0, sizeof(buf)); + memcpy(buf, phys->mac, 6); + memcpy(buf + 6, vlan_remote_mac, 6); + buf[12] = 0x08; buf[13] = 0x00; + + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, sizeof(buf)); + ck_assert_int_ge(ret, 0); + + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + tci_expected = ee16(0x0FFEu); + ck_assert_uint_eq(tci_on_wire, tci_expected); +} +END_TEST + +START_TEST(test_vlan_tx_oversize_rejected) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + /* Frame large enough that +4 VLAN tag exceeds parent MTU */ + uint8_t buf[LINK_MTU]; + uint32_t send_len; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + phys->mtu = LINK_MTU; + + /* sub-iface MTU = parent_mtu - 4; sending parent_mtu - 3 bytes would + * require parent_mtu + 1 bytes after tagging — must be rejected. */ + send_len = (uint32_t)(wolfIP_ll_frame_mtu(phys) - 3u); + memset(buf, 0, send_len); + memcpy(buf, phys->mac, 6); + memcpy(buf + 6, vlan_remote_mac, 6); + buf[12] = 0x08; buf[13] = 0x00; + + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, send_len); + ck_assert_int_lt(ret, 0); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +START_TEST(test_vlan_tx_runt_rejected) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t buf[ETH_HEADER_LEN]; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + phys->mtu = LINK_MTU; + + /* Send ETH_HEADER_LEN - 1 bytes: too short even for the MAC copy */ + memset(buf, 0, sizeof(buf)); + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, (uint32_t)(ETH_HEADER_LEN - 1)); + ck_assert_int_lt(ret, 0); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +/* ========================================================================= + * 3. RX parsing / stripping tests + * + * Strategy: inject a tagged ICMP echo request onto the physical parent via + * wolfIP_recv_on(). The stack strips the tag, finds the matching sub-iface, + * and calls icmp_input() which sends a reply. The reply goes through + * wolfIP_ll_send_frame() for the sub-iface, which re-inserts the VLAN tag + * and calls mock_send() on the parent — captured in last_frame_sent[]. + * ========================================================================= */ + +START_TEST(test_vlan_rx_tagged_match_delivered) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint16_t tci_on_wire; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Give sub-iface an IP so icmp_input() accepts the echo request */ + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + /* wolfIP_recv_on reads ll->mac for the MAC filter; set parent send */ + phys->send = mock_send; + + last_frame_sent_size = 0; + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, + 100, 0, 0); + + /* A reply must have been sent */ + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + + /* Reply must carry a VLAN tag with TPID=0x8100 */ + ck_assert_uint_eq(last_frame_sent[12], 0x81u); + ck_assert_uint_eq(last_frame_sent[13], 0x00u); + + /* TCI VID field = 100 (0x64), PCP=0, DEI=0 */ + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + ck_assert_uint_eq(ee16(tci_on_wire) & 0x0FFFu, 100u); +} +END_TEST + +START_TEST(test_vlan_rx_tagged_mismatch_dropped) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + /* Sub on VID=100 only */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + last_frame_sent_size = 0; + /* Inject frame tagged with VID=999 — no matching sub-iface */ + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, + 999, 0, 0); + + /* No reply must be produced */ + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +START_TEST(test_vlan_rx_untagged_on_physical_ok) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + ICMP_HEADER_LEN]; + uint32_t frame_len; + + setup_vlan_stack(&s); + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + frame_len = build_icmp_echo_request(frame, sizeof(frame), + phys->mac, + VLAN_REMOTE_IP, VLAN_PHYS_IP); + ck_assert_uint_gt(frame_len, 0u); + + last_frame_sent_size = 0; + wolfIP_recv_on(&s, TEST_PRIMARY_IF, frame, frame_len); + + /* An untagged ICMP echo reply must be sent on the physical interface */ + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + + /* No VLAN tag: ethertype at [12..13] must be 0x0800, NOT 0x8100 */ + ck_assert_uint_eq(last_frame_sent[12], 0x08u); + ck_assert_uint_eq(last_frame_sent[13], 0x00u); +} +END_TEST + +START_TEST(test_vlan_rx_runt_tagged_dropped) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + /* ETH_HEADER_LEN + 1 byte — too short to contain a full VLAN tag */ + uint8_t runt[ETH_HEADER_LEN + 1]; + uint16_t tpid; + + setup_vlan_stack(&s); + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + memset(runt, 0, sizeof(runt)); + memcpy(runt, phys->mac, 6); + memcpy(runt + 6, vlan_remote_mac, 6); + tpid = ee16(0x8100u); + memcpy(runt + 12, &tpid, 2); + runt[14] = 0x00; /* partial TCI — only 1 byte available */ + + last_frame_sent_size = 0; + wolfIP_recv_on(&s, TEST_PRIMARY_IF, runt, sizeof(runt)); + + /* Must be silently dropped; no reply, no crash */ + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +/* test_vlan_rx_multiple_subs_correct_dispatch: + * Verify the RX VID-match logic for two distinct VIDs by exercising them + * sequentially: create VID=100, test RX, delete; create VID=200, send a + * stale VID=100 frame (must be dropped now that no sub-iface owns it), + * then send a VID=200 frame (must be delivered). This drives both the + * "VID matches" and "VID no longer matches" branches of wolfIP_recv_on()'s + * sub-interface lookup. A truly concurrent two-sub variant would be valuable + * as a follow-up but is out of scope for this test. */ +START_TEST(test_vlan_rx_multiple_subs_correct_dispatch) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint16_t tci_on_wire; + int ret; + + setup_vlan_stack(&s); + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + /* --- VID=100 --- */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + + last_frame_sent_size = 0; + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, 100, 0, 0); + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + ck_assert_uint_eq(ee16(tci_on_wire) & 0x0FFFu, 100u); + + /* Tear down VID=100, set up VID=200 in the same slot */ + ret = wolfIP_vlan_delete(&s, sub_idx); + ck_assert_int_eq(ret, 0); + + sub_idx = 0xFFFFFFFFu; + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 200, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB200_IP, 0xFFFFFF00U, 0); + + /* VID=100 frame must now be dropped (no matching sub) */ + last_frame_sent_size = 0; + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB200_IP, 100, 0, 0); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); + + /* VID=200 frame must be delivered */ + last_frame_sent_size = 0; + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB200_IP, 200, 0, 0); + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + ck_assert_uint_eq(ee16(tci_on_wire) & 0x0FFFu, 200u); +} +END_TEST + +START_TEST(test_vlan_rx_delete_then_dropped) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + /* First echo — must produce a reply */ + last_frame_sent_size = 0; + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, 100, 0, 0); + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + + /* Delete the sub-iface */ + ret = wolfIP_vlan_delete(&s, sub_idx); + ck_assert_int_eq(ret, 0); + + /* Second echo — must be dropped */ + last_frame_sent_size = 0; + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, 100, 0, 0); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +START_TEST(test_vlan_rx_dei_bit_accepted) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + int ret; + + setup_vlan_stack(&s); + /* Create sub with DEI=0; incoming frame may have DEI=1 in its TCI — + * the RX dispatch only matches on VID, so it should still be delivered. */ + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + last_frame_sent_size = 0; + /* Inject tagged frame with DEI=1 — VID still matches */ + inject_tagged_icmp_echo(&s, TEST_PRIMARY_IF, phys->mac, + VLAN_REMOTE_IP, VLAN_SUB100_IP, + 100, 0, 1 /* dei=1 */); + + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +/* test_vlan_rx_tagged_arp_processed: + * Assumption: the implementation routes arp_recv replies for VLAN sub- + * interfaces through wolfIP_ll_send_frame (which inserts the tag) rather than + * calling sub->ll->send directly (which is NULL for VLAN subs). The parallel + * implementation must ensure this, e.g. by calling wolfIP_ll_send_frame or by + * having arp_recv walk up to the parent and insert the tag itself. */ +START_TEST(test_vlan_rx_tagged_arp_processed) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t plain_arp[sizeof(struct arp_packet)]; + uint8_t tagged_arp[sizeof(struct arp_packet) + 4]; + struct arp_packet *arp; + uint32_t tagged_len; + uint16_t tci_on_wire; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + wolfIP_ipconfig_set_ex(&s, sub_idx, VLAN_SUB100_IP, 0xFFFFFF00U, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + phys->send = mock_send; + + /* Build a tagged ARP request for the sub-iface IP */ + memset(plain_arp, 0, sizeof(plain_arp)); + arp = (struct arp_packet *)plain_arp; + memcpy(arp->eth.dst, phys->mac, 6); + memcpy(arp->eth.src, vlan_remote_mac, 6); + arp->eth.type = ee16(ETH_TYPE_ARP); + arp->htype = ee16(1); + arp->ptype = ee16(0x0800); + arp->hlen = 6; + arp->plen = 4; + arp->opcode = ee16(ARP_REQUEST); + memcpy(arp->sma, vlan_remote_mac, 6); + arp->sip = ee32(VLAN_REMOTE_IP); + memset(arp->tma, 0, 6); + arp->tip = ee32(VLAN_SUB100_IP); + + /* Insert VLAN tag at offset 12 */ + tagged_len = insert_vlan_tag(tagged_arp, sizeof(tagged_arp), + plain_arp, sizeof(plain_arp), 100, 0, 0); + ck_assert_uint_gt(tagged_len, 0u); + + last_frame_sent_size = 0; + wolfIP_recv_on(&s, TEST_PRIMARY_IF, tagged_arp, tagged_len); + + /* ARP reply must have been sent */ + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + + /* Reply must be tagged with VID=100 */ + ck_assert_uint_eq(last_frame_sent[12], 0x81u); + ck_assert_uint_eq(last_frame_sent[13], 0x00u); + memcpy(&tci_on_wire, &last_frame_sent[14], 2); + ck_assert_uint_eq(ee16(tci_on_wire) & 0x0FFFu, 100u); +} +END_TEST + +/* ========================================================================= + * 4. MTU test + * ========================================================================= */ + +START_TEST(test_vlan_mtu_inherited) +{ + struct wolfIP s; + struct wolfIP_ll_dev *phys, *sub; + unsigned int sub_idx = 0xFFFFFFFFu; + uint32_t phys_mtu, sub_mtu; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + phys = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(phys); + sub = wolfIP_getdev_ex(&s, sub_idx); + ck_assert_ptr_nonnull(sub); + + phys_mtu = wolfIP_ll_frame_mtu(phys); + sub_mtu = wolfIP_ll_frame_mtu(sub); + + /* Sub-interface MTU must be exactly 4 bytes less than parent MTU */ + ck_assert_uint_eq(sub_mtu + 4u, phys_mtu); +} +END_TEST + +#endif /* WOLFIP_VLAN */ diff --git a/src/wolfip.c b/src/wolfip.c index db8fcb25..36d61ebd 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -126,6 +126,12 @@ struct wolfIP_icmp_packet; #define ETH_TYPE_IP 0x0800 #define ETH_TYPE_ARP 0x0806 +#if WOLFIP_VLAN +#define ETH_TYPE_VLAN_8021Q 0x8100 +#define WOLFIP_VLAN_TAG_LEN 4 +#define WOLFIP_VLAN_VID_MAX 4094 +#define WOLFIP_VLAN_PCP_MAX 7 +#endif #define NO_TIMER 0 @@ -1406,6 +1412,14 @@ static inline uint32_t wolfIP_ll_frame_mtu(const struct wolfIP_ll_dev *ll) { uint32_t mtu; +#if WOLFIP_VLAN + if (ll && ll->vlan_active && ll->vlan_parent) { + uint32_t pmtu = wolfIP_ll_frame_mtu(ll->vlan_parent); + return (pmtu > WOLFIP_VLAN_TAG_LEN) + ? (pmtu - WOLFIP_VLAN_TAG_LEN) + : 0U; + } +#endif if (!ll || ll->mtu == 0) return LINK_MTU; mtu = ll->mtu; @@ -1594,8 +1608,17 @@ static inline int wolfIP_ll_send_frame(struct wolfIP *s, unsigned int if_idx, if (!s) return -WOLFIP_EINVAL; ll = wolfIP_ll_at(s, if_idx); - if (!ll || !ll->send) + if (!ll) + return -WOLFIP_EINVAL; +#if WOLFIP_VLAN + /* VLAN sub-ifaces have no send callback of their own; let the VLAN block + * below handle validation and delegation to the parent. */ + if (!ll->vlan_active && !ll->send) + return -WOLFIP_EINVAL; +#else + if (!ll->send) return -WOLFIP_EINVAL; +#endif frame_mtu = wolfIP_ll_frame_mtu(ll); if (len > frame_mtu) return -WOLFIP_EINVAL; @@ -1604,6 +1627,30 @@ static inline int wolfIP_ll_send_frame(struct wolfIP *s, unsigned int if_idx, return -WOLFIP_EINVAL; return ll->send(ll, (uint8_t *)buf + ETH_HEADER_LEN, len - ETH_HEADER_LEN); } +#if WOLFIP_VLAN + if (ll->vlan_active && ll->vlan_parent) { + struct wolfIP_ll_dev *parent = ll->vlan_parent; + uint32_t parent_mtu; + uint16_t tpid, tci; + uint8_t staging[LINK_MTU + WOLFIP_VLAN_TAG_LEN]; + if (len < (uint32_t)ETH_HEADER_LEN) + return -WOLFIP_EINVAL; + if (!parent->send) + return -WOLFIP_EINVAL; + parent_mtu = wolfIP_ll_frame_mtu(parent); + if (len + WOLFIP_VLAN_TAG_LEN > parent_mtu) + return -WOLFIP_EINVAL; + memcpy(staging, buf, 12); /* dst+src MAC */ + tpid = ee16(ETH_TYPE_VLAN_8021Q); + memcpy(staging + 12, &tpid, 2); + tci = ee16((uint16_t)(((uint16_t)(ll->vlan_pcp & 0x7) << 13) + | ((uint16_t)(ll->vlan_dei & 0x1) << 12) + | (ll->vlan_vid & 0x0FFF))); + memcpy(staging + 14, &tci, 2); + memcpy(staging + 16, (uint8_t *)buf + 12, len - 12); + return parent->send(parent, staging, len + WOLFIP_VLAN_TAG_LEN); + } +#endif return ll->send(ll, buf, len); } @@ -8155,12 +8202,10 @@ static void arp_request(struct wolfIP *s, unsigned int if_idx, ip4 tip) memset(arp.tma, 0, 6); arp.tip = ee32(tip); arp_pending_record(s, if_idx, tip); - if (ll->send) { - if (wolfIP_filter_notify_eth(WOLFIP_FILT_SENDING, s, if_idx, &arp.eth, - sizeof(struct arp_packet)) != 0) - return; - ll->send(ll, &arp, sizeof(struct arp_packet)); - } + if (wolfIP_filter_notify_eth(WOLFIP_FILT_SENDING, s, if_idx, &arp.eth, + sizeof(struct arp_packet)) != 0) + return; + wolfIP_ll_send_frame(s, if_idx, &arp, sizeof(struct arp_packet)); } static void arp_recv(struct wolfIP *s, unsigned int if_idx, void *buf, int len) @@ -8215,11 +8260,9 @@ static void arp_recv(struct wolfIP *s, unsigned int if_idx, void *buf, int len) } } eth_output_add_header(s, if_idx, arp->tma, &arp->eth, ETH_TYPE_ARP); - if (ll->send) { - if (wolfIP_filter_notify_eth(WOLFIP_FILT_SENDING, s, if_idx, &arp->eth, len) != 0) - return; - ll->send(ll, buf, len); - } + if (wolfIP_filter_notify_eth(WOLFIP_FILT_SENDING, s, if_idx, &arp->eth, len) != 0) + return; + wolfIP_ll_send_frame(s, if_idx, buf, (uint32_t)len); } else if (arp->opcode == ee16(ARP_REPLY)) { ip4 sip = ee32(arp->sip); @@ -8308,6 +8351,141 @@ struct wolfIP_ll_dev *wolfIP_getdev_ex(struct wolfIP *s, unsigned int if_idx) return wolfIP_ll_at(s, if_idx); } +#if WOLFIP_VLAN +int wolfIP_vlan_create(struct wolfIP *s, unsigned int parent_if_idx, + uint16_t vid, uint8_t pcp, uint8_t dei, + unsigned int *out_if_idx) +{ + struct wolfIP_ll_dev *parent; + struct wolfIP_ll_dev *slot; + unsigned int i; + unsigned int new_idx; + unsigned int vlan_count = 0; + + if (!s || !out_if_idx) return -WOLFIP_EINVAL; + if (parent_if_idx >= s->if_count) return -WOLFIP_EINVAL; + parent = &s->ll_dev[parent_if_idx]; + /* Parent must be a real, initialized Ethernet device: + * - not a VLAN sub-iface (would imply Q-in-Q, unsupported) + * - has a send callback (rejects uninitialized / deleted slots) + * - is an Ethernet device (rejects loopback / non-ethernet — VLAN + * is an IEEE 802.3 concept). */ + if (parent->vlan_parent != NULL || parent->vlan_active) return -WOLFIP_EINVAL; + if (parent->send == NULL) return -WOLFIP_EINVAL; + if (parent->non_ethernet) return -WOLFIP_EINVAL; + if (vid > WOLFIP_VLAN_VID_MAX) return -WOLFIP_EINVAL; + if (pcp > WOLFIP_VLAN_PCP_MAX) return -WOLFIP_EINVAL; + if (dei > 1) return -WOLFIP_EINVAL; + /* Reject duplicate VID on same parent and count active VLANs. */ + for (i = 0; i < s->if_count; i++) { + if (s->ll_dev[i].vlan_active) { + vlan_count++; + if (s->ll_dev[i].vlan_parent == parent + && s->ll_dev[i].vlan_vid == vid) + return -WOLFIP_EINVAL; + } + } + if (vlan_count >= WOLFIP_VLAN_MAX) return -WOLFIP_EINVAL; + /* Find a free slot: a slot with no send/poll function and no vlan_active/parent. + * Deleted VLAN slots and unused pre-allocated physical slots both qualify. + * Skip the parent slot itself. */ + slot = NULL; + new_idx = s->if_count; + for (i = 0; i < s->if_count; i++) { + struct wolfIP_ll_dev *cand = &s->ll_dev[i]; + if (cand == parent) continue; + if (!cand->vlan_active && cand->vlan_parent == NULL + && cand->send == NULL && cand->poll == NULL) { + slot = cand; + new_idx = i; + break; + } + } + if (!slot) return -WOLFIP_EINVAL; + memset(slot, 0, sizeof(*slot)); + memcpy(slot->mac, parent->mac, 6); + /* Build "." without depending on . Truncate parent + * name to leave 1 + up to 4 VID digits + NUL within ifname[16]. */ + { + size_t pn = 0; + size_t cap; + unsigned int v; + char *out = slot->ifname; + cap = sizeof(slot->ifname); + while (pn < cap - 7 && parent->ifname[pn] != '\0') { + out[pn] = parent->ifname[pn]; + pn++; + } + out[pn++] = '.'; + v = (unsigned int)vid; + if (v == 0) { + out[pn++] = '0'; + } else { + char digits[6]; + int nd = 0; + while (v > 0 && nd < 6) { + digits[nd++] = (char)('0' + (v % 10)); + v /= 10; + } + while (nd > 0) + out[pn++] = digits[--nd]; + } + out[pn] = '\0'; + } + slot->non_ethernet = parent->non_ethernet; + slot->mtu = 0; /* MTU derived dynamically from parent via wolfIP_ll_frame_mtu */ + slot->poll = NULL; + slot->send = NULL; + slot->priv = NULL; + slot->vlan_parent = parent; + slot->vlan_vid = vid; + slot->vlan_pcp = pcp; + slot->vlan_dei = dei; + slot->vlan_active = 1; + memset(&s->ipconf[new_idx], 0, sizeof(s->ipconf[new_idx])); + s->ipconf[new_idx].ll = slot; + *out_if_idx = new_idx; + return 0; +} + +int wolfIP_vlan_delete(struct wolfIP *s, unsigned int if_idx) +{ + struct wolfIP_ll_dev *slot; + if (!s) return -WOLFIP_EINVAL; + if (if_idx >= s->if_count) return -WOLFIP_EINVAL; + slot = &s->ll_dev[if_idx]; + if (!slot->vlan_active || !slot->vlan_parent) return -WOLFIP_EINVAL; + /* Wipe the slot so it can be reused. s->if_count is not changed to avoid + * renumbering active sub-ifaces. */ + memset(slot, 0, sizeof(*slot)); + memset(&s->ipconf[if_idx], 0, sizeof(s->ipconf[if_idx])); + return 0; +} + +int wolfIP_vlan_get(struct wolfIP *s, unsigned int if_idx, + unsigned int *parent_if_idx, uint16_t *vid, + uint8_t *pcp, uint8_t *dei) +{ + struct wolfIP_ll_dev *slot; + unsigned int i; + if (!s || !parent_if_idx || !vid || !pcp || !dei) return -WOLFIP_EINVAL; + if (if_idx >= s->if_count) return -WOLFIP_EINVAL; + slot = &s->ll_dev[if_idx]; + if (!slot->vlan_active || !slot->vlan_parent) return -WOLFIP_EINVAL; + *parent_if_idx = 0; + for (i = 0; i < s->if_count; i++) { + if (&s->ll_dev[i] == slot->vlan_parent) { + *parent_if_idx = i; + break; + } + } + *vid = slot->vlan_vid; + *pcp = slot->vlan_pcp; + *dei = slot->vlan_dei; + return 0; +} +#endif /* WOLFIP_VLAN */ + int wolfIP_mtu_set(struct wolfIP *s, unsigned int if_idx, uint32_t mtu) { struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); @@ -8625,6 +8803,38 @@ static void wolfIP_recv_on(struct wolfIP *s, unsigned int if_idx, void *buf, uin #endif /* DEBUG_ETH */ if (wolfIP_filter_notify_eth(WOLFIP_FILT_RECEIVING, s, if_idx, eth, len) != 0) return; +#if WOLFIP_VLAN + if (eth->type == ee16(ETH_TYPE_VLAN_8021Q)) { + uint16_t tci_be, tci, vid; + unsigned int sub_idx; + int found = 0; + /* Require enough bytes for Ethernet header + 4-byte VLAN tag. */ + if (len < (uint32_t)(ETH_HEADER_LEN + WOLFIP_VLAN_TAG_LEN)) + return; + memcpy(&tci_be, (uint8_t *)buf + ETH_HEADER_LEN, 2); + tci = ee16(tci_be); + vid = tci & 0x0FFF; + /* Walk sub-interfaces to find a match on this physical link + VID. */ + for (sub_idx = 0; sub_idx < s->if_count; sub_idx++) { + struct wolfIP_ll_dev *cand = &s->ll_dev[sub_idx]; + if (cand->vlan_active && cand->vlan_parent == ll + && cand->vlan_vid == vid) { + found = 1; + break; + } + } + if (!found) + return; /* No matching VLAN sub-interface; drop. */ + /* Strip the 4-byte tag in place: slide MAC headers forward. */ + memmove((uint8_t *)buf + WOLFIP_VLAN_TAG_LEN, buf, 12); + buf = (uint8_t *)buf + WOLFIP_VLAN_TAG_LEN; + len -= WOLFIP_VLAN_TAG_LEN; + /* Rebind dispatch context to the matched sub-interface. */ + eth = (struct wolfIP_eth_frame *)buf; + if_idx = sub_idx; + ll = wolfIP_ll_at(s, if_idx); + } +#endif /* WOLFIP_VLAN */ #if WOLFIP_PACKET_SOCKETS packet_try_recv(s, if_idx, eth, len); #endif @@ -9609,11 +9819,7 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) break; eth_output_add_header(s, tx_if, r->nexthop_mac, &ip->eth, ETH_TYPE_IP); #endif - { - struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, tx_if); - if (ll && ll->send) - ll->send(ll, ip, desc->len); - } + wolfIP_ll_send_frame(s, tx_if, ip, desc->len); fifo_pop(&r->txbuf); desc = fifo_peek(&r->txbuf); (void)nexthop; @@ -9637,11 +9843,7 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) desc = fifo_next(&p->txbuf, desc); continue; } - { - struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, tx_if); - if (ll && ll->send) - ll->send(ll, frame, desc->len); - } + wolfIP_ll_send_frame(s, tx_if, frame, desc->len); fifo_pop(&p->txbuf); desc = fifo_peek(&p->txbuf); } diff --git a/wolfip.h b/wolfip.h index 03b24eba..3aaf36fd 100644 --- a/wolfip.h +++ b/wolfip.h @@ -177,6 +177,15 @@ struct wolfIP_ll_dev { int (*send)(struct wolfIP_ll_dev *ll, void *buf, uint32_t len); /* optional context private pointer */ void *priv; +#if WOLFIP_VLAN + /* 802.1Q VLAN sub-interface descriptor. When vlan_active is 0, this slot + * is either a physical interface or a deleted/empty slot. */ + struct wolfIP_ll_dev *vlan_parent; /* NULL => physical; else points into ll_dev[] */ + uint16_t vlan_vid; /* 0..4094 */ + uint8_t vlan_pcp; /* 0..7 (802.1p priority) */ + uint8_t vlan_dei; /* 0..1 (drop-eligible indicator) */ + uint8_t vlan_active; /* 1 if this slot is a live sub-iface */ +#endif }; /* Struct to contain an IP device configuration */ @@ -397,6 +406,37 @@ void wolfIP_ipconfig_set_ex(struct wolfIP *s, unsigned int if_idx, ip4 ip, ip4 m void wolfIP_ipconfig_get_ex(struct wolfIP *s, unsigned int if_idx, ip4 *ip, ip4 *mask, ip4 *gw); int wolfIP_arp_lookup_ex(struct wolfIP *s, unsigned int if_idx, ip4 ip, uint8_t *mac); +#if WOLFIP_VLAN +/* 802.1Q VLAN sub-interface management. + * + * A VLAN sub-interface is a logical interface that sits on top of a physical + * (untagged) interface. Frames sent out of a sub-interface are 802.1Q-tagged + * with the configured VID/PCP/DEI; frames arriving on the physical interface + * with a matching tag are stripped and delivered as if they had arrived on + * the sub-interface. Each sub-interface gets its own ipconf slot (own IP, + * mask, gateway, DHCP, ARP table behavior). + * + * Returns 0 on success, -WOLFIP_EINVAL on validation failure (null stack, + * bad parent index, parent not physical, VID >= 4095, PCP > 7, DEI > 1, + * duplicate VID on the same parent, no free ll_dev slot, exhausted + * WOLFIP_VLAN_MAX). + */ +int wolfIP_vlan_create(struct wolfIP *s, unsigned int parent_if_idx, + uint16_t vid, uint8_t pcp, uint8_t dei, + unsigned int *out_if_idx); + +/* Remove a VLAN sub-interface. Refuses to delete a physical interface or an + * already-inactive slot. The slot is marked inactive (vlan_active = 0) but + * the index remains valid; subsequent wolfIP_vlan_create may reuse it. */ +int wolfIP_vlan_delete(struct wolfIP *s, unsigned int if_idx); + +/* Query VLAN configuration on a sub-interface. Returns -WOLFIP_EINVAL if + * if_idx is not a live sub-interface or any output pointer is NULL. */ +int wolfIP_vlan_get(struct wolfIP *s, unsigned int if_idx, + unsigned int *parent_if_idx, uint16_t *vid, + uint8_t *pcp, uint8_t *dei); +#endif /* WOLFIP_VLAN */ + /* Callback flags */ #define CB_EVENT_READABLE 0x01 /* Accepted connection or data available */ #define CB_EVENT_TIMEOUT 0x02 /* Timeout */ From 88e8986bb0995f4f25e57346d1c21d56a32f0424 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 14 May 2026 18:13:19 +0200 Subject: [PATCH 2/3] Added missing test script --- tools/scripts/debug-m33mu-vlan-local.sh | 248 ++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100755 tools/scripts/debug-m33mu-vlan-local.sh diff --git a/tools/scripts/debug-m33mu-vlan-local.sh b/tools/scripts/debug-m33mu-vlan-local.sh new file mode 100755 index 00000000..bd8de8e2 --- /dev/null +++ b/tools/scripts/debug-m33mu-vlan-local.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: tools/scripts/debug-m33mu-vlan-local.sh + +Build and run the STM32H563 demo against m33mu over an 802.1Q VLAN. The host +sets up tap0 with a VLAN sub-interface (tap0.) carrying IP 10.10..1. +The firmware (built with ENABLE_VLAN=1) is expected to: + - tag every outgoing frame with TPID=0x8100 and the configured VID, + - accept incoming frames matching the same VID, + - serve the built-in TCP echo service on port 7 over the VLAN. + +Verification: + 1. Connect to :7 from the host over tap0., send a probe string, + and assert it is echoed back byte-for-byte (proves TCP+VLAN+ARP works + in both directions at L4). + 2. Capture a pcap on tap0 (the parent, where tagged frames are visible) + and assert via tshark that 802.1Q-tagged traffic flowed in BOTH + directions on the configured VID. + +Environment: + VLAN_VID=<1-4094> VLAN ID (default: 100) + VLAN_PCP=<0-7> Priority code point (default: 0) + M33MU_TIMEOUT= m33mu run timeout (default: 30) + ECHO_PAYLOAD= Bytes to send through the TCP echo + (default: "hello-vlan-") + +Outputs: + /tmp/m33mu-vlan.log UART/stdout from m33mu + /tmp/m33mu-vlan.pcap Packet capture on tap0 (sees tagged frames) + /tmp/m33mu-vlan-tshark.txt tshark filter output + /tmp/m33mu-vlan-echo.txt Bytes received from the TCP echo +EOF +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "${repo_root}" + +vid="${VLAN_VID:-100}" +pcp="${VLAN_PCP:-0}" +m33mu_timeout="${M33MU_TIMEOUT:-30}" +echo_payload="${ECHO_PAYLOAD:-hello-vlan-$(date +%s)}" + +if ! [[ "${vid}" =~ ^[0-9]+$ ]] || [ "${vid}" -lt 1 ] || [ "${vid}" -gt 4094 ]; then + echo "VLAN_VID must be an integer in [1, 4094]" >&2 + exit 2 +fi + +host_ip="10.10.${vid}.1" +dev_ip="10.10.${vid}.2" +mask="255.255.255.0" +echo_port=7 + +cleanup() { + local rc=$? + set +e + if [ -f /tmp/m33mu-vlan.pid ]; then + sudo kill "$(cat /tmp/m33mu-vlan.pid)" 2>/dev/null || true + fi + if [ -f /tmp/tcpdump-vlan.pid ]; then + sudo kill "$(cat /tmp/tcpdump-vlan.pid)" 2>/dev/null || true + fi + sudo pkill -x m33mu 2>/dev/null || true + sudo ip link del "tap0.${vid}" 2>/dev/null || true + sudo ip link del tap0 2>/dev/null || true + exit "${rc}" +} +trap cleanup EXIT + +rm -f /tmp/m33mu-vlan.log /tmp/m33mu-vlan.pcap /tmp/m33mu-vlan-tshark.txt \ + /tmp/m33mu-vlan-echo.txt /tmp/m33mu-vlan.pid /tmp/tcpdump-vlan.pid + +echo "==> Building STM32H563 firmware with VLAN_VID=${vid} VLAN_PCP=${pcp}" +make -C src/port/stm32h563 clean \ + CC=arm-none-eabi-gcc OBJCOPY=arm-none-eabi-objcopy >/dev/null +make -C src/port/stm32h563 \ + CC=arm-none-eabi-gcc OBJCOPY=arm-none-eabi-objcopy \ + TZEN=0 ENABLE_VLAN=1 \ + VLAN_VID="${vid}" VLAN_PCP="${pcp}" \ + VLAN_IP="${dev_ip}" VLAN_MASK="${mask}" VLAN_GW="${host_ip}" + +echo "==> Setting up tap0 + tap0.${vid} VLAN sub-interface" +# Use ${USER:-root} so the script works both on a multi-user dev box (where +# $USER is set) and inside a GitHub Actions container (where it may not be). +sudo ip tuntap add dev tap0 mode tap user "${USER:-root}" +sudo ip link set tap0 up +sudo ip link add link tap0 name "tap0.${vid}" type vlan id "${vid}" +sudo ip addr add "${host_ip}/24" dev "tap0.${vid}" +sudo ip link set "tap0.${vid}" up + +echo "==> Starting tcpdump on tap0 (parent; sees tagged frames)" +sudo tcpdump -i tap0 -nn -U -w /tmp/m33mu-vlan.pcap > /dev/null 2>&1 & +printf '%s\n' "$!" > /tmp/tcpdump-vlan.pid +sleep 1 + +echo "==> Starting m33mu" +sudo m33mu src/port/stm32h563/app.bin \ + --cpu stm32h563 --tap:tap0 --uart-stdout \ + --timeout "${m33mu_timeout}" --quit-on-faults \ + > /tmp/m33mu-vlan.log 2>&1 & +printf '%s\n' "$!" > /tmp/m33mu-vlan.pid + +echo "==> Waiting for firmware to reach the main loop (TCP echo ready)" +ready=0 +for _ in $(seq 1 $((m33mu_timeout + 5))); do + if grep -q "Entering main loop" /tmp/m33mu-vlan.log 2>/dev/null; then + ready=1 + break + fi + sleep 1 +done + +if [ "${ready}" = "1" ]; then + echo "FW_READY=yes" +else + echo "FW_READY=no" +fi + +echo "==> Probing TCP echo at ${dev_ip}:${echo_port} via tap0.${vid}" +echo_ok=0 +echo_recv="" +if [ "${ready}" = "1" ]; then + # Use bash's /dev/tcp so we don't depend on a particular nc variant. + # Try a few times because the firmware's listen socket may take a moment + # to become accept()-ready after the log message. + for try in 1 2 3 4 5; do + if exec 3<>"/dev/tcp/${dev_ip}/${echo_port}" 2>/dev/null; then + printf '%s\n' "${echo_payload}" >&3 + if IFS= read -r -t 5 echo_recv <&3; then + echo_ok=1 + fi + exec 3<&- 2>/dev/null || true + exec 3>&- 2>/dev/null || true + [ "${echo_ok}" = "1" ] && break + fi + sleep 1 + done +fi +printf '%s\n' "${echo_recv}" > /tmp/m33mu-vlan-echo.txt +echo "ECHO_SENT=${echo_payload}" +echo "ECHO_RECV=${echo_recv}" + +# Give tcpdump a moment to flush +sleep 2 +sudo kill "$(cat /tmp/tcpdump-vlan.pid)" 2>/dev/null || true +sleep 1 + +echo "==> Asserting 802.1Q tagged traffic on VID=${vid}" +fw_mac="" +egress=0 +ingress=0 +tshark_out="" + +# Pull the firmware's Ethernet MAC out of the UART log. The demo prints it +# as a line " MAC: XX:XX:XX:XX:XX:XX" right after eth init. Anchor on the +# " MAC:" prefix so we ignore unrelated lines from m33mu like +# "[ETH_MAC] assigned MAC ..." that emit during boot. +fw_mac="$(sed -nE 's/^[[:space:]]+MAC:[[:space:]]+([0-9A-Fa-f:]+).*/\1/p' \ + /tmp/m33mu-vlan.log \ + | head -n1 \ + | tr 'A-F' 'a-f')" +echo "FW_MAC=${fw_mac:-}" + +if ! command -v tshark >/dev/null 2>&1; then + echo "WARNING: tshark not installed; skipping pcap assertion." >&2 +else + # Tab-separated columns: frame eth.src eth.dst vlan eth.type ip.src ip.dst + # tcp.sport tcp.dport info + tshark_out="$(tshark -r /tmp/m33mu-vlan.pcap -n \ + -Y "vlan.id==${vid}" \ + -T fields \ + -e frame.number -e eth.src -e eth.dst -e vlan.id -e eth.type \ + -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e _ws.col.Info \ + 2>/dev/null || true)" + printf '%s\n' "${tshark_out}" > /tmp/m33mu-vlan-tshark.txt + + if [ -n "${fw_mac}" ] && [ -n "${tshark_out}" ]; then + # tshark -T fields uses tab as the column separator. Field 2 is eth.src + # (egress when == fw_mac), field 3 is eth.dst (ingress when == fw_mac). + # `grep -cFx` exits 1 when no matches; `|| true` keeps set -o pipefail + # from aborting on an empty direction. + egress=$(printf '%s\n' "${tshark_out}" \ + | cut -f2 | tr 'A-F' 'a-f' \ + | grep -cFx "${fw_mac}" || true) + ingress=$(printf '%s\n' "${tshark_out}" \ + | cut -f3 | tr 'A-F' 'a-f' \ + | grep -cFx "${fw_mac}" || true) + fi +fi +echo "EGRESS_FRAMES=${egress} INGRESS_FRAMES=${ingress}" + +echo "--- m33mu log tail ---" +tail -n 60 /tmp/m33mu-vlan.log || true +echo "--- tshark (vlan.id==${vid}) ---" +head -n 20 /tmp/m33mu-vlan-tshark.txt 2>/dev/null || true +echo "--- pcap summary ---" +ls -l /tmp/m33mu-vlan.pcap || true + +rc=0 +if [ "${ready}" != "1" ]; then + echo "FAIL: firmware did not reach main loop within ${m33mu_timeout}s." >&2 + rc=1 +fi +if [ "${echo_ok}" != "1" ]; then + echo "FAIL: TCP echo probe did not receive a response from ${dev_ip}:${echo_port}." >&2 + rc=1 +elif [ "${echo_recv}" != "${echo_payload}" ]; then + echo "FAIL: TCP echo payload mismatch (sent='${echo_payload}', recv='${echo_recv}')." >&2 + rc=1 +else + echo "OK: TCP echo over VLAN returned the expected payload." +fi + +if command -v tshark >/dev/null 2>&1; then + if [ -z "${tshark_out}" ]; then + echo "FAIL: no frames with VID=${vid} captured on tap0." >&2 + rc=1 + else + echo "OK: captured 802.1Q frames with VID=${vid}." + fi + + if [ -z "${fw_mac}" ]; then + echo "FAIL: could not determine firmware MAC from UART log; cannot verify bidirectional VLAN traffic." >&2 + rc=1 + else + if [ "${egress}" -eq 0 ]; then + echo "FAIL: no egress VLAN frames from firmware MAC ${fw_mac} on VID=${vid}." >&2 + rc=1 + else + echo "OK: ${egress} egress VLAN frames from firmware MAC ${fw_mac}." + fi + if [ "${ingress}" -eq 0 ]; then + echo "FAIL: no ingress VLAN frames to firmware MAC ${fw_mac} on VID=${vid}." >&2 + rc=1 + else + echo "OK: ${ingress} ingress VLAN frames to firmware MAC ${fw_mac}." + fi + fi +fi + +exit "${rc}" From 8825f53e89499d72925c884e20bfceb62dd73dd5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 14 May 2026 19:04:11 +0200 Subject: [PATCH 3/3] Addressed copilot's comments --- src/test/unit/unit.c | 2 + src/test/unit/unit_tests_vlan.c | 81 +++++++++++++++++++++++++++++++++ src/wolfip.c | 26 +++++++++-- 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 604476c5..8d579259 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1501,6 +1501,8 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_vlan_api_delete_physical_rejected); tcase_add_test(tc_proto, test_vlan_api_delete_bad_ifidx_rejected); tcase_add_test(tc_proto, test_vlan_api_get_null_args_rejected); + tcase_add_test(tc_proto, test_vlan_api_get_dangling_parent_pointer_rejected); + tcase_add_test(tc_proto, test_vlan_tx_active_without_parent_rejected); tcase_add_test(tc_proto, test_vlan_tx_tag_inserted); tcase_add_test(tc_proto, test_vlan_tx_pcp_and_dei_encoded); tcase_add_test(tc_proto, test_vlan_tx_vid_zero_priority_tag); diff --git a/src/test/unit/unit_tests_vlan.c b/src/test/unit/unit_tests_vlan.c index 28a01378..1bed48b4 100644 --- a/src/test/unit/unit_tests_vlan.c +++ b/src/test/unit/unit_tests_vlan.c @@ -506,6 +506,87 @@ START_TEST(test_vlan_api_delete_bad_ifidx_rejected) } END_TEST +/* Regression: wolfIP_vlan_get used to default *parent_if_idx to 0 if the + * parent pointer didn't match any slot in ll_dev[], silently reporting the + * wrong parent. After the fix it must return -WOLFIP_EINVAL and leave the + * caller's out pointers untouched. */ +START_TEST(test_vlan_api_get_dangling_parent_pointer_rejected) +{ + struct wolfIP s; + struct wolfIP_ll_dev *sub; + unsigned int sub_idx = 0xFFFFFFFFu; + unsigned int got_parent = 0xEEEEEEEEu; + uint16_t got_vid = 0xEEEE; + uint8_t got_pcp = 0xEE, got_dei = 0xEE; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Sanity: get() succeeds while the parent pointer is intact. */ + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, &got_vid, &got_pcp, &got_dei); + ck_assert_int_eq(ret, 0); + ck_assert_uint_eq(got_parent, TEST_PRIMARY_IF); + + /* Corrupt the parent pointer so it no longer matches any slot. */ + sub = wolfIP_getdev_ex(&s, sub_idx); + ck_assert_ptr_nonnull(sub); + sub->vlan_parent = (struct wolfIP_ll_dev *)(uintptr_t)0xDEADBEEFu; + + got_parent = 0xEEEEEEEEu; + got_vid = 0xEEEE; + got_pcp = 0xEE; + got_dei = 0xEE; + ret = wolfIP_vlan_get(&s, sub_idx, &got_parent, &got_vid, &got_pcp, &got_dei); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + /* Output pointers must be untouched on failure. */ + ck_assert_uint_eq(got_parent, 0xEEEEEEEEu); + ck_assert_uint_eq(got_vid, 0xEEEE); + ck_assert_uint_eq(got_pcp, 0xEE); + ck_assert_uint_eq(got_dei, 0xEE); + + /* Restore so the cleanup path doesn't dereference the bogus pointer. */ + sub->vlan_active = 0; + sub->vlan_parent = NULL; +} +END_TEST + +/* Regression: wolfIP_ll_send_frame used to allow vlan_active=1 to bypass + * the !ll->send guard, then -- if vlan_parent was NULL -- fell through to + * `ll->send(...)` and dereferenced a NULL function pointer. The hardened + * guard rejects that inconsistent state explicitly. */ +START_TEST(test_vlan_tx_active_without_parent_rejected) +{ + struct wolfIP s; + struct wolfIP_ll_dev *sub; + unsigned int sub_idx = 0xFFFFFFFFu; + uint8_t buf[60]; + int ret; + + setup_vlan_stack(&s); + ret = wolfIP_vlan_create(&s, TEST_PRIMARY_IF, 100, 0, 0, &sub_idx); + ck_assert_int_eq(ret, 0); + + /* Force the inconsistent state directly: vlan_active=1, vlan_parent=NULL. + * (wolfIP_vlan_create itself never produces this, but defensive code + * must still refuse to send rather than crash.) */ + sub = wolfIP_getdev_ex(&s, sub_idx); + ck_assert_ptr_nonnull(sub); + sub->vlan_parent = NULL; + ck_assert_ptr_null(sub->send); + + memset(buf, 0, sizeof(buf)); + last_frame_sent_size = 0; + ret = wolfIP_ll_send_frame(&s, sub_idx, buf, sizeof(buf)); + ck_assert_int_eq(ret, -WOLFIP_EINVAL); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); + + /* Restore so teardown doesn't trip. */ + sub->vlan_active = 0; +} +END_TEST + START_TEST(test_vlan_api_get_null_args_rejected) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 36d61ebd..38939b1a 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1611,10 +1611,18 @@ static inline int wolfIP_ll_send_frame(struct wolfIP *s, unsigned int if_idx, if (!ll) return -WOLFIP_EINVAL; #if WOLFIP_VLAN - /* VLAN sub-ifaces have no send callback of their own; let the VLAN block - * below handle validation and delegation to the parent. */ - if (!ll->vlan_active && !ll->send) + /* A live VLAN sub-iface delegates transmission to its parent (its own + * send callback is intentionally NULL), so validate the sub-iface state + * instead of the (always-NULL) send pointer. For a physical interface + * we still require a non-NULL send callback. Reject inconsistent state + * (vlan_active=1 with no parent) before it reaches the send path below + * where it would otherwise dereference a NULL function pointer. */ + if (ll->vlan_active) { + if (!ll->vlan_parent) + return -WOLFIP_EINVAL; + } else if (!ll->send) { return -WOLFIP_EINVAL; + } #else if (!ll->send) return -WOLFIP_EINVAL; @@ -8468,17 +8476,25 @@ int wolfIP_vlan_get(struct wolfIP *s, unsigned int if_idx, { struct wolfIP_ll_dev *slot; unsigned int i; + unsigned int parent_idx = 0; + int parent_found = 0; if (!s || !parent_if_idx || !vid || !pcp || !dei) return -WOLFIP_EINVAL; if (if_idx >= s->if_count) return -WOLFIP_EINVAL; slot = &s->ll_dev[if_idx]; if (!slot->vlan_active || !slot->vlan_parent) return -WOLFIP_EINVAL; - *parent_if_idx = 0; + /* The parent pointer must resolve to a slot in s->ll_dev[]. If it + * doesn't, the sub-interface state is inconsistent (programming error + * or memory corruption); fail loudly rather than reporting parent 0. */ for (i = 0; i < s->if_count; i++) { if (&s->ll_dev[i] == slot->vlan_parent) { - *parent_if_idx = i; + parent_idx = i; + parent_found = 1; break; } } + if (!parent_found) + return -WOLFIP_EINVAL; + *parent_if_idx = parent_idx; *vid = slot->vlan_vid; *pcp = slot->vlan_pcp; *dei = slot->vlan_dei;