Tailkitty
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:
- Executable named by
TAILKITTY_BACKEND; an invalid path is an error. - Platform-compatible bundled executable with a valid integrity manifest.
- Development executable at
.tools/bin/tailcat(then the legacytailcat-gofilename). - A
tailcat-goexecutable onPATH.
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
--allowallows 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--alloworallow=[...]for restricted access. Passing an empty Python list denies every client. TAILKITTY_BACKENDis 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:
- BUILDING.md — reproducible binary and wheel pipeline
- SECURITY.md — integrity checks and trust boundaries
- COMPARISON.md — differences from the existing PyPI package
- THIRD_PARTY_NOTICES.md — bundled upstream licensing
- CHANGELOG.md — release history
- ITERATIONS.md — the initial 100-pass implementation audit
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)
| 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)
| File | Actions Reset | |||
|---|---|---|---|---|
| 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.