Summary
The dhcpserver.c used by pico_w/wifi/access_point (and copied into many downstream projects) never receives a client's DHCPDISCOVER if the project's lwipopts.h sets LWIP_DHCP 0 — which is the natural, reasonable setting for any project that isn't using lwIP's own DHCP client (i.e. every AP-mode project that supplies its own DHCP server, like this example does).
There is no error anywhere. udp_bind() succeeds, the pcb is valid, the callback is registered correctly — the packet is silently dropped one layer lower, inside ip4_input(), before it ever reaches udp_input() or the DHCP server's udp_recv callback. From the outside it just looks like "the client never gets an IP" and falls back to a 169.254.x.x self-assigned address.
The official access_point example does not hit this, but only by accident: its lwipopts.h includes the shared pico_w/wifi/lwipopts_examples_common.h, which sets LWIP_DHCP 1 unconditionally for all pico_w WiFi examples, for reasons unrelated to this example (the AP example never actually uses lwIP's DHCP client). That incidentally masks the bug. Any project that reasonably disables the unused client flag while keeping this same dhcpserver.c will hit it.
Root cause
In lwip/src/core/ipv4/ip4.c:
#if LWIP_DHCP || defined(LWIP_IP_ACCEPT_UDP_PORT)
#define IP_ACCEPT_LINK_LAYER_ADDRESSING 1
...
#else /* LWIP_DHCP */
#define IP_ACCEPT_LINK_LAYER_ADDRESSING 0
#endif /* LWIP_DHCP */
and further down, inside ip4_input():
/* broadcast or multicast packet source address? Compliant with RFC 1122: 3.2.1.3 */
#if LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING
if (check_ip_src
#if IP_ACCEPT_LINK_LAYER_ADDRESSING
/* DHCP servers need 0.0.0.0 to be allowed as source address (RFC 1.1.2.2: 3.2.1.3/a) */
&& !ip4_addr_isany_val(*ip4_current_src_addr())
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */
)
#endif /* LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING */
{
if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) ||
(ip4_addr_ismulticast(ip4_current_src_addr()))) {
/* packet source is not valid */
pbuf_free(p);
return ERR_OK;
}
}
With LWIP_IGMP=0 and IP_ACCEPT_LINK_LAYER_ADDRESSING=0 (the latter following directly from LWIP_DHCP=0), the entire outer #if — including the if (check_ip_src ...) guard line and the !ip4_addr_isany_val(...) exception nested inside it — is preprocessed away. But the { ... } body is outside that #if, so it survives unconditionally. The result: the bare ip4_addr_isbroadcast(...) || ip4_addr_ismulticast(...) check on the packet's source address now runs unconditionally, with no DHCP exception at all.
A DHCPDISCOVER's source address is always 0.0.0.0 (the client has no address yet — that's the entire point of the request). ip4_addr_isbroadcast_u32() special-cases this:
/* all ones (broadcast) or all zeroes (old skool broadcast) */
if ((~addr == IPADDR_ANY) || (addr == IPADDR_ANY)) {
return 1;
So 0.0.0.0 is classified as an "old-skool broadcast" source, the RFC 1122 check treats it as invalid, and the packet is freed and dropped — before raw_input(), before the protocol dispatch switch, before udp_input(), before any application code ever sees it.
Why the official example doesn't show this symptom
pico_w/wifi/lwipopts_examples_common.h sets LWIP_DHCP 1 for all pico_w WiFi examples. That happens to also set IP_ACCEPT_LINK_LAYER_ADDRESSING=1 via the coupling above, which restores the 0.0.0.0 exception — even though this example never uses lwIP's own DHCP client at all. It's an unrelated setting accidentally papering over this interaction.
Minimal repro
- Start from
pico_w/wifi/access_point.
- In its
lwipopts.h, explicitly set #define LWIP_DHCP 0 (reasonable if a project doesn't want lwIP's own DHCP client compiled in).
- Flash, join the AP from a phone/laptop configured for DHCP.
- Client never receives an IP and falls back to link-local (169.254.x.x/APIPA). No log output anywhere indicates why.
Confirmed via direct packet tracing (temporary printfs at each stage of ip4_input()) that the DHCPDISCOVER reaches ip4_input(), passes the netif-accept check, and is then dropped at the RFC 1122 source-validity check described above — never reaching raw_input() or udp_input().
Suggested fix
Have dhcpserver.c/dhcpserver.h define its own escape hatch, independent of LWIP_DHCP, e.g.:
#define LWIP_IP_ACCEPT_UDP_PORT(port) ((port) == PP_NTOHS(67))
placed before lwip/udp.h/lwip/ip.h are first included (or documented as required in the consuming project's lwipopts.h). This flips IP_ACCEPT_LINK_LAYER_ADDRESSING on via the defined(LWIP_IP_ACCEPT_UDP_PORT) branch of the same guard, restoring the 0.0.0.0 exception, without requiring LWIP_DHCP (lwIP's own client) to be enabled.
At minimum, a comment in dhcpserver.c/dhcpserver.h and the access_point example's README noting "this file depends on IP_ACCEPT_LINK_LAYER_ADDRESSING being enabled, which currently only happens automatically if LWIP_DHCP=1" would save the next person a long night of packet tracing.
Environment
- Board: Pico 2 W (
pico2_w / RP2350), also applicable to original Pico W
pico-sdk with lib/lwip pinned to STABLE-2_2_1_RELEASE
lib/cyw43-driver pinned to v1.1.1
- Build:
PICO_CYW43_ARCH_POLL=1, CMAKE_BUILD_TYPE=Release
Summary
The
dhcpserver.cused bypico_w/wifi/access_point(and copied into many downstream projects) never receives a client's DHCPDISCOVER if the project'slwipopts.hsetsLWIP_DHCP 0— which is the natural, reasonable setting for any project that isn't using lwIP's own DHCP client (i.e. every AP-mode project that supplies its own DHCP server, like this example does).There is no error anywhere.
udp_bind()succeeds, the pcb is valid, the callback is registered correctly — the packet is silently dropped one layer lower, insideip4_input(), before it ever reachesudp_input()or the DHCP server'sudp_recvcallback. From the outside it just looks like "the client never gets an IP" and falls back to a 169.254.x.x self-assigned address.The official
access_pointexample does not hit this, but only by accident: itslwipopts.hincludes the sharedpico_w/wifi/lwipopts_examples_common.h, which setsLWIP_DHCP 1unconditionally for all pico_w WiFi examples, for reasons unrelated to this example (the AP example never actually uses lwIP's DHCP client). That incidentally masks the bug. Any project that reasonably disables the unused client flag while keeping this samedhcpserver.cwill hit it.Root cause
In
lwip/src/core/ipv4/ip4.c:and further down, inside
ip4_input():With
LWIP_IGMP=0andIP_ACCEPT_LINK_LAYER_ADDRESSING=0(the latter following directly fromLWIP_DHCP=0), the entire outer#if— including theif (check_ip_src ...)guard line and the!ip4_addr_isany_val(...)exception nested inside it — is preprocessed away. But the{ ... }body is outside that#if, so it survives unconditionally. The result: the bareip4_addr_isbroadcast(...) || ip4_addr_ismulticast(...)check on the packet's source address now runs unconditionally, with no DHCP exception at all.A DHCPDISCOVER's source address is always
0.0.0.0(the client has no address yet — that's the entire point of the request).ip4_addr_isbroadcast_u32()special-cases this:So
0.0.0.0is classified as an "old-skool broadcast" source, the RFC 1122 check treats it as invalid, and the packet is freed and dropped — beforeraw_input(), before the protocol dispatch switch, beforeudp_input(), before any application code ever sees it.Why the official example doesn't show this symptom
pico_w/wifi/lwipopts_examples_common.hsetsLWIP_DHCP 1for all pico_w WiFi examples. That happens to also setIP_ACCEPT_LINK_LAYER_ADDRESSING=1via the coupling above, which restores the0.0.0.0exception — even though this example never uses lwIP's own DHCP client at all. It's an unrelated setting accidentally papering over this interaction.Minimal repro
pico_w/wifi/access_point.lwipopts.h, explicitly set#define LWIP_DHCP 0(reasonable if a project doesn't want lwIP's own DHCP client compiled in).Confirmed via direct packet tracing (temporary printfs at each stage of
ip4_input()) that the DHCPDISCOVER reachesip4_input(), passes the netif-accept check, and is then dropped at the RFC 1122 source-validity check described above — never reachingraw_input()orudp_input().Suggested fix
Have
dhcpserver.c/dhcpserver.hdefine its own escape hatch, independent ofLWIP_DHCP, e.g.:placed before
lwip/udp.h/lwip/ip.hare first included (or documented as required in the consuming project'slwipopts.h). This flipsIP_ACCEPT_LINK_LAYER_ADDRESSINGon via thedefined(LWIP_IP_ACCEPT_UDP_PORT)branch of the same guard, restoring the0.0.0.0exception, without requiringLWIP_DHCP(lwIP's own client) to be enabled.At minimum, a comment in
dhcpserver.c/dhcpserver.hand theaccess_pointexample's README noting "this file depends onIP_ACCEPT_LINK_LAYER_ADDRESSINGbeing enabled, which currently only happens automatically ifLWIP_DHCP=1" would save the next person a long night of packet tracing.Environment
pico2_w/ RP2350), also applicable to original Pico Wpico-sdkwithlib/lwippinned toSTABLE-2_2_1_RELEASElib/cyw43-driverpinned tov1.1.1PICO_CYW43_ARCH_POLL=1,CMAKE_BUILD_TYPE=Release