donglora-bridge 3.0.1

Peer-to-peer LoRa bridge using iroh and DongLoRa
Documentation
# donglora-bridge

[![CI](https://github.com/donglora/bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/donglora/bridge/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![Rust](https://img.shields.io/badge/rust-1.94.1%2B-orange)

**donglora-bridge** connects LoRa radios to the internet so packets heard by one
radio can be retransmitted by another radio anywhere in the world.

[LoRa](https://en.wikipedia.org/wiki/LoRa) is a long-range, low-power wireless
protocol used by off-grid mesh networks like
[MeshCore](https://github.com/rocketgithub/meshcore-firmware). A single LoRa
radio might cover a few kilometers. This bridge removes that limit: any packet
your radio hears gets relayed over the internet to every other bridge running the
same passphrase, and their radios retransmit it locally. The result is a single
mesh network across a larger geographic area.

There is no central server. Bridges discover each other automatically using the
[Mainline DHT](https://en.wikipedia.org/wiki/Mainline_DHT) and communicate
through an encrypted peer-to-peer gossip swarm.

![TUI Screenshot](screenshot.png)

## What you need

### 1. A DongLoRa radio

[DongLoRa](https://github.com/donglora/firmware) 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:

1. **Get a board** -- any ESP32 board with an SX1262 LoRa radio that DongLoRa
   supports. See the [DongLoRa README]https://github.com/donglora/firmware for
   the full list.
2. **Flash the DongLoRa firmware** onto the board. Instructions are in the
   DongLoRa repository.
3. **Plug it in via USB**. The board appears as a serial device
   (e.g. `/dev/ttyACM0` on 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).

```sh
cargo install donglora-mux
```

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:

```sh
cargo install donglora-bridge
```

Or via [mise](https://mise.jdx.dev/):

```sh
mise use -g cargo:donglora-bridge
```

Or build from source:

```sh
git clone https://github.com/donglora/bridge.git
cd donglora-bridge
cargo build --release
```

Requires Linux. macOS may work but is untested.

## Quick start

### 1. Plug in your DongLoRa board and (optionally) start the mux

```sh
donglora-mux          # optional -- skip if not sharing the radio
```

### 2. Run the bridge

```sh
donglora-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.

```sh
donglora-bridge                    # TUI mode (default)
donglora-bridge --log-only         # headless mode (structured logs to stdout)
donglora-bridge --config path.toml # custom config file
donglora-bridge config             # re-run setup wizard
```

### 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`:

```sh
donglora-bridge --config ~/bridge-network-a.toml
donglora-bridge --config ~/bridge-network-b.toml
```

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]https://github.com/n0-computer/iroh, a QUIC-based overlay network
- **Zero-config peer discovery** -- automatic bootstrapping via BitTorrent mainline DHT ([BEP44]http://bittorrent.org/beps/bep_0044.html 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`

```toml
[radio]
# port = "/dev/ttyACM0"   # serial port (omit for auto-detect: tries mux, then USB)
frequency = 910525000      # Hz (default: 910.525 MHz)
bandwidth = "62.5kHz"      # 7.8kHz, 10.4kHz, 15.6kHz, 20.8kHz, 31.25kHz,
                           # 41.7kHz, 62.5kHz, 125kHz, 250kHz, 500kHz
spreading_factor = 7       # 7-12
coding_rate = 5            # 5-8 (meaning 4/5 to 4/8)
sync_word = 0x3444         # hex, 2 bytes
tx_power = "22"            # dBm integer or "max"
preamble = 16              # symbols
cad = true                 # channel activity detection

[bridge]
passphrase = "your-shared-secret"  # REQUIRED: all bridges with same passphrase join the same swarm
dedup_window_secs = 300            # duplicate suppression window (default: 5 minutes)
tx_queue_size = 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:

1. **Topic ID** -- `blake3(APP_PREFIX + passphrase)` -- identifies the gossip swarm
2. **DHT signing key** -- ed25519 from `SHA-256(APP_PREFIX + "sign:" + passphrase)` -- authenticates DHT records
3. **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

```sh
cargo test                      # unit tests + property tests (proptest)
cargo clippy --all-targets      # lint check (pedantic + nursery)
cargo fmt --check               # format check
cargo mutants                   # mutation testing (install: cargo install cargo-mutants)
cargo +nightly fuzz run fuzz_gossip_frame_decode   # fuzz testing (requires nightly)
cargo +nightly fuzz run fuzz_decode_peer_list
```

## 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` |

## License

[MIT](LICENSE)