Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
769d9c5
config: add IPv6 build knobs
danielinux Aug 4, 2026
f8085bc
ipv6: address type, predicates and text conversion
danielinux Aug 4, 2026
bc3cbbb
ipv6: header encapsulation, parsing and pseudo-header checksum
danielinux Aug 4, 2026
44fb91f
tests: requirement tests for unimplemented IPv6 features
danielinux Aug 4, 2026
0b864d1
ci: add IPv6 workflow
danielinux Aug 4, 2026
ac7eff0
dlr: declare the integration surface for a ring implementation
danielinux Aug 4, 2026
9118f8b
docs: IPv6 rows in README and CHANGELOG
danielinux Aug 4, 2026
5654468
ifaddr: several addresses per interface
danielinux Aug 4, 2026
bd0f6cf
udp: honour a wildcard bind with several addresses per interface
danielinux Aug 4, 2026
2c1cb78
tests: make the ip_recv martian-filter tests exercise ip_recv
danielinux Aug 4, 2026
3b01798
icmp6: answer Echo Requests
danielinux Aug 4, 2026
c0ec1ec
test: end-to-end ICMPv6 echo against a real host stack
danielinux Aug 4, 2026
4b6d55e
nd6: Neighbor Discovery, duplicate address detection and host routing
danielinux Aug 4, 2026
6258968
test: SLAAC end to end against Linux, with radvd and capture
danielinux Aug 4, 2026
85a6d68
ci: do not build the wolfSSL-dependent examples in the IPv6 workflow
danielinux Aug 4, 2026
e9abfbc
esp: replace wc_ForceZero with a portable local wipe
danielinux Aug 4, 2026
29b43f2
nd6: fix timer stall, add teardown, drop dead state
danielinux Aug 6, 2026
f23ab9a
nd6: make arming the tick idempotent instead of guarded
danielinux Aug 6, 2026
2b6e57a
config: move IPv6 defaults out of config.h so port builds see them
danielinux Aug 7, 2026
5fc4d2a
docs, test: correct stale IPv6 status and restore host state in the r…
danielinux Aug 7, 2026
96018be
test: print the build configuration from the unit binary
danielinux Aug 7, 2026
03185b3
test: zero the whole frame in build_udp_frame
danielinux Aug 7, 2026
a7d678e
nd6: validate ND messages before acting on them
danielinux Aug 7, 2026
047fa87
test: add adversarial IPv6 validation cases
danielinux Aug 7, 2026
63bbfdb
nd6: reject malformed and misdirected control traffic
danielinux Aug 7, 2026
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
157 changes: 157 additions & 0 deletions .github/workflows/ipv6.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: wolfIP IPv6

on:
push:
branches:
- "**"
pull_request:

jobs:
ipv6-unit:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential check gcovr libwolfssl-dev \
radvd tcpdump iputils-ping

# The IPv6 addressing layer lives in wolfip6.h, which is included
# unconditionally, so it is already covered by the default `make unit`
# in linux.yml. This job covers everything that needs the IPv6 stack
# itself compiled in.
- name: Unit tests with IPv6 enabled
run: |
make clean
make unit-ipv6
./build/test/unit

- name: Unit tests with IPv6 enabled (ASan)
run: |
make clean
make unit-ipv6-asan
./build/test/unit

- name: Unit tests with IPv6 enabled (UBSan)
run: |
make clean
make unit-ipv6-ubsan
./build/test/unit

# The IPv4-only build must be completely unaffected by the IPv6 work.
# This is the regression that matters most.
#
# Builds the library and the unit tests rather than the full `make`.
# The default target also builds the ESP, wolfGuard and supplicant
# example binaries, which need a wolfSSL built from source with the
# right options - linux.yml installs one from the nightly snapshot for
# exactly that reason. Against the apt libwolfssl-dev used here,
# src/wolfesp.c fails on an undeclared wc_ForceZero, which has nothing
# to do with IPv6 and is already covered by linux.yml.
- name: IPv4-only build is unchanged
run: |
make clean
make libwolfip.so
make unit
./build/test/unit

- name: Library builds with IPv6 enabled
run: |
make clean
make libwolfip.so EXTRA_CFLAGS="-DWOLFIP_IPV6=1"

# Every board port ships its own config.h with the same include guard
# and replaces the one at the top of the tree, so a macro defined only
# there is invisible to a port build. This compiles wolfip.c against
# each port's configuration, which is what the embedded jobs would
# otherwise be the first to notice.
- name: wolfip.c builds against every port config
run: |
set -e
for c in src/port/*/config.h; do
d=$(mktemp -d)
cp "$c" "$d/config.h"
echo "-- $(dirname $c)"
gcc -w -I"$d" -I. -D_GNU_SOURCE -c src/wolfip.c -o /dev/null
done

# Regression guard for the wc_ForceZero declaration in wolfip.h. This
# object is what failed here before: it compiles against whichever
# wolfSSL the distribution packages, whose header chain differs from
# the source build linux.yml uses, so it is the one environment that
# catches a missing include.
- name: ESP object builds against the packaged wolfSSL
run: make build/esp/wolfip.o

# Informational, not a gate. IPv6 still carries deliberate stubs, so
# 100% function coverage is not achievable yet; the enforced gate
# remains the one on src/wolfip.c in wolfip-autocov.yml.
- name: IPv6 coverage report
run: |
make clean
make autocov-ipv6
gcovr -r . --exclude "src/test/unit/.*" \
--gcov-ignore-parse-errors=all --json -o build/coverage/ipv6.json
python3 - <<'PY'
import json
with open("build/coverage/ipv6.json", encoding="utf-8") as f:
data = json.load(f)
for name in ("src/wolfip6.c", "wolfip6.h"):
entry = next((e for e in data.get("files", [])
if e.get("file", "").endswith(name)), None)
if entry is None:
print(f"{name}: no coverage data")
continue
fns = entry.get("functions", [])
if not fns:
print(f"{name}: no function data")
continue
covered = sum(1 for fn in fns if fn.get("execution_count", 0) > 0)
print(f"{name}: {covered}/{len(fns)} functions "
f"({covered * 100.0 / len(fns):.2f}%)")
for fn in fns:
if fn.get("execution_count", 0) == 0:
print(" uncovered:", fn.get("name"))
PY

# End-to-end against the Linux stack over a TAP device. Needs root for
# the device, and captures to a pcap so a CI failure can be opened in
# wireshark from the artifacts.
- name: ICMPv6 echo end to end
run: |
make clean
make build/test-ipv6-ping
sudo ./build/test-ipv6-ping --selftest

- name: SLAAC end to end (advertisement injected by the test)
run: |
make build/test-ipv6-slaac
sudo ./build/test-ipv6-slaac --selftest

# The same again, but the Router Advertisement comes from radvd. This
# is the one that proves interoperability with a real router
# implementation rather than with a frame this repository wrote.
- name: SLAAC end to end (radvd)
run: sudo ./build/test-ipv6-slaac --selftest --with-radvd

- name: Duplicate address detection collision
run: sudo ./build/test-ipv6-slaac --dad-collision

- name: Upload captures
if: always()
uses: actions/upload-artifact@v4
with:
name: ipv6-pcaps
path: "*.pcap"
if-no-files-found: ignore

# Surfaces how much requirement-derived test material is still switched
# off, so the pending set cannot quietly rot.
- name: Pending requirement tests
run: make unit-ipv6-pending-count
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ Initial public wolfIP release.
- Host link drivers for Linux TAP/TUN, Darwin utun, FreeBSD TAP, and VDE2.
- Embedded ports for STM32H753ZI, STM32H563, STM32N6, VA416xx, and Raspberry Pi Pico USB networking demos.
- Shared Ethernet support for STM32 and VA416xx targets, plus common embedded service glue and certificates under `src/port`.

## Unreleased

- IPv6 support (`WOLFIP_IPV6`, off by default): the `ip6` address type with scope/type predicates, prefix operations and RFC 5952 text conversion; header encapsulation and parsing with the RFC 8200 40-byte pseudo-header checksum; ICMPv6 Echo; Neighbor Discovery with address resolution, router discovery and duplicate address detection; SLAAC address formation from a Router Advertisement. Not implemented yet: AF_INET6 sockets, ICMPv6 error messages, extension headers, fragmentation, MLD and DHCPv6.
- New `WOLFIP_IF_MULTICONF` feature (off by default): several addresses per interface, via `wolfIP_ifaddr_add4()` / `add6()` / `del4()` / `del6()` / `count()` / `get()` / `is_local4()`. Required by IPv6, and independently useful for IPv4 aliasing. `struct ipconf` still holds the primary IPv4 address of each interface, so every existing caller is unaffected and the default build does not grow.
- Declared the integration surface for a third-party DLR implementation: `wolfIP_register_l2_handler()` and `struct wolfIP_switch_ops`. See `docs/dlr_integration.md`.
158 changes: 157 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,61 @@ build/test/wolfip_forwarding.o: src/wolfip.c
@$(CC) $(CFLAGS) -DWOLFIP_MAX_INTERFACES=2 -DWOLFIP_ENABLE_FORWARDING=1 -c $< -o $@

build/test/test_ttl_expired.o: CFLAGS+=-DWOLFIP_MAX_INTERFACES=2 -DWOLFIP_ENABLE_FORWARDING=1
# IPv6 end-to-end ping test. Needs its own wolfip object because
# WOLFIP_IPV6 has to reach wolfip.h, which is included before config.h.
build/ipv6/wolfip.o: src/wolfip.c
@mkdir -p `dirname $@` || true
@echo "[CC] $< (ipv6)"
@$(CC) $(CFLAGS) -DWOLFIP_IPV6=1 -c $< -o $@

build/test/test_ipv6_ping.o: src/test/test_ipv6_ping.c
@mkdir -p build/test || true
@echo "[CC] $<"
@$(CC) $(CFLAGS) -DWOLFIP_IPV6=1 -c $< -o $@

build/test-ipv6-ping: build/ipv6/wolfip.o build/test/test_ipv6_ping.o $(NETDEV_OBJ)
@echo "[LD] $@"
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP)

build/test/test_ipv6_slaac.o: src/test/test_ipv6_slaac.c
@mkdir -p build/test || true
@echo "[CC] $<"
@$(CC) $(CFLAGS) -DWOLFIP_IPV6=1 -c $< -o $@

build/test-ipv6-slaac: build/ipv6/wolfip.o build/test/test_ipv6_slaac.o $(NETDEV_OBJ)
@echo "[LD] $@"
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP)

# SLAAC end to end, with the Router Advertisement injected by the test.
.PHONY: ipv6-slaac-test
ipv6-slaac-test: build/test-ipv6-slaac
@echo "[RUN] $< --selftest (requires root)"
@sudo -n true >/dev/null 2>&1 || { echo "ipv6-slaac-test needs to run as root (sudo)"; exit 1; }
@sudo ./build/test-ipv6-slaac --selftest

# Same, but the advertisement comes from radvd, which is what proves
# interoperability with a real router implementation rather than with a
# frame this repository wrote itself.
.PHONY: ipv6-slaac-radvd-test
ipv6-slaac-radvd-test: build/test-ipv6-slaac
@command -v radvd >/dev/null 2>&1 || { echo "radvd is not installed"; exit 1; }
@sudo -n true >/dev/null 2>&1 || { echo "ipv6-slaac-radvd-test needs to run as root (sudo)"; exit 1; }
@echo "[RUN] $< with radvd"
@sudo ./build/test-ipv6-slaac --selftest --with-radvd

# Duplicate address detection against a host that claims the address first.
.PHONY: ipv6-dad-test
ipv6-dad-test: build/test-ipv6-slaac
@sudo -n true >/dev/null 2>&1 || { echo "ipv6-dad-test needs to run as root (sudo)"; exit 1; }
@echo "[RUN] $< --dad-collision"
@sudo ./build/test-ipv6-slaac --dad-collision

.PHONY: ipv6-ping-test
ipv6-ping-test: build/test-ipv6-ping
@echo "[RUN] $< --selftest (requires root)"
@sudo -n true >/dev/null 2>&1 || { echo "ipv6-ping-test needs to run as root (sudo)"; exit 1; }
@sudo ./build/test-ipv6-ping --selftest

build/test-ttl-expired: build/test/test_ttl_expired.o build/test/wolfip_forwarding.o $(WOLFIP_TFTP_OBJ)
@echo "[LD] $@"
@$(CC) $(CFLAGS) -o $@ $(BEGIN_GROUP) $(^) $(LDFLAGS) $(END_GROUP)
Expand Down Expand Up @@ -832,7 +887,14 @@ UNIT_TEST_SRCS:=src/test/unit/unit.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_vlan.c
src/test/unit/unit_tests_vlan.c \
src/test/unit/unit_tests_ifaddr.c \
src/test/unit/unit_tests_ipv6_addr.c \
src/test/unit/unit_tests_ipv6_hdr.c \
src/test/unit/unit_tests_ipv6_recv.c \
src/test/unit/unit_tests_ipv6_icmp.c \
src/test/unit/unit_tests_ipv6_nd.c \
src/test/unit/unit_tests_ipv6_pending.c

unit: build/test/unit

Expand All @@ -849,6 +911,58 @@ unit-multicast: clean-unit unit
unit-vlan: CFLAGS+=-DWOLFIP_VLAN=1 -DWOLFIP_MAX_INTERFACES=6
unit-vlan: clean-unit unit

# Multiple addresses per interface without IPv6: IPv4 aliasing uses the same
# machinery, so the feature is useful and must be tested on its own.
unit-multiconf: CFLAGS+=-DWOLFIP_IF_MULTICONF=1
unit-multiconf: clean-unit unit

unit-multiconf-asan: CFLAGS+=-DWOLFIP_IF_MULTICONF=1 -fsanitize=address
unit-multiconf-asan: LDFLAGS+=-fsanitize=address $(UNIT_LIBS)
unit-multiconf-asan: clean-unit build/test/unit

# IPv6. WOLFIP_IPV6 must be passed on the command line rather than set only in
# config.h: wolfip.c includes wolfip.h *before* config.h, so a macro that
# affects the public header is not visible there otherwise. WOLFIP_VLAN has the
# same constraint.
#
# Note the IPv6 *addressing* tests (unit_tests_ipv6_addr.c) are not gated and
# run in the plain `make unit` build; this target adds the tests that need the
# IPv6 stack itself compiled in.
UNIT_IPV6_CFLAGS:=-DWOLFIP_IPV6=1 -DWOLFIP_IF_MULTICONF=1

unit-ipv6: CFLAGS+=$(UNIT_IPV6_CFLAGS)
unit-ipv6: clean-unit unit

unit-ipv6-asan: CFLAGS+=$(UNIT_IPV6_CFLAGS) -fsanitize=address
unit-ipv6-asan: LDFLAGS+=-fsanitize=address $(UNIT_LIBS)
unit-ipv6-asan: clean-unit build/test/unit

unit-ipv6-ubsan: CFLAGS+=$(UNIT_IPV6_CFLAGS) -fsanitize=undefined -fno-sanitize-recover=all
unit-ipv6-ubsan: LDFLAGS+=-fsanitize=undefined $(UNIT_LIBS)
unit-ipv6-ubsan: clean-unit build/test/unit

unit-ipv6-leaksan: CFLAGS+=$(UNIT_IPV6_CFLAGS) -fsanitize=leak
unit-ipv6-leaksan: LDFLAGS+=-fsanitize=leak $(UNIT_LIBS)
unit-ipv6-leaksan: clean-unit build/test/unit

# Report how much requirement-derived IPv6 test material is still switched off.
# The pending tests are guarded by named WOLFIP_IPV6_HAVE_* macros rather than
# "#if 0" precisely so that this count is possible.
IPV6_PENDING_SRC:=src/test/unit/unit_tests_ipv6_pending.c

.PHONY: unit-ipv6-pending-count
unit-ipv6-pending-count:
@total=`grep -c '^START_TEST' $(IPV6_PENDING_SRC) 2>/dev/null || echo 0`; \
echo "[IPv6] $$total requirement test(s) written and awaiting implementation"; \
for m in EXTHDR ICMP6 ND6 SLAAC DHCP6 SOCKETS; do \
if grep -q "define WOLFIP_IPV6_HAVE_$$m 1" config.h 2>/dev/null; then \
state=enabled; \
else \
state=pending; \
fi; \
echo " WOLFIP_IPV6_HAVE_$$m: $$state"; \
done

ESP_UNIT_CHECK_CFLAGS := $(CHECK_PKG_CFLAGS)
ifeq ($(UNAME_S),Darwin)
ifneq ($(CHECK_PREFIX),)
Expand Down Expand Up @@ -913,6 +1027,8 @@ 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_IPV6_UNIT:=$(COV_DIR)/unit-ipv6
COV_IPV6_UNIT_O:=$(COV_DIR)/unit-ipv6.o

$(COV_UNIT_O): $(UNIT_TEST_SRCS)
@mkdir -p $(COV_DIR)
Expand Down Expand Up @@ -1023,6 +1139,44 @@ autocov-vlan: unit-vlan $(COV_VLAN_UNIT)
--merge-mode-functions=merge-use-line-min \
--html-details -o build/coverage/vlan.html

$(COV_IPV6_UNIT_O): $(UNIT_TEST_SRCS)
@mkdir -p $(COV_DIR)
@echo "[CC] unit.c (ipv6 coverage)"
@$(CC) $(UNIT_CFLAGS) $(CFLAGS) $(UNIT_IPV6_CFLAGS) --coverage -c src/test/unit/unit.c -o $(COV_IPV6_UNIT_O)

$(COV_IPV6_UNIT): LDFLAGS+=--coverage $(UNIT_LIBS)
$(COV_IPV6_UNIT): $(COV_IPV6_UNIT_O)
@echo "[LD] $@"
@$(CC) $(COV_IPV6_UNIT_O) -o $(COV_IPV6_UNIT) $(UNIT_LDFLAGS) $(LDFLAGS)

# Informational only. The 100%-function-coverage gate applies to src/wolfip.c
# in the default build; IPv6 code is still growing and carries deliberate
# stubs, so it is reported but not enforced.
cov-ipv6: unit-ipv6 $(COV_IPV6_UNIT)
@echo "[RUN] unit ipv6 (coverage)"
@rm -f $(COV_DIR)/*.gcda
@$(COV_IPV6_UNIT)
@echo "[COV] gcovr ipv6 html"
@mkdir -p build/coverage
@gcovr -r . --exclude "src/test/unit/.*" \
--gcov-ignore-errors=no_working_dir_found \
--gcov-ignore-parse-errors=all \
--merge-mode-functions=merge-use-line-min \
--html-details -o build/coverage/ipv6.html
@$(OPEN_CMD) build/coverage/ipv6.html

autocov-ipv6: unit-ipv6 $(COV_IPV6_UNIT)
@echo "[RUN] unit ipv6 (coverage)"
@rm -f $(COV_DIR)/*.gcda
@$(COV_IPV6_UNIT)
@echo "[COV] gcovr ipv6 html"
@mkdir -p build/coverage
@gcovr -r . --exclude "src/test/unit/.*" \
--gcov-ignore-errors=no_working_dir_found \
--gcov-ignore-parse-errors=all \
--merge-mode-functions=merge-use-line-min \
--html-details -o build/coverage/ipv6.html

# Install dynamic library to re-link linux applications
#
install:
Expand Down Expand Up @@ -1122,6 +1276,8 @@ 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-vlan cov-vlan autocov-vlan unit-asan unit-ubsan unit-leaksan clean-unit \
unit-ipv6 unit-ipv6-asan unit-ipv6-ubsan unit-ipv6-leaksan cov-ipv6 autocov-ipv6 \
unit-multiconf unit-multiconf-asan \
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 \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ wolfIP exposes a BSD-like `socket(2)` API for IPv4 sockets:
| **Network** | IPv4 | Datagram delivery, TTL handling | [RFC 791](https://datatracker.ietf.org/doc/html/rfc791) |
| **Network** | IPv4 Forwarding | Multi-interface routing (optional) | [RFC 1812](https://datatracker.ietf.org/doc/html/rfc1812) |
| **Network** | ICMP | Echo request/reply, TTL exceeded | [RFC 792](https://datatracker.ietf.org/doc/html/rfc792) |
| **Network** | IPv6 | Header encapsulation and parsing, upper-layer checksum. No extension headers or fragmentation; sockets and DHCPv6 not yet implemented | [RFC 8200](https://datatracker.ietf.org/doc/html/rfc8200) |
| **Network** | ICMPv6 | Echo request/reply. Error messages not yet implemented | [RFC 4443](https://datatracker.ietf.org/doc/html/rfc4443) |
| **Network** | Neighbor Discovery | Address resolution (NS/NA), router discovery (RS/RA), neighbour cache | [RFC 4861](https://datatracker.ietf.org/doc/html/rfc4861) |
| **Network** | SLAAC | Link-local and global address formation, duplicate address detection | [RFC 4862](https://datatracker.ietf.org/doc/html/rfc4862) |
| **Network** | IPv6 Addressing | Address types, scopes, prefix operations, RFC 5952 text form | [RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291), [RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193), [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952) |
| **Network** | IGMPv3 | ASM membership reports for IPv4 multicast (optional) | [RFC 3376](https://datatracker.ietf.org/doc/html/rfc3376) |
| **Network** | IPsec | ESP Transport mode | [RFC 4303](https://datatracker.ietf.org/doc/html/rfc4303) |
| **Transport** | UDP | Unicast datagrams, checksum, optional IPv4 multicast | [RFC 768](https://datatracker.ietf.org/doc/html/rfc768) |
Expand Down Expand Up @@ -187,6 +192,7 @@ This port follows the same model as the POSIX wrapper:

- [API reference](docs/API.md): core stack, socket, and protocol-client APIs
- [Porting guide](docs/porting_guide.md): designing device drivers (with and without DMA) and porting wolfIP to a new operating system
- [DLR integration](docs/dlr_integration.md): the L2 protocol hook and switch-control vtable a third-party Device Level Ring implementation needs from wolfIP

Module how-tos:

Expand Down
Loading
Loading