Replace the white-background pixel bubble with the radar bubble: a green
chat bubble with two broadcast rings on a near-black ground. The rings
open toward the tail (bottom-right, like BitChatIcon) and all shapes stay
inside the 66dp adaptive-icon safe zone.
The notification small icon and ongoing-activity static icon now use the
same glyph instead of a stock chat bubble, and the monochrome layer
matches the launcher.
AGENTS.md requires protocol and security changes to update the relevant
specification. sync.md already carries the receiver-side DoS rules — the
filter-length cap and the malformed-payload rejections — but says nothing
about how often a request may be serviced or how much it may pull, which
is the gap this branch closes in the Android client.
Record it where the other receiver limits live, including why the budget
cannot be keyed on the sender ID: REQUEST_SYNC carries no signature that
receivers verify, so that field is free to rotate. Other implementations
need some equivalent even if they pick different numbers.
SecurityManager.verifyPacketSignature only authenticates ANNOUNCE,
MESSAGE, FILE_TRANSFER, VOICE_FRAME and LEAVE; every other type returns
early as verified. REQUEST_SYNC is not in that set, so the sender ID is
raw bytes off the wire and costs nothing to rotate.
Keyed on that, the budget was bypassable: cycling more IDs than the LRU
tracks gives every request a brand new bucket with a full allowance, so
the flood this was meant to stop still went through. Found by Codex on
the first push.
Key it on RoutedPacket.ingressLinkID instead — assigned by the transport,
never serialized onto the mesh, and distinct per link. A flooder is then
bounded by the connections it can actually hold open rather than by the
names it is willing to invent. Falls back to the sender ID only when a
transport supplies no link identity.
Pins both halves: a flood is shed after the burst allowance and cannot
outrun the refill window, and a peer syncing normally still receives
every packet it is missing. Also covers per-peer isolation, a backwards
clock jump, and the cap on the requester table.
Reverting the admission check fails four of the eight; the other four are
the honest-peer guards and pass either way, which is what they are for.
REQUEST_SYNC is a public packet, handled in PacketProcessor's
unauthenticated branch and passed straight to GossipSyncManager. Nothing
between the radio and the response limits how often a neighbor may send
one, so a single peer decided how often we decode a GCS filter, run
SHA-256 over every stored packet, and transmit whatever its filter
claims to be missing.
A filter that claims to hold nothing is a few bytes and makes us replay
up to seenCapacity() packets — 500 by default. Repeating it costs the
sender nothing and saturates a link measured in kbps.
Give each requester a token bucket: one request per 5s with a burst of
2, so the scheduled first sync and a periodic one both land, and a
response allowance that refills to seenCapacity() over the 30s periodic
interval — the most a peer can legitimately be missing in one round. An
honest peer never reaches either limit, so what it receives is unchanged.
The bookkeeping is keyed by a peer ID off the wire, so it is an
access-ordered LinkedHashMap capped at 256 entries; otherwise the
mitigation would be its own memory exhaustion vector.
If SQLite clearAll fails mid-panic, in-memory state was already cleared
but a process restart could reload encrypted history from disk (#699).
Fall back to deleting the database files before reporting failure.