- Python 99.7%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Inbound: peers sending LXMF's FIELD_ICON_APPEARANCE (MDI glyph name + foreground/background colors) get it rendered via a vendored MDI font into a PNG, set as both the ghost user's and portal room's Matrix avatar. A fingerprint on the contacts table avoids re-rendering/ re-uploading on every message. Outbound: the bridge can present its own configured icon appearance (icon_name/icon_fg_color/icon_bg_color in [lxmf]) to LXMF peers on every outbound message. Also fixes room names staying as the raw hex identity when a peer's display name hadn't been learned yet: message delivery only requires a cached identity, not a prior announce with app_data, so the bridge now requests the peer's path on first contact to solicit a fresh announce instead of passively waiting for their next periodic one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| bridge | ||
| .dockerignore | ||
| .gitignore | ||
| config.example.toml | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
matrix-lxmf
A private, single-owner Matrix ↔ LXMF puppeting bridge. It runs as a Matrix appservice and lets one Matrix account exchange messages with contacts on the LXMF/Reticulum network from inside normal Matrix rooms.
Each LXMF contact is puppeted by a "ghost" Matrix user
(@<ghost_prefix><hash>:yourserver) in a 1:1 portal room. Messages sent by the
configured owner in a portal room are relayed out over LXMF; messages received
over LXMF appear as messages from the ghost user in Matrix.
Features
- One portal room per LXMF contact, created automatically on first contact
- A management room for bridge commands (
connect,disconnect,list,ping,status, identity export/import, ...) - Image/file/audio/video attachments in both directions, with automatic image downscaling/recompression to WebP for outbound Matrix → LXMF transfers
- Contact profile pictures: peers sending a Sideband/Columba-style icon
appearance (LXMF field
FIELD_ICON_APPEARANCE) get it rendered as a Matrix avatar for their ghost user and portal room; the bridge can also present its own configured icon appearance to LXMF peers on outbound messages - LXMF identity export/import (Sideband/NomadNet-compatible base32 keys)
- SQLite or PostgreSQL storage backend
- Can attach to an existing Reticulum config/shared instance instead of spinning up its own
Requirements
- Python 3.11+
- A Matrix homeserver you can register an appservice on (e.g. Synapse)
- Access to a Reticulum network (via a local interface or a shared
rnsdinstance)
Installation
pip install .
# or, for PostgreSQL support:
pip install .[postgres]
A Dockerfile is also provided; it installs with the postgres extra and
runs python -m bridge.main --config /data/config.toml.
Configuration
The easiest way to get a working config is matrix-lxmf-bridge init, which
generates fresh as_token/hs_token secrets, writes config.toml, and
creates the bridge's persistent LXMF identity so its address is known
up front:
matrix-lxmf-bridge init \
--homeserver-url https://matrix.example.org \
--server-name example.org \
--owner-mxid @you:example.org
Run matrix-lxmf-bridge init --help for the full set of options (bind
host/port, public URL, app ID, bot localpart, ghost prefix, ...); anything
not passed falls back to a sane default. Re-running init refuses to
overwrite an existing config.toml unless you pass --force.
Alternatively, copy config.example.toml to config.toml by hand and fill
in your values (each field is documented inline). Either way, config.toml
is gitignored since it holds secrets.
Once you have a config.toml:
- Run the bridge once; it writes
registration.yamlnext to your config file, generated from theas_token/hs_token/app_idin it. - Add that
registration.yamlto your homeserver's appservice registrations (for Synapse, list its path underapp_service_config_filesinhomeserver.yaml) and restart the homeserver. - Restart the bridge.
Running
matrix-lxmf-bridge run --config config.toml
The run subcommand can be omitted for backwards compatibility
(matrix-lxmf-bridge --config config.toml still works, as used by the
Dockerfile).
On first run, the bridge creates a DM "management room" with the configured
owner_mxid and invites you to it.
Running with Docker
docker build -t matrix-lxmf .
The image's entrypoint is python3 -m bridge.main, defaulting to
run --config /data/config.toml, so mount a host directory at /data for
config, registration, and persistent state:
mkdir -p /opt/matrix-lxmf/data
# One-time: generate config.toml + LXMF identity into that directory.
docker run --rm -v /opt/matrix-lxmf/data:/data matrix-lxmf init \
--config /data/config.toml \
--homeserver-url https://matrix.example.org \
--server-name example.org \
--owner-mxid @you:example.org
init's generated config.toml defaults storage_dir, identity_file, and
sqlite_path to relative paths (var/lxmf, var/lxmf/identity,
var/bridge.db). Inside the container these resolve against the image's
WORKDIR (/app), not /data — left as-is, your LXMF identity and
database would live outside the mounted volume and vanish when the container
is removed. Edit those three paths in /opt/matrix-lxmf/data/config.toml to
live under /data (e.g. storage_dir = "/data/var/lxmf",
identity_file = "/data/var/lxmf/identity",
sqlite_path = "/data/var/bridge.db") before starting the container for
real.
This bridge always attaches to an existing Reticulum shared instance
(bridge/lxmf_side.py calls RNS.Reticulum(..., require_shared_instance=True)
unconditionally — it never runs its own network interfaces). On Linux, that
attachment defaults to an abstract AF_UNIX socket, which is scoped per
network namespace. Docker's default bridge networking gives the container
its own namespace, so it cannot see a shared instance (e.g. rnsd) running
on the host — the container needs --network host to share the host's
network namespace instead. If you're pointing reticulum_config_dir at a
host Reticulum config (e.g. /etc/reticulum), bind-mount it read-only —
but overlay two writable directories at <configdir>/storage/resources
and <configdir>/storage/ratchets:
mkdir -p /opt/matrix-lxmf/rns-storage-resources /opt/matrix-lxmf/rns-storage-ratchets
docker run -d \
--name matrix-lxmf \
--restart unless-stopped \
--network host \
-v /opt/matrix-lxmf/data:/data \
-v /etc/reticulum:/etc/reticulum:ro \
-v /opt/matrix-lxmf/rns-storage-resources:/etc/reticulum/storage/resources \
-v /opt/matrix-lxmf/rns-storage-ratchets:/etc/reticulum/storage/ratchets \
matrix-lxmf
These two overlays are required, not optional: even when
require_shared_instance=True, RNS.Reticulum() still writes locally to
storage/resources/ (the reassembly buffer for any incoming transfer that
doesn't fit in a single packet) and storage/ratchets/ (forward-secrecy
ratchet cleanup) from within the client process itself. Mounting the whole
config dir :ro without these overlays makes the bridge silently fail to
reassemble any large incoming LXMF message (FileNotFoundError in
storage/resources/... in the container logs) while small messages keep
working fine.
Don't overlay the whole storage/ directory — only these two
subdirectories. The rest of storage/ (notably transport_identity) must
stay shared with the host's rnsd: RNS derives its shared-instance RPC
auth key from transport_identity, so a container-private copy generates a
different key and breaks the shared-instance RPC channel (used for
interface/path-table stats) with multiprocessing.context.AuthenticationError: digest sent was rejected.
With --network host, bind_host = "127.0.0.1" in config.toml refers to
the host's own loopback, so Synapse on the same host can reach it there
without further port mapping.
Management room commands
| Command | Description |
|---|---|
help |
Show help |
status |
Check RNS/shared instance connectivity |
connect <hash> [display name] |
Start a new conversation with an LXMF address |
disconnect <hash> |
Remove a specific portal |
disconnect-all |
Remove all portals |
list |
List all portals |
ping <hash> |
Probe reachability to an LXMF address |
export-identity |
Print the bridge's LXMF private key (base32) |
import-identity <base32 key> |
Replace the bridge's LXMF identity (or just send a .rnsidentity file) |
Portal room commands
Prefixed with !lxmf inside portal rooms:
| Command | Description |
|---|---|
!lxmf delete-portal |
Remove this portal |
!lxmf ping |
Probe reachability to this contact |
!lxmf show-hash |
Print the bridge's own LXMF address |
License
No license has been chosen yet for this project.