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.
Warning
Both upstream Tailcat and Tailkitty are experimental. Do not depend on stable token, CLI, or API compatibility before a stable release.
- 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.
For the command-line tool:
uv tool install tailkitty
tailkitty doctorFor library use inside a project:
uv add tailkittyTo 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.0-py3-none-<platform>.whlA 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.
Install mise, then run:
mise install
mise run setup
uv run tailkitty doctorThis 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.
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.
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...'Important
A server allows any client by default. For controlled access, generate a client key with
tailkitty genkey --client and start the server with --allow=<client-public-key>. See
Security before exposing a service.
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 diagnosticsEvery 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 --clientRun 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..."
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().
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.
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.
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 | 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.
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.
- 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.
| 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.
The normal contributor loop is:
mise install
uv sync --all-groups --locked
mise run testUseful 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
# 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 pytestGenerated 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.
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.