Skip to content

whatsapp_native: allow_from silently drops all messages from LID-migrated accounts (format mismatch + device-index drift) #2540

@aporb

Description

@aporb

I hit this while deploying the whatsapp_native channel on an account that WhatsApp has migrated to Linked-Device ID (LID) format. Every incoming message was silently dropped despite the sender being in allow_from. Took a while to track down because nothing appears in the logs at INFO level.


Reproduction

Config (~/.picoclaw/config.json):

{
  "channels": {
    "whatsapp": {
      "enabled": true,
      "use_native": true,
      "session_store_path": "/absolute/path/to/session/",
      "allow_from": ["+15550001234"]
    }
  }
}
  1. Build with -tags whatsapp_native, pair via QR.
  2. The paired device is LID-migrated — evt.Info.Sender.String() returns 100000000000001:1@lid.
  3. Send a message from that number. No reply. No log output at INFO.
  4. Enable debug logging: "sender_id": "100000000000001:1@lid" appears, but the allow check already returned before that line is reached.

Observed symptom

[DEBUG] whatsapp_native: WhatsApp message received  sender_id=100000000000001:1@lid  content_preview=<redacted test message>

…but only after the IsAllowedSender check passes. When it fails (as it always does with a phone-format allow list vs. a LID sender), the handler returns silently at line 277 of pkg/channels/base.go with zero log output at WARN or above.


Root cause

Two separate problems compound each other.

1. Format mismatch between documentation and wire format.

PR #1620 (docs/channels/whatsapp/README.md) documents allow_from as taking E.164 phone numbers:

allow_from — Phone number whitelist in E.164 format (e.g. ["+15550001234"]). Leave empty [] to accept messages from all numbers.

In whatsapp_native.go (line 349), the sender ID is set from evt.Info.Sender.String(). For a LID-migrated account whatsmeow returns 100000000000001:1@lid — not an E.164 string.

That value lands in SenderInfo.PlatformID and SenderInfo.CanonicalID (lines 383–384):

sender := bus.SenderInfo{
    Platform:    "whatsapp",
    PlatformID:  senderID,                                     // "100000000000001:1@lid"
    CanonicalID: identity.BuildCanonicalID("whatsapp", senderID), // "whatsapp:100000000000001:1@lid"
}

IsAllowedSender in pkg/channels/base.go (line 249) iterates the allow list and calls identity.MatchAllowed for each entry (line 255).

MatchAllowed in pkg/identity/identity.go (line 41) first tries canonical matching. It calls ParseCanonicalID("+15550001234") — finds no colon, returns ok=false — then falls through to the PlatformID exact-string comparison at line 77:

if sender.PlatformID != "" && sender.PlatformID == allowedID {
    return true
}

"100000000000001:1@lid" == "+15550001234" is false. Every non-LID entry in the list fails the same way. The function returns false, the handler returns at line 277, and nothing is logged.

2. Device-index drift.

Even after an operator discovers the mismatch (e.g. via debug logs) and locks the list to "100000000000001:1@lid", the :1 device/agent index is not stable. WhatsApp session housekeeping can reassign it (:2, :3, …) during normal operation. The exact-string check then silently breaks again without any log warning.


Possible directions

I'm not sure which fix is right here — happy to defer to maintainers:

  • Strip device suffix for LID matching: accept <user>@lid in allow_from and match it against any <user>:<N>@lid on the wire. This is the approach least likely to break non-LID paths.
  • Resolve LID → phone before comparison: use client.Store.Contacts (whatsmeow) to look up the E.164 number for a LID sender, then compare against the documented phone format. More work but makes the documented behaviour actually work.
  • At minimum, emit a WARN when formats are mismatched: if every entry in the allow list is E.164 but the incoming sender is @lid (or vice versa), log a breadcrumb like "allow_from entries appear to be phone numbers but sender is LID-format; messages will be silently dropped". This alone would have saved me a couple of hours.

Happy to put together a PR if there's a preferred direction — just let me know if there's related work in flight so I can coordinate.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions