From da55beb7e8c13be2deaeacdc166c54d135d86235 Mon Sep 17 00:00:00 2001 From: snowpaper <121743268+snowpaper@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:57:53 +0700 Subject: [PATCH] =?UTF-8?q?ml=5Fwg=5Fmgr:=20don't=20pre-install=20unvalida?= =?UTF-8?q?ted=20peer=20endpoints=20=E2=80=94=20DERP=20until=20DISCO=20val?= =?UTF-8?q?idates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At peer-add, the peer's advertised endpoints[0] from the MapResponse was installed as the WireGuard endpoint before any path validation, and wireguardif's output path treats any non-zero endpoint as a working direct path. For a CGNAT peer (e.g. cellular hotspot) that address is unreachable, so the tunnel went one-way: inbound arrived via DERP while every outbound data packet was sent direct-UDP into a black hole. Tailscale semantics: data flows via DERP until a direct path is validated by a DISCO pong. Leave the endpoint blank at peer-add; the only endpoint installs are the already-gated ones (process_disco_pong on a direct pong, and the handshake path gated on best_ip). Same-LAN peers still upgrade to direct within seconds. Verified on ESP32-S3 (ESP-IDF v5.3): a CGNAT peer that previously could never reach a TCP service on the node (ping timeout, connect fail) gets ping replies over DERP, TCP connects, and bulk transfers complete. Same-LAN direct path unaffected. Fixes #18 --- components/microlink/src/ml_wg_mgr.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/components/microlink/src/ml_wg_mgr.c b/components/microlink/src/ml_wg_mgr.c index ed40942..24979e4 100644 --- a/components/microlink/src/ml_wg_mgr.c +++ b/components/microlink/src/ml_wg_mgr.c @@ -497,18 +497,20 @@ static int add_peer(microlink_t *ml, const ml_peer_update_t *update) { IP4_ADDR(&wg_peer.allowed_ip.u_addr.ip4, ip_a, ip_b, ip_c, ip_d); IP4_ADDR(&wg_peer.allowed_mask.u_addr.ip4, 255, 255, 255, 255); - /* Set endpoint if available, otherwise leave blank for DERP-only */ - if (p->endpoint_count > 0 && p->endpoints[0].ip != 0) { - uint8_t ea = (p->endpoints[0].ip >> 24) & 0xFF; - uint8_t eb = (p->endpoints[0].ip >> 16) & 0xFF; - uint8_t ec = (p->endpoints[0].ip >> 8) & 0xFF; - uint8_t ed = p->endpoints[0].ip & 0xFF; - IP4_ADDR(&wg_peer.endpoint_ip.u_addr.ip4, ea, eb, ec, ed); - wg_peer.endport_port = p->endpoints[0].port; - } else { - ip_addr_set_any(false, &wg_peer.endpoint_ip); - wg_peer.endport_port = 0; - } + /* Leave the endpoint BLANK: data output goes via DERP until DISCO + * validates a direct path (Tailscale semantics). Pre-installing + * endpoints[0] from the MapResponse advertisement made wireguardif + * treat an UNVALIDATED address as a working direct path + * (wireguardif.c peer_output: any non-zero endpoint => direct UDP), + * so with a CGNAT peer (e.g. PC on cellular) every data reply was + * fired at an unreachable advertised endpoint and silently lost, + * while inbound arrived via DERP — one-way tunnel, ping/TCP dead. + * The validated path install lives in process_disco_pong (only a + * DIRECT pong sets best_ip -> wireguardif_update_endpoint), which + * still upgrades us to direct within seconds when it truly works + * (proven same-LAN). */ + ip_addr_set_any(false, &wg_peer.endpoint_ip); + wg_peer.endport_port = 0; wg_peer.keep_alive = 25;