donglora-bridge
donglora-bridge relays raw LoRa packets over the internet. Every packet your radio hears is forwarded to every other bridge running the same passphrase, and their radios retransmit it locally. Because it operates on raw packets, it works with any protocol built on LoRa -- Meshtastic, MeshCore, or anything else -- simultaneously and without configuration. If your radio can hear it, the bridge will relay it.
LoRa is a long-range, low-power wireless modulation used by off-grid mesh networks. A single radio might cover a few kilometers. This bridge removes that limit, extending a local LoRa network across a larger geographic area.
There is no central server. Bridges discover each other automatically using the Mainline DHT and communicate through an encrypted peer-to-peer gossip swarm.

What you need
1. A DongLoRa radio
DongLoRa is open-source firmware that turns a supported LoRa development board (such as a Heltec WiFi LoRa 32 or similar ESP32+SX1262 board) into a USB LoRa dongle. You will need to:
- Get a board -- any ESP32 board with an SX1262 LoRa radio that DongLoRa supports. See the DongLoRa README for the full list.
- Flash the DongLoRa firmware onto the board. Instructions are in the DongLoRa repository.
- Plug it in via USB. The board appears as a serial device
(e.g.
/dev/ttyACM0on Linux).
2. (Optional) donglora-mux
The bridge works fine without the mux -- it connects directly to the USB device. The mux is only needed if you want multiple programs to share a single DongLoRa radio at the same time (e.g. running two bridges with different configs, or a bridge alongside another donglora-client program).
Run donglora-mux before starting the bridge. The bridge auto-detects the mux
when it's running and falls back to direct USB serial when it's not.
3. donglora-bridge (this project)
Install via cargo:
Or via mise:
Or build from source:
Requires Linux. macOS may work but is untested.
Quick start
1. Plug in your DongLoRa board and (optionally) start the mux
2. Run the bridge
On first run, an interactive wizard walks you through configuration:
- Passphrase (required) -- every bridge using the same passphrase joins the same network. Pick something unique to your group.
- Radio settings -- frequency, bandwidth, spreading factor, etc. The defaults match MeshCore US/Canada recommended values. If you're bridging a MeshCore network, just press Enter through these.
The config is saved to ~/.config/donglora-bridge/config.toml. To change it
later, run donglora-bridge config.
3. That's it
The TUI shows real-time stats. On another machine (or continent), repeat the same steps with the same passphrase. The two bridges will discover each other via DHT and start relaying packets within seconds.
Running multiple bridges
To run more than one bridge on the same machine (e.g. bridging two separate
MeshCore networks), create a config file per bridge and run each with
--config:
Each config can specify a different passphrase, different radio settings, and
(if using the mux) share the same radio. Without the mux, each bridge needs its
own USB dongle and an explicit port in its config to avoid both trying to open
the same device.
Features
- Gossip swarm -- broadcast packets via iroh-gossip, a QUIC-based overlay network
- Zero-config peer discovery -- automatic bootstrapping via BitTorrent mainline DHT (BEP44 mutable items)
- Content-hash deduplication -- blake3 hashing suppresses duplicate packets across all bridges
- Air-time-aware rate limiting -- token bucket calculated from LoRa radio parameters (Semtech LoRa time-on-air formula)
- Real-time terminal UI -- network status, radio config, packet log, activity sparklines
- Resilient radio connection -- exponential backoff reconnection with liveness pings
- Headless mode -- structured logs to stdout for servers (
--log-only) - Interactive setup wizard -- guided first-run configuration
- Shared passphrase security -- all cryptographic keys derived deterministically from a single passphrase
TUI overview
| Panel | Content |
|---|---|
| Network | Topic ID, peer count, DHT status, last publish time |
| Radio | Device, frequency, bandwidth, SF, CR, sync word, TX power, preamble, CAD |
| Stats | Uptime, RF/Net RX/TX, dedup hits, rate limit drops, queue drops |
| Packet Log | Scrolling log: age, hash, RF/Net direction, size, RSSI, SNR, action |
| Activity | Sparkline charts: RX rate, TX rate, average SNR (1-second buckets, 5-minute window) |
Press q or Ctrl+C to shut down gracefully.
Configuration
Default location: ~/.config/donglora-bridge/config.toml
[]
# port = "/dev/ttyACM0" # serial port (omit for auto-detect: tries mux, then USB)
= 910525000 # Hz (default: 910.525 MHz)
= "62.5kHz" # 7.8kHz, 10.4kHz, 15.6kHz, 20.8kHz, 31.25kHz,
# 41.7kHz, 62.5kHz, 125kHz, 250kHz, 500kHz
= 7 # 7-12
= 5 # 5-8 (meaning 4/5 to 4/8)
= 0x3444 # hex, 2 bytes
= "22" # dBm integer or "max"
= 16 # symbols
= true # channel activity detection
[]
= "your-shared-secret" # REQUIRED: all bridges with same passphrase join the same swarm
= 300 # duplicate suppression window (default: 5 minutes)
= 32 # max pending radio TX packets
# rate_limit_pps = 5.0 # optional: override calculated TX rate (packets/sec)
# log_file = "/path/to/bridge.log" # optional: override log path
Default radio settings match MeshCore US/Canada recommended parameters.
How it works
Passphrase key derivation
A single passphrase deterministically derives three values:
- Topic ID --
blake3(APP_PREFIX + passphrase)-- identifies the gossip swarm - DHT signing key -- ed25519 from
SHA-256(APP_PREFIX + "sign:" + passphrase)-- authenticates DHT records - DHT salt -- first 20 bytes of
SHA-256(APP_PREFIX + "salt:" + passphrase)-- namespaces the DHT key
All nodes with the same passphrase compute identical keys and automatically join the same network.
Gossip swarm
Each bridge creates an ephemeral iroh endpoint (fresh ed25519 identity per
launch) and subscribes to the derived topic. Radio packets are wrapped in a
GossipFrame (32-byte sender ID + RSSI + SNR + payload) and broadcast to all
neighbors. Received frames are decoded and queued for radio transmission.
DHT peer discovery
Peers find each other through BitTorrent's mainline DHT using BEP44 mutable
items. Peer lists are encoded as [count:u16 LE][id:32B]... (max 27 peers per
record to stay under the 1000-byte value limit).
When alone (no neighbors): reads DHT every 3 seconds, merge-publishes every 30 seconds (preserves existing peers, never destructive).
When connected: relaxed 5-minute heartbeat with random jitter. Publishes self + live neighbors only (purges stale peers).
On shutdown: publishes neighbors without self, so other nodes can still bootstrap from live peers.
Deduplication
Every packet payload is hashed with blake3 (32 bytes). A time-bounded cache (default 300s, pruned every 30s) suppresses duplicates from both radio and gossip directions.
Rate limiting
A token bucket rate limiter prevents radio channel saturation. The rate is calculated from the LoRa air time formula (Semtech SX1276):
air_time = preamble_time + payload_symbol_count * symbol_time
rate = 0.5 / air_time (50% duty cycle target)
Default burst allowance is 3 packets. The rate can be manually overridden with
rate_limit_pps in the config.
Radio communication
A dedicated blocking thread communicates with the DongLoRa hardware via
donglora-client. Features:
- Config negotiation: reads existing config first; only sets if needed
- Exponential backoff reconnection: 250ms to 5s, resets after 5s of uptime
- Liveness pings: every 2s of idle, detects silently-dead connections
- SNR filtering: drops packets below the demodulation floor
Architecture
┌─────────────────────────────────────┐
│ donglora-bridge │
│ │
DongLoRa ┌────────┐│ ┌────────┐ ┌──────┐ ┌───────┐ │ Internet
USB dongle◄─┤ Radio ├┤◄─┤ Router ├──►│Gossip├──►│ iroh ├──┤◄──► Other
│ Thread ││─►│ (dedup ││ │Swarm │ │ QUIC │ │ Bridges
└────────┘│ │ rate ││ └──┬───┘ └───────┘ │
│ │ limit)│| │ │
│ └────────┘│ ┌──┴───┐ │
│ ▲ │ │ DHT │ │
│ │ │ │(peer │ │
│ ┌──┴──┐ │ │disc.)│ │
│ │ TUI │ │ └──────┘ │
│ └─────┘ │ │
└─────────────────────────────────────┘
Testing
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "passphrase is required" | Empty or placeholder passphrase | Run donglora-bridge config |
| No peers found | Passphrase mismatch or firewall | Check passphrase matches; iroh uses QUIC (UDP), DHT uses UDP |
| Radio disconnected | USB dongle unplugged or mux crash | Check USB connection; bridge will auto-reconnect |
Yellow ! on radio fields |
Mux has different radio config | Another client set different params; bridge adopts mux config |
| Log file location | Default path | ~/.local/state/donglora-bridge/bridge.log |