Skip to content

3.1.0: vpn_ip stays 0.0.0.0 while state=CONNECTED, control-plane last-seen never advances #72

Description

@fudio101

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.Addressesml->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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions