diff --git a/.github/workflows/ipv6.yml b/.github/workflows/ipv6.yml new file mode 100644 index 00000000..623cc243 --- /dev/null +++ b/.github/workflows/ipv6.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c5255ab5..875ce8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/Makefile b/Makefile index 8bb424e8..fd397d2d 100644 --- a/Makefile +++ b/Makefile @@ -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) @@ -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 @@ -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),) @@ -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) @@ -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: @@ -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 \ diff --git a/README.md b/README.md index d97be9e1..c96bd1f5 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -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: diff --git a/docs/dlr_integration.md b/docs/dlr_integration.md new file mode 100644 index 00000000..9c09d732 --- /dev/null +++ b/docs/dlr_integration.md @@ -0,0 +1,183 @@ +# Integrating a DLR implementation with wolfIP + +## Table of Contents + +1. [Scope](#1-scope) +2. [What DLR needs that wolfIP does not have](#2-what-dlr-needs-that-wolfip-does-not-have) +3. [The L2 protocol hook](#3-the-l2-protocol-hook) +4. [The switch control vtable](#4-the-switch-control-vtable) +5. [Timing constraints](#5-timing-constraints) +6. [Driver checklist](#6-driver-checklist) +7. [Status and open items](#7-status-and-open-items) + +--- + +## 1. Scope + +**wolfIP does not implement DLR.** This document describes the integration +surface wolfIP exposes so that a Device Level Ring implementation — written +in-house or supplied by a third party — can be layered on top without +modifying the core stack. + +DLR is ODVA's Ethernet ring-redundancy protocol, specified in the *CIP +Networks Library, Volume 2: EtherNet/IP Adaptation of CIP*, chapter 9, and +referenced by IEC 61784-2. It provides sub-10ms recovery on a single ring of +two-port devices: a designated ring supervisor blocks one of its ports to +break the loop, watches for beacon loss, and unblocks on a fault. + +> **The ODVA specification is not publicly available.** Values quoted in this +> document — notably the DLR ethertype `0x80E1` and the +> `01:21:6C:00:00:0x` multicast MAC range — must be confirmed against the +> specification before being committed to code. They are recorded here to +> describe the *shape* of the integration, not as normative constants. + +Two things from an earlier draft are explicitly **not** part of this work: + +- **ISO 11898 / CAN FD.** ISO 11898 is the CAN bus standard; ISO 11898-5 is + "high-speed medium access unit with low-power mode". It has nothing to do + with DLR and nothing to do with an Ethernet IP stack. +- **DLR over anything but Ethernet.** DLR is an Ethernet ring protocol. + +--- + +## 2. What DLR needs that wolfIP does not have + +| Need | wolfIP before this change | Provided by | +|---|---|---| +| Receive frames of a non-IP ethertype | Only `0x0800`, `0x0806`, `0x8100`, and `0x888E` hardwired in the demux | `wolfIP_register_l2_handler()` | +| Receive frames sent to a protocol-specific multicast MAC | Ingress filter accepts unicast, broadcast, and IP multicast mappings only | `accept_macs` argument of the same call | +| Know which of the two ports a frame arrived on | No port concept; `wolfIP_ll_dev` is one interface | `switch_ops->last_rx_port()` | +| Send a frame out of one specific port | `wolfIP_ll_send_frame()` targets an interface | `switch_ops->send_on_port()` | +| Block ordinary traffic on a port while still passing ring frames | No port control | `switch_ops->port_set_blocked()` | +| Detect a link transition quickly | No link state reporting | `switch_ops->set_link_change_cb()` | +| Flush the switch MAC table after a topology change | Not applicable | `switch_ops->flush_mac_table()` | +| Beacon every few hundred microseconds | Poll loop is millisecond-granular | **Not provided** — see §5 | + +--- + +## 3. The L2 protocol hook + +```c +int wolfIP_register_l2_handler(struct wolfIP *s, uint16_t ethertype, + int (*handler)(void *ctx, unsigned int if_idx, + const uint8_t *frame, + uint32_t len), + void *ctx, + const uint8_t *accept_macs, unsigned int count); +``` + +This generalises the existing `wolfIP_register_eapol_handler()`, which does +the same job for ethertype `0x888E` only. A DLR module registers `0x80E1` +along with the ring multicast MAC addresses it must receive. + +Two details matter and are easy to get wrong: + +- **`frame`/`len` cover the entire Ethernet frame, header included.** The + EAPOL hook strips the 14-byte header before calling out; a ring protocol + cannot afford that, because it needs the source MAC to identify the + advertising node. +- **`accept_macs` is not optional for DLR.** The ingress path filters on + destination MAC before it reaches any dispatch. A protocol using its own + multicast group that does not declare its MACs will simply never be + called, and the failure is silent. + +--- + +## 4. The switch control vtable + +```c +struct wolfIP_switch_ops { + int (*port_count)(struct wolfIP_ll_dev *ll); + int (*port_link_state)(struct wolfIP_ll_dev *ll, unsigned int port); + int (*set_link_change_cb)(struct wolfIP_ll_dev *ll, + void (*cb)(void *ctx, unsigned int port, int up), + void *ctx); + int (*port_set_blocked)(struct wolfIP_ll_dev *ll, unsigned int port, + int blocked); + int (*flush_mac_table)(struct wolfIP_ll_dev *ll, int port_or_all); + int (*send_on_port)(struct wolfIP_ll_dev *ll, unsigned int port, + void *buf, uint32_t len); + int (*last_rx_port)(struct wolfIP_ll_dev *ll); +}; +``` + +A driver publishes this by setting `ll->switch_ops`. It is `NULL` on every +ordinary single-port driver, and the field is appended **last** in +`struct wolfIP_ll_dev`, after `wifi_ops`, so that adding it does not shift +the offset of any pre-existing member. That is the same ABI convention the +Wi-Fi vtable follows, and it is why ports do not need recompiling. + +**`port_set_blocked()` carries the subtlety.** A blocked port must continue +to pass frames whose ethertype has been claimed through +`wolfIP_register_l2_handler()`. If the hardware filter is all-or-nothing, +the ring protocol stops seeing its own beacons across the block and the ring +never converges. Drivers that cannot express this selectively should say so +rather than approximating it. + +--- + +## 5. Timing constraints + +This is the part that cannot be papered over. + +wolfIP's poll loop is millisecond-granular: `wolfIP_poll(s, now)` takes a +millisecond timestamp, and the timer heap is built on it. DLR beacon +intervals are **sub-millisecond** (hundreds of microseconds), with beacon +timeouts a small multiple of that. + +Therefore: + +- A DLR implementation **must** drive beacon transmission and the beacon + timeout from its own hardware timer or a dedicated high-priority task. + wolfIP's timers are not suitable and `struct wolfIP_switch_ops` does not + pretend to offer them. +- `set_link_change_cb()` exists because link-down detection via polling + would be far too slow. Recovery within the DLR budget depends on the PHY + interrupt path, not on the stack's poll cadence. +- Beacon transmission should go through `send_on_port()` directly rather + than through wolfIP's transmit path, which is designed around a + poll-driven flush and offers no latency guarantee. + +In short: wolfIP is the IP stack alongside the ring protocol, not the engine +driving it. + +--- + +## 6. Driver checklist + +For a two-port part intended to support DLR: + +- [ ] `ll->switch_ops` populated, `port_count()` returning 2. +- [ ] `send_on_port()` reaches each external port individually, bypassing + the switch's normal forwarding decision. +- [ ] `last_rx_port()` reports the ingress port of the frame most recently + handed to `wolfIP_recv_ex()`. +- [ ] `port_set_blocked()` blocks ordinary traffic while still passing + claimed ethertypes. +- [ ] `flush_mac_table()` clears learned entries, per port and for all. +- [ ] `set_link_change_cb()` fires from the PHY link-change interrupt, not + from a poll. +- [ ] A hardware timer is available to the DLR module at sub-millisecond + resolution. + +--- + +## 7. Status and open items + +**Declared, not implemented.** `struct wolfIP_switch_ops` and +`wolfIP_register_l2_handler()` are part of the public header so that driver +and integration work can proceed against a stable contract. The stack side +of `wolfIP_register_l2_handler()` — the registration table and the demux and +MAC-filter changes that consume it — is not written yet, and no driver +currently populates `switch_ops`. + +Open items before an implementation lands: + +1. Confirm the DLR ethertype and multicast MAC range against the ODVA + specification. +2. Decide the size of the registered-protocol table and whether it is + per-interface or global. +3. Decide whether `accept_macs` should support a prefix match. Exact + matching covers DLR; other protocols may want `33:33:*`-style rules. +4. Settle whether a blocked port is a property the stack should know about, + or remain entirely inside the driver and the DLR module. diff --git a/src/test/test_ipv6_ping.c b/src/test/test_ipv6_ping.c new file mode 100644 index 00000000..779001bb --- /dev/null +++ b/src/test/test_ipv6_ping.c @@ -0,0 +1,259 @@ +/* test_ipv6_ping.c + * + * End-to-end check that wolfIP answers ICMPv6 Echo Requests from a real + * host stack, over a TAP device or a VDE switch. + * + * Copyright (C) 2026 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 + */ + +/* + * Needs root, for the TAP device. With a VDE switch, build with BUILD_VDE=1 + * and point VDE_SOCKET_PATH at the switch's control socket. + * + * sudo ./build/test-ipv6-ping # run and wait, ping it yourself + * sudo ./build/test-ipv6-ping --selftest # run ping(8) against it and exit + * + * The addresses here are configured directly rather than by SLAAC, which + * keeps this test narrow: it exercises ICMPv6 Echo and nothing else. The + * host still has to resolve our link-layer address first, and Neighbor + * Discovery answers that solicitation, so no static neighbour entry is + * needed. test_ipv6_slaac.c is the wider test, where nothing at all is + * configured on the wolfIP side. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "wolfip.h" + +#if !WOLFIP_IPV6 +#error "test_ipv6_ping requires -DWOLFIP_IPV6=1" +#endif + +extern int tap_init(struct wolfIP_ll_dev *dev, const char *name, + uint32_t host_ip); +#if WOLFIP_USE_VDE +extern int vde_init(struct wolfIP_ll_dev *ll, const char *socket_path, + const char *port, const uint8_t *mac); +#endif + +#define PING_IFNAME "wtcp0" +/* Documentation prefix, RFC 3849. Handy for watching global-scope traffic + * alongside the link-local address. */ +#define PING_GLOBAL "2001:db8::1" + +/* Spawn tcpdump on the TAP interface, the same way the IPv4 interop tests + * do, so the exchange can be opened in wireshark straight afterwards. The + * pid is kept so the capture is stopped on the way out rather than left + * running. Set WOLFIP_NO_PCAP to skip it. */ +static pid_t pcap_pid; + +static void pcap_start(const char *ifname, const char *file) +{ + if (getenv("WOLFIP_NO_PCAP") != NULL) + return; + pcap_pid = fork(); + if (pcap_pid < 0) { + pcap_pid = 0; + return; + } + if (pcap_pid == 0) { + /* -U so frames hit the file as they arrive: if the test aborts, the + * capture up to that point is still readable. */ + execlp("tcpdump", "tcpdump", "-i", ifname, "-w", file, "-U", + "-s", "0", (char *)NULL); + _exit(127); + } + /* Let tcpdump attach before any traffic is generated. */ + usleep(500000); + printf("capturing to %s (pid %d)\n", file, (int)pcap_pid); +} + +static void pcap_stop(void) +{ + if (pcap_pid > 0) { + int status; + + kill(pcap_pid, SIGTERM); + waitpid(pcap_pid, &status, 0); + pcap_pid = 0; + } +} + +static volatile sig_atomic_t stop_requested; + +static void on_sigint(int sig) +{ + (void)sig; + stop_requested = 1; +} + +static uint64_t now_ms(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + return ((uint64_t)tv.tv_sec * 1000u) + ((uint64_t)tv.tv_usec / 1000u); +} + +/* Run the stack until `until_ms`, or until interrupted, or until `child` + * (when non-zero) exits. Returns the child's exit status, or 0. */ +static int run_stack(struct wolfIP *s, uint64_t until_ms, pid_t child) +{ + int status = 0; + + while (!stop_requested && (now_ms() < until_ms)) { + wolfIP_poll(s, now_ms()); + usleep(1000); + if (child != 0) { + pid_t r = waitpid(child, &status, WNOHANG); + + if (r == child) + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; + } + } + if (child != 0) { + kill(child, SIGTERM); + waitpid(child, &status, 0); + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; + } + return 0; +} + +int main(int argc, char **argv) +{ + struct wolfIP *s = NULL; + struct wolfIP_ll_dev *dev; + char ll_str[WOLFIP_IP6_ADDRSTRLEN]; + ip6 link_local; + ip6 prefix; + ip6 iid; + ip6 global; + int selftest = 0; + int i; + int rc; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--selftest") == 0) + selftest = 1; + } + + signal(SIGINT, on_sigint); + signal(SIGTERM, on_sigint); + + wolfIP_init_static(&s); + dev = wolfIP_getdev(s); + if (!dev) { + fprintf(stderr, "no device\n"); + return 1; + } + +#if WOLFIP_USE_VDE + { + const char *sock = getenv("VDE_SOCKET_PATH"); + + if (!sock) + sock = "/tmp/vde_switch.ctl"; + if (vde_init(dev, sock, NULL, NULL) < 0) { + perror("vde_init"); + return 2; + } + printf("VDE switch socket: %s\n", sock); + } +#else + { + struct in_addr host_ip; + + /* The IPv4 address is only there to get the interface configured + * and up; nothing in this test uses it. */ + inet_aton(HOST_STACK_IP, &host_ip); + if (tap_init(dev, PING_IFNAME, host_ip.s_addr) < 0) { + perror("tap_init (are you root?)"); + return 2; + } + } +#endif + + pcap_start(dev->ifname, "ipv6-ping.pcap"); + + /* Link-local address from the interface MAC, the way RFC 4862 section + * 5.3 forms it: fe80::/64 plus a modified EUI-64 interface identifier. + * SLAAC will do this by itself later; here it is done explicitly. */ + if (atoip6("fe80::", &prefix) != 0) + return 3; + ip6_iid_from_mac(&iid, dev->mac); + ip6_make_addr(&link_local, &prefix, 64, &iid); + if (wolfIP_ifaddr_add6(s, 0, &link_local, 64) != 0) { + fprintf(stderr, "could not add link-local address\n"); + return 3; + } + if (atoip6(PING_GLOBAL, &global) == 0) + (void)wolfIP_ifaddr_add6(s, 0, &global, 64); + + ip6toa(&link_local, ll_str); + printf("interface : %s\n", dev->ifname); + printf("mac : %02x:%02x:%02x:%02x:%02x:%02x\n", + dev->mac[0], dev->mac[1], dev->mac[2], + dev->mac[3], dev->mac[4], dev->mac[5]); + printf("link-local: %s\n", ll_str); + printf("global : %s\n", PING_GLOBAL); + printf("\n"); + + if (!selftest) { + printf("From another terminal:\n\n" + " ping -6 -c 3 %s%%%s\n\n", ll_str, dev->ifname); + printf("Running. Ctrl-C to stop.\n"); + rc = run_stack(s, now_ms() + (3600u * 1000u), 0); + pcap_stop(); + return rc; + } + + { + pid_t child = fork(); + + if (child < 0) { + perror("fork"); + return 4; + } + if (child == 0) { + char pingcmd[256]; + + /* Give the parent a moment to start polling. */ + usleep(300000); + snprintf(pingcmd, sizeof(pingcmd), + "ping -6 -c 3 -W 2 %s%%%s", ll_str, dev->ifname); + printf("+ %s\n", pingcmd); + fflush(stdout); + _exit(system(pingcmd) == 0 ? 0 : 1); + } + rc = run_stack(s, now_ms() + (20u * 1000u), child); + } + + pcap_stop(); + printf("\nICMPv6 echo self-test: %s\n", (rc == 0) ? "PASS" : "FAIL"); + return rc; +} diff --git a/src/test/test_ipv6_slaac.c b/src/test/test_ipv6_slaac.c new file mode 100644 index 00000000..d9447868 --- /dev/null +++ b/src/test/test_ipv6_slaac.c @@ -0,0 +1,609 @@ +/* test_ipv6_slaac.c + * + * End-to-end check that wolfIP configures itself by SLAAC and is then + * reachable from a real Linux host: link-local formation, duplicate address + * detection, router discovery, global address formation, and Neighbor + * Discovery driven by the host's own stack. + * + * Copyright (C) 2026 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 + */ + +/* + * Needs root, for the TAP device and for configuring the host end. + * + * sudo ./build/test-ipv6-slaac # run, print what happened, wait + * sudo ./build/test-ipv6-slaac --selftest # assert it all worked, then exit + * + * What is actually being tested, end to end against the Linux stack: + * + * 1. wolfIP forms its link-local address from the interface MAC and runs + * duplicate address detection on it (RFC 4862 sections 5.3 and 5.4). + * 2. It solicits routers (RFC 4861 section 6.3.7). + * 3. A Router Advertisement carrying a Prefix Information option arrives, + * and wolfIP forms a global address from the prefix plus its interface + * identifier, again verifying it with duplicate address detection + * (RFC 4862 section 5.5.3). + * 4. Linux pings that global address. Its stack does not know wolfIP's + * link-layer address, so it sends a Neighbor Solicitation first, and + * wolfIP has to answer it before any ping can succeed. + * + * Step 4 is the part that distinguishes this from test_ipv6_ping.c, which + * configures addresses by hand and needs a static neighbour entry because it + * predates Neighbor Discovery. Here nothing is configured on the wolfIP side + * and no neighbour entry is installed: address resolution happens for real, + * in both directions. + * + * Where the Router Advertisement comes from + * + * By default the test injects it itself, over an AF_PACKET socket bound to + * the host end of the TAP device. That keeps the test self-contained and + * deterministic, and the frame is a real frame on a real link - but its + * contents are ours, so it does not prove interoperability with a real + * router implementation. With radvd installed and WOLFIP_SLAAC_USE_RADVD=1 + * in the environment, radvd is used instead, which does. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#include +#include +#include +#endif + +#include "config.h" +#include "wolfip.h" + +#if !WOLFIP_IPV6 +#error "test_ipv6_slaac requires -DWOLFIP_IPV6=1" +#endif + +extern int tap_init(struct wolfIP_ll_dev *dev, const char *name, + uint32_t host_ip); + +#define SLAAC_IFNAME "wtcp0" +/* RFC 3849 documentation prefix. */ +#define SLAAC_PREFIX "2001:db8:1:2::" +#define SLAAC_HOSTADDR "2001:db8:1:2::1" +#define SLAAC_ROUTER_LL "fe80::1" + +static const uint8_t slaac_router_mac[6] = {0x02, 0xBB, 0x00, 0x00, 0x00, 0x01}; + +/* Spawn tcpdump on the TAP interface, the same way the IPv4 interop tests + * do, so the exchange can be opened in wireshark straight afterwards. The + * pid is kept so the capture is stopped on the way out rather than left + * running. Set WOLFIP_NO_PCAP to skip it. */ +static pid_t pcap_pid; + +static void pcap_start(const char *ifname, const char *file) +{ + if (getenv("WOLFIP_NO_PCAP") != NULL) + return; + pcap_pid = fork(); + if (pcap_pid < 0) { + pcap_pid = 0; + return; + } + if (pcap_pid == 0) { + /* -U so frames hit the file as they arrive: if the test aborts, the + * capture up to that point is still readable. */ + execlp("tcpdump", "tcpdump", "-i", ifname, "-w", file, "-U", + "-s", "0", (char *)NULL); + _exit(127); + } + /* Let tcpdump attach before any traffic is generated. */ + usleep(500000); + printf("capturing to %s (pid %d)\n", file, (int)pcap_pid); +} + +static void radvd_stop_if_started(int started, const char *ifname) +{ + char cmd[160]; + + if (!started) + return; + snprintf(cmd, sizeof(cmd), "tools/scripts/wolfip-radvd.sh stop %s", ifname); + (void)system(cmd); +} + +static void pcap_stop(void) +{ + if (pcap_pid > 0) { + int status; + + kill(pcap_pid, SIGTERM); + waitpid(pcap_pid, &status, 0); + pcap_pid = 0; + } +} + +static volatile sig_atomic_t stop_requested; + +static void on_sigint(int sig) +{ + (void)sig; + stop_requested = 1; +} + +static uint64_t now_ms(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + return ((uint64_t)tv.tv_sec * 1000u) + ((uint64_t)tv.tv_usec / 1000u); +} + +static void run_for(struct wolfIP *s, uint64_t ms) +{ + uint64_t until = now_ms() + ms; + + while (!stop_requested && (now_ms() < until)) { + wolfIP_poll(s, now_ms()); + usleep(1000); + } +} + +/* One's complement sum, for the ICMPv6 checksum over the pseudo-header. */ +static uint16_t slaac_csum(const uint8_t *a, size_t alen, + const uint8_t *b, size_t blen) +{ + uint32_t sum = 0; + size_t i; + + for (i = 0; i + 1 < alen; i += 2) + sum += (uint32_t)((a[i] << 8) | a[i + 1]); + if (alen & 1u) + sum += (uint32_t)(a[alen - 1] << 8); + for (i = 0; i + 1 < blen; i += 2) + sum += (uint32_t)((b[i] << 8) | b[i + 1]); + if (blen & 1u) + sum += (uint32_t)(b[blen - 1] << 8); + while (sum >> 16) + sum = (sum & 0xFFFFu) + (sum >> 16); + return (uint16_t)(~sum); +} + +#ifdef __linux__ +/* Send a Router Advertisement onto the link from the host end of the TAP, + * advertising SLAAC_PREFIX as on-link and autonomous. */ +static int slaac_send_ra(const char *ifname) +{ + uint8_t frame[128]; + uint8_t pseudo[40]; + uint8_t dst_mac[6] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01}; + struct sockaddr_ll sll; + struct ifreq ifr; + ip6 src; + ip6 dst; + ip6 prefix; + int fd; + int rc; + size_t icmp_off = 14 + 40; + size_t icmp_len = 16 + 32; /* RA header + one prefix option */ + uint16_t csum; + + if ((atoip6(SLAAC_ROUTER_LL, &src) != 0) || + (atoip6(SLAAC_PREFIX, &prefix) != 0)) + return -1; + ip6_set_all_nodes(&dst); + + memset(frame, 0, sizeof(frame)); + /* Ethernet */ + memcpy(frame, dst_mac, 6); + memcpy(frame + 6, slaac_router_mac, 6); + frame[12] = 0x86; + frame[13] = 0xDD; + /* IPv6 */ + frame[14] = 0x60; + frame[18] = (uint8_t)(icmp_len >> 8); + frame[19] = (uint8_t)(icmp_len & 0xFFu); + frame[20] = 58; /* ICMPv6 */ + frame[21] = 255; /* hop limit, mandatory for Neighbor Discovery */ + memcpy(frame + 22, src.addr, 16); + memcpy(frame + 38, dst.addr, 16); + /* Router Advertisement */ + frame[icmp_off + 0] = 134; /* type */ + frame[icmp_off + 4] = 64; /* cur hop limit */ + frame[icmp_off + 6] = 0x07; /* router lifetime 1800s */ + frame[icmp_off + 7] = 0x08; + /* Prefix Information option */ + frame[icmp_off + 16] = 3; /* type */ + frame[icmp_off + 17] = 4; /* length, 32 octets */ + frame[icmp_off + 18] = 64; /* prefix length */ + frame[icmp_off + 19] = 0xC0; /* on-link + autonomous */ + frame[icmp_off + 20] = 0x00; /* valid lifetime 7200s */ + frame[icmp_off + 21] = 0x00; + frame[icmp_off + 22] = 0x1C; + frame[icmp_off + 23] = 0x20; + frame[icmp_off + 24] = 0x00; /* preferred lifetime 7200s */ + frame[icmp_off + 25] = 0x00; + frame[icmp_off + 26] = 0x1C; + frame[icmp_off + 27] = 0x20; + memcpy(&frame[icmp_off + 32], prefix.addr, 16); + + /* IPv6 pseudo-header, RFC 8200 section 8.1 */ + memset(pseudo, 0, sizeof(pseudo)); + memcpy(pseudo, src.addr, 16); + memcpy(pseudo + 16, dst.addr, 16); + pseudo[34] = (uint8_t)(icmp_len >> 8); + pseudo[35] = (uint8_t)(icmp_len & 0xFFu); + pseudo[39] = 58; + csum = slaac_csum(pseudo, sizeof(pseudo), &frame[icmp_off], icmp_len); + frame[icmp_off + 2] = (uint8_t)(csum >> 8); + frame[icmp_off + 3] = (uint8_t)(csum & 0xFFu); + + fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (fd < 0) { + perror("socket(AF_PACKET)"); + return -1; + } + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); + if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) { + perror("SIOCGIFINDEX"); + close(fd); + return -1; + } + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_ifindex = ifr.ifr_ifindex; + sll.sll_halen = 6; + memcpy(sll.sll_addr, dst_mac, 6); + rc = (int)sendto(fd, frame, icmp_off + icmp_len, 0, + (struct sockaddr *)&sll, sizeof(sll)); + close(fd); + return (rc > 0) ? 0 : -1; +} +#else +static int slaac_send_ra(const char *ifname) +{ + (void)ifname; + fprintf(stderr, "Router Advertisement injection is Linux-only\n"); + return -1; +} +#endif + +#ifdef __linux__ +/* Send a Neighbor Advertisement claiming `target`, from the host end of the + * TAP. Used to provoke a duplicate address collision at a precise moment: + * wolfIP must abandon an address somebody else advertises while it is still + * tentative (RFC 4862 section 5.4.4). */ +static int slaac_send_na(const char *ifname, const ip6 *target) +{ + uint8_t frame[128]; + uint8_t pseudo[40]; + uint8_t dst_mac[6] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01}; + struct sockaddr_ll sll; + struct ifreq ifr; + ip6 dst; + int fd; + int rc; + size_t icmp_off = 14 + 40; + size_t icmp_len = 24 + 8; /* NA header + target link-layer address */ + uint16_t csum; + + ip6_set_all_nodes(&dst); + memset(frame, 0, sizeof(frame)); + memcpy(frame, dst_mac, 6); + memcpy(frame + 6, slaac_router_mac, 6); + frame[12] = 0x86; + frame[13] = 0xDD; + frame[14] = 0x60; + frame[18] = (uint8_t)(icmp_len >> 8); + frame[19] = (uint8_t)(icmp_len & 0xFFu); + frame[20] = 58; + frame[21] = 255; + /* Source is the address being claimed, which is what a node defending + * its own address does. */ + memcpy(frame + 22, target->addr, 16); + memcpy(frame + 38, dst.addr, 16); + frame[icmp_off + 0] = 136; /* Neighbor Advertisement */ + frame[icmp_off + 4] = 0x20; /* Override */ + memcpy(&frame[icmp_off + 8], target->addr, 16); + frame[icmp_off + 24] = 2; /* target link-layer address option */ + frame[icmp_off + 25] = 1; + memcpy(&frame[icmp_off + 26], slaac_router_mac, 6); + + memset(pseudo, 0, sizeof(pseudo)); + memcpy(pseudo, target->addr, 16); + memcpy(pseudo + 16, dst.addr, 16); + pseudo[34] = (uint8_t)(icmp_len >> 8); + pseudo[35] = (uint8_t)(icmp_len & 0xFFu); + pseudo[39] = 58; + csum = slaac_csum(pseudo, sizeof(pseudo), &frame[icmp_off], icmp_len); + frame[icmp_off + 2] = (uint8_t)(csum >> 8); + frame[icmp_off + 3] = (uint8_t)(csum & 0xFFu); + + fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (fd < 0) + return -1; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); + if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) { + close(fd); + return -1; + } + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_ifindex = ifr.ifr_ifindex; + sll.sll_halen = 6; + memcpy(sll.sll_addr, dst_mac, 6); + rc = (int)sendto(fd, frame, icmp_off + icmp_len, 0, + (struct sockaddr *)&sll, sizeof(sll)); + close(fd); + return (rc > 0) ? 0 : -1; +} +#else +static int slaac_send_na(const char *ifname, const ip6 *target) +{ + (void)ifname; (void)target; + return -1; +} +#endif + +/* First non-link-local IPv6 address that has completed duplicate address + * detection, or -1 if there is none yet. */ +static int slaac_global_addr(struct wolfIP *s, ip6 *out) +{ + struct wolfIP_ifaddr_info info; + unsigned int n = wolfIP_ifaddr_count(s, 0, AF_INET6); + unsigned int i; + + for (i = 0; i < n; i++) { + if (wolfIP_ifaddr_get(s, 0, AF_INET6, i, &info) != 0) + continue; + if (ip6_is_link_local(&info.v6)) + continue; + if (info.state != WOLFIP_IFADDR_PREFERRED) + continue; + ip6_copy(out, &info.v6); + return 0; + } + return -1; +} + +static void report_addresses(struct wolfIP *s) +{ + struct wolfIP_ifaddr_info info; + unsigned int n = wolfIP_ifaddr_count(s, 0, AF_INET6); + unsigned int i; + char buf[WOLFIP_IP6_ADDRSTRLEN]; + + printf("wolfIP has %u IPv6 address(es):\n", n); + for (i = 0; i < n; i++) { + const char *state; + + if (wolfIP_ifaddr_get(s, 0, AF_INET6, i, &info) != 0) + continue; + switch (info.state) { + case WOLFIP_IFADDR_TENTATIVE: state = "tentative"; break; + case WOLFIP_IFADDR_DEPRECATED: state = "deprecated"; break; + default: state = "preferred"; break; + } + ip6toa(&info.v6, buf); + printf(" %-40s /%u %s%s\n", buf, info.prefix_len, state, + ip6_is_link_local(&info.v6) ? " (link-local)" : ""); + } +} + +int main(int argc, char **argv) +{ + struct wolfIP *s = NULL; + struct wolfIP_ll_dev *dev; + char cmd[256]; + char addr_str[WOLFIP_IP6_ADDRSTRLEN]; + ip6 global; + int selftest = 0; + int dad_collision = 0; + int use_radvd = (getenv("WOLFIP_SLAAC_USE_RADVD") != NULL); + int radvd_started = 0; + int i; + int rc = 0; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--selftest") == 0) + selftest = 1; + if (strcmp(argv[i], "--dad-collision") == 0) + dad_collision = 1; + if (strcmp(argv[i], "--with-radvd") == 0) + use_radvd = 1; + } + + signal(SIGINT, on_sigint); + signal(SIGTERM, on_sigint); + + wolfIP_init_static(&s); + dev = wolfIP_getdev(s); + if (!dev) { + fprintf(stderr, "no device\n"); + return 1; + } + { + struct in_addr host_ip; + + inet_aton(HOST_STACK_IP, &host_ip); + if (tap_init(dev, SLAAC_IFNAME, host_ip.s_addr) < 0) { + perror("tap_init (are you root?)"); + return 2; + } + } + + pcap_start(dev->ifname, "ipv6-slaac.pcap"); + + /* Give the host end an address in the prefix we are about to advertise, + * so it can reach whatever wolfIP configures for itself. */ + snprintf(cmd, sizeof(cmd), + "ip -6 addr replace %s/64 dev %s nodad >/dev/null 2>&1", + SLAAC_HOSTADDR, dev->ifname); + if (system(cmd) != 0) + fprintf(stderr, "warning: could not add host address\n"); + + /* Nothing is configured on the wolfIP side: this is the whole point. */ + if (wolfIP_ipv6_start(s, 0) != 0) { + fprintf(stderr, "wolfIP_ipv6_start failed\n"); + return 3; + } + printf("IPv6 started on %s, running duplicate address detection...\n", + dev->ifname); + + if (dad_collision) { + struct wolfIP_ifaddr_info info; + ip6 claimed; + + /* Punctual: claim the link-local address while it is still + * tentative, before the detection window closes. */ + run_for(s, 300); + if (wolfIP_ifaddr_get(s, 0, AF_INET6, 0, &info) != 0) { + fprintf(stderr, "FAIL: no tentative address to collide with\n"); + return 7; + } + ip6_copy(&claimed, &info.v6); + ip6toa(&claimed, addr_str); + printf("claiming %s from the host while it is %s...\n", addr_str, + (info.state == WOLFIP_IFADDR_TENTATIVE) ? "tentative" + : "already assigned"); + if (slaac_send_na(dev->ifname, &claimed) != 0) { + fprintf(stderr, "could not send the Neighbor Advertisement\n"); + return 8; + } + run_for(s, 2000); + report_addresses(s); + if (wolfIP_ifaddr_count(s, 0, AF_INET6) != 0) { + pcap_stop(); + printf("\nDAD collision test: FAIL (address was kept)\n"); + return 1; + } + pcap_stop(); + printf("\nDAD collision test: PASS (address abandoned)\n"); + return 0; + } + + run_for(s, 2000); + report_addresses(s); + + if (use_radvd) { + /* radvd does not autostart, and should not: the script spawns it + * directly against a generated config for this interface only. */ + snprintf(cmd, sizeof(cmd), + "tools/scripts/wolfip-radvd.sh start %s", dev->ifname); + printf("\nStarting radvd: %s\n", cmd); + if (system(cmd) != 0) { + fprintf(stderr, "could not start radvd\n"); + return 4; + } + radvd_started = 1; + } else { + printf("\nInjecting a Router Advertisement for %s/64...\n", + SLAAC_PREFIX); + if (slaac_send_ra(dev->ifname) != 0) { + fprintf(stderr, "could not send the Router Advertisement\n"); + return 4; + } + } + + /* Forming the address and verifying it takes another detection cycle. + * radvd answers our Router Solicitation, but the solicitation itself is + * only sent once the link-local address is usable. */ + run_for(s, use_radvd ? 9000 : 3000); + printf("\n"); + report_addresses(s); + + if (slaac_global_addr(s, &global) != 0) { + fprintf(stderr, "\nFAIL: no global address was configured by SLAAC\n"); + return 5; + } + ip6toa(&global, addr_str); + printf("\nSLAAC configured: %s\n", addr_str); + + if (!selftest) { + printf("\nNeighbor Discovery is implemented, so no static neighbour\n" + "entry is needed. From another terminal:\n\n" + " ping -6 -c 3 %s\n\n", addr_str); + printf("Running. Ctrl-C to stop.\n"); + run_for(s, 3600u * 1000u); + pcap_stop(); + return 0; + } + + { + pid_t child = fork(); + int status = 0; + + if (child < 0) { + perror("fork"); + return 6; + } + if (child == 0) { + char pingcmd[256]; + + usleep(300000); + /* No neighbour entry is installed: the host has to resolve + * wolfIP's link-layer address with a Neighbor Solicitation, and + * wolfIP has to answer it. */ + snprintf(pingcmd, sizeof(pingcmd), + "ping -6 -c 3 -W 2 %s", addr_str); + printf("+ %s\n", pingcmd); + fflush(stdout); + _exit(system(pingcmd) == 0 ? 0 : 1); + } + { + uint64_t until = now_ms() + (20u * 1000u); + + while (!stop_requested && (now_ms() < until)) { + pid_t r; + + wolfIP_poll(s, now_ms()); + usleep(1000); + r = waitpid(child, &status, WNOHANG); + if (r == child) { + rc = WIFEXITED(status) ? WEXITSTATUS(status) : 1; + break; + } + } + } + } + + /* The host resolved us, so we should have it in our own cache too. */ + { + uint8_t mac[6]; + ip6 host; + + if ((atoip6(SLAAC_HOSTADDR, &host) == 0) && + (wolfIP_nd6_lookup(s, 0, &host, mac) == 0)) { + printf("neighbour cache: %s is at %02x:%02x:%02x:%02x:%02x:%02x\n", + SLAAC_HOSTADDR, mac[0], mac[1], mac[2], mac[3], mac[4], + mac[5]); + } + } + + radvd_stop_if_started(radvd_started, dev->ifname); + pcap_stop(); + printf("\nSLAAC end-to-end test: %s\n", (rc == 0) ? "PASS" : "FAIL"); + return rc; +} diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c347ab1a..a87b01a7 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -37,6 +37,13 @@ #include "unit_tests_dns_edges.c" #include "unit_tests_misc_edges.c" #include "unit_tests_vlan.c" +#include "unit_tests_ifaddr.c" +#include "unit_tests_ipv6_addr.c" +#include "unit_tests_ipv6_hdr.c" +#include "unit_tests_ipv6_recv.c" +#include "unit_tests_ipv6_icmp.c" +#include "unit_tests_ipv6_nd.c" +#include "unit_tests_ipv6_pending.c" Suite *wolf_suite(void) { @@ -961,6 +968,240 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_ip_output_add_header_icmp); tcase_add_test(tc_utils, test_regression_icmp_ip_len_below_header); + /* Per-interface address list. The first group runs in every build, + * including the default IPv4-only one. */ + tcase_add_test(tc_utils, test_ifaddr_primary_is_the_ipconf_address); + tcase_add_test(tc_utils, test_ifaddr_unconfigured_interface_has_no_addresses); + tcase_add_test(tc_utils, test_ifaddr_add4_sets_the_primary_when_unconfigured); + tcase_add_test(tc_utils, test_ifaddr_add4_rejects_a_duplicate); + tcase_add_test(tc_utils, test_ifaddr_del4_clears_the_primary); + tcase_add_test(tc_utils, test_ifaddr_is_local4_matches_the_primary); + tcase_add_test(tc_utils, test_ifaddr_capacity_is_one_without_multiconf); + tcase_add_test(tc_utils, test_ifaddr_rejects_invalid_arguments); + tcase_add_test(tc_utils, test_ifaddr_v6_requires_ipv6_enabled); +#if WOLFIP_IF_MULTICONF + tcase_add_test(tc_utils, test_ifaddr_multiple_ipv4_addresses_on_one_interface); + tcase_add_test(tc_utils, test_ifaddr_aliases_are_local_addresses); + tcase_add_test(tc_utils, test_ifaddr_alias_removal_leaves_the_others); + tcase_add_test(tc_utils, test_ifaddr_clearing_the_primary_keeps_aliases); + tcase_add_test(tc_utils, test_ifaddr_per_interface_capacity_is_enforced); + tcase_add_test(tc_utils, test_ifaddr_interfaces_do_not_share_addresses); + tcase_add_test(tc_utils, test_ifaddr_the_same_alias_may_not_be_added_twice); + tcase_add_test(tc_utils, test_ifaddr_pool_is_shared_across_interfaces); + tcase_add_test(tc_utils, test_ifaddr_socket_binds_to_an_alias_on_the_right_interface); + tcase_add_test(tc_utils, test_ifaddr_bind_to_alias_receives_only_its_own_traffic); + tcase_add_test(tc_utils, test_ifaddr_bind_to_primary_does_not_receive_alias_traffic); + tcase_add_test(tc_utils, test_ifaddr_bind_to_a_foreign_address_is_refused); + tcase_add_test(tc_utils, test_ifaddr_sendto_from_an_alias_uses_it_as_source); + tcase_add_test(tc_utils, test_ifaddr_wildcard_bind_receives_traffic_to_any_local_address); + tcase_add_test(tc_utils, test_ifaddr_tcp_wildcard_listener_accepts_connections_to_an_alias); +#endif +#if WOLFIP_IPV6 + tcase_add_test(tc_utils, test_ifaddr_v6_addresses_are_independent_of_v4); + tcase_add_test(tc_utils, test_ifaddr_v6_duplicate_is_rejected_and_removal_works); + tcase_add_test(tc_utils, test_ifaddr_v6_link_local_address_is_scoped_per_interface); + tcase_add_test(tc_utils, test_ifaddr_v6_and_v4_share_the_per_interface_budget); +#endif + + /* IPv6 addressing (wolfip6.h). Not gated on WOLFIP_IPV6: the header holds + * only inline helpers and is always included, so these run in the default + * build and are covered by the whole CI compiler matrix. */ + tcase_add_test(tc_utils, test_ip6_cmp_and_copy); + tcase_add_test(tc_utils, test_ip6_cmp_differs_in_every_byte_position); + tcase_add_test(tc_utils, test_ip6_set_wellknown_addresses); + tcase_add_test(tc_utils, test_ip6_init_macros_match_setters); + tcase_add_test(tc_utils, test_ip6_is_unspecified); + tcase_add_test(tc_utils, test_ip6_is_loopback); + tcase_add_test(tc_utils, test_ip6_is_multicast); + tcase_add_test(tc_utils, test_ip6_is_link_local_covers_whole_fe80_10); + tcase_add_test(tc_utils, test_ip6_is_ula_covers_whole_fc00_7); + tcase_add_test(tc_utils, test_ip6_is_global_covers_whole_2000_3); + tcase_add_test(tc_utils, test_ip6_multicast_flags_and_scope); + tcase_add_test(tc_utils, test_ip6_is_all_nodes_and_all_routers); + tcase_add_test(tc_utils, test_ip6_v4mapped_roundtrip); + tcase_add_test(tc_utils, test_ip6_v4mapped_boundaries); + tcase_add_test(tc_utils, test_ip6_v4compat_excludes_any_and_loopback); + tcase_add_test(tc_utils, test_ip6_prefix_cmp_byte_aligned); + tcase_add_test(tc_utils, test_ip6_prefix_cmp_non_byte_aligned); + tcase_add_test(tc_utils, test_ip6_prefix_cmp_zero_and_clamped); + tcase_add_test(tc_utils, test_ip6_prefix_mask); + tcase_add_test(tc_utils, test_ip6_prefix_mask_every_length_is_consistent); + tcase_add_test(tc_utils, test_ip6_prefix_mask_clears_a_set_host_part); + tcase_add_test(tc_utils, test_ip6_make_addr_from_prefix_and_iid); + tcase_add_test(tc_utils, test_ip6_make_addr_non_byte_aligned_prefix); + tcase_add_test(tc_utils, test_ip6_solicited_node_from_target); + tcase_add_test(tc_utils, test_ip6_solicited_node_depends_only_on_low_24_bits); + tcase_add_test(tc_utils, test_ip6_is_solicited_node_rejects_near_misses); + tcase_add_test(tc_utils, test_ip6_mcast_to_eth_mapping); + tcase_add_test(tc_utils, test_ip6_iid_from_mac_eui64); + tcase_add_test(tc_utils, test_ip6_iid_from_mac_inverts_ul_bit_both_ways); + tcase_add_test(tc_utils, test_ip6_parse_canonical_forms); + tcase_add_test(tc_utils, test_ip6_parse_normalises_noncanonical_input); + tcase_add_test(tc_utils, test_ip6_parse_embedded_ipv4); + tcase_add_test(tc_utils, test_ip6_parse_gap_at_every_position); + tcase_add_test(tc_utils, test_ip6_parse_accepts_single_group_gap_but_writes_it_out); + tcase_add_test(tc_utils, test_ip6_parse_rejects_malformed); + tcase_add_test(tc_utils, test_ip6_parse_rejects_gap_that_elides_nothing); + tcase_add_test(tc_utils, test_ip6_parse_rejects_bad_ipv4_tail); + tcase_add_test(tc_utils, test_ip6_parse_rejects_null_arguments); + tcase_add_test(tc_utils, test_ip6_parse_leaves_output_untouched_on_failure); + tcase_add_test(tc_utils, test_ip6toa_compresses_longest_zero_run); + tcase_add_test(tc_utils, test_ip6toa_compresses_leftmost_run_on_tie); + tcase_add_test(tc_utils, test_ip6toa_does_not_compress_single_zero_group); + tcase_add_test(tc_utils, test_ip6toa_run_at_start_and_end); + tcase_add_test(tc_utils, test_ip6toa_handles_null_arguments); + tcase_add_test(tc_utils, test_ip6toa_never_exceeds_addrstrlen); + tcase_add_test(tc_utils, test_ip6_text_roundtrip_is_stable); + tcase_add_test(tc_utils, test_ip6_text_roundtrip_exhaustive_single_bit); + +#if WOLFIP_IPV6 + /* IPv6 header layout, checksum and encapsulation. Needs the IPv6 stack + * compiled in, so these only run under `make unit-ipv6`. */ + tcase_add_test(tc_proto, test_ip6_header_wire_layout); + tcase_add_test(tc_proto, test_ip6_transport_wire_layout); + tcase_add_test(tc_proto, test_ip6_pseudo_header_is_40_bytes); + tcase_add_test(tc_proto, test_ip6_hdr_version_traffic_class_flow_label); + tcase_add_test(tc_proto, test_ip6_hdr_first_word_byte_order); + tcase_add_test(tc_proto, test_ip6_hdr_address_accessors_roundtrip); + tcase_add_test(tc_proto, test_transport6_checksum_self_verifies); + tcase_add_test(tc_proto, test_transport6_checksum_detects_every_single_bit_flip); + tcase_add_test(tc_proto, test_transport6_checksum_covers_the_pseudo_header); + tcase_add_test(tc_proto, test_transport6_checksum_handles_odd_length_payload); + tcase_add_test(tc_proto, test_ip6_output_add_header_fills_the_header); + tcase_add_test(tc_proto, test_ip6_output_add_header_defaults_hop_limit); + tcase_add_test(tc_proto, test_ip6_output_add_header_udp_zero_checksum_becomes_ffff); + tcase_add_test(tc_proto, test_ip6_output_add_header_icmp6_checksum_uses_pseudo_header); + tcase_add_test(tc_proto, test_ip6_output_add_header_tcp_checksum); + tcase_add_test(tc_proto, test_ip6_output_add_header_rejects_null_arguments); + tcase_add_test(tc_proto, test_ip6_output_add_header_without_mac_leaves_ethernet_alone); + + tcase_add_test(tc_proto, test_ip6_recv_accepts_upper_layer_protocols); + tcase_add_test(tc_proto, test_ip6_recv_tolerates_ethernet_padding); + tcase_add_test(tc_proto, test_ip6_recv_accepts_hop_limit_zero); + tcase_add_test(tc_proto, test_ip6_recv_accepts_unspecified_source); + tcase_add_test(tc_proto, test_ip6_recv_accepts_all_scopes_as_destination); + tcase_add_test(tc_proto, test_ip6_recv_rejects_short_frame); + tcase_add_test(tc_proto, test_ip6_recv_rejects_wrong_version); + tcase_add_test(tc_proto, test_ip6_recv_rejects_truncated_payload); + tcase_add_test(tc_proto, test_ip6_recv_rejects_multicast_source); + tcase_add_test(tc_proto, test_ip6_recv_rejects_unspecified_destination); + tcase_add_test(tc_proto, test_ip6_recv_rejects_loopback_on_the_wire); + tcase_add_test(tc_proto, test_ip6_recv_rejects_v4mapped_on_the_wire); + tcase_add_test(tc_proto, test_ip6_recv_rejects_v4compat_on_the_wire); + tcase_add_test(tc_proto, test_ip6_recv_rejects_every_extension_header); + tcase_add_test(tc_proto, test_ip6_recv_rejects_unknown_next_header); + tcase_add_test(tc_proto, test_ip6_recv_rejects_short_upper_layer_header); + tcase_add_test(tc_proto, test_ip6_recv_checks_structure_before_addresses); + tcase_add_test(tc_proto, test_eth_is_ipv6_multicast_mac); + tcase_add_test(tc_proto, test_ip6_demux_accepts_unicast_and_multicast_frames); + tcase_add_test(tc_proto, test_ip6_demux_ignores_frames_for_other_hosts); + tcase_add_test(tc_proto, test_ip6_demux_survives_a_truncated_frame); + tcase_add_test(tc_proto, test_ip6_ethertype_does_not_disturb_ipv4_or_arp); + tcase_add_test(tc_proto, test_icmp6_echo_request_is_answered); + tcase_add_test(tc_proto, test_icmp6_echo_request_with_no_payload_is_answered); + tcase_add_test(tc_proto, test_icmp6_echo_to_link_local_is_answered_from_it); + tcase_add_test(tc_proto, test_icmp6_echo_with_bad_checksum_is_ignored); + tcase_add_test(tc_proto, test_icmp6_echo_with_nonzero_code_is_ignored); + tcase_add_test(tc_proto, test_icmp6_echo_to_an_address_that_is_not_ours_is_ignored); + tcase_add_test(tc_proto, test_icmp6_echo_from_unspecified_source_is_ignored); + tcase_add_test(tc_proto, test_icmp6_echo_to_tentative_address_is_ignored); + tcase_add_test(tc_proto, test_icmp6_link_local_address_is_not_local_on_another_interface); + tcase_add_test(tc_proto, test_icmp6_echo_reply_does_not_generate_another_reply); + tcase_add_test(tc_proto, test_icmp6_unhandled_types_are_ignored_without_replying); + tcase_add_test(tc_proto, test_icmp6_truncated_echo_is_ignored); + tcase_add_test(tc_proto, test_nd_join_forms_link_local_and_probes_it); + tcase_add_test(tc_proto, test_nd_join_completes_and_solicits_routers); + tcase_add_test(tc_proto, test_nd_na_resolves_an_incomplete_entry); + tcase_add_test(tc_proto, test_nd_na_without_target_lla_resolves_nothing); + tcase_add_test(tc_proto, test_nd_na_without_override_may_not_replace_a_known_mac); + tcase_add_test(tc_proto, test_nd_messages_with_wrong_hop_limit_are_refused); + tcase_add_test(tc_proto, test_nd_na_from_unspecified_source_is_refused); + tcase_add_test(tc_proto, test_nd_na_for_a_foreign_destination_is_refused); + tcase_add_test(tc_proto, test_nd_solicitation_for_our_address_is_answered); + tcase_add_test(tc_proto, test_nd_solicitation_for_a_foreign_address_is_ignored); + tcase_add_test(tc_proto, test_nd_solicitation_to_a_foreign_destination_is_ignored); + tcase_add_test(tc_proto, test_nd_solicitation_with_spoofed_slla_is_ignored); + tcase_add_test(tc_proto, test_dad_succeeds_when_nobody_answers); + tcase_add_test(tc_proto, test_dad_fails_when_a_neighbour_advertises_the_address); + tcase_add_test(tc_proto, test_dad_fails_on_a_simultaneous_probe_from_another_node); + tcase_add_test(tc_proto, test_dad_ignores_malformed_neighbor_solicitations); + tcase_add_test(tc_proto, test_dad_tentative_address_is_not_defended); + tcase_add_test(tc_proto, test_ra_assigns_a_global_address_and_a_default_router); + tcase_add_test(tc_proto, test_ra_from_a_non_link_local_source_is_ignored); + tcase_add_test(tc_proto, test_ra_with_wrong_hop_limit_is_ignored); + tcase_add_test(tc_proto, test_ra_ignores_a_link_local_prefix_option); + tcase_add_test(tc_proto, test_ra_prefix_that_is_not_64_bits_forms_no_address); + tcase_add_test(tc_proto, test_ra_with_zero_router_lifetime_is_not_a_default_route); + tcase_add_test(tc_proto, test_ra_with_a_zero_length_option_terminates); + tcase_add_test(tc_proto, test_ra_for_a_foreign_destination_is_ignored); + tcase_add_test(tc_proto, test_ra_ignores_oversized_prefix_information_option); + tcase_add_test(tc_proto, test_ra_ignores_prefix_with_preferred_lifetime_above_valid); + tcase_add_test(tc_proto, test_ra_zero_valid_lifetime_withdraws_prefix_immediately); + tcase_add_test(tc_proto, test_nd_static_neighbor_rejects_non_wire_addresses); + tcase_add_test(tc_proto, test_ula_is_verified_by_dad_and_then_usable); + tcase_add_test(tc_proto, test_ula_is_defended_and_survives_a_router_advertisement); + tcase_add_test(tc_proto, test_ula_duplicate_is_rejected); + tcase_add_test(tc_proto, test_nd_uses_one_timer_slot_for_the_whole_stack); + tcase_add_test(tc_proto, test_nd_timer_quiesces_when_periodic_work_is_finished); + tcase_add_test(tc_proto, test_nd_recovers_when_the_timer_heap_is_full); + tcase_add_test(tc_proto, test_nd_stop_releases_the_timer_slot); + tcase_add_test(tc_proto, test_nd_stop_drops_a_tentative_address); + tcase_add_test(tc_proto, test_nd_restarts_after_being_stopped); + tcase_add_test(tc_proto, test_nd_stop_rejects_invalid_arguments); + + /* Requirement-derived tests for IPv6 features not implemented yet. + * Each block switches on with its feature macro. */ +#if WOLFIP_IPV6_HAVE_ICMP6 + tcase_add_test(tc_proto, test_icmp6_error_is_not_sent_in_response_to_an_error); + tcase_add_test(tc_proto, test_icmp6_error_is_not_sent_for_multicast_destinations); + tcase_add_test(tc_proto, test_icmp6_error_quotes_as_much_as_fits_in_min_mtu); + tcase_add_test(tc_proto, test_icmp6_destination_unreachable_codes); + tcase_add_test(tc_proto, test_icmp6_packet_too_big_carries_the_mtu); + tcase_add_test(tc_proto, test_icmp6_time_exceeded_on_hop_limit_zero_when_forwarding); + tcase_add_test(tc_proto, test_icmp6_parameter_problem_points_at_the_bad_octet); + tcase_add_test(tc_proto, test_icmp6_unknown_informational_message_is_discarded); +#endif +#if WOLFIP_IPV6_HAVE_ND6 + tcase_add_test(tc_proto, test_nd6_cache_state_machine_transitions); + tcase_add_test(tc_proto, test_nd6_cache_eviction_when_full); + tcase_add_test(tc_proto, test_nd6_queues_one_packet_per_pending_resolution); + tcase_add_test(tc_proto, test_nd6_prefix_option_with_length_over_128_is_ignored); +#endif +#if WOLFIP_IPV6_HAVE_SLAAC + tcase_add_test(tc_proto, test_slaac_preferred_lifetime_expiry_deprecates_address); + tcase_add_test(tc_proto, test_slaac_valid_lifetime_expiry_removes_address); + tcase_add_test(tc_proto, test_slaac_lifetime_extension_is_bounded); + tcase_add_test(tc_proto, test_slaac_respects_the_address_table_limit); +#endif +#if WOLFIP_IPV6_HAVE_DHCP6 + tcase_add_test(tc_proto, test_dhcp6_solicit_goes_to_all_dhcp_servers_multicast); + tcase_add_test(tc_proto, test_dhcp6_solicit_carries_client_id_and_ia_na); + tcase_add_test(tc_proto, test_dhcp6_advertise_with_mismatched_transaction_id_is_ignored); + tcase_add_test(tc_proto, test_dhcp6_reply_assigns_the_offered_address); + tcase_add_test(tc_proto, test_dhcp6_retransmission_uses_exponential_backoff); + tcase_add_test(tc_proto, test_dhcp6_renew_at_t1_and_rebind_at_t2); + tcase_add_test(tc_proto, test_dhcp6_option_longer_than_the_message_is_rejected); + tcase_add_test(tc_proto, test_dhcp6_message_larger_than_buffer_is_rejected); +#endif +#if WOLFIP_IPV6_HAVE_EXTHDR + tcase_add_test(tc_proto, test_ip6_walks_hop_by_hop_and_destination_options); + tcase_add_test(tc_proto, test_ip6_extension_header_chain_length_is_capped); + tcase_add_test(tc_proto, test_ip6_extension_header_with_zero_length_is_rejected); + tcase_add_test(tc_proto, test_ip6_routing_header_type_zero_is_still_rejected); +#endif +#if WOLFIP_IPV6_HAVE_SOCKETS + tcase_add_test(tc_proto, test_socket_af_inet6_stream_and_dgram_are_created); + tcase_add_test(tc_proto, test_socket_bind_and_getsockname_roundtrip_ipv6); + tcase_add_test(tc_proto, test_socket_v4_mapped_destination_is_framed_as_ipv4); + tcase_add_test(tc_proto, test_socket_v4_mapped_peer_is_reported_as_mapped_address); + tcase_add_test(tc_proto, test_socket_ipv6_only_option_is_honoured_not_silently_accepted); + tcase_add_test(tc_proto, test_socket_ipv6_only_socket_rejects_v4_mapped_destination); + tcase_add_test(tc_proto, test_socket_tcp_mss_accounts_for_the_40_byte_header); + tcase_add_test(tc_proto, test_socket_udp_oversize_datagram_is_refused); + tcase_add_test(tc_proto, test_socket_ipv4_and_ipv6_sockets_coexist_on_one_port); +#endif +#endif /* WOLFIP_IPV6 */ + tcase_add_test(tc_wolfssl, test_wolfssl_io_ctx_registers_callbacks); tcase_add_test(tc_wolfssl, test_wolfssl_io_setio_success); tcase_add_test(tc_wolfssl, test_wolfssl_io_setio_invalid_ssl); @@ -1604,9 +1845,25 @@ Suite *wolf_suite(void) int main(void) { int n_fail = 0; + + /* The build configuration this binary was compiled with. Printed because + * the suite is compiled from one source but the resulting test set + * depends on these, and a platform computing them differently shows up + * only as a differing check count. */ + Suite *s; SRunner *sr; + printf("wolfIP unit config: WOLFIP_IPV6=%d WOLFIP_IF_MULTICONF=%d " + "WOLFIP_IF_CONF_MAX=%d WOLFIP_MAX_INTERFACES=%d " + "WOLFIP_ENABLE_LOOPBACK=%d WOLFIP_ENABLE_FORWARDING=%d " + "AF_INET=%d AF_INET6=%d\n", + (int)WOLFIP_IPV6, (int)WOLFIP_IF_MULTICONF, + (int)WOLFIP_IF_CONF_MAX, (int)WOLFIP_MAX_INTERFACES, + (int)WOLFIP_ENABLE_LOOPBACK, (int)WOLFIP_ENABLE_FORWARDING, + (int)AF_INET, (int)AF_INET6); + fflush(stdout); + s = wolf_suite(); sr = srunner_create(s); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index a19421c0..10572b3f 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -595,20 +595,34 @@ static void fix_udp_checksum_raw(struct wolfIP_ip_packet *ip, void *udp_hdr, uin *udp_csum = ee16(transport_checksum(&ph, udp_hdr)); } -static void inject_udp_datagram(struct wolfIP *s, unsigned int if_idx, ip4 src_ip, ip4 dst_ip, - uint16_t src_port, uint16_t dst_port, const uint8_t *payload, uint16_t payload_len) +/* Build a well formed UDP-over-IPv4 Ethernet frame into `frame` and return + * its total length. Shared by the two injection helpers below so that the + * frame is identical whichever ingress path a test chooses. */ +static uint32_t build_udp_frame(uint8_t *frame, struct wolfIP *s, + unsigned int if_idx, ip4 src_ip, ip4 dst_ip, + uint16_t src_port, uint16_t dst_port, const uint8_t *payload, + uint16_t payload_len) { - uint8_t frame[LINK_MTU]; struct wolfIP_udp_datagram *udp = (struct wolfIP_udp_datagram *)frame; struct wolfIP_ll_dev *ll = wolfIP_getdev_ex(s, if_idx); static const uint8_t src_mac[6] = {0x90, 0x91, 0x92, 0x93, 0x94, 0x95}; ck_assert_ptr_nonnull(ll); - memset(udp, 0, sizeof(frame)); + /* Not sizeof(frame): frame is a pointer here, so that would clear eight + * bytes and leave the rest of the header holding whatever was on the + * stack. Zero exactly the frame being built. */ + memset(frame, 0, (size_t)(ETH_HEADER_LEN + IP_HEADER_LEN + + UDP_HEADER_LEN) + payload_len); memcpy(udp->ip.eth.dst, ll->mac, 6); memcpy(udp->ip.eth.src, src_mac, 6); udp->ip.eth.type = ee16(ETH_TYPE_IP); udp->ip.ver_ihl = 0x45; + /* Set explicitly rather than relying on the caller's buffer being + * zeroed. A non-zero flags_fo in particular makes ip_recv() drop the + * frame as a fragment, silently. */ + udp->ip.tos = 0; + udp->ip.id = 0; + udp->ip.flags_fo = 0; udp->ip.ttl = 64; udp->ip.proto = WI_IPPROTO_UDP; udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + payload_len); @@ -625,7 +639,36 @@ static void inject_udp_datagram(struct wolfIP *s, unsigned int if_idx, ip4 src_i memcpy(udp->data, payload, payload_len); } - udp_try_recv(s, if_idx, udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + payload_len)); + return (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + + payload_len); +} + +/* Hand a datagram straight to udp_try_recv, skipping the whole IP layer. + * + * Convenient for exercising the UDP socket demux in isolation, but it means + * NONE of ip_recv's checks run: no header validation, no checksum, no + * martian filtering. A test whose subject is ip_recv must use + * recv_udp_datagram() instead, or it will pass no matter what ip_recv does. */ +static void inject_udp_datagram(struct wolfIP *s, unsigned int if_idx, ip4 src_ip, ip4 dst_ip, + uint16_t src_port, uint16_t dst_port, const uint8_t *payload, uint16_t payload_len) +{ + uint8_t frame[LINK_MTU]; + uint32_t len = build_udp_frame(frame, s, if_idx, src_ip, dst_ip, + src_port, dst_port, payload, payload_len); + + udp_try_recv(s, if_idx, (struct wolfIP_udp_datagram *)frame, len); +} + +/* Deliver a datagram through the real ingress path: + * wolfIP_recv_ex -> wolfIP_recv_on -> ip_recv -> udp_try_recv. */ +static void recv_udp_datagram(struct wolfIP *s, unsigned int if_idx, ip4 src_ip, ip4 dst_ip, + uint16_t src_port, uint16_t dst_port, const uint8_t *payload, uint16_t payload_len) +{ + uint8_t frame[LINK_MTU]; + uint32_t len = build_udp_frame(frame, s, if_idx, src_ip, dst_ip, + src_port, dst_port, payload, payload_len); + + wolfIP_recv_ex(s, if_idx, frame, len); } static int enqueue_tcp_tx(struct tsocket *ts, uint32_t payload_len, uint8_t flags) diff --git a/src/test/unit/unit_tests_ifaddr.c b/src/test/unit/unit_tests_ifaddr.c new file mode 100644 index 00000000..ae545421 --- /dev/null +++ b/src/test/unit/unit_tests_ifaddr.c @@ -0,0 +1,728 @@ +/* unit_tests_ifaddr.c + * + * Copyright (C) 2026 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 + */ + +/* ========================================================================= + * Environment note + * ========================================================================= + * The per-interface address list. + * + * struct ipconf is reached by more than fifty files, every board port among + * them, so it cannot be replaced. The address list is therefore additive: + * + * - ipconf[if_idx].ip/mask/gw remains the authoritative *primary* IPv4 + * address of an interface, exactly as before. wolfIP_ipconfig_set/get + * and every existing caller keep working untouched. + * - The pool holds the *additional* addresses: IPv4 aliases and every + * IPv6 address. The primary is never duplicated into it, so the two + * cannot drift apart. + * + * Index 0 of an interface's IPv4 list is therefore always the primary, and + * indices above 0 are aliases. + * + * WOLFIP_IF_CONF_MAX caps the total per interface, primary included. It is + * 1 unless WOLFIP_IF_MULTICONF is on, which is what makes the feature + * optional: with it off the API still exists and behaves, but an interface + * holds exactly one address and the pool is not compiled in at all, so + * struct wolfIP does not grow. + * + * The tests in the first section run in every build, including the default + * IPv4-only one, which is what keeps the 100%-function-coverage gate on + * src/wolfip.c satisfied. The later sections are gated. + */ + +/* ========================================================================= + * Local helpers + * ========================================================================= */ + +#define IFA_IP_A 0x0A0A0A02U /* 10.10.10.2 */ +#define IFA_IP_B 0x0A0A0A03U /* 10.10.10.3 */ +#define IFA_IP_C 0xC0A80101U /* 192.168.1.1 */ +#define IFA_MASK 0xFFFFFF00U + +static const uint8_t ifaddr_peer_mac[6] = {0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x22}; + +static void ifaddr_setup(struct wolfIP *s) +{ + wolfIP_init(s); + mock_link_init(s); + wolfIP_ipconfig_set_ex(s, TEST_PRIMARY_IF, IFA_IP_A, IFA_MASK, 0); +} + +/* ========================================================================= + * Always-on: behaviour with a single configuration per interface + * ========================================================================= */ + +START_TEST(test_ifaddr_primary_is_the_ipconf_address) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + + ifaddr_setup(&s); + + /* The primary IPv4 address is whatever wolfIP_ipconfig_set_ex put in + * ipconf; the list is a view over it, not a second copy. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 0, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_A); + ck_assert_uint_eq(info.family, AF_INET); + ck_assert_uint_eq(info.if_idx, TEST_PRIMARY_IF); + ck_assert_uint_eq(info.prefix_len, 24); + ck_assert_uint_eq(info.state, WOLFIP_IFADDR_PREFERRED); + + /* Changing it through the old API is immediately visible here. */ + wolfIP_ipconfig_set_ex(&s, TEST_PRIMARY_IF, IFA_IP_C, 0xFFFF0000U, 0); + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 0, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_C); + ck_assert_uint_eq(info.prefix_len, 16); +} +END_TEST + +START_TEST(test_ifaddr_unconfigured_interface_has_no_addresses) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + + wolfIP_init(&s); + mock_link_init(&s); + + /* IPADDR_ANY is "unconfigured", not an address. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 0); + ck_assert_int_lt(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 0, &info), 0); +} +END_TEST + +START_TEST(test_ifaddr_add4_sets_the_primary_when_unconfigured) +{ + struct wolfIP s; + ip4 ip = 0, mask = 0, gw = 0; + + wolfIP_init(&s); + mock_link_init(&s); + + /* With no primary yet, an add lands in ipconf so that a single-address + * build is usable through the new API alone. */ + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_A, 24), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); + + wolfIP_ipconfig_get_ex(&s, TEST_PRIMARY_IF, &ip, &mask, &gw); + ck_assert_uint_eq(ip, IFA_IP_A); + ck_assert_uint_eq(mask, IFA_MASK); +} +END_TEST + +START_TEST(test_ifaddr_add4_rejects_a_duplicate) +{ + struct wolfIP s; + + ifaddr_setup(&s); + /* Already the primary. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_A, 24), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); +} +END_TEST + +START_TEST(test_ifaddr_del4_clears_the_primary) +{ + struct wolfIP s; + ip4 ip = 0, mask = 0, gw = 0; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_del4(&s, TEST_PRIMARY_IF, IFA_IP_A), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 0); + + wolfIP_ipconfig_get_ex(&s, TEST_PRIMARY_IF, &ip, &mask, &gw); + ck_assert_uint_eq(ip, IPADDR_ANY); + + /* Deleting an address that is not configured is an error, not a + * silent success. */ + ck_assert_int_lt(wolfIP_ifaddr_del4(&s, TEST_PRIMARY_IF, IFA_IP_A), 0); +} +END_TEST + +START_TEST(test_ifaddr_is_local4_matches_the_primary) +{ + struct wolfIP s; + unsigned int found = 0xFFFFFFFFu; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_A, &found), 1); + ck_assert_uint_eq(found, TEST_PRIMARY_IF); + + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_C, &found), 0); + /* The out parameter is optional. */ + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_A, NULL), 1); + /* IPADDR_ANY is never one of ours, even on an unconfigured stack. */ + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IPADDR_ANY, NULL), 0); +} +END_TEST + +START_TEST(test_ifaddr_capacity_is_one_without_multiconf) +{ + struct wolfIP s; + + ifaddr_setup(&s); +#if WOLFIP_IF_MULTICONF + /* With the feature on there is room for aliases; covered below. */ + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); +#else + /* With the feature off an interface holds exactly one address, and a + * second must be refused rather than silently replacing the first. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_A, NULL), 1); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_B, NULL), 0); +#endif +} +END_TEST + +START_TEST(test_ifaddr_rejects_invalid_arguments) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + ip6 a6; + + ifaddr_setup(&s); + ip6_set_loopback(&a6); + + ck_assert_int_lt(wolfIP_ifaddr_add4(NULL, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_int_lt(wolfIP_ifaddr_del4(NULL, TEST_PRIMARY_IF, IFA_IP_A), 0); + ck_assert_int_lt(wolfIP_ifaddr_add6(NULL, TEST_PRIMARY_IF, &a6, 64), 0); + ck_assert_int_lt(wolfIP_ifaddr_del6(NULL, TEST_PRIMARY_IF, &a6), 0); + ck_assert_int_lt(wolfIP_ifaddr_get(NULL, TEST_PRIMARY_IF, AF_INET, 0, &info), 0); + ck_assert_int_eq(wolfIP_ifaddr_count(NULL, TEST_PRIMARY_IF, AF_INET), 0); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(NULL, IFA_IP_A, NULL), 0); + + /* Out-of-range interface index. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, 99, IFA_IP_B, 24), 0); + ck_assert_int_lt(wolfIP_ifaddr_get(&s, 99, AF_INET, 0, &info), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, 99, AF_INET), 0); + + /* NULL address pointers and output buffer. */ + ck_assert_int_lt(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, NULL, 64), 0); + ck_assert_int_lt(wolfIP_ifaddr_del6(&s, TEST_PRIMARY_IF, NULL), 0); + ck_assert_int_lt(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 0, NULL), 0); + + /* Nonsense prefix lengths. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 33), 0); + ck_assert_int_lt(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &a6, 129), 0); + + /* Unknown family. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, 1234), 0); + ck_assert_int_lt(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, 1234, 0, &info), 0); + + /* Index past the end. */ + ck_assert_int_lt(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 99, &info), 0); +} +END_TEST + +START_TEST(test_ifaddr_v6_requires_ipv6_enabled) +{ + struct wolfIP s; + ip6 a6; + + ifaddr_setup(&s); + ck_assert_int_eq(atoip6("2001:db8::1", &a6), 0); + +#if WOLFIP_IPV6 + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &a6, 64), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_int_eq(wolfIP_ifaddr_del6(&s, TEST_PRIMARY_IF, &a6), 0); +#else + /* Without IPv6 compiled in there is nowhere to put a v6 address, and + * the call must say so rather than appear to succeed. */ + ck_assert_int_lt(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &a6, 64), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 0); + ck_assert_int_lt(wolfIP_ifaddr_del6(&s, TEST_PRIMARY_IF, &a6), 0); +#endif +} +END_TEST + +/* ========================================================================= + * IPv4 aliasing - needs WOLFIP_IF_MULTICONF, not IPv6 + * ========================================================================= */ +#if WOLFIP_IF_MULTICONF + +START_TEST(test_ifaddr_multiple_ipv4_addresses_on_one_interface) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + ip4 ip = 0, mask = 0, gw = 0; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_C, 16), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 3); + + /* Index 0 is always the primary; aliases follow in insertion order. */ + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 0, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_A); + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 1, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_B); + ck_assert_uint_eq(info.prefix_len, 24); + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 2, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_C); + ck_assert_uint_eq(info.prefix_len, 16); + + /* Adding an alias must not disturb the primary, which is what every + * existing caller of wolfIP_ipconfig_get_ex still reads. */ + wolfIP_ipconfig_get_ex(&s, TEST_PRIMARY_IF, &ip, &mask, &gw); + ck_assert_uint_eq(ip, IFA_IP_A); + ck_assert_uint_eq(mask, IFA_MASK); +} +END_TEST + +START_TEST(test_ifaddr_aliases_are_local_addresses) +{ + struct wolfIP s; + unsigned int found = 0xFFFFFFFFu; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + + /* An alias has to count as one of ours, or inbound traffic addressed + * to it is dropped and the alias is decorative. */ + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_B, &found), 1); + ck_assert_uint_eq(found, TEST_PRIMARY_IF); +} +END_TEST + +START_TEST(test_ifaddr_alias_removal_leaves_the_others) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_C, 16), 0); + + /* Remove the middle one. */ + ck_assert_int_eq(wolfIP_ifaddr_del4(&s, TEST_PRIMARY_IF, IFA_IP_B), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 2); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_B, NULL), 0); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_A, NULL), 1); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_C, NULL), 1); + + /* The surviving alias is still reachable by index, with no hole. */ + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 1, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_C); +} +END_TEST + +START_TEST(test_ifaddr_clearing_the_primary_keeps_aliases) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + + /* Removing the primary must not take the aliases with it. The first + * remaining address becomes index 0. */ + ck_assert_int_eq(wolfIP_ifaddr_del4(&s, TEST_PRIMARY_IF, IFA_IP_A), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET, 0, &info), 0); + ck_assert_uint_eq(info.v4, IFA_IP_B); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_B, NULL), 1); +} +END_TEST + +START_TEST(test_ifaddr_per_interface_capacity_is_enforced) +{ + struct wolfIP s; + unsigned int i; + + ifaddr_setup(&s); + /* The primary counts towards WOLFIP_IF_CONF_MAX, so there is room for + * that many minus one aliases. */ + for (i = 1; i < WOLFIP_IF_CONF_MAX; i++) { + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, + IFA_IP_A + i, 24), 0); + } + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), + WOLFIP_IF_CONF_MAX); + /* One more must be refused, not silently dropped or overflow. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_C, 24), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), + WOLFIP_IF_CONF_MAX); +} +END_TEST + +START_TEST(test_ifaddr_interfaces_do_not_share_addresses) +{ + struct wolfIP s; + unsigned int found = 0xFFFFFFFFu; + + ifaddr_setup(&s); + wolfIP_ipconfig_set_ex(&s, TEST_SECOND_IF, IFA_IP_C, 0xFFFF0000U, 0); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_SECOND_IF, IFA_IP_B, 16), 0); + + /* An alias belongs to exactly one interface. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_SECOND_IF, AF_INET), 2); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_B, &found), 1); + ck_assert_uint_eq(found, TEST_SECOND_IF); + + /* Deleting it from the wrong interface must fail. */ + ck_assert_int_lt(wolfIP_ifaddr_del4(&s, TEST_PRIMARY_IF, IFA_IP_B), 0); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_B, NULL), 1); +} +END_TEST + +START_TEST(test_ifaddr_the_same_alias_may_not_be_added_twice) +{ + struct wolfIP s; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 2); + /* Nor may an alias collide with the primary. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_A, 24), 0); +} +END_TEST + +START_TEST(test_ifaddr_pool_is_shared_across_interfaces) +{ + struct wolfIP s; + unsigned int added = 0; + unsigned int i; + + ifaddr_setup(&s); + wolfIP_ipconfig_set_ex(&s, TEST_SECOND_IF, IFA_IP_C, 0xFFFF0000U, 0); + + /* The pool is a flat shared array, so it can be exhausted from one + * interface. Filling it must never corrupt another interface's list. */ + for (i = 0; i < (WOLFIP_IFADDR_MAX + 4u); i++) { + if (wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, + 0x0B000000U + i, 24) == 0) + added++; + } + ck_assert_uint_lt(added, WOLFIP_IF_CONF_MAX); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), + added + 1u); + ck_assert_int_eq(wolfIP_ifaddr_is_local4(&s, IFA_IP_C, NULL), 1); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_SECOND_IF, AF_INET), 1); +} +END_TEST + +START_TEST(test_ifaddr_socket_binds_to_an_alias_on_the_right_interface) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in sin; + struct tsocket *ts; + int fd; + + ifaddr_setup(&s); + wolfIP_ipconfig_set_ex(&s, TEST_SECOND_IF, IFA_IP_C, 0xFFFF0000U, 0); + /* The alias lives on the *second* interface, not the primary. */ + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_SECOND_IF, IFA_IP_B, 16), 0); + + fd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_ge(fd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(7777); + sin.sin_addr.s_addr = ee32(IFA_IP_B); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + + /* Binding to an alias must resolve to the interface that owns it. + * Without the alias list this silently fell back to the primary + * interface, which is the bug that makes an alias decorative. */ + ts = wolfIP_socket_from_fd(&s, fd); + ck_assert_ptr_nonnull(ts); + ck_assert_uint_eq(ts->if_idx, TEST_SECOND_IF); + ck_assert_uint_eq(ts->local_ip, IFA_IP_B); +} +END_TEST + +/* ========================================================================= + * bind() and source selection with several addresses on one interface + * ========================================================================= + * Reference behaviour (Linux, and what POSIX implies for the TCP 4-tuple): + * + * - Binding to a specific local address restricts the socket to that + * address. Traffic to a *different* local address on the same + * interface must not be delivered to it. + * - Binding to INADDR_ANY is a wildcard: the socket receives traffic + * addressed to any local address. + * - Outbound traffic from a socket bound to a specific address uses that + * address as its source (RFC 6724 rule 1 states this explicitly for + * IPv6; for IPv4 it is RFC 1122 section 3.3.4.3 plus long-standing + * practice). + * - Binding to an address that is not local must fail. + */ + +#define IFA_REMOTE 0x0A0A0A63U /* 10.10.10.99 */ + +static int ifaddr_udp_bind(struct wolfIP *s, ip4 addr, uint16_t port) +{ + struct wolfIP_sockaddr_in sin; + int fd = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + + ck_assert_int_ge(fd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(port); + sin.sin_addr.s_addr = ee32(addr); + if (wolfIP_sock_bind(s, fd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)) != 0) + return -1; + return fd; +} + +/* Did a datagram sent to dst_ip reach the socket? */ +static int ifaddr_udp_delivered(struct wolfIP *s, int fd, ip4 dst_ip, + uint16_t port) +{ + static const uint8_t payload[4] = {'p', 'i', 'n', 'g'}; + uint8_t buf[16]; + + inject_udp_datagram(s, TEST_PRIMARY_IF, IFA_REMOTE, dst_ip, 4444, port, + payload, sizeof(payload)); + return (wolfIP_sock_recvfrom(s, fd, buf, sizeof(buf), 0, NULL, NULL) > 0); +} + +START_TEST(test_ifaddr_bind_to_alias_receives_only_its_own_traffic) +{ + struct wolfIP s; + int fd; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + + fd = ifaddr_udp_bind(&s, IFA_IP_B, 5001); + ck_assert_int_ge(fd, 0); + + /* Traffic to the bound alias arrives. */ + ck_assert_int_eq(ifaddr_udp_delivered(&s, fd, IFA_IP_B, 5001), 1); + /* Traffic to the primary on the same interface must not, or binding to + * an address would mean nothing. */ + ck_assert_int_eq(ifaddr_udp_delivered(&s, fd, IFA_IP_A, 5001), 0); +} +END_TEST + +START_TEST(test_ifaddr_bind_to_primary_does_not_receive_alias_traffic) +{ + struct wolfIP s; + int fd; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + + fd = ifaddr_udp_bind(&s, IFA_IP_A, 5002); + ck_assert_int_ge(fd, 0); + ck_assert_int_eq(ifaddr_udp_delivered(&s, fd, IFA_IP_A, 5002), 1); + ck_assert_int_eq(ifaddr_udp_delivered(&s, fd, IFA_IP_B, 5002), 0); +} +END_TEST + +START_TEST(test_ifaddr_bind_to_a_foreign_address_is_refused) +{ + struct wolfIP s; + + ifaddr_setup(&s); + /* Not configured anywhere on this stack. */ + ck_assert_int_lt(ifaddr_udp_bind(&s, IFA_REMOTE, 5003), 0); + /* An alias that has been removed again is equally foreign. */ + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + ck_assert_int_eq(wolfIP_ifaddr_del4(&s, TEST_PRIMARY_IF, IFA_IP_B), 0); + ck_assert_int_lt(ifaddr_udp_bind(&s, IFA_IP_B, 5004), 0); +} +END_TEST + +START_TEST(test_ifaddr_sendto_from_an_alias_uses_it_as_source) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in dst; + struct wolfIP_ip_packet *ip; + static const uint8_t payload[4] = {'d', 'a', 't', 'a'}; + int fd; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + fd = ifaddr_udp_bind(&s, IFA_IP_B, 5005); + ck_assert_int_ge(fd, 0); + + /* Teach ARP the peer so the datagram can actually leave. */ + arp_store_neighbor(&s, TEST_PRIMARY_IF, IFA_REMOTE, ifaddr_peer_mac); + + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = ee16(4444); + dst.sin_addr.s_addr = ee32(IFA_REMOTE); + last_frame_sent_size = 0; + ck_assert_int_gt(wolfIP_sock_sendto(&s, fd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&dst, + sizeof(dst)), 0); + wolfIP_poll(&s, 1000); + + /* The source address on the wire must be the address the socket was + * bound to, not the interface's primary. */ + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); + ip = (struct wolfIP_ip_packet *)last_frame_sent; + ck_assert_uint_eq(ee32(ip->src), IFA_IP_B); +} +END_TEST + +START_TEST(test_ifaddr_wildcard_bind_receives_traffic_to_any_local_address) +{ + struct wolfIP s; + int fd; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + + fd = ifaddr_udp_bind(&s, IPADDR_ANY, 5006); + ck_assert_int_ge(fd, 0); + + /* A wildcard bind must accept traffic to every local address, which is + * what INADDR_ANY means on every other stack. */ + ck_assert_int_eq(ifaddr_udp_delivered(&s, fd, IFA_IP_A, 5006), 1); + ck_assert_int_eq(ifaddr_udp_delivered(&s, fd, IFA_IP_B, 5006), 1); +} +END_TEST + +START_TEST(test_ifaddr_tcp_wildcard_listener_accepts_connections_to_an_alias) +{ + struct wolfIP s; + struct wolfIP_sockaddr_in sin; + int fd; + + ifaddr_setup(&s); + ck_assert_int_eq(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); + + fd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, 0); + ck_assert_int_ge(fd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5007); + sin.sin_addr.s_addr = ee32(IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_bind(&s, fd, (struct wolfIP_sockaddr *)&sin, + sizeof(sin)), 0); + ck_assert_int_eq(wolfIP_sock_listen(&s, fd, 1), 0); + + /* A wildcard listener records bound_local_ip == IPADDR_ANY, and the SYN + * path filters on that rather than on local_ip, so a connection to an + * alias is accepted. */ + ck_assert_uint_eq(s.tcpsockets[SOCKET_UNMARK(fd)].bound_local_ip, + IPADDR_ANY); +} +END_TEST + +#endif /* WOLFIP_IF_MULTICONF */ + +/* ========================================================================= + * IPv6 addresses in the list + * ========================================================================= */ +#if WOLFIP_IPV6 + +START_TEST(test_ifaddr_v6_addresses_are_independent_of_v4) +{ + struct wolfIP s; + struct wolfIP_ifaddr_info info; + ip6 ll6; + ip6 gua; + + ifaddr_setup(&s); + ck_assert_int_eq(atoip6("fe80::1", &ll6), 0); + ck_assert_int_eq(atoip6("2001:db8::1", &gua), 0); + + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &ll6, 64), 0); + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &gua, 64), 0); + + /* A link-local and a global address coexisting is the normal IPv6 case + * and is the reason this feature exists at all. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 2); + /* The IPv4 list is untouched by any of it. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET), 1); + + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET6, 0, &info), 0); + ck_assert_uint_eq(info.family, AF_INET6); + ck_assert_int_eq(ip6_cmp(&info.v6, &ll6), 0); + ck_assert_uint_eq(info.prefix_len, 64); + ck_assert_int_eq(wolfIP_ifaddr_get(&s, TEST_PRIMARY_IF, AF_INET6, 1, &info), 0); + ck_assert_int_eq(ip6_cmp(&info.v6, &gua), 0); +} +END_TEST + +START_TEST(test_ifaddr_v6_duplicate_is_rejected_and_removal_works) +{ + struct wolfIP s; + ip6 a6; + + ifaddr_setup(&s); + ck_assert_int_eq(atoip6("2001:db8::1", &a6), 0); + + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &a6, 64), 0); + ck_assert_int_lt(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &a6, 64), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + + ck_assert_int_eq(wolfIP_ifaddr_del6(&s, TEST_PRIMARY_IF, &a6), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 0); + ck_assert_int_lt(wolfIP_ifaddr_del6(&s, TEST_PRIMARY_IF, &a6), 0); +} +END_TEST + +START_TEST(test_ifaddr_v6_link_local_address_is_scoped_per_interface) +{ + struct wolfIP s; + ip6 ll6; + + ifaddr_setup(&s); + mock_link_init_idx(&s, TEST_SECOND_IF, NULL); + ck_assert_int_eq(atoip6("fe80::1", &ll6), 0); + + /* RFC 4007 section 5: the zone is part of a link-local address's + * identity, so the same numeric address may exist on two links. */ + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &ll6, 64), 0); + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_SECOND_IF, &ll6, 64), 0); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_SECOND_IF, AF_INET6), 1); +} +END_TEST + +START_TEST(test_ifaddr_v6_and_v4_share_the_per_interface_budget) +{ + struct wolfIP s; + ip6 a6; + unsigned int i; + unsigned int added = 0; + + ifaddr_setup(&s); + ip6_set_unspecified(&a6); + + /* Both families draw on the same WOLFIP_IF_CONF_MAX budget, so a v6 + * address consumes a slot a v4 alias could have used. */ + for (i = 0; i < (WOLFIP_IF_CONF_MAX + 2u); i++) { + a6.addr[0] = 0x20; + a6.addr[1] = 0x01; + a6.addr[15] = (uint8_t)(i + 1u); + if (wolfIP_ifaddr_add6(&s, TEST_PRIMARY_IF, &a6, 64) == 0) + added++; + } + ck_assert_uint_eq(added, WOLFIP_IF_CONF_MAX - 1u); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), added); + /* And with the budget spent, a v4 alias no longer fits. */ + ck_assert_int_lt(wolfIP_ifaddr_add4(&s, TEST_PRIMARY_IF, IFA_IP_B, 24), 0); +} +END_TEST + +#endif /* WOLFIP_IPV6 */ diff --git a/src/test/unit/unit_tests_ip_arp_recv.c b/src/test/unit/unit_tests_ip_arp_recv.c index e2705a87..b8e7b57e 100644 --- a/src/test/unit/unit_tests_ip_arp_recv.c +++ b/src/test/unit/unit_tests_ip_arp_recv.c @@ -729,6 +729,7 @@ END_TEST START_TEST(test_ip_recv_loopback_dst_on_non_loopback_dropped) { struct wolfIP s; + struct tsocket *ctrl; struct tsocket *ts; ip4 local_ip = 0x0A000001U; ip4 remote_ip = 0x0A000002U; @@ -738,16 +739,33 @@ START_TEST(test_ip_recv_loopback_dst_on_non_loopback_dropped) mock_link_init(&s); wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + /* Positive control on its own port: an otherwise identical datagram to + * an ordinary local address must be delivered. Without it, the drop + * assertion below would also pass if the frame were malformed, the + * socket misconfigured, or ip_recv rejecting everything. */ + ctrl = udp_new_socket(&s); + ck_assert_ptr_nonnull(ctrl); + ctrl->src_port = 1234; + ctrl->local_ip = local_ip; + + /* The socket under test is set to accept exactly the loopback + * destination, so if ip_recv let the frame through it would be + * delivered. Non-delivery can therefore only be ip_recv's martian + * filter, which is the behaviour this test is named for. */ ts = udp_new_socket(&s); ck_assert_ptr_nonnull(ts); - ts->src_port = 1234; - ts->local_ip = IPADDR_ANY; - - /* Inject from non-loopback interface to loopback destination */ - inject_udp_datagram(&s, TEST_PRIMARY_IF, remote_ip, loop_dst, - 9999, 1234, NULL, 0); - - /* Must be dropped — loopback addresses must not arrive on wire */ + ts->src_port = 1235; + ts->local_ip = loop_dst; + + recv_udp_datagram(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + 9999, 1234, NULL, 0); + ck_assert_ptr_nonnull(fifo_peek(&ctrl->sock.udp.rxbuf)); + + /* RFC 5735 section 4 / RFC 6890: 127/8 is host loopback and must never + * appear on the wire. Delivered through the real ingress path, so + * ip_recv actually runs. */ + recv_udp_datagram(&s, TEST_PRIMARY_IF, remote_ip, loop_dst, + 9999, 1235, NULL, 0); ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); ck_assert_uint_eq(ts->events & CB_EVENT_READABLE, 0); } @@ -761,23 +779,38 @@ END_TEST START_TEST(test_ip_recv_loopback_src_on_non_loopback_dropped) { struct wolfIP s; + struct tsocket *ctrl; struct tsocket *ts; ip4 local_ip = 0x0A000001U; - ip4 loop_src = 0x7F000002U; /* 127.0.0.2 as source */ + ip4 remote_ip = 0x0A000002U; + ip4 loop_src = 0x7F000001U; /* 127.0.0.1 */ wolfIP_init(&s); mock_link_init(&s); wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + /* Positive control: same destination, ordinary source. */ + ctrl = udp_new_socket(&s); + ck_assert_ptr_nonnull(ctrl); + ctrl->src_port = 1234; + ctrl->local_ip = local_ip; + ts = udp_new_socket(&s); ck_assert_ptr_nonnull(ts); - ts->src_port = 1234; - ts->local_ip = IPADDR_ANY; + ts->src_port = 1235; + ts->local_ip = local_ip; - inject_udp_datagram(&s, TEST_PRIMARY_IF, loop_src, local_ip, - 9999, 1234, NULL, 0); + recv_udp_datagram(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + 9999, 1234, NULL, 0); + ck_assert_ptr_nonnull(fifo_peek(&ctrl->sock.udp.rxbuf)); + /* Only the source changes. The symmetric source check is what stops an + * off-link attacker forging src=127.0.0.1 to impersonate locally + * originated traffic to higher-layer code. */ + recv_udp_datagram(&s, TEST_PRIMARY_IF, loop_src, local_ip, + 9999, 1235, NULL, 0); ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); + ck_assert_uint_eq(ts->events & CB_EVENT_READABLE, 0); } END_TEST @@ -892,12 +925,33 @@ START_TEST(test_ip_recv_dest_matches_secondary_iface_ip_is_local) ts->src_port = 1234; ts->local_ip = secondary_ip; /* listening on secondary IP */ - /* Inject on primary iface, dst=secondary IP → local, not forwarded */ - inject_udp_datagram(&s, TEST_PRIMARY_IF, remote_src, secondary_ip, - 9999, 1234, NULL, 0); + /* Arrives on the primary interface addressed to the *secondary* + * interface's own IP. Delivered through the real ingress path so that + * ip_recv's is_local decision is the thing under test: the packet must + * be delivered locally and must not be forwarded. */ + last_frame_sent_size = 0; + recv_udp_datagram(&s, TEST_PRIMARY_IF, remote_src, secondary_ip, + 9999, 1234, NULL, 0); - /* Must be delivered locally */ ck_assert_int_ne(ts->events & CB_EVENT_READABLE, 0); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); + + /* Control for the assertion above: an address on the same subnet that + * is NOT ours does leave the stack (as a forwarded frame, or as the ARP + * request that precedes one). Without this, "nothing was sent" would + * also hold if the setup were simply incapable of forwarding. + * + * Note this test asserts the observable contract - delivered locally, + * not forwarded - rather than any single internal decision. The stack + * reaches that outcome by two independent routes: ip_recv's is_local + * check, and wolfIP_forward_interface() declining a destination that is + * one of our own addresses. Defeating either one alone leaves the + * behaviour correct. */ + wolfIP_poll(&s, 2000); /* past the 1/s ARP request rate limit */ + last_frame_sent_size = 0; + recv_udp_datagram(&s, TEST_PRIMARY_IF, remote_src, secondary_ip + 0x31U, + 9999, 1234, NULL, 0); + ck_assert_uint_gt((uint32_t)last_frame_sent_size, 0u); } END_TEST diff --git a/src/test/unit/unit_tests_ipv6_addr.c b/src/test/unit/unit_tests_ipv6_addr.c new file mode 100644 index 00000000..22061c0e --- /dev/null +++ b/src/test/unit/unit_tests_ipv6_addr.c @@ -0,0 +1,852 @@ +/* unit_tests_ipv6_addr.c + * + * Copyright (C) 2026 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 + */ + +/* ========================================================================= + * Environment note + * ========================================================================= + * These tests cover the IPv6 addressing layer in wolfip6.h. That header holds + * only a typedef and static inline functions and is included unconditionally + * from wolfip.h, so this file is deliberately NOT gated on WOLFIP_IPV6: the + * address layer is exercised by the default `make unit` build, and therefore + * by every compiler in the CI matrix, on macOS and FreeBSD, and under the + * sanitizers, without needing the IPv6 stack to be enabled. + * + * Reference material: RFC 4291 (addressing architecture), RFC 4193 (unique + * local addresses), RFC 3587 (global unicast format), RFC 2464 section 7 + * (Ethernet multicast mapping), RFC 4862 section 5.5.3 (address formation) + * and RFC 5952 (canonical text representation). + */ + +/* ========================================================================= + * Local helpers + * ========================================================================= */ + +/* Parse a literal that the test asserts is well formed. */ +static ip6 ip6_lit(const char *s) +{ + ip6 a; + + ck_assert_int_eq(atoip6(s, &a), 0); + return a; +} + +/* Assert that a literal parses and renders back to the expected canonical + * form. `in` and `expect` differ whenever `in` is not already canonical. */ +static void ip6_check_text(const char *in, const char *expect) +{ + char buf[WOLFIP_IP6_ADDRSTRLEN]; + ip6 a; + + ck_assert_int_eq(atoip6(in, &a), 0); + ip6toa(&a, buf); + ck_assert_str_eq(buf, expect); +} + +/* Assert that a literal is rejected. */ +static void ip6_check_bad(const char *in) +{ + ip6 a; + + ip6_set_loopback(&a); /* poison, so a silent success is visible */ + ck_assert_int_eq(atoip6(in, &a), -1); +} + +/* ========================================================================= + * Basic operations + * ========================================================================= */ + +START_TEST(test_ip6_cmp_and_copy) +{ + ip6 a = ip6_lit("2001:db8::1"); + ip6 b = ip6_lit("2001:db8::1"); + ip6 c = ip6_lit("2001:db8::2"); + ip6 dst; + + ck_assert_int_eq(ip6_cmp(&a, &b), 0); + ck_assert_int_ne(ip6_cmp(&a, &c), 0); + /* Ordering is stable and antisymmetric. */ + ck_assert_int_lt(ip6_cmp(&a, &c), 0); + ck_assert_int_gt(ip6_cmp(&c, &a), 0); + + ip6_set_unspecified(&dst); + ip6_copy(&dst, &a); + ck_assert_int_eq(ip6_cmp(&dst, &a), 0); +} +END_TEST + +START_TEST(test_ip6_cmp_differs_in_every_byte_position) +{ + int i; + + /* A byte-wise comparison must not miss a difference at any offset. */ + for (i = 0; i < 16; i++) { + ip6 a; + ip6 b; + + ip6_set_unspecified(&a); + ip6_set_unspecified(&b); + b.addr[i] = 0x01; + ck_assert_int_ne(ip6_cmp(&a, &b), 0); + ck_assert_int_lt(ip6_cmp(&a, &b), 0); + } +} +END_TEST + +START_TEST(test_ip6_set_wellknown_addresses) +{ + ip6 a; + ip6 ref; + + ip6_set_unspecified(&a); + ref = ip6_lit("::"); + ck_assert_int_eq(ip6_cmp(&a, &ref), 0); + + ip6_set_loopback(&a); + ref = ip6_lit("::1"); + ck_assert_int_eq(ip6_cmp(&a, &ref), 0); + + ip6_set_all_nodes(&a); + ref = ip6_lit("ff02::1"); + ck_assert_int_eq(ip6_cmp(&a, &ref), 0); + + ip6_set_all_routers(&a); + ref = ip6_lit("ff02::2"); + ck_assert_int_eq(ip6_cmp(&a, &ref), 0); +} +END_TEST + +START_TEST(test_ip6_init_macros_match_setters) +{ + ip6 any = WOLFIP_IN6ADDR_ANY_INIT; + ip6 lo = WOLFIP_IN6ADDR_LOOPBACK_INIT; + ip6 ref; + + ip6_set_unspecified(&ref); + ck_assert_int_eq(ip6_cmp(&any, &ref), 0); + ip6_set_loopback(&ref); + ck_assert_int_eq(ip6_cmp(&lo, &ref), 0); +} +END_TEST + +/* ========================================================================= + * Type and scope predicates + * ========================================================================= */ + +START_TEST(test_ip6_is_unspecified) +{ + ip6 a = ip6_lit("::"); + ip6 b = ip6_lit("::1"); + int i; + + ck_assert_int_eq(ip6_is_unspecified(&a), 1); + ck_assert_int_eq(ip6_is_unspecified(&b), 0); + ck_assert_int_eq(ip6_is_unicast(&a), 0); + + /* A single non-zero byte anywhere disqualifies it. */ + for (i = 0; i < 16; i++) { + ip6 c; + + ip6_set_unspecified(&c); + c.addr[i] = 0x01; + ck_assert_int_eq(ip6_is_unspecified(&c), 0); + } +} +END_TEST + +START_TEST(test_ip6_is_loopback) +{ + ip6 a = ip6_lit("::1"); + ip6 b = ip6_lit("::2"); + ip6 c = ip6_lit("::"); + ip6 d = ip6_lit("1::1"); + + ck_assert_int_eq(ip6_is_loopback(&a), 1); + ck_assert_int_eq(ip6_is_loopback(&b), 0); + ck_assert_int_eq(ip6_is_loopback(&c), 0); + ck_assert_int_eq(ip6_is_loopback(&d), 0); +} +END_TEST + +START_TEST(test_ip6_is_multicast) +{ + ip6 a = ip6_lit("ff02::1"); + ip6 b = ip6_lit("ff00::"); + ip6 c = ip6_lit("fe80::1"); + ip6 d = ip6_lit("2001:db8::1"); + + ck_assert_int_eq(ip6_is_multicast(&a), 1); + ck_assert_int_eq(ip6_is_multicast(&b), 1); + ck_assert_int_eq(ip6_is_multicast(&c), 0); + ck_assert_int_eq(ip6_is_multicast(&d), 0); + /* Multicast is never a valid unicast source. */ + ck_assert_int_eq(ip6_is_unicast(&a), 0); +} +END_TEST + +START_TEST(test_ip6_is_link_local_covers_whole_fe80_10) +{ + /* fe80::/10 spans fe80:: through febf:ffff:... */ + ip6 lo = ip6_lit("fe80::"); + ip6 hi = ip6_lit("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ip6 just_below = ip6_lit("fe7f:ffff::"); + ip6 just_above = ip6_lit("fec0::"); + + ck_assert_int_eq(ip6_is_link_local(&lo), 1); + ck_assert_int_eq(ip6_is_link_local(&hi), 1); + ck_assert_int_eq(ip6_is_link_local(&just_below), 0); + /* fec0::/10 is the deprecated site-local range, not link-local. */ + ck_assert_int_eq(ip6_is_link_local(&just_above), 0); +} +END_TEST + +START_TEST(test_ip6_is_ula_covers_whole_fc00_7) +{ + ip6 fc = ip6_lit("fc00::"); + ip6 fd = ip6_lit("fd00::1"); + ip6 hi = ip6_lit("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ip6 below = ip6_lit("fbff::"); + ip6 above = ip6_lit("fe00::"); + + ck_assert_int_eq(ip6_is_ula(&fc), 1); + ck_assert_int_eq(ip6_is_ula(&fd), 1); + ck_assert_int_eq(ip6_is_ula(&hi), 1); + ck_assert_int_eq(ip6_is_ula(&below), 0); + ck_assert_int_eq(ip6_is_ula(&above), 0); + /* A ULA is not global unicast. */ + ck_assert_int_eq(ip6_is_global(&fd), 0); +} +END_TEST + +START_TEST(test_ip6_is_global_covers_whole_2000_3) +{ + ip6 lo = ip6_lit("2000::"); + ip6 doc = ip6_lit("2001:db8::1"); + ip6 hi = ip6_lit("3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ip6 below = ip6_lit("1fff::"); + ip6 above = ip6_lit("4000::"); + + ck_assert_int_eq(ip6_is_global(&lo), 1); + ck_assert_int_eq(ip6_is_global(&doc), 1); + ck_assert_int_eq(ip6_is_global(&hi), 1); + ck_assert_int_eq(ip6_is_global(&below), 0); + ck_assert_int_eq(ip6_is_global(&above), 0); + ck_assert_int_eq(ip6_is_unicast(&doc), 1); +} +END_TEST + +START_TEST(test_ip6_multicast_flags_and_scope) +{ + ip6 iface = ip6_lit("ff01::1"); + ip6 link = ip6_lit("ff02::1"); + ip6 site = ip6_lit("ff05::1"); + ip6 global = ip6_lit("ff0e::1"); + ip6 transient = ip6_lit("ff12::1"); + + ck_assert_uint_eq(ip6_mcast_scope(&iface), WOLFIP_IP6_SCOPE_INTERFACE_LOCAL); + ck_assert_uint_eq(ip6_mcast_scope(&link), WOLFIP_IP6_SCOPE_LINK_LOCAL); + ck_assert_uint_eq(ip6_mcast_scope(&site), WOLFIP_IP6_SCOPE_SITE_LOCAL); + ck_assert_uint_eq(ip6_mcast_scope(&global), WOLFIP_IP6_SCOPE_GLOBAL); + + ck_assert_uint_eq(ip6_mcast_flags(&link), 0); + ck_assert_uint_eq(ip6_mcast_flags(&transient), 1); + + ck_assert_int_eq(ip6_is_mcast_link_local(&link), 1); + ck_assert_int_eq(ip6_is_mcast_link_local(&site), 0); + /* A unicast address is never link-local multicast. */ + ck_assert_int_eq(ip6_is_mcast_link_local(&(ip6){{0xfe, 0x80}}), 0); +} +END_TEST + +START_TEST(test_ip6_is_all_nodes_and_all_routers) +{ + ip6 nodes = ip6_lit("ff02::1"); + ip6 routers = ip6_lit("ff02::2"); + ip6 other = ip6_lit("ff02::3"); + ip6 wrong_scope = ip6_lit("ff05::1"); + + ck_assert_int_eq(ip6_is_all_nodes(&nodes), 1); + ck_assert_int_eq(ip6_is_all_nodes(&routers), 0); + ck_assert_int_eq(ip6_is_all_routers(&routers), 1); + ck_assert_int_eq(ip6_is_all_routers(&nodes), 0); + ck_assert_int_eq(ip6_is_all_nodes(&other), 0); + ck_assert_int_eq(ip6_is_all_nodes(&wrong_scope), 0); +} +END_TEST + +/* ========================================================================= + * IPv4-mapped and IPv4-compatible ranges + * ========================================================================= */ + +START_TEST(test_ip6_v4mapped_roundtrip) +{ + ip6 a; + ip6 parsed; + + ip6_set_v4mapped(&a, 0x0A0A0A02U); /* 10.10.10.2 */ + ck_assert_int_eq(ip6_is_v4mapped(&a), 1); + ck_assert_uint_eq(ip6_get_v4mapped(&a), 0x0A0A0A02U); + + /* The literal form must produce the identical byte pattern. */ + parsed = ip6_lit("::ffff:10.10.10.2"); + ck_assert_int_eq(ip6_cmp(&a, &parsed), 0); +} +END_TEST + +START_TEST(test_ip6_v4mapped_boundaries) +{ + ip6 prefix = ip6_lit("::ffff:0.0.0.0"); + ip6 broadcast = ip6_lit("::ffff:255.255.255.255"); + ip6 not_mapped = ip6_lit("::fffe:0:0"); + ip6 high_bit_set = ip6_lit("1::ffff:0:0"); + + ck_assert_int_eq(ip6_is_v4mapped(&prefix), 1); + ck_assert_int_eq(ip6_is_v4mapped(&broadcast), 1); + ck_assert_uint_eq(ip6_get_v4mapped(&broadcast), 0xFFFFFFFFU); + ck_assert_int_eq(ip6_is_v4mapped(¬_mapped), 0); + ck_assert_int_eq(ip6_is_v4mapped(&high_bit_set), 0); + + /* :: and ::1 must not be mistaken for the mapped range. */ + ck_assert_int_eq(ip6_is_v4mapped(&(ip6)WOLFIP_IN6ADDR_ANY_INIT), 0); + ck_assert_int_eq(ip6_is_v4mapped(&(ip6)WOLFIP_IN6ADDR_LOOPBACK_INIT), 0); +} +END_TEST + +START_TEST(test_ip6_v4compat_excludes_any_and_loopback) +{ + ip6 compat = ip6_lit("::1.2.3.4"); + ip6 any = ip6_lit("::"); + ip6 lo = ip6_lit("::1"); + ip6 mapped = ip6_lit("::ffff:1.2.3.4"); + + ck_assert_int_eq(ip6_is_v4compat(&compat), 1); + /* :: and ::1 are the unspecified and loopback addresses, not + * IPv4-compatible addresses, even though they sit in ::/96. */ + ck_assert_int_eq(ip6_is_v4compat(&any), 0); + ck_assert_int_eq(ip6_is_v4compat(&lo), 0); + ck_assert_int_eq(ip6_is_v4compat(&mapped), 0); +} +END_TEST + +/* ========================================================================= + * Prefix operations + * ========================================================================= */ + +START_TEST(test_ip6_prefix_cmp_byte_aligned) +{ + ip6 a = ip6_lit("2001:db8:1:2::5"); + ip6 b = ip6_lit("2001:db8:1:2::9"); + ip6 c = ip6_lit("2001:db8:1:3::5"); + + ck_assert_int_eq(ip6_prefix_cmp(&a, &b, 64), 0); + ck_assert_int_ne(ip6_prefix_cmp(&a, &b, 128), 0); + ck_assert_int_eq(ip6_prefix_cmp(&a, &c, 48), 0); + ck_assert_int_ne(ip6_prefix_cmp(&a, &c, 64), 0); +} +END_TEST + +START_TEST(test_ip6_prefix_cmp_non_byte_aligned) +{ + /* 2001:0db8 vs 2001:0dbf differ only in the low nibble of byte 3, so + * they agree on the first 28 bits but not on 32. */ + ip6 a = ip6_lit("2001:db8::1"); + ip6 b = ip6_lit("2001:dbf::2"); + + ck_assert_int_eq(ip6_prefix_cmp(&a, &b, 28), 0); + ck_assert_int_ne(ip6_prefix_cmp(&a, &b, 32), 0); + /* Bit 29 is the first difference. */ + ck_assert_int_eq(ip6_prefix_cmp(&a, &b, 29), 0); + ck_assert_int_ne(ip6_prefix_cmp(&a, &b, 30), 0); +} +END_TEST + +START_TEST(test_ip6_prefix_cmp_zero_and_clamped) +{ + ip6 a = ip6_lit("2001:db8::1"); + ip6 b = ip6_lit("fe80::abcd"); + + /* A zero-length prefix matches everything. */ + ck_assert_int_eq(ip6_prefix_cmp(&a, &b, 0), 0); + /* Lengths above 128 are clamped rather than reading out of bounds. */ + ck_assert_int_ne(ip6_prefix_cmp(&a, &b, 200), 0); + ck_assert_int_eq(ip6_prefix_cmp(&a, &a, 255), 0); +} +END_TEST + +START_TEST(test_ip6_prefix_mask) +{ + ip6 a = ip6_lit("2001:db8:1:2:3:4:5:6"); + ip6 expect; + + ip6_prefix_mask(&a, 64); + expect = ip6_lit("2001:db8:1:2::"); + ck_assert_int_eq(ip6_cmp(&a, &expect), 0); + + a = ip6_lit("2001:dbf::1"); + ip6_prefix_mask(&a, 28); + expect = ip6_lit("2001:db0::"); + ck_assert_int_eq(ip6_cmp(&a, &expect), 0); + + /* /0 clears everything, /128 changes nothing. */ + a = ip6_lit("2001:db8::1"); + ip6_prefix_mask(&a, 0); + ck_assert_int_eq(ip6_is_unspecified(&a), 1); + + a = ip6_lit("2001:db8::1"); + expect = a; + ip6_prefix_mask(&a, 128); + ck_assert_int_eq(ip6_cmp(&a, &expect), 0); +} +END_TEST + +START_TEST(test_ip6_prefix_mask_every_length_is_consistent) +{ + unsigned int plen; + + /* For every prefix length, including the non-byte-aligned ones, masking + * must preserve every bit inside the prefix and clear every bit outside + * it. Checked bit by bit rather than by comparing whole addresses, + * because masking is legitimately a no-op when the host part is already + * zero. */ + for (plen = 0; plen <= 128; plen++) { + ip6 orig = ip6_lit("2001:db8:aaaa:5555:1234:5678:9abc:def0"); + ip6 masked = orig; + unsigned int bit; + + ip6_prefix_mask(&masked, (uint8_t)plen); + ck_assert_int_eq(ip6_prefix_cmp(&orig, &masked, (uint8_t)plen), 0); + for (bit = 0; bit < 128; bit++) { + unsigned int got = (masked.addr[bit / 8] >> (7 - (bit % 8))) & 1u; + unsigned int want = (orig.addr[bit / 8] >> (7 - (bit % 8))) & 1u; + + if (bit >= plen) + want = 0; + ck_assert_uint_eq(got, want); + } + } +} +END_TEST + +START_TEST(test_ip6_prefix_mask_clears_a_set_host_part) +{ + /* The companion to the loop above: when the host part really does carry + * set bits, masking must visibly change the address. */ + ip6 a = ip6_lit("2001:db8::ffff:ffff:ffff:ffff"); + ip6 before = a; + ip6 expect = ip6_lit("2001:db8::"); + + ip6_prefix_mask(&a, 64); + ck_assert_int_ne(ip6_cmp(&a, &before), 0); + ck_assert_int_eq(ip6_cmp(&a, &expect), 0); +} +END_TEST + +START_TEST(test_ip6_make_addr_from_prefix_and_iid) +{ + ip6 prefix = ip6_lit("2001:db8:1:2::"); + ip6 iid = ip6_lit("::1122:33ff:fe44:5566"); + ip6 out; + ip6 expect; + + ip6_make_addr(&out, &prefix, 64, &iid); + expect = ip6_lit("2001:db8:1:2:1122:33ff:fe44:5566"); + ck_assert_int_eq(ip6_cmp(&out, &expect), 0); +} +END_TEST + +START_TEST(test_ip6_make_addr_non_byte_aligned_prefix) +{ + /* A /60 splits byte 7: the top nibble comes from the prefix, the bottom + * nibble from the interface identifier. */ + ip6 prefix = ip6_lit("2001:db8:1:20::"); + ip6 iid = ip6_lit("::f:1122:33ff:fe44:5566"); + ip6 out; + ip6 expect; + + ip6_make_addr(&out, &prefix, 60, &iid); + expect = ip6_lit("2001:db8:1:2f:1122:33ff:fe44:5566"); + ck_assert_int_eq(ip6_cmp(&out, &expect), 0); +} +END_TEST + +/* ========================================================================= + * Link layer mapping + * ========================================================================= */ + +START_TEST(test_ip6_solicited_node_from_target) +{ + ip6 target = ip6_lit("fe80::11:22ff:fe33:4455"); + ip6 sol; + ip6 expect; + + ip6_set_solicited_node(&sol, &target); + expect = ip6_lit("ff02::1:ff33:4455"); + ck_assert_int_eq(ip6_cmp(&sol, &expect), 0); + ck_assert_int_eq(ip6_is_solicited_node(&sol), 1); + ck_assert_int_eq(ip6_is_multicast(&sol), 1); +} +END_TEST + +START_TEST(test_ip6_solicited_node_depends_only_on_low_24_bits) +{ + ip6 a = ip6_lit("2001:db8::aabb:ccdd"); + ip6 b = ip6_lit("fe80::9999:99bb:ccdd"); + ip6 sa; + ip6 sb; + + /* Different addresses that share the low 24 bits map to the same + * solicited-node group - this collision is by design, and the neighbour + * cache must not assume the mapping is unique. */ + ip6_set_solicited_node(&sa, &a); + ip6_set_solicited_node(&sb, &b); + ck_assert_int_eq(ip6_cmp(&sa, &sb), 0); +} +END_TEST + +START_TEST(test_ip6_is_solicited_node_rejects_near_misses) +{ + ip6 good = ip6_lit("ff02::1:ff33:4455"); + ip6 wrong_scope = ip6_lit("ff05::1:ff33:4455"); + ip6 wrong_prefix = ip6_lit("ff02::2:ff33:4455"); + ip6 all_nodes = ip6_lit("ff02::1"); + ip6 transient = ip6_lit("ff12::1:ff33:4455"); + + ck_assert_int_eq(ip6_is_solicited_node(&good), 1); + ck_assert_int_eq(ip6_is_solicited_node(&wrong_scope), 0); + ck_assert_int_eq(ip6_is_solicited_node(&wrong_prefix), 0); + ck_assert_int_eq(ip6_is_solicited_node(&all_nodes), 0); + /* Flags must be zero for a solicited-node address. */ + ck_assert_int_eq(ip6_is_solicited_node(&transient), 0); +} +END_TEST + +START_TEST(test_ip6_mcast_to_eth_mapping) +{ + ip6 sol = ip6_lit("ff02::1:ff33:4455"); + ip6 nodes = ip6_lit("ff02::1"); + uint8_t mac[6]; + const uint8_t expect_sol[6] = {0x33, 0x33, 0xFF, 0x33, 0x44, 0x55}; + const uint8_t expect_nodes[6] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01}; + + ip6_mcast_to_eth(&sol, mac); + ck_assert_mem_eq(mac, expect_sol, 6); + + ip6_mcast_to_eth(&nodes, mac); + ck_assert_mem_eq(mac, expect_nodes, 6); +} +END_TEST + +START_TEST(test_ip6_iid_from_mac_eui64) +{ + /* RFC 4291 appendix A: 00:1b:21:0a:0b:0c becomes 021b:21ff:fe0a:0b0c, + * with the universal/local bit inverted. */ + const uint8_t mac[6] = {0x00, 0x1b, 0x21, 0x0a, 0x0b, 0x0c}; + ip6 iid; + ip6 prefix = ip6_lit("fe80::"); + ip6 out; + ip6 expect; + + ip6_iid_from_mac(&iid, mac); + ck_assert_uint_eq(iid.addr[8], 0x02); + ck_assert_uint_eq(iid.addr[11], 0xFF); + ck_assert_uint_eq(iid.addr[12], 0xFE); + + ip6_make_addr(&out, &prefix, 64, &iid); + expect = ip6_lit("fe80::21b:21ff:fe0a:b0c"); + ck_assert_int_eq(ip6_cmp(&out, &expect), 0); +} +END_TEST + +START_TEST(test_ip6_iid_from_mac_inverts_ul_bit_both_ways) +{ + const uint8_t local_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; + const uint8_t universal_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x01}; + ip6 iid; + + /* A locally administered MAC (bit set) yields a cleared bit. */ + ip6_iid_from_mac(&iid, local_mac); + ck_assert_uint_eq(iid.addr[8], 0x00); + /* A universal MAC (bit clear) yields a set bit. */ + ip6_iid_from_mac(&iid, universal_mac); + ck_assert_uint_eq(iid.addr[8], 0x02); +} +END_TEST + +/* ========================================================================= + * Text parsing - well formed input + * ========================================================================= */ + +START_TEST(test_ip6_parse_canonical_forms) +{ + ip6_check_text("::", "::"); + ip6_check_text("::1", "::1"); + ip6_check_text("1::", "1::"); + ip6_check_text("fe80::1", "fe80::1"); + ip6_check_text("2001:db8::1", "2001:db8::1"); + ip6_check_text("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8"); + ip6_check_text("ff02::1", "ff02::1"); +} +END_TEST + +START_TEST(test_ip6_parse_normalises_noncanonical_input) +{ + /* Leading zeros are dropped. */ + ip6_check_text("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::1"); + /* Uppercase is normalised to lowercase. */ + ip6_check_text("FE80::ABCD", "fe80::abcd"); + ip6_check_text("2001:DB8::1", "2001:db8::1"); + /* A fully written out zero address compresses. */ + ip6_check_text("0:0:0:0:0:0:0:0", "::"); +} +END_TEST + +START_TEST(test_ip6_parse_embedded_ipv4) +{ + ip6_check_text("::ffff:1.2.3.4", "::ffff:1.2.3.4"); + ip6_check_text("::ffff:0.0.0.0", "::ffff:0.0.0.0"); + ip6_check_text("::ffff:255.255.255.255", "::ffff:255.255.255.255"); + /* An IPv4-compatible address is not printed in dotted form. */ + ip6_check_text("::1.2.3.4", "::102:304"); + /* The dotted tail is also accepted in a non-compressed literal. */ + ip6_check_text("0:0:0:0:0:ffff:1.2.3.4", "::ffff:1.2.3.4"); +} +END_TEST + +START_TEST(test_ip6_parse_gap_at_every_position) +{ + /* "::" must work wherever it appears. Each of these elides two or more + * groups, so the compressed form survives the round trip unchanged. */ + ip6_check_text("::3:4:5:6:7:8", "::3:4:5:6:7:8"); + ip6_check_text("1::4:5:6:7:8", "1::4:5:6:7:8"); + ip6_check_text("1:2::5:6:7:8", "1:2::5:6:7:8"); + ip6_check_text("1:2:3::6:7:8", "1:2:3::6:7:8"); + ip6_check_text("1:2:3:4::7:8", "1:2:3:4::7:8"); + ip6_check_text("1:2:3:4:5::8", "1:2:3:4:5::8"); + ip6_check_text("1:2:3:4:5:6::", "1:2:3:4:5:6::"); +} +END_TEST + +START_TEST(test_ip6_parse_accepts_single_group_gap_but_writes_it_out) +{ + /* RFC 4291 allows "::" to stand for a single zero group on input, but + * RFC 5952 section 4.2.2 forbids compressing one group on output. Such + * literals are therefore accepted and normalised to the expanded form. + * This asymmetry is deliberate; it is the reason the round-trip tests + * above use gaps of two or more groups. */ + ip6_check_text("::2:3:4:5:6:7:8", "0:2:3:4:5:6:7:8"); + ip6_check_text("1::3:4:5:6:7:8", "1:0:3:4:5:6:7:8"); + ip6_check_text("1:2::4:5:6:7:8", "1:2:0:4:5:6:7:8"); + ip6_check_text("1:2:3:4:5:6::8", "1:2:3:4:5:6:0:8"); + ip6_check_text("1:2:3:4:5:6:7::", "1:2:3:4:5:6:7:0"); +} +END_TEST + +/* ========================================================================= + * Text parsing - malformed input must be rejected + * ========================================================================= */ + +START_TEST(test_ip6_parse_rejects_malformed) +{ + ip6_check_bad(""); + ip6_check_bad(":"); + ip6_check_bad(":1"); /* single leading colon */ + ip6_check_bad("1:"); /* trailing single colon */ + ip6_check_bad("1:::2"); /* three colons */ + ip6_check_bad("::1::2"); /* two "::" runs */ + ip6_check_bad("1::2::3"); + ip6_check_bad("gg::"); /* non-hex digit */ + ip6_check_bad("12345::"); /* group longer than four digits */ + ip6_check_bad("1:2:3:4:5:6:7:8:9"); /* too many groups */ + ip6_check_bad("1:2:3:4:5:6:7"); /* too few, no "::" */ + ip6_check_bad("1.2.3.4"); /* bare IPv4 is not an IPv6 literal */ +} +END_TEST + +START_TEST(test_ip6_parse_rejects_gap_that_elides_nothing) +{ + /* "::" must stand for at least one group of zeros, so a literal that + * already supplies all eight groups may not also contain one. */ + ip6_check_bad("1:2:3:4::5:6:7:8"); +} +END_TEST + +START_TEST(test_ip6_parse_rejects_bad_ipv4_tail) +{ + ip6_check_bad("::1.2.3"); /* too few octets */ + ip6_check_bad("::1.2.3.4.5"); /* too many octets */ + ip6_check_bad("::ffff:256.1.1.1"); /* octet out of range */ + ip6_check_bad("::ffff:1.2.3."); /* trailing dot */ + ip6_check_bad("::ffff:1.2.3.4:5"); /* garbage after the quad */ + ip6_check_bad("::ffff:0001.2.3.4"); /* over-long octet */ + ip6_check_bad("1.2.3.4::"); +} +END_TEST + +START_TEST(test_ip6_parse_rejects_null_arguments) +{ + ip6 a; + + ck_assert_int_eq(atoip6(NULL, &a), -1); + ck_assert_int_eq(atoip6("::1", NULL), -1); +} +END_TEST + +START_TEST(test_ip6_parse_leaves_output_untouched_on_failure) +{ + ip6 a = ip6_lit("2001:db8::1"); + ip6 expect = a; + + ck_assert_int_eq(atoip6("not-an-address", &a), -1); + /* A rejected parse must not have partially overwritten the caller's + * address - callers routinely parse into a live configuration slot. */ + ck_assert_int_eq(ip6_cmp(&a, &expect), 0); +} +END_TEST + +/* ========================================================================= + * Text rendering - RFC 5952 canonical form + * ========================================================================= */ + +START_TEST(test_ip6toa_compresses_longest_zero_run) +{ + /* Two runs of zeros: the longer one must be compressed. */ + ip6_check_text("1:0:0:2:0:0:0:3", "1:0:0:2::3"); + ip6_check_text("1:0:0:0:2:0:0:3", "1::2:0:0:3"); +} +END_TEST + +START_TEST(test_ip6toa_compresses_leftmost_run_on_tie) +{ + /* Equal length runs: RFC 5952 section 4.2.3 requires the first. */ + ip6_check_text("2001:db8:0:0:1:0:0:1", "2001:db8::1:0:0:1"); + ip6_check_text("1:0:0:2:0:0:3:4", "1::2:0:0:3:4"); +} +END_TEST + +START_TEST(test_ip6toa_does_not_compress_single_zero_group) +{ + /* RFC 5952 section 4.2.2: a lone zero group is written as "0". */ + ip6_check_text("1:0:2:3:4:5:6:7", "1:0:2:3:4:5:6:7"); + ip6_check_text("1:2:3:4:5:6:0:8", "1:2:3:4:5:6:0:8"); + ip6_check_text("0:1:2:3:4:5:6:7", "0:1:2:3:4:5:6:7"); +} +END_TEST + +START_TEST(test_ip6toa_run_at_start_and_end) +{ + ip6_check_text("0:0:0:0:0:0:0:1", "::1"); + ip6_check_text("1:0:0:0:0:0:0:0", "1::"); + ip6_check_text("0:1:0:0:0:0:0:0", "0:1::"); + ip6_check_text("0:0:0:0:0:0:1:0", "::1:0"); +} +END_TEST + +START_TEST(test_ip6toa_handles_null_arguments) +{ + char buf[WOLFIP_IP6_ADDRSTRLEN]; + + buf[0] = 'x'; + ip6toa(NULL, buf); + ck_assert_str_eq(buf, ""); + /* A NULL buffer must simply be ignored rather than dereferenced. */ + ip6toa(&(ip6)WOLFIP_IN6ADDR_LOOPBACK_INIT, NULL); +} +END_TEST + +START_TEST(test_ip6toa_never_exceeds_addrstrlen) +{ + /* The longest possible output is the fully expanded mapped form. */ + ip6 widest = ip6_lit("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ip6 mapped = ip6_lit("::ffff:255.255.255.255"); + char buf[WOLFIP_IP6_ADDRSTRLEN]; + size_t i; + size_t len = 0; + + ip6toa(&widest, buf); + while (buf[len] != '\0') + len++; + ck_assert_uint_lt(len, (size_t)WOLFIP_IP6_ADDRSTRLEN); + + ip6toa(&mapped, buf); + len = 0; + while (buf[len] != '\0') + len++; + ck_assert_uint_lt(len, (size_t)WOLFIP_IP6_ADDRSTRLEN); + + /* Every group at maximum width, to confirm the bound is not merely + * met by the compressed forms above. */ + for (i = 0; i < 16; i++) + widest.addr[i] = 0xFF; + ip6toa(&widest, buf); + len = 0; + while (buf[len] != '\0') + len++; + ck_assert_uint_lt(len, (size_t)WOLFIP_IP6_ADDRSTRLEN); +} +END_TEST + +START_TEST(test_ip6_text_roundtrip_is_stable) +{ + static const char *addrs[] = { + "::", "::1", "1::", "fe80::1", "2001:db8::1", + "ff02::1", "ff02::1:ff33:4455", "fd12:3456:789a:1::1", + "1:2:3:4:5:6:7:8", "::ffff:192.168.1.1", + "2001:db8:0:1:1:1:1:1", "1:0:0:2::3", + }; + size_t i; + + /* Rendering a parsed address and parsing it again must be a fixed + * point, for every canonical form. */ + for (i = 0; i < (sizeof(addrs) / sizeof(addrs[0])); i++) { + char first[WOLFIP_IP6_ADDRSTRLEN]; + char second[WOLFIP_IP6_ADDRSTRLEN]; + ip6 a; + ip6 b; + + ck_assert_int_eq(atoip6(addrs[i], &a), 0); + ip6toa(&a, first); + ck_assert_int_eq(atoip6(first, &b), 0); + ip6toa(&b, second); + ck_assert_int_eq(ip6_cmp(&a, &b), 0); + ck_assert_str_eq(first, second); + } +} +END_TEST + +START_TEST(test_ip6_text_roundtrip_exhaustive_single_bit) +{ + int bit; + + /* Every one of the 128 bits must survive a render/parse round trip. + * This is the cheapest way to catch a byte-order or shift mistake in + * either direction. */ + for (bit = 0; bit < 128; bit++) { + char buf[WOLFIP_IP6_ADDRSTRLEN]; + ip6 a; + ip6 b; + + ip6_set_unspecified(&a); + a.addr[bit / 8] = (uint8_t)(1u << (7 - (bit % 8))); + ip6toa(&a, buf); + ck_assert_int_eq(atoip6(buf, &b), 0); + ck_assert_int_eq(ip6_cmp(&a, &b), 0); + } +} +END_TEST diff --git a/src/test/unit/unit_tests_ipv6_hdr.c b/src/test/unit/unit_tests_ipv6_hdr.c new file mode 100644 index 00000000..3936f1f7 --- /dev/null +++ b/src/test/unit/unit_tests_ipv6_hdr.c @@ -0,0 +1,606 @@ +/* unit_tests_ipv6_hdr.c + * + * Copyright (C) 2026 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_IPV6 + +/* ========================================================================= + * Environment note + * ========================================================================= + * Covers the IPv6 header itself: the wire layout, the field accessors, the + * 40-byte pseudo-header checksum of RFC 8200 section 8.1, and the transmit + * side encapsulation in ip6_output_add_header(). + * + * Built only by `make unit-ipv6`, which passes -DWOLFIP_IPV6=1. The address + * layer that this builds on is tested unconditionally in + * unit_tests_ipv6_addr.c. + */ + +/* ========================================================================= + * Local helpers + * ========================================================================= */ + +#define IP6_TEST_FRAME_LEN(payload) \ + ((uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + (payload))) + +/* Lay down a well formed IPv6 header at the start of buf and return it. */ +static struct wolfIP_ip6_packet *ip6_mkhdr(uint8_t *buf, const char *src, + const char *dst, uint8_t next_hdr, + uint16_t payload_len) +{ + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + ip6 s6; + ip6 d6; + + memset(buf, 0, ETH_HEADER_LEN + IP6_HEADER_LEN); + ck_assert_int_eq(atoip6(src, &s6), 0); + ck_assert_int_eq(atoip6(dst, &d6), 0); + ip6_hdr_set_vtf(pkt, 0, 0); + pkt->payload_len = ee16(payload_len); + pkt->next_hdr = next_hdr; + pkt->hop_limit = 64; + ip6_hdr_set_src(pkt, &s6); + ip6_hdr_set_dst(pkt, &d6); + return pkt; +} + +/* ========================================================================= + * Wire layout + * ========================================================================= */ + +START_TEST(test_ip6_header_wire_layout) +{ + /* RFC 8200 section 3: the header is exactly 40 bytes and every field + * sits at a fixed offset. The Ethernet header is embedded by value, in + * the same way the IPv4 structures do it. */ + ck_assert_uint_eq(sizeof(struct wolfIP_ip6_wire), IP6_HEADER_LEN); + ck_assert_uint_eq(sizeof(struct wolfIP_ip6_packet), + (size_t)(ETH_HEADER_LEN + IP6_HEADER_LEN)); + + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, ver_tc_fl), 0); + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, payload_len), 4); + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, next_hdr), 6); + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, hop_limit), 7); + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, src), 8); + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, dst), 24); + ck_assert_uint_eq(offsetof(struct wolfIP_ip6_wire, data), 40); +} +END_TEST + +START_TEST(test_ip6_transport_wire_layout) +{ + /* The upper-layer headers sit immediately after the 40-byte IPv6 + * header, 20 bytes further along than their IPv4 counterparts. Getting + * this wrong is the single easiest way to corrupt every packet, so the + * offsets are pinned. */ + ck_assert_uint_eq(offsetof(struct wolfIP_tcp6_seg, src_port), + (size_t)(ETH_HEADER_LEN + IP6_HEADER_LEN)); + ck_assert_uint_eq(offsetof(struct wolfIP_udp6_datagram, src_port), + (size_t)(ETH_HEADER_LEN + IP6_HEADER_LEN)); + ck_assert_uint_eq(offsetof(struct wolfIP_icmp6_packet, type), + (size_t)(ETH_HEADER_LEN + IP6_HEADER_LEN)); + + /* And they are exactly 20 bytes further along than the IPv4 layout. */ + ck_assert_uint_eq(offsetof(struct wolfIP_tcp6_seg, src_port) - + offsetof(struct wolfIP_tcp_seg, src_port), 20); + ck_assert_uint_eq(offsetof(struct wolfIP_udp6_datagram, src_port) - + offsetof(struct wolfIP_udp_datagram, src_port), 20); +} +END_TEST + +START_TEST(test_ip6_pseudo_header_is_40_bytes) +{ + /* RFC 8200 section 8.1. The IPv4 pseudo-header is 12 bytes; conflating + * the two silently produces checksums that no peer will accept. */ + ck_assert_uint_eq(sizeof(union transport6_pseudo_header), 40); + ck_assert_uint_eq(offsetof(struct ph6, src), 0); + ck_assert_uint_eq(offsetof(struct ph6, dst), 16); + ck_assert_uint_eq(offsetof(struct ph6, len), 32); + ck_assert_uint_eq(offsetof(struct ph6, zero), 36); + ck_assert_uint_eq(offsetof(struct ph6, proto), 39); +} +END_TEST + +/* ========================================================================= + * Field accessors + * ========================================================================= */ + +START_TEST(test_ip6_hdr_version_traffic_class_flow_label) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + + memset(buf, 0, sizeof(buf)); + + ip6_hdr_set_vtf(pkt, 0, 0); + ck_assert_uint_eq(ip6_hdr_version(pkt), 6); + ck_assert_uint_eq(ip6_hdr_traffic_class(pkt), 0); + ck_assert_uint_eq(ip6_hdr_flow_label(pkt), 0); + + /* All three fields must be independently recoverable. */ + ip6_hdr_set_vtf(pkt, 0xB8, 0xFEDCB); + ck_assert_uint_eq(ip6_hdr_version(pkt), 6); + ck_assert_uint_eq(ip6_hdr_traffic_class(pkt), 0xB8); + ck_assert_uint_eq(ip6_hdr_flow_label(pkt), 0xFEDCB); + + /* Maximum values do not bleed into one another. */ + ip6_hdr_set_vtf(pkt, 0xFF, 0xFFFFF); + ck_assert_uint_eq(ip6_hdr_version(pkt), 6); + ck_assert_uint_eq(ip6_hdr_traffic_class(pkt), 0xFF); + ck_assert_uint_eq(ip6_hdr_flow_label(pkt), 0xFFFFF); + + /* A flow label wider than 20 bits is masked, never allowed to corrupt + * the traffic class or version above it. */ + ip6_hdr_set_vtf(pkt, 0, 0xFFFFFFFFu); + ck_assert_uint_eq(ip6_hdr_version(pkt), 6); + ck_assert_uint_eq(ip6_hdr_traffic_class(pkt), 0); + ck_assert_uint_eq(ip6_hdr_flow_label(pkt), 0xFFFFF); +} +END_TEST + +START_TEST(test_ip6_hdr_first_word_byte_order) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + const uint8_t *raw = buf + ETH_HEADER_LEN; + + memset(buf, 0, sizeof(buf)); + ip6_hdr_set_vtf(pkt, 0x00, 0); + /* Version 6 occupies the high nibble of the first octet on the wire, + * regardless of host endianness. */ + ck_assert_uint_eq(raw[0] & 0xF0u, 0x60); + + ip6_hdr_set_vtf(pkt, 0xFF, 0); + /* Traffic class straddles the first two octets: low nibble of byte 0, + * high nibble of byte 1. */ + ck_assert_uint_eq(raw[0], 0x6F); + ck_assert_uint_eq(raw[1] & 0xF0u, 0xF0); + + ip6_hdr_set_vtf(pkt, 0, 0x12345); + ck_assert_uint_eq(raw[1] & 0x0Fu, 0x01); + ck_assert_uint_eq(raw[2], 0x23); + ck_assert_uint_eq(raw[3], 0x45); +} +END_TEST + +START_TEST(test_ip6_hdr_address_accessors_roundtrip) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + ip6 src; + ip6 dst; + ip6 got; + + memset(buf, 0, sizeof(buf)); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("fe80::2", &dst), 0); + + ip6_hdr_set_src(pkt, &src); + ip6_hdr_set_dst(pkt, &dst); + + ip6_hdr_get_src(pkt, &got); + ck_assert_int_eq(ip6_cmp(&got, &src), 0); + ip6_hdr_get_dst(pkt, &got); + ck_assert_int_eq(ip6_cmp(&got, &dst), 0); + + /* Source and destination must not overlap: writing one may not disturb + * the other. */ + ck_assert_mem_eq(buf + ETH_HEADER_LEN + 8, src.addr, 16); + ck_assert_mem_eq(buf + ETH_HEADER_LEN + 24, dst.addr, 16); +} +END_TEST + +/* ========================================================================= + * Pseudo-header checksum + * ========================================================================= */ + +START_TEST(test_transport6_checksum_self_verifies) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_udp6_datagram *udp; + union transport6_pseudo_header ph; + ip6 src; + ip6 dst; + const uint16_t payload = 8 + 4; /* UDP header + 4 bytes of data */ + + ip6_mkhdr(buf, "2001:db8::1", "2001:db8::2", IP6_NEXTHDR_UDP, payload); + udp = (struct wolfIP_udp6_datagram *)buf; + udp->src_port = ee16(1234); + udp->dst_port = ee16(5678); + udp->len = ee16(payload); + udp->csum = 0; + memcpy(udp->data, "abcd", 4); + + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + transport6_pseudo_header_init(&ph, &src, &dst, payload, IP6_NEXTHDR_UDP); + udp->csum = ee16(transport6_checksum(&ph, &udp->src_port)); + + /* A non-trivial packet must not produce a zero checksum by accident, + * and re-summing including the checksum field must give zero. */ + ck_assert_uint_ne(udp->csum, 0); + ck_assert_int_eq(transport6_verify_checksum(&ph, &udp->src_port), 0); + ck_assert_int_eq(ip6_verify_transport_checksum( + (struct wolfIP_ip6_packet *)buf), 0); +} +END_TEST + +START_TEST(test_transport6_checksum_detects_every_single_bit_flip) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_udp6_datagram *udp; + union transport6_pseudo_header ph; + ip6 src; + ip6 dst; + const uint16_t payload = 8 + 6; + unsigned int byte; + + ip6_mkhdr(buf, "2001:db8::1", "2001:db8::2", IP6_NEXTHDR_UDP, payload); + udp = (struct wolfIP_udp6_datagram *)buf; + udp->src_port = ee16(1234); + udp->dst_port = ee16(5678); + udp->len = ee16(payload); + udp->csum = 0; + memcpy(udp->data, "abcdef", 6); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + transport6_pseudo_header_init(&ph, &src, &dst, payload, IP6_NEXTHDR_UDP); + udp->csum = ee16(transport6_checksum(&ph, &udp->src_port)); + ck_assert_int_eq(transport6_verify_checksum(&ph, &udp->src_port), 0); + + /* Flip one bit at a time across the whole upper-layer region. A one's + * complement sum catches every single-bit error, so each flip must be + * detected and the packet must verify again once restored. */ + for (byte = 0; byte < payload; byte++) { + uint8_t *p = ((uint8_t *)&udp->src_port) + byte; + unsigned int bit; + + for (bit = 0; bit < 8; bit++) { + *p = (uint8_t)(*p ^ (1u << bit)); + ck_assert_int_ne(transport6_verify_checksum(&ph, &udp->src_port), 0); + *p = (uint8_t)(*p ^ (1u << bit)); + } + } + ck_assert_int_eq(transport6_verify_checksum(&ph, &udp->src_port), 0); +} +END_TEST + +START_TEST(test_transport6_checksum_covers_the_pseudo_header) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_udp6_datagram *udp; + union transport6_pseudo_header ph; + ip6 src; + ip6 dst; + ip6 other; + const uint16_t payload = 8; + uint16_t csum_a; + uint16_t csum_b; + + ip6_mkhdr(buf, "2001:db8::1", "2001:db8::2", IP6_NEXTHDR_UDP, payload); + udp = (struct wolfIP_udp6_datagram *)buf; + udp->src_port = ee16(1234); + udp->dst_port = ee16(5678); + udp->len = ee16(payload); + udp->csum = 0; + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + ck_assert_int_eq(atoip6("2001:db8::3", &other), 0); + + transport6_pseudo_header_init(&ph, &src, &dst, payload, IP6_NEXTHDR_UDP); + csum_a = transport6_checksum(&ph, &udp->src_port); + + /* Changing only the destination address must change the checksum: that + * is the whole point of the pseudo-header. */ + transport6_pseudo_header_init(&ph, &src, &other, payload, IP6_NEXTHDR_UDP); + csum_b = transport6_checksum(&ph, &udp->src_port); + ck_assert_uint_ne(csum_a, csum_b); + + /* So must changing the next header, which is why TCP and UDP payloads + * that are otherwise identical checksum differently. */ + transport6_pseudo_header_init(&ph, &src, &dst, payload, IP6_NEXTHDR_TCP); + ck_assert_uint_ne(transport6_checksum(&ph, &udp->src_port), csum_a); +} +END_TEST + +START_TEST(test_transport6_checksum_handles_odd_length_payload) +{ + uint8_t buf[LINK_MTU]; + struct wolfIP_udp6_datagram *udp; + union transport6_pseudo_header ph; + ip6 src; + ip6 dst; + const uint16_t payload = 8 + 5; /* odd data length */ + + ip6_mkhdr(buf, "2001:db8::1", "2001:db8::2", IP6_NEXTHDR_UDP, payload); + udp = (struct wolfIP_udp6_datagram *)buf; + udp->src_port = ee16(1); + udp->dst_port = ee16(2); + udp->len = ee16(payload); + udp->csum = 0; + memcpy(udp->data, "hello", 5); + /* Poison the byte just past the payload: the odd-length tail handling + * must not read it. */ + udp->data[5] = 0xFF; + + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + transport6_pseudo_header_init(&ph, &src, &dst, payload, IP6_NEXTHDR_UDP); + udp->csum = ee16(transport6_checksum(&ph, &udp->src_port)); + ck_assert_int_eq(transport6_verify_checksum(&ph, &udp->src_port), 0); + + /* Changing the poisoned byte beyond the payload must not invalidate it. */ + udp->data[5] = 0x00; + ck_assert_int_eq(transport6_verify_checksum(&ph, &udp->src_port), 0); +} +END_TEST + +/* ========================================================================= + * Transmit encapsulation + * ========================================================================= */ + +START_TEST(test_ip6_output_add_header_fills_the_header) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_udp6_datagram *udp = (struct wolfIP_udp6_datagram *)buf; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + const uint8_t mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; + ip6 src; + ip6 dst; + ip6 got; + const uint16_t payload = 8 + 4; + + wolfIP_init(&s); + mock_link_init(&s); + memset(buf, 0, sizeof(buf)); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + + udp->src_port = ee16(1234); + udp->dst_port = ee16(5678); + udp->len = ee16(payload); + memcpy(udp->data, "wxyz", 4); + + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &src, &dst, + IP6_NEXTHDR_UDP, payload, 64, mac), + 0); + + ck_assert_uint_eq(ip6_hdr_version(pkt), 6); + /* RFC 8200 section 6: a source that does not use flow labelling sets + * the field to zero. */ + ck_assert_uint_eq(ip6_hdr_flow_label(pkt), 0); + ck_assert_uint_eq(ip6_hdr_traffic_class(pkt), 0); + ck_assert_uint_eq(ee16(pkt->payload_len), payload); + ck_assert_uint_eq(pkt->next_hdr, IP6_NEXTHDR_UDP); + ck_assert_uint_eq(pkt->hop_limit, 64); + ip6_hdr_get_src(pkt, &got); + ck_assert_int_eq(ip6_cmp(&got, &src), 0); + ip6_hdr_get_dst(pkt, &got); + ck_assert_int_eq(ip6_cmp(&got, &dst), 0); + + /* The Ethernet header must carry the IPv6 ethertype and the supplied + * next-hop MAC. */ + ck_assert_uint_eq(ee16(pkt->eth.type), ETH_TYPE_IPV6); + ck_assert_mem_eq(pkt->eth.dst, mac, 6); + + /* And the checksum must be valid over the resulting header. */ + ck_assert_int_eq(ip6_verify_transport_checksum(pkt), 0); +} +END_TEST + +START_TEST(test_ip6_output_add_header_defaults_hop_limit) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + const uint8_t mac[6] = {0x02, 0, 0, 0, 0, 1}; + ip6 src; + ip6 dst; + + wolfIP_init(&s); + mock_link_init(&s); + memset(buf, 0, sizeof(buf)); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + + /* A hop limit of zero means "caller did not care", not "expire this + * packet immediately". */ + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &src, &dst, + IP6_NEXTHDR_UDP, 8, 0, mac), 0); + ck_assert_uint_eq(pkt->hop_limit, IP6_HOP_LIMIT_DEFAULT); + + /* An explicit 255, as Neighbor Discovery requires, is preserved. */ + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &src, &dst, + IP6_NEXTHDR_ICMPV6, 8, 255, mac), 0); + ck_assert_uint_eq(pkt->hop_limit, 255); +} +END_TEST + +START_TEST(test_ip6_output_add_header_udp_zero_checksum_becomes_ffff) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_udp6_datagram *udp = (struct wolfIP_udp6_datagram *)buf; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + const uint8_t mac[6] = {0x02, 0, 0, 0, 0, 1}; + ip6 src; + ip6 dst; + unsigned int attempt; + int saw_ffff = 0; + + wolfIP_init(&s); + mock_link_init(&s); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + + /* RFC 8200 section 8.1: a zero UDP checksum is forbidden over IPv6 + * (unlike IPv4, where it means "not computed"), so a computed zero must + * be sent as 0xFFFF. Search a small space of payloads for one that sums + * to zero, then assert the substitution happened. */ + for (attempt = 0; attempt < 0x10000u; attempt++) { + const uint16_t payload = 8 + 2; + + memset(buf, 0, sizeof(buf)); + udp->src_port = ee16(1); + udp->dst_port = ee16(2); + udp->len = ee16(payload); + udp->data[0] = (uint8_t)(attempt >> 8); + udp->data[1] = (uint8_t)(attempt & 0xFFu); + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &src, + &dst, IP6_NEXTHDR_UDP, payload, + 64, mac), 0); + /* Whatever the payload, the checksum field is never left at zero. */ + ck_assert_uint_ne(udp->csum, 0); + if (udp->csum == 0xFFFFu) { + saw_ffff = 1; + break; + } + } + ck_assert_int_eq(saw_ffff, 1); +} +END_TEST + +START_TEST(test_ip6_output_add_header_icmp6_checksum_uses_pseudo_header) +{ + struct wolfIP s; + uint8_t buf_a[LINK_MTU]; + uint8_t buf_b[LINK_MTU]; + struct wolfIP_icmp6_packet *a = (struct wolfIP_icmp6_packet *)buf_a; + struct wolfIP_icmp6_packet *b = (struct wolfIP_icmp6_packet *)buf_b; + const uint8_t mac[6] = {0x02, 0, 0, 0, 0, 1}; + ip6 src; + ip6 dst1; + ip6 dst2; + + wolfIP_init(&s); + mock_link_init(&s); + ck_assert_int_eq(atoip6("fe80::1", &src), 0); + ck_assert_int_eq(atoip6("ff02::1", &dst1), 0); + ck_assert_int_eq(atoip6("ff02::2", &dst2), 0); + + memset(buf_a, 0, sizeof(buf_a)); + memset(buf_b, 0, sizeof(buf_b)); + a->type = 128; /* Echo Request */ + a->code = 0; + b->type = 128; + b->code = 0; + + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)a, + &src, &dst1, IP6_NEXTHDR_ICMPV6, + 4, 255, mac), 0); + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)b, + &src, &dst2, IP6_NEXTHDR_ICMPV6, + 4, 255, mac), 0); + + /* Identical ICMPv6 bodies to different destinations must checksum + * differently. ICMPv4 has no pseudo-header and would produce the same + * value for both; this is the check that catches that mistake being + * carried over. */ + ck_assert_uint_ne(a->csum, b->csum); + ck_assert_int_eq(ip6_verify_transport_checksum( + (struct wolfIP_ip6_packet *)a), 0); + ck_assert_int_eq(ip6_verify_transport_checksum( + (struct wolfIP_ip6_packet *)b), 0); +} +END_TEST + +START_TEST(test_ip6_output_add_header_tcp_checksum) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_tcp6_seg *tcp = (struct wolfIP_tcp6_seg *)buf; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + const uint8_t mac[6] = {0x02, 0, 0, 0, 0, 1}; + ip6 src; + ip6 dst; + + wolfIP_init(&s); + mock_link_init(&s); + memset(buf, 0, sizeof(buf)); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + + tcp->src_port = ee16(443); + tcp->dst_port = ee16(51000); + tcp->seq = ee32(0x11223344); + tcp->hlen = 0x50; + tcp->flags = 0x02; /* SYN */ + tcp->win = ee16(64240); + + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &src, &dst, + IP6_NEXTHDR_TCP, 20, 64, mac), 0); + ck_assert_uint_ne(tcp->csum, 0); + ck_assert_int_eq(ip6_verify_transport_checksum(pkt), 0); + ck_assert_uint_eq(ee16(pkt->eth.type), ETH_TYPE_IPV6); +} +END_TEST + +START_TEST(test_ip6_output_add_header_rejects_null_arguments) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + ip6 a; + + wolfIP_init(&s); + mock_link_init(&s); + ip6_set_loopback(&a); + + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, NULL, &a, &a, + IP6_NEXTHDR_UDP, 8, 64, NULL), + -WOLFIP_EINVAL); + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, NULL, &a, + IP6_NEXTHDR_UDP, 8, 64, NULL), + -WOLFIP_EINVAL); + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &a, NULL, + IP6_NEXTHDR_UDP, 8, 64, NULL), + -WOLFIP_EINVAL); +} +END_TEST + +START_TEST(test_ip6_output_add_header_without_mac_leaves_ethernet_alone) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + ip6 src; + ip6 dst; + + wolfIP_init(&s); + mock_link_init(&s); + memset(buf, 0, sizeof(buf)); + ck_assert_int_eq(atoip6("2001:db8::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8::2", &dst), 0); + + /* A NULL next-hop MAC means the caller handles the link layer, as the + * raw-IP ports do. The Ethernet header must be left untouched. */ + ck_assert_int_eq(ip6_output_add_header(&s, TEST_PRIMARY_IF, pkt, &src, &dst, + IP6_NEXTHDR_UDP, 8, 64, NULL), 0); + ck_assert_uint_eq(pkt->eth.type, 0); + /* The IPv6 header itself is still fully populated. */ + ck_assert_uint_eq(ip6_hdr_version(pkt), 6); + ck_assert_uint_eq(ee16(pkt->payload_len), 8); +} +END_TEST + +#endif /* WOLFIP_IPV6 */ diff --git a/src/test/unit/unit_tests_ipv6_icmp.c b/src/test/unit/unit_tests_ipv6_icmp.c new file mode 100644 index 00000000..369e8033 --- /dev/null +++ b/src/test/unit/unit_tests_ipv6_icmp.c @@ -0,0 +1,424 @@ +/* unit_tests_ipv6_icmp.c + * + * Copyright (C) 2026 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_IPV6 + +/* ========================================================================= + * Environment note + * ========================================================================= + * ICMPv6 Echo (RFC 4443 section 4). + * + * Answering an Echo Request needs neither sockets nor Neighbor Discovery: + * the reply goes back to the source MAC of the request, exactly as the + * IPv4 path does. That makes it the first piece of IPv6 that is useful on + * its own, and it gives ip6_output_add_header() its first production + * caller. + * + * Sending Echo Requests is deliberately not here. That needs the peer's + * link-layer address, which means Neighbor Discovery. + * + * Every frame is delivered through the real ingress path with + * wolfIP_recv_ex(), and every assertion is made against the frame the + * stack actually transmitted (last_frame_sent). + */ + +/* ICMP6_ECHO_REQUEST / ICMP6_ECHO_REPLY come from src/wolfip6.c. */ + +#define ICMP6_OUR_ADDR "2001:db8::1" +#define ICMP6_OUR_LL "fe80::1" +#define ICMP6_PEER_ADDR "2001:db8::2" + +static const uint8_t icmp6_peer_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}; + +/* Bring up a stack with one global and one link-local IPv6 address. */ +static void icmp6_setup(struct wolfIP *s) +{ + ip6 a; + + wolfIP_init(s); + mock_link_init(s); + ck_assert_int_eq(atoip6(ICMP6_OUR_ADDR, &a), 0); + ck_assert_int_eq(wolfIP_ifaddr_add6(s, TEST_PRIMARY_IF, &a, 64), 0); + ck_assert_int_eq(atoip6(ICMP6_OUR_LL, &a), 0); + ck_assert_int_eq(wolfIP_ifaddr_add6(s, TEST_PRIMARY_IF, &a, 64), 0); + last_frame_sent_size = 0; +} + +/* Build an ICMPv6 message of `type` with a valid checksum and return the + * frame length. `body_len` counts the bytes after the 4-byte ICMPv6 header + * (identifier, sequence and payload for Echo). */ +static uint32_t icmp6_build(uint8_t *frame, struct wolfIP *s, + const char *src, const char *dst, uint8_t type, + uint16_t id, uint16_t seq, + const uint8_t *payload, uint16_t payload_len, + int corrupt_csum) +{ + struct wolfIP_icmp6_packet *icmp = (struct wolfIP_icmp6_packet *)frame; + struct wolfIP_ll_dev *ll = wolfIP_getdev_ex(s, TEST_PRIMARY_IF); + union transport6_pseudo_header ph; + uint16_t body_len = (uint16_t)(4u + payload_len); /* id+seq+payload */ + uint16_t upper_len = (uint16_t)(4u + body_len); /* + ICMPv6 header */ + ip6 s6; + ip6 d6; + + ck_assert_ptr_nonnull(ll); + memset(frame, 0, LINK_MTU); + ck_assert_int_eq(atoip6(src, &s6), 0); + ck_assert_int_eq(atoip6(dst, &d6), 0); + + /* Destination MAC: our unicast address, or the 33:33 mapping when the + * request is addressed to a multicast group. */ + if (ip6_is_multicast(&d6)) + ip6_mcast_to_eth(&d6, icmp->ip6.eth.dst); + else + memcpy(icmp->ip6.eth.dst, ll->mac, 6); + memcpy(icmp->ip6.eth.src, icmp6_peer_mac, 6); + icmp->ip6.eth.type = ee16(ETH_TYPE_IPV6); + + ip6_hdr_set_vtf(&icmp->ip6, 0, 0); + icmp->ip6.payload_len = ee16(upper_len); + icmp->ip6.next_hdr = IP6_NEXTHDR_ICMPV6; + icmp->ip6.hop_limit = 64; + ip6_hdr_set_src(&icmp->ip6, &s6); + ip6_hdr_set_dst(&icmp->ip6, &d6); + + icmp->type = type; + icmp->code = 0; + icmp->csum = 0; + icmp->data[0] = (uint8_t)(id >> 8); + icmp->data[1] = (uint8_t)(id & 0xFFu); + icmp->data[2] = (uint8_t)(seq >> 8); + icmp->data[3] = (uint8_t)(seq & 0xFFu); + if (payload_len && payload) + memcpy(&icmp->data[4], payload, payload_len); + + transport6_pseudo_header_init(&ph, &s6, &d6, upper_len, + IP6_NEXTHDR_ICMPV6); + icmp->csum = ee16(transport6_checksum(&ph, &icmp->type)); + if (corrupt_csum) + icmp->csum = (uint16_t)(icmp->csum ^ ee16(0x0001)); + + return (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + upper_len); +} + +/* The reply the stack transmitted, or NULL if it sent nothing. */ +static struct wolfIP_icmp6_packet *icmp6_reply(void) +{ + if (last_frame_sent_size == 0) + return NULL; + return (struct wolfIP_icmp6_packet *)last_frame_sent; +} + +/* ========================================================================= + * Echo Request is answered + * ========================================================================= */ + +START_TEST(test_icmp6_echo_request_is_answered) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + static const uint8_t payload[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + struct wolfIP_icmp6_packet *rep; + uint32_t len; + ip6 ours; + ip6 peer; + ip6 got; + + icmp6_setup(&s); + ck_assert_int_eq(atoip6(ICMP6_OUR_ADDR, &ours), 0); + ck_assert_int_eq(atoip6(ICMP6_PEER_ADDR, &peer), 0); + + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 0xABCD, 7, payload, sizeof(payload), + 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + + rep = icmp6_reply(); + ck_assert_ptr_nonnull(rep); + /* RFC 4443 section 4.2 */ + ck_assert_uint_eq(rep->type, ICMP6_ECHO_REPLY); + ck_assert_uint_eq(rep->code, 0); + /* Identifier, sequence number and payload are echoed verbatim. */ + ck_assert_uint_eq(rep->data[0], 0xAB); + ck_assert_uint_eq(rep->data[1], 0xCD); + ck_assert_uint_eq(rep->data[2], 0); + ck_assert_uint_eq(rep->data[3], 7); + ck_assert_mem_eq(&rep->data[4], payload, sizeof(payload)); + + /* Source and destination are swapped. */ + ip6_hdr_get_src(&rep->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &ours), 0); + ip6_hdr_get_dst(&rep->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &peer), 0); + + /* It goes back to the requester's MAC, which is why no Neighbor + * Discovery is needed to answer a ping. */ + ck_assert_mem_eq(rep->ip6.eth.dst, icmp6_peer_mac, 6); + ck_assert_uint_eq(ee16(rep->ip6.eth.type), ETH_TYPE_IPV6); + + /* And the whole thing is a valid IPv6 packet. */ + ck_assert_uint_eq(ip6_hdr_version(&rep->ip6), 6); + ck_assert_uint_eq(ee16(rep->ip6.payload_len), 4u + 4u + sizeof(payload)); + ck_assert_int_eq(ip6_verify_transport_checksum(&rep->ip6), 0); +} +END_TEST + +START_TEST(test_icmp6_echo_request_with_no_payload_is_answered) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + + icmp6_setup(&s); + /* A bare Echo with identifier and sequence but no data is legal. */ + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + + ck_assert_ptr_nonnull(icmp6_reply()); + ck_assert_uint_eq(icmp6_reply()->type, ICMP6_ECHO_REPLY); + ck_assert_int_eq(ip6_verify_transport_checksum(&icmp6_reply()->ip6), 0); +} +END_TEST + +START_TEST(test_icmp6_echo_to_link_local_is_answered_from_it) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + ip6 ll6; + ip6 got; + + icmp6_setup(&s); + ck_assert_int_eq(atoip6(ICMP6_OUR_LL, &ll6), 0); + + len = icmp6_build(frame, &s, "fe80::2", ICMP6_OUR_LL, + ICMP6_ECHO_REQUEST, 5, 5, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + + ck_assert_ptr_nonnull(icmp6_reply()); + /* The reply must come from the address that was pinged, not from some + * other address of ours - a link-local request answered from a global + * address would be dropped by the peer. */ + ip6_hdr_get_src(&icmp6_reply()->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &ll6), 0); +} +END_TEST + +/* ========================================================================= + * Requests that must NOT be answered + * ========================================================================= */ + +START_TEST(test_icmp6_echo_with_bad_checksum_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + + icmp6_setup(&s); + /* RFC 4443 section 2.3: a message with a bad checksum is silently + * discarded. */ + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 1 /* corrupt */); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_echo_with_nonzero_code_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + struct wolfIP_icmp6_packet *icmp = (struct wolfIP_icmp6_packet *)frame; + union transport6_pseudo_header ph; + uint32_t len; + uint16_t upper_len; + ip6 src; + ip6 dst; + + icmp6_setup(&s); + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + icmp->code = 7; + icmp->csum = 0; + upper_len = ee16(icmp->ip6.payload_len); + ip6_hdr_get_src(&icmp->ip6, &src); + ip6_hdr_get_dst(&icmp->ip6, &dst); + transport6_pseudo_header_init(&ph, &src, &dst, upper_len, + IP6_NEXTHDR_ICMPV6); + icmp->csum = ee16(transport6_checksum(&ph, &icmp->type)); + + /* RFC 4443 section 4.1 defines Code as zero. A valid checksum must not + * make an otherwise malformed Echo Request actionable. */ + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_echo_to_an_address_that_is_not_ours_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + + icmp6_setup(&s); + /* Addressed to a global address we do not hold. Frames like this reach + * the stack whenever the link is shared, and answering one would both + * leak our existence and answer for somebody else. */ + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, "2001:db8::99", + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_echo_from_unspecified_source_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + + icmp6_setup(&s); + /* There is nowhere to send the reply, and :: as a source is reserved + * for duplicate address detection. */ + len = icmp6_build(frame, &s, "::", ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_echo_to_tentative_address_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + ip6 tentative; + + wolfIP_init(&s); + mock_link_init(&s); + ck_assert_int_eq(atoip6(ICMP6_OUR_ADDR, &tentative), 0); + ck_assert_int_eq(wolfIP_ipv6_addr_add(&s, TEST_PRIMARY_IF, &tentative, + 64), 0); + + /* RFC 4862 section 5.4.5: a tentative address is not assigned to the + * interface yet and may only be used by Duplicate Address Detection. */ + last_frame_sent_size = 0; + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_link_local_address_is_not_local_on_another_interface) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + ip6 ll6; + + wolfIP_init(&s); + mock_link_init(&s); + mock_link_init_idx(&s, TEST_SECOND_IF, NULL); + ck_assert_int_eq(atoip6(ICMP6_OUR_LL, &ll6), 0); + ck_assert_int_eq(wolfIP_ifaddr_add6(&s, TEST_SECOND_IF, &ll6, 64), 0); + + /* The numeric address belongs to the second link's zone, not the ingress + * link. Answering here would violate RFC 4007 scoping. */ + last_frame_sent_size = 0; + len = icmp6_build(frame, &s, "fe80::2", ICMP6_OUR_LL, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_echo_reply_does_not_generate_another_reply) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + + icmp6_setup(&s); + /* Answering a reply with a reply is an infinite loop between two + * hosts. */ + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REPLY, 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); +} +END_TEST + +START_TEST(test_icmp6_unhandled_types_are_ignored_without_replying) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + uint32_t len; + unsigned int type; + static const uint8_t types[] = { + 1, /* Destination Unreachable */ + 2, /* Packet Too Big */ + 3, /* Time Exceeded */ + 4, /* Parameter Problem */ + 133, /* Router Solicitation */ + 134, /* Router Advertisement */ + 135, /* Neighbor Solicitation */ + 136, /* Neighbor Advertisement */ + 137, /* Redirect */ + 200 /* unassigned */ + }; + + icmp6_setup(&s); + /* None of these are implemented yet. They must be dropped quietly, and + * in particular must never draw an Echo Reply. */ + for (type = 0; type < (sizeof(types) / sizeof(types[0])); type++) { + last_frame_sent_size = 0; + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + types[type], 1, 1, NULL, 0, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, len); + ck_assert_ptr_null(icmp6_reply()); + } +} +END_TEST + +START_TEST(test_icmp6_truncated_echo_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + struct wolfIP_icmp6_packet *icmp = (struct wolfIP_icmp6_packet *)frame; + uint32_t len; + + icmp6_setup(&s); + len = icmp6_build(frame, &s, ICMP6_PEER_ADDR, ICMP6_OUR_ADDR, + ICMP6_ECHO_REQUEST, 1, 1, NULL, 0, 0); + + /* An Echo needs 8 bytes: type, code, checksum, identifier, sequence. + * Claim fewer and the identifier and sequence are not there to copy. */ + icmp->ip6.payload_len = ee16(4); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + 4)); + ck_assert_ptr_null(icmp6_reply()); + (void)len; +} +END_TEST + +#endif /* WOLFIP_IPV6 */ diff --git a/src/test/unit/unit_tests_ipv6_nd.c b/src/test/unit/unit_tests_ipv6_nd.c new file mode 100644 index 00000000..98ccd048 --- /dev/null +++ b/src/test/unit/unit_tests_ipv6_nd.c @@ -0,0 +1,1436 @@ +/* unit_tests_ipv6_nd.c + * + * Copyright (C) 2026 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_IPV6 + +/* ========================================================================= + * Environment note + * ========================================================================= + * Neighbor Discovery (RFC 4861) and duplicate address detection + * (RFC 4862 section 5.4). + * + * Five scenarios, in the order they matter operationally: + * 1. joining a network from cold + * 2. neighbour advertisements, the ones to accept and the ones to refuse + * 3. duplicate address detection, both outcomes + * 4. router advertisements, enough for an ordinary site network + * 5. a statically assigned ULA alongside all of it + * + * Everything is driven through the real ingress path with wolfIP_recv_ex() + * and observed through the frames the stack actually transmits, plus the + * public address list. Time advances by explicit wolfIP_poll() calls, so + * the periodic ND tick is deterministic. + */ + +#define ND_TICK_STEP_MS 50u + +static const uint8_t nd_peer_mac[6] = {0x02, 0xAA, 0x00, 0x00, 0x00, 0x01}; +static const uint8_t nd_router_mac[6] = {0x02, 0xBB, 0x00, 0x00, 0x00, 0x01}; +static const uint8_t nd_other_mac[6] = {0x02, 0xCC, 0x00, 0x00, 0x00, 0x01}; + +/* Advance the clock, polling often enough that the 100ms ND tick fires. */ +static void nd_advance(struct wolfIP *s, uint64_t *now, uint64_t ms) +{ + uint64_t target = *now + ms; + + while (*now < target) { + *now += ND_TICK_STEP_MS; + wolfIP_poll(s, *now); + } +} + +/* The stack's own link-local address, as wolfIP_ipv6_start() forms it. */ +static void nd_our_link_local(struct wolfIP *s, ip6 *out) +{ + struct wolfIP_ll_dev *ll = wolfIP_getdev_ex(s, TEST_PRIMARY_IF); + ip6 prefix; + ip6 iid; + + ck_assert_ptr_nonnull(ll); + ck_assert_int_eq(atoip6("fe80::", &prefix), 0); + ip6_iid_from_mac(&iid, ll->mac); + ip6_make_addr(out, &prefix, 64, &iid); +} + +/* State of an address in the interface list, or -1 if it is not there. */ +static int nd_addr_state(struct wolfIP *s, const ip6 *addr) +{ + struct wolfIP_ifaddr_info info; + unsigned int n = wolfIP_ifaddr_count(s, TEST_PRIMARY_IF, AF_INET6); + unsigned int i; + + for (i = 0; i < n; i++) { + if (wolfIP_ifaddr_get(s, TEST_PRIMARY_IF, AF_INET6, i, &info) != 0) + continue; + if (ip6_cmp(&info.v6, addr) == 0) + return (int)info.state; + } + return -1; +} + +static struct wolfIP_icmp6_packet *nd_sent(void) +{ + if (last_frame_sent_size == 0) + return NULL; + return (struct wolfIP_icmp6_packet *)last_frame_sent; +} + +/* Finish an ICMPv6 frame: checksum it and hand it to the stack. */ +static void nd_deliver(struct wolfIP *s, uint8_t *frame, const ip6 *src, + const ip6 *dst, uint16_t payload_len, + uint8_t hop_limit, const uint8_t *src_mac) +{ + struct wolfIP_icmp6_packet *icmp = (struct wolfIP_icmp6_packet *)frame; + struct wolfIP_ll_dev *ll = wolfIP_getdev_ex(s, TEST_PRIMARY_IF); + union transport6_pseudo_header ph; + + ck_assert_ptr_nonnull(ll); + if (ip6_is_multicast(dst)) + ip6_mcast_to_eth(dst, icmp->ip6.eth.dst); + else + memcpy(icmp->ip6.eth.dst, ll->mac, 6); + memcpy(icmp->ip6.eth.src, src_mac, 6); + icmp->ip6.eth.type = ee16(ETH_TYPE_IPV6); + ip6_hdr_set_vtf(&icmp->ip6, 0, 0); + icmp->ip6.payload_len = ee16(payload_len); + icmp->ip6.next_hdr = IP6_NEXTHDR_ICMPV6; + icmp->ip6.hop_limit = hop_limit; + ip6_hdr_set_src(&icmp->ip6, src); + ip6_hdr_set_dst(&icmp->ip6, dst); + icmp->csum = 0; + transport6_pseudo_header_init(&ph, src, dst, payload_len, + IP6_NEXTHDR_ICMPV6); + icmp->csum = ee16(transport6_checksum(&ph, &icmp->type)); + wolfIP_recv_ex(s, TEST_PRIMARY_IF, frame, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + payload_len); +} + +/* Neighbor Solicitation. `lla` may be NULL to omit the option, which is what + * a duplicate-address-detection probe looks like. */ +static void nd_send_ns(struct wolfIP *s, const ip6 *src, const ip6 *dst, + const ip6 *target, const uint8_t *lla, + uint8_t hop_limit, const uint8_t *src_mac) +{ + uint8_t frame[LINK_MTU]; + struct nd6_msg *ns = (struct nd6_msg *)frame; + uint16_t payload_len = 24; + + memset(frame, 0, sizeof(frame)); + ns->type = ICMP6_NEIGHBOR_SOLICIT; + ns->code = 0; + memcpy(ns->target, target->addr, 16); + if (lla != NULL) { + struct nd6_opt_lla *opt = (struct nd6_opt_lla *)ns->options; + + opt->type = ND6_OPT_SLLA; + opt->len = 1; + memcpy(opt->mac, lla, 6); + payload_len = (uint16_t)(payload_len + 8u); + } + nd_deliver(s, frame, src, dst, payload_len, hop_limit, src_mac); +} + +/* Neighbor Advertisement. */ +static void nd_send_na(struct wolfIP *s, const ip6 *src, const ip6 *dst, + const ip6 *target, uint8_t flags, const uint8_t *tlla, + uint8_t hop_limit, const uint8_t *src_mac) +{ + uint8_t frame[LINK_MTU]; + struct nd6_msg *na = (struct nd6_msg *)frame; + uint16_t payload_len = 24; + + memset(frame, 0, sizeof(frame)); + na->type = ICMP6_NEIGHBOR_ADVERT; + na->code = 0; + na->flags = flags; + memcpy(na->target, target->addr, 16); + if (tlla != NULL) { + struct nd6_opt_lla *opt = (struct nd6_opt_lla *)na->options; + + opt->type = ND6_OPT_TLLA; + opt->len = 1; + memcpy(opt->mac, tlla, 6); + payload_len = (uint16_t)(payload_len + 8u); + } + nd_deliver(s, frame, src, dst, payload_len, hop_limit, src_mac); +} + +/* Router Advertisement with at most one Prefix Information option. Pass + * prefix_str NULL for an advertisement carrying no prefix. */ +static void nd_send_ra(struct wolfIP *s, const char *src_str, + uint16_t router_lifetime, const char *prefix_str, + uint8_t prefix_len, uint8_t pio_flags, + uint32_t valid, uint8_t hop_limit) +{ + uint8_t frame[LINK_MTU]; + struct nd6_ra_msg *ra = (struct nd6_ra_msg *)frame; + uint16_t payload_len = 16; + ip6 src; + ip6 dst; + + memset(frame, 0, sizeof(frame)); + ck_assert_int_eq(atoip6(src_str, &src), 0); + ip6_set_all_nodes(&dst); + ra->type = ICMP6_ROUTER_ADVERT; + ra->code = 0; + ra->cur_hop_limit = 64; + ra->flags = 0; + ra->router_lifetime = ee16(router_lifetime); + if (prefix_str != NULL) { + struct nd6_opt_prefix *po = (struct nd6_opt_prefix *)ra->options; + ip6 prefix; + + ck_assert_int_eq(atoip6(prefix_str, &prefix), 0); + po->type = ND6_OPT_PREFIX; + po->len = 4; /* 32 octets */ + po->prefix_len = prefix_len; + po->flags = pio_flags; + po->valid_lifetime = ee32(valid); + po->preferred_lifetime = ee32(valid); + memcpy(po->prefix, prefix.addr, 16); + payload_len = (uint16_t)(payload_len + 32u); + } + nd_deliver(s, frame, &src, &dst, payload_len, hop_limit, nd_router_mac); +} + +/* Deliberately flexible RA builder for malformed/adversarial PIO cases. */ +static void nd_send_ra_pio_raw(struct wolfIP *s, const char *dst_str, + const char *prefix_str, uint8_t prefix_len, + uint8_t pio_flags, uint32_t valid, + uint32_t preferred, uint8_t option_units) +{ + uint8_t frame[LINK_MTU]; + struct nd6_ra_msg *ra = (struct nd6_ra_msg *)frame; + struct nd6_opt_prefix *po = (struct nd6_opt_prefix *)ra->options; + uint16_t payload_len = (uint16_t)(16u + ((uint16_t)option_units * 8u)); + ip6 src; + ip6 dst; + ip6 prefix; + + memset(frame, 0, sizeof(frame)); + ck_assert_int_eq(atoip6("fe80::1", &src), 0); + if (dst_str != NULL) + ck_assert_int_eq(atoip6(dst_str, &dst), 0); + else + ip6_set_all_nodes(&dst); + ck_assert_int_eq(atoip6(prefix_str, &prefix), 0); + ra->type = ICMP6_ROUTER_ADVERT; + ra->code = 0; + ra->router_lifetime = 0; + po->type = ND6_OPT_PREFIX; + po->len = option_units; + po->prefix_len = prefix_len; + po->flags = pio_flags; + po->valid_lifetime = ee32(valid); + po->preferred_lifetime = ee32(preferred); + memcpy(po->prefix, prefix.addr, 16); + nd_deliver(s, frame, &src, &dst, payload_len, 255, nd_router_mac); +} + +static void nd_setup(struct wolfIP *s) +{ + wolfIP_init(s); + mock_link_init(s); + last_frame_sent_size = 0; +} + +/* ========================================================================= + * 1. Joining a network + * ========================================================================= */ + +START_TEST(test_nd_join_forms_link_local_and_probes_it) +{ + struct wolfIP s; + struct nd6_msg *ns; + uint64_t now = 1000; + ip6 ll6; + ip6 solicited; + ip6 got; + ip6 target; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + + /* RFC 4862 section 5.3: the link-local address exists immediately but is + * tentative, so nothing may use it yet. */ + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); + + /* The first tick sends the duplicate address detection probe. */ + last_frame_sent_size = 0; + nd_advance(&s, &now, 200); + ck_assert_ptr_nonnull(nd_sent()); + ns = (struct nd6_msg *)last_frame_sent; + ck_assert_uint_eq(ns->type, ICMP6_NEIGHBOR_SOLICIT); + ck_assert_uint_eq(ns->code, 0); + + /* RFC 4861 section 7.1.1: hop limit 255, which is what keeps Neighbor + * Discovery on the local link. */ + ck_assert_uint_eq(ns->ip6.hop_limit, 255); + /* RFC 4862 section 5.4.2: source is the unspecified address... */ + ip6_hdr_get_src(&ns->ip6, &got); + ck_assert_int_eq(ip6_is_unspecified(&got), 1); + /* ...and the destination is the target's solicited-node group. */ + ip6_set_solicited_node(&solicited, &ll6); + ip6_hdr_get_dst(&ns->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &solicited), 0); + memcpy(target.addr, ns->target, 16); + ck_assert_int_eq(ip6_cmp(&target, &ll6), 0); + /* 33:33:ff:xx:xx:xx, the multicast mapping of that group. */ + ck_assert_uint_eq(ns->ip6.eth.dst[0], 0x33); + ck_assert_uint_eq(ns->ip6.eth.dst[1], 0x33); + ck_assert_uint_eq(ns->ip6.eth.dst[2], 0xFF); + + /* RFC 4861 section 4.3: no Source Link-Layer Address option is allowed + * when the source is unspecified - there is no address to advertise. */ + ck_assert_uint_eq(ee16(ns->ip6.payload_len), 24); +} +END_TEST + +START_TEST(test_nd_join_completes_and_solicits_routers) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + ip6 all_routers; + ip6 got; + int saw_rs = 0; + int i; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + + /* Nobody objects, so after the retransmit interval the address is ours + * (RFC 4862 section 5.4.4). */ + nd_advance(&s, &now, 1500); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); + + /* With a usable source address the stack solicits routers. */ + ip6_set_all_routers(&all_routers); + for (i = 0; i < 40; i++) { + last_frame_sent_size = 0; + nd_advance(&s, &now, 100); + if ((last_frame_sent_size > 0) && + (((struct nd6_msg *)last_frame_sent)->type == + ICMP6_ROUTER_SOLICIT)) { + struct nd6_rs_msg *rs = (struct nd6_rs_msg *)last_frame_sent; + + ip6_hdr_get_dst(&rs->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &all_routers), 0); + ck_assert_uint_eq(rs->ip6.hop_limit, 255); + /* Sourced from the link-local address, so it carries a Source + * Link-Layer Address option. */ + ip6_hdr_get_src(&rs->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &ll6), 0); + ck_assert_uint_eq(ee16(rs->ip6.payload_len), 16); + saw_rs = 1; + break; + } + } + ck_assert_int_eq(saw_rs, 1); +} +END_TEST + +/* ========================================================================= + * 2. Neighbor advertisements, good and bad + * ========================================================================= */ + +START_TEST(test_nd_na_resolves_an_incomplete_entry) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + + /* Unresolved to begin with. */ + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + (void)nd6_store_neighbor(&s, TEST_PRIMARY_IF, &peer, NULL, + ND6_INCOMPLETE, 0); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + + /* A solicited advertisement with a target link-layer address answers it. */ + nd_send_na(&s, &peer, &ll6, &peer, + ND6_NA_SOLICITED | ND6_NA_OVERRIDE, nd_peer_mac, 255, + nd_peer_mac); + ck_assert_int_eq(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + ck_assert_mem_eq(mac, nd_peer_mac, 6); + ck_assert_int_eq(s.nd6.neighbors[nd6_neighbor_index(&s, TEST_PRIMARY_IF, + &peer)].state, + ND6_REACHABLE); +} +END_TEST + +START_TEST(test_nd_na_without_target_lla_resolves_nothing) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + (void)nd6_store_neighbor(&s, TEST_PRIMARY_IF, &peer, NULL, + ND6_INCOMPLETE, 0); + + /* Nothing has been learned, so the entry must stay unresolved rather + * than become reachable with a garbage address. */ + nd_send_na(&s, &peer, &ll6, &peer, ND6_NA_SOLICITED, NULL, 255, + nd_peer_mac); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); +} +END_TEST + +START_TEST(test_nd_na_without_override_may_not_replace_a_known_mac) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + ck_assert_int_eq(wolfIP_nd6_neighbor_add(&s, TEST_PRIMARY_IF, &peer, + nd_peer_mac), 0); + + /* RFC 4861 section 7.2.5: without the Override flag, a different + * link-layer address must not replace the one already held. This is + * what stops an advertisement hijacking an established neighbour. */ + nd_send_na(&s, &peer, &ll6, &peer, ND6_NA_SOLICITED, nd_other_mac, 255, + nd_other_mac); + ck_assert_int_eq(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + ck_assert_mem_eq(mac, nd_peer_mac, 6); + /* The entry does drop to STALE, so its reachability is re-verified + * rather than being confirmed by an advertisement we refused to act + * on. Asserting the state, not just the address, is what makes this + * test able to tell the two behaviours apart. */ + ck_assert_int_eq(s.nd6.neighbors[nd6_neighbor_index(&s, TEST_PRIMARY_IF, + &peer)].state, + ND6_STALE); + + /* With the Override flag it is accepted. */ + nd_send_na(&s, &peer, &ll6, &peer, ND6_NA_SOLICITED | ND6_NA_OVERRIDE, + nd_other_mac, 255, nd_other_mac); + ck_assert_int_eq(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + ck_assert_mem_eq(mac, nd_other_mac, 6); +} +END_TEST + +START_TEST(test_nd_messages_with_wrong_hop_limit_are_refused) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + (void)nd6_store_neighbor(&s, TEST_PRIMARY_IF, &peer, NULL, + ND6_INCOMPLETE, 0); + + /* RFC 4861 sections 6.1 and 7.1: anything other than 255 means the + * message crossed a router. A hop limit of 254 is the off-link attacker + * case, and 64 is what a careless implementation would send. */ + nd_send_na(&s, &peer, &ll6, &peer, ND6_NA_SOLICITED | ND6_NA_OVERRIDE, + nd_peer_mac, 254, nd_peer_mac); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + nd_send_na(&s, &peer, &ll6, &peer, ND6_NA_SOLICITED | ND6_NA_OVERRIDE, + nd_peer_mac, 64, nd_peer_mac); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + + /* 255 is accepted, so the refusals above are about the hop limit and + * nothing else. */ + nd_send_na(&s, &peer, &ll6, &peer, ND6_NA_SOLICITED | ND6_NA_OVERRIDE, + nd_peer_mac, 255, nd_peer_mac); + ck_assert_int_eq(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); +} +END_TEST + +START_TEST(test_nd_na_from_unspecified_source_is_refused) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 unspec; + ip6 ll6; + ip6 peer; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ip6_set_unspecified(&unspec); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + (void)nd6_store_neighbor(&s, TEST_PRIMARY_IF, &peer, NULL, + ND6_INCOMPLETE, 0); + + /* RFC 4861 section 7.1.2 requires the source of an NA to be unicast. */ + nd_send_na(&s, &unspec, &ll6, &peer, + ND6_NA_SOLICITED | ND6_NA_OVERRIDE, nd_peer_mac, 255, + nd_peer_mac); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); +} +END_TEST + +START_TEST(test_nd_na_for_a_foreign_destination_is_refused) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 peer; + ip6 foreign; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + ck_assert_int_eq(atoip6("2001:db8:dead::1", &foreign), 0); + (void)nd6_store_neighbor(&s, TEST_PRIMARY_IF, &peer, NULL, + ND6_INCOMPLETE, 0); + + /* A frame forced to our Ethernet MAC is not ours when its IPv6 + * destination belongs to another node. It must not poison our cache. */ + nd_send_na(&s, &peer, &foreign, &peer, + ND6_NA_SOLICITED | ND6_NA_OVERRIDE, nd_peer_mac, 255, + nd_peer_mac); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); +} +END_TEST + +START_TEST(test_nd_solicitation_for_our_address_is_answered) +{ + struct wolfIP s; + struct nd6_msg *na; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + ip6 got; + ip6 target; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + + last_frame_sent_size = 0; + nd_send_ns(&s, &peer, &ll6, &ll6, nd_peer_mac, 255, nd_peer_mac); + + ck_assert_ptr_nonnull(nd_sent()); + na = (struct nd6_msg *)last_frame_sent; + ck_assert_uint_eq(na->type, ICMP6_NEIGHBOR_ADVERT); + ck_assert_uint_eq(na->ip6.hop_limit, 255); + /* Solicited and Override, addressed back to the asker. */ + ck_assert_uint_eq(na->flags & ND6_NA_SOLICITED, ND6_NA_SOLICITED); + memcpy(target.addr, na->target, 16); + ck_assert_int_eq(ip6_cmp(&target, &ll6), 0); + ip6_hdr_get_dst(&na->ip6, &got); + ck_assert_int_eq(ip6_cmp(&got, &peer), 0); + /* The Source Link-Layer Address option in the solicitation is recorded, + * so the reverse direction is resolved without another exchange. */ + ck_assert_int_eq(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); + ck_assert_mem_eq(mac, nd_peer_mac, 6); +} +END_TEST + +START_TEST(test_nd_solicitation_for_a_foreign_address_is_ignored) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 peer; + ip6 foreign; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + ck_assert_int_eq(atoip6("fe80::dead", &foreign), 0); + + /* Answering for an address we do not hold would be answering on behalf + * of another node. */ + last_frame_sent_size = 0; + nd_send_ns(&s, &peer, &foreign, &foreign, nd_peer_mac, 255, nd_peer_mac); + ck_assert_ptr_null(nd_sent()); +} +END_TEST + +START_TEST(test_nd_solicitation_to_a_foreign_destination_is_ignored) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + ip6 foreign; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + ck_assert_int_eq(atoip6("2001:db8:dead::1", &foreign), 0); + + last_frame_sent_size = 0; + nd_send_ns(&s, &peer, &foreign, &ll6, nd_peer_mac, 255, nd_peer_mac); + ck_assert_ptr_null(nd_sent()); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); +} +END_TEST + +START_TEST(test_nd_solicitation_with_spoofed_slla_is_ignored) +{ + struct wolfIP s; + uint64_t now = 1000; + uint8_t mac[6]; + ip6 ll6; + ip6 peer; + ip6 solicited; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + ip6_set_solicited_node(&solicited, &ll6); + + /* On Ethernet, the SLLA is the sender's address and must agree with the + * frame source. Otherwise an attacker can redirect our reply and cache + * entry to an uninvolved victim. */ + last_frame_sent_size = 0; + nd_send_ns(&s, &peer, &solicited, &ll6, nd_other_mac, 255, nd_peer_mac); + ck_assert_ptr_null(nd_sent()); + ck_assert_int_lt(wolfIP_nd6_lookup(&s, TEST_PRIMARY_IF, &peer, mac), 0); +} +END_TEST + +/* ========================================================================= + * 3. Duplicate address detection + * ========================================================================= */ + +START_TEST(test_dad_succeeds_when_nobody_answers) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); +} +END_TEST + +START_TEST(test_dad_fails_when_a_neighbour_advertises_the_address) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + ip6 all_nodes; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + nd_advance(&s, &now, 200); /* probe is out */ + + /* Somebody already owns it and says so. RFC 4862 section 5.4.4. */ + ip6_set_all_nodes(&all_nodes); + nd_send_na(&s, &ll6, &all_nodes, &ll6, ND6_NA_OVERRIDE, nd_peer_mac, 255, + nd_peer_mac); + + /* RFC 4862 section 5.4.5: the address must not be assigned. */ + ck_assert_int_eq(nd_addr_state(&s, &ll6), -1); + nd_advance(&s, &now, 2000); + ck_assert_int_eq(nd_addr_state(&s, &ll6), -1); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 0); +} +END_TEST + +START_TEST(test_dad_fails_on_a_simultaneous_probe_from_another_node) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + ip6 unspec; + ip6 solicited; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + nd_advance(&s, &now, 200); + + /* RFC 4862 section 5.4.3: a solicitation for our tentative address from + * the unspecified source means another node is probing the same address + * at the same time. Neither may use it. */ + ip6_set_unspecified(&unspec); + ip6_set_solicited_node(&solicited, &ll6); + last_frame_sent_size = 0; + nd_send_ns(&s, &unspec, &solicited, &ll6, NULL, 255, nd_peer_mac); + + ck_assert_int_eq(nd_addr_state(&s, &ll6), -1); + /* And it must not have been defended: a tentative address is not ours. */ + ck_assert_ptr_null(nd_sent()); +} +END_TEST + +START_TEST(test_dad_ignores_malformed_neighbor_solicitations) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + ip6 unspec; + ip6 solicited; + ip6 all_nodes; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + ip6_set_unspecified(&unspec); + ip6_set_solicited_node(&solicited, &ll6); + ip6_set_all_nodes(&all_nodes); + + /* RFC 4861 section 7.1.1: an NS from :: is a DAD probe only when sent to + * the target's solicited-node address and without an SLLA option. */ + nd_send_ns(&s, &unspec, &all_nodes, &ll6, NULL, 255, nd_other_mac); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); + nd_send_ns(&s, &unspec, &solicited, &ll6, nd_other_mac, 255, + nd_other_mac); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); +} +END_TEST + +START_TEST(test_dad_tentative_address_is_not_defended) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + ip6 peer; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + nd_advance(&s, &now, 200); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + + /* An ordinary solicitation from a real node, while we are still + * probing. Replying would claim an address we have not yet verified. */ + last_frame_sent_size = 0; + nd_send_ns(&s, &peer, &ll6, &ll6, nd_peer_mac, 255, nd_peer_mac); + ck_assert_ptr_null(nd_sent()); +} +END_TEST + +/* ========================================================================= + * 4. Router advertisements + * ========================================================================= */ + +START_TEST(test_ra_assigns_a_global_address_and_a_default_router) +{ + struct wolfIP s; + struct wolfIP_ll_dev *ll; + uint64_t now = 1000; + ip6 prefix; + ip6 iid; + ip6 expected; + ip6 router; + ip6 nexthop; + ip6 offlink; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + /* The ordinary site case: one router, one /64, autonomous and on-link. */ + nd_send_ra(&s, "fe80::1", 1800, "2001:db8:1:2::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 255); + + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + ck_assert_int_eq(atoip6("2001:db8:1:2::", &prefix), 0); + ip6_iid_from_mac(&iid, ll->mac); + ip6_make_addr(&expected, &prefix, 64, &iid); + + /* RFC 4862 section 5.5.3: prefix plus interface identifier, and it too + * has to pass duplicate address detection before use. */ + ck_assert_int_eq(nd_addr_state(&s, &expected), WOLFIP_IFADDR_TENTATIVE); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(nd_addr_state(&s, &expected), WOLFIP_IFADDR_PREFERRED); + /* The link-local address is still there alongside it. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 2); + + /* The advertising router became the default route, and an off-link + * destination now resolves to it. */ + ck_assert_int_eq(atoip6("fe80::1", &router), 0); + ck_assert_int_eq(atoip6("2001:db8:99::1", &offlink), 0); + ck_assert_int_eq(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); + ck_assert_int_eq(ip6_cmp(&nexthop, &router), 0); + + /* A destination inside the advertised prefix is on-link, so the next hop + * is the destination itself. */ + { + ip6 onlink; + + ck_assert_int_eq(atoip6("2001:db8:1:2::99", &onlink), 0); + ck_assert_int_eq(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &onlink, + &nexthop), 0); + ck_assert_int_eq(ip6_cmp(&nexthop, &onlink), 0); + } +} +END_TEST + +START_TEST(test_ra_from_a_non_link_local_source_is_ignored) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 offlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + /* RFC 4861 section 6.1.2. Accepting a global source would let anything + * off the link install a default route and a prefix. */ + nd_send_ra(&s, "2001:db8::ffff", 1800, "2001:db8:1:2::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 255); + + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_int_eq(atoip6("2001:db8:99::1", &offlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_with_wrong_hop_limit_is_ignored) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 offlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + nd_send_ra(&s, "fe80::1", 1800, "2001:db8:1:2::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 64); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_int_eq(atoip6("2001:db8:99::1", &offlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_ignores_a_link_local_prefix_option) +{ + struct wolfIP s; + uint64_t now = 1000; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + /* RFC 4862 section 5.5.3 (a): an advertised link-local prefix is + * silently ignored, so a hostile advertisement cannot redefine + * fe80::/10 or make us form a second link-local address. */ + nd_send_ra(&s, "fe80::1", 1800, "fe80::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 255); + nd_advance(&s, &now, 1500); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); +} +END_TEST + +START_TEST(test_ra_prefix_that_is_not_64_bits_forms_no_address) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 onlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + /* RFC 4862 section 5.5.3 (d): the prefix length plus the interface + * identifier length must be 128. A /48 leaves no room, so no address is + * formed - but the prefix is still on-link. */ + nd_send_ra(&s, "fe80::1", 1800, "2001:db8:1::", 48, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 255); + nd_advance(&s, &now, 1500); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + + ck_assert_int_eq(atoip6("2001:db8:1::5", &onlink), 0); + ck_assert_int_eq(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &onlink, + &nexthop), 0); + ck_assert_int_eq(ip6_cmp(&nexthop, &onlink), 0); +} +END_TEST + +START_TEST(test_ra_with_zero_router_lifetime_is_not_a_default_route) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 offlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + /* RFC 4861 section 6.3.4: lifetime zero means the sender is not a + * default router, though its prefixes still apply. */ + nd_send_ra(&s, "fe80::1", 0, "2001:db8:1:2::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 255); + ck_assert_int_eq(atoip6("2001:db8:99::1", &offlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); + + /* A later advertisement with a real lifetime installs the route, and + * one with lifetime zero withdraws it again. */ + nd_send_ra(&s, "fe80::1", 1800, NULL, 0, 0, 0, 255); + ck_assert_int_eq(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); + nd_send_ra(&s, "fe80::1", 0, NULL, 0, 0, 0, 255); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_with_a_zero_length_option_terminates) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + struct nd6_ra_msg *ra = (struct nd6_ra_msg *)frame; + uint64_t now = 1000; + ip6 src; + ip6 dst; + ip6 offlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + + /* RFC 4861 section 4.6: option lengths are in units of 8 octets and zero + * is invalid. A parser that does not reject it makes no forward progress + * and loops forever on a frame an attacker controls. If this test hangs, + * that is the bug. */ + memset(frame, 0, sizeof(frame)); + ck_assert_int_eq(atoip6("fe80::1", &src), 0); + ip6_set_all_nodes(&dst); + ra->type = ICMP6_ROUTER_ADVERT; + ra->code = 0; + ra->router_lifetime = ee16(1800); + ra->options[0] = ND6_OPT_PREFIX; + ra->options[1] = 0; /* invalid */ + nd_deliver(&s, frame, &src, &dst, (uint16_t)(16 + 8), 255, nd_router_mac); + + /* The whole advertisement is invalid, so none of its state may be + * installed before the malformed option is discovered. */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_int_eq(atoip6("2001:db8:99::1", &offlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_for_a_foreign_destination_is_ignored) +{ + struct wolfIP s; + uint8_t frame[LINK_MTU]; + struct nd6_ra_msg *ra = (struct nd6_ra_msg *)frame; + uint64_t now = 1000; + ip6 src; + ip6 foreign; + ip6 offlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + memset(frame, 0, sizeof(frame)); + ck_assert_int_eq(atoip6("fe80::1", &src), 0); + ck_assert_int_eq(atoip6("2001:db8:dead::1", &foreign), 0); + ra->type = ICMP6_ROUTER_ADVERT; + ra->router_lifetime = ee16(1800); + nd_deliver(&s, frame, &src, &foreign, 16, 255, nd_router_mac); + + ck_assert_int_eq(atoip6("2001:db8:99::1", &offlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &offlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_ignores_oversized_prefix_information_option) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 onlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_send_ra_pio_raw(&s, NULL, "2001:db8:44::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 7200, 5); + + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_int_eq(atoip6("2001:db8:44::99", &onlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &onlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_ignores_prefix_with_preferred_lifetime_above_valid) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 onlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_send_ra_pio_raw(&s, NULL, "2001:db8:45::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 100, 101, 4); + + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); + ck_assert_int_eq(atoip6("2001:db8:45::99", &onlink), 0); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &onlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_ra_zero_valid_lifetime_withdraws_prefix_immediately) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 onlink; + ip6 nexthop; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_send_ra(&s, "fe80::1", 0, "2001:db8:46::", 64, + ND6_PREFIX_ONLINK, 7200, 255); + ck_assert_int_eq(atoip6("2001:db8:46::99", &onlink), 0); + ck_assert_int_eq(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &onlink, + &nexthop), 0); + + nd_send_ra(&s, "fe80::1", 0, "2001:db8:46::", 64, + ND6_PREFIX_ONLINK, 0, 255); + ck_assert_int_lt(wolfIP_ipv6_nexthop(&s, TEST_PRIMARY_IF, &onlink, + &nexthop), 0); +} +END_TEST + +START_TEST(test_nd_static_neighbor_rejects_non_wire_addresses) +{ + struct wolfIP s; + ip6 addr; + + nd_setup(&s); + ck_assert_int_eq(atoip6("::1", &addr), 0); + ck_assert_int_lt(wolfIP_nd6_neighbor_add(&s, TEST_PRIMARY_IF, &addr, + nd_peer_mac), 0); + ck_assert_int_eq(atoip6("::ffff:192.0.2.1", &addr), 0); + ck_assert_int_lt(wolfIP_nd6_neighbor_add(&s, TEST_PRIMARY_IF, &addr, + nd_peer_mac), 0); + ck_assert_int_eq(atoip6("::192.0.2.1", &addr), 0); + ck_assert_int_lt(wolfIP_nd6_neighbor_add(&s, TEST_PRIMARY_IF, &addr, + nd_peer_mac), 0); +} +END_TEST + +/* ========================================================================= + * 5. A statically assigned ULA + * ========================================================================= */ + +START_TEST(test_ula_is_verified_by_dad_and_then_usable) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ula; + ip6 ll6; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(atoip6("fd00:db8:1::1", &ula), 0); + + /* A pre-assigned unique local address, configured before the interface + * has any other IPv6 configuration. RFC 4862 section 5.4 wants + * duplicate address detection on it too, so it starts tentative. */ + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_int_eq(wolfIP_ipv6_addr_add(&s, TEST_PRIMARY_IF, &ula, 64), 0); + ck_assert_int_eq(nd_addr_state(&s, &ula), WOLFIP_IFADDR_TENTATIVE); + ck_assert_int_eq(ip6_is_ula(&ula), 1); + + nd_advance(&s, &now, 1500); + ck_assert_int_eq(nd_addr_state(&s, &ula), WOLFIP_IFADDR_PREFERRED); + + /* It coexists with the link-local address. */ + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 2); +} +END_TEST + +START_TEST(test_ula_is_defended_and_survives_a_router_advertisement) +{ + struct wolfIP s; + struct nd6_msg *na; + uint64_t now = 1000; + ip6 ula; + ip6 peer; + ip6 target; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(atoip6("fd00:db8:1::1", &ula), 0); + ck_assert_int_eq(atoip6("fe80::2", &peer), 0); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_int_eq(wolfIP_ipv6_addr_add(&s, TEST_PRIMARY_IF, &ula, 64), 0); + nd_advance(&s, &now, 1500); + + /* Once it is ours, solicitations for it are answered. */ + last_frame_sent_size = 0; + nd_send_ns(&s, &peer, &ula, &ula, nd_peer_mac, 255, nd_peer_mac); + ck_assert_ptr_nonnull(nd_sent()); + na = (struct nd6_msg *)last_frame_sent; + ck_assert_uint_eq(na->type, ICMP6_NEIGHBOR_ADVERT); + memcpy(target.addr, na->target, 16); + ck_assert_int_eq(ip6_cmp(&target, &ula), 0); + + /* A router advertising a global prefix adds an address; it must not + * disturb the statically configured one. */ + nd_send_ra(&s, "fe80::1", 1800, "2001:db8:1:2::", 64, + ND6_PREFIX_ONLINK | ND6_PREFIX_AUTO, 7200, 255); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(nd_addr_state(&s, &ula), WOLFIP_IFADDR_PREFERRED); + /* link-local + ULA + SLAAC global */ + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 3); +} +END_TEST + +START_TEST(test_ula_duplicate_is_rejected) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ula; + ip6 all_nodes; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(atoip6("fd00:db8:1::1", &ula), 0); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_int_eq(wolfIP_ipv6_addr_add(&s, TEST_PRIMARY_IF, &ula, 64), 0); + nd_advance(&s, &now, 200); + + /* Two nodes configured with the same ULA by mistake is exactly what + * duplicate address detection is for. */ + ip6_set_all_nodes(&all_nodes); + nd_send_na(&s, &ula, &all_nodes, &ula, ND6_NA_OVERRIDE, nd_peer_mac, 255, + nd_peer_mac); + ck_assert_int_eq(nd_addr_state(&s, &ula), -1); + + /* The link-local address is unaffected: only the duplicate is dropped. */ + nd_advance(&s, &now, 1500); + ck_assert_uint_eq(wolfIP_ifaddr_count(&s, TEST_PRIMARY_IF, AF_INET6), 1); +} +END_TEST + +/* ========================================================================= + * 6. Timer lifecycle + * ========================================================================= + * Neighbor Discovery adds exactly one entry to the shared timer heap for + * the whole stack, not one per interface or per address. Everything else - + * duplicate address detection, router solicitation retries, cache ageing, + * prefix and router lifetimes - is a deadline field that this one tick + * polls. + */ + +/* Fill the timer heap with entries that never fire. */ +static void nd_fill_timer_heap(struct wolfIP *s) +{ + while (s->timers.size < MAX_TIMERS) { + struct wolfIP_timer tmr; + + memset(&tmr, 0, sizeof(tmr)); + tmr.expires = s->last_tick + 1000000u; + tmr.arg = s; + tmr.cb = nd6_tick_cb; + if (timers_binheap_insert(&s->timers, tmr) == NO_TIMER) + break; + } +} + +START_TEST(test_nd_uses_one_timer_slot_for_the_whole_stack) +{ + struct wolfIP s; + uint64_t now = 1000; + uint32_t before; + + nd_setup(&s); + wolfIP_poll(&s, now); + before = s.timers.size; + + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_uint_eq(s.timers.size, before + 1u); + + /* A second interface reuses the same tick rather than arming another. */ + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_SECOND_IF), 0); + ck_assert_uint_eq(s.timers.size, before + 1u); + + /* And it stays at one across many firings. */ + nd_advance(&s, &now, 2000); + ck_assert_uint_eq(s.timers.size, before + 1u); + + /* Arming is idempotent: nd6_arm_tick() cancels any live timer before + * inserting, so a repeated start cannot leave a stray entry running + * alongside the new one. Counted as live entries rather than heap size, + * because cancellation only tombstones with expires = 0 and the slot is + * reclaimed later. */ + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + { + unsigned int i; + int live = 0; + + for (i = 0; i < s.timers.size; i++) { + if ((s.timers.timers[i].expires != 0) && + (s.timers.timers[i].cb == nd6_tick_cb)) + live++; + } + ck_assert_int_eq(live, 1); + } +} +END_TEST + +START_TEST(test_nd_timer_quiesces_when_periodic_work_is_finished) +{ + struct wolfIP s; + uint64_t now = 1000; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + + /* DAD and all three Router Solicitations have finite schedules. Once + * those finish, merely having IPv6 enabled is not periodic work. */ + nd_advance(&s, &now, 15000); + ck_assert_uint_eq(s.nd6.rs_left[TEST_PRIMARY_IF], 0); + ck_assert_uint_eq(s.nd6.tick_timer, NO_TIMER); +} +END_TEST + +START_TEST(test_nd_recovers_when_the_timer_heap_is_full) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + + nd_setup(&s); + wolfIP_poll(&s, now); + + /* No slot available when IPv6 starts. Duplicate address detection would + * otherwise never run, and nothing would notice: timers_binheap_insert() + * returns 0 on a full heap and NO_TIMER is 0, so the failure is + * indistinguishable from "not armed". */ + nd_fill_timer_heap(&s); + ck_assert_uint_eq(s.timers.size, MAX_TIMERS); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_uint_eq(s.nd6.tick_timer, NO_TIMER); + + nd_our_link_local(&s, &ll6); + nd_advance(&s, &now, 2000); + /* Still stuck, because there is genuinely nowhere to put the timer. */ + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); + + /* Free a slot. wolfIP_poll() must notice and arm the tick; without that + * recovery the stack would never do Neighbor Discovery again. */ + timers_binheap_pop(&s.timers); + nd_advance(&s, &now, 2000); + ck_assert_uint_ne(s.nd6.tick_timer, NO_TIMER); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); +} +END_TEST + +START_TEST(test_nd_stop_releases_the_timer_slot) +{ + struct wolfIP s; + uint64_t now = 1000; + uint32_t before; + ip6 ll6; + + nd_setup(&s); + wolfIP_poll(&s, now); + before = s.timers.size; + + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); + ck_assert_uint_ne(s.nd6.tick_timer, NO_TIMER); + + /* Nothing else is running, so stopping must give the slot back rather + * than leave the tick scanning five tables every 100ms forever. + * + * timer_binheap_cancel() is lazy: it tombstones the entry with + * expires = 0 and the slot is reclaimed by the drain loop at the top of + * timers_binheap_insert(). So heap->size does not drop straight away, + * and asserting on it here would be asserting the wrong contract. What + * matters is that the tick is disarmed, does not fire again, and does + * not leak the slot. */ + ck_assert_int_eq(wolfIP_ipv6_stop(&s, TEST_PRIMARY_IF), 0); + ck_assert_uint_eq(s.nd6.tick_timer, NO_TIMER); + + /* Tombstoned, so it can never fire. */ + { + unsigned int i; + int live = 0; + + for (i = 0; i < s.timers.size; i++) { + if (s.timers.timers[i].expires != 0) + live++; + } + ck_assert_int_eq(live, 0); + } + + /* An address that had already passed detection is kept, and is still + * answered for. */ + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); + nd_advance(&s, &now, 500); + ck_assert_uint_eq(s.nd6.tick_timer, NO_TIMER); + + /* The slot is reclaimed rather than leaked: starting again reuses it + * instead of growing the heap. */ + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_uint_eq(s.timers.size, before + 1u); +} +END_TEST + +START_TEST(test_nd_stop_drops_a_tentative_address) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_our_link_local(&s, &ll6); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_TENTATIVE); + + /* Detection never completed, so the address was never ours. Leaving it + * behind would strand it tentative for good. */ + ck_assert_int_eq(wolfIP_ipv6_stop(&s, TEST_PRIMARY_IF), 0); + ck_assert_int_eq(nd_addr_state(&s, &ll6), -1); +} +END_TEST + +START_TEST(test_nd_restarts_after_being_stopped) +{ + struct wolfIP s; + uint64_t now = 1000; + ip6 ll6; + + nd_setup(&s); + wolfIP_poll(&s, now); + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(wolfIP_ipv6_stop(&s, TEST_PRIMARY_IF), 0); + ck_assert_uint_eq(s.nd6.tick_timer, NO_TIMER); + + /* Starting again re-arms and duplicate address detection runs afresh. */ + ck_assert_int_eq(wolfIP_ipv6_start(&s, TEST_PRIMARY_IF), 0); + ck_assert_uint_ne(s.nd6.tick_timer, NO_TIMER); + nd_our_link_local(&s, &ll6); + nd_advance(&s, &now, 1500); + ck_assert_int_eq(nd_addr_state(&s, &ll6), WOLFIP_IFADDR_PREFERRED); +} +END_TEST + +START_TEST(test_nd_stop_rejects_invalid_arguments) +{ + struct wolfIP s; + + nd_setup(&s); + ck_assert_int_lt(wolfIP_ipv6_stop(NULL, TEST_PRIMARY_IF), 0); + ck_assert_int_lt(wolfIP_ipv6_stop(&s, 99), 0); + /* Stopping an interface that was never started is not an error. */ + ck_assert_int_eq(wolfIP_ipv6_stop(&s, TEST_PRIMARY_IF), 0); +} +END_TEST + +#endif /* WOLFIP_IPV6 */ diff --git a/src/test/unit/unit_tests_ipv6_pending.c b/src/test/unit/unit_tests_ipv6_pending.c new file mode 100644 index 00000000..527cf123 --- /dev/null +++ b/src/test/unit/unit_tests_ipv6_pending.c @@ -0,0 +1,402 @@ +/* unit_tests_ipv6_pending.c + * + * Copyright (C) 2026 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 + */ + +/* ========================================================================= + * Requirement-derived tests for IPv6 behaviour that is not implemented yet + * ========================================================================= + * + * Each group is guarded by a named WOLFIP_IPV6_HAVE_* macro, all of which + * default to 0 in config.h. The phase that implements a feature flips its + * macro to 1 in the same commit, which switches these tests on. They are + * named macros rather than "#if 0" so that the amount of work still + * outstanding is greppable - `make unit-ipv6-pending-count` reports it. + * + * These tests are the specification. They were written from the low-level + * requirements before the code exists, so they also fix the API shape: the + * function names and signatures used below are the contract the + * implementation is expected to meet. Changing a name here is a deliberate + * interface decision, not a test fixup. + * + * Test names are descriptive and carry no requirement identifiers: the + * requirement documents are internal, and the mapping from requirement to + * test is maintained off-tree, keyed by test function name. Keep the names + * stable. + */ + +#if WOLFIP_IPV6 + +/* ========================================================================= + * ICMPv6 - RFC 4443 + * ========================================================================= */ +#if WOLFIP_IPV6_HAVE_ICMP6 + +/* Echo Request and Reply, and the receive checksum, are implemented and + * have real tests in unit_tests_ipv6_icmp.c. What remains here is the + * error-message half of RFC 4443. */ + + + + +START_TEST(test_icmp6_error_is_not_sent_in_response_to_an_error) +{ + /* RFC 4443 s2.4 (e.3): an ICMPv6 error message must never be generated + * in response to another ICMPv6 error message. Without this rule two + * hosts can sustain an error loop. */ + ck_abort_msg("pending: ICMPv6 error suppression"); +} +END_TEST + +START_TEST(test_icmp6_error_is_not_sent_for_multicast_destinations) +{ + /* RFC 4443 s2.4 (e.3): no error for a packet sent to a multicast + * address, with the two Packet Too Big and Parameter Problem + * exceptions. This is the rule that stops multicast amplification. */ + ck_abort_msg("pending: ICMPv6 multicast error suppression"); +} +END_TEST + +START_TEST(test_icmp6_error_quotes_as_much_as_fits_in_min_mtu) +{ + /* RFC 4443 s2.4 (c): the error carries as much of the offending packet + * as fits without exceeding the 1280-byte minimum IPv6 MTU. */ + ck_abort_msg("pending: ICMPv6 error quoting"); +} +END_TEST + +START_TEST(test_icmp6_destination_unreachable_codes) +{ + /* Type 1, codes 0 to 4 (RFC 4443 s3.1). Port unreachable (code 4) is + * the one UDP needs when no socket matches. */ + ck_abort_msg("pending: ICMPv6 destination unreachable"); +} +END_TEST + +START_TEST(test_icmp6_packet_too_big_carries_the_mtu) +{ + /* Type 2 (RFC 4443 s3.2). IPv6 routers never fragment, so this is the + * only path MTU signal there is. */ + ck_abort_msg("pending: ICMPv6 packet too big"); +} +END_TEST + +START_TEST(test_icmp6_time_exceeded_on_hop_limit_zero_when_forwarding) +{ + /* Type 3 code 0 (RFC 4443 s3.3). Note this is a forwarding-time event: + * a packet addressed to us at hop limit zero is accepted, which + * test_ip6_recv_accepts_hop_limit_zero already pins down. */ + ck_abort_msg("pending: ICMPv6 time exceeded"); +} +END_TEST + +START_TEST(test_icmp6_parameter_problem_points_at_the_bad_octet) +{ + /* Type 4 (RFC 4443 s3.4): the pointer field must be the offset of the + * offending octet from the start of the IPv6 header. */ + ck_abort_msg("pending: ICMPv6 parameter problem"); +} +END_TEST + +START_TEST(test_icmp6_unknown_informational_message_is_discarded) +{ + /* RFC 4443 s2.4 (b): an unknown informational message (type >= 128) is + * silently discarded, whereas an unknown error message (type < 128) + * must be passed to the upper layer. */ + ck_abort_msg("pending: ICMPv6 unknown type handling"); +} +END_TEST + +#endif /* WOLFIP_IPV6_HAVE_ICMP6 */ + +/* ========================================================================= + * Neighbor Discovery - RFC 4861 + * ========================================================================= */ +#if WOLFIP_IPV6_HAVE_ND6 + + + + + + + +START_TEST(test_nd6_cache_state_machine_transitions) +{ + /* RFC 4861 s7.3.2: INCOMPLETE, REACHABLE, STALE, DELAY, PROBE. */ + ck_abort_msg("pending: ND cache state machine"); +} +END_TEST + +START_TEST(test_nd6_cache_eviction_when_full) +{ + /* WOLFIP_ND6_CACHE_SIZE entries. Note the IPv4 ARP table silently + * refuses new entries when full rather than evicting; the IPv6 cache + * must not inherit that, or one burst of scan traffic locks out every + * real neighbour. */ + ck_abort_msg("pending: ND cache eviction"); +} +END_TEST + +START_TEST(test_nd6_queues_one_packet_per_pending_resolution) +{ + /* RFC 4861 s7.2.2: at least one packet is queued while resolution is + * in flight, and the queue is bounded. */ + ck_abort_msg("pending: ND pending packet queue"); +} +END_TEST + + + + +START_TEST(test_nd6_prefix_option_with_length_over_128_is_ignored) +{ + /* RFC 4861 s4.6.2. A prefix length above 128 would otherwise index off + * the end of a 16-byte address. */ + ck_abort_msg("pending: ND prefix option validation"); +} +END_TEST + + + +#endif /* WOLFIP_IPV6_HAVE_ND6 */ + +/* ========================================================================= + * SLAAC and DAD - RFC 4862 + * ========================================================================= */ +#if WOLFIP_IPV6_HAVE_SLAAC + + + + + + + + + + +START_TEST(test_slaac_preferred_lifetime_expiry_deprecates_address) +{ + /* RFC 4862 s5.5.4: a deprecated address may still be used by existing + * connections but must not be chosen for new ones. */ + ck_abort_msg("pending: SLAAC address deprecation"); +} +END_TEST + +START_TEST(test_slaac_valid_lifetime_expiry_removes_address) +{ + ck_abort_msg("pending: SLAAC address expiry"); +} +END_TEST + +START_TEST(test_slaac_lifetime_extension_is_bounded) +{ + /* RFC 4862 s5.5.3 (e): the two-hour rule. Without it an attacker can + * extend the lifetime of an address indefinitely with one forged RA. */ + ck_abort_msg("pending: SLAAC two-hour rule"); +} +END_TEST + +START_TEST(test_slaac_respects_the_address_table_limit) +{ + /* WOLFIP_IP6_ADDR_MAX per interface. A stream of RAs advertising + * distinct prefixes must not overflow the table. */ + ck_abort_msg("pending: SLAAC address table bound"); +} +END_TEST + +#endif /* WOLFIP_IPV6_HAVE_SLAAC */ + +/* ========================================================================= + * DHCPv6 client - RFC 8415 + * ========================================================================= */ +#if WOLFIP_IPV6_HAVE_DHCP6 + +START_TEST(test_dhcp6_solicit_goes_to_all_dhcp_servers_multicast) +{ + /* RFC 8415 s16: ff02::1:2, from a link-local source, UDP 546 to 547. */ + ck_abort_msg("pending: DHCPv6 solicit"); +} +END_TEST + +START_TEST(test_dhcp6_solicit_carries_client_id_and_ia_na) +{ + /* RFC 8415 s18.2.1 */ + ck_abort_msg("pending: DHCPv6 solicit options"); +} +END_TEST + +START_TEST(test_dhcp6_advertise_with_mismatched_transaction_id_is_ignored) +{ + /* RFC 8415 s16.3. Without this an off-path attacker can answer a + * request it never saw. */ + ck_abort_msg("pending: DHCPv6 transaction id check"); +} +END_TEST + +START_TEST(test_dhcp6_reply_assigns_the_offered_address) +{ + ck_abort_msg("pending: DHCPv6 reply handling"); +} +END_TEST + +START_TEST(test_dhcp6_retransmission_uses_exponential_backoff) +{ + /* RFC 8415 s15: RT is doubled with randomisation, and clamped at MRT. */ + ck_abort_msg("pending: DHCPv6 retransmission timing"); +} +END_TEST + +START_TEST(test_dhcp6_renew_at_t1_and_rebind_at_t2) +{ + /* RFC 8415 s18.2.4 and s18.2.5 */ + ck_abort_msg("pending: DHCPv6 renew and rebind"); +} +END_TEST + +START_TEST(test_dhcp6_option_longer_than_the_message_is_rejected) +{ + /* An option whose declared length runs past the end of the datagram + * must abort parsing, not read beyond the buffer. */ + ck_abort_msg("pending: DHCPv6 option bounds"); +} +END_TEST + +START_TEST(test_dhcp6_message_larger_than_buffer_is_rejected) +{ + /* WOLFIP_DHCP6_BUF_SIZE */ + ck_abort_msg("pending: DHCPv6 buffer bound"); +} +END_TEST + +#endif /* WOLFIP_IPV6_HAVE_DHCP6 */ + +/* ========================================================================= + * Extension headers - RFC 8200 section 4 + * ========================================================================= */ +#if WOLFIP_IPV6_HAVE_EXTHDR + +START_TEST(test_ip6_walks_hop_by_hop_and_destination_options) +{ + /* When extension header support lands, these two must be skipped to + * reach the upper-layer header. Until then ip6_recv rejects them, which + * test_ip6_recv_rejects_every_extension_header pins down. */ + ck_abort_msg("pending: extension header chain walk"); +} +END_TEST + +START_TEST(test_ip6_extension_header_chain_length_is_capped) +{ + /* An unbounded chain of tiny option headers is a denial of service. + * The walk must stop after a fixed number of headers. */ + ck_abort_msg("pending: extension header chain cap"); +} +END_TEST + +START_TEST(test_ip6_extension_header_with_zero_length_is_rejected) +{ + /* A header claiming zero total length makes the walk loop forever. */ + ck_abort_msg("pending: extension header length validation"); +} +END_TEST + +START_TEST(test_ip6_routing_header_type_zero_is_still_rejected) +{ + /* RFC 5095 deprecated routing header type 0 because it enabled traffic + * amplification. Enabling the chain walk must not resurrect it. */ + ck_abort_msg("pending: routing header type 0 rejection"); +} +END_TEST + +#endif /* WOLFIP_IPV6_HAVE_EXTHDR */ + +/* ========================================================================= + * Sockets and dual stack + * ========================================================================= */ +#if WOLFIP_IPV6_HAVE_SOCKETS + +START_TEST(test_socket_af_inet6_stream_and_dgram_are_created) +{ + ck_abort_msg("pending: AF_INET6 socket creation"); +} +END_TEST + +START_TEST(test_socket_bind_and_getsockname_roundtrip_ipv6) +{ + ck_abort_msg("pending: AF_INET6 bind"); +} +END_TEST + +START_TEST(test_socket_v4_mapped_destination_is_framed_as_ipv4) +{ + /* The family that decides framing is that of the destination address, + * not the socket domain: a v4-mapped destination on an AF_INET6 socket + * must go out as a 20-byte IPv4 header, not a 40-byte IPv6 one. This is + * the central correctness risk of the dual-stack design. */ + ck_abort_msg("pending: v4-mapped framing"); +} +END_TEST + +START_TEST(test_socket_v4_mapped_peer_is_reported_as_mapped_address) +{ + /* An IPv4 peer arriving on a dual-stack AF_INET6 socket is reported as + * ::ffff:a.b.c.d by recvfrom and getpeername. */ + ck_abort_msg("pending: v4-mapped peer reporting"); +} +END_TEST + +START_TEST(test_socket_ipv6_only_option_is_honoured_not_silently_accepted) +{ + /* setsockopt and getsockopt currently return 0 for unknown options, so + * IPV6_V6ONLY would appear to work whatever the stack actually does. + * The option must be genuinely stored and reported. */ + ck_abort_msg("pending: IPV6_V6ONLY"); +} +END_TEST + +START_TEST(test_socket_ipv6_only_socket_rejects_v4_mapped_destination) +{ + ck_abort_msg("pending: IPV6_V6ONLY enforcement"); +} +END_TEST + +START_TEST(test_socket_tcp_mss_accounts_for_the_40_byte_header) +{ + /* The IPv6 header is 20 bytes larger than IPv4, so the MSS derived + * from the same link MTU must be 20 bytes smaller. */ + ck_abort_msg("pending: IPv6 TCP MSS"); +} +END_TEST + +START_TEST(test_socket_udp_oversize_datagram_is_refused) +{ + /* No fragmentation: a datagram larger than the path MTU less headers + * must be refused at sendto rather than silently truncated. */ + ck_abort_msg("pending: IPv6 UDP size limit"); +} +END_TEST + +START_TEST(test_socket_ipv4_and_ipv6_sockets_coexist_on_one_port) +{ + ck_abort_msg("pending: dual-stack port sharing"); +} +END_TEST + +#endif /* WOLFIP_IPV6_HAVE_SOCKETS */ + +#endif /* WOLFIP_IPV6 */ diff --git a/src/test/unit/unit_tests_ipv6_recv.c b/src/test/unit/unit_tests_ipv6_recv.c new file mode 100644 index 00000000..4b197f70 --- /dev/null +++ b/src/test/unit/unit_tests_ipv6_recv.c @@ -0,0 +1,600 @@ +/* unit_tests_ipv6_recv.c + * + * Copyright (C) 2026 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_IPV6 + +/* ========================================================================= + * Environment note + * ========================================================================= + * Receive-path tests for the IPv6 header: ip6_recv() validation, the + * ethertype and MAC-layer demux in wolfIP_recv_on(), and the adversarial + * corpus of malformed frames. + * + * ip6_recv() deliberately returns a distinct negative code per rejection + * reason rather than a bare "dropped", so these tests assert *why* a frame + * was refused. A frame rejected for the wrong reason is a real bug - it + * usually means an earlier check is shadowing a later one - and would go + * unnoticed against a boolean result. + * + * Built only by `make unit-ipv6`. + */ + +/* ========================================================================= + * Local helpers + * ========================================================================= */ + +static const uint8_t ip6_test_peer_mac[6] = {0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x11}; + +/* Build a complete, valid IPv6 frame carrying `payload_len` bytes of upper + * layer. Returns the total frame length including the Ethernet header. */ +static uint32_t ip6_build_frame(uint8_t *buf, const char *src, const char *dst, + uint8_t next_hdr, uint16_t payload_len) +{ + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + ip6 s6; + ip6 d6; + + memset(buf, 0, LINK_MTU); + ck_assert_int_eq(atoip6(src, &s6), 0); + ck_assert_int_eq(atoip6(dst, &d6), 0); + ip6_hdr_set_vtf(pkt, 0, 0); + pkt->payload_len = ee16(payload_len); + pkt->next_hdr = next_hdr; + pkt->hop_limit = 64; + ip6_hdr_set_src(pkt, &s6); + ip6_hdr_set_dst(pkt, &d6); + return (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + payload_len); +} + +/* Build a valid UDP-carrying frame and hand it to ip6_recv(). */ +static int ip6_recv_frame(const char *src, const char *dst) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + uint32_t len = ip6_build_frame(buf, src, dst, IP6_NEXTHDR_UDP, 8); + + wolfIP_init(&s); + mock_link_init(&s); + return ip6_recv(&s, TEST_PRIMARY_IF, (struct wolfIP_ip6_packet *)buf, len); +} + +/* ========================================================================= + * Well formed frames are accepted + * ========================================================================= */ + +START_TEST(test_ip6_recv_accepts_upper_layer_protocols) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 8); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, len), + IP6_ACCEPTED); + + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_TCP, 20); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, len), + IP6_ACCEPTED); + + len = ip6_build_frame(buf, "fe80::1", "ff02::1", IP6_NEXTHDR_ICMPV6, 8); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, len), + IP6_ACCEPTED); +} +END_TEST + +START_TEST(test_ip6_recv_tolerates_ethernet_padding) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 8); + + /* Ethernet pads anything below 60 bytes, so the frame handed up may be + * longer than the header plus payload_len. Requiring an exact match + * would drop every small packet on a real link. */ + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, len + 18), + IP6_ACCEPTED); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, 64), + IP6_ACCEPTED); +} +END_TEST + +START_TEST(test_ip6_recv_accepts_hop_limit_zero) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 8); + pkt->hop_limit = 0; + + /* RFC 8200 section 3: the hop limit is decremented and tested by + * forwarding nodes. A destination host must accept a packet addressed + * to it even at hop limit zero. Dropping it here is a common bug. */ + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), IP6_ACCEPTED); +} +END_TEST + +START_TEST(test_ip6_recv_accepts_unspecified_source) +{ + /* RFC 4862 section 5.4.2: duplicate address detection sends a Neighbor + * Solicitation from :: . Rejecting an unspecified source outright would + * break DAD before it is even implemented. */ + ck_assert_int_eq(ip6_recv_frame("::", "ff02::1:ff00:1"), IP6_ACCEPTED); +} +END_TEST + +START_TEST(test_ip6_recv_accepts_all_scopes_as_destination) +{ + ck_assert_int_eq(ip6_recv_frame("fe80::1", "fe80::2"), IP6_ACCEPTED); + ck_assert_int_eq(ip6_recv_frame("fd00::1", "fd00::2"), IP6_ACCEPTED); + ck_assert_int_eq(ip6_recv_frame("2001:db8::1", "2001:db8::2"), + IP6_ACCEPTED); + ck_assert_int_eq(ip6_recv_frame("fe80::1", "ff02::1"), IP6_ACCEPTED); +} +END_TEST + +/* ========================================================================= + * Malformed frames - length and version + * ========================================================================= */ + +START_TEST(test_ip6_recv_rejects_short_frame) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + uint32_t full; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + full = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 8); + + /* Every length short of a complete header must be refused, and refused + * before any header field is read - reading src/dst out of a 20-byte + * frame would be an overread. */ + for (len = 0; len < (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN); len++) { + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, len), + IP6_DROP_SHORT_FRAME); + } + /* Exactly a header with no payload declared is structurally fine. */ + ck_assert_int_ne(ip6_recv(&s, TEST_PRIMARY_IF, + (struct wolfIP_ip6_packet *)buf, full), + IP6_DROP_SHORT_FRAME); +} +END_TEST + +START_TEST(test_ip6_recv_rejects_wrong_version) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + uint32_t len; + unsigned int v; + + wolfIP_init(&s); + mock_link_init(&s); + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 8); + + for (v = 0; v < 16u; v++) { + uint32_t vtf = ((uint32_t)v << 28); + + pkt->ver_tc_fl = ee32(vtf); + if (v == 6) + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_ACCEPTED); + else + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_DROP_BAD_VERSION); + } +} +END_TEST + +START_TEST(test_ip6_recv_rejects_truncated_payload) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + + wolfIP_init(&s); + mock_link_init(&s); + ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", IP6_NEXTHDR_UDP, 8); + + /* payload_len claims more than the frame actually holds. Trusting it + * would let a peer walk the checksum routine off the end of the + * buffer. */ + pkt->payload_len = ee16(1200); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + 8)), + IP6_DROP_TRUNCATED_PAYLOAD); + + /* One byte short is still short. */ + pkt->payload_len = ee16(9); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + 8)), + IP6_DROP_TRUNCATED_PAYLOAD); + + /* The maximum declarable payload against a minimal frame. */ + pkt->payload_len = ee16(0xFFFF); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + 8)), + IP6_DROP_TRUNCATED_PAYLOAD); +} +END_TEST + +/* ========================================================================= + * Malformed frames - address sanity + * ========================================================================= */ + +START_TEST(test_ip6_recv_rejects_multicast_source) +{ + /* RFC 4291 section 2.7: a multicast address is never a valid source. + * Accepting one invites reflection and amplification. */ + ck_assert_int_eq(ip6_recv_frame("ff02::1", "2001:db8::2"), + IP6_DROP_MCAST_SOURCE); + ck_assert_int_eq(ip6_recv_frame("ff0e::1", "2001:db8::2"), + IP6_DROP_MCAST_SOURCE); +} +END_TEST + +START_TEST(test_ip6_recv_rejects_unspecified_destination) +{ + /* RFC 4291 section 2.5.2: :: must never appear as a destination. */ + ck_assert_int_eq(ip6_recv_frame("2001:db8::1", "::"), + IP6_DROP_UNSPECIFIED_DESTINATION); +} +END_TEST + +START_TEST(test_ip6_recv_rejects_loopback_on_the_wire) +{ + /* RFC 4291 section 2.5.3: ::1 must never cross a real link. The source + * check is what stops an off-link attacker forging a loopback identity + * to impersonate locally originated traffic. */ + ck_assert_int_eq(ip6_recv_frame("::1", "2001:db8::2"), + IP6_DROP_LOOPBACK_ON_WIRE); + ck_assert_int_eq(ip6_recv_frame("2001:db8::1", "::1"), + IP6_DROP_LOOPBACK_ON_WIRE); +} +END_TEST + +START_TEST(test_ip6_recv_rejects_v4mapped_on_the_wire) +{ + /* RFC 4291 section 2.5.5.2: IPv4-mapped addresses exist only inside the + * socket API. wolfIP presents them to dual-stack AF_INET6 sockets, so a + * peer that could smuggle one in over the wire could present an + * arbitrary IPv4 identity to an application. Both fields are checked. */ + ck_assert_int_eq(ip6_recv_frame("::ffff:10.0.0.1", "2001:db8::2"), + IP6_DROP_V4MAPPED_ON_WIRE); + ck_assert_int_eq(ip6_recv_frame("2001:db8::1", "::ffff:10.0.0.1"), + IP6_DROP_V4MAPPED_ON_WIRE); + ck_assert_int_eq(ip6_recv_frame("::ffff:127.0.0.1", "2001:db8::2"), + IP6_DROP_V4MAPPED_ON_WIRE); +} +END_TEST + +START_TEST(test_ip6_recv_rejects_v4compat_on_the_wire) +{ + /* RFC 4291 section 2.5.5.1: IPv4-compatible addresses are deprecated. */ + ck_assert_int_eq(ip6_recv_frame("::1.2.3.4", "2001:db8::2"), + IP6_DROP_V4COMPAT_ON_WIRE); + ck_assert_int_eq(ip6_recv_frame("2001:db8::1", "::1.2.3.4"), + IP6_DROP_V4COMPAT_ON_WIRE); +} +END_TEST + +/* ========================================================================= + * Next Header handling + * ========================================================================= */ + +START_TEST(test_ip6_recv_rejects_every_extension_header) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + uint32_t len; + unsigned int i; + static const uint8_t ext[] = { + IP6_NEXTHDR_HOPOPT, IP6_NEXTHDR_ROUTING, IP6_NEXTHDR_FRAGMENT, + IP6_NEXTHDR_ESP, IP6_NEXTHDR_AH, IP6_NEXTHDR_DSTOPTS, + IP6_NEXTHDR_NONE + }; + + wolfIP_init(&s); + mock_link_init(&s); + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 40); + + /* Phase 0 parses the upper-layer header only. Walking a Next Header + * chain is a denial-of-service surface (nested and looping option + * headers), so each extension header is named and refused rather than + * half-processed. Routing headers in particular were a documented + * amplification vector before RFC 5095 deprecated type 0. */ + for (i = 0; i < (sizeof(ext) / sizeof(ext[0])); i++) { + pkt->next_hdr = ext[i]; + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_DROP_EXTENSION_HEADER); + } +} +END_TEST + +START_TEST(test_ip6_recv_rejects_unknown_next_header) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + uint32_t len; + unsigned int nh; + + wolfIP_init(&s); + mock_link_init(&s); + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, 40); + + /* Sweep the whole 8-bit space. Exactly three values are upper-layer + * protocols we accept; the extension headers get their own reason; and + * everything else is unknown. */ + for (nh = 0; nh < 256u; nh++) { + int expect; + + pkt->next_hdr = (uint8_t)nh; + if ((nh == IP6_NEXTHDR_TCP) || (nh == IP6_NEXTHDR_UDP) || + (nh == IP6_NEXTHDR_ICMPV6)) + expect = IP6_ACCEPTED; + else if (ip6_nexthdr_is_extension((uint8_t)nh)) + expect = IP6_DROP_EXTENSION_HEADER; + else + expect = IP6_DROP_UNKNOWN_NEXTHDR; + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), expect); + } +} +END_TEST + +START_TEST(test_ip6_recv_rejects_short_upper_layer_header) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + uint32_t len; + uint16_t payload; + + wolfIP_init(&s); + mock_link_init(&s); + + /* A declared payload too small to hold the upper-layer header at all. + * Without this check the transport demux would parse fields that are + * not there. */ + for (payload = 0; payload < IP6_MIN_TCP_LEN; payload++) { + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_TCP, payload); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_DROP_SHORT_TRANSPORT); + } + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_TCP, IP6_MIN_TCP_LEN); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), IP6_ACCEPTED); + + for (payload = 0; payload < IP6_MIN_UDP_LEN; payload++) { + len = ip6_build_frame(buf, "2001:db8::1", "2001:db8::2", + IP6_NEXTHDR_UDP, payload); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_DROP_SHORT_TRANSPORT); + } + + for (payload = 0; payload < IP6_MIN_ICMPV6_LEN; payload++) { + len = ip6_build_frame(buf, "fe80::1", "ff02::1", + IP6_NEXTHDR_ICMPV6, payload); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_DROP_SHORT_TRANSPORT); + } +} +END_TEST + +/* ========================================================================= + * Check ordering + * ========================================================================= */ + +START_TEST(test_ip6_recv_checks_structure_before_addresses) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + + /* A frame that is both too short and carries a bad source address must + * be rejected for being short: the address fields have not been proven + * to be inside the buffer yet. */ + len = ip6_build_frame(buf, "ff02::1", "2001:db8::2", IP6_NEXTHDR_UDP, 8); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, 20), + IP6_DROP_SHORT_FRAME); + + /* Likewise version is checked before the addresses are interpreted. */ + pkt->ver_tc_fl = ee32(0x40000000u); + ck_assert_int_eq(ip6_recv(&s, TEST_PRIMARY_IF, pkt, len), + IP6_DROP_BAD_VERSION); +} +END_TEST + +/* ========================================================================= + * Ethertype and MAC demux + * ========================================================================= */ + +START_TEST(test_eth_is_ipv6_multicast_mac) +{ + const uint8_t sol[6] = {0x33, 0x33, 0xFF, 0x00, 0x00, 0x01}; + const uint8_t nodes[6] = {0x33, 0x33, 0x00, 0x00, 0x00, 0x01}; + const uint8_t unicast[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; + const uint8_t broadcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const uint8_t ipv4_mcast[6] = {0x01, 0x00, 0x5E, 0x00, 0x00, 0x01}; + const uint8_t near_miss[6] = {0x33, 0x34, 0x00, 0x00, 0x00, 0x01}; + + ck_assert_int_eq(eth_is_ipv6_multicast_mac(sol), 1); + ck_assert_int_eq(eth_is_ipv6_multicast_mac(nodes), 1); + ck_assert_int_eq(eth_is_ipv6_multicast_mac(unicast), 0); + ck_assert_int_eq(eth_is_ipv6_multicast_mac(broadcast), 0); + ck_assert_int_eq(eth_is_ipv6_multicast_mac(ipv4_mcast), 0); + ck_assert_int_eq(eth_is_ipv6_multicast_mac(near_miss), 0); +} +END_TEST + +START_TEST(test_ip6_demux_accepts_unicast_and_multicast_frames) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + struct wolfIP_ll_dev *ll; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + + len = ip6_build_frame(buf, "fe80::1", "ff02::1", IP6_NEXTHDR_ICMPV6, 8); + pkt->eth.type = ee16(ETH_TYPE_IPV6); + memcpy(pkt->eth.src, ip6_test_peer_mac, 6); + + /* Unicast to our own MAC. */ + memcpy(pkt->eth.dst, ll->mac, 6); + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, buf, len); + /* Phase 0 has no upper-layer delivery, so nothing is transmitted; the + * check here is that a well formed frame traverses the demux without + * crashing or provoking a spurious reply. */ + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); + + /* 33:33:.. multicast must also be accepted: Neighbor Discovery and + * Router Advertisements never arrive on our unicast MAC. */ + ip6_mcast_to_eth(&(ip6)WOLFIP_IN6ADDR_ANY_INIT, pkt->eth.dst); + pkt->eth.dst[0] = 0x33; + pkt->eth.dst[1] = 0x33; + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, buf, len); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +START_TEST(test_ip6_demux_ignores_frames_for_other_hosts) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + const uint8_t other[6] = {0x02, 0xDE, 0xAD, 0xBE, 0xEF, 0x99}; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + len = ip6_build_frame(buf, "fe80::1", "ff02::1", IP6_NEXTHDR_ICMPV6, 8); + pkt->eth.type = ee16(ETH_TYPE_IPV6); + memcpy(pkt->eth.src, ip6_test_peer_mac, 6); + memcpy(pkt->eth.dst, other, 6); + + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, buf, len); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +START_TEST(test_ip6_demux_survives_a_truncated_frame) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + struct wolfIP_ll_dev *ll; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + + ip6_build_frame(buf, "fe80::1", "ff02::1", IP6_NEXTHDR_ICMPV6, 8); + pkt->eth.type = ee16(ETH_TYPE_IPV6); + memcpy(pkt->eth.dst, ll->mac, 6); + memcpy(pkt->eth.src, ip6_test_peer_mac, 6); + + /* Drivers do not always pad. Every truncation from a bare Ethernet + * header upwards must be survivable; under ASan this is where an + * overread would surface. */ + for (len = ETH_HEADER_LEN; + len < (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN + 8); len++) { + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, buf, len); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); + } +} +END_TEST + +START_TEST(test_ip6_ethertype_does_not_disturb_ipv4_or_arp) +{ + struct wolfIP s; + uint8_t buf[LINK_MTU]; + struct wolfIP_ip6_packet *pkt = (struct wolfIP_ip6_packet *)buf; + struct wolfIP_ll_dev *ll; + uint32_t len; + + wolfIP_init(&s); + mock_link_init(&s); + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + ck_assert_ptr_nonnull(ll); + + len = ip6_build_frame(buf, "fe80::1", "ff02::1", IP6_NEXTHDR_ICMPV6, 8); + memcpy(pkt->eth.dst, ll->mac, 6); + memcpy(pkt->eth.src, ip6_test_peer_mac, 6); + + /* An IPv6 payload announced as IPv4 must be handled by the IPv4 path + * and dropped there (version nibble is 6), never leak into ip6_recv. */ + pkt->eth.type = ee16(ETH_TYPE_IP); + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, buf, len); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); + + /* An unknown ethertype is ignored entirely. */ + pkt->eth.type = ee16(0x1234); + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, buf, len); + ck_assert_uint_eq((uint32_t)last_frame_sent_size, 0u); +} +END_TEST + +#endif /* WOLFIP_IPV6 */ diff --git a/src/wolfesp.c b/src/wolfesp.c index 05397833..0175468d 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -22,6 +22,27 @@ #if defined(WOLFIP_ESP) && !defined(WOLFESP_SRC) #define WOLFESP_SRC #include "wolfesp.h" +/* Wipe key material. + * + * wolfSSL offers two spellings and neither is portable for a consumer of an + * installed library: wolfIP_esp_forcezero() is a recent addition to memory.h and is + * absent from, for example, 5.6.6, while ForceZero() is the inline helper in + * wolfcrypt/src/misc.c, which a package such as libwolfssl-dev does not ship + * - misc.h only declares it when NO_INLINE is set. Depending on either makes + * the build succeed or fail according to which wolfSSL happens to be + * installed. + * + * The volatile pointer is what stops the compiler treating this as a dead + * store and removing it, which is the whole point of the wolfSSL helpers + * too. */ +static void wolfIP_esp_forcezero(void *mem, size_t len) +{ + volatile unsigned char *p = (volatile unsigned char *)mem; + + while (len-- > 0) + *p++ = 0; +} + static WC_RNG wc_rng; static volatile int rng_inited = 0; /* security association static pool*/ @@ -66,8 +87,8 @@ int wolfIP_esp_init(void) void wolfIP_esp_sa_del_all(void) { - wc_ForceZero(in_sa_list, sizeof(in_sa_list)); - wc_ForceZero(out_sa_list, sizeof(out_sa_list)); + wolfIP_esp_forcezero(in_sa_list, sizeof(in_sa_list)); + wolfIP_esp_forcezero(out_sa_list, sizeof(out_sa_list)); return; } @@ -108,7 +129,7 @@ void wolfIP_esp_sa_del(int in, uint8_t * spi) wolfIP_esp_sa * sa = NULL; sa = esp_sa_get(in, spi); if (sa != NULL) { - wc_ForceZero(sa, sizeof(*sa)); + wolfIP_esp_forcezero(sa, sizeof(*sa)); } return; } @@ -203,7 +224,7 @@ int wolfIP_esp_sa_new_gcm(int in, uint8_t * spi, ip4 src, ip4 dst, ESP_GCM_RFC4106_IV_LEN); if (err) { ESP_LOG("error: wc_RNG_GenerateBlock: %d\n", err); - wc_ForceZero(new_sa, sizeof(*new_sa)); + wolfIP_esp_forcezero(new_sa, sizeof(*new_sa)); err = -1; } @@ -1007,7 +1028,7 @@ esp_aes_rfc4106_dec(const wolfIP_esp_sa * esp_sa, uint8_t * esp_data, } rfc4106_dec_out: - wc_ForceZero(nonce, salt_len); + wolfIP_esp_forcezero(nonce, salt_len); if (inited) { wc_AesFree(&gcm_dec); inited = 0; @@ -1080,7 +1101,7 @@ esp_aes_rfc4106_enc(const wolfIP_esp_sa * esp_sa, uint8_t * esp_data, } rfc4106_enc_out: - wc_ForceZero(nonce, salt_len); + wolfIP_esp_forcezero(nonce, salt_len); if (inited) { wc_AesFree(&gcm_enc); inited = 0; @@ -1134,7 +1155,7 @@ esp_aes_rfc4543_dec(const wolfIP_esp_sa * esp_sa, uint8_t * esp_data, } rfc4543_dec_out: - wc_ForceZero(nonce, salt_len); + wolfIP_esp_forcezero(nonce, salt_len); return err; } @@ -1190,7 +1211,7 @@ esp_aes_rfc4543_enc(const wolfIP_esp_sa * esp_sa, uint8_t * esp_data, } rfc4543_enc_out: - wc_ForceZero(nonce, salt_len); + wolfIP_esp_forcezero(nonce, salt_len); if (inited) { wc_AesFree(&gmac_enc.aes); inited = 0; diff --git a/src/wolfip.c b/src/wolfip.c index e45bb504..b65c0d2c 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -30,7 +30,17 @@ #include #endif #include "wolfip.h" +/* WOLFIP_CONFIG names an alternative configuration header, so a port or a + * test build can select one without editing the tree: + * -DWOLFIP_CONFIG='"myconfig.h"' + */ +#ifdef WOLFIP_CONFIG +#include WOLFIP_CONFIG +#else #include "config.h" +#endif +/* Applies the IPv6 defaults on top of whichever configuration was used. */ +#include "wolfip6_config.h" #ifndef LINK_MTU_MIN #define LINK_MTU_MIN 64U @@ -74,7 +84,15 @@ struct wolfIP_udp_datagram; struct wolfIP_icmp_packet; /* Fixed size binary heap: each element is a timer. */ +#if WOLFIP_IPV6 +/* Neighbor Discovery drives duplicate address detection, router + * solicitation retries and every cache expiry from a single periodic tick, + * so it needs one slot rather than one per address. The spare few are for + * the heap not to be exactly full when DHCP, DNS and TCP are all armed. */ +#define MAX_TIMERS ((MAX_TCPSOCKETS * 3) + 4) +#else #define MAX_TIMERS (MAX_TCPSOCKETS * 3) +#endif /* Constants */ #define ICMP_ECHO_REPLY 0 @@ -131,6 +149,9 @@ struct wolfIP_icmp_packet; #define ETH_TYPE_IP 0x0800 #define ETH_TYPE_ARP 0x0806 +#if WOLFIP_IPV6 +#define ETH_TYPE_IPV6 0x86DD +#endif #if WOLFIP_VLAN #define ETH_TYPE_VLAN_8021Q 0x8100 #define WOLFIP_VLAN_TAG_LEN 4 @@ -1333,6 +1354,55 @@ static void wolfIP_forward_packet(struct wolfIP *s, unsigned int out_if, #endif +#if WOLFIP_IPV6 +/* Neighbor Discovery data structures (RFC 4861 section 5.1). Defined here, + * beside struct arp_neighbor, because struct wolfIP embeds them; the + * implementation lives in src/wolfip6.c. + * + * Deliberately shaped like the ARP structures above: same fields where the + * two protocols agree, plus the reachability state machine and the router + * flag that ARP has no equivalent for. */ +enum nd6_state { + ND6_INCOMPLETE = 1, /* address resolution in flight, no MAC yet */ + ND6_REACHABLE, /* confirmed reachable recently */ + ND6_STALE, /* MAC known, reachability unconfirmed */ + ND6_DELAY, /* waiting before probing */ + ND6_PROBE /* unicast probes in flight */ +}; + +struct nd6_neighbor { + ip6 addr; + uint8_t mac[6]; + uint8_t if_idx; + uint8_t state; /* enum nd6_state; 0 means the slot is free */ + uint8_t is_router; + uint8_t probes; /* solicitations sent in the current state */ + uint64_t ts; /* last state change */ +}; + +/* On-link prefix, from a Prefix Information option (RFC 4861 section 4.6.2). */ +struct nd6_prefix { + ip6 prefix; + uint8_t prefix_len; + uint8_t if_idx; + uint8_t used; + uint8_t onlink; /* L flag */ + uint8_t autonomous; /* A flag: may be used to form an address */ + uint32_t valid_lifetime; + uint32_t preferred_lifetime; + uint64_t ts; +}; + +/* Default router (RFC 4861 section 6.3.4). */ +struct nd6_router { + ip6 addr; + uint8_t if_idx; + uint8_t used; + uint16_t lifetime; /* seconds; zero means "not a default router" */ + uint64_t ts; +}; +#endif /* WOLFIP_IPV6 */ + struct wolfIP; struct wolfIP_timer { @@ -1404,6 +1474,37 @@ struct wolfIP { uint32_t route_generation; struct wolfIP_route_entry routes[WOLFIP_MAX_ROUTES]; #endif +#if WOLFIP_IF_MULTICONF + /* Additional interface addresses: IPv4 aliases and every IPv6 address. + * The primary IPv4 address of each interface stays in ipconf[] and is + * never duplicated here, so the two cannot drift apart. A flat pool + * shared by all interfaces, so its size grows independently of + * WOLFIP_MAX_INTERFACES. Compiled out entirely when the feature is off, + * which is why struct wolfIP does not grow in the default build. */ + struct wolfIP_ifaddr_slot { + uint8_t used; + /* Duplicate address detection state, only meaningful while + * info.state is WOLFIP_IFADDR_TENTATIVE (RFC 4862 section 5.4). */ + uint8_t dad_probes; /* solicitations still to send */ + uint64_t dad_due; /* when the next probe or the decision is due */ + struct wolfIP_ifaddr_info info; + } ifaddr[WOLFIP_IFADDR_MAX]; +#endif +#if WOLFIP_IPV6 + /* Neighbor Discovery state (RFC 4861 section 5.1): the neighbour cache, + * the on-link prefix list and the default router list. The IPv6 + * counterpart of struct wolfIP_arp below, and deliberately shaped like + * it. */ + struct wolfIP_nd6 { + struct nd6_neighbor neighbors[WOLFIP_ND6_CACHE_SIZE]; + struct nd6_prefix prefixes[WOLFIP_ND6_PREFIX_MAX]; + struct nd6_router routers[WOLFIP_ND6_ROUTER_MAX]; + uint64_t rs_due[WOLFIP_MAX_INTERFACES]; /* next router solicitation */ + uint8_t rs_left[WOLFIP_MAX_INTERFACES]; /* solicitations remaining */ + uint8_t started[WOLFIP_MAX_INTERFACES]; /* wolfIP_ipv6_start() called */ + uint32_t tick_timer; + } nd6; +#endif #ifdef ETHERNET struct wolfIP_arp { uint64_t last_arp[WOLFIP_MAX_INTERFACES]; @@ -2157,13 +2258,13 @@ static unsigned int wolfIP_if_for_local_ip(struct wolfIP *s, ip4 local_ip, int * primary = WOLFIP_PRIMARY_IF_IDX; if (local_ip == IPADDR_ANY) return primary; - for (i = 0; i < s->if_count; i++) { - struct ipconf *conf = &s->ipconf[i]; - if (conf->ip == local_ip) { - if (found) - *found = 1; - return i; - } + /* Consults the alias list as well as the primary addresses. An alias + * that did not resolve to its interface here would be decorative: + * binding a socket to it would silently pick the wrong interface. */ + if (wolfIP_ifaddr_is_local4(s, local_ip, &i) && (i < s->if_count)) { + if (found) + *found = 1; + return i; } return primary; } @@ -2547,9 +2648,20 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, int peer_match = (t->sock.udp.connected == 0) || ((t->dst_port == 0 || t->dst_port == ee16(udp->src_port)) && (t->remote_ip == 0 || t->remote_ip == src_ip)); + /* A socket bound to INADDR_ANY is a wildcard and must accept + * datagrams addressed to any of our local addresses. bind() resolves + * the wildcard to the interface's primary address for source + * selection, so local_ip alone cannot express this; bound_local_ip + * records what the application actually asked for. Until an + * interface could carry more than one address the two were the same + * thing and the distinction did not matter. The TCP listen path + * already filters on bound_local_ip for the same reason. */ int addr_match = (((t->local_ip == 0) && DHCP_IS_RUNNING(s)) || - (t->local_ip == dst_ip && peer_match)); + (t->local_ip == dst_ip && peer_match) || + ((t->local_ip != IPADDR_ANY) && + (t->bound_local_ip == IPADDR_ANY) && peer_match && + wolfIP_ifaddr_is_local4(s, dst_ip, NULL))); #ifdef IP_MULTICAST if (wolfIP_ip_is_multicast(dst_ip)) { addr_match = udp_socket_has_mcast(t, if_idx, dst_ip) && @@ -4485,6 +4597,15 @@ static int ip_output_add_header(struct tsocket *t, struct wolfIP_ip_packet *ip, return 0; } +#if WOLFIP_IPV6 +/* IPv6 header encapsulation and parsing. Included here, rather than compiled + * separately, because it needs struct wolfIP and the static checksum, + * Ethernet and link-layer helpers above - the same arrangement as + * src/wolfesp.c. Placed after ip_output_add_header() so every helper it uses + * is already defined. */ +#include "src/wolfip6.c" +#endif /* WOLFIP_IPV6 */ + /* Process timestamp option, calculate RTT */ static int tcp_process_ts(struct tsocket *t, const struct wolfIP_tcp_seg *tcp, uint32_t frame_len) @@ -5808,6 +5929,341 @@ static struct packetsocket *wolfIP_packetsocket_from_fd(struct wolfIP *s, int so #endif +/* ---------------------------------------------------------------------- */ +/* Per-interface address list */ +/* ---------------------------------------------------------------------- */ + +/* Prefix length to netmask. wolfIP_prefix_mask() does this already but is + * compiled only with forwarding enabled, and this API is always present. */ +static uint32_t ifaddr_plen_to_mask(uint8_t prefix_len) +{ + if (prefix_len == 0U) + return 0U; + if (prefix_len >= 32U) + return 0xFFFFFFFFU; + return 0xFFFFFFFFU << (32U - prefix_len); +} + +static int ifaddr_if_valid(struct wolfIP *s, unsigned int if_idx) +{ + if (!s) + return 0; + if (if_idx >= WOLFIP_MAX_INTERFACES) + return 0; + return 1; +} + +/* Number of pool entries of `family` belonging to an interface. */ +static unsigned int ifaddr_pool_count(struct wolfIP *s, unsigned int if_idx, + int family) +{ +#if WOLFIP_IF_MULTICONF + unsigned int n = 0; + unsigned int i; + + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.if_idx != (uint8_t)if_idx) + continue; + if (s->ifaddr[i].info.family == (uint16_t)family) + n++; + } + return n; +#else + (void)s; (void)if_idx; (void)family; + return 0; +#endif +} + +/* Total addresses on an interface, both families, primary included. */ +static unsigned int ifaddr_total(struct wolfIP *s, unsigned int if_idx) +{ + unsigned int n = 0; + + if (s->ipconf[if_idx].ip != IPADDR_ANY) + n++; + n += ifaddr_pool_count(s, if_idx, AF_INET); + n += ifaddr_pool_count(s, if_idx, AF_INET6); + return n; +} + +unsigned int wolfIP_ifaddr_count(struct wolfIP *s, unsigned int if_idx, + int family) +{ + unsigned int n; + + if (!ifaddr_if_valid(s, if_idx)) + return 0; + if ((family != AF_INET) && (family != AF_INET6)) + return 0; + n = ifaddr_pool_count(s, if_idx, family); + if ((family == AF_INET) && (s->ipconf[if_idx].ip != IPADDR_ANY)) + n++; + return n; +} + +int wolfIP_ifaddr_get(struct wolfIP *s, unsigned int if_idx, int family, + unsigned int index, struct wolfIP_ifaddr_info *out) +{ +#if WOLFIP_IF_MULTICONF + unsigned int seen = 0; + unsigned int i; +#endif + + if (!ifaddr_if_valid(s, if_idx) || !out) + return -WOLFIP_EINVAL; + if ((family != AF_INET) && (family != AF_INET6)) + return -WOLFIP_EINVAL; + + /* Index 0 of the IPv4 list is the primary, when there is one. */ + if ((family == AF_INET) && (s->ipconf[if_idx].ip != IPADDR_ANY)) { + if (index == 0) { + uint8_t plen = 0; + + memset(out, 0, sizeof(*out)); + out->family = AF_INET; + out->if_idx = (uint8_t)if_idx; + out->v4 = s->ipconf[if_idx].ip; + if (wolfIP_mask_prefix_len(s->ipconf[if_idx].mask, &plen) != 0) + plen = 0; + out->prefix_len = plen; + out->state = WOLFIP_IFADDR_PREFERRED; + out->flags = WOLFIP_IFADDR_FLAG_PRIMARY; + return 0; + } + index--; + } + +#if WOLFIP_IF_MULTICONF + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.if_idx != (uint8_t)if_idx) + continue; + if (s->ifaddr[i].info.family != (uint16_t)family) + continue; + if (seen == index) { + memcpy(out, &s->ifaddr[i].info, sizeof(*out)); + return 0; + } + seen++; + } +#endif + return -WOLFIP_EINVAL; +} + +int wolfIP_ifaddr_is_local4(struct wolfIP *s, ip4 addr, unsigned int *if_idx) +{ + unsigned int i; + + if (!s || (addr == IPADDR_ANY)) + return 0; + for (i = 0; i < WOLFIP_MAX_INTERFACES; i++) { + if (s->ipconf[i].ip == addr) { + if (if_idx) + *if_idx = i; + return 1; + } + } +#if WOLFIP_IF_MULTICONF + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.family != AF_INET) + continue; + if (s->ifaddr[i].info.v4 == addr) { + if (if_idx) + *if_idx = s->ifaddr[i].info.if_idx; + return 1; + } + } +#endif + return 0; +} + +#if WOLFIP_IF_MULTICONF +/* First unused pool slot, or -1. */ +static int ifaddr_pool_alloc(struct wolfIP *s) +{ + unsigned int i; + + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + return (int)i; + } + return -1; +} +#endif + +int wolfIP_ifaddr_add4(struct wolfIP *s, unsigned int if_idx, ip4 addr, + uint8_t prefix_len) +{ +#if WOLFIP_IF_MULTICONF + int slot; +#endif + + if (!ifaddr_if_valid(s, if_idx)) + return -WOLFIP_EINVAL; + if ((addr == IPADDR_ANY) || (prefix_len > 32U)) + return -WOLFIP_EINVAL; + /* An address may live on exactly one interface. */ + if (wolfIP_ifaddr_is_local4(s, addr, NULL)) + return -WOLFIP_EINVAL; + if (ifaddr_total(s, if_idx) >= WOLFIP_IF_CONF_MAX) + return -WOLFIP_ENOMEM; + + /* With no primary yet this becomes it, so a single-address build is + * fully usable through this API alone. */ + if (s->ipconf[if_idx].ip == IPADDR_ANY) { + s->ipconf[if_idx].ip = addr; + s->ipconf[if_idx].mask = ifaddr_plen_to_mask(prefix_len); + return 0; + } +#if WOLFIP_IF_MULTICONF + slot = ifaddr_pool_alloc(s); + if (slot < 0) + return -WOLFIP_ENOMEM; + memset(&s->ifaddr[slot].info, 0, sizeof(s->ifaddr[slot].info)); + s->ifaddr[slot].info.family = AF_INET; + s->ifaddr[slot].info.if_idx = (uint8_t)if_idx; + s->ifaddr[slot].info.prefix_len = prefix_len; + s->ifaddr[slot].info.state = WOLFIP_IFADDR_PREFERRED; + s->ifaddr[slot].info.v4 = addr; + s->ifaddr[slot].used = 1; + return 0; +#else + return -WOLFIP_ENOMEM; +#endif +} + +int wolfIP_ifaddr_del4(struct wolfIP *s, unsigned int if_idx, ip4 addr) +{ +#if WOLFIP_IF_MULTICONF + unsigned int i; +#endif + + if (!ifaddr_if_valid(s, if_idx) || (addr == IPADDR_ANY)) + return -WOLFIP_EINVAL; + + if (s->ipconf[if_idx].ip == addr) { +#if WOLFIP_IF_MULTICONF + /* Promote the first alias so the interface keeps a usable primary; + * removing the primary must not strand the aliases. */ + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.if_idx != (uint8_t)if_idx) + continue; + if (s->ifaddr[i].info.family != AF_INET) + continue; + s->ipconf[if_idx].ip = s->ifaddr[i].info.v4; + s->ipconf[if_idx].mask = + ifaddr_plen_to_mask(s->ifaddr[i].info.prefix_len); + s->ifaddr[i].used = 0; + return 0; + } +#endif + s->ipconf[if_idx].ip = IPADDR_ANY; + s->ipconf[if_idx].mask = 0; + return 0; + } +#if WOLFIP_IF_MULTICONF + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.if_idx != (uint8_t)if_idx) + continue; + if (s->ifaddr[i].info.family != AF_INET) + continue; + if (s->ifaddr[i].info.v4 == addr) { + s->ifaddr[i].used = 0; + return 0; + } + } +#endif + return -WOLFIP_EINVAL; +} + +int wolfIP_ifaddr_add6(struct wolfIP *s, unsigned int if_idx, const ip6 *addr, + uint8_t prefix_len) +{ +#if WOLFIP_IPV6 + unsigned int i; + int slot; +#endif + + if (!ifaddr_if_valid(s, if_idx) || !addr) + return -WOLFIP_EINVAL; + if (prefix_len > 128U) + return -WOLFIP_EINVAL; +#if !WOLFIP_IPV6 + /* Nothing can hold a v6 address in this build, and saying so beats + * appearing to succeed. */ + return -WOLFIP_EINVAL; +#else + if (ip6_is_unspecified(addr) || ip6_is_multicast(addr)) + return -WOLFIP_EINVAL; + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.family != AF_INET6) + continue; + if (ip6_cmp(&s->ifaddr[i].info.v6, addr) == 0) { + /* Link-local addresses are identified by {address, zone}. The + * same numeric address on another interface is not a duplicate + * (RFC 4007 section 5). Other unicast addresses remain unique + * across this host. */ + if (!ip6_is_link_local(addr) || + (s->ifaddr[i].info.if_idx == (uint8_t)if_idx)) + return -WOLFIP_EINVAL; + } + } + if (ifaddr_total(s, if_idx) >= WOLFIP_IF_CONF_MAX) + return -WOLFIP_ENOMEM; + slot = ifaddr_pool_alloc(s); + if (slot < 0) + return -WOLFIP_ENOMEM; + memset(&s->ifaddr[slot].info, 0, sizeof(s->ifaddr[slot].info)); + s->ifaddr[slot].info.family = AF_INET6; + s->ifaddr[slot].info.if_idx = (uint8_t)if_idx; + s->ifaddr[slot].info.prefix_len = prefix_len; + s->ifaddr[slot].info.state = WOLFIP_IFADDR_PREFERRED; + if (ip6_is_link_local(addr)) + s->ifaddr[slot].info.flags = WOLFIP_IFADDR_FLAG_LINKLOCAL; + ip6_copy(&s->ifaddr[slot].info.v6, addr); + s->ifaddr[slot].used = 1; + return 0; +#endif +} + +int wolfIP_ifaddr_del6(struct wolfIP *s, unsigned int if_idx, const ip6 *addr) +{ +#if WOLFIP_IPV6 + unsigned int i; +#endif + + if (!ifaddr_if_valid(s, if_idx) || !addr) + return -WOLFIP_EINVAL; +#if !WOLFIP_IPV6 + return -WOLFIP_EINVAL; +#else + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.if_idx != (uint8_t)if_idx) + continue; + if (s->ifaddr[i].info.family != AF_INET6) + continue; + if (ip6_cmp(&s->ifaddr[i].info.v6, addr) == 0) { + s->ifaddr[i].used = 0; + return 0; + } + } + return -WOLFIP_EINVAL; +#endif +} + int wolfIP_sock_socket(struct wolfIP *s, int domain, int type, int protocol) { struct tsocket *ts; @@ -9581,6 +10037,20 @@ static void wolfIP_recv_on(struct wolfIP *s, unsigned int if_idx, void *buf, uin } else if (eth->type == ee16(ETH_TYPE_ARP)) { arp_recv(s, if_idx, buf, len); } +#if WOLFIP_IPV6 + else if (eth->type == ee16(ETH_TYPE_IPV6)) { + struct wolfIP_ip6_packet *ip6pkt = (struct wolfIP_ip6_packet *)eth; + /* Unicast to us, or any IPv6 multicast group: Neighbor Discovery and + * Router Advertisements all arrive on 33:33:.. addresses, so the + * filter has to let them through. Per-group membership filtering + * belongs with the multicast layer, not here. */ + if ((memcmp(eth->dst, ll->mac, 6) != 0) && + !eth_is_ipv6_multicast_mac(eth->dst)) { + return; /* Not for us */ + } + (void)ip6_recv(s, if_idx, ip6pkt, len); + } +#endif #else /* No ethernet, assume IP */ ip = (struct wolfIP_ip_packet *)buf; @@ -10659,6 +11129,12 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) s->last_tick = now; /* Poll the device */ +#if WOLFIP_IPV6 + /* Re-arms the Neighbor Discovery tick when there is work and no timer + * running: the wake-up after an idle period, and the recovery path if an + * earlier insert lost to a full heap. */ + nd6_poll(s); +#endif poll_devices(s); /* Handle timers */ diff --git a/src/wolfip6.c b/src/wolfip6.c new file mode 100644 index 00000000..5dbfed41 --- /dev/null +++ b/src/wolfip6.c @@ -0,0 +1,1870 @@ +/* wolfip6.c + * + * IPv6 header encapsulation and parsing for the wolfIP TCP/IP stack. + * + * Copyright (C) 2026 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 + */ + +/* This file is textually included into src/wolfip.c under #if WOLFIP_IPV6, + * after ip_output_add_header(), the same way src/wolfesp.c is included. It is + * not a separate translation unit: struct wolfIP and the checksum, Ethernet + * and link-layer helpers it needs are all static symbols inside wolfip.c. + * + * Scope of this file today is deliberately narrow: the IPv6 header itself. + * Parsing and validation on receive, encapsulation on transmit, and the + * upper-layer checksum that both need. ICMPv6, Neighbor Discovery, SLAAC and + * DHCPv6 are not here yet; the receive path classifies what it cannot handle + * and reports a distinct reason rather than pretending to process it. + */ + +/* ---------------------------------------------------------------------- */ +/* Constants */ +/* ---------------------------------------------------------------------- */ + +#define IP6_HEADER_LEN 40 +#define IP6_VERSION 6 +#define IP6_HOP_LIMIT_DEFAULT 64 + +/* Next Header values (RFC 8200 section 4.1). The extension headers are + * listed so the receive path can name what it is refusing, rather than + * lumping them in with unknown protocols. */ +#define IP6_NEXTHDR_HOPOPT 0 +#define IP6_NEXTHDR_TCP 6 +#define IP6_NEXTHDR_UDP 17 +#define IP6_NEXTHDR_ROUTING 43 +#define IP6_NEXTHDR_FRAGMENT 44 +#define IP6_NEXTHDR_ESP 50 +#define IP6_NEXTHDR_AH 51 +#define IP6_NEXTHDR_ICMPV6 58 +#define IP6_NEXTHDR_NONE 59 +#define IP6_NEXTHDR_DSTOPTS 60 + +/* ICMPv6 message types (RFC 4443 sections 4.1 and 4.2, RFC 4861 section 4). */ +#define ICMP6_ECHO_REQUEST 128 +#define ICMP6_ECHO_REPLY 129 +#define ICMP6_ROUTER_SOLICIT 133 +#define ICMP6_ROUTER_ADVERT 134 +#define ICMP6_NEIGHBOR_SOLICIT 135 +#define ICMP6_NEIGHBOR_ADVERT 136 +#define ICMP6_REDIRECT 137 + +/* An Echo message carries type, code and checksum, then an identifier and a + * sequence number: eight bytes before any payload. */ +#define ICMP6_ECHO_MIN_LEN 8 + +/* Minimum bytes an upper-layer header needs before it can be parsed. */ +#define IP6_MIN_TCP_LEN 20 +#define IP6_MIN_UDP_LEN 8 +#define IP6_MIN_ICMPV6_LEN 4 + +/* Outcome of ip6_recv(). Zero means the packet was accepted; every rejection + * has its own value so the malformed-input tests can assert precisely why a + * frame was dropped instead of merely that it was. */ +enum ip6_recv_result { + IP6_ACCEPTED = 0, + IP6_DROP_SHORT_FRAME = -1, + IP6_DROP_BAD_VERSION = -2, + IP6_DROP_TRUNCATED_PAYLOAD = -3, + IP6_DROP_MCAST_SOURCE = -4, + IP6_DROP_LOOPBACK_ON_WIRE = -5, + IP6_DROP_V4MAPPED_ON_WIRE = -6, + IP6_DROP_V4COMPAT_ON_WIRE = -7, + IP6_DROP_MCAST_DESTINATION_IS_SOURCE_ONLY = -8, + IP6_DROP_EXTENSION_HEADER = -9, + IP6_DROP_UNKNOWN_NEXTHDR = -10, + IP6_DROP_SHORT_TRANSPORT = -11, + IP6_DROP_UNSPECIFIED_DESTINATION = -12 +}; + +/* ---------------------------------------------------------------------- */ +/* Wire structures */ +/* ---------------------------------------------------------------------- */ + +/* The IPv4 structures in wolfip.c embed the link-layer and network headers + * by value, so the transport payload sits at a fixed offset of eth(14) + + * ip(20). The IPv6 header is 40 bytes, so these are parallel definitions + * rather than a reuse of the IPv4 ones; there is no headroom mechanism to + * borrow. */ +struct PACKED wolfIP_ip6_packet { +#ifdef ETHERNET + struct wolfIP_eth_frame eth; +#endif + uint32_t ver_tc_fl; /* version(4) | traffic class(8) | flow label(20) */ + uint16_t payload_len; + uint8_t next_hdr; + uint8_t hop_limit; + uint8_t src[16]; + uint8_t dst[16]; + uint8_t data[0]; +}; + +/* The same header without the Ethernet prefix, for quoting inside ICMPv6 + * error messages later (mirrors struct wolfIP_ip_wire). */ +struct PACKED wolfIP_ip6_wire { + uint32_t ver_tc_fl; + uint16_t payload_len; + uint8_t next_hdr; + uint8_t hop_limit; + uint8_t src[16]; + uint8_t dst[16]; + uint8_t data[0]; +}; + +struct PACKED wolfIP_tcp6_seg { + struct wolfIP_ip6_packet ip6; + uint16_t src_port, dst_port; + uint32_t seq, ack; + uint8_t hlen, flags; + uint16_t win, csum, urg; + uint8_t data[0]; +}; + +struct PACKED wolfIP_udp6_datagram { + struct wolfIP_ip6_packet ip6; + uint16_t src_port, dst_port, len, csum; + uint8_t data[0]; +}; + +struct PACKED wolfIP_icmp6_packet { + struct wolfIP_ip6_packet ip6; + uint8_t type, code; + uint16_t csum; + uint8_t data[0]; +}; + +/* The IPv6 pseudo-header (RFC 8200 section 8.1): 40 bytes, with a 32-bit + * upper-layer length and the next header in the last octet. Structurally + * different enough from the IPv4 one that it gets its own union and its own + * checksum routine rather than a widened shared version. */ +union transport6_pseudo_header { + struct PACKED ph6 { + uint8_t src[16]; + uint8_t dst[16]; + uint32_t len; + uint8_t zero[3]; + uint8_t proto; + } ph; + uint16_t buf[20]; +}; + +/* IPv6 multicast maps onto 33:33:xx:xx:xx:xx (RFC 2464 section 7). Used by + * the ingress MAC filter, which would otherwise discard every Neighbor + * Discovery and Router Advertisement frame as "not for us". */ +static inline int eth_is_ipv6_multicast_mac(const uint8_t *mac) +{ + return ((mac[0] == 0x33) && (mac[1] == 0x33)) ? 1 : 0; +} + +/* ---------------------------------------------------------------------- */ +/* Header field accessors */ +/* ---------------------------------------------------------------------- */ + +static inline uint8_t ip6_hdr_version(const struct wolfIP_ip6_packet *pkt) +{ + return (uint8_t)((ee32(pkt->ver_tc_fl) >> 28) & 0x0Fu); +} + +static inline uint8_t ip6_hdr_traffic_class(const struct wolfIP_ip6_packet *pkt) +{ + return (uint8_t)((ee32(pkt->ver_tc_fl) >> 20) & 0xFFu); +} + +static inline uint32_t ip6_hdr_flow_label(const struct wolfIP_ip6_packet *pkt) +{ + return ee32(pkt->ver_tc_fl) & 0xFFFFFu; +} + +static inline void ip6_hdr_set_vtf(struct wolfIP_ip6_packet *pkt, + uint8_t traffic_class, uint32_t flow_label) +{ + uint32_t v = ((uint32_t)IP6_VERSION << 28) | + ((uint32_t)traffic_class << 20) | + (flow_label & 0xFFFFFu); + + pkt->ver_tc_fl = ee32(v); +} + +/* The address fields are plain byte arrays on the wire, so they are copied + * in and out rather than aliased: the header sits at an odd offset behind + * the 14-byte Ethernet header, and wolfIP runs on strict-alignment targets. */ +static inline void ip6_hdr_get_src(const struct wolfIP_ip6_packet *pkt, ip6 *out) +{ + int i; + + for (i = 0; i < 16; i++) + out->addr[i] = pkt->src[i]; +} + +static inline void ip6_hdr_get_dst(const struct wolfIP_ip6_packet *pkt, ip6 *out) +{ + int i; + + for (i = 0; i < 16; i++) + out->addr[i] = pkt->dst[i]; +} + +static inline void ip6_hdr_set_src(struct wolfIP_ip6_packet *pkt, const ip6 *a) +{ + int i; + + for (i = 0; i < 16; i++) + pkt->src[i] = a->addr[i]; +} + +static inline void ip6_hdr_set_dst(struct wolfIP_ip6_packet *pkt, const ip6 *a) +{ + int i; + + for (i = 0; i < 16; i++) + pkt->dst[i] = a->addr[i]; +} + +/* ---------------------------------------------------------------------- */ +/* Checksums */ +/* ---------------------------------------------------------------------- */ + +/* Fill the pseudo-header for an upper-layer checksum. upper_len is the + * length of the upper-layer header plus its payload. */ +static void transport6_pseudo_header_init(union transport6_pseudo_header *ph, + const ip6 *src, const ip6 *dst, + uint32_t upper_len, uint8_t next_hdr) +{ + int i; + + for (i = 0; i < 16; i++) { + ph->ph.src[i] = src->addr[i]; + ph->ph.dst[i] = dst->addr[i]; + } + ph->ph.len = ee32(upper_len); + ph->ph.zero[0] = 0; + ph->ph.zero[1] = 0; + ph->ph.zero[2] = 0; + ph->ph.proto = next_hdr; +} + +/* One's complement sum over the 40-byte pseudo-header followed by the + * upper-layer data, per RFC 1071. The data length is taken from the + * pseudo-header, which the caller has already validated against the frame. */ +static uint16_t transport6_checksum(union transport6_pseudo_header *ph, + const void *_data) +{ + uint32_t sum = 0; + uint32_t i; + const uint8_t *ptr = (const uint8_t *)ph->buf; + const uint8_t *data = (const uint8_t *)_data; + uint32_t len = ee32(ph->ph.len); + uint16_t word; + + for (i = 0; i < 40u; i += 2) { + memcpy(&word, ptr + i, sizeof(word)); + sum += ee16(word); + } + for (i = 0; i < (len & ~1u); i += 2) { + memcpy(&word, data + i, sizeof(word)); + sum += ee16(word); + } + if ((len & 0x01u) != 0) { + uint16_t spare = (uint16_t)((uint16_t)data[len - 1] << 8); + + sum += spare; + } + while ((sum >> 16) != 0) + sum = (sum & 0xffffu) + (sum >> 16); + return (uint16_t)(~sum); +} + +static int transport6_verify_checksum(union transport6_pseudo_header *ph, + const void *data) +{ + return (transport6_checksum(ph, data) == 0) ? 0 : -1; +} + +/* ---------------------------------------------------------------------- */ +/* Receive path */ +/* ---------------------------------------------------------------------- */ + +/* Is this Next Header value an extension header we deliberately refuse? + * + * Phase 0 parses the upper-layer header only. Walking a Next Header chain is + * a well known denial-of-service surface (nested and looping option headers), + * so rather than half-implement it these are recognised and dropped with a + * reason of their own. */ +static int ip6_nexthdr_is_extension(uint8_t next_hdr) +{ + switch (next_hdr) { + case IP6_NEXTHDR_HOPOPT: + case IP6_NEXTHDR_ROUTING: + case IP6_NEXTHDR_FRAGMENT: + case IP6_NEXTHDR_ESP: + case IP6_NEXTHDR_AH: + case IP6_NEXTHDR_DSTOPTS: + case IP6_NEXTHDR_NONE: + return 1; + default: + return 0; + } +} + +/* Smallest upper-layer header for a Next Header we accept, or 0 if we do not + * recognise it as an upper-layer protocol at all. */ +static uint32_t ip6_upper_min_len(uint8_t next_hdr) +{ + switch (next_hdr) { + case IP6_NEXTHDR_TCP: + return IP6_MIN_TCP_LEN; + case IP6_NEXTHDR_UDP: + return IP6_MIN_UDP_LEN; + case IP6_NEXTHDR_ICMPV6: + return IP6_MIN_ICMPV6_LEN; + default: + return 0; + } +} + +static void icmp6_input(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t len); + +/* Validate an inbound IPv6 packet. + * + * Returns IP6_ACCEPTED when the header is well formed and carries an + * upper-layer protocol we handle, otherwise a negative ip6_recv_result + * saying why it was rejected. + * + * `len` is the whole frame length including the Ethernet header. Note the + * frame may legitimately be *longer* than the header plus payload_len: + * Ethernet pads anything under 60 bytes, so the length check is "at least", + * never "exactly". + * + * Deliberately NOT checked here: the hop limit. RFC 8200 section 3 has the + * hop limit decremented and tested by forwarding nodes only; a destination + * host must accept a packet addressed to it even with a hop limit of zero. + * Dropping such packets is a common bug, so there is a test asserting the + * RFC behaviour. + */ +static int ip6_recv(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t len) +{ + uint32_t payload_len; + uint32_t min_upper; + ip6 src; + ip6 dst; + + (void)s; + (void)if_idx; + + if (len < (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN)) + return IP6_DROP_SHORT_FRAME; + if (ip6_hdr_version(pkt) != IP6_VERSION) + return IP6_DROP_BAD_VERSION; + + payload_len = ee16(pkt->payload_len); + /* Ethernet padding means the frame can be longer, never shorter. */ + if (len < ((uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + payload_len)) + return IP6_DROP_TRUNCATED_PAYLOAD; + + ip6_hdr_get_src(pkt, &src); + ip6_hdr_get_dst(pkt, &dst); + + /* RFC 4291 section 2.7: a multicast address is never a valid source. */ + if (ip6_is_multicast(&src)) + return IP6_DROP_MCAST_SOURCE; + /* RFC 4291 section 2.5.2: the unspecified address must never be a + * destination. It is legitimate as a source, during duplicate address + * detection, so only the destination is rejected here. */ + if (ip6_is_unspecified(&dst)) + return IP6_DROP_UNSPECIFIED_DESTINATION; + /* RFC 4291 section 2.5.3: ::1 must never appear on a real link. */ + if (ip6_is_loopback(&src) || ip6_is_loopback(&dst)) + return IP6_DROP_LOOPBACK_ON_WIRE; + /* RFC 4291 section 2.5.5.2: IPv4-mapped addresses exist only inside the + * socket API. Seeing one on the wire means either a broken peer or an + * attempt to smuggle an IPv4 identity through the IPv6 path, which + * matters here because wolfIP presents v4-mapped addresses to + * dual-stack AF_INET6 sockets. */ + if (ip6_is_v4mapped(&src) || ip6_is_v4mapped(&dst)) + return IP6_DROP_V4MAPPED_ON_WIRE; + /* RFC 4291 section 2.5.5.1: IPv4-compatible addresses are deprecated. */ + if (ip6_is_v4compat(&src) || ip6_is_v4compat(&dst)) + return IP6_DROP_V4COMPAT_ON_WIRE; + + if (ip6_nexthdr_is_extension(pkt->next_hdr)) + return IP6_DROP_EXTENSION_HEADER; + + min_upper = ip6_upper_min_len(pkt->next_hdr); + if (min_upper == 0) + return IP6_DROP_UNKNOWN_NEXTHDR; + if (payload_len < min_upper) + return IP6_DROP_SHORT_TRANSPORT; + + /* ICMPv6 is handled in the stack itself and needs no socket. Everything + * else waits for the socket phase. The header is fully validated at this + * point and the payload bounds are known good. */ + if (pkt->next_hdr == IP6_NEXTHDR_ICMPV6) + icmp6_input(s, if_idx, pkt, len); + return IP6_ACCEPTED; +} + +/* Verify the upper-layer checksum of a validated IPv6 packet. Split out from + * ip6_recv() so the transport demux can call it once it exists, and so it can + * be tested against known-good vectors on its own. */ +static inline int ip6_verify_transport_checksum(const struct wolfIP_ip6_packet *pkt) +{ + union transport6_pseudo_header ph; + ip6 src; + ip6 dst; + uint32_t payload_len = ee16(pkt->payload_len); + + ip6_hdr_get_src(pkt, &src); + ip6_hdr_get_dst(pkt, &dst); + transport6_pseudo_header_init(&ph, &src, &dst, payload_len, pkt->next_hdr); + return transport6_verify_checksum(&ph, pkt->data); +} + +/* ---------------------------------------------------------------------- */ +/* Transmit path */ +/* ---------------------------------------------------------------------- */ + +/* Build an IPv6 header in front of an already-assembled upper-layer payload + * and compute its checksum. The sibling of ip_output_add_header(). + * + * `payload_len` is the upper-layer length: the transport header plus its + * data, excluding the 40-byte IPv6 header. Unlike IPv4 there is no header + * checksum to compute, and the flow label is left at zero (RFC 8200 section + * 6 permits this for a source that does not use flow labelling). + * + * `nexthop_mac` may be NULL, in which case no Ethernet header is added and + * the caller is responsible for the link layer - the raw-IP (non-Ethernet) + * ports need that. + */ +static inline int ip6_output_add_header(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, + const ip6 *src, const ip6 *dst, + uint8_t next_hdr, uint16_t payload_len, + uint8_t hop_limit, + const uint8_t *nexthop_mac) +{ + union transport6_pseudo_header ph; + + if ((pkt == NULL) || (src == NULL) || (dst == NULL)) + return -WOLFIP_EINVAL; + + ip6_hdr_set_vtf(pkt, 0, 0); + pkt->payload_len = ee16(payload_len); + pkt->next_hdr = next_hdr; + pkt->hop_limit = (hop_limit != 0) ? hop_limit : IP6_HOP_LIMIT_DEFAULT; + ip6_hdr_set_src(pkt, src); + ip6_hdr_set_dst(pkt, dst); + + transport6_pseudo_header_init(&ph, src, dst, payload_len, next_hdr); + if (next_hdr == IP6_NEXTHDR_TCP) { + struct wolfIP_tcp6_seg *tcp = (struct wolfIP_tcp6_seg *)pkt; + + tcp->csum = 0; + tcp->csum = ee16(transport6_checksum(&ph, &tcp->src_port)); + } else if (next_hdr == IP6_NEXTHDR_UDP) { + struct wolfIP_udp6_datagram *udp = (struct wolfIP_udp6_datagram *)pkt; + + udp->csum = 0; + /* RFC 8200 section 8.1: unlike IPv4, a zero UDP checksum is not + * permitted over IPv6, so a computed zero is transmitted as 0xFFFF. */ + udp->csum = ee16(transport6_checksum(&ph, &udp->src_port)); + if (udp->csum == 0) + udp->csum = 0xFFFFu; + } else if (next_hdr == IP6_NEXTHDR_ICMPV6) { + struct wolfIP_icmp6_packet *icmp6 = (struct wolfIP_icmp6_packet *)pkt; + + /* ICMPv6 checksums cover the pseudo-header, which is the notable + * difference from ICMPv4. */ + icmp6->csum = 0; + icmp6->csum = ee16(transport6_checksum(&ph, &icmp6->type)); + } + +#ifdef ETHERNET + if ((nexthop_mac != NULL) && !wolfIP_ll_is_non_ethernet(s, if_idx)) { + eth_output_add_header(s, if_idx, nexthop_mac, + (struct wolfIP_eth_frame *)pkt, ETH_TYPE_IPV6); + } +#else + (void)s; + (void)if_idx; + (void)nexthop_mac; +#endif + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* ICMPv6 */ +/* ---------------------------------------------------------------------- */ + +/* IPv6 counterpart of wolfIP_if_for_local_ip(): which interface holds this + * address, and is it one of ours at all? Same shape as the IPv4 helper, and + * like it this searches every interface for global addresses (the weak + * end-system model), while link-local addresses remain scoped to the ingress + * interface as required by RFC 4007. */ +static unsigned int wolfIP_if_for_local_ip6(struct wolfIP *s, + unsigned int ingress_if, + const ip6 *addr, int *found) +{ + struct wolfIP_ifaddr_info info; + unsigned int i; + + if (found) + *found = 0; + if (!s || !addr) + return 0; + for (i = 0; i < WOLFIP_MAX_INTERFACES; i++) { + unsigned int count; + unsigned int j; + + /* Link-local addresses are scoped to one link. The same address may + * legitimately exist on another interface, but it is not local to + * the link on which this packet arrived (RFC 4007 section 5). */ + if (ip6_is_link_local(addr) && (i != ingress_if)) + continue; + count = wolfIP_ifaddr_count(s, i, AF_INET6); + + for (j = 0; j < count; j++) { + if (wolfIP_ifaddr_get(s, i, AF_INET6, j, &info) != 0) + continue; + /* RFC 4862 section 5.4.5: tentative addresses are not assigned + * yet and are usable only by Duplicate Address Detection. */ + if ((info.state != WOLFIP_IFADDR_TENTATIVE) && + (ip6_cmp(&info.v6, addr) == 0)) { + if (found) + *found = 1; + return i; + } + } + } + return 0; +} + +static void nd6_input(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t payload_len); + +/* ICMPv6 receive path. Deliberately laid out like icmp_input() above it: + * length checks, then the checksum, then one arm per message type, with the + * echo reply built in place over the request. + * + * Only Echo is handled so far. That is useful on its own because it needs + * neither sockets nor Neighbor Discovery - the reply goes back to the source + * MAC of the request, exactly as the IPv4 path does - and it gives + * ip6_output_add_header() its first production caller. + * + * Not handled yet, each with its requirement test already written in + * unit_tests_ipv6_pending.c: the error messages of RFC 4443 sections 3.1 to + * 3.4, and Echo Requests addressed to a multicast group, which need a + * unicast source chosen per RFC 4443 section 4.2. + * + * Unlike icmp_input() there is no wolfIP_filter_notify_icmp() call: the + * packet filter has no IPv6 hooks yet. It belongs here when it grows them. + */ +static void icmp6_input(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t len) +{ + struct wolfIP_icmp6_packet *icmp = (struct wolfIP_icmp6_packet *)pkt; + uint32_t payload_len = ee16(pkt->payload_len); + uint8_t peer_mac[6]; + ip6 src; + ip6 dst; + + /* validate minimum ICMPv6 packet length */ + if (len < sizeof(struct wolfIP_icmp6_packet)) + return; + /* validate payload_len covers at least the ICMPv6 header */ + if (payload_len < IP6_MIN_ICMPV6_LEN) + return; + /* validate payload_len doesn't exceed actual received data */ + if (len < ((uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + payload_len)) + return; + /* validate ICMPv6 checksum before processing (RFC 4443 section 2.3). + * Unlike ICMPv4 this covers the IPv6 pseudo-header. */ + if (ip6_verify_transport_checksum(pkt) != 0) + return; + + ip6_hdr_get_src(pkt, &src); + ip6_hdr_get_dst(pkt, &dst); + + /* Neighbor Discovery is ICMPv6 but has its own validation rules, most + * importantly the hop limit of 255 that confines it to the local link. */ + if ((icmp->type >= ICMP6_ROUTER_SOLICIT) && + (icmp->type <= ICMP6_REDIRECT)) { + nd6_input(s, if_idx, pkt, payload_len); + return; + } + if (icmp->type == ICMP6_ECHO_REPLY) { + /* icmp_input() hands this to icmp_try_recv() for delivery to an + * application ICMP socket. There is no IPv6 socket yet, so the reply + * is consumed here; answering it would loop between two hosts. */ + return; + } + if (icmp->type == ICMP6_ECHO_REQUEST) { + int dst_match = 0; + + /* An Echo needs identifier and sequence as well as the header. */ + if (payload_len < ICMP6_ECHO_MIN_LEN) + return; + /* RFC 4443 section 4.1 assigns only Code 0 to Echo Request. */ + if (icmp->code != 0) + return; + /* Nowhere to send a reply, and :: as a source is reserved for + * duplicate address detection (RFC 4862 section 5.4.2). */ + if (ip6_is_unspecified(&src)) + return; + /* Same guard as the ICMPv4 arm, for the same reason: only reply to + * requests destined to one of our own addresses. Without it an + * L2-adjacent attacker can address a frame to our MAC with an + * arbitrary destination and have us emit a reply with a source of + * their choosing. This also declines multicast destinations, which + * need a unicast source selected explicitly. */ + (void)wolfIP_if_for_local_ip6(s, if_idx, &dst, &dst_match); + if (!dst_match) + return; + + /* The Ethernet header is about to be rewritten, so keep the + * requester's address first. */ + memcpy(peer_mac, pkt->eth.src, 6); + + /* Reply in place, as icmp_input() does: the identifier, sequence + * number and payload are already where they belong and are left + * untouched. Source and destination swap, so the reply comes from + * the address that was pinged. */ + icmp->type = ICMP6_ECHO_REPLY; + icmp->code = 0; + ip6_output_add_header(s, if_idx, pkt, &dst, &src, IP6_NEXTHDR_ICMPV6, + (uint16_t)payload_len, IP6_HOP_LIMIT_DEFAULT, + peer_mac); + + /* Send exactly the packet, never any Ethernet padding that arrived + * with the request. */ + wolfIP_ll_send_frame(s, if_idx, pkt, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + + payload_len); + } +} + +/* ---------------------------------------------------------------------- */ +/* Neighbor Discovery (RFC 4861) and DAD (RFC 4862 section 5.4) */ +/* ---------------------------------------------------------------------- */ + +/* Option types (RFC 4861 section 4.6). */ +#define ND6_OPT_SLLA 1 +#define ND6_OPT_TLLA 2 +#define ND6_OPT_PREFIX 3 +#define ND6_OPT_MTU 5 + +/* Neighbor Advertisement flags (RFC 4861 section 4.4). */ +#define ND6_NA_ROUTER 0x80 +#define ND6_NA_SOLICITED 0x40 +#define ND6_NA_OVERRIDE 0x20 + +/* Prefix Information flags (RFC 4861 section 4.6.2). */ +#define ND6_PREFIX_ONLINK 0x80 /* L */ +#define ND6_PREFIX_AUTO 0x40 /* A */ + +/* Protocol constants (RFC 4861 section 10, RFC 4862 section 5.1). Values are + * the defaults; a Router Advertisement may override the reachable and + * retransmit timers, which is not implemented. */ +#define ND6_RETRANS_TIMER_MS 1000U +#define ND6_REACHABLE_TIME_MS 30000U +#define ND6_DELAY_FIRST_PROBE_MS 5000U +#define ND6_MAX_MULTICAST_SOLICIT 3U +#define ND6_MAX_UNICAST_SOLICIT 3U +#define ND6_MAX_RTR_SOLICITATIONS 3U +#define ND6_RTR_SOLICITATION_INTERVAL_MS 4000U +#define ND6_DUP_ADDR_DETECT_TRANSMITS 1U + +/* Every Neighbor Discovery message must arrive with a hop limit of exactly + * 255 (RFC 4861 sections 6.1.1, 6.1.2, 7.1.1 and 7.1.2). A router cannot + * forward a packet and leave the hop limit at 255, so this single check is + * what confines Neighbor Discovery to the local link. It is the most + * important validation in this file. */ +#define ND6_HOP_LIMIT 255 + +/* One periodic tick drives duplicate address detection, router solicitation + * retries and every cache expiry. One timer for the whole subsystem rather + * than one per address or per neighbour, which keeps the shared timer heap + * small and makes the behaviour easy to drive from a test. */ +#define ND6_TICK_MS 100U + +/* Neighbor Solicitation and Advertisement share a layout up to the target + * address; the byte at `flags` is reserved (and must be zero) in a + * solicitation. */ +struct PACKED nd6_msg { + struct wolfIP_ip6_packet ip6; + uint8_t type, code; + uint16_t csum; + uint8_t flags; + uint8_t reserved[3]; + uint8_t target[16]; + uint8_t options[0]; +}; + +struct PACKED nd6_rs_msg { + struct wolfIP_ip6_packet ip6; + uint8_t type, code; + uint16_t csum; + uint32_t reserved; + uint8_t options[0]; +}; + +struct PACKED nd6_ra_msg { + struct wolfIP_ip6_packet ip6; + uint8_t type, code; + uint16_t csum; + uint8_t cur_hop_limit; + uint8_t flags; + uint16_t router_lifetime; + uint32_t reachable_time; + uint32_t retrans_timer; + uint8_t options[0]; +}; + +struct PACKED nd6_opt_lla { + uint8_t type, len; + uint8_t mac[6]; +}; + +struct PACKED nd6_opt_prefix { + uint8_t type, len, prefix_len, flags; + uint32_t valid_lifetime; + uint32_t preferred_lifetime; + uint32_t reserved; + uint8_t prefix[16]; +}; + +static void nd6_tick_cb(void *arg); +static void nd6_arm_tick(struct wolfIP *s); +static int nd6_has_work(struct wolfIP *s); + +/* ---------------------------------------------------------------------- */ +/* Option parsing */ +/* ---------------------------------------------------------------------- */ + +/* Walk the option area, returning the first option of `want`, or NULL. + * + * Option lengths are in units of 8 octets and a length of zero is invalid + * (RFC 4861 section 4.6). Accepting zero makes this loop run forever on a + * frame an attacker controls, so it is rejected explicitly - the same class + * of bug as an extension-header chain with no progress. */ +static const uint8_t *nd6_find_option(const uint8_t *opts, uint32_t len, + uint8_t want) +{ + uint32_t off = 0; + + while ((off + 2u) <= len) { + uint8_t type = opts[off]; + uint32_t olen = (uint32_t)opts[off + 1] * 8u; + + if (olen == 0) + return NULL; /* malformed: no forward progress */ + if ((off + olen) > len) + return NULL; /* runs past the end of the message */ + if (type == want) + return &opts[off]; + off += olen; + } + return NULL; +} + +/* Validate the framing of the entire option area before acting on any one + * option. RFC 4861 section 4.6 requires an ND packet containing a zero-length + * option to be discarded. Doing this as a separate first pass also prevents + * an RA from installing a router or prefix before a malformed later option + * is discovered. */ +static int nd6_options_valid(const uint8_t *opts, uint32_t len) +{ + uint32_t off = 0; + + while (off < len) { + uint32_t olen; + + if ((len - off) < 2u) + return 0; + olen = (uint32_t)opts[off + 1] * 8u; + if ((olen == 0) || (olen > (len - off))) + return 0; + off += olen; + } + return 1; +} + +/* ---------------------------------------------------------------------- */ +/* Neighbour cache - the IPv6 counterpart of the arp_* helpers */ +/* ---------------------------------------------------------------------- */ + +/* Linear scan by {address, interface}, expiring stale entries lazily the way + * arp_neighbor_index() does. Returns the slot index or -1. */ +static int nd6_neighbor_index(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr) +{ + unsigned int i; + + for (i = 0; i < WOLFIP_ND6_CACHE_SIZE; i++) { + struct nd6_neighbor *n = &s->nd6.neighbors[i]; + + if (n->state == 0) + continue; + if (n->if_idx != (uint8_t)if_idx) + continue; + if (ip6_cmp(&n->addr, addr) != 0) + continue; + return (int)i; + } + return -1; +} + +/* Insert or refresh an entry. `mac` may be NULL to create an INCOMPLETE + * entry for an address whose link-layer address is still unknown. + * + * Unlike arp_store_neighbor(), which silently refuses when the table is + * full, this evicts the oldest entry. A full table that cannot be reclaimed + * means one burst of scan traffic locks out every real neighbour. */ +static int nd6_store_neighbor(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr, const uint8_t *mac, + uint8_t state, int is_router) +{ + struct nd6_neighbor *n; + int idx = nd6_neighbor_index(s, if_idx, addr); + unsigned int i; + + if (idx < 0) { + int oldest = 0; + + for (i = 0; i < WOLFIP_ND6_CACHE_SIZE; i++) { + if (s->nd6.neighbors[i].state == 0) { + idx = (int)i; + break; + } + if (s->nd6.neighbors[i].ts < s->nd6.neighbors[oldest].ts) + oldest = (int)i; + } + if (idx < 0) + idx = oldest; + memset(&s->nd6.neighbors[idx], 0, sizeof(struct nd6_neighbor)); + ip6_copy(&s->nd6.neighbors[idx].addr, addr); + s->nd6.neighbors[idx].if_idx = (uint8_t)if_idx; + } + n = &s->nd6.neighbors[idx]; + if (mac != NULL) + memcpy(n->mac, mac, 6); + n->state = state; + n->probes = 0; + n->ts = s->last_tick; + if (is_router) + n->is_router = 1; + return idx; +} + +/* Resolve an address to a link-layer address. Returns 0 and fills `mac` when + * the entry is usable, negative otherwise. Counterpart of arp_lookup(). */ +static int nd6_lookup(struct wolfIP *s, unsigned int if_idx, const ip6 *addr, + uint8_t *mac) +{ + int idx; + + /* Multicast needs no resolution: the mapping is algorithmic. */ + if (ip6_is_multicast(addr)) { + ip6_mcast_to_eth(addr, mac); + return 0; + } + idx = nd6_neighbor_index(s, if_idx, addr); + if (idx < 0) + return -1; + if (s->nd6.neighbors[idx].state == ND6_INCOMPLETE) + return -1; + memcpy(mac, s->nd6.neighbors[idx].mac, 6); + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Transmit */ +/* ---------------------------------------------------------------------- */ + +/* Send a Neighbor Solicitation for `target`. + * + * There is no rate limit here, unlike arp_request()'s one per second per + * interface. The only caller is duplicate address detection, which is + * already paced by dad_due. When address resolution is driven from the + * transmit path it will be able to ask for the same neighbour repeatedly, + * and will need a throttle at that point. + * + * `src` is the source address: a real address of ours for ordinary address + * resolution, or the unspecified address during duplicate address detection. + * RFC 4861 section 4.3 forbids the Source Link-Layer Address option when the + * source is unspecified, which is exactly the DAD case - there is no address + * to advertise yet. */ +static void nd6_send_ns(struct wolfIP *s, unsigned int if_idx, + const ip6 *target, const ip6 *src) +{ + uint8_t frame[LINK_MTU]; + struct nd6_msg *ns = (struct nd6_msg *)frame; + struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); + struct nd6_opt_lla *opt; + ip6 dst; + uint8_t mac[6]; + uint16_t payload_len = 24; /* type,code,csum,reserved + target */ + int with_slla = !ip6_is_unspecified(src); + + if (!ll) + return; + memset(frame, 0, ETH_HEADER_LEN + IP6_HEADER_LEN + 32); + ns->type = ICMP6_NEIGHBOR_SOLICIT; + ns->code = 0; + memcpy(ns->target, target->addr, 16); + if (with_slla) { + opt = (struct nd6_opt_lla *)ns->options; + opt->type = ND6_OPT_SLLA; + opt->len = 1; /* 8 octets */ + memcpy(opt->mac, ll->mac, 6); + payload_len = (uint16_t)(payload_len + 8u); + } + + /* Solicitations go to the target's solicited-node group, so only the + * handful of nodes sharing its low 24 bits are interrupted. */ + ip6_set_solicited_node(&dst, target); + ip6_mcast_to_eth(&dst, mac); + ip6_output_add_header(s, if_idx, &ns->ip6, src, &dst, IP6_NEXTHDR_ICMPV6, + payload_len, ND6_HOP_LIMIT, mac); + wolfIP_ll_send_frame(s, if_idx, frame, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + + payload_len); +} + +/* Send a Neighbor Advertisement for `target` to `dst`/`dst_mac`. */ +static void nd6_send_na(struct wolfIP *s, unsigned int if_idx, + const ip6 *target, const ip6 *src, const ip6 *dst, + const uint8_t *dst_mac, uint8_t flags) +{ + uint8_t frame[LINK_MTU]; + struct nd6_msg *na = (struct nd6_msg *)frame; + struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); + struct nd6_opt_lla *opt; + uint16_t payload_len = 24 + 8; + + if (!ll) + return; + memset(frame, 0, ETH_HEADER_LEN + IP6_HEADER_LEN + 32); + na->type = ICMP6_NEIGHBOR_ADVERT; + na->code = 0; + na->flags = flags; + memcpy(na->target, target->addr, 16); + /* The Target Link-Layer Address option is what actually answers the + * question the solicitation asked. */ + opt = (struct nd6_opt_lla *)na->options; + opt->type = ND6_OPT_TLLA; + opt->len = 1; + memcpy(opt->mac, ll->mac, 6); + + ip6_output_add_header(s, if_idx, &na->ip6, src, dst, IP6_NEXTHDR_ICMPV6, + payload_len, ND6_HOP_LIMIT, dst_mac); + wolfIP_ll_send_frame(s, if_idx, frame, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + + payload_len); +} + +/* Send a Router Solicitation to the all-routers group (RFC 4861 s6.3.7). */ +static void nd6_send_rs(struct wolfIP *s, unsigned int if_idx) +{ + uint8_t frame[LINK_MTU]; + struct nd6_rs_msg *rs = (struct nd6_rs_msg *)frame; + struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); + struct nd6_opt_lla *opt; + struct wolfIP_ifaddr_info info; + ip6 src; + ip6 dst; + uint8_t mac[6]; + uint16_t payload_len = 8; + unsigned int count; + unsigned int i; + int have_src = 0; + + if (!ll) + return; + /* Prefer a link-local source; until DAD completes there may be none, in + * which case the unspecified address is used and the Source Link-Layer + * Address option must be omitted (RFC 4861 section 4.1). */ + ip6_set_unspecified(&src); + count = wolfIP_ifaddr_count(s, if_idx, AF_INET6); + for (i = 0; i < count; i++) { + if (wolfIP_ifaddr_get(s, if_idx, AF_INET6, i, &info) != 0) + continue; + if ((info.state == WOLFIP_IFADDR_PREFERRED) && + ip6_is_link_local(&info.v6)) { + ip6_copy(&src, &info.v6); + have_src = 1; + break; + } + } + + memset(frame, 0, ETH_HEADER_LEN + IP6_HEADER_LEN + 16); + rs->type = ICMP6_ROUTER_SOLICIT; + rs->code = 0; + if (have_src) { + opt = (struct nd6_opt_lla *)rs->options; + opt->type = ND6_OPT_SLLA; + opt->len = 1; + memcpy(opt->mac, ll->mac, 6); + payload_len = (uint16_t)(payload_len + 8u); + } + + ip6_set_all_routers(&dst); + ip6_mcast_to_eth(&dst, mac); + ip6_output_add_header(s, if_idx, &rs->ip6, &src, &dst, IP6_NEXTHDR_ICMPV6, + payload_len, ND6_HOP_LIMIT, mac); + wolfIP_ll_send_frame(s, if_idx, frame, + (uint32_t)(ETH_HEADER_LEN + IP6_HEADER_LEN) + + payload_len); +} + +/* ---------------------------------------------------------------------- */ +/* Duplicate address detection (RFC 4862 section 5.4) */ +/* ---------------------------------------------------------------------- */ + +/* Find the address slot holding `addr` on `if_idx`, or NULL. */ +static struct wolfIP_ifaddr_slot *nd6_slot_for(struct wolfIP *s, + unsigned int if_idx, + const ip6 *addr) +{ + unsigned int i; + + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + struct wolfIP_ifaddr_slot *slot = &s->ifaddr[i]; + + if (!slot->used) + continue; + if (slot->info.family != AF_INET6) + continue; + if (slot->info.if_idx != (uint8_t)if_idx) + continue; + if (ip6_cmp(&slot->info.v6, addr) == 0) + return slot; + } + return NULL; +} + +/* Neighbor Discovery is link-scoped even for globally routable addresses. + * A unicast destination is actionable only when it is assigned on the + * ingress interface and has completed DAD. Unsolicited RA/NA messages may + * instead use the all-nodes multicast group. */ +static int nd6_destination_is_local(struct wolfIP *s, unsigned int if_idx, + const ip6 *dst) +{ + struct wolfIP_ifaddr_slot *slot; + + if (ip6_is_all_nodes(dst)) + return 1; + slot = nd6_slot_for(s, if_idx, dst); + return ((slot != NULL) && + (slot->info.state != WOLFIP_IFADDR_TENTATIVE)) ? 1 : 0; +} + +/* Abandon a tentative address that turned out to be a duplicate. + * + * RFC 4862 section 5.4.5: the address must not be assigned. If it was the + * link-local address the interface has no usable IPv6 configuration at all, + * which is reported by leaving the interface with no link-local address + * rather than by retrying with a different identifier. */ +static void nd6_dad_failed(struct wolfIP *s, struct wolfIP_ifaddr_slot *slot) +{ + (void)s; + slot->info.state = WOLFIP_IFADDR_DEPRECATED; + slot->used = 0; +} + +/* Start duplicate address detection on a tentative address. */ +static void nd6_dad_start(struct wolfIP *s, struct wolfIP_ifaddr_slot *slot) +{ + slot->info.state = WOLFIP_IFADDR_TENTATIVE; + slot->dad_probes = ND6_DUP_ADDR_DETECT_TRANSMITS; + /* Send the first solicitation on the next tick, so that a caller adding + * an address mid-poll does not transmit from inside its own call. */ + slot->dad_due = s->last_tick; +} + +/* ---------------------------------------------------------------------- */ +/* Prefix and router lists */ +/* ---------------------------------------------------------------------- */ + +static void nd6_prefix_store(struct wolfIP *s, unsigned int if_idx, + const ip6 *prefix, uint8_t prefix_len, + uint8_t onlink, uint8_t autonomous, + uint32_t valid, uint32_t preferred) +{ + unsigned int i; + int free_slot = -1; + + for (i = 0; i < WOLFIP_ND6_PREFIX_MAX; i++) { + struct nd6_prefix *p = &s->nd6.prefixes[i]; + + if (!p->used) { + if (free_slot < 0) + free_slot = (int)i; + continue; + } + if ((p->if_idx == (uint8_t)if_idx) && (p->prefix_len == prefix_len) && + (ip6_prefix_cmp(&p->prefix, prefix, prefix_len) == 0)) { + /* RFC 4861 section 6.3.4: zero invalidates an on-link prefix + * immediately, rather than at the next timer tick. */ + if (valid == 0) { + p->used = 0; + return; + } + p->onlink = onlink; + p->autonomous = autonomous; + p->valid_lifetime = valid; + p->preferred_lifetime = preferred; + p->ts = s->last_tick; + return; + } + } + if (valid == 0) + return; + if (free_slot < 0) + return; /* table full: the advertisement is ignored, never truncated */ + { + struct nd6_prefix *p = &s->nd6.prefixes[free_slot]; + + memset(p, 0, sizeof(*p)); + ip6_copy(&p->prefix, prefix); + p->prefix_len = prefix_len; + p->if_idx = (uint8_t)if_idx; + p->used = 1; + p->onlink = onlink; + p->autonomous = autonomous; + p->valid_lifetime = valid; + p->preferred_lifetime = preferred; + p->ts = s->last_tick; + } +} + +static void nd6_router_store(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr, uint16_t lifetime) +{ + unsigned int i; + int free_slot = -1; + + for (i = 0; i < WOLFIP_ND6_ROUTER_MAX; i++) { + struct nd6_router *r = &s->nd6.routers[i]; + + if (!r->used) { + if (free_slot < 0) + free_slot = (int)i; + continue; + } + if ((r->if_idx == (uint8_t)if_idx) && (ip6_cmp(&r->addr, addr) == 0)) { + /* RFC 4861 section 6.3.4: a lifetime of zero means the sender is + * no longer a default router. */ + if (lifetime == 0) { + r->used = 0; + return; + } + r->lifetime = lifetime; + r->ts = s->last_tick; + return; + } + } + if ((lifetime == 0) || (free_slot < 0)) + return; + { + struct nd6_router *r = &s->nd6.routers[free_slot]; + + memset(r, 0, sizeof(*r)); + ip6_copy(&r->addr, addr); + r->if_idx = (uint8_t)if_idx; + r->used = 1; + r->lifetime = lifetime; + r->ts = s->last_tick; + } +} + +/* Is this destination on-link, according to the prefix list? */ +static int nd6_is_onlink(struct wolfIP *s, unsigned int if_idx, + const ip6 *dst) +{ + unsigned int i; + + if (ip6_is_link_local(dst) || ip6_is_multicast(dst)) + return 1; + for (i = 0; i < WOLFIP_ND6_PREFIX_MAX; i++) { + struct nd6_prefix *p = &s->nd6.prefixes[i]; + + if (!p->used || !p->onlink) + continue; + if (p->if_idx != (uint8_t)if_idx) + continue; + if (ip6_prefix_cmp(&p->prefix, dst, p->prefix_len) == 0) + return 1; + } + return 0; +} + +/* Pick the next hop for a destination: the destination itself when it is + * on-link, otherwise a default router. The IPv6 counterpart of + * wolfIP_select_nexthop_ex(), driven by the prefix and router lists rather + * than by a configured netmask and gateway. Returns 0 on success. */ +static int nd6_select_nexthop(struct wolfIP *s, unsigned int if_idx, + const ip6 *dst, ip6 *nexthop) +{ + unsigned int i; + + if (nd6_is_onlink(s, if_idx, dst)) { + ip6_copy(nexthop, dst); + return 0; + } + for (i = 0; i < WOLFIP_ND6_ROUTER_MAX; i++) { + struct nd6_router *r = &s->nd6.routers[i]; + + if (!r->used || (r->if_idx != (uint8_t)if_idx)) + continue; + ip6_copy(nexthop, &r->addr); + return 0; + } + return -1; /* no route */ +} + +/* ---------------------------------------------------------------------- */ +/* Receive */ +/* ---------------------------------------------------------------------- */ + +/* Neighbor Solicitation (RFC 4861 section 7.1.1 and 7.2.3). */ +static void nd6_recv_ns(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t payload_len) +{ + struct nd6_msg *ns = (struct nd6_msg *)pkt; + const uint8_t *opt; + struct wolfIP_ifaddr_slot *slot; + ip6 target; + ip6 src; + ip6 dst; + ip6 reply_dst; + uint8_t reply_mac[6]; + uint8_t flags = ND6_NA_SOLICITED | ND6_NA_OVERRIDE; + + if (payload_len < 24u) + return; + if (!nd6_options_valid(ns->options, payload_len - 24u)) + return; + memcpy(target.addr, ns->target, 16); + /* RFC 4861 section 7.1.1: the target must not be a multicast address. */ + if (ip6_is_multicast(&target)) + return; + ip6_hdr_get_src(pkt, &src); + ip6_hdr_get_dst(pkt, &dst); + + slot = nd6_slot_for(s, if_idx, &target); + if (slot == NULL) + return; /* not our address: nothing to answer */ + + if (ip6_is_unspecified(&src)) { + ip6 solicited; + + ip6_set_solicited_node(&solicited, &target); + /* RFC 4861 section 7.1.1: a DAD solicitation must go to the + * target's solicited-node multicast address and must not carry a + * Source Link-Layer Address option. */ + if ((ip6_cmp(&dst, &solicited) != 0) || + (nd6_find_option(ns->options, payload_len - 24u, + ND6_OPT_SLLA) != NULL)) + return; + /* Somebody else is running duplicate address detection for this + * address (RFC 4862 section 5.4.3). + * + * If it is still tentative for us too, we are both probing at the + * same time and neither may use it. If it is already ours, they + * must be told, and the answer goes to the all-nodes group because + * a node with no address cannot be addressed directly. */ + if (slot->info.state == WOLFIP_IFADDR_TENTATIVE) { + nd6_dad_failed(s, slot); + return; + } + ip6_set_all_nodes(&reply_dst); + ip6_mcast_to_eth(&reply_dst, reply_mac); + /* Unsolicited: the requester is not yet addressable. */ + flags = ND6_NA_OVERRIDE; + nd6_send_na(s, if_idx, &target, &target, &reply_dst, reply_mac, flags); + return; + } + + { + ip6 solicited; + + /* RFC 4861 sections 4.3 and 7.1.1: ordinary address resolution uses + * either the target's solicited-node group or the target itself for + * Neighbor Unreachability Detection. */ + ip6_set_solicited_node(&solicited, &target); + if ((ip6_cmp(&dst, &target) != 0) && + (ip6_cmp(&dst, &solicited) != 0)) + return; + } + + /* A tentative address must not be defended and must not answer: it is + * not ours yet. */ + if (slot->info.state == WOLFIP_IFADDR_TENTATIVE) + return; + + /* RFC 4861 section 7.2.3: record the sender so the advertisement has + * somewhere to go and the reverse direction is already resolved. */ + opt = nd6_find_option(ns->options, payload_len - 24u, ND6_OPT_SLLA); + if (opt != NULL) { + const struct nd6_opt_lla *lla = (const struct nd6_opt_lla *)opt; + + if (lla->len != 1u) + return; + /* RFC 2464 section 6: on Ethernet the SLLA is the sender's link-layer + * address. A disagreement would redirect both our reply and cache + * entry to an uninvolved host. */ + if (memcmp(lla->mac, pkt->eth.src, 6) != 0) + return; + nd6_store_neighbor(s, if_idx, &src, lla->mac, ND6_STALE, 0); + memcpy(reply_mac, lla->mac, 6); + } else { + memcpy(reply_mac, pkt->eth.src, 6); + } + ip6_copy(&reply_dst, &src); + nd6_send_na(s, if_idx, &target, &target, &reply_dst, reply_mac, flags); +} + +/* Neighbor Advertisement (RFC 4861 section 7.1.2 and 7.2.5). */ +static void nd6_recv_na(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t payload_len) +{ + struct nd6_msg *na = (struct nd6_msg *)pkt; + const uint8_t *opt; + struct wolfIP_ifaddr_slot *slot; + struct nd6_neighbor *n; + ip6 target; + ip6 src; + ip6 dst; + int idx; + + if (payload_len < 24u) + return; + if (!nd6_options_valid(na->options, payload_len - 24u)) + return; + memcpy(target.addr, na->target, 16); + if (ip6_is_multicast(&target)) + return; + ip6_hdr_get_src(pkt, &src); + /* RFC 4861 section 7.1.2: an Advertisement source must be unicast. */ + if (ip6_is_unspecified(&src)) + return; + ip6_hdr_get_dst(pkt, &dst); + if (!nd6_destination_is_local(s, if_idx, &dst)) + return; + /* RFC 4861 section 7.1.2: a solicited advertisement must not be sent to + * a multicast address. */ + if ((na->flags & ND6_NA_SOLICITED) && ip6_is_multicast(&dst)) + return; + + /* Somebody is using an address we are still probing for. */ + slot = nd6_slot_for(s, if_idx, &target); + if ((slot != NULL) && (slot->info.state == WOLFIP_IFADDR_TENTATIVE)) { + nd6_dad_failed(s, slot); + return; + } + + idx = nd6_neighbor_index(s, if_idx, &target); + if (idx < 0) + return; /* unsolicited advertisement for an unknown neighbour */ + n = &s->nd6.neighbors[idx]; + + opt = nd6_find_option(na->options, payload_len - 24u, ND6_OPT_TLLA); + if (opt != NULL) { + const struct nd6_opt_lla *lla = (const struct nd6_opt_lla *)opt; + + if (lla->len != 1u) + return; + if (n->state == ND6_INCOMPLETE) { + /* The answer we were waiting for. */ + memcpy(n->mac, lla->mac, 6); + } else if ((na->flags & ND6_NA_OVERRIDE) == 0) { + if (memcmp(n->mac, lla->mac, 6) != 0) { + /* RFC 4861 section 7.2.5: without the Override flag a + * differing link-layer address must not replace the one we + * hold - that is what stops an advertisement hijacking an + * established neighbour. A REACHABLE entry still drops to + * STALE so its reachability is re-verified; in any other + * state the advertisement is ignored outright. */ + if (n->state == ND6_REACHABLE) { + n->state = ND6_STALE; + n->ts = s->last_tick; + } + return; + } + } else { + memcpy(n->mac, lla->mac, 6); + } + } else if (n->state == ND6_INCOMPLETE) { + /* No link-layer address and none known: nothing has been learned. */ + return; + } + + if (na->flags & ND6_NA_SOLICITED) + n->state = ND6_REACHABLE; + else if (n->state == ND6_INCOMPLETE) + n->state = ND6_STALE; + n->probes = 0; + n->ts = s->last_tick; + if (na->flags & ND6_NA_ROUTER) + n->is_router = 1; +} + +/* Router Advertisement (RFC 4861 section 6.3.4). Minimal on purpose: the + * default router and the Prefix Information options, which is what an + * ordinary site network needs to hand out an address. Managed/Other flags, + * MTU, retransmit and reachable timer overrides are parsed past but not + * acted on. */ +static void nd6_recv_ra(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t payload_len) +{ + struct nd6_ra_msg *ra = (struct nd6_ra_msg *)pkt; + const uint8_t *opts; + uint32_t opt_len; + uint32_t off; + ip6 src; + ip6 dst; + + if (payload_len < 16u) + return; + ip6_hdr_get_src(pkt, &src); + /* RFC 4861 section 6.1.2: the source of a Router Advertisement must be a + * link-local address. Accepting a global source would let anything off + * the link install a default route. */ + if (!ip6_is_link_local(&src)) + return; + ip6_hdr_get_dst(pkt, &dst); + if (!nd6_destination_is_local(s, if_idx, &dst)) + return; + + opts = ra->options; + opt_len = payload_len - 16u; + if (!nd6_options_valid(opts, opt_len)) + return; + + /* Only mutate the router, neighbor and prefix tables after the complete + * option area has passed framing validation. */ + nd6_router_store(s, if_idx, &src, ee16(ra->router_lifetime)); + nd6_store_neighbor(s, if_idx, &src, pkt->eth.src, ND6_STALE, 1); + off = 0; + while ((off + 2u) <= opt_len) { + uint8_t type = opts[off]; + uint32_t olen = (uint32_t)opts[off + 1] * 8u; + + if (olen == 0) + return; /* malformed: would not terminate */ + if ((off + olen) > opt_len) + return; + if ((type == ND6_OPT_PREFIX) && + (olen == sizeof(struct nd6_opt_prefix))) { + const struct nd6_opt_prefix *po = + (const struct nd6_opt_prefix *)&opts[off]; + ip6 prefix; + uint32_t valid = ee32(po->valid_lifetime); + uint32_t preferred = ee32(po->preferred_lifetime); + + memcpy(prefix.addr, po->prefix, 16); + /* RFC 4862 section 5.5.3 (a): an advertised link-local prefix is + * silently ignored, which stops a hostile advertisement from + * redefining fe80::/10. */ + if ((po->prefix_len <= 128u) && !ip6_is_link_local(&prefix) && + (preferred <= valid)) { + nd6_prefix_store(s, if_idx, &prefix, po->prefix_len, + (po->flags & ND6_PREFIX_ONLINK) ? 1 : 0, + (po->flags & ND6_PREFIX_AUTO) ? 1 : 0, + valid, preferred); + /* RFC 4862 section 5.5.3 (d): only a prefix of exactly 64 + * bits leaves room for a 64-bit interface identifier. */ + if ((po->flags & ND6_PREFIX_AUTO) && (po->prefix_len == 64u) && + (valid != 0)) { + struct wolfIP_ll_dev *ll = wolfIP_ll_at(s, if_idx); + ip6 iid; + ip6 formed; + + if (ll != NULL) { + ip6_iid_from_mac(&iid, ll->mac); + ip6_make_addr(&formed, &prefix, 64, &iid); + /* Adding it is a no-op when it is already there, so + * a repeated advertisement does not restart DAD. */ + if (nd6_slot_for(s, if_idx, &formed) == NULL) { + if (wolfIP_ifaddr_add6(s, if_idx, &formed, 64) == 0) { + struct wolfIP_ifaddr_slot *slot = + nd6_slot_for(s, if_idx, &formed); + + if (slot != NULL) + nd6_dad_start(s, slot); + } + } + } + } + } + } + off += olen; + } +} + +/* Neighbor Discovery entry point, called from icmp6_input(). */ +static void nd6_input(struct wolfIP *s, unsigned int if_idx, + struct wolfIP_ip6_packet *pkt, uint32_t payload_len) +{ + struct nd6_msg *msg = (struct nd6_msg *)pkt; + + /* RFC 4861 sections 6.1 and 7.1: a hop limit other than 255 means the + * message crossed a router and cannot be trusted. This one check is what + * keeps Neighbor Discovery on the local link. */ + if (pkt->hop_limit != ND6_HOP_LIMIT) + return; + /* All Neighbor Discovery messages carry code 0. */ + if (msg->code != 0) + return; + + switch (msg->type) { + case ICMP6_NEIGHBOR_SOLICIT: + nd6_recv_ns(s, if_idx, pkt, payload_len); + break; + case ICMP6_NEIGHBOR_ADVERT: + nd6_recv_na(s, if_idx, pkt, payload_len); + break; + case ICMP6_ROUTER_ADVERT: + nd6_recv_ra(s, if_idx, pkt, payload_len); + break; + case ICMP6_ROUTER_SOLICIT: + case ICMP6_REDIRECT: + default: + /* Router Solicitations are a router's job. Redirect is out of + * scope for a host-only stack and must be ignored rather than + * acted on. */ + break; + } +} + +/* ---------------------------------------------------------------------- */ +/* Periodic work */ +/* ---------------------------------------------------------------------- */ + +/* Arm the periodic tick, replacing any timer already armed. + * + * Shaped like dhcp_schedule_timer_at(): the id is simply overwritten, so + * there is no "am I already running" flag for callers to keep in sync. Any + * live timer is cancelled first, which makes calling this twice harmless + * rather than leaving a stray entry in the heap. + * + * timers_binheap_insert() returns 0 when the heap is full, and NO_TIMER is + * 0, so a failed insert simply leaves the tick disarmed. That is not fatal + * because nd6_poll() retries from the main loop; without that retry a + * momentarily full heap would stop duplicate address detection, router + * solicitation and every expiry permanently, with nothing to notice. */ +static void nd6_arm_tick(struct wolfIP *s) +{ + struct wolfIP_timer tmr; + + if (s->nd6.tick_timer != NO_TIMER) + timer_binheap_cancel(&s->timers, s->nd6.tick_timer); + memset(&tmr, 0, sizeof(tmr)); + tmr.expires = s->last_tick + ND6_TICK_MS; + tmr.arg = s; + tmr.cb = nd6_tick_cb; + s->nd6.tick_timer = timers_binheap_insert(&s->timers, tmr); +} + +/* Is there anything for the tick to do? + * + * Deliberately conservative: it answers yes if any one of the five sources + * of periodic work is live, so the tick is never stopped with work still + * queued. A quiescent but populated cache therefore keeps it running, which + * is the safe direction to err in. */ +static int nd6_has_work(struct wolfIP *s) +{ + unsigned int i; + + for (i = 0; i < WOLFIP_MAX_INTERFACES; i++) { + if (s->nd6.rs_left[i] != 0) + return 1; + } + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + if (!s->ifaddr[i].used) + continue; + if (s->ifaddr[i].info.family != AF_INET6) + continue; + if (s->ifaddr[i].info.state == WOLFIP_IFADDR_TENTATIVE) + return 1; + } + for (i = 0; i < WOLFIP_ND6_CACHE_SIZE; i++) { + uint8_t st = s->nd6.neighbors[i].state; + + /* REACHABLE and STALE need the tick only to age, which the two + * transient states below cover; INCOMPLETE, DELAY and PROBE are + * mid-resolution and must be driven. REACHABLE also ages out, so it + * counts as work. */ + if ((st == ND6_INCOMPLETE) || (st == ND6_DELAY) || + (st == ND6_PROBE) || (st == ND6_REACHABLE)) + return 1; + } + for (i = 0; i < WOLFIP_ND6_PREFIX_MAX; i++) { + if (s->nd6.prefixes[i].used && + (s->nd6.prefixes[i].valid_lifetime != 0xFFFFFFFFu)) + return 1; + } + for (i = 0; i < WOLFIP_ND6_ROUTER_MAX; i++) { + if (s->nd6.routers[i].used) + return 1; + } + return 0; +} + +/* Called from wolfIP_poll(). Arms the tick whenever there is work and no + * timer running - which is both the normal wake-up after an idle period and + * the recovery path when an earlier insert failed on a full heap. */ +static void nd6_poll(struct wolfIP *s) +{ + if (s->nd6.tick_timer != NO_TIMER) + return; + if (nd6_has_work(s)) + nd6_arm_tick(s); +} + +static void nd6_tick_cb(void *arg) +{ + struct wolfIP *s = (struct wolfIP *)arg; + unsigned int i; + + if (!s) + return; + /* The heap has already popped this entry, so the recorded id is stale. + * Clearing it here keeps the field truthful for the rest of the tick: + * nd6_arm_tick() then has nothing to cancel, and nd6_poll() sees the + * tick as unarmed if this pass decides not to re-arm. */ + s->nd6.tick_timer = NO_TIMER; + + /* Duplicate address detection. */ + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + struct wolfIP_ifaddr_slot *slot = &s->ifaddr[i]; + + if (!slot->used || (slot->info.family != AF_INET6)) + continue; + if (slot->info.state != WOLFIP_IFADDR_TENTATIVE) + continue; + if (s->last_tick < slot->dad_due) + continue; + if (slot->dad_probes > 0) { + ip6 unspec; + + slot->dad_probes--; + ip6_set_unspecified(&unspec); + nd6_send_ns(s, slot->info.if_idx, &slot->info.v6, &unspec); + slot->dad_due = s->last_tick + ND6_RETRANS_TIMER_MS; + } else { + /* RFC 4862 section 5.4.4: no answer within the retransmit + * interval means the address is unique. */ + slot->info.state = WOLFIP_IFADDR_PREFERRED; + } + } + + /* Router solicitation, once the link-local address is usable. */ + for (i = 0; i < WOLFIP_MAX_INTERFACES; i++) { + if (!s->nd6.started[i] || (s->nd6.rs_left[i] == 0)) + continue; + if (s->last_tick < s->nd6.rs_due[i]) + continue; + s->nd6.rs_left[i]--; + nd6_send_rs(s, i); + s->nd6.rs_due[i] = s->last_tick + ND6_RTR_SOLICITATION_INTERVAL_MS; + } + + /* Neighbour cache ageing: a REACHABLE entry whose confirmation has timed + * out drops to STALE rather than being discarded, so the link-layer + * address is kept and only its reachability is in doubt. */ + for (i = 0; i < WOLFIP_ND6_CACHE_SIZE; i++) { + struct nd6_neighbor *n = &s->nd6.neighbors[i]; + + if (n->state == 0) + continue; + if ((n->state == ND6_REACHABLE) && + ((s->last_tick - n->ts) > ND6_REACHABLE_TIME_MS)) { + n->state = ND6_STALE; + n->ts = s->last_tick; + } + if ((n->state == ND6_INCOMPLETE) && + ((s->last_tick - n->ts) > (ND6_RETRANS_TIMER_MS * + ND6_MAX_MULTICAST_SOLICIT))) { + n->state = 0; /* resolution gave up */ + } + } + + /* Prefix and router lifetimes, both in seconds on the wire. */ + for (i = 0; i < WOLFIP_ND6_PREFIX_MAX; i++) { + struct nd6_prefix *p = &s->nd6.prefixes[i]; + + if (!p->used || (p->valid_lifetime == 0xFFFFFFFFu)) + continue; + if ((s->last_tick - p->ts) > ((uint64_t)p->valid_lifetime * 1000u)) + p->used = 0; + } + for (i = 0; i < WOLFIP_ND6_ROUTER_MAX; i++) { + struct nd6_router *r = &s->nd6.routers[i]; + + if (!r->used) + continue; + if ((s->last_tick - r->ts) > ((uint64_t)r->lifetime * 1000u)) + r->used = 0; + } + + /* Only keep ticking while something needs it. An interface that has + * been stopped, with no tentative address, no solicitation outstanding, + * no neighbour mid-resolution and no finite lifetime to expire, has + * nothing for this to do. nd6_poll() arms it again when work appears. */ + if (nd6_has_work(s)) + nd6_arm_tick(s); +} + +/* ---------------------------------------------------------------------- */ +/* Public entry points */ +/* ---------------------------------------------------------------------- */ + +int wolfIP_ipv6_start(struct wolfIP *s, unsigned int if_idx) +{ + struct wolfIP_ll_dev *ll; + struct wolfIP_ifaddr_slot *slot; + ip6 prefix; + ip6 iid; + ip6 link_local; + + if (!s || (if_idx >= WOLFIP_MAX_INTERFACES)) + return -WOLFIP_EINVAL; + ll = wolfIP_ll_at(s, if_idx); + if (!ll) + return -WOLFIP_EINVAL; + + /* RFC 4862 section 5.3: the link-local address is formed from fe80::/64 + * and a modified EUI-64 interface identifier, then verified with + * duplicate address detection before it may be used. */ + if (atoip6("fe80::", &prefix) != 0) + return -WOLFIP_EINVAL; + ip6_iid_from_mac(&iid, ll->mac); + ip6_make_addr(&link_local, &prefix, 64, &iid); + + slot = nd6_slot_for(s, if_idx, &link_local); + if (slot == NULL) { + if (wolfIP_ifaddr_add6(s, if_idx, &link_local, 64) != 0) + return -WOLFIP_ENOMEM; + slot = nd6_slot_for(s, if_idx, &link_local); + if (slot == NULL) + return -WOLFIP_ENOMEM; + } + slot->info.flags |= WOLFIP_IFADDR_FLAG_LINKLOCAL; + nd6_dad_start(s, slot); + + /* Router solicitations begin once there is a source address to send + * them from; the tick handles the ordering. */ + s->nd6.started[if_idx] = 1; + s->nd6.rs_left[if_idx] = (uint8_t)ND6_MAX_RTR_SOLICITATIONS; + s->nd6.rs_due[if_idx] = s->last_tick + ND6_RETRANS_TIMER_MS; + + nd6_arm_tick(s); + return 0; +} + +/* Stop Neighbor Discovery on an interface. + * + * Halts what the tick drives: router solicitation, and duplicate address + * detection for anything still tentative, which is dropped because it never + * completed. Addresses that had already passed detection are left alone and + * are still defended by nd6_recv_ns(); remove them with wolfIP_ifaddr_del6() + * if that is wanted. When no interface is left running, the tick releases + * its slot in the shared timer heap. */ +int wolfIP_ipv6_stop(struct wolfIP *s, unsigned int if_idx) +{ + unsigned int i; + + if (!s || (if_idx >= WOLFIP_MAX_INTERFACES)) + return -WOLFIP_EINVAL; + + s->nd6.started[if_idx] = 0; + s->nd6.rs_left[if_idx] = 0; + s->nd6.rs_due[if_idx] = 0; + + for (i = 0; i < WOLFIP_IFADDR_MAX; i++) { + struct wolfIP_ifaddr_slot *slot = &s->ifaddr[i]; + + if (!slot->used || (slot->info.family != AF_INET6)) + continue; + if (slot->info.if_idx != (uint8_t)if_idx) + continue; + if (slot->info.state == WOLFIP_IFADDR_TENTATIVE) { + slot->used = 0; + slot->dad_probes = 0; + } + } + + if (!nd6_has_work(s) && (s->nd6.tick_timer != NO_TIMER)) { + timer_binheap_cancel(&s->timers, s->nd6.tick_timer); + s->nd6.tick_timer = NO_TIMER; + } + return 0; +} + +int wolfIP_ipv6_addr_add(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr, uint8_t prefix_len) +{ + struct wolfIP_ifaddr_slot *slot; + int ret; + + if (!s || !addr || (if_idx >= WOLFIP_MAX_INTERFACES)) + return -WOLFIP_EINVAL; + ret = wolfIP_ifaddr_add6(s, if_idx, addr, prefix_len); + if (ret != 0) + return ret; + slot = nd6_slot_for(s, if_idx, addr); + if (slot == NULL) + return -WOLFIP_EINVAL; + /* RFC 4862 section 5.4: duplicate address detection applies to every + * unicast address, however it was obtained - statically configured ones + * included. */ + nd6_dad_start(s, slot); + return 0; +} + +int wolfIP_nd6_neighbor_add(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr, const uint8_t *mac) +{ + if (!s || !addr || !mac || (if_idx >= WOLFIP_MAX_INTERFACES)) + return -WOLFIP_EINVAL; + if (ip6_is_multicast(addr) || ip6_is_unspecified(addr) || + ip6_is_loopback(addr) || ip6_is_v4mapped(addr) || + ip6_is_v4compat(addr)) + return -WOLFIP_EINVAL; + (void)nd6_store_neighbor(s, if_idx, addr, mac, ND6_REACHABLE, 0); + return 0; +} + +int wolfIP_nd6_lookup(struct wolfIP *s, unsigned int if_idx, const ip6 *addr, + uint8_t *mac) +{ + if (!s || !addr || !mac || (if_idx >= WOLFIP_MAX_INTERFACES)) + return -WOLFIP_EINVAL; + return (nd6_lookup(s, if_idx, addr, mac) == 0) ? 0 : -1; +} + +int wolfIP_ipv6_nexthop(struct wolfIP *s, unsigned int if_idx, const ip6 *dst, + ip6 *nexthop) +{ + if (!s || !dst || !nexthop || (if_idx >= WOLFIP_MAX_INTERFACES)) + return -WOLFIP_EINVAL; + return nd6_select_nexthop(s, if_idx, dst, nexthop); +} diff --git a/tools/scripts/wolfip-radvd.sh b/tools/scripts/wolfip-radvd.sh new file mode 100755 index 00000000..ad6faf6d --- /dev/null +++ b/tools/scripts/wolfip-radvd.sh @@ -0,0 +1,114 @@ +#!/bin/sh +# wolfip-radvd.sh - run radvd on a wolfIP test interface +# +# Copyright (C) 2026 wolfSSL Inc. +# This file is part of wolfIP TCP/IP stack, distributed under the GPLv3. +# +# Advertises a documentation prefix on a TAP interface so that wolfIP can +# configure itself by SLAAC from a real router implementation, rather than +# from a Router Advertisement the test wrote itself. +# +# sudo tools/scripts/wolfip-radvd.sh start [iface] [prefix] +# sudo tools/scripts/wolfip-radvd.sh stop [iface] +# sudo tools/scripts/wolfip-radvd.sh status +# +# Defaults match src/test/test_ipv6_slaac.c: wtcp0 and 2001:db8:1:2::/64. +# +# radvd refuses to advertise on an interface that does not have IPv6 +# forwarding enabled - it will not act as a router on a host - so this turns +# it on for the one interface and puts it back on stop. + +set -e + +ACTION="${1:-start}" +IFACE="${2:-wtcp0}" +PREFIX="${3:-2001:db8:1:2::/64}" +HOSTADDR="${4:-2001:db8:1:2::1/64}" + +CONF="/tmp/wolfip-radvd-${IFACE}.conf" +PIDFILE="/tmp/wolfip-radvd-${IFACE}.pid" +# Prior forwarding setting, saved on start so stop can put it back rather +# than assuming it was off. Running this against a real interface would +# otherwise silently disable forwarding on it. +FWSAVE="/tmp/wolfip-radvd-${IFACE}.fwd" + +die() { echo "wolfip-radvd: $*" >&2; exit 1; } + +[ "$(id -u)" = "0" ] || die "must run as root" + +case "$ACTION" in +start) + command -v radvd >/dev/null 2>&1 || die "radvd is not installed" + ip link show "$IFACE" >/dev/null 2>&1 || \ + die "interface $IFACE does not exist (start the wolfIP test first)" + + # radvd will not advertise unless the interface forwards. Remember what + # it was so stop can restore it. + sysctl -n "net.ipv6.conf.${IFACE}.forwarding" > "$FWSAVE" 2>/dev/null || \ + echo 0 > "$FWSAVE" + sysctl -qw "net.ipv6.conf.${IFACE}.forwarding=1" + # An address in the prefix, so the host can reach whatever wolfIP picks. + ip -6 addr replace "$HOSTADDR" dev "$IFACE" nodad + + cat > "$CONF" </dev/null)" 2>/dev/null; then + echo "wolfip-radvd: already running on $IFACE" + exit 0 + fi + radvd -C "$CONF" -p "$PIDFILE" -m stderr_syslog + echo "wolfip-radvd: advertising $PREFIX on $IFACE (pid $(cat "$PIDFILE"))" + ;; + +stop) + if [ -f "$PIDFILE" ]; then + kill "$(cat "$PIDFILE")" 2>/dev/null || true + rm -f "$PIDFILE" + fi + rm -f "$CONF" + # Put the interface back as it was: drop the address this script added, + # and restore the forwarding setting rather than forcing it off. + ip -6 addr del "$HOSTADDR" dev "$IFACE" 2>/dev/null || true + if [ -f "$FWSAVE" ]; then + sysctl -qw "net.ipv6.conf.${IFACE}.forwarding=$(cat "$FWSAVE")" \ + 2>/dev/null || true + rm -f "$FWSAVE" + fi + echo "wolfip-radvd: stopped on $IFACE" + ;; + +status) + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then + echo "running (pid $(cat "$PIDFILE"))" + else + echo "not running" + exit 1 + fi + ;; + +*) + die "usage: $0 {start|stop|status} [iface] [prefix] [hostaddr]" + ;; +esac diff --git a/wolfip.h b/wolfip.h index cc29948d..d1356f07 100644 --- a/wolfip.h +++ b/wolfip.h @@ -139,6 +139,13 @@ typedef uint32_t ip4; # error "Cannot determine byte order; define __BYTE_ORDER__" #endif +/* IPv6 address type and helpers. Included unconditionally: the header holds + * only a typedef and static inline functions, so it adds no code when IPv6 is + * disabled and cannot alter the layout of any structure below. The parts of + * IPv6 that *do* affect layout live behind WOLFIP_IPV6 inside wolfip.c, which + * includes config.h. */ +#include "wolfip6.h" + #ifndef WOLFIP_EAGAIN #ifdef EAGAIN #define WOLFIP_EAGAIN EAGAIN @@ -219,6 +226,61 @@ struct wolfIP_wifi_ops { int (*get_bssid)(struct wolfIP_ll_dev *ll, uint8_t out_bssid[6]); }; +/* Optional embedded-switch control surface. + * + * Populated only by drivers for parts with a multi-port switch that the CPU + * shares, which is what Ethernet ring-redundancy protocols require. The + * motivating case is DLR (Device Level Ring, ODVA CIP Networks Library + * Vol. 2 chapter 9, also IEC 61784-2): a DLR node has two ports on one MAC, + * and the ring supervisor breaks the loop by blocking ordinary traffic on + * one of them while still passing ring-protocol frames. + * + * wolfIP does not implement DLR. This vtable, together with the L2 protocol + * hook below, is the contract a third-party DLR implementation needs from + * the stack and from the driver, so that adding one later requires no + * changes to the core. + * + * A note on timing, because it constrains any such implementation: wolfIP's + * poll loop is millisecond-granular (wolfIP_poll takes `now` in + * milliseconds) and its timer heap is built on that. DLR beacon intervals + * are sub-millisecond. A DLR implementation must therefore drive beacon + * transmission and the beacon timeout from its own hardware timer; the + * stack's timers are not suitable and this vtable does not pretend + * otherwise. + */ +struct wolfIP_switch_ops { + /* Number of external ports behind this interface (2 for a DLR node). */ + int (*port_count)(struct wolfIP_ll_dev *ll); + /* Per-port link state: 1 up, 0 down, negative on error. A ring + * implementation needs the transition, not just the level, so drivers + * that can report link change events should do so via link_changed. */ + int (*port_link_state)(struct wolfIP_ll_dev *ll, unsigned int port); + /* Register a callback invoked when a port's link state changes. This is + * what lets a ring detect a break in well under the beacon timeout. */ + int (*set_link_change_cb)(struct wolfIP_ll_dev *ll, + void (*cb)(void *ctx, unsigned int port, + int up), + void *ctx); + /* Block or unblock ordinary traffic on a port. A blocked port must + * still pass frames whose ethertype has been claimed via + * wolfIP_register_l2_handler(), otherwise the ring protocol cannot see + * its own beacons across the block. */ + int (*port_set_blocked)(struct wolfIP_ll_dev *ll, unsigned int port, + int blocked); + /* Flush the switch's learned MAC address table, for the whole device or + * for one port. Required after a ring topology change, or traffic keeps + * being forwarded towards the broken segment. */ + int (*flush_mac_table)(struct wolfIP_ll_dev *ll, int port_or_all); + /* Transmit a raw frame out of one specific port, bypassing normal + * forwarding. Ring protocols must be able to address each port + * individually; wolfIP's ordinary send path targets an interface. */ + int (*send_on_port)(struct wolfIP_ll_dev *ll, unsigned int port, + void *buf, uint32_t len); + /* Report which port a received frame arrived on, for the most recent + * frame handed to the stack. Negative if the driver cannot tell. */ + int (*last_rx_port)(struct wolfIP_ll_dev *ll); +}; + /* Struct to contain link-layer (ll) device description */ struct wolfIP_ll_dev { @@ -244,6 +306,9 @@ struct wolfIP_ll_dev { /* Optional Wi-Fi vtable. NULL on Ethernet ports. Appended last so adding * it does not shift the offsets of the pre-existing members. */ const struct wolfIP_wifi_ops *wifi_ops; + /* Optional embedded-switch vtable. NULL on ordinary single-port drivers. + * Appended last, after wifi_ops, for the same ABI reason. */ + const struct wolfIP_switch_ops *switch_ops; }; /* Struct to contain an IP device configuration */ @@ -417,6 +482,133 @@ struct wolfIP_sockaddr_ll { typedef struct wolfIP_sockaddr_ll wolfIP_sockaddr_ll; #endif +/* AF_INET6 is not guaranteed to exist on a bare-metal target, and its value + * is not the same everywhere: 10 on Linux, 28 on FreeBSD, 30 on macOS. When + * a system header supplies one it wins; this is only the fallback, and it + * matters solely for values crossing the wolfIP API boundary, never on the + * wire. */ +#ifndef AF_INET6 +#define AF_INET6 10 +#endif + +/* Per-interface address list. + * + * struct ipconf above holds the *primary* IPv4 address of an interface and + * keeps doing so: it is reached by every board port and by wolfIP_ipconfig_*, + * and none of that changes. This list is additive - it holds the additional + * addresses, meaning IPv4 aliases and every IPv6 address. The primary is + * never copied into it, so the two cannot drift apart. + * + * Index 0 of an interface's IPv4 list is therefore always the primary, and + * higher indices are aliases in insertion order. + * + * WOLFIP_IF_CONF_MAX caps the total per interface, primary included. It is 1 + * unless WOLFIP_IF_MULTICONF is enabled, so with the feature off these calls + * still work but an interface holds exactly one address and struct wolfIP + * does not grow at all. IPv6 requires the feature, because a link-local + * address always coexists with any global one; IPv4 aliasing is the same + * machinery and is available without IPv6. + */ +#define WOLFIP_IFADDR_PREFERRED 0 +#define WOLFIP_IFADDR_TENTATIVE 1 /* IPv6: duplicate address detection */ +#define WOLFIP_IFADDR_DEPRECATED 2 /* IPv6: preferred lifetime expired */ + +#define WOLFIP_IFADDR_FLAG_PRIMARY 0x01 +#define WOLFIP_IFADDR_FLAG_LINKLOCAL 0x02 +#define WOLFIP_IFADDR_FLAG_SLAAC 0x04 +#define WOLFIP_IFADDR_FLAG_DHCP 0x08 + +struct wolfIP_ifaddr_info { + uint16_t family; /* AF_INET or AF_INET6 */ + uint8_t if_idx; + uint8_t prefix_len; + uint8_t state; /* WOLFIP_IFADDR_* */ + uint8_t flags; /* WOLFIP_IFADDR_FLAG_* */ + ip4 v4; /* valid when family == AF_INET */ + ip6 v6; /* valid when family == AF_INET6 */ + uint32_t valid_lifetime; /* seconds; 0 means unlimited */ + uint32_t preferred_lifetime; /* seconds; 0 means unlimited */ +}; + +/* Add an address. Returns 0, or negative on a bad argument, a duplicate, or + * when the interface has reached WOLFIP_IF_CONF_MAX. Adding the first IPv4 + * address of an interface sets the primary, so a single-address build is + * fully usable through this API alone. */ +int wolfIP_ifaddr_add4(struct wolfIP *s, unsigned int if_idx, ip4 addr, + uint8_t prefix_len); +int wolfIP_ifaddr_add6(struct wolfIP *s, unsigned int if_idx, const ip6 *addr, + uint8_t prefix_len); + +/* Remove an address. Returns 0, or negative if it is not configured on that + * interface. Removing the primary does not disturb the aliases. */ +int wolfIP_ifaddr_del4(struct wolfIP *s, unsigned int if_idx, ip4 addr); +int wolfIP_ifaddr_del6(struct wolfIP *s, unsigned int if_idx, const ip6 *addr); + +/* Number of addresses of `family` on an interface. Zero for a bad argument, + * an unknown family, or an unconfigured interface. */ +unsigned int wolfIP_ifaddr_count(struct wolfIP *s, unsigned int if_idx, + int family); + +/* Fetch the index'th address of `family`. Returns 0 on success. */ +int wolfIP_ifaddr_get(struct wolfIP *s, unsigned int if_idx, int family, + unsigned int index, struct wolfIP_ifaddr_info *out); + +/* Is this IPv4 address configured on any interface? Returns 1 and, when + * if_idx is non-NULL, the owning interface. Aliases count: an address that + * did not would be decorative, since inbound traffic to it would be + * dropped. IPADDR_ANY is never local. */ +int wolfIP_ifaddr_is_local4(struct wolfIP *s, ip4 addr, unsigned int *if_idx); + +/* IPv6 Neighbor Discovery and address autoconfiguration. + * + * wolfIP_ipv6_start() brings IPv6 up on an interface: it forms the + * link-local address from the interface MAC (RFC 4862 section 5.3), runs + * duplicate address detection on it, and then solicits routers so that a + * Router Advertisement can supply a global prefix. It is the IPv6 + * counterpart of dhcp_client_init(), and like it the work continues in the + * background from wolfIP_poll(). + * + * Progress is observable through wolfIP_ifaddr_count()/get(): an address is + * WOLFIP_IFADDR_TENTATIVE while duplicate address detection runs and + * WOLFIP_IFADDR_PREFERRED once it has passed. An address that turns out to + * be a duplicate is removed rather than assigned (RFC 4862 section 5.4.5). + */ +int wolfIP_ipv6_start(struct wolfIP *s, unsigned int if_idx); + +/* Configure an address and verify it with duplicate address detection + * before it is used (RFC 4862 section 5.4). This is the entry point for a + * statically assigned address, a ULA among them: it behaves like + * wolfIP_ifaddr_add6() but leaves the address TENTATIVE until the probe + * completes, whereas wolfIP_ifaddr_add6() configures it immediately and + * runs no detection. */ +int wolfIP_ipv6_addr_add(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr, uint8_t prefix_len); + +/* Stop Neighbor Discovery on an interface: no more router solicitation, + * and anything still tentative is dropped. Addresses that already passed + * duplicate address detection are kept and still answered for. When no + * interface is left running, the periodic tick releases its slot in the + * shared timer heap. */ +int wolfIP_ipv6_stop(struct wolfIP *s, unsigned int if_idx); + +/* Install a static neighbour cache entry. Useful before Router + * Advertisements have been seen, and for talking to a peer that does not + * answer solicitations. */ +int wolfIP_nd6_neighbor_add(struct wolfIP *s, unsigned int if_idx, + const ip6 *addr, const uint8_t *mac); + +/* Resolve an address to a link-layer address from the neighbour cache. + * Returns 0 and fills `mac` when the entry is usable. Multicast needs no + * cache entry: the mapping is algorithmic. */ +int wolfIP_nd6_lookup(struct wolfIP *s, unsigned int if_idx, const ip6 *addr, + uint8_t *mac); + +/* Next hop for a destination: the destination itself when it is on-link + * according to the prefix list, otherwise a default router. Returns 0 on + * success, negative when there is no route. */ +int wolfIP_ipv6_nexthop(struct wolfIP *s, unsigned int if_idx, const ip6 *dst, + ip6 *nexthop); + int wolfIP_sock_socket(struct wolfIP *s, int domain, int type, int protocol); int wolfIP_sock_bind(struct wolfIP *s, int sockfd, const struct wolfIP_sockaddr *addr, socklen_t addrlen); @@ -485,6 +677,41 @@ int wolfIP_register_eapol_handler(struct wolfIP *s, const uint8_t *frame, uint32_t len), void *ctx); +/* Generic link-layer protocol hook. + * + * wolfIP_register_eapol_handler() above is hardwired to ethertype 0x888E. + * This is the same idea without the hardwiring: a module claims an + * ethertype and receives every frame carrying it, so that protocols the + * stack does not implement can be layered on without editing the demux. + * + * The motivating consumer is a third-party DLR (Device Level Ring) + * implementation, which claims ethertype 0x80E1 and drives the switch + * through struct wolfIP_switch_ops. Nothing here is DLR-specific. + * + * `accept_macs` addresses the second half of the problem. The ingress path + * filters on destination MAC before dispatch - unicast to us, broadcast, + * and the IP multicast mappings - so a protocol using its own multicast + * group would never see a frame. A registered handler declares the + * destination MACs it wants delivered; `count` entries of 6 bytes each, + * matched exactly. Pass NULL/0 to receive only unicast and broadcast. + * + * `frame`/`len` cover the whole Ethernet frame including the header, since + * a ring protocol needs the source MAC and the ingress port. The ingress + * port, when the driver can report it, comes from + * switch_ops->last_rx_port(). + * + * Returns 0 on success, negative on bad arguments or when the table of + * registered protocols is full. Pass a NULL handler to unregister. + * + * NOTE: declared, not yet implemented. See docs/dlr_integration.md. + */ +int wolfIP_register_l2_handler(struct wolfIP *s, uint16_t ethertype, + int (*handler)(void *ctx, unsigned int if_idx, + const uint8_t *frame, + uint32_t len), + void *ctx, + const uint8_t *accept_macs, unsigned int count); + size_t wolfIP_instance_size(void); int wolfIP_poll(struct wolfIP *s, uint64_t now); void wolfIP_recv(struct wolfIP *s, void *buf, uint32_t len); diff --git a/wolfip6.h b/wolfip6.h new file mode 100644 index 00000000..6d95750a --- /dev/null +++ b/wolfip6.h @@ -0,0 +1,662 @@ +/* wolfip6.h + * + * IPv6 addressing for the wolfIP TCP/IP stack: the 128-bit address type, + * well-known addresses, scope and type predicates, prefix operations and + * RFC 4291 section 3 / RFC 5952 text conversion. + * + * Copyright (C) 2026 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 + */ +#ifndef WOLFIP6_H +#define WOLFIP6_H +#include +#include /* NULL */ + +/* This header is included unconditionally from wolfip.h. Everything in it is + * a type definition or a static inline function, so it costs nothing when + * IPv6 is disabled and it cannot change the layout or ABI of any existing + * structure. That matters because wolfip.c includes wolfip.h *before* + * config.h, so a feature macro set only in config.h is not yet visible here; + * anything that did affect layout would have to be driven from the command + * line (-DWOLFIP_IPV6=1), exactly as WOLFIP_VLAN already is. + * + * No dependency: the fixed 16-byte loops below are written out so + * that freestanding builds without a libc still work. + */ + +/* An IPv6 address, always stored in network byte order. + * + * A byte array rather than a word array: wolfIP targets big-endian (PowerPC + * e5500) and strict-alignment (PIC32MZ, Cortex-M) machines, and IPv6 + * addresses sit at odd offsets in an Ethernet frame, so a word view would be + * an unaligned access waiting to happen. Wrapped in a struct so it can be + * assigned and passed by value and cannot silently decay to a pointer - + * which also means it must be compared with ip6_cmp(), never with ==. + */ +typedef struct wolfIP_ip6_addr { + uint8_t addr[16]; +} ip6; + +/* Longest textual form is "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255", + * 45 characters plus the terminator. */ +#define WOLFIP_IP6_ADDRSTRLEN 46 + +/* Brace initialisers. Deliberately macros rather than file-scope const + * objects: an unused "static const" in a header trips -Wunused-const-variable + * under -Werror. */ +#define WOLFIP_IN6ADDR_ANY_INIT \ + {{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }} +#define WOLFIP_IN6ADDR_LOOPBACK_INIT \ + {{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }} + +/* Multicast scope values (RFC 4291 section 2.7). */ +#define WOLFIP_IP6_SCOPE_INTERFACE_LOCAL 0x1 +#define WOLFIP_IP6_SCOPE_LINK_LOCAL 0x2 +#define WOLFIP_IP6_SCOPE_ADMIN_LOCAL 0x4 +#define WOLFIP_IP6_SCOPE_SITE_LOCAL 0x5 +#define WOLFIP_IP6_SCOPE_ORG_LOCAL 0x8 +#define WOLFIP_IP6_SCOPE_GLOBAL 0xE + +/* ---------------------------------------------------------------------- */ +/* Basic operations */ +/* ---------------------------------------------------------------------- */ + +/* Ordering comparison. Returns 0 when equal, and otherwise the difference of + * the first differing byte, so the sign gives a stable total order. */ +static inline int ip6_cmp(const ip6 *a, const ip6 *b) +{ + int i; + + for (i = 0; i < 16; i++) { + if (a->addr[i] != b->addr[i]) + return (int)a->addr[i] - (int)b->addr[i]; + } + return 0; +} + +static inline void ip6_copy(ip6 *dst, const ip6 *src) +{ + int i; + + for (i = 0; i < 16; i++) + dst->addr[i] = src->addr[i]; +} + +static inline void ip6_set_unspecified(ip6 *a) +{ + int i; + + for (i = 0; i < 16; i++) + a->addr[i] = 0; +} + +static inline void ip6_set_loopback(ip6 *a) +{ + ip6_set_unspecified(a); + a->addr[15] = 1; +} + +/* ff02::1, all-nodes link-local multicast (RFC 4291 section 2.7.1). */ +static inline void ip6_set_all_nodes(ip6 *a) +{ + ip6_set_unspecified(a); + a->addr[0] = 0xFF; + a->addr[1] = 0x02; + a->addr[15] = 1; +} + +/* ff02::2, all-routers link-local multicast (RFC 4291 section 2.7.1). */ +static inline void ip6_set_all_routers(ip6 *a) +{ + ip6_set_unspecified(a); + a->addr[0] = 0xFF; + a->addr[1] = 0x02; + a->addr[15] = 2; +} + +/* Solicited-node multicast address for a target: ff02::1:ffXX:XXXX, formed + * from the low 24 bits of the target (RFC 4291 section 2.7.1). */ +static inline void ip6_set_solicited_node(ip6 *dst, const ip6 *target) +{ + ip6_set_unspecified(dst); + dst->addr[0] = 0xFF; + dst->addr[1] = 0x02; + dst->addr[11] = 0x01; + dst->addr[12] = 0xFF; + dst->addr[13] = target->addr[13]; + dst->addr[14] = target->addr[14]; + dst->addr[15] = target->addr[15]; +} + +/* Build ::ffff:a.b.c.d from an IPv4 address. The IPv4 value is in host byte + * order, matching the ip4 convention used throughout the public API. */ +static inline void ip6_set_v4mapped(ip6 *dst, uint32_t v4) +{ + ip6_set_unspecified(dst); + dst->addr[10] = 0xFF; + dst->addr[11] = 0xFF; + dst->addr[12] = (uint8_t)(v4 >> 24); + dst->addr[13] = (uint8_t)(v4 >> 16); + dst->addr[14] = (uint8_t)(v4 >> 8); + dst->addr[15] = (uint8_t)(v4 & 0xFFu); +} + +/* Recover the embedded IPv4 address, in host byte order. Only meaningful + * when ip6_is_v4mapped() is true. */ +static inline uint32_t ip6_get_v4mapped(const ip6 *a) +{ + return ((uint32_t)a->addr[12] << 24) | ((uint32_t)a->addr[13] << 16) | + ((uint32_t)a->addr[14] << 8) | (uint32_t)a->addr[15]; +} + +/* ---------------------------------------------------------------------- */ +/* Type and scope predicates */ +/* ---------------------------------------------------------------------- */ + +/* :: (RFC 4291 section 2.5.2) */ +static inline int ip6_is_unspecified(const ip6 *a) +{ + int i; + + for (i = 0; i < 16; i++) { + if (a->addr[i] != 0) + return 0; + } + return 1; +} + +/* ::1 (RFC 4291 section 2.5.3) */ +static inline int ip6_is_loopback(const ip6 *a) +{ + int i; + + for (i = 0; i < 15; i++) { + if (a->addr[i] != 0) + return 0; + } + return (a->addr[15] == 1) ? 1 : 0; +} + +/* ff00::/8 (RFC 4291 section 2.7) */ +static inline int ip6_is_multicast(const ip6 *a) +{ + return (a->addr[0] == 0xFF) ? 1 : 0; +} + +/* fe80::/10 (RFC 4291 section 2.5.6) */ +static inline int ip6_is_link_local(const ip6 *a) +{ + return ((a->addr[0] == 0xFE) && ((a->addr[1] & 0xC0) == 0x80)) ? 1 : 0; +} + +/* fc00::/7 unique local (RFC 4193) */ +static inline int ip6_is_ula(const ip6 *a) +{ + return ((a->addr[0] & 0xFE) == 0xFC) ? 1 : 0; +} + +/* 2000::/3 global unicast (RFC 3587) */ +static inline int ip6_is_global(const ip6 *a) +{ + return ((a->addr[0] & 0xE0) == 0x20) ? 1 : 0; +} + +/* ::ffff:0:0/96, the IPv4-mapped range (RFC 4291 section 2.5.5.2). + * + * These addresses represent an IPv4 node inside the IPv6 API. They must never + * appear in an IPv6 packet on the wire, so the receive path drops any frame + * carrying one in either address field. */ +static inline int ip6_is_v4mapped(const ip6 *a) +{ + int i; + + for (i = 0; i < 10; i++) { + if (a->addr[i] != 0) + return 0; + } + return ((a->addr[10] == 0xFF) && (a->addr[11] == 0xFF)) ? 1 : 0; +} + +/* ::a.b.c.d, the deprecated IPv4-compatible range (RFC 4291 section 2.5.5.1). + * Deprecated by RFC 4291 and never valid on the wire; recognised so that the + * receive path can drop it explicitly. :: and ::1 are excluded, being the + * unspecified and loopback addresses rather than compatible addresses. */ +static inline int ip6_is_v4compat(const ip6 *a) +{ + int i; + + for (i = 0; i < 12; i++) { + if (a->addr[i] != 0) + return 0; + } + if (ip6_is_unspecified(a) || ip6_is_loopback(a)) + return 0; + return 1; +} + +/* Multicast flags and scope nibbles (RFC 4291 section 2.7). Only meaningful + * when ip6_is_multicast() is true. */ +static inline uint8_t ip6_mcast_flags(const ip6 *a) +{ + return (uint8_t)((a->addr[1] >> 4) & 0x0F); +} + +static inline uint8_t ip6_mcast_scope(const ip6 *a) +{ + return (uint8_t)(a->addr[1] & 0x0F); +} + +/* ff02::/16 */ +static inline int ip6_is_mcast_link_local(const ip6 *a) +{ + return (ip6_is_multicast(a) && + (ip6_mcast_scope(a) == WOLFIP_IP6_SCOPE_LINK_LOCAL)) ? 1 : 0; +} + +/* ff02::1 */ +static inline int ip6_is_all_nodes(const ip6 *a) +{ + ip6 ref; + + ip6_set_all_nodes(&ref); + return (ip6_cmp(a, &ref) == 0) ? 1 : 0; +} + +/* ff02::2 */ +static inline int ip6_is_all_routers(const ip6 *a) +{ + ip6 ref; + + ip6_set_all_routers(&ref); + return (ip6_cmp(a, &ref) == 0) ? 1 : 0; +} + +/* ff02::1:ff00:0/104 */ +static inline int ip6_is_solicited_node(const ip6 *a) +{ + if (!ip6_is_mcast_link_local(a)) + return 0; + if (ip6_mcast_flags(a) != 0) + return 0; + if ((a->addr[2] != 0) || (a->addr[3] != 0) || (a->addr[4] != 0) || + (a->addr[5] != 0) || (a->addr[6] != 0) || (a->addr[7] != 0) || + (a->addr[8] != 0) || (a->addr[9] != 0) || (a->addr[10] != 0)) + return 0; + return ((a->addr[11] == 0x01) && (a->addr[12] == 0xFF)) ? 1 : 0; +} + +/* An address usable as a packet source or destination on the wire. */ +static inline int ip6_is_unicast(const ip6 *a) +{ + return (!ip6_is_multicast(a) && !ip6_is_unspecified(a)) ? 1 : 0; +} + +/* ---------------------------------------------------------------------- */ +/* Prefix operations */ +/* ---------------------------------------------------------------------- */ + +/* Compare the first prefix_len bits. Returns 0 when they match, non-zero + * otherwise. A prefix_len above 128 is clamped, and 0 matches everything. */ +static inline int ip6_prefix_cmp(const ip6 *a, const ip6 *b, uint8_t prefix_len) +{ + int full; + int rem; + int i; + + if (prefix_len > 128) + prefix_len = 128; + full = prefix_len / 8; + rem = prefix_len % 8; + for (i = 0; i < full; i++) { + if (a->addr[i] != b->addr[i]) + return 1; + } + if (rem != 0) { + uint8_t mask = (uint8_t)(0xFFu << (8 - rem)); + if ((a->addr[full] & mask) != (b->addr[full] & mask)) + return 1; + } + return 0; +} + +/* Zero every bit beyond prefix_len, leaving the network part. */ +static inline void ip6_prefix_mask(ip6 *a, uint8_t prefix_len) +{ + int full; + int rem; + int i; + + if (prefix_len > 128) + prefix_len = 128; + full = prefix_len / 8; + rem = prefix_len % 8; + if (rem != 0) { + a->addr[full] = (uint8_t)(a->addr[full] & (uint8_t)(0xFFu << (8 - rem))); + full++; + } + for (i = full; i < 16; i++) + a->addr[i] = 0; +} + +/* Combine a prefix with an interface identifier: the first prefix_len bits + * come from prefix, the remainder from iid (RFC 4862 section 5.5.3). */ +static inline void ip6_make_addr(ip6 *dst, const ip6 *prefix, + uint8_t prefix_len, const ip6 *iid) +{ + int full; + int rem; + int i; + + if (prefix_len > 128) + prefix_len = 128; + full = prefix_len / 8; + rem = prefix_len % 8; + for (i = 0; i < 16; i++) + dst->addr[i] = 0; + for (i = 0; i < full; i++) + dst->addr[i] = prefix->addr[i]; + if (rem != 0) { + uint8_t mask = (uint8_t)(0xFFu << (8 - rem)); + dst->addr[full] = (uint8_t)((prefix->addr[full] & mask) | + (iid->addr[full] & (uint8_t)~mask)); + full++; + } + for (i = full; i < 16; i++) + dst->addr[i] = iid->addr[i]; +} + +/* ---------------------------------------------------------------------- */ +/* Link layer mapping */ +/* ---------------------------------------------------------------------- */ + +/* Ethernet multicast MAC for an IPv6 multicast address: 33:33 followed by + * the last four bytes of the address (RFC 2464 section 7). */ +static inline void ip6_mcast_to_eth(const ip6 *a, uint8_t *mac) +{ + mac[0] = 0x33; + mac[1] = 0x33; + mac[2] = a->addr[12]; + mac[3] = a->addr[13]; + mac[4] = a->addr[14]; + mac[5] = a->addr[15]; +} + +/* Modified EUI-64 interface identifier from a 48-bit MAC (RFC 4291 appendix + * A): insert fffe in the middle and invert the universal/local bit. The + * result is written into the low 64 bits of iid, the high 64 bits are zeroed + * so it can be handed straight to ip6_make_addr(). */ +static inline void ip6_iid_from_mac(ip6 *iid, const uint8_t *mac) +{ + int i; + + for (i = 0; i < 16; i++) + iid->addr[i] = 0; + iid->addr[8] = (uint8_t)(mac[0] ^ 0x02); + iid->addr[9] = mac[1]; + iid->addr[10] = mac[2]; + iid->addr[11] = 0xFF; + iid->addr[12] = 0xFE; + iid->addr[13] = mac[3]; + iid->addr[14] = mac[4]; + iid->addr[15] = mac[5]; +} + +/* ---------------------------------------------------------------------- */ +/* Text conversion */ +/* ---------------------------------------------------------------------- */ + +/* Does the group starting at p contain a '.' before the next ':' or the end? + * Used to spot the dotted-quad tail of an IPv4-mapped literal. */ +static inline int wolfIP_ip6_group_has_dot(const char *p) +{ + while ((*p != '\0') && (*p != ':')) { + if (*p == '.') + return 1; + p++; + } + return 0; +} + +/* Parse 1..4 hex digits. Returns the count consumed, 0 when there are none. */ +static inline int wolfIP_ip6_parse_group(const char *p, uint32_t *out) +{ + uint32_t v = 0; + int n = 0; + + while (n < 4) { + char c = p[n]; + int d; + + if ((c >= '0') && (c <= '9')) + d = c - '0'; + else if ((c >= 'a') && (c <= 'f')) + d = (c - 'a') + 10; + else if ((c >= 'A') && (c <= 'F')) + d = (c - 'A') + 10; + else + break; + v = (v << 4) | (uint32_t)d; + n++; + } + *out = v; + return n; +} + +/* Parse a trailing dotted quad. Returns 0 on success. The quad must run to + * the end of the string. */ +static inline int wolfIP_ip6_parse_v4tail(const char *p, uint8_t *out) +{ + int i; + + for (i = 0; i < 4; i++) { + uint32_t oct = 0; + int n = 0; + + while ((n < 3) && (p[n] >= '0') && (p[n] <= '9')) { + oct = (oct * 10u) + (uint32_t)(p[n] - '0'); + n++; + } + if ((n == 0) || (oct > 255)) + return -1; + out[i] = (uint8_t)oct; + p += n; + if (i < 3) { + if (*p != '.') + return -1; + p++; + } + } + return (*p == '\0') ? 0 : -1; +} + +/* Parse an IPv6 literal (RFC 4291 section 2.2), including the "::" run and + * the trailing dotted-quad form. Returns 0 on success, -1 on any malformed + * input. Strict: rejects a lone ':', more than one "::", too many or too few + * groups, and trailing garbage. */ +static inline int atoip6(const char *s, ip6 *out) +{ + uint8_t buf[16]; + uint8_t quad[4]; + const char *p; + int gap = -1; + int filled = 0; + int i; + + if ((s == NULL) || (out == NULL)) + return -1; + for (i = 0; i < 16; i++) + buf[i] = 0; + p = s; + if (p[0] == ':') { + if (p[1] != ':') + return -1; + p += 2; + gap = 0; + } + while (*p != '\0') { + uint32_t v; + int n; + + if (wolfIP_ip6_group_has_dot(p)) { + if (filled > 12) + return -1; + if (wolfIP_ip6_parse_v4tail(p, quad) != 0) + return -1; + for (i = 0; i < 4; i++) + buf[filled++] = quad[i]; + break; + } + n = wolfIP_ip6_parse_group(p, &v); + if (n == 0) + return -1; + p += n; + if (filled > 14) + return -1; + buf[filled++] = (uint8_t)(v >> 8); + buf[filled++] = (uint8_t)(v & 0xFFu); + if (*p == '\0') + break; + if (*p != ':') + return -1; + p++; + if (*p == ':') { + if (gap >= 0) + return -1; /* only one "::" is allowed */ + gap = filled; + p++; + if (*p == '\0') + break; + } + } + if (gap >= 0) { + int move = filled - gap; + + /* "::" must stand for at least one elided group. */ + if (filled >= 16) + return -1; + for (i = 0; i < move; i++) + buf[15 - i] = buf[filled - 1 - i]; + for (i = gap; i < (16 - move); i++) + buf[i] = 0; + } else if (filled != 16) { + return -1; + } + for (i = 0; i < 16; i++) + out->addr[i] = buf[i]; + return 0; +} + +/* Render an IPv6 address in the canonical form of RFC 5952: lowercase hex, + * no leading zeros within a group, the longest run of zero groups replaced + * by "::" (leftmost run on a tie, and never a run of only one group), and + * IPv4-mapped addresses printed with a dotted-quad tail. + * + * buf must hold at least WOLFIP_IP6_ADDRSTRLEN bytes. */ +static inline void ip6toa(const ip6 *a, char *buf) +{ + static const char hexd[] = "0123456789abcdef"; + uint16_t g[8]; + int best = -1; + int bestlen = 0; + int cur = -1; + int curlen = 0; + int i; + int j = 0; + + if (buf == NULL) + return; + if (a == NULL) { + buf[0] = '\0'; + return; + } + if (ip6_is_v4mapped(a)) { + /* RFC 5952 section 5 */ + buf[j++] = ':'; + buf[j++] = ':'; + buf[j++] = 'f'; + buf[j++] = 'f'; + buf[j++] = 'f'; + buf[j++] = 'f'; + buf[j++] = ':'; + for (i = 12; i < 16; i++) { + uint8_t o = a->addr[i]; + + if (o > 99) + buf[j++] = (char)('0' + (o / 100)); + if (o > 9) + buf[j++] = (char)('0' + ((o / 10) % 10)); + buf[j++] = (char)('0' + (o % 10)); + if (i < 15) + buf[j++] = '.'; + } + buf[j] = '\0'; + return; + } + for (i = 0; i < 8; i++) + g[i] = (uint16_t)(((uint16_t)a->addr[i * 2] << 8) | a->addr[(i * 2) + 1]); + for (i = 0; i < 8; i++) { + if (g[i] == 0) { + if (cur < 0) { + cur = i; + curlen = 0; + } + curlen++; + if (curlen > bestlen) { + best = cur; + bestlen = curlen; + } + } else { + cur = -1; + curlen = 0; + } + } + /* A single zero group is written out, not compressed. */ + if (bestlen < 2) { + best = -1; + bestlen = 0; + } + i = 0; + while (i < 8) { + uint16_t v; + int started = 0; + int s; + + if (i == best) { + buf[j++] = ':'; + if ((best + bestlen) >= 8) + buf[j++] = ':'; + i += bestlen; + continue; + } + if (i > 0) + buf[j++] = ':'; + v = g[i]; + for (s = 12; s >= 0; s -= 4) { + int nib = (v >> s) & 0xF; + + if ((nib != 0) || started || (s == 0)) { + buf[j++] = hexd[nib]; + started = 1; + } + } + i++; + } + buf[j] = '\0'; +} + +#endif /* !WOLFIP6_H */ diff --git a/wolfip6_config.h b/wolfip6_config.h new file mode 100644 index 00000000..99dad3b6 --- /dev/null +++ b/wolfip6_config.h @@ -0,0 +1,187 @@ +/* wolfip6_config.h + * + * Default sizing and feature switches for the IPv6 support. + * + * Copyright (C) 2026 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 + */ +#ifndef WOLFIP6_CONFIG_H +#define WOLFIP6_CONFIG_H + +/* Included by src/wolfip.c immediately after the configuration header, + * whichever one that is. + * + * These defaults deliberately do NOT live in config.h. Every board port + * ships its own config.h with the same WOLF_CONFIG_H guard and replaces the + * one at the top of the tree, so anything defined only there is invisible to + * a port build - and src/wolfip.c refers to WOLFIP_IF_CONF_MAX and + * WOLFIP_IFADDR_MAX outside any WOLFIP_IPV6 guard. Putting the defaults here + * means every configuration gets them, while a config.h that sets one first + * still wins: every macro below is #ifndef-guarded. + */ + +/* IPv6 support. Off by default; when off, all IPv6 code is removed by the + * preprocessor and the behavior/ABI of the stack is unchanged. + * + * Defined first because WOLFIP_IF_MULTICONF and the table sizes below key + * off it. */ +#ifndef WOLFIP_IPV6 +#define WOLFIP_IPV6 0 +#endif + +/* Multiple IP configurations (addresses) per interface. Off by default; when + * off, each interface carries exactly one configuration and the layout, + * behavior and ABI of the stack are unchanged. + * + * IPv6 cannot work with a single address per interface: a link-local address + * always coexists with any global address obtained by SLAAC or DHCPv6, so + * WOLFIP_IPV6 forces this feature on. A minimal IPv4-only build can leave it + * at 0 and keep the historical one-configuration-per-interface layout. + * + * WOLFIP_IF_CONF_MAX is a hard cap on the number of configurations that may + * be live on a *single* interface. WOLFIP_IFADDR_MAX sizes the flat pool + * shared by every interface, and so grows independently of + * WOLFIP_MAX_INTERFACES. */ +#ifndef WOLFIP_IF_MULTICONF +#if WOLFIP_IPV6 +#define WOLFIP_IF_MULTICONF 1 +#else +#define WOLFIP_IF_MULTICONF 0 +#endif +#endif + +#if WOLFIP_IPV6 && !WOLFIP_IF_MULTICONF +#error "WOLFIP_IPV6 requires WOLFIP_IF_MULTICONF" +#endif + +#ifndef WOLFIP_IF_CONF_MAX +#if WOLFIP_IF_MULTICONF +#define WOLFIP_IF_CONF_MAX 4 +#else +#define WOLFIP_IF_CONF_MAX 1 +#endif +#endif + +#if WOLFIP_IF_CONF_MAX < 1 +#error "WOLFIP_IF_CONF_MAX must be at least 1" +#endif + +#if !WOLFIP_IF_MULTICONF && (WOLFIP_IF_CONF_MAX != 1) +#error "WOLFIP_IF_CONF_MAX must be 1 unless WOLFIP_IF_MULTICONF is enabled" +#endif + +/* IPv6 needs at least a link-local address plus one other per interface. */ +#if WOLFIP_IPV6 && (WOLFIP_IF_CONF_MAX < 2) +#error "WOLFIP_IPV6 requires WOLFIP_IF_CONF_MAX >= 2 (link-local + one more)" +#endif + +#ifndef WOLFIP_IFADDR_MAX +#define WOLFIP_IFADDR_MAX (WOLFIP_MAX_INTERFACES * WOLFIP_IF_CONF_MAX) +#endif + +#if WOLFIP_IFADDR_MAX < WOLFIP_MAX_INTERFACES +#error "WOLFIP_IFADDR_MAX must provide at least one address per interface" +#endif + +/* WOLFIP_IPV6_PROFILE_LARGE raises every IPv6 table below in one switch, for + * networks larger than the small embedded default this stack targets. Each + * table can still be overridden individually. */ +#ifndef WOLFIP_IPV6_PROFILE_LARGE +#define WOLFIP_IPV6_PROFILE_LARGE 0 +#endif + +/* Addresses per interface (link-local, SLAAC/DHCPv6 globals, ULA). */ +#ifndef WOLFIP_IP6_ADDR_MAX +#if WOLFIP_IPV6_PROFILE_LARGE +#define WOLFIP_IP6_ADDR_MAX 8 +#else +#define WOLFIP_IP6_ADDR_MAX 4 +#endif +#endif + +#if WOLFIP_IPV6 && (WOLFIP_IP6_ADDR_MAX < 2) +#error "WOLFIP_IP6_ADDR_MAX must be at least 2 (link-local + one more)" +#endif + +/* Neighbor cache (RFC 4861 section 5.1). IPv6 counterpart of MAX_NEIGHBORS. */ +#ifndef WOLFIP_ND6_CACHE_SIZE +#if WOLFIP_IPV6_PROFILE_LARGE +#define WOLFIP_ND6_CACHE_SIZE 64 +#else +#define WOLFIP_ND6_CACHE_SIZE 16 +#endif +#endif + +/* On-link prefix list (RFC 4861 section 5.1). */ +#ifndef WOLFIP_ND6_PREFIX_MAX +#if WOLFIP_IPV6_PROFILE_LARGE +#define WOLFIP_ND6_PREFIX_MAX 16 +#else +#define WOLFIP_ND6_PREFIX_MAX 4 +#endif +#endif + +/* Default router list (RFC 4861 section 5.1). */ +#ifndef WOLFIP_ND6_ROUTER_MAX +#if WOLFIP_IPV6_PROFILE_LARGE +#define WOLFIP_ND6_ROUTER_MAX 4 +#else +#define WOLFIP_ND6_ROUTER_MAX 2 +#endif +#endif + +/* DHCPv6 client message buffer (RFC 8415). */ +#ifndef WOLFIP_DHCP6_BUF_SIZE +#if WOLFIP_IPV6_PROFILE_LARGE +#define WOLFIP_DHCP6_BUF_SIZE 1024 +#else +#define WOLFIP_DHCP6_BUF_SIZE 512 +#endif +#endif + +#if WOLFIP_IPV6 && !defined(ETHERNET) +/* Neighbor Discovery replaces ARP and is defined over link layers with + * addresses. A non-Ethernet (raw IP) build has no link-layer address to + * resolve, so only statically configured IPv6 peers would work. */ +#error "WOLFIP_IPV6 currently requires ETHERNET" +#endif + +/* Per-feature switches for IPv6 functionality that is not implemented yet. + * Each one is flipped to 1 by the phase that implements it, which also + * enables the matching requirement-derived tests. They are named (rather + * than plain #if 0) so the amount of pending work stays greppable. */ +#ifndef WOLFIP_IPV6_HAVE_EXTHDR +#define WOLFIP_IPV6_HAVE_EXTHDR 0 +#endif +#ifndef WOLFIP_IPV6_HAVE_ICMP6 +#define WOLFIP_IPV6_HAVE_ICMP6 0 +#endif +#ifndef WOLFIP_IPV6_HAVE_ND6 +#define WOLFIP_IPV6_HAVE_ND6 0 +#endif +#ifndef WOLFIP_IPV6_HAVE_SLAAC +#define WOLFIP_IPV6_HAVE_SLAAC 0 +#endif +#ifndef WOLFIP_IPV6_HAVE_DHCP6 +#define WOLFIP_IPV6_HAVE_DHCP6 0 +#endif +#ifndef WOLFIP_IPV6_HAVE_SOCKETS +#define WOLFIP_IPV6_HAVE_SOCKETS 0 +#endif + +#endif /* WOLFIP6_CONFIG_H */