This guide shows how to use wolfIP's advanced IPv4 features: UDP multicast with IGMP membership reports, IPv4 forwarding between interfaces, multi-interface configuration, and the internal loopback interface. Each feature is opt-in and removed by the preprocessor when disabled, so the default single-interface endpoint build is unchanged.
It is a getting-started document, not a reference manual. The authoritative API
is wolfip.h (with the compile-time switches in config.h); the worked
examples come from src/test/test_multicast_interop.c,
src/test/unit/unit_tests_multicast.c, and src/test/test_wolfssl_forwarding.c.
- 1. Multicast and IGMP
- 2. IPv4 forwarding
- 3. Multiple interfaces
- 4. The loopback interface
- 5. Troubleshooting
IPv4 UDP multicast is compiled out by default. Define IP_MULTICAST to enable
the BSD-style multicast socket options and the IGMPv3 any-source-multicast (ASM)
membership reports. When the macro is undefined, none of the multicast code or
state is built into the stack.
The socket options are exposed under wolfIP-prefixed names so they resolve to the
host's IP_* constants when those headers are present and to fixed fallback
values otherwise (wolfip.h):
| Option | Fallback value | optval type |
Meaning |
|---|---|---|---|
WOLFIP_IP_ADD_MEMBERSHIP |
35 | struct wolfIP_ip_mreq |
Join a multicast group on an interface. |
WOLFIP_IP_DROP_MEMBERSHIP |
36 | struct wolfIP_ip_mreq |
Leave a previously joined group. |
WOLFIP_IP_MULTICAST_IF |
32 | struct wolfIP_mreq_addr |
Pin the egress interface for multicast sends. |
WOLFIP_IP_MULTICAST_TTL |
33 | int or uint8_t |
TTL for outgoing multicast datagrams. |
WOLFIP_IP_MULTICAST_LOOP |
34 | int or uint8_t |
Deliver this socket's own multicast sends back to local members. |
All multicast options use socket level WOLFIP_SOL_IP, and they apply only to
UDP sockets. The struct wolfIP_ip_mreq / struct wolfIP_mreq_addr types are
defined in wolfip.h:
struct wolfIP_mreq_addr {
uint32_t s_addr;
};
struct wolfIP_ip_mreq {
struct wolfIP_mreq_addr imr_multiaddr; /* the group address */
struct wolfIP_mreq_addr imr_interface; /* local iface IP, or ANY */
};The s_addr fields are in network byte order — fill them with inet_pton()
or htonl(), exactly as with a host struct ip_mreq.
To receive multicast, bind a UDP socket to the destination port, then join the
group with WOLFIP_IP_ADD_MEMBERSHIP. From src/test/test_multicast_interop.c:
wolf_fd = wolfIP_sock_socket(s, AF_INET, IPSTACK_SOCK_DGRAM, 17);
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = htons(MCAST_PORT);
bind_addr.sin_addr.s_addr = 0;
wolfIP_sock_bind(s, wolf_fd, (struct wolfIP_sockaddr *)&bind_addr,
sizeof(bind_addr));
memset(&mreq, 0, sizeof(mreq));
inet_pton(AF_INET, MCAST_GROUP, &mreq.imr_multiaddr.s_addr);
mreq.imr_interface.s_addr = htonl(INADDR_ANY); /* let routing pick the iface */
wolfIP_sock_setsockopt(s, wolf_fd, WOLFIP_SOL_IP,
WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));imr_interface selects the interface:
INADDR_ANYresolves the interface from the route to the group, and that interface must already have a configured source IP — the join is rejected with-WOLFIP_EINVALotherwise, because there would be no valid source address to build the IGMP report from (mcast_if_from_addr,src/wolfip.c).- A specific local-interface IP pins the membership to that interface.
A datagram for a joined group is delivered only after the join: an unjoined socket bound to the same port does not receive the group's traffic.
The join/leave path is driven by the membership table in struct wolfIP
(src/wolfip.c), and emits IGMPv3 reports automatically:
- On the first join of a
{interface, group}pair, wolfIP sends an IGMPv3 Current-State Report with record typeMODE_IS_EXCLUDE(an ASM join). The report is sent to the IGMPv3 all-routers address224.0.0.22(igmp_send_report). - Memberships are reference-counted: multiple sockets joining the same
{interface, group}share one membership entry, so only the first join and the last leave hit the wire. - On the last leave, wolfIP sends a
CHANGE_TO_INCLUDEreport (the ASM leave). - Incoming IGMP Membership Queries are answered, but not synchronously. Per
RFC 3376 §5.2, wolfIP schedules a Current-State Report after a random delay
drawn from the query's Max-Response-Time window, which coalesces a query flood
into one deferred report per group (
igmp_input,igmp_report_timer_cb). - A query is accepted only if it arrives with IP TTL 1 and is addressed to
224.0.0.1(all-hosts) or to the group itself; anything else is dropped as off-link or spoofed.
To transmit, set the desired TX options and sendto() the group address. The
default multicast TTL is 1 and loopback defaults to 1 for a new UDP socket
(src/wolfip.c). From src/test/test_multicast_interop.c:
int ttl = 3;
wolfIP_sock_setsockopt(s, wolf_fd, WOLFIP_SOL_IP,
WOLFIP_IP_MULTICAST_TTL, &ttl, sizeof(ttl));
memset(&dst, 0, sizeof(dst));
dst.sin_family = AF_INET;
dst.sin_port = htons(WOLFIP_MCAST_PORT);
inet_pton(AF_INET, MCAST_GROUP, &dst.sin_addr.s_addr);
wolfIP_sock_sendto(s, wolf_fd, payload, sizeof(payload), 0,
(struct wolfIP_sockaddr *)&dst, sizeof(dst));Two more TX controls:
WOLFIP_IP_MULTICAST_IFpins the egress interface (by local-interface IP) for this socket's multicast sends. PassingINADDR_ANYclears the pin and reverts to per-destination routing.WOLFIP_IP_MULTICAST_LOOPcontrols local delivery of the socket's own sends. When enabled (the default), a multicast datagram is looped back to local group members after a successful wire send, insidewolfIP_poll()(src/wolfip.c). A single socket that both joined a group and sent to it can therefore read its own datagram back:
/* unit_tests_multicast.c: join + set TTL/LOOP, send, poll, then recv self */
wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, WOLFIP_IP_MULTICAST_TTL, &ttl, sizeof(ttl));
wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, WOLFIP_IP_MULTICAST_LOOP, &loop, sizeof(loop));
wolfIP_sock_sendto(&s, sd, payload, sizeof(payload), 0, (struct wolfIP_sockaddr *)&dst, sizeof(dst));
wolfIP_poll(&s, 1); /* drives the wire send + loopback */
wolfIP_sock_recvfrom(&s, sd, out, sizeof(out), 0, NULL, NULL); /* reads its own datagram */The repository ships a Linux interop test that creates a TAP interface
(wmcast0) and validates both directions — Linux sending to a wolfIP receiver
and wolfIP sending to a Linux receiver (src/test/test_multicast_interop.c):
make unit-multicast
./build/test/unit
make build/test-multicast-interop
sudo ./build/test-multicast-interopPer-membership compile-time sizing (src/wolfip.c): each UDP socket holds up to
WOLFIP_UDP_MCAST_MEMBERSHIPS (default 4) joins, and the stack-wide table holds
MAX_UDPSOCKETS * WOLFIP_UDP_MCAST_MEMBERSHIPS distinct memberships.
By default wolfIP is an endpoint and does not route. Set
WOLFIP_ENABLE_FORWARDING to 1 at compile time (default 0 in config.h) to
turn the stack into a simple IPv4 router between its interfaces.
Forwarding is inherently multi-interface: wolfIP_forward_interface() returns
-1 whenever if_count < 2 (src/wolfip.c), so a useful forwarding build also
needs WOLFIP_MAX_INTERFACES >= 2 (see section 3).
When a frame arrives whose destination is not local to the receiving
interface, the IP input path asks wolfIP_forward_interface() for an egress
interface. That lookup covers both:
- directly connected subnets on the stack's other interfaces, and
- optional static routes added with
wolfIP_route_add().
If a route is found, the packet is forwarded out that interface:
in_if ─▶ IP input ─▶ dest local to this host? ─yes▶ deliver up the stack
│ no
▼
forward_interface(in_if, dest)
│ (connected-subnet or static-route lookup)
┌───────────┴───────────┐
│ no out iface │ out iface found
▼ ▼
(dropped) ttl <= 1 ? ─yes▶ ICMP TTL-exceeded back to src
│ no
▼
ttl--, recompute IP checksum,
ARP-resolve next hop, send on out_if
From the dispatch in src/wolfip.c:
int out_if = wolfIP_forward_interface(s, if_idx, dest);
if (out_if >= 0) {
if (ip->ttl <= 1) {
wolfIP_send_ttl_exceeded(s, if_idx, ip); /* ICMP type 11 to the source */
return;
}
if (!wolfIP_forward_prepare(s, out_if, dest, mac, &broadcast)) {
arp_queue_packet(s, out_if, dest, ip, len); /* queue until ARP resolves */
return;
}
ip->ttl--;
ip->csum = 0;
iphdr_set_checksum(ip);
wolfIP_forward_packet(s, out_if, ip, len, broadcast ? NULL : mac, broadcast);
return;
}Key behaviors, all from src/wolfip.c:
- TTL is decremented by one on every forwarded packet, and the IP header checksum is recomputed.
- TTL exhaustion (
ttl <= 1) produces an ICMP TTL-exceeded (type 11) back to the original source instead of forwarding. - Next-hop resolution uses ARP on Ethernet out-interfaces; if the MAC is not
yet known the packet is queued (
arp_queue_packet) and sent once ARP resolves — it is not dropped. - A reverse-path (RPF) check drops a packet whose source address is local to another of the host's interfaces before it is forwarded.
- Frames arriving on a non-loopback interface with a
127/8source or destination are dropped (loopback addresses must not appear on the wire).
When forwarding is enabled, the optional static-route API is also compiled in
(wolfip.h): wolfIP_route_add(), wolfIP_route_delete(),
wolfIP_route_lookup(), wolfIP_route_get(), and wolfIP_route_count(). The
route lookup performs longest-prefix matching across connected subnets and static
routes together.
wolfIP is an endpoint stack first; the forwarding path deliberately omits IPv4 fragmentation and reassembly. These are documented, intended deviations, not bugs:
- No egress fragmentation. A forwarded datagram is never split into fragments. A DF-set datagram larger than the egress IP MTU is dropped with an ICMP Destination Unreachable (Fragmentation Needed, type 3 code 4) carrying the egress next-hop MTU (RFC 1812 4.3.2.4); a DF-clear datagram that does not fit is dropped silently on transmit. Datagrams that fit the egress MTU are forwarded normally.
- No reassembly. The IP input path drops every fragment (MF set or non-zero fragment offset); the stack never reassembles fragmented datagrams.
- Locally generated UDP.
wolfIP_sock_sendto()fails with-1when the datagram does not fit the socket's IP MTU (headers excluded) — a clean error to the caller instead of a silent drop. - TCP. The advertised and accepted MSS is clamped to the MTU, so TCP segments never require fragmentation.
The practical consequence for a router build: keep every link's MTU at or above the largest datagram that traverses it (the usual 1500-byte Ethernet baseline). A higher-MTU upstream (e.g. jumbo frames) that injects datagrams larger than a downstream link's IP MTU will see them dropped at the egress (no Fragmentation Needed reply when the DF bit is clear). If your topology cannot guarantee that, IPv4 fragmentation is out of scope for wolfIP and a different stack is needed.
wolfIP's forwarding path does not implement the router portion of ICMP Router Discovery (RFC 1256): it neither sends Router Advertisements (type 9), solicited or periodic, nor answers Router Solicitations (type 10). This is a documented, intended deviation, not a bug:
- wolfIP hosts are configured by DHCP or static configuration; the stack has no host-side router-discovery consumer that would need the router side.
- Routing is connected-subnet plus static routes only (no dynamic protocol), so there is no route information to advertise beyond what DHCP or a static default gateway already provides.
- Periodic advertisement would add per-interface timer and scheduling state to the forwarding path for no in-ecosystem consumer.
Hosts attached to a wolfIP router must obtain their default gateway from DHCP or be configured with the router's address statically.
src/test/test_wolfssl_forwarding.c builds a two-interface router: interface 0
on the LAN, interface 1 on the WAN, each with its own IP config.
/* router has WOLFIP_MAX_INTERFACES = 2, WOLFIP_ENABLE_FORWARDING = 1 */
wolfIP_init(router_stack);
tap_dev = wolfIP_getdev(router_stack); /* iface 0 driver */
tap_init(tap_dev, TAP_IFNAME, host_addr.s_addr);
mem_link_attach(wolfIP_getdev_ex(router_stack, 1), /* iface 1 driver */ ...);
wolfIP_ipconfig_set_ex(router_stack, 0, router_lan_ip4, IP4(255,255,255,0), IP4(0,0,0,0));
wolfIP_ipconfig_set_ex(router_stack, 1, router_wan_ip4, IP4(255,255,255,0), IP4(0,0,0,0));A host on the LAN reaching a server on the WAN sets the router's LAN address as its gateway; the router forwards between the two connected subnets automatically.
If the next hop is not directly on one of those connected subnets, add a static route:
/* 10.20.0.0/16 is reachable via 192.168.1.254 on interface 0 */
wolfIP_route_add(s, 0, IP4(10,20,0,0), 16, IP4(192,168,1,254));The static-route API is compiled only when forwarding is enabled:
wolfIP_route_add(), wolfIP_route_delete(), wolfIP_route_lookup(),
wolfIP_route_get(), and wolfIP_route_count().
WOLFIP_MAX_INTERFACES (default 2 in config.h) sizes the per-stack arrays of
link-layer descriptors and IP configurations. wolfIP_init() sets if_count to
WOLFIP_MAX_INTERFACES and initialises every slot (src/wolfip.c).
Each interface slot is addressed by a zero-based if_idx. There are two parallel
accessor families in wolfip.h:
| Legacy (first hardware iface) | Indexed (_ex) |
Purpose |
|---|---|---|
wolfIP_getdev(s) |
wolfIP_getdev_ex(s, if_idx) |
Get the struct wolfIP_ll_dev * to wire to a driver. |
wolfIP_ipconfig_set(s, ip, mask, gw) |
wolfIP_ipconfig_set_ex(s, if_idx, ip, mask, gw) |
Set IP / netmask / gateway. |
wolfIP_ipconfig_get(s, &ip, &mask, &gw) |
wolfIP_ipconfig_get_ex(s, if_idx, &ip, &mask, &gw) |
Read IP config. |
wolfIP_recv(s, buf, len) |
wolfIP_recv_ex(s, if_idx, buf, len) |
Hand an inbound frame to the stack. |
wolfIP_getdev_ex() returns NULL when if_idx is out of range. The legacy
helpers all target the first hardware interface, which is index 0 normally
but index 1 when loopback is enabled (see section 4
and the WOLFIP_PRIMARY_IF_IDX definition in src/wolfip.c).
Wire each slot's struct wolfIP_ll_dev to a driver (set mac, mtu, and the
poll/send callbacks) and give each slot an IP config
(src/test/test_wolfssl_forwarding.c):
wolfIP_init(s);
struct wolfIP_ll_dev *dev0 = wolfIP_getdev_ex(s, 0);
struct wolfIP_ll_dev *dev1 = wolfIP_getdev_ex(s, 1);
/* attach each dev to its driver: dev->poll, dev->send, dev->mac, dev->mtu ... */
wolfIP_ipconfig_set_ex(s, 0, IP4(192,168,1,1), IP4(255,255,255,0), IP4(0,0,0,0));
wolfIP_ipconfig_set_ex(s, 1, IP4(10,0,0,1), IP4(255,255,255,0), IP4(0,0,0,0));wolfIP_poll() calls each interface's poll callback and routes the resulting
frame to the correct interface internally (poll_devices, src/wolfip.c) — so a
driver that implements poll needs no extra plumbing.
If instead you push frames into the stack yourself (for example from an ISR or a
bridge), tag each frame with the interface it arrived on using
wolfIP_recv_ex():
/* a frame arrived on interface 1 */
wolfIP_recv_ex(s, 1, frame_buf, frame_len);wolfIP_recv(s, ...) is shorthand for wolfIP_recv_ex(s, <primary iface>, ...),
so use the _ex form whenever more than one interface can deliver frames.
Set WOLFIP_ENABLE_LOOPBACK to 1 (default 0) to give the stack an internal
loopback interface. It requires WOLFIP_MAX_INTERFACES > 1 — config.h
enforces this with a compile-time #error:
#if WOLFIP_ENABLE_LOOPBACK && WOLFIP_MAX_INTERFACES < 2
#error "WOLFIP_ENABLE_LOOPBACK requires WOLFIP_MAX_INTERFACES > 1"
#endifWhen enabled, wolfIP_init() configures interface slot 0 as the loopback
device (src/wolfip.c):
- IP
127.0.0.1, mask255.0.0.0(WOLFIP_LOOPBACK_IP/WOLFIP_LOOPBACK_MASK, i.e.127.0.0.1/8), gateway none. ifname"lo",non_ethernet = 1, with internalpoll/sendcallbacks that move frames through an in-memory queue (wolfIP_loopback_poll/wolfIP_loopback_send) — there is no driver to wire.
Enabling loopback claims index 0, so the first hardware interface shifts to
index 1. This is encoded by WOLFIP_PRIMARY_IF_IDX (src/wolfip.c):
WOLFIP_ENABLE_LOOPBACK = 0 WOLFIP_ENABLE_LOOPBACK = 1
idx 0 : first hardware iface idx 0 : loopback (127.0.0.1/8)
idx 1 : second hardware iface idx 1 : first hardware iface
... idx 2 : second hardware iface
...
WOLFIP_PRIMARY_IF_IDX = 0 WOLFIP_PRIMARY_IF_IDX = 1
The consequence for the legacy accessors is exact: wolfIP_getdev(),
wolfIP_ipconfig_set()/_get(), and wolfIP_recv() all operate on
WOLFIP_PRIMARY_IF_IDX, so with loopback enabled they act on your hardware NIC at
index 1, not index 0. To touch a specific slot regardless of build, use the
_ex accessors with an explicit index:
/* loopback build: configure the real NIC explicitly at index 1 */
wolfIP_ipconfig_set_ex(s, 1, my_ip, my_mask, my_gw);
struct wolfIP_ll_dev *nic = wolfIP_getdev_ex(s, 1); /* same as wolfIP_getdev() here */The loopback interface is a normal interface as far as the socket layer is
concerned: a socket bound to or connecting to 127.0.0.1 exchanges UDP datagrams
and TCP segments with another local socket through the in-memory loopback queue,
never touching hardware. Loopback addresses are confined to it — frames carrying a
127/8 source or destination that arrive on a non-loopback interface are dropped
(src/wolfip.c). The queue depth is WOLFIP_LOOPBACK_QUEUE_DEPTH (default 4);
when it drains, blocked senders are woken via
wolfIP_notify_loopback_space_available().
WOLFIP_IP_ADD_MEMBERSHIP returns -WOLFIP_EINVAL. Either the address is not
a multicast group, or you joined with imr_interface = INADDR_ANY before giving
the resolved interface a source IP. Call wolfIP_ipconfig_set*() first, then
join. Joining the same {interface, group} twice on one socket also returns
-WOLFIP_EINVAL.
Joined but no multicast arrives. Confirm the socket is bound to the destination port and that you actually joined (an unjoined socket on the same port gets nothing). On a real link, also confirm the upstream switch/router honors the IGMPv3 report wolfIP sends on join.
Multicast sends never reach the network. The default multicast TTL is 1,
which does not cross a router. Raise it with WOLFIP_IP_MULTICAST_TTL. If you
expect to read your own sends back, leave WOLFIP_IP_MULTICAST_LOOP enabled and
remember the loopback copy is delivered inside wolfIP_poll() after the wire
send — poll the stack before recvfrom().
Forwarding does nothing. Check that WOLFIP_ENABLE_FORWARDING = 1, that
WOLFIP_MAX_INTERFACES >= 2, and that both interfaces have a configured,
non-zero IP — wolfIP_forward_interface() skips interfaces whose IP is
IPADDR_ANY and returns -1 when if_count < 2.
Forwarded traffic stops with ICMP "time exceeded". The packet's TTL reached 1 at the router; this is expected behavior, not a bug. The originating host should be using a TTL large enough for the hop count.
Wrong interface after enabling loopback. Remember the index shift: the legacy
accessors now target index 1 (the first NIC). Use wolfIP_getdev_ex() /
wolfIP_ipconfig_set_ex() with explicit indices to avoid ambiguity.