Tailcat
Tailcat is een remix van open-source onderdelen van Tailscale. Het functioneert als netcat, maar werkt over het dataplane van Tailscale, zonder gebruik te maken van het control plane van Tailscale.
Het dataplane van Tailscale (intern bekend als magicsock) biedt point-to-point WireGuard®-versleutelde tunnels tussen twee machines. Hierbij wordt DERP gebruikt als communicatiekanaal voor NAT-hole-punching en als ultieme relay-oplossing als NAT-traversal faalt. In plaats van het control plane van Tailscale te gebruiken, worden alle metadata voor de verbinding van Tailcat "out of band" uitgewisseld, op een manier naar keuze van de gebruiker.
De Tailcat CLI (te vinden in cmd/tailcat) is gebouwd op de Tailcat Go-bibliotheek (te importeren via github.com/tailscale/tailcat).
Of u nu de CLI-tool of de bibliotheek gebruikt: één zijde draait een Tailcat-server (listener) en ontvangt een kort verbindingstoken. De andere zijde geeft dit token door aan de Tailcat-client om verbinding te maken. Al het verkeer tussen beide partijen is end-to-end versleuteld met WireGuard. De initiële verbinding start via een DERP-server, waarna magicsock NAT-traversal uitvoert om, indien mogelijk, te upgraden naar een directe peer-to-peer UDP-verbinding.
U heeft geen Tailscale-account nodig en geen root/admin-rechten op de machine, aangezien Tailcat de routingtabellen of DNS van uw machine niet wijzigt. Het is simpelweg een userspace-bibliotheek en CLI-tool.
Het project is volledig open source. U kunt gebruikmaken van de gratis (rate-limited) DERP-relays (de standaard DERP-map is beschikbaar via https://tailcat.dev/derpmap.json) of uw eigen relay draaien.
Installatie
Installeer via Go:
$ go install github.com/tailscale/tailcat/cmd/tailcat@latest
Of met Nix flakes, door het direct uit te voeren of te installeren:
$ nix run github:tailscale/tailcat
$ nix profile install github:tailscale/tailcat
Gebruik
Stdin/stdout pijpen tussen twee machines
De server start en print het tijdelijke adres:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
(wachten...)
De client kan vervolgens het volgende doen:
$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
De server blokkeert daarna niet meer en toont de output:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
hello
Lokale poorten exposen via de tunnel
U kunt een lokale TCP-poort serveren en doorsturen naar localhost:
$ tailcat --serve=8080,8443 # of --serve=all
# 🐈 Server listening with new address: tcXXXXXXXXX
De client verbindt dan als volgt:
$ tailcat tcXXXXXXXXX 8080
GET / HTTP/1.1
Host: foo
HTTP/1.1 200 OK
....
Authenticatie-vrije SSH-server
Op Linux en macOS kunt u een SSH-server draaien zonder authenticatie. (Wilt u wel authenticatie? Gebruik dan tailcat --serve=22 en proxy naar uw systeem-SSH-server).
$ tailcat --serve=no-auth-ssh
# 🐈 Server listening with new address: tcXXXXXXXXX
Aan de clientzijde:
$ tailcat ssh tcXXXXXXXXX
$ tailcat ssh tcXXXXXXXXX ls -la
Overige commando's
- Ping: Test de connectiviteit. Elke 'pong' meldt of deze via een DERP-relay of een direct pad is gearriveerd. Met
--until-directblijft Tailcat pingen (tot aan de--timeout, standaard 10s) totdat een direct pad werkt.
``bash $ tailcat ping --until-direct <token> pong in 42.1ms via DERP(sfo) pong in 1.2ms via 203.0.113.7:41641 ``
- SOCKS5 Proxy: Voer een commando uit via een SOCKS5-proxy die over de tunnel is gerouteerd.
``bash $ tailcat socks <token> curl http://server.tailcat:8081/ ` Tokens werken ook direct als URL-hostnamen (de SOCKS-proxy herkent en belt ze op), waardoor het token-argument optioneel is. Let op: tokens zijn hoofdlettergevoelig; dit werkt met curl en de meeste CLI-tools, maar niet met browsers (die hostnamen omzetten naar kleine letters). `bash $ tailcat socks curl http://<token>:8081/ ``
- Exit Node: Fungeer als exit node zodat de client het netwerk van de server kan bereiken:
``bash $ tailcat --serve=exit-node ``
- Parse: Analyseer een verbindingstoken en print de inhoud (de publieke WireGuard-sleutel van de server en DERP-info) als JSON, zonder verbinding te maken:
``bash $ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu { "ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34", "RegionID": 302 } ``
- Resolve: Zet een kort token (dat verwijst naar een DERP-regio via ID) om in een langer, zelfvoorzienend token met ingebedde DERP-serverinformatie.
``bash $ tailcat resolve tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8UdKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA ` Het parsen van dit opgeloste token toont de ingebedde DERP-info: `bash $ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8UdKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA { "ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34", "Region": [ { "Nodes": [ { "HostName": "tc302a.ipn.dev", "IPv4": "208.111.39.38", "IPv6": "2607:f740:0:3f::720" } ] } ] } ` Een server kan de lange, zelfvoorzienende vorm direct printen met de vlag --full-address`.
Sleutelbeheer
Het adres (verbindingstoken) van een server is afgeleid van de WireGuard-sleutel. De gebruikte sleutel bepaalt wie u kan bereiken:
- Ephemeral keys (standaard): Elke serverrun genereert een nieuwe sleutel in het geheugen en print een adres dat nog nooit eerder is gezien. Wanneer het proces stopt, wordt de sleutel verwijderd en is het adres definitief dood. Dit is de veilige standaardinstelling.
- Saved keys:
tailcat genkeygenereert een sleutel die op disk wordt opgeslagen, zodat het adres stabiel blijft na restarts. Keerzijde: iedereen met wie u dit adres heeft gedeeld, kan verbinding maken met toekomstige servers die deze sleutel gebruiken, tenzij u clients beperkt met--allow(zietailcat genkey --client).
Bij het opstarten geeft de CLI aan welk type sleutel wordt gebruikt.
$ tailcat genkey --region=nyc
# print het token; sleutel wordt opgeslagen in ~/.config/tailcat/keys/default.private.json
# De sleutel genaamd "default" wordt automatisch gebruikt zodra deze bestaat:
$ tailcat --serve=8080
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
# Tenzij u expliciet een nieuwe tijdelijke sleutel forceert:
$ tailcat --serve=8080 --key=new
# 🐈 Server listening with new address: tcXXXXXXXXX
default is een magische sleutelnaam. Als deze bestaat, gebruikt tailcat deze stilzwijgend. Gebruik --key=new voor een tijdelijke sleutel, --key=<naam> voor een andere opgeslagen sleutel, of tailcat genkey --delete --key=default om de standaard sleutel te verwijderen. Met tailcat genkey --list krijgt u een overzicht van uw opgeslagen sleutels.
Tokens kunnen ook worden gepubliceerd als DNS TXT-records en via naam worden opgezocht:
# Als example.com een TXT-record heeft: "tailcat=tc..."
$ tailcat example.com 8080
$ tailcat ssh example.com
$ tailcat ping example.com
Voorbeelden
Beveiligde SSH-server via DNS
Deze methode vervangt port forwarding of port knocking. De SSH-server is overal via naam bereikbaar, maar er staan geen open inkomende poorten op de server; WireGuard authenticeert de client voordat de SSH-server überhaupt een pakket ziet.
- Client: Genereer een client-identiteit sleutelpaar.
``bash client$ tailcat genkey --client # schrijft bestand naar ~/.config/tailcat/keys/client-default.private.json nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16 ``
- Server: Genereer een server-sleutelpaar gepind aan de dichtstbijzijnde DERP-regio, en serveer SSH alleen aan die specifieke client:
```bash server$ tailcat genkey --fixed-region # schrijft bestand naar ~/.config/tailcat/keys/default.private.json tcXXXXXXXXX
server$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16 # 🐈 Server listening with saved key "default": tcXXXXXXXXX ```
- DNS: Publiceer het token in DNS als een TXT-record:
my-server.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX"
- Client: Maak verbinding:
``bash client$ tailcat ssh my-server.example.com ` Client-modi gebruiken automatisch de opgeslagen client-default` sleutel. Handshakes van anderen worden stilzwijgend genegeerd.
Waarom --fixed-region? Het ontdekt eenmaal bij genkey de dichtstbijzijnde DERP-regio en bakt dit ID in het token en het sleutelbestand. Hierdoor binden server-restarts aan dezelfde regio zonder opnieuw te hoeven zoeken, wat essentieel is voor tokens die in DNS zijn gepubliceerd.
Eigen DERP-relay gebruiken
U kunt uw eigen DERP-server draaien (vereist een hostname met TLS-certificaat). Genereer een server-sleutel die deze gebruikt door de hostname als regio door te geven:
server$ tailcat genkey --region=derp.example.com
tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
server$ tailcat --serve=22
Het token bevat nu uw eigen relay-hostname, waardoor clients geen contact hoeven te hebben met de DERP-map of relays van Tailscale. Alternatief kunt u een eigen DERP-map JSON serveren en beide zijden hiernaar laten verwijzen met --derpmap-url.
Go-bibliotheek
Minimale Server
Een server die elke TCP-poort via de tunnel beantwoordt en het token print:
package main
import (
"fmt"
"log"
"net"
"github.com/tailscale/tailcat"
)
func main() {
s := &tailcat.Server{
OnTCP: func(port uint16) func(net.Conn) {
return func(c net.Conn) {
fmt.Fprintf(c, "hello from port %v\n", port)
c.Close()
}
},
}
if err := s.Start(); err != nil {
log.Fatal(err)
}
fmt.Println(s.ConnBlob())
select {}
}
Minimale Client
Een client die verbinding maakt met de server aan de hand van het token:
package main
import (
"context"
"io"
"log"
"os"
"github.com/tailscale/tailcat"
)
func main() {
cl := tailcat.NewClient(tailcat.ConnBlob(os.Args[1]))
defer cl.Close()
c, err := cl.DialTCPPort(context.Background(), 80)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, c)
}
Uitvoering: $ ./client tcomFwWCAWf933BLELdzd3RkHiOufJ... → hello from port 80
Hoe het werkt
Verbindingstokens
Een Tailcat-server wordt geïdentificeerd door een verbindingstoken (ConnBlob). Dit begint met tc gevolgd door base64-encoded CBOR dat bevat:
- De publieke WireGuard-sleutel van de server (Curve25519, 32 bytes).
- DERP-informatie:
- Een klein geheel getal (referentie naar een standaard Tailscale-server).
- Of volledige DERP-server metadata (voor custom servers of om round-trips naar de DERP-map te vermijden).
Netwerkstack
Tailcat hergebruikt de netwerkcomponenten van Tailscale, maar zonder het control plane:
- WireGuard: Een userspace-implementatie voor versleuteling. Gebruikt geen kernel TUN/TAP-device, waardoor root-rechten niet nodig zijn.
- magicsock: De transportlaag die verkeer multiplext over direct UDP en DERP-relays. Regelt STUN-based endpoint discovery en UDP hole-punching.
- Netstack (gVisor): Een userspace TCP/IP-stack die TCP-verbindingen binnen het proces beëindigt. Hiermee kan Tailcat verbindingen accepteren en maken zonder OS-netwerkconfiguratie.
- DERP relay: Versleuteld relay-protocol, gebruikt als rendezvous-kanaal en fallback-pad.
Verbindingsflow
- Server start: Genereert/laadt een WireGuard-sleutelpaar, verbindt met een DERP-relay en print het token.
- Client: Parst het token, genereert een eigen tijdelijk sleutelpaar en verbindt met dezelfde DERP-relay.
- Discovery handshake: De client stuurt een "Meow" ping-bericht via DERP. De server voegt de client toe aan zijn WireGuard peer-lijst en antwoordt met een "Meowed" bevestiging.
- WireGuard tunnel: De standaard WireGuard handshake vindt plaats (initieel via DERP). Zodra deze voltooid is, is de versleutelde tunnel actief.
- NAT traversal: Beide zijden adverteren hun UDP-endpoints via DERP. Er wordt gepoogd UDP hole-punching uit te voeren. Bij succes upgrade het verkeer van DERP naar een direct peer-to-peer pad.
- Dataoverdracht: De client opent een TCP-poort op de server via de tunnel. gVisor handelt de TCP/IP-stack af. De server stuurt de verbinding door naar de juiste handler (localhost, stdout, SSH, etc.).
Adressering
Elke peer leidt momenteel een deterministisch IPv6-adres af van zijn publieke WireGuard-sleutel. Dit is een implementatiedetail en is niet zichtbaar voor eindgebruikers.
Stabiliteit
Tailcat is gratis te gebruiken, maar er zijn geen stabiliteitsbeloften voor de API of CLI. De Go API, CLI-vlaggen, output en het wire-formaat kunnen veranderen. De publieke rate-limited DERP-relays hebben geen uptime-SLA's of throughput-doelstellingen; toegang kan op elk moment worden ingetrokken. Alles wordt aangeboden op "best effort" basis.
Historie
Tailcat begon in september 2023 als "derpcat", geschreven tijdens een lange vlucht. Het leefde aanvankelijk in een fork van de tailscale.com repository. Later is het refactored tot een reguliere Go-module client van de hoofdrepository. Het project is in augustus 2026 open-sourced tijdens de TailscaleUp-conferentie.
Groetjes,