Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service reconciliation, and browser Wi-Fi administration. Include a guarded build-local fix for mDNS 1.12.0 membership handling, focused regression suites, and Phase 12 acceptance documentation.
8.4 KiB
mDNS 1.12.0 multicast membership regression
This is a narrow dependency correctness fix for the lwIP backend, not a general patch framework or a revival of the abandoned Phase 9 patches. Dependency versions, the component manifest/lock, managed sources, socket backend, and application mDNS lifecycle policy are unchanged.
Defects and fix
In the inspected mdns_networking_lwip.c, upstream pcb_if_deinit() (original
lines 267–277) only leaves a group when the interface's last protocol bit is
cleared. Removing one family while the other remains active therefore leaks a
membership reference. Restoration joins again; repeated partial transitions
accumulate references in lwIP's bounded group-use counter. Deinitializing an
already-disabled family can also issue an unmatched leave.
The generated copy checks the requested family's active bit, returns immediately
if absent, and leaves that family's group before clearing its bit. It clears
ready only after the last family on that interface is removed, and frees the
shared PCB only if no other interface remains ready.
In pcb_if_init() (original lines 282–301), a successful group join followed by
pcb_init() failure was not unwound. The copy attempts a leave before returning
the original PCB error. No readiness or protocol bit is published on failure.
These operations still run through upstream's existing lwIP-thread wrappers.
No new locks, allocations, task ownership, retry state, or public API are added.
Upstream join_group() is unchanged, including its refusal to act when the netif
is absent/down. Leaves remain best effort: the patch guarantees balanced leave
attempts, not successful lwIP cleanup after an interface disappears. It does
not retry a failed leave or retain a PCB solely because leave failed; physical
netif teardown remains responsible for its own membership cleanup.
Build integration
- Root
CMakeLists.txtincludescmake/mdns_membership.cmakeafter IDF'sproject(), when component targets and resolved versions are available. - The include obtains
espressif__mdns'sCOMPONENT_DIR,COMPONENT_LIB, andCOMPONENT_VERSIONvia IDF component properties. Socket-backend builds skip the overlay entirely; the fix is only relevant to the lwIP source. - The resolved component version and manifest version must both be exactly
1.12.0. Using IDF's configured Python interpreter, the helper verifies the complete source SHA-256, then performs two exact, unique replacements. - It writes only
${CMAKE_BINARY_DIR}/mdns_membership/mdns_networking_lwip.c. Unchanged output is not rewritten, avoiding gratuitous rebuilds. Everything outside the two replacement regions, including provenance/license headers, remains byte-for-byte intact. The actual upstream file identifies itself as Apache-2.0, copyright 2022–2025 Espressif; it is not relabeled as GPL. Existing project GPL/license material is untouched. - The include replaces exactly one matching entry in the existing component
target's
SOURCESproperty, accepting absolute or component-relative paths. All other source entries and all component compile definitions, include paths, dependencies, and target linkage are retained. The original file is not also compiled. An unexpected source list fails configuration. - The helper, original source, manifest, and generated copy are registered as configure dependencies. Reconfiguration regenerates/verifies the copy; a clean build simply recreates it. The CMake include itself is automatically a CMake input. Version/hash/replacement errors fail configure rather than silently compiling an unpatched dependency.
The integration uses the component's existing target; no whole-component copy,
managed in-place edits, dependency overrides, manifest changes, or extra source
library are involved. The inspected upstream CMake file offers no per-source
substitution option; replacing the target source after project() is the local
integration point.
Exact reviewed baseline
- Component:
espressif/mdns, version1.12.0. - Manifest repository:
espressif/esp-protocols,components/mdns. - Manifest commit:
db06b19b7be729c163d346f62ec0eba01047b7f1(provenance; guards are the resolved/manifest version and complete source hash). - Source:
managed_components/espressif__mdns/mdns_networking_lwip.c. - SHA-256:
adc139fa504a925ab644f21f8dce3659927f534e390a176b72b0ae3206c6a3ea.
Run the host tests
From the repository root, on a POSIX host with Python 3, CMake >= 3.16, and a C11 compiler:
./tests/mdns_membership/run.py
# Optional compiler selection (CC is an executable, not a shell command):
CC=clang ./tests/mdns_membership/run.py
The managed component must already be installed. The runner uses temporary
directories, does not fetch dependencies, does not build firmware, and disables
core dumps for deliberate negative tests. It generates the overlay with the
same helper used by configure, extracts the actual patched state declarations
and six functions (pcb_init, pcb_deinit, mdns_priv_if_ready,
is_any_pcb_in_use, pcb_if_init, pcb_if_deinit), and compiles them with
-std=c11 -Wall -Wextra -Werror -pedantic. Only group operations and low-level
UDP APIs are mocked; the lifecycle logic under test is not reimplemented.
Coverage:
- 512 IPv4 loss/restoration cycles while IPv6 stays active, and 512 inverse cycles; exact group references and readiness checked after each transition.
- Repeated disabled-family deinit, final-family teardown, and duplicate init.
- Join failure without an unmatched leave or PCB allocation.
- 512 allocation failures and 512 bind failures per family, with successful joins unwound, no state publication, original error preserved, and recovery.
- Leave failure on teardown and unwind: one attempt, no repeated disabled leave, original allocation error retained.
- 512 cycles per family with a second interface holding the shared PCB; exactly one allocation, no premature free, and one final removal.
- Separate negative controls restoring each original buggy function must fail the very same executable harness.
- Mock-IDF CMake configure fixtures for absolute/relative source replacement, unchanged unrelated source, repeat configure without rewriting, socket bypass, missing/duplicate networking source rejection, resolved-version rejection, and full-source hash rejection. Manifest-version rejection is tested through the helper CLI. Rejected fresh helper runs must not produce output.
- Original managed networking bytes are checked unchanged after the run.
These are host unit/configure tests, not a real ESP-IDF integration build or
hardware/network test. The real join_group(), lwIP IGMP/MLD counters, scheduling,
and network teardown are not exercised by mocks. Run the normal pio run
separately (never concurrently with another full build) to validate actual IDF
integration. In its generated compile_commands.json, the mDNS networking entry
must point to the build-local mdns_membership/mdns_networking_lwip.c, with no
original managed networking entry; other mDNS sources must remain managed paths.
On device, exercise repeated IPv4-only and IPv6-only loss/restoration while the
other family remains active, final network teardown/recovery, and continued
mDNS discovery. No hardware result is implied by the host PASS output.
Maintenance / removal
Do not update the expected hash merely to make a new dependency build. On any mismatch, review the entire changed networking source and component CMake, especially protocol state, join/leave behavior, netif-down semantics, and shared PCB lifetime. Establish whether upstream has fixed both defects first. A version bump is a separate explicitly reviewed change; this overlay does not select or upgrade the dependency.
If upstream fixes both paths, remove the root include and these narrowly scoped helper/test files (or replace the tests with suitable upstream coverage), then clean/reconfigure and check that only the upstream source compiles. If a local fix is still necessary, re-audit the exact new source/version, update the guards and exact replacements together, update this provenance record, rerun the host suite with its negative controls, and run a serial full firmware build. Never fall back to unguarded search/replace or silently skip a failed patch. A stale build-local copy is not authority: it must always be reproducible from the managed source plus the reviewed helper.