party list already shows invite-only parties; knock closes the loop. The knocker can't run any tmux command against the server, so the channel is write(1), the same one invite uses toward guests, except here delivery failure is surfaced: the ping is the whole feature.
130 lines
8.4 KiB
Markdown
130 lines
8.4 KiB
Markdown
# tmux-party
|
|
|
|

|
|
|
|
Share a live tmux session with your buddies on a wide range of UNICES!
|
|
|
|
`tmux-party` ships `party`, a POSIX shell utility. One person hosts a party, and others join the fun! Wanna pair-program, mentor, debug an outage together, or demo a workflow? `ssh` in, `party join` & off you go!
|
|
|
|
Built for small, mutually trusted groups: a hacklab, a tech team, a circle of friends, not strangers across the internet. Inspired by epic sessions running GNU's `screen -x` way back when, now on BSD's `tmux` modern codebase!
|
|
|
|
## Requirements
|
|
|
|
- POSIX shell (`/bin/sh`)
|
|
- `tmux` ≥ 3.3 (for `server-access`) on `$PATH`, or named via `PARTY_TMUX`
|
|
- A shared system group. A party is gated on the first of `staff`, `users`, `wheel`, `party` the host belongs to (`--group <name>` or `TMUX_PARTY_GROUP` always win). Boxes where people already share one of those groups need no setup; creating a dedicated `party` group is the fallback recipe. Every host and guest must be a member of whichever group ends up gating the party.
|
|
- `write(1)`, optional: used to ping invited guests. Silently skipped when absent.
|
|
|
|
## Install
|
|
|
|
```sh
|
|
make install # installs to /usr/local
|
|
make install PREFIX=$HOME/.local # or any prefix you prefer
|
|
```
|
|
|
|
Installs `party` to `$(PREFIX)/bin` and `party.1` to `$(PREFIX)/share/man/man1`; `man party` is the full reference. `make uninstall` removes both.
|
|
|
|
On illumos/Solaris the default prefix is `/opt/party` and the install needs root (`pfexec make install`) for the system bindir and `/etc/default/login`. The Makefile is plain POSIX make (works with `/usr/bin/make`, BSD make, GNU make).
|
|
|
|
## Quick start
|
|
|
|
One-time setup, as root. Create the group and add members:
|
|
|
|
```sh
|
|
groupadd party
|
|
usermod -aG party alice
|
|
usermod -aG party bob
|
|
# alice and bob log out and back in so the new group takes effect
|
|
```
|
|
|
|
Host a party, then invite from inside it:
|
|
|
|
```sh
|
|
party host standup # spawn a dedicated party tmux server
|
|
party invite alice # add alice to the allowlist (read/write)
|
|
party invite bob -r # invite bob as a read-only watcher
|
|
```
|
|
|
|
Guests join:
|
|
|
|
```sh
|
|
party list # see what's running on this host
|
|
party join standup # attach (auto-attaches if only one party is running)
|
|
party leave # detach and clean up
|
|
```
|
|
|
|
The `party` group is the partyline: filesystem access to party sockets is shared among members, and `tmux server-access` decides who is actually let into a given session. No shared install directory is needed: each party keeps its own state under its own per-party private directory. Parties you haven't been invited to yet show up in `party list` as `invite-only`; `party join` on one tells you which host to ask (exit status 13).
|
|
|
|
## How it works
|
|
|
|
`party host` spawns a *dedicated* tmux server for the party (separate from your personal tmux) so inviting someone exposes only the party, not your other sessions. Each party gets a private directory at `${PARTY_SOCKET_DIR}/party-${USER}:${PARTY_NAME}.d/`, holding the tmux socket and a join/leave notification helper. Guests never type the path; `party join` reads the roster and handles the attach.
|
|
|
|
Two gates protect every party, and both must pass:
|
|
|
|
- **Filesystem gate**: the per-party directory is `chgrp`'d to `TMUX_PARTY_GROUP` at mode `0750`; the socket is `g+rw`. Non-members can't traverse in or reach the socket.
|
|
- **Auth gate**: `tmux server-access` allowlist, editable only by the host. A user who clears the FS gate but isn't on the allowlist is rejected at the protocol layer.
|
|
|
|
Party metadata (host, server pid, group, creation time) lives in a `roster` file beside the socket. It's bookkeeping only, as security-relevant fields (host user, socket path) are re-derived from the directory on every read, never trusted from the file.
|
|
|
|
## Configuration
|
|
|
|
| Variable | Default | Purpose |
|
|
|---|---|---|
|
|
| `TMUX_PARTY_GROUP` | auto-selected | Shared group for socket access. When unset, `party host` auto-selects (see `PARTY_GROUP_CANDIDATES`); pin an existing group here, or pass `--group <name>` to `party host`. |
|
|
| `PARTY_GROUP_CANDIDATES` | `staff users wheel party` | Space-separated candidate list `party host` walks when `TMUX_PARTY_GROUP` is unset; gates on the first the host belongs to. |
|
|
| `PARTY_SOCKET_DIR` | `/tmp` | Where each party's private directory (socket + roster) is created. No whitespace in the path. |
|
|
| `PARTY_TMUX` | `tmux` | tmux binary to use. Override if tmux ≥ 3.3 lives at a non-standard path. |
|
|
|
|
## Support
|
|
|
|
The mechanism is identical everywhere: group ownership and mode bits plus `tmux server-access`. No ACL syscalls involved, so the trust path is one piece of code, not a per-OS matrix.
|
|
|
|
| Platform | Status |
|
|
|---|---|
|
|
| Linux | supported, validated on Debian 13 & Alpine v3.21 |
|
|
| FreeBSD UFS or ZFS | supported, validated on FreeBSD 15.0 ZFS |
|
|
| illumos LX-branded zones | supported, validated on 4.4 BrandZ linux |
|
|
| illumos native, Solaris | supported, validated on omnios-r151058 |
|
|
| NetBSD, DragonFly | unconfirmed, should work, tests welcome |
|
|
| OpenBSD | supported, validated on OpenBSD 7.8 |
|
|
| macOS | supported, validated on Darwin 25.5.0 |
|
|
|
|
## Security
|
|
|
|
`party` assumes you already know and trust everyone you add to the group; it is not a public access-control system. Both gates above protect each party, and the **auth gate** (`tmux server-access`) is authoritative, with the filesystem gate as defense in depth.
|
|
|
|
The threat model is **accidents, not adversaries**. `party` runs where everyone already shares the box, so the ownership and symlink gates in front of destructive operations (`close`'s directory removal, moderation) are there so a stale roster, a typo, or two racing `party host` invocations can't remove or hijack the wrong thing: that the same checks would also stop a malicious group member is a side effect, not the design goal. New checks should earn their place by preventing a plausible *mistake*, not by closing a hypothetical attack from a friend.
|
|
|
|
Three honest caveats, with the full detail in `man party`:
|
|
|
|
- On ACL-enabled filesystems (ZFS, HFS+/APFS), inherited ACLs can override the mode bits, so the FS gate is best-effort. The auth gate still holds.
|
|
- A party's *existence* is not hidden the way *attaching* is. That confidentiality rides on the FS gate.
|
|
- Active guests share one tmux server, where any write-capable invitee is trusted by design. Invite read-only (`-r`) if you do not trust that far.
|
|
|
|
## Status
|
|
|
|
`party` is pre-release. The on-disk layout (per-party directory, roster format, socket path) may change between commits, with no upgrade migration, so close running parties before pulling. A stale roster pointing at an unreadable socket simply won't be discovered; old tmux servers left running must be killed by hand.
|
|
|
|
## Reference
|
|
|
|
| Subcommand | Effect |
|
|
|---|---|
|
|
| `party host [name]` | Spawn a dedicated party tmux server. Only the host is on the allowlist. Names: letters, digits, `_`, `-`; max 63 chars. |
|
|
| `party invite alice [-r]` | Add alice to the allowlist. `-r` / `--read-only` invites as a watcher. |
|
|
| `party voice alice` | Promote alice to read/write. `--party <name>` selects among several hosted parties. |
|
|
| `party mute alice` | Demote alice to read-only. `--party <name>` selects among several hosted parties. |
|
|
| `party kick alice` | Revoke alice's invite, disconnect her, kill her guest session. `--party <name>` selects among several hosted parties. |
|
|
| `party detach alice` | Disconnect alice; keep her on the allowlist. `--party <name>` selects among several hosted parties. |
|
|
| `party list` | List live parties on this host. |
|
|
| `party who [--short]` | Show invited and attached users for the current party. |
|
|
| `party status` | Show the caller's own state: hosting, attached, or idle. |
|
|
| `party close` | Tear down the party server and its roster entry. Host-only. |
|
|
| `party join [name] [--passive]` | Join a party. Auto-attaches when one is running; picker otherwise. `--passive` attaches read-only to the host's view (watcher mode). Read-only invitees always join passive. |
|
|
| `party leave` | Detach and clean up the per-guest session. |
|
|
| `party knock <name>` | Ask the host of an invite-only party for an invite (write(1) ping with the exact invite command). |
|
|
| `party role [active\|passive\|switch]` | Flip your clients between guest and host session. No arg prints the current role. |
|
|
| `party --help` | Help text. |
|
|
|
|
## Credits
|
|
|
|
Written by veg and kol3rby in the context of the [UNIX Social Club](https://club.unix.rocks/). Issues and contributions welcome.
|