Skip to content

AP-mode dhcpserver.c silently drops every DHCPDISCOVER when LWIP_DHCP is 0 #783

Description

@brianetchells

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

  1. Start from pico_w/wifi/access_point.
  2. In its lwipopts.h, explicitly set #define LWIP_DHCP 0 (reasonable if a project doesn't want lwIP's own DHCP client compiled in).
  3. Flash, join the AP from a phone/laptop configured for DHCP.
  4. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions