Mesh Platform Support - Dark-Alex-17/coyote GitHub Wiki

The Coyote Mesh runs with full parity on Linux, macOS and Windows. The product code is not platform-gated, with one exception: how the identity file is protected, which is the part of the filesystem that differs between unix permission bits and Windows ACLs.

Identity file protection

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 &&:

  1. 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.
  2. icacls "<path>" /reset
  3. icacls "<path>" /inheritance:r /grant:r *<your SID>:F

Caveats the code cannot close:

  • A local Administrator can always take ownership. This is parity with 0600 against root, 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.

Running rnsd per platform

The default mesh.interfaces dials a local Reticulum daemon on 127.0.0.1:4242; Mesh Deployment has what the setup script writes. Per platform:

  • Linux: scripts/mesh-relay.sh registers the systemd user unit coyote-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 the launchctl bootout gui/<uid>/com.coyote.rnsd && launchctl bootstrap gui/<uid> <plist> pair is printed, not run.
  • Windows: scripts/mesh-relay.ps1 (or the installer with -WithMesh) registers the Scheduled Task Coyote rnsd, which runs rnsd hidden at logon; log %LOCALAPPDATA%\coyote\logs\rnsd.log, config %USERPROFILE%\.reticulum\config.

The config enables AutoInterface, 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.

What is covered where

Windows mesh behaviour is covered by the unit tests, the platform-independent vectors and manual verification, not by the Python interop matrix. The windows-latest CI leg runs the non-runtime unit tests (codecs, stores, trust, identity, the DACL tests included), the conformance vectors and the fuzz harness; the tests that start a node or drive a live link are unix-only, since they use unix-only loopback fixtures. The Python interop harness is unix-only and the network namespace suite Linux-only, and the Mesh Interop CI job that runs them runs on Linux only; the manual procedure below is what stands in for them. See How it is verified.

Manual two-machine Windows verification

Note: This procedure has not been executed in the run that produced this page. The result is to be recorded in the pull request that ships it.

Setup

  • Two Windows machines, W1 and W2, on one network.

  • rnsd on both, set up by scripts/mesh-relay.ps1; Coyote keeps the shipped private 127.0.0.1:4242 and the two daemons find each other over the LAN through AutoInterface (allow the firewall prompt). The alternative is a relay both reach: the propagation node container on any host, or an rnsd on another host with the scripts/mesh-relay.sh config and listen_ip = 0.0.0.0 on 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 with host: <relay> in place of its own daemon (Running without rnsd).

  • Coyote on both, each with a config.yaml carrying function_calling_support: true and:

    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 peers shows in place of a hash.

  • Optionally, a global hook on every mesh.* event that appends %COYOTE_EVENT% and the COYOTE_MESH_* variables to a file, to check the hook column below. See Hooks.

Steps

  1. Start rnsd on both (the script does; rnstatus lists its interfaces). Expect: rnsd is listening on 127.0.0.1:4242 from the script, and rnstatus shows Coyote Local and Coyote Sessions.
  2. W1: coyote, .session w1, .mesh on. Expect: the preview names private 127.0.0.1:4242: the nodes joined to that relay; after confirming, Mesh is on for this session. with identity:, destination:, instance: and interfaces: rows. %APPDATA%\coyote\mesh\identity.key exists and icacls on it shows a single entry for your user with (F) and nothing inherited. No FAT32/exFAT warning on NTFS. Hook log: mesh.started with COYOTE_MESH_INTERFACES=private.
  3. W2: the same with .session w2. Expect: within a few seconds .mesh peers on W1 lists w2 with trust untrusted. W2 does not list W1 yet: the relay does not replay W1's announce, and Coyote re-announces every 15 minutes, so W2 lists w1 only when that heartbeat lands (up to 15 minutes). Wait until both .mesh peers show the other before step 4. Hook on each: mesh.peer.discovered with COYOTE_MESH_FIRST_SEEN=true.
  4. W2: .mesh status <W1-destination>. Expect: refused locally with ... Status is only requested from trusted destinations.
  5. W2: .mesh trust <W1-destination>. Expect: Trust <short>?, then Trusted <dest-short> (identity <id-short>) - added. Hook: mesh.trust.granted with COYOTE_MESH_TRUST_TIER=destination.
  6. W2: .mesh status <W1-destination> again. Expect: Asking <short> for its status card; Ctrl-C cancels..., then after the 30 s request timeout 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. 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 knocks shows No knocks. An untrusted node that wants in appears here.
  7. W1: .mesh trust <W2-destination>. Expect: Trust <short>?, then Trusted <dest-short> (identity <id-short>) - added. Hook: mesh.trust.granted with COYOTE_MESH_TRUST_TIER=destination.
  8. W2: .mesh status <W1-destination>. Expect: the card (name:, objective:, state:, ..., served:).
  9. W2: .mesh reply <W1-destination> "hello from w2". Expect: This sends your text to w1 (<dest8>, trust: trusted) over the mesh., then Send it? (or pass --yes), then Sent <id8> to <dest8> (via direct). W1's envoy answers (a model call on W1) with a reply carrying in_reply_to; W2 asked no question (only mesh__ask opens one), so it delivers the answer as a message: W2 prints [mesh:message] w1 says: ... and .mesh inbox on W2 shows [message] from w1 (<dest8>) via direct at HH:MM UTC: ... (reply to <id8>). [mesh:reply] and [reply] appear only for answers to mesh__ask questions. Hooks: mesh.message.sent on W2 with COYOTE_MESH_VIA=direct; mesh.message.received on W1 with COYOTE_MESH_ROUTED=envoy.
  10. Knock check. W2: .mesh off, .mesh on --fresh (a new destination under the same identity), then .mesh status <W1-destination>. Expect: first `Destination <hash> cannot be reached yet: its announce has not been heard since this node started. Wait for it to announce, or check `.mesh peers`.` because the rejoined W2 has a fresh transport and has not heard W1's announce yet (.mesh peers still lists w1 from the saved peer table, but that is not a route). Wait for W1's next heartbeat (last seen for w1 refreshes, up to 15 minutes), then repeat the request. Expect W2 to see The 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 the trust: | who: | silence: line, and .mesh knocks lists 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.received with COYOTE_MESH_VIA=direct. W1: .mesh trust <new W2 destination> clears it.
  11. 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 again needs W1's next heartbeat before it can contact W1): .mesh peers shows W2's trusted instance with the key changed: announced under identity ... marker and the key changes row of .mesh info reads 1 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 peers on W1 or the destination: row of .mesh on on W2.
  12. Both: .mesh off. Expect: `Mesh is off for this session. Run `.mesh on` to join again.` Hook: mesh.stopped.

Record per step: pass or fail, plus the observed lines.

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