Repository navigation
Mesh Platform Support
The Coyote Mesh works the same on Linux, macOS and Windows. The one difference is how the identity file, the key pair your node signs with, is protected: unix uses permission bits and Windows uses ACLs.
| Platform | How the key is protected | What is refused |
|---|---|---|
| unix |
0600 file inside a 0700 mesh/ directory |
A key readable by others: `Mesh identity file '<path>' is readable by other users (mode <mode>). A private key other users can read must not be used. Run `chmod 600 <path>` and try again.`
|
| Windows | Created owner-only with a protected, single-ACE DACL | A widened DACL or a wrong owner; the refusal carries the curative remedy below |
On Windows the refusal reads:
Mesh identity file '<path>' <problem>. A private key whose permissions cannot be vouched for must not be used. Run <remedy> and try again, or delete the file to mint a new identity; any trust other peers hold for the old identity is lost.
where <problem> is one of is owned by another user, has no DACL, so every user can read it,
inherits permissions from its directory,
carries an access-control entry that is not a plain allow for the current user or grants access to nobody, and
<remedy> is up to three commands, printed separately because PowerShell 5.1 has no &&:
-
takeown /F "<path>", only when another user owns the file. It has to run from an elevated (Administrator) prompt; the refusal appends a note saying so when this step is included. icacls "<path>" /reseticacls "<path>" /inheritance:r /grant:r *<your SID>:F
Limits to keep in mind:
- A local Administrator can always take ownership. That is the same as
0600against root on unix, not absolute protection. - On FAT32 or exFAT the DACL is a silent no-op. Coyote warns and proceeds:
Mesh identity file '<path>' is on a <FAT32|exFAT> volume, which stores no file permissions: every user of this machine can read the key. Keep the config dir on an NTFS or ReFS volume to keep the key private. - The instance lock is mandatory on Windows rather than advisory, so a refusal there can omit the holder's pid: the locked file cannot be read through another handle.
Default directories on Windows: the config dir is %APPDATA%\coyote, so the key is
%APPDATA%\coyote\mesh\identity.key; the cache dir is %LOCALAPPDATA%\coyote.
The mesh runs over Reticulum, a networking stack that gives every participant a cryptographic identity. The usual way
to run it is one daemon per machine that owns the network interfaces: rnsd, a small background program that lets
several programs on one machine share them. The default mesh.interfaces dials that daemon on loopback, 127.0.0.1:4242,
the machine itself; Mesh Deployment has what the setup script writes. Per
platform:
- Linux:
scripts/mesh-relay.shregisters the systemd user unitcoyote-rnsd;loginctl enable-linger <user>keeps it running when you are logged out. - macOS: the script registers the LaunchAgent
com.coyote.rnsd(~/Library/LaunchAgents/com.coyote.rnsd.plist, log~/Library/Logs/coyote-rnsd.log). A changed plist is rewritten, but launchd cannot reload it in place, so thelaunchctl bootout gui/<uid>/com.coyote.rnsd && launchctl bootstrap gui/<uid> <plist>pair is printed, not run. The config enables AutoInterface, and a LaunchAgent cannot raise the macOS firewall prompt itself: until Python is allowed, the log showsAutoInterface[Coyote Local] No multicast echoes received on en0(one line per interface) and no LAN peer is found. Allow it under System Settings > Network > Firewall > Options (Python, or rnsd), then restart the agent; the script prints the same pointer after bootstrapping the agent. Nodes that use only the loopback relay (private 127.0.0.1:4242and a Team Relay) are unaffected. - Windows:
scripts/mesh-relay.ps1(or the installer with-WithMesh) registers the Scheduled TaskCoyote rnsd, which runsrnsdhidden at logon; log%LOCALAPPDATA%\coyote\logs\rnsd.log, config%USERPROFILE%\.reticulum\config(or%USERPROFILE%\.config\reticulum\configwhen that one already exists).
The config enables AutoInterface, Reticulum's automatic discovery of other machines on the same local network, so
macOS and Windows ask whether to allow incoming connections. Both scripts print:
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).
On macOS this is followed by
macOS will ask whether python/rnsd may accept incoming connections; allow it or discovery of LAN hosts will not work.
and on Windows by
Windows will ask whether python/rnsd may accept incoming connections; allow it or discovery of LAN hosts will not work.
No automated test has run a live node on Windows; this walkthrough is how to check one by hand. It takes two machines through joining, trusting, messaging, a knock and a key rotation, and says what each step should print.
-
Two Windows machines, W1 and W2, on one network.
-
rnsdon both, set up byscripts/mesh-relay.ps1; Coyote keeps the shippedprivate 127.0.0.1:4242and the two daemons find each other over the LAN through AutoInterface (allow the firewall prompt). The alternative is a relay both reach, a node that forwards traffic between others: the propagation node container (a node that also holds a message for an offline recipient until they fetch it) on any host, or anrnsdon another host with thescripts/mesh-relay.shconfig andlisten_ip = 0.0.0.0on its TCP server interface (Setting up rnsd quotes the[[Coyote Sessions]]stanza to edit; the relay authenticates nobody: keep 4242 firewalled to W1 and W2). Each machine's rnsd then dials it with-Relay <relay>:4242, or Coyote dials it directly withhost: <relay>in place of its own daemon (Running without rnsd). -
Coyote on both, each with a
config.yamlcarryingfunction_calling_support: trueand:mesh: enabled: true # the REPL joins at start when a session is open; started without --session it prints the no-session refusal and step 2 joins announce: true display_name: w1 # w2 on the other machine interfaces: - type: private host: 127.0.0.1 # <relay> for the alternative port: 4242
The display name is what the knock line in step 10 quotes and what
.mesh peersshows in place of a hash. -
Optionally, a global hook on every
mesh.*event that appends%COYOTE_EVENT%and theCOYOTE_MESH_*variables to a file, to check the hook lines below. See Hooks.
- Start rnsd on both (the script does;
rnstatuslists its interfaces). Expect:rnsd is listening on 127.0.0.1:4242from the script, andrnstatusshowsCoyote LocalandCoyote Sessions. - W1:
coyote,.session w1,.mesh on. Expect: the preview namesprivate 127.0.0.1:4242: the nodes joined to that relay; after confirming,Mesh is on for this session.withidentity:,destination:,instance:andinterfaces:rows.%APPDATA%\coyote\mesh\identity.keyexists andicaclson it shows a single entry for your user with(F)and nothing inherited. No FAT32/exFAT warning on NTFS. Hook log:mesh.startedwithCOYOTE_MESH_INTERFACES=private. - W2: the same with
.session w2. Expect: within a few seconds.mesh peerson W1 listsw2with trustuntrusted. W2 does not list W1 yet: the relay does not replay W1's announce (the periodic "I am here" broadcast by which nodes learn of each other), and Coyote re-announces hourly (plus a minute of slack, so the relay never counts a heartbeat as early), so W2 listsw1only when that heartbeat lands (up to an hour). Wait until both.mesh peersshow the other before step 4. Hook on each:mesh.peer.discoveredwithCOYOTE_MESH_FIRST_SEEN=true. - W2:
.mesh status <W1-destination>. Expect: refused locally with... Status is only requested from trusted destinations. - W2:
.mesh trust <W1-destination>. Expect:Trust <short>?, thenTrusted <dest-short> (identity <id-short>) - added.Hook:mesh.trust.grantedwithCOYOTE_MESH_TRUST_TIER=destination. - W2:
.mesh status <W1-destination>again. Expect:Asking <short> for its status card; Ctrl-C cancels..., then after the 30 s request timeoutNo response to the mesh request /status within 30.0s. A status request is answered live or not at all; nothing queues it for later.W1 drops the request silently because W2's identity is not on its list at any tier: nothing is printed there, no hook fires, and.mesh knocksshowsNo knocks. An untrusted node that wants in appears here. - W1:
.mesh trust <W2-destination>. Expect:Trust <short>?, thenTrusted <dest-short> (identity <id-short>) - added.Hook:mesh.trust.grantedwithCOYOTE_MESH_TRUST_TIER=destination. - W2:
.mesh status <W1-destination>. Expect: the card (name:,objective:,state:, ...,served:). - W2:
.mesh reply <W1-destination> "hello from w2". Expect:This sends your text to w1 (<dest8>, trust: trusted) over the mesh., thenSend it?(or pass--yes), thenSent <id8> to <dest8> (via direct).W1's envoy answers (a model call on W1) with a reply carryingin_reply_to; W2 asked no question (onlymesh__askopens one), so it delivers the answer as a message: W2 prints[mesh:message] w1 says: ...and.mesh inboxon W2 shows[message] from w1 (<dest8>) via direct at HH:MM UTC: ... (reply to <id8>).[mesh:reply]and[reply]appear only for answers tomesh__askquestions. Hooks:mesh.message.senton W2 withCOYOTE_MESH_VIA=direct;mesh.message.receivedon W1 withCOYOTE_MESH_ROUTED=envoy. - Knock check. W2:
.mesh off,.mesh on --fresh(a new destination under the same identity), then.mesh status <W1-destination>. Expect: the request goes through without waiting for W1's next heartbeat. The rejoined W2 has a fresh transport that holds no route to W1, but.mesh peersstill listsw1from the saved peer table, so W2 printsNo route to <dest8> yet; asking the network for one (up to 5 s); Ctrl-C cancels...andrnsdanswers from its path table within a second or two;last seenforw1does not move (a path answer is not a sighting). With W1 alone off, W2's ownrnsdstill answers the path request from its table (a path lives there for days), and the request fails later, at the link stage, after the 10 s link timeout:No response to the mesh request /status within 10.0s. A status request is answered live or not at all; nothing queues it for later.Only when nothing on the network holds a path for W1 (itsrnsdgone too, or the path expired) would the request fail at the path request, and then within 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. A status request is answered live or not at all, so nothing was queued; try again in a minute. `.mesh peers` shows when it was last heard.`Expect W2 to seeThe mesh peer refused the request: no access (0xf1). A status request is answered live or not at all; nothing queues it for later.and W1 to print"w2" (identity <hash>) knocks from instance <hash>with thetrust: | who: | silence:line, and.mesh knockslists it: W2's identity is on W1's list through the step 7 grant, but the new instance is not, which is the one case that is refused out loud and registers a knock. Hook:mesh.knock.receivedwithCOYOTE_MESH_VIA=direct. W1:.mesh trust <new W2 destination>clears it. - Rotation check. W2:
.mesh off,.mesh rotate(the dry run prints its block),.mesh rotate --confirm rotate-<short>,.mesh on. Expect on W1, once W2's announce lands (W1 hears it at once; W2 can contact W1 right away, through the same path request as in step 10):.mesh peersshows W2's trusted instance with thekey changed: announced under identity ...marker and thekey changesrow of.mesh inforeads1 instance(s) announced under another identity; see .mesh peers. W1:.mesh trust <W2's post-rotation destination>clears the mark (... key-change mark is cleared.); read it from.mesh peerson W1 or thedestination:row of.mesh onon W2. - Both:
.mesh off. Expect:`Mesh is off for this session. Run `.mesh on` to join again.`Hook:mesh.stopped.
If a step does not match, the Expect line tells you which side to look at.