Real TCP/IP networking over LoRa between two Linux hosts, using Heltec WiFi LoRa 32 V4 boards (ESP32-S3 + SX1262) flashed with RNode firmware and bridged via tncattach. Secured with WireGuard.
Linux host A โโUSBโโ Heltec V4 #1 โ LoRa โ Heltec V4 #2 โโUSBโโ Linux host B
tncattach (tnc0) tncattach (tnc0)
10.0.0.1 10.0.0.2
โ WireGuard wg1 (encrypted) โ
10.99.0.1 10.99.0.2
Each Heltec becomes a KISS TNC modem. tncattach exposes it as a Linux kernel network interface (tnc0). WireGuard (wg1) encrypts all traffic on the link.
| Metric | Value | Conditions |
|---|---|---|
| Packet loss | 0% (20/20 ICMP, 5/5 ping) | SF7 / 125 kHz / 7 dBm, desk range |
| RTT (raw IP) | 187โ534 ms (median 220 ms) | 868 MHz, USB-serial KISS |
| RTT (WireGuard) | 1.2โ2.0 s | WG handshake + encrypted ICMP |
| Throughput | ~0.9 kbps app-level | stop-and-wait, 32B payload |
| TCP handshake | 0.40 s | 3-way handshake + echo |
| SSH session | end-to-end success | SSH < WireGuard < IP < KISS < LoRa |
| Firmware | RNode v1.86 (markqvist upstream) | Not CE fork, not microReticulum |
install-rnode-lora.sh Wizard installer/uninstaller (801 lines)
tests/kisstest.py KISS frame round-trip test (AโB)
tests/iptest.py Real IP/ICMP packet test over LoRa
tests/reliability.py RTT/loss/throughput characterization
tests/sandbox-test.sh Isolated netns test harness (17 tests)
- Two Heltec WiFi LoRa 32 V4 boards (ESP32-S3 + SX1262)
- Two Linux hosts (tncattach is Linux-only โ no macOS support)
- USB-C data cables connecting each board to its host
sudo ./install-rnode-lora.shThe wizard will:
- Install dependencies (git, build-essential, python3-pip, wireguard-tools, rns)
- Detect the board on
/dev/ttyACM* - Verify/flash RNode firmware (offers
rnodeconf --autoinstallif needed) - Set TNC mode + radio params (defaults: 868 MHz / 125 kHz / SF7 / CR5 / 7 dBm)
- Build + install
tncattach, bring uptnc0 - Configure WireGuard
wg1(generates keys, prints public key for peer) - Open firewall for
tnc0+wg1 - Verify the link (ping peer)
- Optionally create systemd units for reboot persistence
- Snapshot pre-install state (wg configs, firewall, routing) for rollback
Run on host A first, note the printed public key, then run on host B with mirrored IPs and host A's public key.
sudo ./install-rnode-lora.sh # interactive wizard
sudo ./install-rnode-lora.sh install # force install
sudo ./install-rnode-lora.sh uninstall # remove + restore pre-install state
sudo ./install-rnode-lora.sh status # show current state (read-only)
sudo ./install-rnode-lora.sh --dry-run install # print actions, mutate nothing
sudo ./install-rnode-lora.sh --dry-run uninstallsudo ./install-rnode-lora.sh uninstallRestores the host to its pre-install state using the snapshot:
- Restores WireGuard configs (e.g.
wg0.conf) verbatim from snapshot - Removes only
tnc0/wg1firewall rules (leaves fleet/NetBird rules untouched) - Re-brings-up any WireGuard interface that was running before install
- Removes
tncattachbinary,wg1.conf, logs, state, snapshot - Offers to restore the board to Normal mode (
rnodeconf -N) - Does NOT remove shared deps (git, wireguard-tools) by default
sudo tests/sandbox-test.sh install-rnode-lora.shRuns 17 tests in an isolated network namespace โ never touches the live host network.
All values are readonly constants at the top of install-rnode-lora.sh:
| Parameter | Default | Notes |
|---|---|---|
| Frequency | 868 MHz (863โ870 MHz EU band) | Must match on both boards |
| Bandwidth | 125 kHz | |
| Spreading factor | SF7 (fastest) | SF11 = longest range, ~937 bps |
| Coding rate | 4/5 | |
| TX power | 7 dBm | Board max 28 dBm; low for desk range |
| Serial baud | 115200 | RNode firmware default |
| tnc0 MTU | 496 | RNode P2P (500 โ 4) |
| wg1 MTU | 400 | tnc0 496 โ ~80 WireGuard overhead |
| wg1 port | 51821 | Avoids default 51820 + fleet |
The raw KISS IP link is unencrypted. Two layers secure it:
- WireGuard (
wg1) โ encrypts all IP traffic on the LoRa link. Handshake (Curve25519) completes over LoRa (~1 s at SF7). A LoRa eavesdropper sees only encrypted UDP noise. - SSH โ end-to-end encryption + host-key auth, independent of the VPN. Sufficient if SSH is the only application.
Both were tested live: WG handshake completed, encrypted ICMP traversed the tunnel, and an interactive SSH-over-WG-over-LoRa session ran successfully.
- Not Meshtastic โ the original Meshtastic 2.7.26 firmware was reflashed; private keys are unrecoverable, so reflashing Meshtastic again creates new node identities.
- Not Reticulum โ Reticulum is its own L3 stack (announces, LXMF, rnsh), not TCP/IP. The jrl290/RTNode-HeltecV4 and microReticulum firmwares carry Reticulum, not IP.
- Not fast โ LoRa at SF7 gives ~0.9 kbps app-level. Usable for ping, text, laggy SSH. Not for bulk transfer, web, or NetBird.
- Not standalone โ the Heltec is a KISS modem, not an IP node. A Linux host provides the IP stack via
tncattach. There is no native ESP32 lwIP-over-LoRa firmware.
| Question | Answer | Why |
|---|---|---|
| Which firmware? | RNode v1.86 (markqvist upstream) | Supports Heltec V4 + KISS TNC mode; CE fork doesn't support V4 |
| Why not microReticulum? | Reticulum โ TCP/IP | It's its own network stack, no ifconfig, no sockets |
| Why not WiFi-KISS? | Untested live | Firmware exposes KISS-over-TCP on port 7633, but we used USB-serial (more reliable, boards are USB-attached) |
Why wg1 not wg0? |
wg0 = fleet |
NetBird uses wt0; legacy WG hub uses wg0. Never touch fleet interfaces. |
| Why MTU 496? | RNode P2P | Radio MTU 500 โ 4 bytes KISS P2P header |
- Board: Heltec WiFi LoRa 32 V4.3 (ESP32-S3FH4R2, 16 MB flash, 2 MB PSRAM, SX1262 + KCT8103L PA)
- Firmware: RNode v1.86,
heltec32v4pabuild, TNC mode - Hosts: host A (x86-64, Ubuntu 6.8 kernel) + host B (aarch64 Jetson)
Reflash via the Meshtastic web flasher (Chrome/Edge). New node identities will be generated on reflash (the original keys are unrecoverable).