diff --git a/FORK_PRS.md b/FORK_PRS.md index 04bba41..44c3221 100644 --- a/FORK_PRS.md +++ b/FORK_PRS.md @@ -30,11 +30,11 @@ become its own scoped PR. Issue numbers filled in once created. | 10 | ✅ [#29](https://github.com/fugo101/microlink/issues/29) (done) | [`liestrela/microlink`](https://github.com/liestrela/microlink) | `1649d987` | Gate ESP32 temp-sensor feature behind `CONFIG_SOC_TEMP_SENSOR_SUPPORTED` — `esp_driver_tsens` was an unconditional `REQUIRES`, breaks build on SoC variants without a temp sensor. Cherry-picked verbatim, no adaptation needed. | 1 | | 11 | [#30](https://github.com/fugo101/microlink/issues/30) | `cplewes` vs `Csontikka` (conflict) | `cplewes` `83a102be`/`01d51697` vs [`Csontikka/microlink`](https://github.com/Csontikka/microlink) `dd5714c4`→`2e68e546` | `ip_input()` vs `netif->input`/`tcpip_input` threading fix for decrypted WG RX pbufs — cplewes fixes a UAF this way, Csontikka's own branch later reverted the equivalent fix citing a throughput regression (~30pps cap). Needs research, not a blind cherry-pick | 2 | | 12 | [#31](https://github.com/fugo101/microlink/issues/31) | `Csontikka/microlink` | `647c2ab0` | `LOCK_TCPIP_CORE` around `netif_set_link_up/down` — compare against our documented `netif->state` NULL-and-restore workaround in `ESP_IDF_6X_COMPAT.md` before adopting | 2 | -| 13 | [#32](https://github.com/fugo101/microlink/issues/32) | `Csontikka/microlink` | `5bda1783` | Peer direct-handshake init was one-shot; peers with dropped/unanswered handshakes stayed permanently blackholed — retry every 30s via timestamp instead of boolean latch | 2 | +| 13 | ✅ [#32](https://github.com/fugo101/microlink/issues/32) (done) | `Csontikka/microlink` | `5bda1783` | Peer direct-handshake init was one-shot; peers with dropped/unanswered handshakes stayed permanently blackholed — retry every 30s via timestamp instead of boolean latch. Adapted, not literal cherry-pick: the source commit's diff still cleared `peer->active` after `wireguardif_connect()`, which issue #26/#28's PR #53 (same session) proved permanently blocks a peer's handshake — kept that fix removed while porting the retry-timestamp logic. | 2 | | 14 | [#33](https://github.com/fugo101/microlink/issues/33) | `Csontikka/microlink` | `2e68e546`, `f1de3143` | DISCO trust-expiry gate (15s→60s, checks `last_rx` first), NAT-rebind handshake-skip when data path still alive, SPIRAM pbuf headroom fix for `wg_udp_output_cb` | 2 | | 15 | [#34](https://github.com/fugo101/microlink/issues/34) | `Csontikka/microlink` | `b7442d0f` | Handshake retries throttled to 1/tick round-robin instead of firing to every eligible peer in one tick (was blocking the caller ~280ms with 7 peers) | 2 | | 16 | [#35](https://github.com/fugo101/microlink/issues/35) | `Csontikka/microlink` | `7a24a9b3`, `4d3b3add` | DERP client backpressure/reconnect-storm fixes: don't tear down on TLS write backpressure, TCP_NODELAY + coalesced writes, fix a hot-spinning I/O loop, split into concurrent reader/writer tasks — check overlap against cplewes's DERP session-resumption fix (#6) before scoping | 2 | -| 17 | [#36](https://github.com/fugo101/microlink/issues/36) | `Csontikka/microlink` | `27806be3` | `microlink_stop()` never closed `derp.sockfd`/`coord_sock`, letting `derp_tx` outlive the teardown wait → UAF — reconcile against cplewes's teardown UAF fix (#2), may be the same root cause | 2 | +| 17 | [#36](https://github.com/fugo101/microlink/issues/36) | `Csontikka/microlink` | `27806be3` | `microlink_stop()` never closed `derp.sockfd`/`coord_sock`, letting `derp_tx` outlive the teardown wait → UAF — reconcile against cplewes's teardown UAF fix (#2), may be the same root cause. **Investigated, not needed here**: this fork's `microlink_stop()` (from issue #21/#22's PR #47) never frees context until `ml_join_tasks()` proves every worker exited, and `ml_derp.c`'s blocking loops already call `ml_shutdown_pending()` on every SO_RCVTIMEO-bounded iteration (100-200ms) to bail out cooperatively — the source commit's "shutdown() the socket to force-wake a blocked task" fixes a UAF that doesn't exist here. Leaving open in case future review disagrees, but no port planned. | 2 | | 18 | [#37](https://github.com/fugo101/microlink/issues/37) | `Csontikka/microlink` | `cbdf1603`, `aad403af`, `533f1f88`, `46e34917`, `017b3588`, `372ca277` | H2 frame reassembly across `noise_recv()` read boundaries — large MapResponses spanning reads deterministically corrupt the stream ("implausible message size"); genuine protocol-correctness bug independent of control-plane backend | 2 | | 19 | [#38](https://github.com/fugo101/microlink/issues/38), [#39](https://github.com/fugo101/microlink/issues/39) | [`djorr5/microlink`](https://github.com/djorr5/microlink) | `67b230b2` | Two independent changes, split into two issues: (a) dynamic H2 RX window sizing based on free heap for RAM-constrained boards, roughly halving MapResponse-parsing PSRAM footprint; (b) `ip4_route_src_hook` to force tailnet-range traffic onto the WG netif directly | 2 | | 20 | [#40](https://github.com/fugo101/microlink/issues/40) | [`AELovelace/LAIN-MicrolinkRouter`](https://github.com/AELovelace/LAIN-MicrolinkRouter) | `e1239460` | `microlink_set_exit_node()` — default-route exit-node support while preserving per-peer `/32` routes, with routing-loop and liveness fixes. New capability, not a bugfix — needs a product decision (does any current/future downstream want SoftAP/NAPT exit-node routing?) before it's worth the review cost | 2 | diff --git a/components/microlink/include/microlink_internal.h b/components/microlink/include/microlink_internal.h index dc3a660..5f9d8ae 100644 --- a/components/microlink/include/microlink_internal.h +++ b/components/microlink/include/microlink_internal.h @@ -303,8 +303,13 @@ typedef struct { /* WireGuard peer index in wireguard-lwip */ int wg_peer_index; - /* On-demand handshake: tried once on first DISCO direct path discovery */ - bool tried_initial_handshake; + /* On-demand handshake retry timestamp. When a direct DISCO PONG arrives + * for a peer without an active WG session, we fire a one-shot handshake + * init. A single boolean latch left a peer permanently un-sessioned if + * that one init was lost or unanswered. Instead, record + * ml_get_time_ms() of the last attempt and re-fire after + * INITIAL_HANDSHAKE_RETRY_MS so a dropped initiation gets another try. */ + uint64_t last_init_handshake_ms; } ml_peer_t; /* ============================================================================ diff --git a/components/microlink/src/ml_wg_mgr.c b/components/microlink/src/ml_wg_mgr.c index 453a989..b373daf 100644 --- a/components/microlink/src/ml_wg_mgr.c +++ b/components/microlink/src/ml_wg_mgr.c @@ -1029,15 +1029,17 @@ static void process_disco_pong(microlink_t *ml, const ml_rx_packet_t *pkt, (int)((pkt->src_ip >> 24) & 0xFF), (int)((pkt->src_ip >> 16) & 0xFF), (int)((pkt->src_ip >> 8) & 0xFF), (int)(pkt->src_ip & 0xFF), (int)pkt->src_port, p->hostname); - /* First direct path discovery — send a one-shot handshake - * via direct UDP. Do NOT use wireguardif_connect() which - * sets peer->active=true and causes infinite handshake - * retries (every 5s) when the peer has us trimmed. - * Instead, just fire a single handshake init. If the peer - * has us configured, it will respond and establish session. - * If not, we stop and wait for them to initiate. */ - if (!p->tried_initial_handshake) { - p->tried_initial_handshake = true; + /* Direct path discovered but no WG session yet. Fire a + * handshake init via direct UDP, rate-limited so a + * dropped or unanswered init gets another try every + * INITIAL_HANDSHAKE_RETRY_MS instead of leaving the peer + * permanently un-sessioned after a single lost attempt. */ +#define INITIAL_HANDSHAKE_RETRY_MS 30000ULL + bool first_try = (p->last_init_handshake_ms == 0); + bool retry_due = !first_try && + (now - p->last_init_handshake_ms > INITIAL_HANDSHAKE_RETRY_MS); + if (first_try || retry_due) { + p->last_init_handshake_ms = now; /* Store endpoint so wireguardif_connect sends to it */ wireguardif_update_endpoint(netif, (u8_t)p->wg_peer_index, &ep_ip, pkt->src_port); @@ -1047,11 +1049,14 @@ static void process_disco_pong(microlink_t *ml, const ml_rx_packet_t *pkt, * checks before a peer may initiate a session at all. * Clearing it here meant no session could ever be * established with this peer — permanently blackholed - * on the direct path. tried_initial_handshake above is - * what actually bounds this to a one-shot attempt. */ + * on the direct path. The retry timestamp above is + * what bounds re-attempts to once per + * INITIAL_HANDSHAKE_RETRY_MS. */ wireguardif_connect(netif, (u8_t)p->wg_peer_index); - ESP_LOGI(TAG, "WG one-shot handshake to %s (first direct path)", p->hostname); + ESP_LOGI(TAG, "WG direct handshake %s to %s", + first_try ? "init" : "retry", p->hostname); } +#undef INITIAL_HANDSHAKE_RETRY_MS } } }