Skip to main content

Tailkitty

PyPI Python CI

Python tooling for Tailscale Tailcat: encrypted, account-free, netcat-style connections over Tailscale's data plane.

Tailkitty implements connection tokens, DNS destination lookup, and DERP-map resolution in Python. For interoperable network transport, its platform wheels contain a pinned upstream Tailcat executable whose platform, version, size, and SHA-256 digest are checked before execution.

Why use it?

  • Inspect, validate, and resolve Tailcat tokens without starting a native process.
  • Use typed synchronous and asyncio clients instead of assembling subprocess commands.
  • Manage server startup, readiness, timeouts, and cleanup safely from Python.
  • Keep upstream CLI compatibility for streaming, forwarding, files, UDP, ping, SOCKS, SSH, and key commands.
  • Install a self-contained platform wheel with runtime bundle-integrity checks.
  • Reproduce releases with pinned Python, Go, uv, Tailcat, and cross-platform build inputs.

Contents

Install

Install a bundled wheel

For the command-line tool:

uv tool install tailkitty
tailkitty doctor

For library use inside a project:

uv add tailkitty

To build and install the host wheel from this checkout instead:

mise install
uv sync --all-groups --locked
mise run wheel
uv tool install --force ./dist/tailkitty-0.2.1-py3-none-<platform>.whl

A bundled wheel does not require Go at runtime. It provides the tailkitty command plus tailcat as an upstream-compatible alias. See the platform matrix for tags.

Work from this checkout

Install mise, then run:

mise install
mise run setup
uv run tailkitty doctor

This installs Python 3.13.11, Go 1.27.1, and uv 0.12.7; synchronizes the uv environment; and builds the pinned development backend at .tools/bin/tailcat.

Use only the Python functionality

Token parsing, token resolution, DERP caching, and DNS destination lookup are pure Python. They can be used from a binary-free source installation. Network commands and Client/ServerProcess still require either a bundled wheel or an executable selected by TAILKITTY_BACKEND.

Quickstart

The following example creates an ephemeral, one-shot byte stream between two machines. Both need the tailkitty command from a bundled wheel or configured backend.

On the receiving machine:

tailkitty --key=new < /dev/null
# 🐈 Server listening with new address: tc...

Copy the complete tc... address to the sending machine:

printf 'hello from tailcat\n' | tailkitty --key=new 'tc...'

The message appears on the receiving terminal. This default pipe mode accepts one finite stream and exits. Use serve, forward, or a Python Client connection for long-lived bidirectional traffic.

The PowerShell equivalents are:

$null | tailkitty --key=new
'hello from tailcat' | tailkitty --key=new 'tc...'

CLI

Three commands are implemented natively in Python:

tailkitty parse 'tc...'             # decode and validate a token as JSON
tailkitty resolve 'tc...'           # embed the referenced DERP region
tailkitty doctor                    # show the selected backend and provenance
tailkitty doctor --json             # machine-readable diagnostics

Every other argument sequence is passed unchanged to the upstream-compatible data plane:

tailkitty serve 8080,8443
tailkitty 'tc...' 8080
tailkitty ping 'tc...'
tailkitty ssh 'tc...'
tailkitty socks 'tc...' curl http://server.tailcat:8080/
tailkitty forward 'tc...' 18080:8080
tailkitty serve --files=/srv/share:rw files
tailkitty cp ./report.pdf 'tc...':
tailkitty serve --ssh-authorized-keys=alice@github ssh
tailkitty genkey --client

Run tailkitty --help for the Python command summary. Upstream commands retain their own help, for example tailkitty genkey --help.

Destinations may be literal connection tokens or DNS names with a TXT record of the form:

server.example.com. 300 IN TXT "tailcat=tc..."

DNS records are public, so publishing an address removes its normal secrecy. A DNS-named server must authenticate clients independently with --allow, SSH authorized keys, or both. Never put an unrestricted service—especially no-auth-ssh—behind a public tailcat= record.

Python API

Inspect tokens without the native backend

from tailkitty import parse_token, resolve_token

info = parse_token("tc...")
print(info.server_public.hex())
print(info.region_id)

# Fetch and embed the referenced DERP region. Cached maps are revalidated with ETags.
self_contained_token = resolve_token("tc...")

Malformed tokens raise TokenError. DNS lookup raises DestinationError, and DERP-map failures raise DerpMapError or are translated to TokenError by resolve_token(). Unknown CBOR fields are retained in the extensions mapping on ConnInfo, DerpRegion, and DerpNode, then preserved by to_token(); this prevents Python resolution from silently stripping fields introduced by a newer Tailcat.

Send a finite request

import subprocess

from tailkitty import Client

client = Client("tc...")  # A DNS name with a tailcat= TXT record also works.

# request() checks the exit status and returns stdout bytes.
response = client.request(b"GET / HTTP/1.0\r\n\r\n", port=8080, timeout=30)

# run() preserves status, stdout, and stderr; checking is opt-in.
result = client.run(b"hello", timeout=30, check=False)
if result.returncode:
    raise subprocess.CalledProcessError(
        result.returncode, result.args, result.stdout, result.stderr
    )

Use Client.connect() when a long-lived, full-duplex subprocess.Popen stream is needed. DNS results are cached on each client; call client.refresh() to resolve the destination again.

Manage a server

from tailkitty import ServerProcess

with ServerProcess(serve=[8080, 8443], key="new", allow=["nodekey:..."]) as server:
    print(f"share this address with the allowed client: {server.token}")
    print(f"native process id: {server.process.pid}")
    # The context remains active while the remote client uses the forwarded ports.

ServerProcess.start() waits up to 20 seconds by default for a validated connection token. The context manager terminates the server and escalates to a kill if it does not stop within its grace period. allow=[] means --allow=none; allow=None preserves upstream's allow-all default.

Use asyncio

import asyncio

from tailkitty import AsyncClient, AsyncServerProcess


async def main() -> None:
    response = await AsyncClient("tc...").request(b"hello", timeout=30)
    print(response)

    async with AsyncServerProcess(serve=8080, key="new", allow=[]) as server:
        print(server.token)  # Starts successfully, but rejects all clients.


asyncio.run(main())

Cancellation and timeout paths kill and reap their native child process before propagating the exception.

API behavior at a glance

API Backend needed? Result
parse_token(token) No Typed ConnInfo
resolve_token(token) No Self-contained token string
resolve_destination(name) No Validated token string
Client.request(data, ...) Yes Response bytes; raises on non-zero exit
Client.run(data, ...) Yes subprocess.CompletedProcess[bytes]
Client.connect(...) Yes Streaming subprocess.Popen[bytes]
ServerProcess(...) Yes Managed synchronous server
AsyncClient / AsyncServerProcess Yes Asyncio equivalents
run(arguments, ...) / run_async(arguments, ...) Yes Low-level upstream command execution

The package is marked with py.typed and is checked with strict mypy.

How it works

Tailcat's connection-token format is CBOR encoded as unpadded base64url after a tc prefix. That wire format and the lightweight control-plane operations are implemented in Python. The encrypted transport remains upstream because it depends on Tailscale's Go implementations of magicsock, userspace WireGuard, DERP routing, and gVisor netstack.

Python caller / CLI
        |
        +-- token.py -------- CBOR token codec (pure Python)
        +-- destination.py -- token or DNS TXT resolution (pure Python)
        +-- derp.py --------- DERP-map cache and HTTP revalidation (pure Python)
        +-- client.py ------- typed sync/async client facade
        +-- process.py ------ managed server and subprocess lifecycle
        |
        +-- backend.py ------ backend discovery and command execution
                |
                +-- verified wheel bundle, development build, or explicit executable

Backend discovery is deterministic and fail-closed:

  1. Executable named by TAILKITTY_BACKEND; an invalid path is an error.
  2. Platform-compatible bundled executable with a valid integrity manifest.
  3. Development executable at .tools/bin/tailcat (then the legacy tailcat-go filename).
  4. A tailcat-go executable on PATH.

If a bundled executable is present but fails validation, Tailkitty reports an integrity error instead of silently selecting another backend. tailkitty doctor --json shows the selected source, target, upstream revision, compiler, size, and digest.

DERP maps use a one-hour disk cache by default, ETag revalidation, a 5 MiB response limit, atomic cache writes, and stale-cache fallback when a refresh fails.

Security

  • Tailcat traffic uses upstream's encrypted Tailscale data plane, but authorization is a separate choice: omitting --allow allows every client that can reach the server.
  • A connection token contains routing information and a server public key, not the server's private key. Current addresses also contain a separate discovery public key and a WireGuard pre-shared key. Treat the complete address as sensitive capability material and avoid publishing it.
  • Use tailkitty genkey --client, then pass its public key through --allow or allow=[...] for restricted access. Passing an empty Python list denies every client.
  • TAILKITTY_BACKEND is an explicit code-execution override. Tailkitty verifies that it is executable but cannot prove the provenance of a user-selected file.
  • Bundled executables are checked against their manifest for schema, upstream module and revision, runtime platform, filename safety, symlinks, size, and SHA-256 before use.
  • Do not include active addresses, saved private keys, or verbose networking logs in public bug reports without reviewing them first.

Read SECURITY.md for the bundle trust model and reporting guidance.

Compatibility and limitations

Supported platform wheels

Operating system Architecture Wheel platform tag
macOS 12 or newer arm64 macosx_12_0_arm64
macOS 12 or newer x86-64 macosx_12_0_x86_64
Linux, glibc 2.17 or newer x86-64 manylinux_2_17_x86_64
Linux, glibc 2.17 or newer arm64 manylinux_2_17_aarch64
Windows x86-64 win_amd64

Python 3.11 and newer is supported. Wheels use the py3-none-<platform> tag because the Python modules are not tied to a CPython ABI; the embedded executable is still platform-specific. The bundled data plane is Tailcat v0.6.0.

Current limitations:

  • There is no pure-Python network data plane. A binary-free install supports control-plane APIs only.
  • Public DERP relays are rate-limited external infrastructure with no uptime guarantee. A relay timeout does not necessarily indicate a local packaging or token error.
  • Wheel smoke tests also run an isolated local DERP/STUN relay and require a real encrypted peer handshake to complete within five seconds, so public-relay availability cannot hide a broken bundled data plane.
  • Upstream Tailcat is experimental and its token and CLI interfaces can change.
  • Only the five targets above are built. Other platforms may use an explicitly supplied compatible backend, but are not release-tested here.

See COMPARISON.md for a feature-by-feature comparison with the existing PyPI project.

Development

The normal contributor loop is:

mise install
uv sync --all-groups --locked
mise run test

Useful build and verification commands:

mise run backend          # development helper in .tools/bin
mise run bundle           # host helper in src/tailkitty/bin
mise run bundle-verify    # verify the host bundle manifest
mise run wheel            # build the host platform wheel
mise run wheels           # cross-build and verify all five wheels
mise run upstream-check   # compare the immutable pin with the latest stable Tailcat release

# Install and inspect a newly built host wheel in isolation:
uv run python -m scripts.smoke_wheel dist/wheels/<host-wheel>.whl

# Exercise the minimum supported Python version:
uv run --isolated --python 3.11 --all-groups pytest

Generated executables, manifests, wheel files, and source archives are build artifacts; do not edit them manually. The release pin is defined in src/tailkitty/constants.py, and Go must match the version in .mise.toml exactly.

Additional documentation:

Coding agents and automated contributors must also follow AGENTS.md.

License and relationship to Tailscale

This project is licensed under the MIT License. Bundled wheels contain upstream Tailcat and its Go dependencies; see THIRD_PARTY_NOTICES.md.

Tailcat and Tailscale are trademarks of Tailscale Inc. This project is not an official Tailscale product.

Metadata

Release files for tailkitty 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tailkitty 0.2.1
File Size Uploaded Provenance Actions
tailkitty-0.2.1.tar.gz 94.2 kB Without provenance Details for tailkitty-0.2.1.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for tailkitty 0.2.1
File Actions
tailkitty-0.2.1-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for tailkitty-0.2.1-py3-none-win_amd64.whl
tailkitty-0.2.1-py3-none-manylinux_2_17_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details for tailkitty-0.2.1-py3-none-manylinux_2_17_x86_64.whl
tailkitty-0.2.1-py3-none-manylinux_2_17_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details for tailkitty-0.2.1-py3-none-manylinux_2_17_aarch64.whl
tailkitty-0.2.1-py3-none-macosx_12_0_x86_64.whl Python 3 none macOS 12.0+ x86-64 Details for tailkitty-0.2.1-py3-none-macosx_12_0_x86_64.whl
tailkitty-0.2.1-py3-none-macosx_12_0_arm64.whl Python 3 none macOS 12.0+ ARM64 Details for tailkitty-0.2.1-py3-none-macosx_12_0_arm64.whl

Total release size: 34.2 MB

Release files / tailkitty-0.2.1.tar.gz

Download URL tailkitty-0.2.1.tar.gz
Size 94.2 kB
Tags Source
SHA-256 checksum
How to use checksums
c790b09c064d12ae3d1772dd491fb68e36edbb49ff40886d89b07a7ee93f897f
BLAKE2b-256 checksum
How to use checksums
2be7f271d9ce11d622eb99f6f7c18d1cc4e0b86572aebfe92e3cb98403139573
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / tailkitty-0.2.1-py3-none-win_amd64.whl

Download URL tailkitty-0.2.1-py3-none-win_amd64.whl
Size 7.1 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
1ede57828c7d18c6d900627390bb84caf039540f3fea878d6c1cb3c91db20c24
BLAKE2b-256 checksum
How to use checksums
1bd767bafac228f3cd19cafb62a5d256df8671baf17e723af9cd2c1b506746da
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / tailkitty-0.2.1-py3-none-manylinux_2_17_x86_64.whl

Download URL tailkitty-0.2.1-py3-none-manylinux_2_17_x86_64.whl
Size 7.1 MB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
2aac64f527457ab885c261720740ae848c64e80a12079d3185d74ae6c68c11c0
BLAKE2b-256 checksum
How to use checksums
afeff9cb5750b7e680b326c6a3f71968f76a116c1e3fe5358c87e0bb23a31182
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / tailkitty-0.2.1-py3-none-manylinux_2_17_aarch64.whl

Download URL tailkitty-0.2.1-py3-none-manylinux_2_17_aarch64.whl
Size 6.4 MB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
9a8e55a89b22c611a78fcd586596295ee1a7803083fd8032fab71f48a90d7e51
BLAKE2b-256 checksum
How to use checksums
c464ab6c1c85ecdc9aef0240113d7f3e6d17d6156fe31e4e540d55c1f689eb3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / tailkitty-0.2.1-py3-none-macosx_12_0_x86_64.whl

Download URL tailkitty-0.2.1-py3-none-macosx_12_0_x86_64.whl
Size 7.0 MB
Tags Python 3 macOS 12.0+ x86-64
SHA-256 checksum
How to use checksums
e139f3bdc27e24e291c96503f51787b1d24b6b7bc750ab6c5fd0165172507edd
BLAKE2b-256 checksum
How to use checksums
c608bcb23eb840be4abb4545f60dcbf74b28b6b29bcf0b5aafce7c71d5e59262
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / tailkitty-0.2.1-py3-none-macosx_12_0_arm64.whl

Download URL tailkitty-0.2.1-py3-none-macosx_12_0_arm64.whl
Size 6.5 MB
Tags Python 3 macOS 12.0+ ARM64
SHA-256 checksum
How to use checksums
ae64a8808dfddd92d04349474e19089518f8ddc5086713b903df352978e4e991
BLAKE2b-256 checksum
How to use checksums
ca4ab80f8eee8159e763cbb65cb4807ead80a27d9d1ddcd27ded738f5d48838d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Security for tailkitty 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page