MeshNet: WireGuard-like VPN over Meshtastic mesh radio
Project description
MeshNet
WireGuard-style VPN over Meshtastic mesh radio
MeshNet creates encrypted, peer-to-peer IP tunnels that run over Meshtastic LoRa mesh radios. It bridges a Linux TAP virtual Ethernet interface to the mesh network, giving every node a routable IP address — even when there is no Internet infrastructure.
Features
| Feature | Description |
|---|---|
| WireGuard-inspired cryptography | X25519 key exchange, ChaCha20-Poly1305 AEAD, HKDF-SHA256 KDF, BLAKE2s MACs |
| Two peer modes | PKI (default) — full X25519 handshake with forward secrecy; Symmetric — PSK-derived key, no handshake needed |
| Simplified handshake (PKI mode) | 1-RTT handshake with mutual static-key authentication and ephemeral forward secrecy |
| Optional preshared key (PSK) | Extra symmetric key mixed into key derivation for post-quantum defence-in-depth (PKI) or sole key material (Symmetric) |
| Automatic rekeying | PKI sessions rekey after 5 hours or 2¹⁶ messages (deferred on idle links) |
| Sliding-window replay protection | 256-packet window rejects duplicates while tolerating reordering |
| Fragmentation / reassembly | Transparently splits payloads exceeding the 233-byte Meshtastic radio MTU |
| Familiar CLI | meshnet genkey, pubkey, genpsk, up, down, show — just like wg |
Architecture
┌──────────────┐ TAP ┌─────────────────┐ TCP/IP ┌────────────────┐
│ Linux apps │ ◄─────────► │ MeshVPN Daemon │ ◄────────► │ Meshtastic │
│ (10.0.0.x) │ Ethernet │ encrypt/decrypt │ 4403 │ LoRa Radio │
└──────────────┘ frames │ fragment/reasm │ └────────────────┘
└─────────────────┘
Package layout
meshnet/
├── __init__.py # version
├── cli/ # CLI entry point ("meshnet" command)
│ └── __init__.py
├── meshtastic_core/ # Meshtastic TCP client abstraction
│ └── __init__.py
└── vpn/
├── config.py # WireGuard-style config parser
├── crypto.py # X25519, ChaCha20-Poly1305, HKDF, BLAKE2s
├── daemon.py # Async VPN daemon (TAP ↔ mesh bridge)
├── routing.py # AllowedIPs routing table + Ethernet frame parsing
├── session.py # Per-peer handshake state machine + encrypt/decrypt
├── tap.py # Linux TAP device via ioctl
└── transport.py # Wire protocol: packets, serialisation, fragmentation
Quick start
Prerequisites
- Python ≥ 3.13
- Linux (for TAP device)
- A Meshtastic device reachable over TCP (default port 4403)
- uv package manager (recommended)
Installation
Install as a uv tool (no clone needed):
uv tool install meshnet
Or clone and install locally:
git clone https://github.com/esoadamo/meshnet.git
cd meshnet
uv sync # installs into .venv
Generate keys
# Generate a private key
meshnet genkey > privatekey
# Derive the public key
meshnet pubkey < privatekey > publickey
# Generate an optional preshared key (one per peer pair)
meshnet genpsk > presharedkey
Create a configuration file
Create mesh0.conf:
[Interface]
PrivateKey = <base64 private key>
Address = 10.0.0.1/24
MTU = 180
TapName = mesh0
MeshtasticConnect = tcp://10.1.5.3:4403
[Peer]
PublicKey = <peer's base64 public key>
PresharedKey = <optional base64 PSK>
AllowedIPs = 10.0.0.2/32
Endpoint = !d45b9db8
Security: Set
chmod 600 mesh0.conf— the file contains your private key.
Start the tunnel
sudo meshnet up -c mesh0.conf
Show status
meshnet show -c mesh0.conf
Stop the tunnel
meshnet down
Configuration reference
[Interface] section
| Key | Required | Default | Description |
|---|---|---|---|
PrivateKey |
✅ | — | Base64-encoded X25519 private key (32 bytes) |
Address |
✅ | — | Local IP address with CIDR prefix (e.g. 10.0.0.1/24) |
MTU |
❌ | 180 |
TAP device MTU (constrained by Meshtastic payload size) |
TapName |
❌ | mesh0 |
Linux TAP interface name |
MeshtasticConnect |
✅ | — | Connection URI for the Meshtastic device (see below) |
RunAsUser |
❌ | — | Drop to this unprivileged user after TAP setup (e.g. nobody) |
RunAsGroup |
❌ | — | Drop to this group after TAP setup (e.g. nogroup) |
MeshtasticConnect URI schemes:
| Scheme | Example | Description |
|---|---|---|
tcp:// |
tcp://10.1.5.3:4403 |
TCP connection (port defaults to 4403) |
serial:// |
serial:///dev/ttyUSB0 |
Linux serial port |
serial:// |
serial://COM3 |
Windows COM port |
[Peer] section (one or more)
| Key | Required | Default | Description |
|---|---|---|---|
PublicKey |
✅ | — | Peer's base64-encoded X25519 public key |
PresharedKey |
❌¹ | — | Base64-encoded 32-byte PSK |
AllowedIPs |
✅ | — | Comma-separated CIDR ranges routed to this peer |
Endpoint |
✅ | — | Meshtastic node ID (e.g. !d45b9db8) |
PeerMode |
❌ | PKI |
PKI — full X25519 handshake; SYMMETRIC — PSK-only, no handshake |
¹ Required when PeerMode = SYMMETRIC.
Peer modes
PKI (default) — a 1-RTT X25519 handshake derives unique ephemeral keys for each session, providing forward secrecy. The optional PSK is mixed into the KDF for post-quantum defence-in-depth. Sessions rekey automatically after 5 hours or 2¹⁶ messages. If a link has been idle for 30 minutes when a rekey is due, the rekey is deferred until the next actual communication to avoid wasting air time.
Symmetric — the transport key is derived from the PSK alone via
HKDF-SHA256(ikm=PSK, salt="symmetric", info="meshnet-symmetric-key").
No handshake is needed — the session is established immediately. Each
side starts its send counter at a random 64-bit value to prevent nonce
reuse across daemon restarts. There is no forward secrecy; compromising
the PSK exposes all past and future traffic. Use this mode when
handshake round-trips are impractical (e.g. very high-latency mesh
links) or for simple pre-shared-key deployments.
Wire protocol
All packets start with a 1-byte type discriminator:
| Type | Name | Layout (after type byte) |
|---|---|---|
0x01 |
HandshakeInit | session(4) + eph_pub(32) + mac(16) = 52 B |
0x02 |
HandshakeResponse | sender(4) + recv(4) + eph_pub(32) + mac(16) = 56 B |
0x03 |
TransportData | counter(8) + ciphertext(N) |
0x04 |
TransportFragment | msg_id(2) + frag_idx(1) + frag_total(1) + chunk(N) |
Handshake flow
Initiator Responder
│ │
│ HandshakeInit (0x01) │
│ session_I + eph_I + MAC │
│ ─────────────────────────────► │
│ │
│ HandshakeResponse (0x02) │
│ session_R + session_I + │
│ eph_R + MAC │
│ ◄───────────────────────────── │
│ │
│ [session established] │
│ TransportData (0x03) │
│ ◄─────────────────────────────►│
Three Diffie-Hellman computations derive transport keys (PKI mode):
- DH(eph_I, eph_R) — ephemeral-ephemeral (forward secrecy)
- DH(static_I, eph_R) — static-ephemeral (identity binding)
- DH(eph_I, static_R) — ephemeral-static (identity binding)
If a PSK is configured, it is mixed into the HKDF salt for additional post-quantum protection.
In Symmetric mode, no handshake occurs. Both peers derive the same ChaCha20-Poly1305 key from the shared PSK using HKDF-SHA256.
Testing
uv run pytest tests/ -v --tb=short
The test suite includes 212+ tests across three categories:
- Unit tests — crypto, transport, config, routing, session, TAP, CLI, meshtastic core
- Integration tests — full handshake→encrypt→fragment→reassemble→decrypt pipelines
- Regression tests — replay protection, tampering, malformed input, cross-session isolation
All external dependencies (Meshtastic library, TAP device, OS calls) are mocked.
Security
See THREAT_MODEL.md for a detailed threat model and security analysis.
Security hardening summary
- Privilege separation: Optional
RunAsUser/RunAsGroupdrops root after TAP device setup - Replay protection: Sliding-window (256 packets) rejects duplicates while tolerating mesh reordering
- Nonce safety: Counter overflow guard prevents nonce reuse
- Input validation: TAP name regex, MTU bounds, fragment metadata checks
- PID file: Atomic creation with
O_EXCLprevents TOCTOU race conditions - Config permissions: Warning emitted if config file (containing private key) is world-readable
- Reassembly limits: Hard cap on concurrent reassembly buffers prevents memory exhaustion
License
See repository for license details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file meshnet-0.1.0.tar.gz.
File metadata
- Download URL: meshnet-0.1.0.tar.gz
- Upload date:
- Size: 102.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb3d754b64dca9e75f1a04d77d0ec7e52dba596ea0c6832a264d9136d41854a1
|
|
| MD5 |
ae73ec65599efcfa82e3fa783dcbd28d
|
|
| BLAKE2b-256 |
9852c59b510d6baa85f469fd1b5aa6f68045d40012177090b09fe0eee4cbc96c
|
Provenance
The following attestation bundles were made for meshnet-0.1.0.tar.gz:
Publisher:
pypi-deploy.yml on esoadamo/meshnet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
meshnet-0.1.0.tar.gz -
Subject digest:
cb3d754b64dca9e75f1a04d77d0ec7e52dba596ea0c6832a264d9136d41854a1 - Sigstore transparency entry: 1197841617
- Sigstore integration time:
-
Permalink:
esoadamo/meshnet@b4cc40f7a6170c866e2679fac86bea9049f5c0c8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/esoadamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-deploy.yml@b4cc40f7a6170c866e2679fac86bea9049f5c0c8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file meshnet-0.1.0-py3-none-any.whl.
File metadata
- Download URL: meshnet-0.1.0-py3-none-any.whl
- Upload date:
- Size: 46.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4c29060dbceb8369889815eee58d2e720f54516e53e020e2bdd1d601bced6ac
|
|
| MD5 |
855164e0a59a2108e7f397e57f27f182
|
|
| BLAKE2b-256 |
e33bdd951f27a7ce63e91dff5b8b5ab6d59e6260d84875796bae3dfef433b84b
|
Provenance
The following attestation bundles were made for meshnet-0.1.0-py3-none-any.whl:
Publisher:
pypi-deploy.yml on esoadamo/meshnet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
meshnet-0.1.0-py3-none-any.whl -
Subject digest:
e4c29060dbceb8369889815eee58d2e720f54516e53e020e2bdd1d601bced6ac - Sigstore transparency entry: 1197841673
- Sigstore integration time:
-
Permalink:
esoadamo/meshnet@b4cc40f7a6170c866e2679fac86bea9049f5c0c8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/esoadamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-deploy.yml@b4cc40f7a6170c866e2679fac86bea9049f5c0c8 -
Trigger Event:
push
-
Statement type: