[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-11641":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":16,"stars7d":17,"stars30d":18,"stars90d":15,"forks30d":15,"starsTrendScore":19,"compositeScore":20,"rankGlobal":9,"rankLanguage":9,"license":21,"archived":22,"fork":22,"defaultBranch":23,"hasWiki":24,"hasPages":22,"topics":25,"createdAt":9,"pushedAt":9,"updatedAt":26,"readmeContent":27,"aiSummary":28,"trendingCount":15,"starSnapshotCount":15,"syncStatus":14,"lastSyncTime":29,"discoverSource":30},11641,"meshtastic-sniffer","alphafox02\u002Fmeshtastic-sniffer","alphafox02","Wideband passive Meshtastic LoRa receiver with multi-station fusion and offline PSK recovery",null,"C",126,28,4,2,0,6,14,53,18,4.39,"Other",false,"main",true,[],"2026-06-12 02:02:33","# meshtastic-sniffer\n\nWideband passive Meshtastic LoRa receiver written in C. Captures one wide IQ slice from a single SDR and decodes **every Meshtastic channel and preset that fits in the slice simultaneously** -- no per-channel hopping, no missed packets. With keys supplied, decrypts in parallel: text messages, GPS positions, node info, telemetry, routing, traceroute, ATAK plugin packets, and more -- all surfaced from one capture.\n\nSister project to [iridium-sniffer](https:\u002F\u002Fgithub.com\u002Falphafox02\u002Firidium-sniffer) and [inmarsat-sniffer](https:\u002F\u002Fgithub.com\u002Falphafox02\u002Finmarsat-sniffer). Same SDR backend matrix, same threading style, same output ecosystem.\n\n## Features\n\n- Polyphase filterbank channelizer (AVX2\u002FSSE4.2\u002FNEON SIMD), one wide IQ stream into N parallel per-channel basebands at `Fs\u002FM` each\n- All 26 Meshtastic regions selectable per run via `--region=`: US (902-928), EU_868, EU_433, CN, JP, ANZ, KR, TW, RU, IN, NZ_865, TH, UA_433, UA_868, MY_433, MY_919, SG_923, KZ_433, KZ_863, NP_865, BR_902, PH_433\u002F868\u002F915, LORA_24. **One region per binary invocation** — Meshtastic regions span 433 MHz to 2.4 GHz, well beyond any commodity SDR's instantaneous bandwidth, so multi-region monitoring is run as multiple sniffer instances on different SDRs aggregated by [meshtastic-fusion](fusion\u002F).\n- All 9 standard presets: ShortTurbo, ShortFast, ShortSlow, MediumFast, MediumSlow, LongFast, LongMod, LongSlow, LongTurbo\n- Multi-key AES-128 \u002F AES-256-CTR with 1-byte channel-hash routing -- adding more keys does NOT slow per-packet decode (steady state: 1 AES op per packet)\n- Per-port protobuf decode for `TEXT_MESSAGE_APP`, `POSITION_APP`, `NODEINFO_APP`, `TELEMETRY_APP` (DeviceMetrics + EnvironmentMetrics + PowerMetrics), `ROUTING_APP`, `TRACEROUTE_APP`, `WAYPOINT_APP`, `ADMIN_APP`, `NEIGHBORINFO_APP`, `KEY_VERIFICATION_APP`, `MAP_REPORT_APP`, `ATAK_PLUGIN`, `REMOTE_HARDWARE_APP`, `DETECTION_SENSOR_APP`, `STORE_FORWARD_APP`, `PAXCOUNTER_APP`. Other ports surface as raw bytes in JSON.\n- ATAK port 72 decoder for `TAKPacket`: callsign, team, role, battery, PLI (lat\u002Flon\u002Falt\u002Fspeed\u002Fcourse), GeoChat. PLIs republished as CoT XML over multicast (see Outputs).\n- Off-grid LoRa scanner: occupied-bandwidth estimator + 3-confirm threshold flags LoRa-shaped energy outside the configured channel grid\n- Per-frame RSSI\u002FSNR + on-failure CRC + drift-only CFO telemetry carried through into the JSON event and the dashboard\n- Built-in web dashboard (Live map \u002F Activity \u002F Topology \u002F Config), runtime key + extra-freq additions without restart\n- JSON, UDP, MQTT, ZMQ PUB, CoT XML multicast, daily-rotated gzipped JSONL archive, libpcap streaming export output sinks\n- PSK dictionary attack against undecrypted frames (`--psk-wordlist=PATH`), geofence ENTRY\u002FEXIT alerts on positioned nodes (`--geofence=PATH`), CurveZMQ on the PUB socket (`--zmq-curve-secret=PATH`)\n- Replay-attack flagging on duplicate `(from, packet_id)` tuples beyond the normal mesh retransmit window\n- Multi-station deployment: emit ZMQ telemetry to a [meshtastic-fusion](fusion\u002F) aggregator, optionally self-register via `--announce-to=URL`, optional outbound DEALER socket (`--c2-dealer=tcp:\u002F\u002Ffusion:7009`) for NAT-friendly C2\n- `--schema` dumps the JSON event schema (JSON Schema 2020-12) so SIEM consumers can validate without guessing\n- AddressSanitizer- and ThreadSanitizer-clean build \u002F smoke-test pipeline\n\n## Use cases\n\nThe same passive observer fits three operational shapes:\n\n- **Operator \u002F surveyor** -- ham, EmComm coordinator, community organizer. \"What Meshtastic networks operate in my area? How busy is each preset? Are nodes reaching each other?\" Live map + Activity + Topology tabs. Default keys supplied.\n- **Pen tester \u002F red team** -- authorized engagement on a target's mesh. \"Do they use default channel keys? Are admin commands flying around unsigned? What's their actual range envelope?\" JSON feed for tooling, libpcap streaming export (`--pcap=PATH` \u002F `--pcap-fifo=PATH` for live Wireshark), PSK dictionary attack against unknown channels (`--psk-wordlist=PATH`).\n- **Defensive monitoring** -- site security wanting to know what's leaving the perimeter via LoRa. Off-grid scanner + daily-rotated gzipped JSONL archive (`--archive=DIR`) + geofence ENTRY\u002FEXIT alerts (`--geofence=PATH`).\n\nThe core is honest passive observation; the use case is mostly which outputs you wire up and which dashboard tabs you stare at.\n\n## Hardware capacity\n\nThe number of channels you stare at simultaneously is set by the SDR's analog bandwidth and your `--rate`. **Any rate works** -- the channelizer auto-fits whichever channels of the configured grid land inside `[center - rate\u002F2, center + rate\u002F2]`. The US ISM band is 902--928 MHz, so **~26 MHz of SDR bandwidth is the threshold to capture every US-band 250 kHz slot from one stare**; below that you cover a contiguous subset that the binary picks for you. Other regions are narrower (EU_868 is ~5 MHz, EU_433 is ~1.7 MHz), so a 20 MHz HackRF is full-coverage in most non-US regions.\n\n| SDR | Bandwidth | Coverage at default rate | Notes |\n|-----|-----------|--------------------------|-------|\n| HackRF One | 20 MHz | 409 channels at `--presets=all` US (41 LongFast slots + every other preset's slot grid that fits) | Most common config; partial cover of LongFast but every preset is decoded in parallel. Full coverage in EU_868 \u002F EU_433 \u002F most non-US regions |\n| BladeRF 2.0 | up to 56 MHz (AD9361) | All 104 US LongFast slots at ~26 Msps | Full US ISM coverage; headroom for adjacent-band scanning |\n| USRP B210 | up to 56 MHz (AD9361) | All 104 US LongFast slots at ~26 Msps | UHD-driven; same headroom |\n| SDRplay (RSPdx, RSP1A) | 10 MHz | One BW group + adjacent presets | Native API |\n| Airspy R2 \u002F Mini | 10 MHz | One BW group + adjacent presets | |\n| RTL-SDR (R820T) | 2.4 MHz | One BW group, ~9 LongFast slots | Cheap entry point |\n| Custom via SoapySDR | varies | -- | Generic |\n| VITA-49 \u002F VRT (network) | varies | -- | Remote IQ over UDP from any sender that emits VITA-49 (`[BIND:]PORT`, big-endian VRT signal-data, optional VRL wrapper). Same wire format as iridium-sniffer \u002F inmarsat-sniffer; if a sender works for those, it works for this. |\n| IQ file replay | -- | -- | Offline; auto-loads `.sigmf-meta` sibling for rate\u002Ffreq\u002Fformat |\n\n`--list` enumerates all attached SDRs across every compiled-in backend.\n\n`.\u002Fmeshtastic-sniffer --rate=20000000 --region=US --presets=all` -- without `--center`, the binary picks a sensible center from region + preset midpoints, logs the resolved coverage window, and warns if a user-supplied `--center` falls outside the configured region.\n\n### HackRF tuning notes\n\nThe single `--gain=DB` knob maps across HackRF's three independent stages, but real-world deployments often benefit from per-knob control. Use `--hackrf-lna=N` (0..40 step 8), `--hackrf-vga=N` (0..62 step 2), and `--hackrf-amp` \u002F `--hackrf-amp-off` to set them explicitly.\n\n**Watch out for close-range desense.** A Meshtastic node sitting 1-2 meters from your HackRF antenna can produce enough RF to overload the analog mixer regardless of LNA gain — the ADC won't clip (signal looks fine), but mixer intermodulation products inside the band corrupt the demod's symbol detection. Symptoms: high SNR (25-30 dB) but every frame from the close node has `payload_crc_ok: false`, and the topology fills with bit-flipped phantom node IDs (`!471c1b98` instead of the real `!433c0b98`, etc.). Two practical fixes:\n\n- Move the HackRF physically further from the node (5+ ft \u002F different room).\n- Inline a 6-10 dB SMA pad between the antenna and HackRF.\n\nDistant signals are unaffected by this and decode cleanly even with the close node hammering the front end. The `payload_crc_ok` field is the diagnostic; if you only see CRC failures from one specific node and clean frames from others, it's RF physics not a software bug.\n\n## Outputs\n\n- **JSON feed** to stdout (always when running) and to UDP endpoints (`--feed=HOST:PORT`, repeatable)\n- **MQTT** publish (`--mqtt=HOST[:PORT]`, topic `meshtastic\u002F\u003Cstation-id>` by default, override with `--mqtt-topic`)\n- **ZMQ PUB** for multi-consumer (`--zmq=tcp:\u002F\u002F*:7008`); optional CurveZMQ encryption with `--zmq-curve-secret=PATH` (generate keypair via `--zmq-curve-keygen=PATH`)\n- **CoT XML multicast** (`--cot-multicast=239.2.3.1:6969`) -- republishes every positioned node (regular Meshtastic POSITION packets *and* ATAK PLIs) as Cursor-on-Target XML to a multicast group. Any LAN ATAK-CIV \u002F WinTAK \u002F iTAK picks them up automatically -- no TAK Server required. CoT UIDs are prefixed with `--station-id` when set so multi-station deployments don't collide.\n- **PCAP** streaming export (`--pcap=PATH` for a rotating file, `--pcap-fifo=PATH` for a named pipe Wireshark can attach to live). Each LoRa frame is wrapped in DLT_USER0 with a libpcap header.\n- **Daily-rotated gzipped JSONL archive** (`--archive=DIR`) -- every emitted event appended to `DIR\u002Fmeshtastic-YYYYMMDD.jsonl.gz`, rotated at UTC midnight. SIEM-friendly.\n- **Built-in web dashboard** (`--web=8888`) -- four tabs (see below)\n\n## Web dashboard\n\n`--web=8888` opens four tabs:\n\n- **Live** -- Leaflet map with node markers + trail polylines (last 8 fixes per node). Nodes table with search box, sortable columns (ID \u002F Name \u002F SNR \u002F Frames \u002F Last seen), CSV export. Click any row to slide in a per-node drawer: name + id, metrics, last-60 SNR sparkline, recent text messages, recent positions, channels-seen-on. Channels table (by hash). Messages and Discoveries panels.\n- **Activity** -- per-preset cards, one per Meshtastic preset (ShortTurbo, ShortFast, ShortSlow, MediumFast, MediumSlow, LongFast, LongMod, LongSlow, LongTurbo). Each card shows fpm (60-second rolling), cumulative frames, decrypted\u002Ftotal channel ratio, sparkline, and the channel sub-list (channel name when decrypted, `(encrypted)` otherwise). Idle presets render greyed.\n- **Topology** -- force-directed graph. Nodes sized by frame count, edges colored by SNR. Real edges from `NEIGHBORINFO_APP` packets and resolved relay-hop hints. A synthetic \"RX\" station at canvas center has a faint dashed pseudo-edge to every node this sniffer hears, so even a sparse mesh with no NEIGHBORINFO traffic gives a useful picture (what *you* are receiving). Hover to highlight; click any real node to open the drawer.\n- **Config** -- runtime forms for adding keys, importing `meshtastic.org\u002Fe\u002F` channel-share URLs, adding extra-frequency decoder slots, changing CoT multicast destination -- all without restarting the binary.\n\nEquivalent endpoints exposed at `POST \u002Fapi\u002Fkeys`, `POST \u002Fapi\u002Fshare-url`, `POST \u002Fapi\u002Fextra-freq`, `POST \u002Fapi\u002Fcot-multicast` for scripting. Optional bearer-token auth via `--api-token=SECRET` (clients send `Authorization: Bearer SECRET`).\n\n## JSON event format\n\n`.\u002Fmeshtastic-sniffer --schema` dumps the canonical JSON Schema 2020-12 for every event the binary emits.\n\n**Per-frame fields:** `from`, `to`, `packet_id`, `channel_hash` (1-byte routing hash from the radio header), optional `slot_id` (which polyphase channelizer slot caught the frame), `hop_limit`\u002F`hop_start`, `rssi_db`\u002F`snr_db`.\n\n**Quality telemetry — silent when healthy, present when actionable:**\n- `payload_crc_ok: false` — CRC was present and failed (frame bytes are corrupt; absence implies CRC passed)\n- `cfo_hz` — only when |drift| > 100 Hz (radio is well-tuned otherwise)\n\n**Multilateration timing:** `station_t_ns` (host-realtime ns at first-replica receive) + `station_t_acc_ns` (operator-self-reported clock-discipline class). Set `--station-t-acc-ns=N` per station: 100 for GPSDO+1PPS, 1000 for chrony+PPS, 1000000 (default) for NTP-class. The fusion-side mlat solver weights observations by this value.\n\n**Decoded-port fields** (text, lat\u002Flon, telemetry, etc.) appear only when the channel key is known *and* the payload parses *and* the LoRa CRC passed. CRC-failed frames are gated explicitly: even when AES-CTR happens to produce parseable-looking bytes from corrupt ciphertext, those bytes are suppressed and the frame is reported as `decrypted: false`. This prevents fictitious POSITION \u002F TEXT_MESSAGE decodes from showing up on the dashboard or in the JSON feed.\n\n**Top-level `event` discriminator** distinguishes from regular packet events:\n- `STATS` — periodic msps + cumulative frames + decryption rate\n- `OFF_GRID_LORA` — scanner detected LoRa-shaped energy outside the configured grid\n- `REPLAY_SUSPECTED` — duplicate `(from, packet_id)` outside the normal mesh retransmit window\n- `GEOFENCE_ENTRY` \u002F `GEOFENCE_EXIT` — positioned node crossed a polygon boundary\n- `PSK_DISCOVERED` — `--psk-wordlist` attack found a key\n- `GEOLOCATED` — fusion-side mlat solver produced an emitter position estimate\n- `HEARTBEAT` — DEALER C2 session keepalive (when `--c2-dealer` is configured)\n\nThe JSON wire format changed in May 2026: the per-event `channel` field was renamed to `channel_hash` to reduce confusion with both decoder slot index and human-readable channel name. Downstream consumers reading the old field name need a one-line rename.\n\n## Stats heartbeat\n\nEvery 5 seconds the binary prints a one-line summary to stderr so you can tell at a glance whether samples are flowing and frames are being decoded:\n\n```\n[stats] 18.45 Msps in, 12 LoRa frames, 9 decrypted\n```\n\n`off-grid hits` is appended only when the scanner is enabled (`--scan*` or `--alert-off-grid`).\n\n## Build\n\n```bash\nmkdir build && cd build\ncmake ..\nmake -j$(nproc)\n```\n\nDependencies: cmake >= 3.9, FFTW3 (float), pthreads, OpenSSL (AES-CTR), and at least one SDR library for live capture. CMake auto-detects every backend you have installed.\n\n```bash\nsudo apt install build-essential cmake pkg-config libfftw3-dev libssl-dev\nsudo apt install librtlsdr-dev libhackrf-dev libbladerf-dev libuhd-dev \\\n                 libsoapysdr-dev libairspy-dev\n# SDRplay native API: install from sdrplay.com\u002Fapi\n# Optional output sinks\nsudo apt install libmosquitto-dev libzmq3-dev\n```\n\n## First-time use (30 seconds)\n\nYou have a Meshtastic node and an SDR. You want to see what your node and its neighbours are saying.\n\n1. **Get your channel key.** In the Meshtastic phone app, open the channel, tap \"Share Channel,\" and copy the URL -- it looks like `https:\u002F\u002Fmeshtastic.org\u002Fe\u002F#CgM...`. That URL contains the channel name and the AES key in a small protobuf payload.\n\n2. **Plug in the SDR.** Run `.\u002Fmeshtastic-sniffer --list` to confirm it's detected.\n\n3. **Run with a sensible default + paste your channel URL:**\n\n   ```bash\n   .\u002Fmeshtastic-sniffer --hackrf --share-url='https:\u002F\u002Fmeshtastic.org\u002Fe\u002F#CgM...' --web=8888\n   ```\n\n   The binary picks a sample rate and center frequency from the SDR + region (`US` by default -- override with `--region=EU_868` etc.). It opens the dashboard at `http:\u002F\u002Flocalhost:8888`.\n\n4. **If nothing shows up after a minute**, check stderr -- the binary prints loud warnings when no samples are flowing or no LoRa frames have decoded. Common causes: gain too low (`--gain=40`), wrong region, no node in range. The HackRF default is LNA=0 \u002F VGA=30 -- right for close traffic but turn `--gain` up for distant captures.\n\n5. **Adding more channels later:** open the dashboard, click **Config** tab, paste another `meshtastic.org\u002Fe\u002F` URL, hit Add. Done -- no restart needed.\n\n## Quickstart\n\n```bash\n# Casual: defaults pick up rate\u002Fcenter for your SDR + region.\n.\u002Fmeshtastic-sniffer --hackrf --keys=default --web=8888\n\n# Or paste your channel-share URL once and skip the dashboard:\n.\u002Fmeshtastic-sniffer --hackrf --share-url='https:\u002F\u002Fmeshtastic.org\u002Fe\u002F#CgM...' --web=8888\n\n# Power: stare at every preset on US 902-928, dump per-channel stats every 5s,\n# tee raw IQ to disk for later replay, multi-station-tagged feed to a collector:\n.\u002Fmeshtastic-sniffer --bladerf --presets=all \\\n                    --keys-file=$HOME\u002F.config\u002Fmeshtastic-sniffer\u002Fkeys \\\n                    --stats-json=\u002Frun\u002Fmeshsniff\u002Fstats.json \\\n                    --iq-record=\u002Fdata\u002Fcapture-$(date +%s).cs8 \\\n                    --feed=collector:5588 --station-id=basement-rx --web=8888\n\n# Receive over a network feed (any sender that emits VITA-49 VRT):\n.\u002Fmeshtastic-sniffer --vita49=4991 --keys=default --web=8888\n# (sample rate + center freq come from the VITA-49 IF-context packets\n#  automatically when the sender emits them; otherwise pin via --rate\u002F--center)\n\n# US LongFast on a HackRF with explicit overrides:\n.\u002Fmeshtastic-sniffer --hackrf --region=US --presets=LongFast \\\n                    --rate=20000000 --center=910000000 \\\n                    --keys=default --web=8888\n\n# Replay an IQ capture (sample rate \u002F freq \u002F format pulled from .sigmf-meta):\n.\u002Fmeshtastic-sniffer --file=capture.cf32 --keys=default\n\n# Multi-output: stdout JSON + UDP feed + MQTT + ZMQ + CoT multicast + web\n.\u002Fmeshtastic-sniffer --hackrf --keys=LongFast=default,Ops=hex:00112233...ff \\\n                    --feed=collector:5588 --mqtt=mqtt.local \\\n                    --zmq=tcp:\u002F\u002F*:7008 --cot-multicast=239.2.3.1:6969 \\\n                    --web=8888 --station-id=basement-rx\n\n# Off-grid scan only (no decode, just discover non-standard LoRa freqs):\n.\u002Fmeshtastic-sniffer --hackrf --scan --alert-off-grid\n\n# Long-running deployment: gzipped daily archive, geofence alerts, PSK dictionary\n# attack against unknown channels, replay-attack flagging on duplicate (from, pid):\n.\u002Fmeshtastic-sniffer --hackrf --keys-file=$HOME\u002F.config\u002Fmeshtastic-sniffer\u002Fkeys \\\n                    --archive=\u002Fvar\u002Flog\u002Fmeshtastic \\\n                    --geofence=$HOME\u002F.config\u002Fmeshtastic-sniffer\u002Fzones.ini \\\n                    --psk-wordlist=\u002Fusr\u002Fshare\u002Fdict\u002Fwords \\\n                    --pcap=\u002Fdata\u002Fmeshtastic.pcap --web=8888\n\n# Multi-station: register with a meshtastic-fusion aggregator on the same VPN,\n# expose a NAT-friendly DEALER C2 socket back to it.\n.\u002Fmeshtastic-sniffer --hackrf --keys=default --station-id=rooftop \\\n                    --gpsd=localhost:2947 --web=8888 \\\n                    --zmq=tcp:\u002F\u002F*:7008 \\\n                    --announce-to=http:\u002F\u002Ffusion.local:9000\u002Fapi\u002Fsensors \\\n                    --c2-dealer=tcp:\u002F\u002Ffusion.local:7009\n\n# List every SDR you have plugged in:\n.\u002Fmeshtastic-sniffer --list\n\n# Run the self-tests (channelizer routing + AES end-to-end + JSON output):\n.\u002Fmeshtastic-sniffer --selftest\n```\n\n## Generating test IQ from gr-lora_sdr\n\nIf you have `gnuradio` and `gr-lora_sdr` installed (`python3 -c \"from gnuradio import lora_sdr\"` should succeed), you can generate a known-good Meshtastic-shaped IQ file without any radio hardware:\n\n```bash\npython3 tools\u002Fgen_meshtastic_iq.py --out=\u002Ftmp\u002Fmeshtastic_test.cf32 \\\n                                    --text=\"Hello\" --sf=11 --bw=250000 --cr=5\n.\u002Fmeshtastic-sniffer --file=\u002Ftmp\u002Fmeshtastic_test.cf32 --rate=250000 \\\n                    --center=903000000 \\\n                    --extra-freq=903000000:bw=250000:sf=11:cr=5 \\\n                    --keys=default\n```\n\nThe generator builds a real Meshtastic frame (16-byte radio header + AES-128-CTR encrypted Data envelope with channel-hash for the default key) and runs it through gr-lora_sdr's modulator. Useful both as a smoke test of the whole pipeline and for iterating on the LoRa demod's sync\u002Fheader\u002Fpayload decoding.\n\n`MESHTASTIC_LORA_TRACE=1` enables a per-symbol state-machine trace on stderr, useful for debugging decode against a known-good reference.\n\n## Self-test and smoke test\n\n`.\u002Fmeshtastic-sniffer --selftest` runs two checks:\n\n1. **Channelizer**: synthesizes a 0.1 sec tone at 902.625 MHz inside a 20 MHz capture centered on 910 MHz; configures four 250 kHz channels at the US LongFast slot 0..3 grid; verifies the tone lands in slot 2 with the expected power profile.\n2. **AES + multi-key + protobuf**: builds a synthetic Meshtastic packet (`TEXT_MESSAGE_APP`, payload `\"Hello\"`, encrypted with the default key), runs it through the decode path, and verifies the callback fires with the right port + payload + channel name.\n\n`bash tests\u002Ftest_smoke.sh` adds SigMF auto-config, `--list`, web `\u002Fapi\u002F*` round-trip, STATS SSE heartbeat, and stats heartbeat. Both tests run clean under AddressSanitizer + UndefinedBehaviorSanitizer; ThreadSanitizer reports no data races on the keyset rwlock under concurrent `\u002Fapi\u002Fkeys` POST load.\n\n## Honest limitations\n\nThings this tool does not do today:\n\n- No direction finding from a single SDR -- amplitude alone can't localize an emitter. Multilateration via 3+ stations works but requires GPSDO-locked SDRs for sub-100m accuracy (Tier-1); chrony+PPS hosts give ~300 m (Tier-2).\n- 16 of the known port numbers are decoded into structured fields; others surface as raw bytes in the JSON event\n- AdminMessage signature verification (ed25519) is not implemented yet -- admin packets are decoded but not validated\n- CurveZMQ is sniffer-side only; the Go-based [meshtastic-fusion](fusion\u002F) aggregator can't yet authenticate to a CURVE-protected PUB (limitation of `go-zeromq\u002Fzmq4` v0.17). Use a libzmq-based proxy or VPN-gate the link.\n- Close-range front-end overload: a Meshtastic node within 1-2 m of the HackRF antenna corrupts demod via mixer intermodulation regardless of LNA gain. Surfaced via `payload_crc_ok: false`; mitigated by physical separation or an inline RF attenuator. See the *HackRF tuning notes* section above.\n\n## Offline PSK recovery\n\nA companion CLI tool [meshtastic-recover](recover\u002F) reads captured pcaps (`--pcap=PATH`) and a wordlist, runs the same channel-hash prefilter + AES-CTR + protobuf-shape verifier the live decoder uses, and prints any keys that successfully decrypt one or more captured frames. OpenMP-parallel across all CPU cores. Output is in `--keys-file=` compatible format ready to feed back to the sniffer.\n\n```bash\n# Try the firmware default keys (simple1..simple255) against a capture\n.\u002Fmeshtastic-recover --pcap=session.pcap --simple-keys --output=recovered.keys\n\n# Add a passphrase wordlist (keeps default-key trials, adds dictionary attack)\n.\u002Fmeshtastic-recover --pcap=session.pcap --simple-keys --wordlist=\u002Fusr\u002Fshare\u002Fdict\u002Fwords\n\n# Re-decode the same capture using whatever keys were recovered\n.\u002Fmeshtastic-sniffer --file=session.pcap --keys-file=recovered.keys\n```\n\nGPU acceleration via a [hashcat](https:\u002F\u002Fhashcat.net) custom-mode plugin (status: working end-to-end on real-radio captures, pending upstream PR cleanup). The recover binary produces hashcat-compatible hash files via `--hashcat-export=PATH` and `--channel-name=NAME`. See [recover\u002FREADME.md](recover\u002FREADME.md) for the format spec and verifier algorithm.\n\nRealistic attack surface: factory-default channels recover instantly; weak-passphrase channels recover from a rockyou-class wordlist in seconds; channels using a strong randomly-generated 16\u002F32-byte PSK are not feasible to recover.\n\n## License\n\nGPL-3.0-or-later. See `LICENSE`. Copyright (c) 2026 CEMAXECUTER LLC.\n\nThis project is independent of and not affiliated with Meshtastic. \"Meshtastic\" is a trademark of [Meshtastic LLC](https:\u002F\u002Fmeshtastic.org). Protocol constants used here (default PSK, channel hash, AES-CTR nonce layout, region\u002Fpreset tables) are interoperability facts derived from the upstream firmware at \u003Chttps:\u002F\u002Fgithub.com\u002Fmeshtastic\u002Ffirmware> (also GPL-3.0-or-later); no proprietary code is included.\n\n### Upstream attribution\n\n- **gr-lora_sdr** by Joachim Tapparel @ EPFL TCL Lab (\u003Chttps:\u002F\u002Fgithub.com\u002Ftapparelj\u002Fgr-lora_sdr>, GPL-3.0-or-later) -- significant portions of `lora.c`'s bit-level decode path (hard-decode Hamming, deinterleave, gray, dewhiten, preamble-mode-vote) are ported from gr-lora_sdr and verified bit-exact against its stage outputs. Per-stage citations appear inline at the relevant call sites in `lora.c`.\n- **Meshtastic firmware** (\u003Chttps:\u002F\u002Fgithub.com\u002Fmeshtastic\u002Ffirmware>, GPL-3.0-or-later) -- the wire format, default PSK, simpleN PSK derivation, channel-hash function, AES-CTR nonce layout, region\u002Fpreset tables, and port number assignments are all derived from the upstream firmware. Implementation here is original; only the on-the-air constants come from upstream.\n- **Felipe Kersting** -- `blocking_queue.h` and `fair_lock.h` are vendored MIT-licensed primitives (Copyright (c) 2020). License preserved in each file.\n","meshtastic-sniffer 是一个用 C 语言编写的宽频带被动 Meshtastic LoRa 接收器，能够同时解码单个 SDR 捕获的宽 IQ 切片中的所有 Meshtastic 信道和预设。其核心功能包括多站融合、离线 PSK 恢复及并行解密多种类型的数据包如文本消息、GPS 位置等。该项目支持选择 26 个不同的 Meshtastic 区域，并且可以处理 9 种标准预设配置。此外，它还提供了丰富的输出选项，比如 JSON、UDP、MQTT 等，并集成了内置的 Web 仪表板以实时监控网络活动。meshtastic-sniffer 特别适用于需要对特定区域内的 LoRa 通信进行全面监听与分析的场景，如物联网设备测试、安全审计或研究用途。","2026-06-11 03:32:13","CREATED_QUERY"]