Complete Phase 12 dual-stack networking

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.
This commit is contained in:
2026-09-20 22:35:34 +02:00
parent ece4ba77e3
commit 8902b25d78
52 changed files with 3042 additions and 163 deletions
+145
View File
@@ -0,0 +1,145 @@
# 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 267277) 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 282301), 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
1. Root `CMakeLists.txt` includes `cmake/mdns_membership.cmake` **after** IDF's
`project()`, when component targets and resolved versions are available.
2. The include obtains `espressif__mdns`'s `COMPONENT_DIR`, `COMPONENT_LIB`, and
`COMPONENT_VERSION` via IDF component properties. Socket-backend builds skip
the overlay entirely; the fix is only relevant to the lwIP source.
3. 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.
4. 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 20222025 Espressif; it is not relabeled as GPL. Existing
project GPL/license material is untouched.
5. The include replaces exactly one matching entry in the existing component
target's `SOURCES` property, 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.
6. 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`, version `1.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:
```sh
./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.