Tin is a small peer-to-peer communication tool for two machines on the same network. One side hosts, the other connects, and everything between them travels encrypted over a single TCP connection.
Two independent links ride that connection: audio, so one machine's microphone reaches the other's speakers, and input, so one machine's keyboard and mouse drive the other. A node sits on exactly one end of each link.
There is no server to sign up for, no account, and no discovery service. Each node owns a long-term Ed25519 identity generated on first run, and every session derives fresh keys from it. It is a tin-can telephone with a real cryptographic string.
Tin is symmetric once the connection is up. The only asymmetry is who binds the listening socket and who dials it.
host connect
------ ---------
bind + listen connect
\ /
\-------- TCP accept / connect ----------/
|
signed handshake, both ways
|
X25519 -> per-direction session keys
|
ChaCha20-Poly1305 over audio and input packets
Every packet goes on the wire the same way, whatever it carries: the payload is
encrypted under the session keys and written as [nonce][ciphertext || tag].
Because both ends know the plaintext size in advance, no length prefix is
needed.
Audio frames are captured at exactly 1920 bytes, which is 20 ms of 48 kHz 16-bit mono PCM. Each frame is wrapped in an audio packet carrying a version byte and a 32-bit sequence number. The capture side computes the RMS amplitude of every frame and drops anything at or below -50 dBFS, keeping a ten-frame hangover so word endings are not clipped. The playback side logs sequence gaps rather than trying to conceal them.
Input events are read straight from evdev and re-emitted on the far side
through a uinput virtual device. Only EV_KEY, EV_REL and EV_SYN are
forwarded; each one becomes a 13-byte input packet:
version (u8) | seq (u32) | type (u16) | code (u16) | value (i32)
Linux, plus the following development packages:
| Dependency | Used for |
|---|---|
| libsodium | Ed25519, X25519, ChaCha20-Poly1305 |
| PulseAudio | Capture and playback (libpulse-simple) |
| ncurses | Interactive source and device selection menus |
| libudev | Input device discovery |
| wayland | Reading the display size for edge switching |
| pkg-config | Locating libsodium and wayland at build time |
Debian and Ubuntu:
sudo apt install build-essential pkg-config libsodium-dev \
libpulse-dev libncurses-dev libudev-dev libwayland-devFedora:
sudo dnf install gcc make pkgconf-pkg-config libsodium-devel \
pulseaudio-libs-devel ncurses-devel systemd-devel \
wayland-develArch:
sudo pacman -S base-devel pkgconf libsodium libpulse ncurses systemd-libs waylandOptional: clang-format for the formatting targets, and lcov for coverage.
makeThat produces the tin binary in the project root and regenerates
compile_commands.json. Sources and headers are discovered recursively, so new
files under src/ and include/ are picked up without editing the Makefile.
make cleanWhatsApp.Video.2026-08-06.at.22.53.00.mp4
./tin <role> <addr[:port]> <caps> [edge]
Addresses are IPv4 literals. The default port is 6969 when none is given, and
host binds to whatever address you pass, so use 0.0.0.0 to accept a peer on
any interface.
| Role | Meaning |
|---|---|
host |
Bind and listen for one incoming peer |
connect |
Dial the given address |
Capabilities are a comma-separated list describing what this node contributes to the call.
| Capability | Meaning |
|---|---|
mic |
Capture from a local source and send it to the peer |
speaker |
Receive from the peer and play it back locally |
input-send |
Capture a local keyboard and mouse and drive the peer |
input-recv |
Replay the peer's events on a local virtual device |
A node holds at most one end of each link: mic,speaker and
input-send,input-recv are rejected as misconfigurations, while combinations
like mic,input-send are fine. The peer is expected to take the other end.
Passing mic opens an ncurses menu listing the available capture sources, and
input-send opens one menu per device class, so you pick the keyboard and mouse
to share before the call starts. Either menu can be left with q or Esc.
The optional fourth argument only means something to an input-send node. It
names the screen side that hands control over:
| Edge | Take the peer over by pushing the pointer into the ... |
|---|---|
left |
left edge of the screen |
right |
right edge of the screen |
top |
top edge of the screen |
bottom |
bottom edge of the screen |
Leave it out and there is no switching at all: every event is forwarded and acted on locally, so both machines react to the same keystroke.
Host a call and speak into a local microphone:
./tin host 0.0.0.0 micConnect from the other machine and listen:
./tin connect 192.168.1.20 speakerUse a non-default port:
./tin connect 192.168.1.20:7000 speakerShare this machine's keyboard and mouse, handing them over at the right edge:
./tin host 0.0.0.0 input-send rightLet the peer drive this machine:
./tin connect 192.168.1.20 input-recvTalk and share input over the same connection:
./tin host 0.0.0.0 mic,input-send rightStop either side with Ctrl-C. The signal handler tears the socket down so the
blocked audio and input threads wake up and exit cleanly.
Input forwarding touches device nodes that are not world-accessible by default:
| Node | Needed by | Access |
|---|---|---|
/dev/input/event* |
input-send |
read |
/dev/uinput |
input-recv |
write |
Adding your user to the input group covers the first; /dev/uinput usually
needs a udev rule or running as root. Audio-only calls need neither.
Display size is read over the Wayland protocol, so edge switching needs
XDG_SESSION_TYPE=wayland. On anything else the size stays unknown, and Tin
warns that it cannot switch on the given edge and falls back to forwarding
everything to both machines.
Tin keeps its identity in $HOME/.config/tin, creating the directory with mode
0700 on first run:
On startup the pair is loaded and validated: the public key must be derivable from the secret key, and a half-present pair is treated as an error rather than silently regenerated. Delete both files to rotate your identity.
make testEvery production object except main.o is linked into the test runner, which is
built on the vendored greatest
header.
Requires lcov:
make coverageThe HTML report lands in coverage/html/index.html.
make format # rewrite sources in place
make format-check # fail if anything is unformattedThe vendored tests/greatest.h is excluded from both.
Tin logs to syslog. To pull its entries into tin.log:
make logs