Mesh Deployment - Dark-Alex-17/coyote GitHub Wiki

This page is about getting a Coyote Mesh node onto a network. The mesh runs over Reticulum, a networking stack that works over ordinary IP links as well as radios and serial lines. The usual way to run Reticulum is one daemon per machine to own the network interfaces: rnsd, a small background program that lets several programs on one machine share them. Coyote itself only dials that daemon.

A node joins exactly the interfaces listed under mesh.interfaces; nothing is auto-detected and there is no fallback between types. The shipped default is a single private 127.0.0.1:4242 entry: loopback, the machine itself, which is where the local rnsd listens. Every Coyote session on the machine dials it. One command sets the daemon up:

curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/mesh-relay.sh | bash
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/scripts/mesh-relay.ps1 | iex"

or pass --with-mesh / -WithMesh to the installer, which runs the same script after installing Coyote.

Two more words come up in the choices below. A relay is a node that forwards traffic between others; Reticulum calls it a transport node. A propagation node holds a message for a recipient who is offline until they fetch it, which is what "store-and-forward" means; lxmd is the reference program for it.

Which setup? Coyote does not manage network interfaces itself. It talks to the mesh through a small daemon or relay that does, and the only question is where that daemon runs:

  1. Default: rnsd on this machine. Run scripts/mesh-relay.sh (Linux and macOS), scripts/mesh-relay.ps1 (Windows) or the installer with --with-mesh / -WithMesh once per machine. It installs and starts rnsd, the Reticulum daemon, and every Coyote session keeps the shipped default private 127.0.0.1:4242 and dials it. This covers several sessions on one machine, the office LAN (rnsd finds the other machines' daemons by itself, through its AutoInterface) and VPN or internet links: a relay your team runs goes into rnsd's config, and --relay host:port writes that [[Team Relay]] stanza for you.
  2. A store-and-forward node for a team. Run one lxmd, the propagation node from deployment/propagation-node, with your teammates' identities in its allowed file. A message to someone who is offline waits there until they fetch it. The node is a relay as well, so it is the natural --relay target for every machine's rnsd, but sessions on one machine never need it to reach each other.
  3. Running without rnsd. Point the loopback entry at a team relay (private host:port) instead, or use lan, which joins the local network directly from Coyote: one session per machine, LAN only, no LoRa or I2P.

Sessions exchange nothing until they are mutually trusted, that is until each side has trusted the other (Mesh Trust Model); there is no interface-level switch and none is needed. Other transports (RNode/LoRa, I2P, UDP, serial) are configured in rnsd and reached through the loopback hop.

The ladder

Each rung reaches further than the one before it.

1. The host: rnsd on loopback

The default. private makes Coyote a TCP client to a Reticulum transport node (TCP Server Interface); with the shipped private 127.0.0.1:4242 that node is the host's own rnsd. Every session on the host dials it, the daemon's AutoInterface (Reticulum's automatic discovery of other machines on the same local network) reaches the other hosts on the LAN, and .mesh info reports reach: the configured relay and the peers it reaches. Setup is in Setting up rnsd.

2. The team: a relay or propagation node in rnsd's config

A relay your team runs, reachable over VPN or the internet, goes into rnsd's config as a TCPClientInterface: scripts/mesh-relay.sh --relay host:port writes it as the [[Team Relay]] stanza. Every node joined to that relay hears every other. The propagation node container is one such relay, since it is a transport node too; see Propagation nodes.

"Private" means network reachability, not access control. The relay authenticates nobody: anyone who can reach port 4242 joins it and sees every announce, the periodic "I am here" broadcast by which nodes learn of each other (it carries display_name, if set). The firewall or VPN in front of the port is the boundary; Coyote's trust list decides who gets a status card or a message, and nothing else.

3. Global: type: public

public has the same wire shape as private, but the relay is a community transport node, so your announce is visible to anyone who reaches it. .mesh on asks before joining:

This node will be WORLD-VISIBLE through public node.example.com:4242 (world-visible): anyone reaching that relay can see its announce and knock on it. Join anyway?

display_name is withheld from every announce while a public entry is configured unless display_name_on_public: true.

Coyote ships no list of community nodes. Addresses rot silently, and publishing them would promise an availability this project does not control, so public needs a host and port you choose. The upstream starting points are the Reticulum manual's Bootstrapping connectivity and Connect to the distributed backbone sections, which name the community directories, and the Reticulum README's community links. The historical public testnet entry point was decommissioned upstream, so check those pages rather than older posts.

Setting up rnsd

scripts/mesh-relay.sh (Linux and macOS) and scripts/mesh-relay.ps1 (Windows) do the same four things:

  1. Find or install rnsd, trying in this order: an rnsd that already works in the bin dir or on PATH is compared to the pinned version: equal, reused as is; older, upgraded through the tool that installed it (uv tool install --force, pipx install --force, or pip install in the script's own venv; an rnsd of unknown origin is kept with a warning and the upgrade command), unless --keep-rnsd / -KeepRnsd keeps it; newer, kept with one warning that it is newer than the version the script was tested with; else uv tool install --python 3.12 "rns==1.5.2" (the version Coyote is tested with); else pipx install "rns==1.5.2"; else a virtualenv under the Coyote config dir (<config dir>/mesh/rns-venv). The rnsd command in /usr/local/bin when it is writable, else ~/.local/bin, is a symlink to the real one (a two-line shim for the venv; Windows: rnsd.cmd in %LOCALAPPDATA%\coyote\bin). --version sets the version to install or upgrade to.

  2. Write the Reticulum config only if it is absent, in the directory rnsd itself reads, in rnsd's order: /etc/reticulum when it holds a config (then the script prints the stanzas and skips the service, since that daemon belongs to the system), else ~/.config/reticulum when it holds a config, else ~/.reticulum (Windows: %USERPROFILE%\.config\reticulum when it holds a config, else %USERPROFILE%\.reticulum). The plan prints which one and why (Config dir: rnsd reads ~/.config/reticulum because it already holds a config), and the service definition passes that directory with rnsd --config <dir>, so the daemon and the script never disagree about the file. An existing config is never overwritten; the script prints already exists, not touched. Make sure it carries these settings and stanzas: followed by the [reticulum], [logging] and [interfaces] sections below (not the four header comment lines), and leaves the merge to you; when that config lacks enable_transport = True it adds WARNING: <config> does not set enable_transport = True; without it this rnsd will not relay between Coyote sessions., and when it enables an AutoInterface the firewall sentence below follows. The written file is (the Windows script puts written by scripts/mesh-relay.ps1 in the first line):

    # Reticulum configuration written by scripts/mesh-relay.sh for Coyote mesh.
    # This daemon owns the interfaces; every Coyote session on this host dials it
    # with mesh.interfaces: [{type: private, host: 127.0.0.1, port: 4242}].
    # Interface options: https://markqvist.github.io/Reticulum/manual/interfaces.html
    
    [reticulum]
    
    # Relay announces and paths between the Coyote sessions that dial in. A transport
    # node also forwards traffic for any other Reticulum peer it hears on its interfaces.
    enable_transport = True
    
    # Coyote sessions announce hourly (3600 s plus a minute of slack, so the relay never counts
    # a heartbeat as early): a transport node rate-limits announces per destination, Reticulum's
    # defaults being announce_rate_target = 3600 s with an announce_rate_grace of 5 (RNS
    # Interfaces/Interface.py, DEFAULT_AR_TARGET and DEFAULT_AR_GRACE), and past that grace it
    # blocks a destination's rebroadcasts until an hour after its last accepted announce. Do not
    # lower announce_rate_target here, nor default_ar_target in the [reticulum] section: team
    # relays and community nodes keep the default.
    
    # Shared instance left at the default so rnstatus, rnpath, Sideband and NomadNet on
    # this host attach to this daemon instead of binding 4242 / AutoInterface again.
    # Any other local account on this host can attach to this daemon too, over
    # 127.0.0.1:4242 and the shared instance; that is inherent to Reticulum's shared-instance model.
    
    [logging]
    
    # rnsd applies its -v flags only when this section exists (Reticulum.py:459-467).
    # 4 = LOG_INFO, the value RNS's own default config ships.
    loglevel = 4
    
    [interfaces]
    
      # Link-local discovery of other hosts on the same LAN.
      [[Coyote Local]]
        type = AutoInterface
        enabled = Yes
    
      # The loopback hop every Coyote session on this host dials.
      [[Coyote Sessions]]
        type = TCPServerInterface
        enabled = Yes
        listen_ip = 127.0.0.1
        listen_port = 4242
    
        # Reticulum holds announces from unknown peers by default, which would delay a
        # brand-new Coyote identity's first announce. Off here, as on the repo's test relay.
        ingress_control = No
    

    With --relay HOST:PORT this stanza is appended:

    
      # Your team's relay (a propagation node or another rnsd reachable over the network).
      [[Team Relay]]
        type = TCPClientInterface
        enabled = Yes
        target_host = HOST
        target_port = PORT
    
  3. Register a user service so the daemon outlives the shell: a systemd user unit coyote-rnsd ($XDG_CONFIG_HOME/systemd/user/coyote-rnsd.service, so ~/.config/systemd/user/coyote-rnsd.service by default; Restart=on-failure, enabled with systemctl --user enable --now coyote-rnsd; logs with journalctl --user -u coyote-rnsd; the script hints 'loginctl enable-linger <user>' keeps it running when you are logged out), a launchd agent com.coyote.rnsd (~/Library/LaunchAgents/com.coyote.rnsd.plist, log ~/Library/Logs/coyote-rnsd.log), or a Windows Scheduled Task Coyote rnsd that runs rnsd hidden at logon (log %LOCALAPPDATA%\coyote\logs\rnsd.log). A service that is already running is never restarted; when its definition changed, the restart command is printed instead (systemctl --user restart coyote-rnsd; on macOS the launchctl bootout gui/<uid>/com.coyote.rnsd && launchctl bootstrap gui/<uid> <plist> pair, with your numeric uid filled in, because launchd cannot reload a loaded agent in place; on Windows Stop-ScheduledTask -TaskName 'Coyote rnsd'; Start-ScheduledTask -TaskName 'Coyote rnsd').

  4. Wait for the daemon: rnsd is listening on 127.0.0.1:4242, or exit 3 after 30 s, after printing which config the service passes (The service starts rnsd with --config <dir>, so it read <dir>/config) and the last 10 lines of the service log.

On Linux without a user session bus (systemctl --user cannot connect: an ssh session with no lingering user instance, a container, WSL without systemd) the unit is written, the enable command is printed (systemctl --user daemon-reload && systemctl --user enable --now coyote-rnsd), nothing is started, the wait is skipped and the exit is still 0.

The script refuses to run as root or Administrator. On Linux and macOS: running as root is a usage error: rnsd, its config and its service belong to your user. Re-run without sudo, or pass --allow-root. On Windows: running as Administrator is a usage error: rnsd, its config and its task belong to your user. Re-run from a normal PowerShell, or pass -AllowRoot.

It ends with Diagnostics: 'rnstatus' attaches to the shared instance and lists its interfaces and Next: start coyote and run `.mesh on`; rnstatus is the first thing to run when a session cannot reach the daemon. It lists the daemon's interfaces, not Coyote sessions; the block to look at is the LAN one:

 AutoInterface[Coyote Local]
    Status    : Up
    Peers     : 1 reachable

Peers counts the other machines' daemons on the same network, so it reads 1 reachable once a second machine's rnsd is up. That machine's Coyote session itself shows up in .mesh peers, after its announce.

Before that, when it wrote the config (or would have, on a dry run) or found an existing one with an AutoInterface enabled, it prints the firewall sentence:

Firewall: AutoInterface listens for LAN peers, and with enable_transport this rnsd forwards traffic for any Reticulum peer on the LAN (and on to the Team Relay when one is configured).

The ports behind that sentence: AutoInterface discovers on UDP 29716 and carries traffic on UDP 42671, both over IPv6 link-local multicast, the same two ports Coyote's own lan type uses. A Team Relay is an outbound TCP connection from this machine and needs no inbound rule. macOS and Windows append a prompt warning; see Mesh Platform Support.

mesh-relay.sh mesh-relay.ps1 Effect
--relay <host:port> -Relay <host:port> Also dial your team's relay (TCPClientInterface)
--version <X> -Version <X> rns version to install (default: 1.5.2)
--bin-dir <dir> -BinDir <path> Where the rnsd command goes (default: /usr/local/bin or ~/.local/bin); Windows %LOCALAPPDATA%\coyote\bin
--no-service -NoService Skip the user service (Windows: skip the Scheduled Task); nothing starts the daemon and the 30 s wait is skipped; start rnsd yourself before .mesh on
--keep-rnsd -KeepRnsd Keep an older rnsd that is already installed instead of upgrading it to the pinned version
--dry-run -DryRun Print the plan and write nothing
--allow-root -AllowRoot Permit running as root (Windows: as Administrator)
-h, --help -Help Show help

Exit codes: 0 done or nothing to do; 1 usage; 2 unsupported OS or no way to install rnsd; 3 service installed but 127.0.0.1:4242 did not come up in 30 s. The installer with --with-mesh / -WithMesh exits 3 when the mesh setup failed; Coyote itself is still installed. On a TTY without the flag, when no rnsd is in its bin dir yet and it is not running as root, the installer asks Set up the local Reticulum daemon for Coyote mesh now? [y/N]; the PowerShell installer asks the same question as a Yes/No choice, without the [y/N] suffix, and not at all when CI is set.

Two laptops on one network

Two people, two laptops on the same Wi-Fi, and the two sessions should see each other and exchange a first message. Call the laptops A and B; every step runs on both unless it names one.

  1. Install Coyote and run the rnsd one-liner from Setting up rnsd; accept the firewall prompt it triggers (the ports are on Mesh Platform Support). A guest Wi-Fi that isolates its clients keeps the two daemons from hearing each other; use the same ordinary network.

  2. In config.yaml, keep function_calling_support: true (the top-level key under # ---- Function Calling ----) and set the mesh: block to:

    mesh:
      enabled: true
      interfaces:
        - type: private
          host: 127.0.0.1
          port: 4242

    The interfaces: entry is the shipped default, spelled out so it is visible; every other mesh key keeps its default. Start coyote --session a on A (--session b on B). With enabled: true the REPL joins by itself, printing mesh.enabled is true in config.yaml: joining the mesh for this session (`.mesh off` leaves it). and then:

    Mesh is on for this session.
      identity: <32 hex>
      destination: <32 hex>
      instance: <32 hex>
      interfaces: private 127.0.0.1:4242
    Turn it off with `.mesh off`.
    

    Started without --session, it prints Mesh needs a session: ... instead; .session a then joins by itself with the same notice and block. .mesh off keeps the mesh off for the rest of the run (.mesh on joins again), and .exit session leaves the mesh, which the next .session joins again. A WARNING: line after the block means an interface did not come up; see Joining with some interfaces down.

  3. Seeing each other: .mesh peers. The laptop that joined second is listed on the first at once; the first is listed on the second only after its next announce, up to an hour later, and nothing hurries it (see Relay gotchas). Brand-new peers appear within an hour; known peers are reachable at once. Until then the answer is No peers heard yet. Nodes appear here as their announces arrive.; after, one row per node under the name dest identity trust hops last seen header, (no name) <dest8> <id8> untrusted 1 12s ago (<dest8> is the first 8 characters of the destination; display_name, when set, replaces (no name)). Wait until both tables show the other.

  4. Trusting: the person at A reads A's destination: row from the block above (the destination row of .mesh info is the same value) to the person at B, over a call or the chat you already use, and B compares it character for character. Once A is listed in B's .mesh peers, the 8 characters that table shows are enough to read aloud and to type: .mesh trust <A-dest8>; the full hash is the out-of-band check and always works. On B: .mesh trust <A-destination>. It previews This trusts instance <dest8>: a peer you trust can message this node and ask it questions., asks Trust <dest8>? and prints Trusted <dest8> (identity <id8>) - added. Then the mirror on A with B's destination. Once each side has trusted the other, .mesh peers on both shows the other with trust trusted.

  5. First exchange, on A: .mesh reply <B-destination> "hello from A". It previews This sends your text to ..., asks Send it? and prints Sent <id8> to <dest8> (via direct). On B, between prompts: [mesh:message] <A's name or dest8> said: hello from A; envoy replied: ... once B's envoy has answered (a model call on B), and .mesh inbox on B lists the message as [message] from ... via direct at HH:MM UTC: hello from A and the answer under it. The answer reaches A as a [mesh:message] ... says: ... line, as step 6 of Two sessions on one host describes. Every other verb is on Mesh Commands.

  6. When it does not work:

    • No peers after an hour: rnstatus on both; the AutoInterface[Coyote Local] block quoted in Setting up rnsd must read 1 reachable, else the daemons do not hear each other: the network or the firewall ports above.
    • .mesh on fails with Nothing is listening on 127.0.0.1:4242: rnsd is not running; the service commands in Setting up rnsd start it.
    • Peers listed but .mesh trust refused: the identity is denied or blocked; see Mesh Trust Model.

Joining with some interfaces down

.mesh on joins every configured interface on its own. With at least one up, the node is on: the summary is printed with the connected interfaces on its interfaces: row, then one WARNING: line per interface that did not come up, on stderr, in one of three shapes:

WARNING: <label> is unreachable (<reason>); sessions on this machine cannot reach each other until a local Reticulum daemon (rnsd) is listening there.
WARNING: lan is unreachable (<reason>); peers on this link-local segment are out of reach.
WARNING: <label> is unreachable (<reason>); peers behind that relay are out of reach.

The first is a loopback relay, the second lan, the third any other relay; <label> is private 127.0.0.1:4242, lan or <type> <host>:<port>; <reason> is the TCP client's last error as the transport words it (it names the endpoint), or connection timed out when the connect never finished. Autostart with mesh.enabled: true prints the same lines.

With none up, .mesh on fails with the first interface's failure text:

Mesh relay <endpoint> (type: <kind>) is unreachable: <reason>. The node cannot join the mesh until that relay is reachable; fix mesh.interfaces or the relay.

and, when every configured interface is a loopback relay, this hint is appended to it:

 Nothing is listening on <endpoint>. The default configuration expects a local Reticulum daemon (rnsd) there. Run scripts/mesh-relay.sh, or use type: lan to join the LAN without one; see Mesh-Deployment.

A lan entry that cannot bind fails with:

Failed to bind the mesh lan interface: <err>. Another process may hold the discovery port; remove the lan entry from mesh.interfaces or stop that process. Two Coyote sessions on one host need a local relay; see Mesh-Deployment.

A relay that failed keeps retrying every 5 s for as long as the node is on. When it connects, the idle line [mesh] <label> connected is printed and the destination is announced over it once. mesh.started fired with the interfaces that were connected at the time, so nothing refires for it (COYOTE_MESH_INTERFACES on that event names those). The lan bind failure is not retried.

.mesh info while the node is on lists one row per configured interface in the node block, in place of the config block's interfaces[i] rows: interfaces[i] <label> connected, unreachable, retrying: <reason> (a TCP relay) or unreachable: <reason> (a lan bind failure).

Running without rnsd

Without a daemon, point the loopback entry at a team relay instead (private host:port; everything under The ladder about reachability applies), or join the LAN directly with lan.

lan is the Reticulum AutoInterface: IPv6 link-local multicast. It reaches the link-local segment your machine is on and nothing else; .mesh info reports reach: this link-local segment only. It does not cross sbx sandboxes, WireGuard, Tailscale or OpenVPN, and there is no LoRa or I2P on it.

Only one process per host can bind it. A second Coyote on the same host fails with an address-in-use error on the AutoInterface data port :42671 (discovery is on 29716), and a config listing lan twice is refused at load with mesh.interfaces lists lan more than once; only one lan interface can be bound per process. With a daemon, a second session on the same host dials the local rnsd instead.

Two sessions on one host

Two REPLs on one host both dial the local rnsd. With the same config dir, .session a in one and .session b in the other share the identity and the cache (peers.json is last-writer-wins between them, which is harmless) and get their own instance ids and destinations; nothing needs splitting to run. mesh/trust.yaml is shared live: a trust, untrust or block made in one REPL is seen by the other before its next decision, and two REPLs never overwrite each other's records; the write is serialised through mesh/trust.yaml.lock. To exercise trust from both sides, two identities are needed, and then both directories are split, so that each identity keeps its own trust, knock cache and propagation store:

mkdir -p ~/mesh-a ~/mesh-a-cache ~/mesh-b ~/mesh-b-cache
COYOTE_CONFIG_DIR=$HOME/mesh-a COYOTE_CACHE_DIR=$HOME/mesh-a-cache coyote
COYOTE_CONFIG_DIR=$HOME/mesh-b COYOTE_CACHE_DIR=$HOME/mesh-b-cache coyote

Each config dir holds your API keys and, once the node has joined, its identity key; each config.yaml carries your real clients: block and keeps the default mesh.interfaces. The .mesh commands need no model at all; only the digest and the envoy make model calls, so any clients: block that loads is enough.

In each: .session <name>, then .mesh on. Then:

  1. .mesh peers in B lists A once A's announce lands (and vice versa). The side that joined second is listed at once; the side that joined first is listed only after its next heartbeat announce, up to an hour later (see Relay gotchas below). Wait until both tables show the other before going on.
  2. .mesh status <A-destination> in B is refused locally until B trusts A: ... Status is only requested from trusted destinations.
  3. .mesh trust <A-destination> in B. Now B's request reaches A, and A drops it without a word: B's identity is not on A's list at any tier, so B is a stranger, and a stranger's request is neither answered nor recorded. B waits out the 30 s request timeout and prints No response to the mesh request /status within 30.0s. A status request is answered live or not at all; nothing queues it for later. A prints nothing and its .mesh knocks stays empty. The silence is deliberate: a stranger gets no answer at all, while a no access (0xf1) refusal (the number is the protocol's code for that refusal) is what a peer gets when its identity is already on the list but the requesting instance is not (see Knocks).
  4. .mesh trust <B-destination> in A.
  5. .mesh status <A-destination> in B renders A's card.
  6. .mesh reply <A-destination> "text" in B previews This sends your text to ... and asks Send it? (or pass --yes); after Sent <id8> to <dest8> (via direct). the answer comes back: A's envoy answers (a model call on A) with a reply that carries in_reply_to. B opened no question for it, since only mesh__ask does, so B delivers the answer as a message: the idle line is [mesh:message] <A's name> says: ... and .mesh inbox on B shows [message] from <A's name> (<dest8>) via direct at HH:MM UTC: ... (reply to <id8>). [mesh:reply] and [reply] are only ever printed for answers to mesh__ask questions.

Relay gotchas

  • The relay does not replay cached announces to a late joiner, so a node lists a peer in .mesh peers only once it hears that peer announce. Coyote announces at join and then hourly (plus a minute of slack, so the relay never counts a heartbeat as early): the node that joined last lists the earlier one only after the earlier one's next announce (at most an hour; last seen refreshes when it lands), while the earlier joiner lists the late one at once. .mesh trust <destination> needs the row, so on first contact turn both nodes on, do something else, and check .mesh peers on each side now and then.
  • When the relay restarts (an rnsd upgrade, a host reboot) it forgets every path it held through the closed sockets. Each Coyote session's client reconnects on its own, and the node announces again as soon as the client is back, printing [mesh] <label> reconnected; announced (for example [mesh] private 127.0.0.1:4242 reconnected; announced), so peers reach each other again within seconds and no .mesh off/.mesh on is needed. When the node announced less than five minutes earlier it prints [mesh] <label> reconnected; the heartbeat will announce it (last announce <age> ago) instead, and the next heartbeat carries the announce.
  • A peer already in the table is another matter. The transport is built afresh on every .mesh on, so .mesh off followed by .mesh on forgets every route, but not the peer table: a request to a trusted peer the transport has no route to asks the network for a path and waits up to 5 s for the answer, which the relay gives from its own path table (it keeps a path for days). So a request to one peer (.mesh status, .mesh reply, a mesh__ask) from a restarted node reaches a peer it already knew at once, without waiting for a heartbeat. A bulletin (.mesh broadcast) still goes only to peers the transport already has a route to, so right after a restart it reaches none of the persisted peers until their next heartbeat. The answer to a path request is not a sighting: last seen does not move, and mesh.peer.discovered does not fire. Only when nothing on the network answers for the peer does the request fail, and then within those 5 s: `Destination <hash> could not be reached: this node asked the network for a route to it and got no answer within 5 s. The peer may be offline or on a network this node cannot reach. Try again in a minute; nothing was queued for it, because a message can be held on a propagation node only after the peer's announce has been heard. `.mesh peers` shows when it was last heard.` Listings never ask: .mesh peers reads the table, and mesh__peers with with_status: true reports such a peer unreachable at once rather than waiting 5 s for each.
  • ingress_control = No on the relay's TCP server interface keeps Reticulum from holding back a burst of announces from new identities. It is a per-interface setting; under [reticulum] it is never read.
  • lan nodes need IPv6 duplicate-address detection to finish (about 2 s after link-up) before they are heard.

Propagation nodes

LXMF, the messaging layer on top of Reticulum, has propagation nodes: store-and-forward. A message that cannot be delivered over a direct link (a direct encrypted connection between two nodes) is posted to one, which holds it until the recipient fetches it. A .mesh knock whose peer is unreachable takes the same path. Coyote is never a propagation node itself; the deployment/propagation-node directory builds one from the reference daemon, lxmd, for a private team network.

The container is python:3.12-slim with rns==1.5.2 and lxmf==0.9.6, an entrypoint that seeds the volume on first start and then runs lxmd, and everything on the /data volume: the config, the allowed file, the identity and the message storage. A fresh volume mints a fresh node identity, so copy the whole /data to move the node, and never run two copies of it. It publishes TCP 4242 and runs with enable_transport = Yes: it is a team relay and store-and-forward node, which each host's rnsd dials with --relay, and never the way sessions on one host reach each other. The shipped config sets [propagation] enable_node = yes, auth_required = yes and autopeer = no, so the node never replicates its store to another propagation node. auth_required reads /data/lxmd/allowed: one Reticulum identity hash per line, 32 hex characters and nothing else on the line, LF or CRLF, read once at start. The hash to list is a Coyote node's identity: line from .mesh on or .mesh info, not its destination. An empty list fails closed: every fetch is answered no-access. auth_required gates fetching only; posting is stamp-gated and open to anyone who reaches 4242, and so is relaying, so the same rule as for the relay applies: the firewall or VPN in front of the port is the boundary, not Coyote's trust list.

Today Coyote discovers propagation nodes from their announces (.mesh info lists them under propagation_nodes, nearest by hops first) and posts to one when a peer's direct link fails (the send reports via: "store_and_forward" in the tool result, (via store-and-forward) on the REPL line and COYOTE_MESH_VIA=store-and-forward to hooks). Held messages are fetched back automatically: once a propagation node is heard after the node joins and then every mesh.propagation_sync_interval_secs seconds (default 300; 0 and announce: false both leave only .mesh sync), and .mesh sync runs one now (see Mesh Commands). The node table is not kept across restarts, so after a restart the first fetch waits for the node's next announce (up to announce_interval, 30 minutes with the shipped lxmd.config). You cannot pick a node by hand; the nearest by hops is used.

Identity portability

The identity is the mesh/ directory under the config dir. Same config dir, same identity: copy that directory to move an identity to another machine, and know that two machines sharing one config dir share one identity. The instance id lives in the session file, so a session file copied along with the identity announces the same destination as the original; join with .mesh on --fresh on one side when that is not what you want. coyote --sandbox without --fresh copies the host config dir, mesh/ included, into the sandbox, so a node started inside it runs under the host's identity and trust list; see Sandboxes.

Success looks like

Both nodes list each other in .mesh peers as trusted, .mesh status <destination> renders the other side's card within a few seconds, and a .mesh reply produces a mesh.message.received hook event on the receiver and, once the receiver's envoy has answered, a [mesh:message] <name> says: ... line on the sender (the answer is a reply on the wire, but the sender asked no question, so it lands as a message). Containers are on Mesh Containers; Windows machines on Mesh Platform Support.

⚠️ **GitHub.com Fallback** ⚠️