Skip to content

IPv6 support - #163

Merged
gasbytes merged 81 commits into
wolfSSL:masterfrom
danielinux:wolfIPv6
Oct 16, 2026
Merged

gasbytes merged 81 commits into
wolfSSL:masterfrom
danielinux:wolfIPv6

Conversation

@danielinux

Copy link
Copy Markdown
Member

No description provided.

Copilot AI lite review requested due to automatic review settings August 24, 2026 07:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread wolfip6.h Outdated
Comment thread Makefile Outdated
Comment thread wolfip.h

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/wolfip6.c
Comment thread src/wolfip6.c Outdated
Comment thread src/wolfip6.c
Comment thread src/wolfip6.c Outdated
Comment thread src/wolfip.c
Comment thread src/wolfip6.c Outdated
Comment thread src/wolfip6.c Outdated
Comment thread src/wolfip.c Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/wolfip.c
Comment thread src/wolfip6.c Outdated
Comment thread src/wolfip6.c Outdated
Comment thread src/wolfip6.c
Comment thread src/wolfip.c Outdated
Comment thread src/wolfip.c Outdated
Comment thread src/wolfip6.c
Comment thread src/wolfip6.c Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/wolfip.c
Comment thread src/wolfip.c Outdated
Comment thread src/wolfip6.c
Comment thread src/wolfip6.c
Comment thread src/wolfip6.c
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.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #163

Scan targets checked: wolfip-src, wolfip-bugs
Coverage: 1 of 1 in-scope changed file(s) opened by the reviewer

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Lite

@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed stale reviews from themself October 8, 2026 19:05

Fenrir's latest completed scan found no issues; clearing the prior automated change request.

@danielinux danielinux assigned gasbytes and unassigned danielinux Oct 12, 2026

@gasbytes gasbytes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good!
left some comments, mostly edge cases

Comment thread src/wolfip.c Outdated
Comment thread src/wolfip6.c
Comment thread src/wolfip6.c
Comment thread src/wolfip.c Outdated
Comment thread src/wolfip6.c
An unconnected ICMPv6 socket that pinged a second host lost the first
host's replies: sendto() saved the destination as the socket's peer, and
the receive path filtered incoming replies against that peer. A ping to A
then B left the peer pinned to B, so A's reply was dropped.

Make the sendto destination per-packet, matching the IPv4 contract where
the sendto address is "this packet only" and the peer is set by connect()
alone. icmp6_sendto() now takes the destination (NULL when connected); the
v6 ICMP sendto branch uses it and no longer writes remote_ip6 / peer_is_v6
/ peer_port on an unconnected socket. A plain send with no peer returns -1:
there is nowhere for the datagram to go.

port_in_use6() now keys off the socket's domain rather than its peer: a
socket owns a v6 source port by its family, not by having a v6 peer, so
auto-allocated ports stay unique now that sendto no longer sets peer_is_v6.

Tests: ping A then B keeps A's replies; a plain send requires connect.
All-nodes is local on every interface, so nd6_recv_ra() accepted router
advertisements even on an interface where IPv6 was never started, or had
been stopped: a router (or a host) could hand a plain IPv4 interface an
IPv6 address and a default route, and wolfIP_ipv6_stop() did not really
stop the interface from listening.

nd6_recv_ra() now returns early when the interface is not started, and
wolfIP_ipv6_stop() clears the RA-derived state - the learned prefixes and
default routers - so the configuration goes with the advertisements that
installed it.

Tests: an RA on a never-started interface is ignored (no address, no
router); an RA after a stop is ignored.
A prefix renewal refreshes the valid lifetime, but an address the tick had
already deprecated (its preferred lifetime ran out) was never set back to
preferred: a router that deprecated a prefix and later revoked the
deprecation left the address deprecated for the rest of its valid lifetime.

nd6_slaac_apply_lifetimes() now sets a deprecated address back to preferred
when a renewal carries a non-zero preferred lifetime (RFC 4862 section
5.5.3 (e)).

Test: an address deprecated by a short preferred lifetime comes back to
preferred when the router re-advertises a healthy one.
A datagram to a destination that is neither on-link nor covered by a
default router was kept at the head of the socket's send queue and
retried forever: nd6_resolve() reported PENDING (waiting for a neighbour
that will never answer), so every later datagram - even ones to a
perfectly resolvable neighbour - waited behind it until the queue filled.

nd6_resolve() now reports NO_ROUTE (-3) for that case, distinct from
UNREACHABLE (-2), which is giving up on a known on-link neighbour. The
datagram flush drops it and carries on, so the queue drains. No ICMPv6
error is sent: the datagram is locally originated, so the RFC 4443 3.1
error would be addressed to one of our own addresses, and the stack has
no loopback delivery for it.

A TCP segment to a no-route destination aborts the connection (the SYN
can never leave) rather than pinning the head of the segment queue.

Tests: a UDP datagram to a no-route destination is dropped and a later
datagram to a resolvable neighbour goes out; a TCP connect to a no-route
destination is aborted.
nd6_is_onlink() skipped RA prefixes with the L flag clear and fell
through to the configured-prefix fallback, so a router advertising a
prefix with L=0 was ignored: the fallback claimed the prefix on-link
and took precedence over the explicit advertisement.

The RA prefix loop now returns the prefix's L flag directly, so a
matching advertisement is authoritative - L=1 on-link, L=0 not on-link
- and the configured-prefix fallback only fires when no RA prefix
matches. RFC 5942 section 4 says on-link determination comes from
router advertisements, not from an address's prefix length; Linux
honors an L=0 advertisement the same way (RT6F_NONLINK).

Test: a statically configured address on an interface, then a router
advertising the same prefix with L=0 - a destination in that prefix now
resolves to the router instead of the destination itself. Fails without
the fix.
@danielinux
danielinux requested a review from gasbytes October 15, 2026 13:18

@gasbytes gasbytes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome, ready to merge.

@gasbytes
gasbytes merged commit f7cb8d7 into wolfSSL:master Oct 16, 2026
49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants