A simple file transfer "wormhole" using GTK4 and tailcat by Tailscale (main-branch-only mirror of https://forge.ejuarezg.com/ejuarezg/tailca

Files
README.md

Wormhole #

A native GTK 4 / libadwaita desktop app for secure file sharing with Tailcat and Sendme, written in Clojure with Babashka's FFI.

Wormhole calls GTK, GLib, and libadwaita directly through babashka.ffi; there is no Java GUI toolkit or GTK wrapper. Tailcat supplies userspace WireGuard encryption, NAT traversal, and DERP fallback. No Tailscale account, administrator access, or network configuration is needed.

What it does #

Two views offer different workflows. They can run independently, including at the same time; changing views does not stop transfers. File Sharing opens by default. The header's view switcher moves to the bottom in narrow windows.

The interface uses standard Adwaita controls and system light/dark appearance, with bounded reading widths, scrollable forms, copy notifications, and collapsible activity details. Closing the window asks for confirmation if a transfer, sharing session, or drop box is active. See UI design for the design rationale and test checklist.

Drop Box · Tailcat #

  • Receive: starts tailcat recv --key=new --accept-dirs against a chosen folder and presents the resulting private capability address.
  • Send: sends one file or a recursive folder with tailcat cp. Folder uploads use a unique destination name by default so repeats cannot collide. The sender can turn that option off to preserve the original folder name after confirming it does not already exist in the drop box.
  • Keeps process arguments separate (no shell interpolation), displays Tailcat's activity, and lets you stop either operation.
  • Watches the receive directory and reports new folders and files once their sizes settle, since the Tailcat receiver CLI does not emit per-file events.
  • Always requests a fresh ephemeral receive key, even if the machine has a saved Tailcat default key.

The receive side is a recursive write-only drop box: senders cannot list the destination, read its contents, or overwrite an existing file. Supporting folder uploads lets senders create and stat directories, so they may infer whether a requested directory name already exists (Tailcat's documented --accept-dirs trade-off).

File Sharing · Sendme #

  • Share: choose a file or folder and start sharing. Copy the generated ticket and give it privately to recipients. Keep sharing until they finish; one completed download does not stop the provider.
  • Download: paste a blob… ticket or the full sendme receive blob… command, choose a destination, and download. The command is parsed as data, never executed through a shell. Tailcat addresses do not work here.
  • Uses Iroh's encrypted QUIC/TLS connections, NAT traversal, and relay fallback, with BLAKE3 content verification from iroh-blobs.
  • Stop sharing or cancel downloads independently. Interrupted downloads keep Sendme's .sendme-recv-* data in the destination; retry the same ticket in the same folder to resume, while the provider is still available. You can remove these hidden directories yourself when abandoning a download.
  • Files retain their source names. Unlike Tailcat mode, this is not a write-only inbox and there is no unique-folder naming option. Use a separate destination folder if existing names conflict.
  • Sharing uses a private temporary working directory, cleaned after the process exits. Downloads use the chosen destination, not Wormhole's working directory. Tickets are omitted from Sendme's activity log.

Confirming closure stops both backends; Sendme gets a graceful interrupt before forced termination if it does not exit within five seconds. SIGKILL or a crash can leave temporary files behind. Stopping sharing cannot revoke copies that recipients already downloaded. Anyone with a ticket can retrieve the shared content while it is available.

Requirements #

The Flatpak and Guix/AppImage packages include both transfer backends. No separate Sendme or Tailcat installation is needed for those builds.

For running from a source checkout:

  • Linux or macOS with GTK 4 and libadwaita 1.4+ installed
  • Babashka 1.13.220 or newer, using an FFI-capable dynamically linked build
  • Tailcat and OpenSSH scp for drop-box mode
  • Sendme for ticket mode (optional; tested with Sendme 0.37.1)

For source checkouts, only the backend for the mode you use is required. A missing backend disables that view's start buttons without preventing the other mode from working.

For example, Tailcat can be installed from source with Go:

go install github.com/tailscale/tailcat/cmd/tailcat@latest

If tailcat is not on PATH, point the app to it:

export TAILCAT_BIN="$HOME/go/bin/tailcat"

To enable Sendme mode:

cargo install sendme
# If it is not on PATH:
export SENDME_BIN="$HOME/.cargo/bin/sendme"

Restart Wormhole after installing a backend or changing its executable path. If SENDME_BIN points to a wrapper script, use exec sendme "$@" rather than starting a detached/background provider, so Wormhole can own its lifetime. Packaged builds use Sendme 0.37.1 and Tailcat 0.6.0, with verified source hashes and Sendme license notices. Flatpak installs both in /app/bin; the Guix/AppImage launcher pins their store paths. See flatpak/README.md for build and verification commands.

Run #

bb run

The window deliberately calls the private address a secret. A Tailcat address contains the pre-shared key needed to connect, so exchange it through a private channel. It becomes unusable when the ephemeral receive process stops.

OCaml port #

An OCaml/ocgtk implementation lives in ocaml/. Its local Guix and Opam setup, launch command, and test commands are in ocaml/README.md. It is developed independently of the Babashka implementation above. Sendme mode is currently implemented only in the Babashka/Clojure app. The libadwaita redesign is also limited to that app; the OCaml port retains its existing GTK interface.

Install and package on GNU Guix #

The Guix package bundles GTK 4, libadwaita, an FFI-capable Babashka 1.13.220, Tailcat 0.6.0, Sendme 0.37.1, OpenSSH scp, and Mozilla's CA certificates. Install it into your current Guix profile with:

guix time-machine -C packaging/channels.scm -- \
  package --install-from-file=guix.scm
wormhole

Build a single-file AppImage instead with:

bb appimage
./dist/Tailcat-Wormhole-x86_64.AppImage

The AppImage is relocatable and can run on a Linux machine without Guix. It uses FUSE when available; otherwise use:

./dist/Tailcat-Wormhole-x86_64.AppImage --appimage-extract-and-run

AppImage limitations #

  • It is currently x86-64 Linux only, because the pinned Babashka, Tailcat, and Sendme binary releases are x86-64 Linux builds.
  • It contains the complete Guix GTK/libadwaita dependency closure rather than relying on host libraries, so the resulting image is large. Rebuild older images to include Sendme and the new interface.
  • It needs FUSE to mount normally. The extraction command above is the fallback; on hosts where Guix's user-namespace execution engine is unavailable, set GUIX_EXECUTION_ENGINE=proot (with slower startup).
  • Its bundled scp ignores system-wide and user SSH configuration. This is intentional: Tailcat supplies the proxy command for every transfer, and it avoids OpenSSH rejecting host config files under the AppImage user namespace.
  • The generated AppImage desktop metadata is still generic and does not provide a .DirIcon; this is cosmetic but may affect how some desktop environments display it.

Flatpak Installation #

Wormhole can be built as a Flatpak for easy distribution on Linux.

Build from source #

./flatpak/build.sh

Run #

flatpak run com.ejuarezg.wormhole

See flatpak/README.md for build, validation, and permission details.

Development #

bb test
bb check
bb test-sendme # Optional real file/folder loopback transfers; requires Sendme
bb test-gtk    # Adaptive views and close/SIGTERM cleanup; use an isolated display
bb package    # Build the Guix package
bb appimage   # Build the standalone AppImage

The code is split into:

License #

Wormhole is available under the GNU Affero General Public License v3.0. The AppImage bundles separately licensed Babashka, Tailcat, and Sendme executables and includes their license notices in the package.

Security notes #

In drop-box mode, transfers are end-to-end encrypted by Tailcat's WireGuard data plane. A public DERP relay can see connection metadata and relay encrypted packets, but not file contents. Tailcat attempts a direct UDP peer-to-peer path and uses DERP as the fallback. See Tailcat's own SECURITY.md for its complete threat model and stability policy.

In ticket mode, Sendme uses Iroh's QUIC/TLS connections and BLAKE3-verified content streaming. A ticket identifies content and its provider; it is not a Tailcat pre-shared secret. Treat it as private download access. Relays can see connection metadata but not plaintext transfer contents.

Wormhole delegates networking to the selected backend and does not invoke a shell for file names, addresses, or tickets.

Development disclosure #

This project has been developed with substantial assistance from large language models (LLMs), a form of probabilistic automation. Such tools can produce plausible but incorrect code or explanations; their output does not establish that behavior is correct. The human maintainer is responsible for reviewing changes and validating them with tests.

This wording follows GNOME's “Probabilistically Automated” label and its focus on describing how work was produced without treating a model as a human-like agent. For more on precise, non-anthropomorphic language, see “We Need to Talk About How We Talk About ‘AI’”.