Repository navigation
IPv6 support - #163
IPv6 support#163danielinux wants to merge 65 commits into
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.
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.
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.
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.
b4193bf to
d3098ed
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-src, wolfip-bugs
Findings: 8
8 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-src, wolfip-bugs
Findings: 8
8 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-src, wolfip-bugs
Findings: 5
5 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
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.
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.
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.
accept()'s ESTABLISHED clone path filled the peer as a hard-coded IPv4 sockaddr, so an IPv6 connection came back as AF_INET. Report it via tsocket_getname and size *addrlen by the socket's domain, as the SYN_RCVD path already does. The three listener tests now finish the handshake before accept(), since master parks the SYN_RCVD clone and hands out only ESTABLISHED connections.
A wildcard UDP socket learns its peer from the first datagram, and sock_addr_from_ip6() stamps sin6_scope_id from the interface the packet arrived on. udp6_recvfrom() was passing the socket's bound interface (t->if_idx, the primary for a wildcard socket) instead, so a link-local peer on a non-primary interface came back with the wrong scope. Carry the ingress index in the high bits of pkt_desc.flags (bits 8-15) so the descriptor stays 16 bytes: growing it broke the FIFO capacity tests and added 4 bytes per entry to every queue, IPv4 included. The low 5 bits are the PKT_FLAG_* lifecycle bits and are untouched. Verification: make unit-ipv6 && ./build/test/unit -> 1956 checks, 0 failures make unit && ./build/test/unit -> 1762 checks, 0 failures (IPv4) make ipv6-bsd-test freertos-ipv6-test -> pass
test_freertos_ipv6.c #includes bsd_socket.c and mocks the wolfIP side, the way test_freertos_close_last_ack.c does, but it was missing the mocks for wolfIP_set_wake_cb() and wolfIP_sock_abort() that the port calls in its init and close paths. The test-freertos-ipv6 target failed to link with undefined references to both. Verification: make build/test-freertos-ipv6 -> links clean make freertos-ipv6-test -> pass
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #163
Scan targets checked: wolfip-src, wolfip-bugs
Coverage: 2 of 4 in-scope changed file(s) opened by the reviewer; not opened: src/port/posix/bsd_socket.c, src/wolfesp.c
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.
Review tier: Lite
| * let an explicit bind land on top of a live connection's local | ||
| * endpoint. Such a socket's address is in local_ip6, the bound | ||
| * one's in bound_local_ip6. */ | ||
| if (!tk->bound_v6 && !TSOCKET_IS_V6(tk)) |
There was a problem hiding this comment.
Dual-stack wildcard bind and IPv6 binds never conflict on the same port · Logic errors
A dual-stack [::]:P bind goes through the IPv4 path, which skips bound_v6 sockets. It leaves peer_is_v6=0, bound_v6=0, so bind_port_in_use6 skips it too. In either order, a v6only or address-specific AF_INET6 bind on the same port succeeds, and udp6_try_recv then delivers every IPv6 datagram to both sockets.
Suggested fix: Treat AF_INET6 sockets that are not v6only and are bound through the IPv4 path as holders of :: in bind_port_in_use6, and do the reverse check when a dual-stack wildcard bind takes the IPv4 path.
Basis: POSIX bind(): [EADDRINUSE] The specified address is already in use.
Related known findings (similar but distinct; listed for context, not part of this finding)
- F-14492: that one is the IPv4 check comparing the resolved
local_ip. This one is the IPv4/IPv6 checks each ignoring the dual-stack socket the other family created.
| return -1; | ||
| if (ip6_addr_is_wire_v6(&dst6)) { | ||
| ip6_set_zone(s, ts, &dst6, scope6); | ||
| ts->peer_is_v6 = 1; |
There was a problem hiding this comment.
AF_INET6 UDP sendto() overwrites a connected socket's peer, even when the send fails · State machine flaws
With a dest_addr, the IPv6 branch always writes remote_ip6/dst_port/peer_is_v6, even on a connected socket and before udp6_sendto checks anything. A sendto() to another host, or one that fails because the port is 0, re-targets the connection and narrows the udp6_try_recv peer filter. The IPv4 path only does this when !connected.
Suggested fix: Build the IPv6 destination in local variables, send with them, and write them to the socket only when !ts->sock.udp.connected and the send succeeds.
Basis: POSIX sendto(): if the socket is connection-mode, dest_addr shall be ignored; nothing in sendto() allows it to change the socket's connected peer.
Related known findings (similar but distinct; listed for context, not part of this finding)
- F-13164: that fix covers the IPv4 sendto path; this is the new AF_INET6 branch, which reintroduces the bug.
| /* RFC 4443 section 3.1 code 4: nothing holds the port. The | ||
| * destination was checked to be ours above, and | ||
| * icmp6_send_error() applies the rest of the suppression rules. */ | ||
| icmp6_send_error(s, if_idx, &udp->ip6, frame_len, |
There was a problem hiding this comment.
ICMPv6 error messages are generated without any rate limiting · Protocol violations
icmp6_send_error sends a Destination Unreachable for every unmatched UDP datagram, and a Parameter Problem for every unknown next header in ip6_recv, with no rate limit. RFC 4443 makes rate limiting mandatory, and without it a sender can make the stack emit one error per packet it sends.
Suggested fix: Add a token-bucket limiter inside icmp6_send_error, driven by s->last_tick, so every ICMPv6 error originated by the stack is bounded.
Basis: RFC 4443 section 2.4(f): an IPv6 node MUST limit the rate of ICMPv6 error messages it originates, e.g. with a token bucket.
Related known findings (similar but distinct; listed for context, not part of this finding)
- F-2726: that one is the IPv4 ICMP Port Unreachable path; this is the new ICMPv6 generator in wolfip6.c.
No description provided.