Pick the machine you are standing in front of. Each section is its prerequisites, the commands, and what you should see — then the reasoning, underneath, for when you want it.
A laptop, a NAS, a home server. One script, a container, a systemd unit.
A full participant, not a client. From a self-hosted F-Droid repository, or built here.
A view onto the daemon already running on this machine, installed as
an .lgx package.
The reachable member that anchors NAT traversal and relays for peers that cannot connect directly.
curl -fsSLO https://raw.githubusercontent.com/vpavlin/shrooms/master/scripts/install.sh
Then one of two verbs, not both. On your first machine, the one that creates the mesh and keeps its admin key:
sudo bash install.sh init --mesh home --name laptop
It prints a recovery key once and never again, so read The first mesh before running it. On every machine after that, which is admitted rather than creating anything:
sudo bash install.sh prepare --name nas
prepare installs the same daemon with no mesh at all and
leaves it waiting, which is what an invite needs it
to be doing.
If a firewall is running, open the port — on laptops too, not only servers:
sudo ufw allow 51820/udp # Ubuntu, Debian
sudo firewall-cmd --add-port=51820/udp --permanent && sudo firewall-cmd --reload # Fedora, RHEL
The daemon is running either way. After init you are on a mesh
of one and the next thing to do is
admit a second device. After prepare it
is waiting, and redeeming an invite brings it up
without a restart.
That pipes somebody else's script into a root shell. It installs a container, writes a systemd unit and hands a network interface to a daemon — which is what you want, and is indistinguishable from what you would not want until you have read it. It is 953 lines with nothing clever in it: scripts/install.sh.
docker or podman, and systemd/dev/net/tun presentTwo peers need at least one of them reachable. A laptop with a default-deny firewall cannot be reached by a phone on the same wifi, so both ends announce perfectly good addresses, see each other as online, and fail every handshake — which looks like almost anything except a closed port.
The installer prints the right command for whichever firewall it finds running, and says nothing when none is.
After prepare:
==> checking this machine
==> fetching ghcr.io/vpavlin/shrooms:latest
==> generating config (prepare)
==> installing the service
Installed and running, waiting for a mesh.
On a machine already on one:
shrooms invite
and back here:
sudo shrooms join <TOKEN> --name nas
After init the same three lines, and then the mesh itself —
network key, admin key, and the recovery key printed once. That output is
in The first mesh.
Three verbs. init creates a mesh, join <NETWORK-KEY>
joins one you have the key for, and prepare installs and starts
the daemon with no mesh at all. prepare is the one to pair with
invites — a daemon without a mesh still holds its control socket and waits
to be told which network it is on, which is exactly what
shrooms join needs it to be doing.
Everything after the verb is handed to shrooms unchanged —
--name, --relay, --advertise,
--port — so the script cannot drift out of date with the flags
the binary supports. The device name defaults to this machine's hostname.
Nothing is built on the box. The container carries the binary and liblogosdelivery, which needs glibc 2.38 and exists only where Logos Basecamp is installed, so shipping an image rather than a tarball is what makes a Debian 12 machine work at all. It runs with host networking on purpose: behind docker's bridge the address peers observe would be the gateway's and the source port would be rewritten, so traversal would be fighting a layer of NAT that does not exist in reality.
It touches /etc/shrooms/ (config), /var/lib/shrooms/
(device identity), /run/shrooms/ (control socket), a
shrooms systemd unit and container, a
shrooms-resolved unit that registers mesh names with the host's
resolver, and
/usr/local/bin/shrooms — a wrapper so shrooms status
works on the host without anyone remembering the docker incantation. It runs
docker exec -i, with a stdin, so the commands that prompt work
through it. Under sudo on a podman host: the daemon's container
is root's, and a rootless podman cannot see it.
With one exception, which is the whole reason the wrapper is more than one
line. The commands that touch the mesh authority —
init, invite, admin,
keycard — read and write ~/.config/shrooms, and the
daemon container has no such directory: its unit mounts the config, the state
and the socket, and nothing of yours. So those run in a sibling
container with your admin directory mounted, rather than inside the daemon's.
The daemon is deliberately left without it: it verifies credentials and never
signs one, and a process that cannot read a key cannot leak it
(ADR-025).
Re-running is safe: an existing config and identity are left alone unless
you pass --force. Losing the identity would mean a new overlay
address and looking like a different device to every peer, so it is never
destroyed by accident. On a SELinux host the bind mounts are relabelled
shared, without which the daemon cannot bind its control socket and systemd
restarts it forever.
Read the script before running it as root, as you should with anything fetched this way.
Worth it when you want the binary on the host rather than in a container —
and it is what scripts/deploy.sh needs on the machine you drive
a remote install from.
git clone https://github.com/vpavlin/shrooms && cd shrooms
make deps-release # prebuilt liblogosdelivery; there is no distribution of it
make shrooms
sudo make install # binary, libraries, systemd unit, bash completion
make install relinks the binary with an rpath pointing at
/usr/local/lib/shrooms, so it keeps working after you delete
the checkout. It needs Go 1.23+ with cgo and a C toolchain to build, and
glibc 2.38 or newer to run — Debian 12 has 2.36 and will not do, which is
the case the container exists for. make deps builds the library
from source and currently fails on an upstream compile error;
make deps-release downloads a repackaged copy instead, which is
the one you want.
In F-Droid: Settings → Repositories → add
https://apps.vpavlin.xyz/fdroid/repo
Install Shrooms from that repository's app list.
Open it and scan the QR from shrooms invite, or paste the
token into the single field — it takes an invite or a network key and tells
them apart itself.
The app on the launcher, opening on a single join screen with one field. After joining, the roster fills with the same peers the daemon reports, and Android's VPN key appears in the status bar.
That F-Droid repository is self-hosted. There is no Google Play listing and there is not going to be one — the repository is the author's own, published alongside the other Logos modules on the same host, and adding it is a decision you are making about who you take software from.
arm64 only. There is no x86_64 build of the delivery library, so an emulator has no node and the app is a phone-shaped thing to test on a phone.
The app is a Compose UI and a VpnService around the same Go core
as the daemon — nothing in internal/ is reimplemented. It joins
several meshes at once and roams between wifi and mobile data without you
touching it: direct on wifi, relayed on a carrier's CGNAT, and the transition
survives in both directions.
ANDROID.md
is the build in detail, and
ADR-016
is why it reuses the core rather than reimplementing it.
make apk # android/shrooms.apk, debug-signed
adb install -r android/shrooms.apk
make fdroid # build unsigned, sign on the repo host, publish
make apk needs the Android SDK and an NDK; it builds the
.aar in a container first, because gomobile wants a JDK and a
newer Go than the core deliberately does. make fdroid is the
publishing half and only useful if you run a repository of your own: the
signing key lives on that host and never leaves it, so the APK is built here
unsigned and signed there.
In Basecamp: Package Manager → Repositories → add
https://apps.vpavlin.xyz/basecamp/logos-repo.json
Install Shrooms from the list. It pulls in
shrooms_core itself — that is a declared dependency, not
something to fetch by hand.
socket_group set in /etc/shrooms/config.toml to
a group you are in
Shrooms in Basecamp's app list, and opening it shows the same roster
shrooms status prints in a terminal: peers, whether each is
direct or relayed, throughput, and how long each took to connect. If the
socket group is not set it says so, rather than showing an empty mesh.
It also changes things, which is the part that removes the terminal: this device's name, whether it runs the rendezvous plane as edge or core, what it publishes and whether those names and its bound ports are announced, whether it relays for a mesh, whether the router is asked for a way in, which meshes run, joining one with an invite token and leaving one — and restarting the daemon, which is what applies the settings that need it. Beside that: every device's services as addresses you can click, when each credential expires, and the daemon's own log, because Basecamp cannot read the journal.
Two packages, versioned separately:
shrooms is the view and shrooms_core
is the part that talks to the daemon. The view declares the
dependency, so installing from the repository brings both.
Fetching files by hand means fetching both, or you get an
interface whose buttons do nothing.
The view reads the daemon through shrooms_core, a companion
module installed alongside it. That indirection is not ceremony. A
ui_qml app runs inside a sandbox where a deny-all network
manager blocks every outgoing request and XMLHttpRequest refuses
local files, so neither a status file nor a port is reachable from the view,
whatever their permissions. A Logos module runs in its own process and is not
sandboxed, which is the route Basecamp prescribes: it does one request over
the daemon's control socket and hands back the JSON.
The socket group is a real grant, like docker's.
Anyone in it can read your mesh's control plane and change this device's
behaviour. Name a group you would trust with that — on a personal machine,
your own login — and leave it unset on a shared one. The socket is
0660 and the daemon runs as root for CAP_NET_ADMIN,
which is what makes the group half of that mean anything.
It can change what this device decides for itself: its name, relay or light
node, published services, which meshes run, and leaving one. What it
cannot do is admit anybody, and that is the line the design
draws. Membership is a credential signed by an admin key the daemon has
never held — a passphrase-protected file in your home directory — so nothing
reachable through that socket can make a device a member or remove one.
Admission stays with shrooms invite and a passphrase prompt,
which is where the friction belongs.
ADR-025 is the whole
argument.
This is why the repository is the way in. An
.lgx on its own is a package, not a repository: it has no
update mechanism, nothing checks for a newer version, and nothing
notifies you. A file installed by hand is the version you have until you
remember to go and look.
Installing from
apps.vpavlin.xyz/basecamp/logos-repo.json gives Basecamp
something to check, and resolves shrooms_core for you.
Fetching the files directly still works and is what
Building the package yourself produces.
make basecamp-check # load the real view offscreen against a fixture
make basecamp-lgx # build the installable package
make basecamp-publish # cut a GitHub release carrying it
basecamp-lgx builds the portable variant through nix, because
the plain one can reference paths in the nix store of the machine that built
it — fine for a dev loop and useless as a download. CI builds it on every
push and uploads it as an artifact. basecamp-publish uploads two
assets to a release: a versioned name so an install can be pinned, and the
stable one linked above.
ssh root@VPS_IP
curl -fsSLO https://raw.githubusercontent.com/vpavlin/shrooms/master/scripts/install.sh
sudo bash install.sh prepare --name vps --relay
sudo ufw allow 51820/udp
Redeem an invite from a machine already on the mesh.
/dev/net/tun,
and it is worth checking before you paydocker or podman, and rootInstalled and running, waiting for a mesh.
A firewall is running here. Peers reach this node on UDP 51820, so open it:
sudo ufw allow 51820/udp
Then redeem an invite from a machine already on the mesh. Resource use is negligible — a measured node acting as a relay sits at about 20 MiB RSS and one to two per cent of one core — so what matters on a VPS is bandwidth allowance and latency, since relayed traffic takes a detour through it.
A node behind NAT learns its own public address by reflection: a peer's reply echoes the address that peer observed. That needs a peer outside your NAT. On a mesh whose members are all behind one router, no node can learn its public address at all, every candidate it announces is a LAN address, and the mesh works from the sofa and not from the street. One reachable member is the anchor that fixes this — and, being reachable, it is also what can relay for two peers that cannot reach each other directly.
You probably do not need a VPS. Any node with a reachable
address can relay: set relay = "true" in its config and restart
it. Nothing else needs configuring — the relay advertises itself in its
ordinary announce and every other node picks it up, so there are no relay
addresses to distribute and its IP can change freely. An office box with a
port forward or a home server on a static address covers every pair that
cannot connect directly.
A node will also ask the router for a way in — PCP first,
then NAT-PMP, both on the gateway's UDP 5351. That is on by default
(port_mapping), and a laptop behind a domestic NAT has been
granted a mapping, announced it, and been dialled directly by a phone on
mobile data, on a mesh with no publicly reachable member at all. Whether
your router answers is between you and your router; when it does not,
nothing is worse than before.
Two things it does not fix. Carrier-grade NAT can be neither punched nor mapped, so a phone on mobile data needs a relay somewhere in the mesh. And punching between two NATed peers is still unproven — a reachable member is what makes the awkward cases work today. ADR-012 covers who should host one.
scripts/deploy.sh is the alternative when you have the
repository checked out and would rather not type on the remote at all. It
wants docker on the far side rather than podman, and a built
./bin/shrooms here.
./scripts/deploy.sh root@VPS_IP --relay --name vps --key <NETWORK-KEY>
./scripts/deploy.sh root@VPS_IP --init --relay --name vps # or create the mesh
With --init the config is generated locally, by your
own binary, and only then copied over — which is why the passphrase prompt
works and why the admin key stays on your machine rather than on the VPS.
The config travels over ssh stdin and lands with install -m600,
so the network key is never readable at a predictable path in the remote's
/tmp. The remote generates its own device identity, so no
private key crosses the wire. It refuses to overwrite an existing config
without --force, pulls the published image on the remote by
default, and builds from your working tree when you set
BUILD_IMAGE=1.
One command, on the machine that will hold the admin key —
shrooms init where the binary is on the host, and
bash install.sh init where it is in a container. It mints the
network key, a pair of admin keys, and this device's own credential, then
asks twice for a passphrase to encrypt the admin key at rest:
It is also what to run on a machine already set up with
install.sh prepare: that writes a config with no key, and
init mints into it, keeping the name, port, mode and relay
setting it already has. A config that is already on a mesh is refused
instead, since minting over it would leave the device holding a key no peer
has heard of.
sudo shrooms init --mesh home --name laptop
Network key: GUPDIZSVIQRCBBC6APQFYDSDKTWK6Q5SYZY6ZLMVECQDQX3GIOYQ
copy this to your other machines — it is the only secret
Device: laptop
Overlay IP: fd12:b993:4542:c6cd:1f4c:cc1a:496c:d791
Mesh prefix: fd12:b993:4542::/48
Wrote /etc/shrooms/config.toml
Minted the mesh authority.
mesh id 65V5K5ISCBHQJAEYWSH2PDPLYQ
admin key /home/you/.config/shrooms/admin-home.json
this device is enrolled
RECOVERY KEY — written down now or never. It is not saved anywhere,
and it is what lets you keep this mesh if the admin key above is lost:
dmaWji2cz8403ySaOkMB18Qguy4yLx783btjYhYZMvUTHxqHJFPJuJ4ms0/QVuNSyDwRbdvx7cHO3xkHkiXzPQ==
Store it away from this machine. A password manager, a Keycard, paper.
Two admin keys, always, because the set is fixed at mint: the mesh id
commits to it and the address prefix derives from the id, so adding a key
later would re-address every node. One stays here, encrypted with the
passphrase you typed a moment ago. The other is the recovery key, printed once and
never written anywhere — if you skip past it and later lose
~/.config/shrooms/admin-home.json, you cannot enrol another device
into this mesh again.
The admin key belongs to the person rather than to the machine, so under
sudo it is written to the invoking user's home, not root's.
Then start the daemon:
sudo systemctl enable --now shrooms
The container installer's init works the same way, and does the
starting for you: the setup container is given a stdin so the passphrase
prompt has somewhere to read from, and your own
~/.config/shrooms is mounted into it so the admin key survives
the container it was minted in. --no-admin, on either, creates
a mesh with no authority at all, where membership is the network key alone
and nothing can be revoked short of rotating it for everybody.
If this machine already ran install.sh prepare,
run the installer again with the other verb — there is nothing to undo
first:
sudo bash install.sh init --mesh home --name laptop
init mints into the config prepare wrote,
keeping the name, port, mode and relay setting already chosen there, and
anything you pass on the command line wins over them. The device
identity under /var/lib/shrooms is untouched either way, so
the machine keeps the keys it generated. A config that is already
on a mesh is refused instead, since minting over it would leave
this device holding a key no peer has heard of.
On a machine that is already a member, with its daemon running — the daemon is what holds the invite open, since it is the node already connected to the fleet. It asks for the admin key's passphrase, then prints a token, a QR code, and waits:
shrooms invite
Invite valid for 15m0s. On the joining device:
shrooms join BEGUZ-N4WOX-PYMTR-CYKWT-QBYSX-U
[QR code]
Waiting. An invite is answered only by the node that issued it,
which is what makes it single-use — so leave this running.
Then, on the newcomer, while the other machine is still waiting:
sudo shrooms join BEGUZ-N4WOX-PYMTR-CYKWT-QBYSX-U --name nas
Asking to join as "nas"...
Enrolled. Credential serial 1786439411, expires 2026-09-10T11:03:51+02:00.
The token is 128 bits, good for one device and fifteen minutes. Both sides derive from it where to meet and what to encrypt with, so a wrong token addresses a topic nobody answers rather than a guess anyone can grind against. What comes back is the network key, the admin keys and a credential issued to that device's own keys — the network key never appears on a screen.
If a daemon is already running on the joining machine and has not joined
anything, the join goes through it and it brings the mesh up itself: no
second command, no restart. That is what a machine installed with
install.sh prepare looks like.
The phone joins the same way. Scan the QR, or paste the token into the one field on the join screen — it takes an invite or a network key and tells them apart itself.
Joining is by invite only. shrooms join
<NETWORK-KEY> was removed on 2026-08-27: a raw network key no longer makes
a device a member. A machine set up by someone who never sees the key still joins by
shrooms invite. Credentials expire in thirty days by default, so renewal
is another invite - and on a mesh minted with --no-admin the invite
carries the network key instead of a credential.
Somebody else — a colleague, a script, an agent — can install and configure a node without ever holding your network key. Prepare the machine without it, and put the key in yourself afterwards.
The key is worth withholding even though, on a mesh with admin keys, it no longer admits anybody: it still decrypts the control plane, so whoever has it can read who is on the mesh and what is announced. Withholding it is about that, not about membership — see the note below for how the device actually becomes a member.
# on the machine being set up, by whoever is doing the setup
sudo shrooms prepare --name nas --relay
Prepared /etc/shrooms/config.toml for "nas".
This device has its keys and its name. It is not on a mesh yet.
sudo systemctl enable --now shrooms
# on a machine already in the mesh
shrooms invite
shrooms join BEGUZ-N4WOX-PYMTR-CYKWT-QBYSX-U
# back on the new machine
sudo shrooms join BEGUZ-N4WOX-PYMTR-CYKWT-QBYSX-U
Enrolled. Credential serial 1786439411, expires 2026-09-26T09:14:02+02:00.
The device identity is generated during prepare, so the
machine's overlay address is settled before it joins anything and does not
change when it does. Nothing secret is in the config that lands: a name, a
port, and this device's own public keys.
The network key stopped being membership, and the commands that
treated it as membership are gone. shrooms join
<NETWORK-KEY> and shrooms set-key were removed on
2026-08-27. They were how this worked before credentials: the key WAS the
membership, so everybody holding it was a member, nobody could be removed
without changing it for everybody, and it travelled by whatever means came
to hand.
An invite carries the same key sealed to one device for fifteen minutes, and what makes that device a member afterwards is an admin-signed credential that can be revoked. The key still decrypts the control plane, so it is worth protecting — it just no longer admits anybody.
For a rollout with nobody present to redeem an invite, the admin issues a credential for that device's keys and hands over the one line it prints:
# wherever the admin key lives
shrooms admin issue --device <hex> --wg <hex> --name nas
Install it on that machine:
shrooms credential set <credential>
Expect discovery in 15 to 25 seconds from cold — most of that is the messaging node joining the fleet — and a handshake shortly after.
shrooms status
network fd48:d107:3fce::/48 peers 2 (2 up)
self laptop fd48:d107:3fce:2b84:226b:ac:f2c3:9f30
NAME OVERLAY IP ANNOUNCE TUNNEL ENDPOINT RX/TX CONNECTED IN
vps (relay) fd48:d107:3fce:a332:855c:1059:8060:59d7 online up 42s 203.0.113.4:51820 12.4K/8.1K 18s
phone fd48:d107:3fce:9d1c:7b2e:41a:cc90:1f22 online up 1m relay:…@203.0.113.4:51820 1.1K/900 24s
ping6 vps.mesh
ssh vps.mesh
A healthy roster has both columns and they mean different things.
ANNOUNCE is the gossip bus: the peer's announcement is
arriving, so discovery works. TUNNEL is WireGuard: a
handshake completed, and the age beside up should keep resetting,
since a live session rekeys about every 165 seconds. A peer that is
online with no handshake is a traversal problem,
not a discovery one — and a tunnel that says stale 12m is dead
whatever the announce column claims.
Names need nothing set up on a host install: the daemon runs a resolver for the mesh and registers it with the system on startup, scoped to its own interface, so mesh names are answered there and every other name keeps going wherever it went before.
The container install cannot do that half itself, so
the installer arranges it. Registering means running
resolvectl, which belongs to systemd and is not in the
image — the daemon serves names correctly and nothing on the host ever
asks it. So install.sh writes a second unit,
shrooms-resolved.service, which runs those two commands
from the host on the daemon's behalf.
It watches rather than firing once, and that is not
belt-and-braces. Redeeming an invite produces no systemd event at all:
shrooms join hands the token to the waiting daemon, which
re-executes itself into the mesh — same pid, no restart — so the moment
names first become answerable is invisible to the service manager. On a
machine set up with prepare, which is every device after
the first, nothing would ever register them. So it reconciles every
thirty seconds: registered already and resolved still holding the link,
do nothing; no mesh in the config yet, do nothing and do not even wake
the container; otherwise register. That also covers a later restart in
place and resolved forgetting a link. Stopping it reverts.
Mounting the host's D-Bus socket into the container would also work, and is deliberately not what happens: that hands a VPN daemon the whole system bus in order to register one domain.
If the host has no resolvectl, the installer says so and
names resolve through /etc/hosts instead. Note that
shrooms hosts --write through the wrapper writes the
container's copy, which nothing reads and the next restart
destroys — so print it and write it yourself:
sudo shrooms hosts | sudo tee -a /etc/hosts
Either way shrooms status is where you find out: it says
names !! not resolving here, with the two commands and this
device's own address, whenever the resolver is serving and nothing has
registered it.
shrooms paths
reflexive addresses (as peers observe us):
203.0.113.9:41001
This is the most informative single output. One address means endpoint-independent NAT, so direct connections between NATed peers should work. Several means endpoint-dependent — symmetric — NAT, punching will not work, and traffic falls back to the relay, which is exactly why the relay exists.
| Symptom | What it means | What to do |
|---|---|---|
tun: … (need CAP_NET_ADMIN) |
the process cannot create the TUN device, or there is none to create | run it under sudo, and check /dev/net/tun exists — OpenVZ and LXC hosts often have none |
| no peers at all after 60 s | the daemon is not reaching the fleet. This is discovery, not tunnels | check outbound connectivity; make s1 confirms whether the rendezvous plane works at all |
!! rendezvous: in status |
the fleet is unreachable, or you are on a different cluster than it is | established tunnels are unaffected while you fix it. different clusterId reported: N vs M in the log means a preset mismatch; cluster_id overrides it |
peer online but no handshake |
traversal, not discovery: it is announced, you cannot reach it | shrooms paths; open the UDP port on whichever side should be reachable; make one member reachable if neither is |
peer shows stale 12m |
the tunnel is dead — the peer has not rekeyed within WireGuard's 180 s session lifetime | check the peer is actually up; a restarted peer leaves the other side holding a session that stays valid for a while |
mesh names do not resolve — ping6 nas.mesh says
Name or service not known while status shows the
peer up |
the resolver is running but the host is not asking it — serving DNS and
being asked are different things. The daemon cannot do the second half from
inside a container (no resolvectl in the image), which is what
shrooms-resolved.service is for; if that unit is missing,
failed, or the host has no systemd-resolved, nothing registered |
systemctl status shrooms-resolved first — it logs which
interface and address it registered, or why it gave up.
shrooms status prints the two commands with this device's
address filled in, to run on the host rather than through the wrapper. Then
resolvectl status shrooms0 should list the overlay address
under DNS Servers and the suffix as a routing domain.
sudo shrooms hosts | sudo tee -a /etc/hosts is the fallback
where there is no resolved at all; --write is for a host
install, since through the wrapper it writes the container's own file |
shrooms join times out, and the daemon log repeats
connectToRelayPeers: won't attempt new connections - node is
offline beside Failed to query DNS … address=one.one.one.one
error="(1) Operation not permitted" |
the delivery library does not use the system resolver. Its
dnsAddrsNameServers defaults to 1.1.1.1 and
1.0.0.1, which is how it resolves the fleet's
/dns4/ entry nodes and how it probes whether it is online at
all. Anything that blocks direct DNS to those addresses — a VPN with
DNS-leak protection, a network that permits only its own resolver, a local
service holding 1.1.1.1 — fails the probe, and the node then
declares itself offline and dials nothing. The mesh never gets as far as
being unreachable |
dig +short @1.1.1.1 one.one.one.one. If that fails while
host one.one.one.one works, this is it — the two go through
different paths and only the direct one matters here. Disconnecting the VPN
restores it; if it does not, look for a "block when disconnected" setting,
which keeps the firewall rules on while the tunnel is off. Then restart
shrooms and read the log with --since: journalctl … |
head shows the oldest entries, and diagnosing last week's
failure as though it were this minute's is the easiest mistake here |
shrooms status says the shrooms container is not
running, and systemctl status shrooms says it plainly
is |
a container install on podman, run as yourself. The daemon's container
belongs to root, and rootless podman is a separate store that
cannot see root's containers at all — including through the
docker shim that podman-docker installs |
sudo shrooms .... On a real docker host the equivalent is
membership of the docker group, which is what lets the check
see the container without root |
shrooms status needs sudo |
the daemon needs CAP_NET_ADMIN and so runs as root, which makes its control socket root:root |
set socket_group = "your-username" in the config. The unit must also carry CAP_CHOWN in both AmbientCapabilities and CapabilityBoundingSet, or the chown fails with EPERM even for uid 0 — the setting appears to work and every read still says permission denied |
error: EOF from a prompt |
the command wanted a passphrase or a key and stdin was closed | run it in a terminal rather than from a script, a unit or a pipe |
| the daemon exits immediately | a shared library is missing under a bare binary | deploy the container rather than a tarball, or install with sudo make install, which sets the rpath |
missing liblogosdelivery.h when building |
the library is not where the build expects it | make deps-release, or make deps-basecamp HDR=… to reuse a local Basecamp install |
| the Basecamp view shows nothing | it cannot reach the daemon's control socket, or the daemon is not running here | the view says which; set socket_group to a group you are in and restart the daemon. The module also needs shrooms_core installed beside it |
Two failures worth knowing about before they happen, because neither looks
like a failure. The rendezvous plane can die while everything looks
fine — tunnels keep carrying traffic, the roster stops changing, and
peers age out one at a time until the status page is a list of healthy
devices marked offline. Both the daemon and the app watch for it now, and
recovery is a restart in both places. And renewal has never been
watched happen on a live mesh: the sweep is built and unit-tested,
but the clock on a credential is thirty days, and
shrooms invite again is the shortest way to reset it.
Names and services, several meshes at once, relays, renewals, and binding a service so only the mesh can reach it.
The architecture, every design decision with its reasoning, and how to build and test it.
What leaks, what is deliberately deferred, and what to know before running this anywhere that matters.