218 lines
6.1 KiB
Markdown
218 lines
6.1 KiB
Markdown
# radicle-reticulum
|
|
|
|
Bridges [Radicle](https://radicle.xyz) (decentralized Git) over [Reticulum](https://reticulum.network) mesh networking — LoRa, packet radio, serial, I2P, and more. Enables offline-first code collaboration without internet infrastructure.
|
|
|
|
**Why:** Radicle requires publicly reachable seed nodes; Reticulum routes over any physical medium. Both use Ed25519 keys — a natural fit.
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
Both machines need:
|
|
- [Radicle](https://radicle.xyz/install) (`rad` CLI + `radicle-node`)
|
|
- [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python package manager)
|
|
- Git
|
|
|
|
---
|
|
|
|
## Install
|
|
|
|
On **each machine**, clone this repo and install:
|
|
|
|
```sh
|
|
git clone rad:z4NMdcKbw2TETQ56fbQfbibFHtZqZ # via radicle
|
|
# or: git clone https://github.com/youruser/radicle-reticulum
|
|
cd radicle-reticulum
|
|
uv sync
|
|
```
|
|
|
|
---
|
|
|
|
## Setup: connect two machines over mesh
|
|
|
|
Do this once on each machine.
|
|
|
|
### Step 1 — Configure radicle-node to listen on localhost
|
|
|
|
Edit `~/.radicle/config.json`, find the `"node"` section, set `"listen"`:
|
|
|
|
```json
|
|
"node": {
|
|
"listen": ["127.0.0.1:8776"],
|
|
...
|
|
}
|
|
```
|
|
|
|
Then (re)start radicle-node:
|
|
|
|
```sh
|
|
rad node start
|
|
rad node status # confirm: "listening for inbound connections on 127.0.0.1:8776"
|
|
```
|
|
|
|
### Step 2 — Start the bridge on both machines
|
|
|
|
Run on each machine:
|
|
|
|
```sh
|
|
uv run radicle-rns bridge
|
|
```
|
|
|
|
Within ~30 seconds the bridges discover each other via RNS announce, connect automatically, and register each other's NIDs with radicle-node. You'll see:
|
|
|
|
```
|
|
[+] Discovered bridge: <hash> (NID: z6Mk...)
|
|
[Status] Tunnels: 0, Remote bridges: 1, TX: 0, RX: 0
|
|
```
|
|
|
|
Once radicle-node connects through the bridge, tunnels open automatically:
|
|
|
|
```
|
|
Tunnel 1 opened / Incoming tunnel 1 opened
|
|
[Status] Tunnels: 1, Remote bridges: 1, TX: 1551, RX: 1831
|
|
```
|
|
|
|
Bytes in TX/RX confirm radicle gossip is flowing over the mesh.
|
|
|
|
> **LoRa note:** The bridge re-announces at t+5s, t+15s, t+30s after startup. On LoRa,
|
|
> use `--announce-retry-delays 60,300,900` to respect duty cycle limits.
|
|
|
|
---
|
|
|
|
## Share a repository
|
|
|
|
### Machine A — init and push a repo
|
|
|
|
```sh
|
|
mkdir myproject && cd myproject
|
|
git init
|
|
git commit --allow-empty -m "init"
|
|
rad init --name myproject --description "my project" --default-branch main
|
|
```
|
|
|
|
`rad init` prints the repository ID (`rad:z3...`). Share it with Machine B.
|
|
|
|
```sh
|
|
# make a commit and push
|
|
echo "hello" > hello.txt
|
|
git add . && git commit -m "hello"
|
|
rad push
|
|
```
|
|
|
|
### Machine B — clone it
|
|
|
|
```sh
|
|
rad clone rad:z3... # use the RID from Machine A
|
|
cd myproject
|
|
cat hello.txt # hello
|
|
```
|
|
|
|
### Machine A — fetch updates from Machine B
|
|
|
|
```sh
|
|
# Machine B: edit, commit, push
|
|
echo "world" >> hello.txt
|
|
git add . && git commit -m "world"
|
|
rad push
|
|
|
|
# Machine A:
|
|
rad fetch
|
|
git pull
|
|
```
|
|
|
|
---
|
|
|
|
## Commands
|
|
|
|
```
|
|
radicle-rns bridge # TCP↔RNS bridge (main command)
|
|
radicle-rns node # lightweight peer-announce node
|
|
radicle-rns peers # discover peers on the mesh
|
|
radicle-rns ping <hash> # RTT probe to a peer
|
|
radicle-rns identity generate # create/show identity
|
|
radicle-rns sync <repo> # LXMF store-and-forward sync
|
|
radicle-rns bundle create <repo> # pack repo into a bundle file
|
|
radicle-rns bundle apply <bundle> <repo> # unpack a bundle into a repo
|
|
radicle-rns bundle info <bundle> # inspect bundle metadata
|
|
radicle-rns bundle qr-encode <bundle> # print ASCII QR (≤2953 bytes)
|
|
radicle-rns bundle qr-decode <image.png> # decode QR back to bundle
|
|
```
|
|
|
|
Global flags: `-v` verbose logging, `--identity PATH` (default `~/.radicle-rns/identity`).
|
|
|
|
### Bridge flags
|
|
|
|
| Flag | Default | Description |
|
|
|------|---------|-------------|
|
|
| `-l, --listen-port` | 8777 | TCP port radicle-node connects to |
|
|
| `--radicle-port` | 8776 | Port radicle-node listens on |
|
|
| `-c, --connect <hash>` | — | Connect to a remote bridge by RNS hash |
|
|
| `--nid <NID>` | auto-detect | Override local radicle NID |
|
|
| `--no-auto-connect` | — | Disable auto-connect on discovery |
|
|
| `--no-auto-seed` | — | Disable auto-registering remote NIDs |
|
|
|
|
---
|
|
|
|
## Air-gapped / QR transfer
|
|
|
|
For links too slow even for Reticulum, transfer tiny incremental bundles via QR code (max 2953 bytes):
|
|
|
|
```sh
|
|
# Sender — create a small incremental bundle and encode it
|
|
uv run radicle-rns bundle create ./myrepo --incremental --basis myrepo.refs.json
|
|
uv run radicle-rns bundle qr-encode myrepo-*.radicle-bundle # prints ASCII QR to terminal
|
|
|
|
# Receiver — photograph the QR, then decode and apply
|
|
uv run radicle-rns bundle qr-decode qr-photo.png -o received.radicle-bundle
|
|
uv run radicle-rns bundle apply received.radicle-bundle ./myrepo
|
|
```
|
|
|
|
QR image output (PNG) and image decoding require optional deps:
|
|
```sh
|
|
uv sync --extra qr # qrcode for PNG output
|
|
pip install pillow pyzbar # for qr-decode from image file
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
radicle-node ──TCP:8777── RadicleBridge ──RNS Link── RadicleBridge ──TCP:8776── radicle-node
|
|
(Machine A) (Machine A) (Machine B) (Machine B)
|
|
```
|
|
|
|
- **Identity** (`identity.py`) — Ed25519 DID ↔ RNS destination; saved to `~/.radicle-rns/identity`
|
|
- **Bridge** (`bridge.py`) — TCP↔RNS tunnel, announces itself, discovers peers
|
|
- **SyncManager** (`sync.py`) — LXMF store-and-forward bundles; auto-pushes on refs announce
|
|
- **AdaptiveSyncManager** (`adaptive.py`) — selects FULL/INCREMENTAL/MINIMAL/QR by RTT + throughput
|
|
- **GitBundle** (`git_bundle.py`) — full and incremental Git bundles for delay-tolerant transfer
|
|
- **QR** (`qr.py`) — visual air-gap transfer for tiny bundles
|
|
|
|
---
|
|
|
|
## Development
|
|
|
|
```sh
|
|
uv run pytest # 158 tests
|
|
uv run pytest -x -q # stop on first failure
|
|
```
|
|
|
|
---
|
|
|
|
## Reticulum interfaces
|
|
|
|
On the same LAN, Reticulum auto-discovers peers via UDP multicast — no config needed. For LoRa / serial / I2P, edit `~/.reticulum/config`:
|
|
|
|
```ini
|
|
[[lora_interface]]
|
|
type = RNodeInterface
|
|
port = /dev/ttyUSB0
|
|
frequency = 868000000
|
|
bandwidth = 125000
|
|
spreadingfactor = 7
|
|
codingrate = 5
|
|
```
|
|
|
|
See [Reticulum docs](https://reticulum.network/manual/) for the full interface list.
|