Symptom
After upgrading a device (LilyGo T-Display-S3, zen-clock firmware) from microlink 3.0.0 to
3.1.0, microlink_get_vpn_ip() returns 0 (displayed as 0.0.0.0) indefinitely, even though
microlink_get_state() reports ML_STATE_CONNECTED and the device is clearly functional at
the relay/peer layer:
- DERP connects successfully (
DERP handshake complete, connected, DERP status: connected=1)
- DISCO probing and PONG exchanges succeed with ~10 known peers (RTT 30-900ms range)
- The control-plane long-poll stream is alive and receiving updates
(ml_coord: Long-poll MapResponse update received)
- No
RegisterResponse error, no Registration not authorized by control plane
(MachineAuthorized check), no ML_STATE_AUTH_FAILED anywhere in the log
Despite this, the device's "Last seen" on the Tailscale admin console stays stuck at a fixed
timestamp and does not advance, even after several minutes of confirmed DERP/control-plane
activity — consistent with the control plane not treating this session as a fresh, fully
registered check-in.
What's ruled out
Diffed components/microlink/src/ml_coord.c between v3.0.0 and v3.1.0: the
Node.Addresses → ml->vpn_ip parsing block in do_register() is byte-for-byte unchanged
between the two versions. The only new registration-path logic in 3.1.0 is the added
MachineAuthorized/Error check (#51's PR, closes #27) — and that would produce a distinct
ML_STATE_AUTH_FAILED + "Registration not authorized by control plane" log line, which does
not appear. So this isn't (as far as the log evidence shows) that new check misfiring.
Known confound
ml_peer_nvs.c caches the peer table across reboots ("Loaded %d cached peers from NVS") and
that cache is loaded unconditionally at microlink_start(), independent of whether the current
session's registration actually completed with a valid Node.Addresses in the response. So
DERP connecting and DISCO-probing cached peers successfully does not by itself prove this
session's registration returned a usable self IP — it's necessary but not sufficient evidence
of a fully successful register.
What's still unconfirmed
Could not yet capture the actual RegisterResponse body / the "Our VPN IP: %s" log line for
this session — it fires within the first ~9s of boot, and reset-triggered serial captures on
this board (ESP32-S3 native USB-CDC re-enumerates on reset) keep missing that exact window.
Repro environment
- Board: LilyGo T-Display-S3 (ESP32-S3)
- microlink 3.1.0 (component_hash
9fb3633d7b0104b804722af15246579ef4c42531d22b00ae2271454d07e478e4),
wireguard_lwip 1.0.3 transitively
CONFIG_ML_ENABLE_CONFIG_HTTPD off, CONFIG_ML_MAX_PEERS=16
- Same device/auth key/tailnet worked correctly on 3.0.0 prior to this upgrade (no other change
besides the dependency bump — consuming project's manifest, Kconfig-relevant settings, and
WiFi/network environment are otherwise unchanged)
Next step
Plan to add temporary ESP_LOGI instrumentation around the Node.Addresses parse in
do_register() to dump the raw RegisterResponse JSON for one boot and see exactly what's
missing or malformed, then report back here with findings.
Symptom
After upgrading a device (LilyGo T-Display-S3, zen-clock firmware) from microlink 3.0.0 to
3.1.0,
microlink_get_vpn_ip()returns 0 (displayed as0.0.0.0) indefinitely, even thoughmicrolink_get_state()reportsML_STATE_CONNECTEDand the device is clearly functional atthe relay/peer layer:
DERP handshake complete, connected,DERP status: connected=1)(
ml_coord: Long-poll MapResponse update received)RegisterResponse error, noRegistration not authorized by control plane(
MachineAuthorizedcheck), noML_STATE_AUTH_FAILEDanywhere in the logDespite this, the device's "Last seen" on the Tailscale admin console stays stuck at a fixed
timestamp and does not advance, even after several minutes of confirmed DERP/control-plane
activity — consistent with the control plane not treating this session as a fresh, fully
registered check-in.
What's ruled out
Diffed
components/microlink/src/ml_coord.cbetweenv3.0.0andv3.1.0: theNode.Addresses→ml->vpn_ipparsing block indo_register()is byte-for-byte unchangedbetween the two versions. The only new registration-path logic in 3.1.0 is the added
MachineAuthorized/Errorcheck (#51's PR, closes #27) — and that would produce a distinctML_STATE_AUTH_FAILED+"Registration not authorized by control plane"log line, which doesnot appear. So this isn't (as far as the log evidence shows) that new check misfiring.
Known confound
ml_peer_nvs.ccaches the peer table across reboots ("Loaded %d cached peers from NVS") andthat cache is loaded unconditionally at
microlink_start(), independent of whether the currentsession's registration actually completed with a valid
Node.Addressesin the response. SoDERP connecting and DISCO-probing cached peers successfully does not by itself prove this
session's registration returned a usable self IP — it's necessary but not sufficient evidence
of a fully successful register.
What's still unconfirmed
Could not yet capture the actual
RegisterResponsebody / the"Our VPN IP: %s"log line forthis session — it fires within the first ~9s of boot, and reset-triggered serial captures on
this board (ESP32-S3 native USB-CDC re-enumerates on reset) keep missing that exact window.
Repro environment
9fb3633d7b0104b804722af15246579ef4c42531d22b00ae2271454d07e478e4),wireguard_lwip 1.0.3 transitively
CONFIG_ML_ENABLE_CONFIG_HTTPDoff,CONFIG_ML_MAX_PEERS=16besides the dependency bump — consuming project's manifest, Kconfig-relevant settings, and
WiFi/network environment are otherwise unchanged)
Next step
Plan to add temporary
ESP_LOGIinstrumentation around theNode.Addressesparse indo_register()to dump the rawRegisterResponseJSON for one boot and see exactly what'smissing or malformed, then report back here with findings.