IPv6 support - #163
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces first-phase IPv6 support for the wolfIP stack, along with the supporting configuration defaults, per-interface multi-address plumbing (required for IPv6 and also usable for IPv4 aliasing), and extensive unit + end-to-end Linux TAP-based tests/CI coverage.
Changes:
- Added an IPv6 address type (
ip6) with inline helpers (RFC 4291/5952) and IPv6 default configuration switches/sizing. - Integrated IPv6 receive/transmit plumbing and Neighbor Discovery state into
src/wolfip.c, plus new per-interface address list APIs. - Added new unit tests, Linux interop tests (ping + SLAAC), CI workflow, and tooling docs/scripts (including DLR integration surface and a radvd helper script).
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
wolfip6.h |
Adds ip6 type and inline helpers for IPv6 address operations and text conversion. |
wolfip6_config.h |
Provides IPv6/multiconf default sizing and feature gating defaults layered over config.h. |
wolfip.h |
Exposes IPv6/public APIs, adds switch/DLR integration vtable, and declares a generic L2 handler hook. |
tools/scripts/wolfip-radvd.sh |
Adds a helper script to run radvd for SLAAC testing on a TAP interface. |
src/wolfip.c |
Adds config selection hook, IPv6 ethertype demux, ND6 state embedding, timer sizing tweak, and per-interface address list implementation; includes src/wolfip6.c when enabled. |
src/wolfesp.c |
Replaces wc_ForceZero dependency with a local portable zeroization routine. |
src/test/unit/unit.c |
Wires new IPv6 + ifaddr unit tests into the suite and prints build config at startup. |
src/test/unit/unit_tests_ipv6_recv.c |
Adds IPv6 receive-path validation tests and L2 demux behavior tests. |
src/test/unit/unit_tests_ipv6_pending.c |
Adds requirement-derived “pending” test skeletons guarded by feature macros. |
src/test/unit/unit_tests_ipv6_icmp.c |
Adds ICMPv6 Echo request/reply behavioral tests. |
src/test/unit/unit_tests_ipv6_hdr.c |
Adds IPv6 header layout/accessor and pseudo-header checksum tests. |
src/test/unit/unit_tests_ip_arp_recv.c |
Strengthens IPv4 loopback martian tests and local-delivery-vs-forwarding coverage. |
src/test/unit/unit_shared.c |
Refactors UDP frame injection to allow testing via real ingress path vs bypassing IP checks. |
src/test/test_ipv6_slaac.c |
Adds end-to-end SLAAC + ND + DAD Linux TAP test (optionally using radvd). |
src/test/test_ipv6_ping.c |
Adds end-to-end ICMPv6 Echo Linux TAP/VDE test. |
README.md |
Documents new IPv6/ND/SLAAC capabilities and limitations. |
Makefile |
Adds IPv6 unit/asan/ubsan/leaksan targets, end-to-end test targets, and IPv6 coverage reporting. |
docs/dlr_integration.md |
Documents the intended DLR integration surface (L2 hook + switch ops vtable). |
CHANGELOG.md |
Notes IPv6 + multiconf + DLR integration surface in “Unreleased”. |
.github/workflows/ipv6.yml |
Adds CI job for IPv6 unit tests (sanitizers), builds, interop tests, and artifacts. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
WOLFIP_IPV6, WOLFIP_IF_MULTICONF, WOLFIP_IF_CONF_MAX, WOLFIP_IFADDR_MAX, WOLFIP_IP6_ADDR_MAX, the WOLFIP_ND6_* table sizes and WOLFIP_DHCP6_BUF_SIZE. All default off or to the smallest useful size. WOLFIP_IPV6 forces WOLFIP_IF_MULTICONF on: a link-local address always coexists with a global one, so IPv6 cannot work with one address per interface. WOLFIP_IPV6_PROFILE_LARGE raises every table at once. The WOLFIP_IPV6_HAVE_* macros mark features not implemented yet. Invariants use the existing "#if ... #error" idiom.
wolfip6.h: the 128-bit address type, well-known addresses, scope and type predicates, prefix operations, the RFC 2464 multicast and RFC 4291 modified EUI-64 mappings, and RFC 4291 / RFC 5952 text conversion. Stored as a byte array, not words: wolfIP targets big-endian and strict-alignment machines, and an IPv6 address sits at an odd offset behind the Ethernet header. Wrapped in a struct so it cannot decay to a pointer, which means comparing with ip6_cmp() rather than ==. No <string.h> dependency, so freestanding builds work. Well-known addresses are brace-initialiser macros because an unused static const in a header trips -Wunused-const-variable. Included unconditionally from wolfip.h: types and static inline functions only, so it adds no code when IPv6 is off and cannot change any struct layout. The tests are ungated for the same reason and run in the default build.
src/wolfip6.c: wire structures, the RFC 8200 section 8.1 pseudo-header and its checksum, ip6_recv() validation, ip6_output_add_header(), and the ethertype and MAC demux. Included textually into wolfip.c under WOLFIP_IPV6, as src/wolfesp.c already is, because it needs struct wolfIP and the static checksum, Ethernet and link-layer helpers. The IPv4 structures embed the network header by value at a fixed 34-byte offset, so the IPv6 transport structures are parallel definitions rather than a reuse. The pseudo-header likewise gets its own union and checksum: 40 bytes against 12. ip6_recv() returns a distinct code per rejection reason so tests can assert why a frame was refused. The hop limit is deliberately not checked. RFC 8200 section 3 has it tested by forwarding nodes only, so a destination host must accept a packet addressed to it at hop limit zero. IPv4-mapped and IPv4-compatible addresses are dropped in either address field (RFC 4291 sections 2.5.5.1 and 2.5.5.2): they exist only inside the socket API, and wolfIP is to present mapped addresses to dual-stack sockets. Extension headers are recognised and refused rather than walked; chain walking is a denial-of-service surface. Adds the unit-ipv6 target with sanitizer and coverage variants. WOLFIP_IPV6 must be passed on the command line, because wolfip.c includes wolfip.h before config.h.
Sixty tests covering ICMPv6, Neighbor Discovery, SLAAC, DAD, the DHCPv6 client, extension headers and AF_INET6 sockets. None of it is implemented yet, so each group is guarded by its WOLFIP_IPV6_HAVE_* macro and none run. They fix the API shape as well as the expected behaviour: the names and signatures they use are the contract the implementation has to meet. Emphasis is on requirements that writing the happy path first would miss: the NDP hop-limit-255 rule, an NDP option length of zero looping the parser, the SLAAC two-hour rule, ICMPv6 error suppression, that framing follows the destination address family rather than the socket domain, and that IPV6_V6ONLY is honoured rather than swallowed by the setsockopt default. Named macros rather than "#if 0" so the outstanding work is greppable; make unit-ipv6-pending-count reports it. Test names carry no requirement identifiers: the requirement documents are internal and the mapping is kept off-tree, keyed by function name.
Runs the IPv6 unit suite with ASan and UBSan, rebuilds the IPv4-only configuration, builds the library with WOLFIP_IPV6=1, and reports the pending requirement test count. The addressing tests are not covered here: wolfip6.h is included unconditionally, so they already run in the default make unit in linux.yml. Coverage is reported, not gated. IPv6 still carries stubs, so 100% function coverage is not achievable; the enforced gate stays on src/wolfip.c in wolfip-autocov.yml.
wolfIP does not implement DLR. This declares what a Device Level Ring implementation needs from the stack and from the driver so one can be added without changing the core. wolfIP_register_l2_handler() generalises the EAPOL hook, which is hardwired to ethertype 0x888E: a module claims an ethertype and also declares the destination MACs it wants delivered, since the ingress path filters on MAC before dispatch. Unlike the EAPOL hook the handler sees the whole frame, header included, because a ring protocol needs the source MAC. struct wolfIP_switch_ops is the driver vtable: port count, per-port link state and change notification, per-port block and unblock, MAC table flush, per-port transmit and ingress port reporting. Appended last in struct wolfIP_ll_dev, after wifi_ops, so no existing member offset shifts. docs/dlr_integration.md records the contract, including that wolfIP's poll loop is millisecond-granular while DLR beacons are sub-millisecond, so beacons must come from a hardware timer. It also notes that the ODVA specification is paywalled and the ethertype and multicast MAC range quoted there must be confirmed before use. ISO 11898 and CAN FD, which appeared in an early draft, are not applicable: ISO 11898 is the CAN bus standard.
Protocol table rows for IPv6 and IPv6 addressing, marked in progress and naming what is not implemented, so the table does not overstate what the stack does. Links the DLR integration guide.
Implements WOLFIP_IF_MULTICONF. IPv6 requires it, since a link-local address always coexists with a global one, and the same machinery gives IPv4 address aliasing with IPv6 off. struct ipconf is reached by more than fifty files, every board port among them, so it is not replaced. The list is additive: ipconf holds the primary IPv4 address of an interface and a flat shared pool holds the rest, IPv4 aliases and every IPv6 address. The primary is never copied into the pool, so the two cannot drift. Index 0 of an interface's IPv4 list is the primary; higher indices are aliases in insertion order. WOLFIP_IF_CONF_MAX caps the total per interface and both families draw on it. With the feature off the pool is preprocessed out and an interface holds one address; struct wolfIP is byte-identical to before at 483664 bytes. Adding the first IPv4 address of an interface sets the primary, so a single-address build is usable through this API alone. Deleting the primary promotes the first alias rather than leaving the interface without one. wolfIP_if_for_local_ip() now consults the alias list; without that, binding a socket to an alias resolved to the primary interface. A unit-multiconf target exercises IPv4 aliasing without IPv6.
…nterface master now fixes this in udp_try_recv itself (F-11428, F-10280, F-11438), and better: it matches the specific-bind case on bound_local_ip too. What remains here is the multi-address coverage, which master cannot exercise.
test_ip_recv_loopback_dst_on_non_loopback_dropped and its _src_ sibling never called ip_recv: inject_udp_datagram() hands a frame straight to udp_try_recv(), skipping header validation, the checksum and the martian filter. They passed because the socket had local_ip = IPADDR_ANY, which the UDP demux does not match. With ip_recv's 127/8 filter disabled the originals still passed; after this change the same mutation fails both. build_udp_frame() is split out of inject_udp_datagram(), and recv_udp_datagram() delivers the same frame through wolfIP_recv_ex(). inject_udp_datagram() keeps its behaviour and documents what it skips. Both tests gained a positive control on a separate port, and the socket under test now accepts exactly the address being filtered, so the negative assertion means something. test_ip_recv_dest_matches_secondary_iface_ip_is_local uses the real path too, with a control proving the fixture can emit a frame when forwarding is expected. It asserts the observable contract rather than one internal decision: ip_recv's is_local check and wolfIP_forward_interface() declining our own address both produce the right outcome, so defeating either alone leaves the behaviour correct.
Answering a ping needs neither sockets nor Neighbor Discovery, because the reply goes to the source MAC of the request. It also gives ip6_output_add_header() its first production caller. icmp6_input() follows icmp_input() above it in wolfip.c: same order of length checks, then the checksum, then one arm per type, with the reply built in place and the identifier, sequence number and payload left untouched. wolfIP_if_for_local_ip6() matches wolfIP_if_for_local_ip(), including its weak end-system model search across all interfaces; link- local zones per RFC 4007 are noted for when NDP lands. Only requests addressed to one of our own addresses are answered, as in the ICMPv4 arm: otherwise 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. Multicast destinations are declined; they need a unicast source per RFC 4443 section 4.2. The checksum covers the IPv6 pseudo-header, unlike ICMPv4. There is no wolfIP_filter_notify_icmp() call because the filter has no IPv6 hooks.
Brings wolfIP up on a TAP device, or a VDE switch with BUILD_VDE=1, forms the link-local address from the interface MAC per RFC 4862 section 5.3, and answers pings from the host. Without arguments it prints the addresses and the commands to run, then polls. With --selftest it runs ping(8) and exits non-zero if the replies do not arrive. Both need root. Neighbor Discovery is not implemented, so the host cannot resolve our MAC and the mapping is installed with ip -6 neigh replace; --selftest does this. It becomes unnecessary once NDP lands. Needs its own build/ipv6/wolfip.o because WOLFIP_IPV6 has to be visible to wolfip.h, which wolfip.c includes before config.h.
RFC 4861 address resolution and router discovery, RFC 4862 section 5.4
duplicate address detection, and the host routing that follows from them.
No sockets.
nd6_lookup(), nd6_store_neighbor() and nd6_neighbor_index() mirror the
arp_* helpers, with the same linear scan keyed on {address, interface}.
The reachability state machine and the router flag are new. One
divergence: arp_store_neighbor() refuses when the table is full, this
evicts the oldest, so a burst of scan traffic cannot lock out every real
neighbour.
wolfIP_ipv6_start() is the counterpart of dhcp_client_init(): it forms
the link-local address, probes it and solicits routers, continuing from
wolfIP_poll(). An address is TENTATIVE until its probe completes.
One periodic tick drives duplicate address detection, router solicitation
retries and every cache and lifetime expiry, rather than a timer per
address. MAX_TIMERS is MAX_TCPSOCKETS * 3 and already carries TCP, DHCP
and DNS; it gains four slots under WOLFIP_IPV6.
Routing is the host model: an on-link prefix list and a default router
list, with nd6_select_nexthop() as the counterpart of
wolfIP_select_nexthop_ex(). The static route table is not generalised to
both families; that only pays off with forwarding and sockets.
Router Advertisement handling is limited to the default router and Prefix
Information options. Managed/Other, MTU and timer overrides are parsed
past.
Validation, each with a test: every ND message must arrive with hop limit
255 (RFC 4861 sections 6.1 and 7.1), a Router Advertisement must have a
link-local source (section 6.1.2), an advertised link-local prefix is
ignored (RFC 4862 section 5.5.3), an option length of zero is rejected
because it stops the option walk making progress, and a tentative address
is neither defended nor used.
RFC 4861 section 7.2.5: without the Override flag a differing link-layer
address does not replace the one held, but a REACHABLE entry still drops
to STALE so reachability is re-verified.
Twenty-two tests: joining from cold, advertisements to accept and refuse,
duplicate address detection in both outcomes including a simultaneous
probe, router advertisements, and a statically assigned ULA alongside
them. Twenty requirement stubs retired.
test_ipv6_ping.c configures its addresses by hand and never calls wolfIP_ipv6_start(), and the ND and SLAAC tests use forged frames, so nothing showed SLAAC working against a real stack. test_ipv6_slaac.c configures nothing on the wolfIP side. It starts IPv6, lets the link-local address form and pass duplicate address detection, waits for a Router Advertisement, forms a global address from the advertised prefix, and has Linux ping it. The host does not know wolfIP's link-layer address, so it sends a Neighbor Solicitation first: address resolution happens for real in both directions, with no static neighbour entry. The advertisement is injected over an AF_PACKET socket on the host end of the TAP by default, which is deterministic but is a frame this repository wrote. With --with-radvd, tools/scripts/wolfip-radvd.sh spawns radvd against a generated config: radvd does not autostart, so the script runs it directly, enables forwarding on the one interface because radvd refuses to advertise otherwise, and restores it on stop. --dad-collision claims wolfIP's address from the host while it is still tentative and asserts it is abandoned. Both end-to-end tests spawn tcpdump as the IPv4 interop tests do, writing ipv6-slaac.pcap and ipv6-ping.pcap. Capture uses -U so an aborted run still leaves a readable file, is stopped on exit, and is skipped with WOLFIP_NO_PCAP. The workflow gains radvd, tcpdump and iputils-ping, runs the four end-to-end tests under sudo and uploads the captures.
The "IPv4-only build is unchanged" step ran a bare make, which also builds the ESP, wolfGuard and supplicant examples. Those need a wolfSSL built from source with the right options, which is why linux.yml installs the nightly snapshot; against the packaged libwolfssl-dev src/wolfesp.c fails to compile. Both affected steps now build libwolfip.so, which is what they assert: that the library and the unit tests are unaffected with and without WOLFIP_IPV6. The examples stay covered by linux.yml.
src/wolfesp.c called wc_ForceZero() without including its header, and no include fixes it: wc_ForceZero() is a recent addition to memory.h and is absent from wolfSSL 5.6.6, which is what Ubuntu packages. The other spelling, ForceZero(), is the inline helper in wolfcrypt/src/misc.c, which libwolfssl-dev does not ship - misc.h only declares it under NO_INLINE - so a consumer of an installed wolfSSL can rely on neither. The build therefore succeeded or failed according to which wolfSSL was installed: it passes in linux.yml, which builds 5.9.x from source, and failed against the packaged 5.6.6. wolfIP_esp_forcezero() writes through a volatile pointer, which is what stops the compiler eliding the store and is what both wolfSSL helpers do. No version checks and no wolfSSL dependency. The IPv6 workflow gains a step building build/esp/wolfip.o, the object that failed. 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 in CI that catches this.
Four defects in the Neighbor Discovery timer handling. nd6_tick_cb() re-armed with the return of timers_binheap_insert() without checking it. That returns 0 when the heap is full and NO_TIMER is 0, so a momentarily full heap left the tick disarmed and stopped all Neighbor Discovery permanently: duplicate address detection never completing, router solicitation never retrying, nothing expiring, and no way to notice. With four TCP sockets able to hold three timers each, plus DHCP, DNS and IGMP, exhausting the sixteen slots is not far-fetched. nd6_poll(), called from wolfIP_poll(), now arms the tick whenever there is work and no timer running. That is the recovery path for a failed insert and the wake-up after an idle period. The tick also re-armed unconditionally, so once started it ran forever, waking every 100ms to scan five tables even with every interface stopped and nothing configured. It now re-arms only while nd6_has_work() holds: an interface started, a solicitation outstanding, an address tentative, a neighbour mid-resolution or ageing, or a prefix or router with a finite lifetime. The predicate is deliberately conservative, so a quiescent but populated cache keeps ticking rather than risking a stop with work queued. wolfIP_ipv6_stop() halts what the tick drives on an interface and drops anything still tentative, since detection never completed and the address was never ours. Addresses that had already passed are kept and still answered for. With no interface left running the tick releases its slot. nd6.tick_due and nd6.last_ns were declared and never used. Both removed. last_ns was to be the per-interface solicitation throttle mirroring arp.last_arp, but the only caller of nd6_send_ns() is duplicate address detection, which dad_due already paces; a comment records that a throttle is needed once address resolution is driven from the transmit path. nd6_arm_tick() treats a non-zero id as already running, so the tick has to clear the recorded id on entry: the heap has already popped the entry by then and the id is stale. Six tests for the lifecycle, which nothing covered before: that the tick is one slot for the whole stack rather than one per interface, that a full heap is recovered from, that stop releases the slot and drops a tentative address, and that a stopped interface restarts cleanly. Cancellation is lazy - timer_binheap_cancel() tombstones with expires = 0 and the slot is reclaimed by the drain in timers_binheap_insert() - so the tests assert the tick is disarmed and the slot reused, not that heap->size drops.
nd6_arm_tick() returned early when nd6.tick_timer was non-zero, which made that field double as an "am I already running" flag and left correctness depending on every path that pops or cancels remembering to clear it. It now cancels any live timer and inserts unconditionally, in the shape of dhcp_schedule_timer_at(): the id is simply overwritten and there is no flag to keep in sync. Calling it twice replaces the timer rather than leaving a stray entry in the heap. The tick still clears the recorded id on entry. The heap has already popped that entry, so the id is stale; clearing keeps the field truthful for the rest of the pass, gives nd6_arm_tick() nothing to cancel, and lets nd6_poll() see the tick as unarmed when a pass decides not to re-arm. test_nd_uses_one_timer_slot_for_the_whole_stack now counts live tick entries after a repeated start, rather than heap size: cancellation tombstones with expires = 0 and the slot is reclaimed later by the drain in timers_binheap_insert(), so size alone does not show whether two ticks are running.
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. The IPv6 sizing and feature macros were added only to the latter, so a port build never saw them, and src/wolfip.c refers to WOLFIP_IF_CONF_MAX and WOLFIP_IFADDR_MAX outside any WOLFIP_IPV6 guard: src/wolfip.c:6071: error: 'WOLFIP_IF_CONF_MAX' undeclared That broke every embedded port, the clang matrix and macOS. The IPv4 default build was unaffected, which is why it went unnoticed: the jobs that build ports only run on the pull request. The block now lives in wolfip6_config.h, included by src/wolfip.c immediately after the configuration header, so every configuration gets the defaults. Each macro stays #ifndef-guarded, so a config.h that sets one first still wins. wolfip.c also honours WOLFIP_CONFIG, naming an alternative configuration header, so a port or a test build can select one without editing the tree: -DWOLFIP_CONFIG='"myconfig.h"' Adds a CI step compiling wolfip.c against every src/port/*/config.h. All fourteen pass. That is cheap and catches this class of breakage in the IPv6 workflow rather than leaving it to the embedded jobs.
…advd helper Five review findings, all correct. README and CHANGELOG still described ICMPv6, Neighbor Discovery and SLAAC as unimplemented. They were written before those landed in this branch and understated it. Both now say what is implemented - Echo, address resolution, router discovery, duplicate address detection, SLAAC address formation - and what is not: AF_INET6 sockets, ICMPv6 error messages, extension headers, fragmentation, MLD and DHCPv6. README gains a row per protocol rather than one row hedged with a parenthesis. test_ipv6_ping.c installed a static neighbour entry and its header explained at length why one was needed. Neighbor Discovery now answers the host's solicitation, so the entry is unnecessary and the explanation was misleading. Both removed. The header now says what the test is for - ICMPv6 Echo with addresses configured directly - and points at test_ipv6_slaac.c for the case where nothing is configured. wolfip-radvd.sh forced net.ipv6.conf.<iface>.forwarding to 1 on start and to 0 on stop, so running it against an interface that already had forwarding enabled would silently turn it off, and the comment claiming it left the interface as it found it was wrong on two counts: it also left behind the address that start had added. It now saves the prior forwarding value and restores it, and removes the address.
The FreeBSD and macOS jobs run 1454 checks where Linux runs 1439. The difference is exactly the fifteen tests gated on WOLFIP_IF_MULTICONF, so those platforms are compiling that macro as 1 while Linux computes 0, and three of the tests fail there as a result. The macro is not reproducible locally: gcc and clang both compute WOLFIP_IF_MULTICONF=0 from config.h plus wolfip6_config.h, and the ordering inside that header is correct, with WOLFIP_IPV6 defined before the block that keys off it. Neither workflow passes extra flags. Rather than guess at a fix, the unit binary now prints the configuration it was built with, so the next run says which macro differs and by how much instead of leaving it to be inferred from a check count.
frame is a pointer parameter, so sizeof(frame) cleared eight bytes instead of the frame. The rest kept stack contents, and a non-zero flags_fo made ip_recv() drop the packet as a fragment. Linux hands back zeroed stack pages so it passed there; FreeBSD and macOS did not. Zero the frame being built, and set tos, id and flags_fo explicitly rather than relying on the caller's buffer.
Eight defects in the Neighbor Discovery and ICMPv6 receive paths:
- Tentative addresses answered ICMPv6 Echo. wolfIP_if_for_local_ip6() had
no state check, so an address still under duplicate address detection
was treated as ours (RFC 4862 section 5.4.5).
- Link-local addresses were matched on any interface. They are scoped to
the link they arrived on (RFC 4007 section 5).
- The same link-local address was rejected on a second interface. Only
the pair {address, zone} has to be unique.
- Neighbor Advertisements from the unspecified address updated the cache.
RFC 4861 section 7.1.2 requires a unicast source.
- A solicitation from the unspecified address was accepted without
checking it was sent to the target's solicited-node group and carried no
source link-layer address option, so a malformed one could invalidate a
tentative address (RFC 4861 section 7.1.1).
- Router Advertisement options took effect before the option area was
validated, leaving a router installed when a later option was malformed.
Framing is now checked in a first pass.
- Ethernet link-layer options were accepted with any length. RFC 2464
section 6 requires length 1.
- nd6_has_work() treated a started interface as work, so the tick never
quiesced. nd6_poll() re-arms when work appears, so it does not need to.
Six regression tests, and the malformed Router Advertisement test now
covers the option area rather than a single option.
The comment claimed on-the-wire validity; the predicate only excludes multicast and the unspecified address, so ::1 passes. Tightening it would be wrong - ::1 is unicast. ip6_recv() already enforces the wire rules separately. Comment only.
unit-ipv6-pending-count grepped config.h, but the WOLFIP_IPV6_HAVE_* defaults moved to wolfip6_config.h, so every feature reported pending. It now reads the defaults there, prefers config.h when it overrides, and prints which file it used.
Declared in wolfip.h, defined nowhere: calling it was a link error. Now defined and returning -WOLFIP_ENOSYS, so a consumer links and finds out at run time. ENOSYS rather than EINVAL, which would invite a retry.
A tun/utun link has no ethertype and no link-layer address. Demux on the version nibble, draw the interface identifier randomly or take it from wolfIP_ipv6_set_iid(), and omit the ND link-layer options. Unit tests plus a tun test against Linux.
wolfip.h declares the IPv6 entry points unconditionally - it is included before config.h - while src/wolfip6.c compiles only under WOLFIP_IPV6, so consumers met undefined symbols. They now return -WOLFIP_ENOSYS. A link-time test covers it, which the unit build cannot.
RFC 3493 dual stack. A socket carries domain, deciding how addresses are reported, and peer_is_v6, deciding framing; a v4-mapped destination keeps using the IPv4 path. IPV6_V6ONLY is stored and enforced rather than merely accepted. No data paths yet.
sendto/recvfrom/connect, the demux and the transmit queue. The header is written at enqueue time because its checksum covers the addresses; oversize is refused rather than truncated; a zero checksum is illegal. wolfip6.c function coverage is now an enforced gate, at 100%.
The state machine touches addresses in eight places, so those read a struct ip_flow and it stays one copy. Segments are built IPv4-shaped and promoted at send time. tcp_recv, tcp_ack and the listener match gained explicit-length variants: the aliased ip header holds nothing.
One error generator carrying the RFC 4443 s2.4 suppression rules and the 1280-byte quoting bound. Port unreachable and parameter problem are wired; packet-too-big and time-exceeded await forwarding. Errors reach sockets, unknown informational messages do not.
Both ports are pass-through except getaddrinfo(), which gained IPv6 literals, passive lookups and AI_V4MAPPED. Testing them found two stack bugs: recvfrom reported a sockaddr_in on a dual-stack socket, and a v4-mapped sendto was re-parsed at IPv4 offsets.
autocov enforces 100% function coverage of src/wolfip.c and the stub added with the declaration had no caller in the unit build. The test also checks it leaves the EAPOL handler alone, since the two sit next to each other.
src/wolfip6.c is #included into wolfip.c and never compiled alone, but cppcheck parses it alone, where IP6_HEADER_LEN_PUB is undefined and so evaluates to 0. Guard on the macro existing. wolfip.c cannot build without it, so nothing is lost.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-bugs, wolfip-src
Findings: 4
3 finding(s) posted as inline comments (see file-level comments below)
Required changes (1)
tcp_listen_ack_matches_child_socket() compares IPv4 addresses for IPv6 flows
File: src/wolfip.c:5645
Function: tcp_listen_ack_matches_child_socket
Category: Logic errors
The function matches on flow->dst/flow->src, which tcp6_input() leaves zeroed for IPv6, and on t->local_ip/t->remote_ip, which sock_bind6() and the accept clone leave at IPADDR_ANY. Any IPv6 child socket with matching ports therefore matches regardless of peer, suppressing the RFC 9293 RST for a stray ACK on an IPv6 listener. Unlike the sibling tsocket_flow_* helpers, it has no flow->is_v6 branch.
Related known finding #8514 (similar but distinct): Both concern TCP listener/accepted-child handling and can produce incorrect reset behavior around child connections, but this candidate matches IPv6 peers using unset IPv4 fields in tcp_listen_ack_matches_child_socket, while #8514 changes a cloned child’s SYN-ACK sequence state. They occur in different operations, have different root causes, and need separate patches.
Recommendation: Add an is_v6 branch comparing t->local_ip6/t->remote_ip6 against flow->dst6/flow->src6 with ip6_cmp().
Referenced code: src/wolfip.c:5645-5646 (2 lines)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
MAX_PACKET_SZ was cut to 4352 without lowering DEFAULT_MAX_PACKET_SZ, so CtxInit() took wolfSSH's 32768 default, which wolfSSH now asserts on. Not IPv6; the config predates this branch. Verified on m33mu: both builds, five hardware tests pass.
Three in-loop reset sites called the IPv4 builder unconditionally. For an IPv6 flow the segment pointer is aliased past the IPv6 header, so the reset was built from address bytes the sender chose. All four now dispatch on flow->is_v6.
A v6only :: bind took the IPv4 path, so it reserved the IPv4 port and received IPv4 datagrams. It now binds natively, tracked by bound_v6. AF_INET6 sockets are also excluded from icmp_try_recv: ICMPv6 is a different protocol.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-bugs, wolfip-src
Findings: 3
3 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
The mapped arm of connect() built a sockaddr_in and recursed without clearing peer_is_v6 or remote_ip6, so a UDP socket re-connected from an IPv6 peer still took the IPv6 transmit path and addressed the old peer. sendto() already cleared both.
udp6_try_recv() treated "this socket has no IPv6 peer" as a match, so a dual-stack socket connected to a v4-mapped address accepted IPv6 datagrams from any source on the right port, bypassing the peer filter its IPv4 arm enforces.
The ingress filter admits every 33:33:* MAC so Neighbor Discovery works. Without a destination check, any on-link host could inject payload into a socket bound to :: by aiming it at a group. Multicast needs membership we lack.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-bugs, wolfip-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
| *if_idx = wolfIP_if_for_local_ip6(S, t->if_idx, &flow->dst6, found); | ||
| if (*found) { | ||
| ip6_copy(&t->local_ip6, &flow->dst6); | ||
| t->peer_is_v6 = 1; |
There was a problem hiding this comment.
Listener keeps peer_is_v6/remote_ip6 after reverting to LISTEN · Logic errors
An incoming IPv6 SYN sets peer_is_v6/remote_ip6 on the listener itself, but tcp_listener_revert_to_listen() (src/wolfip.c:4339) clears only remote_ip/dst_port. A dual-stack AF_INET6 listener stays flagged IPv6, so the next IPv4 connection's SYN-ACK is framed as IPv6 and sent to the previous IPv6 peer.
Related known finding #11427 (similar but distinct): Both concern incomplete state reset when reusing a TCP listener, but this candidate preserves IPv6 peer/address state and misframes a later IPv4 SYN-ACK, while #11427 preserves TCP timestamp/PAWS state. The faulting fields and resulting protocol operations differ, requiring distinct reset coverage.
Fix: Restore peer_is_v6, remote_ip6 and local_ip6 from the socket's bound v6 state in tcp_listener_revert_to_listen() and in the SYN_RCVD RST fallback.
|
|
||
| if (d6 && ((*(const uint8_t *)(ts->rxmem + d6->pos + sizeof(*d6) + | ||
| ETH_HEADER_LEN) >> 4) == 6)) | ||
| return udp6_recvfrom(s, ts, buf, len, src_addr, addrlen); |
There was a problem hiding this comment.
IPv6 recvfrom dispatch bypasses the src_addr/addrlen guard · Unsafe memory operations
The new IPv6 dispatch to udp6_recvfrom() (and icmp6_recvfrom() at line 8405) runs before the if (sin && !addrlen) return -WOLFIP_EINVAL; guard. With src_addr non-NULL and addrlen NULL, sock_addr_from_ip6() writes a 28-byte wolfIP_sockaddr_in6 into the caller's buffer with no size known; the IPv4 path rejects that call.
Related known finding #325 (similar but distinct): Both involve wolfIP_sock_recvfrom, but #325 concerns post-receive readable-event signaling after FIFO consumption, while this concerns pre-dispatch validation of src_addr/addrlen before IPv6 address copying. The faulting operations and root causes differ, and moving the argument guard would not fix #325.
Fix: Move the sin && !addrlen rejection above the IPv6 dispatch in both the UDP and ICMP branches.
No description provided.