Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FORK_PRS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ become its own scoped PR. Issue numbers filled in once created.
| 3 | ✅ [#22](https://github.com/fugo101/microlink/issues/22) (done) | [`cplewes/microlink`](https://github.com/cplewes/microlink) | `7120dfa4` | Three crash sites triggered by captive-portal DNS failures: DERP double-init dangling pointer, `ml_coord_task` touching a freed event group post-destroy, corrupt-state races during parallel teardown. Landed together with #21 in the same PR — `a415d646` (the commit for #21) supersedes most of `7120dfa4`'s NULL-guard approach with a proper liveness bitmask; only the DERP mbedTLS double-init fix from `7120dfa4` was a distinct, still-needed piece. | 1 |
| 4 | [#23](https://github.com/fugo101/microlink/issues/23) | [`cplewes/microlink`](https://github.com/cplewes/microlink) | `8f2ff39b`, `5c7303b4` | Captive-portal detection (HTTP 302 / TCP close) in `ml_coord.c`, backs off 5 min instead of retrying every 16s | 1 |
| 5 | ✅ [#24](https://github.com/fugo101/microlink/issues/24) (done) | [`cplewes/microlink`](https://github.com/cplewes/microlink) | `9f6af750` | Yield 1 tick per peer in `disco_periodic_probes` — prevents starving other same-core tasks on large tailnets | 1 |
| 6 | [#25](https://github.com/fugo101/microlink/issues/25) | [`cplewes/microlink`](https://github.com/cplewes/microlink) | `b9636816` | DERP TLS session resumption (skip full ECDHE handshake on reconnect, ~7.5s→sub-second), connect timeout 10s→25s, per-peer exponential backoff on direct-path upgrade probes | 1 |
| 6 | [#25](https://github.com/fugo101/microlink/issues/25) (done) | [`cplewes/microlink`](https://github.com/cplewes/microlink) | `b9636816` | DERP TLS session resumption (skip full ECDHE handshake on reconnect, ~7.5s→sub-second), connect timeout 10s→25s, per-peer exponential backoff on direct-path upgrade probes. Not a literal cherry-pick — `git apply` conflicted only on `microlink_internal.h`'s `ml_derp_conn_t`, which lacks the `entropy`/`ctr_drbg` fields the source commit anchors near (this fork uses PSA crypto, same divergence as issue #14); hand-ported with identical logic/comments, `ml_derp.c`/`ml_wg_mgr.c` applied cleanly. | 1 |
| 7 | [#26](https://github.com/fugo101/microlink/issues/26) | [`antmanler/microlink`](https://github.com/antmanler/microlink) (branch `duoduo-edge`) | `2a7ba328` | `wg_udp_output_cb` thread-safety bug (raw UDP PCB called from two task contexts, corrupts heap) — same bug class as absorbed upstream PR #20, opposite direction; bundled with WG/DISCO RX queue depth increase (8→32/16) and a handshake-gating bug in `process_disco_pong` that permanently blocked sessions | 1 |
| 8 | ✅ [#27](https://github.com/fugo101/microlink/issues/27) (done) | [`antmanler/microlink`](https://github.com/antmanler/microlink) | `6ef9f5a0` | New `CONFIG_ML_CONFIG_HTTPD` Kconfig toggle to skip the port-80 httpd task (~7-8KB RAM saved); peer-update queue depth fix 400→32 (was allowing an unbounded ~80KB internal-RAM burst). Not a literal cherry-pick — `git apply` failed on unrelated line drift from PR #47's teardown rewrite, so hand-ported with identical logic/comments. | 1 |
| 9 | [#28](https://github.com/fugo101/microlink/issues/28) | [`antmanler/microlink`](https://github.com/antmanler/microlink) | `2a7ba328` | WG netif MTU 1420→1280 (Tailscale-standard) — needs verification against our existing `CONFIG_LWIP_IP4_FRAG`/`IP4_REASSEMBLY=y` before deciding it's still needed | 1 |
Expand Down
16 changes: 16 additions & 0 deletions components/microlink/include/microlink_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ extern "C" {
#define ML_DISCO_TRUST_DURATION_MS 15000
#define ML_DISCO_PING_TIMEOUT_MS 5000
#define ML_DISCO_UPGRADE_INTERVAL_MS 15000
/* Cap for the per-peer direct-path upgrade backoff. On a P2P-hostile network
* (e.g. healthspot blocks peer-to-peer UDP) direct-path upgrades NEVER
* succeed, so re-probing every 15s forever is pure waste (CPU + DERP TX). We
* double each peer's upgrade interval on every failed attempt up to this cap,
* resetting to the base interval when a direct path DOES establish. */
#define ML_DISCO_UPGRADE_BACKOFF_MAX_MS 300000
#define ML_DISCO_SESSION_ACTIVE_MS 45000

/* STUN servers (Tailscale primary, Google fallback) */
Expand Down Expand Up @@ -279,6 +285,7 @@ typedef struct {
uint64_t trust_until_ms; /* Direct path trusted until */
uint64_t last_send_ms; /* Last data sent to this peer */
uint64_t last_upgrade_ms; /* Last path upgrade attempt */
uint32_t upgrade_interval_ms; /* Current direct-path upgrade backoff (grows on repeated failure, resets on success) */

/* Best direct path */
uint32_t best_ip;
Expand Down Expand Up @@ -343,6 +350,15 @@ typedef struct {
int sockfd; /* Raw TCP socket */
mbedtls_ssl_context ssl; /* TLS context (owned exclusively by DERP I/O task) */
mbedtls_ssl_config ssl_conf;
/* Saved TLS session for resumption across DERP reconnects. On a flaky
* network DERP drops+reconnects a lot; without this every reconnect paid
* the full ~7.5s handshake. mbedTLS client session tickets are enabled
* (CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS) and tailscale's derper
* supports resumption, so a resumed handshake skips ECDHE -> sub-second.
* Best-effort: if the ticket expired the server falls back to a full
* handshake transparently. */
mbedtls_ssl_session saved_session;
bool have_saved_session;
bool connected;
uint64_t last_recv_ms; /* For keepalive watchdog */
} ml_derp_conn_t;
Expand Down
38 changes: 35 additions & 3 deletions components/microlink/src/ml_derp.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@

static const char *TAG = "ml_derp";

/* Timeout for DERP connection handshake operations */
#define DERP_CONNECT_TIMEOUT_MS 10000
/* Timeout for DERP connection handshake operations. Bumped 10s -> 25s:
* on a lossy/high-latency captive network (healthspot) the TLS handshake
* itself was observed taking ~7.5s and occasionally exceeding 10s under
* retransmits, tripping "TLS handshake failed: timed out" -> full-retry
* thrash. 25s lets a slow-but-valid handshake complete. Steady-state
* frame polling still uses a 200ms read timeout (set after connect). */
#define DERP_CONNECT_TIMEOUT_MS 25000

/* ============================================================================
* Custom BIO callbacks for TLS I/O
Expand Down Expand Up @@ -838,7 +843,19 @@ esp_err_t ml_derp_connect(microlink_t *ml) {
mbedtls_ssl_set_bio(&ml->derp.ssl, &ml->derp.sockfd,
ml_derp_bio_send, ml_derp_bio_recv, NULL);

/* TLS handshake - socket has 10s SO_RCVTIMEO from connect phase. */
/* Resume a prior TLS session if we saved one from a previous DERP
* connection (saved_session survives derp_free_tls_state across a reco).
* A resumed handshake skips ECDHE -> sub-second instead of ~7.5s. */
if (ml->derp.have_saved_session) {
int sret = mbedtls_ssl_set_session(&ml->derp.ssl, &ml->derp.saved_session);
if (sret == 0) {
ESP_LOGI(TAG, "DERP TLS: resuming saved session");
} else {
ESP_LOGW(TAG, "DERP TLS: set_session -0x%04x, doing full handshake", -sret);
}
}

/* TLS handshake - socket has SO_RCVTIMEO from connect phase. */
int ret;
while ((ret = mbedtls_ssl_handshake(&ml->derp.ssl)) != 0) {
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
Expand All @@ -862,6 +879,21 @@ esp_err_t ml_derp_connect(microlink_t *ml) {
ESP_LOGI(TAG, "[TIMING] DERP TLS handshake: %lld ms", (t_derp_tls - t_derp_tcp) / 1000);
ESP_LOGI(TAG, "TLS connected to DERP");

/* Save the negotiated session so the NEXT reconnect can resume it and
* skip the expensive full handshake. get_session deep-copies the ticket,
* so free any prior copy first. Best-effort: on failure we just do a full
* handshake next time. */
if (ml->derp.have_saved_session) {
mbedtls_ssl_session_free(&ml->derp.saved_session);
ml->derp.have_saved_session = false;
}
mbedtls_ssl_session_init(&ml->derp.saved_session);
if (mbedtls_ssl_get_session(&ml->derp.ssl, &ml->derp.saved_session) == 0) {
ml->derp.have_saved_session = true;
} else {
mbedtls_ssl_session_free(&ml->derp.saved_session);
}

/* HTTP Upgrade: GET /derp with Upgrade: DERP header */
char upgrade_req[256];
snprintf(upgrade_req, sizeof(upgrade_req),
Expand Down
15 changes: 14 additions & 1 deletion components/microlink/src/ml_wg_mgr.c
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ static int add_peer(microlink_t *ml, const ml_peer_update_t *update) {
p->trust_until_ms = 0;
p->last_send_ms = 0;
p->last_upgrade_ms = 0;
p->upgrade_interval_ms = ML_DISCO_UPGRADE_INTERVAL_MS;
p->has_direct_path = false;
p->best_ip = 0;
p->best_port = 0;
Expand Down Expand Up @@ -934,6 +935,9 @@ static void process_disco_pong(microlink_t *ml, const ml_rx_packet_t *pkt,
p->best_port = pkt->src_port;
p->has_direct_path = true;
p->trust_until_ms = now + ML_DISCO_TRUST_DURATION_MS;
/* Direct path established -> reset the upgrade backoff so a later
* loss + re-probe starts fast again. */
p->upgrade_interval_ms = ML_DISCO_UPGRADE_INTERVAL_MS;

/* Update WireGuard endpoint to direct path.
* Always update the stored endpoint. Only force a handshake if we
Expand Down Expand Up @@ -1475,11 +1479,20 @@ static void disco_periodic_probes(microlink_t *ml) {
/* Probe for direct path upgrade (every UPGRADE_INTERVAL when on DERP).
* Skip on cellular: direct paths impossible through carrier-grade NAT.
* Throttled to DISCO_PROBES_PER_TICK to spread load and reduce jitter. */
uint32_t up_interval = p->upgrade_interval_ms ? p->upgrade_interval_ms
: ML_DISCO_UPGRADE_INTERVAL_MS;
if (!ml_at_socket_is_ready() && !p->has_direct_path &&
now - p->last_upgrade_ms > ML_DISCO_UPGRADE_INTERVAL_MS) {
now - p->last_upgrade_ms > up_interval) {
if (upgrade_probes_sent < DISCO_PROBES_PER_TICK) {
disco_send_ping_to_peer(ml, i, false);
p->last_upgrade_ms = now;
/* Back off: this upgrade attempt is (so far) unanswered. Double
* the interval up to the cap so a P2P-hostile network doesn't
* make us re-probe every 15s forever. A direct pong resets it
* to the base interval (see the DISCO pong handler). */
uint32_t next = up_interval * 2;
if (next > ML_DISCO_UPGRADE_BACKOFF_MAX_MS) next = ML_DISCO_UPGRADE_BACKOFF_MAX_MS;
p->upgrade_interval_ms = next;
upgrade_probes_sent++;
}
}
Expand Down