How to Explicitly Perform the Meow Handshake with Tailcat
You explicitly perform the Meow handshake by constructing a raw Meow Ping packet using EncodeMeowPing, transmitting it via MagicSock.SendDERPPacketTo, and blocking until the server responds with a Meowed acknowledgment that registers your node as a WireGuard peer.
Tailcat is an experimental Tailscale component that bootstraps WireGuard connections through a lightweight "Meow" handshake executed before the standard WireGuard handshake begins. While the handshake runs automatically inside the Ping method, explicitly managing the packet flow lets you control timing, debug connectivity issues, or integrate the handshake into custom networking stacks. This guide details the exact source code paths in the tailscale/tailcat repository required to implement this flow manually.
What Is the Meow Handshake?
The Meow handshake is a minimal key-exchange protocol that transmits a client’s NodePublic and DiscoPublicKey to a Tailcat server using raw DERP packets. Unlike standard Disco protocol frames, Meow packets are sent unwrapped directly over the DERP connection. The server validates the magic meow prefix, registers the client as a WireGuard peer, and replies with a Meowed acknowledgment. Once the client receives this acknowledgment, the regular WireGuard handshake proceeds using the newly established peer configuration.
Step-by-Step Handshake Flow
Step 1: Construct and Send the Meow Ping
The client initiates the handshake by encoding its public keys into a Meow Ping payload. In [disco.go](https://github.com/tailscale/tailcat/blob/main/disco.go#L30-L38), the EncodeMeowPing function serializes the keys with the meowMagic prefix:
pkt := tailcat.EncodeMeowPing(clientNodeKey, magicSock.DiscoPublicKey())
This packet must be sent as a raw DERP frame, not inside a Disco wrapper. The transmission happens in [tailcat.go](https://github.com/tailscale/tailcat/blob/main/tailcat.go#L70-L77) via the SendDERPPacketTo method on the underlying MagicSock:
sent, err := magicSock.SendDERPPacketTo(
serverNodePublic,
derpRegionID,
pkt,
)
if err != nil || !sent {
return fmt.Errorf("failed to transmit Meow Ping: %w", err)
}
Step 2: Server-Side Validation and Peer Registration
When the server receives the raw DERP packet, it first checks for the Meow magic constant. The helper IsMeowPacket in [disco.go](https://github.com/tailscale/tailcat/blob/main/disco.go#L25-L28) performs this validation:
if tailcat.IsMeowPacket(pkt) {
nodeKey, discoKey, ok := tailcat.ParseMeowPing(pkt)
// Implementation in disco.go lines 49-62
}
If parsing succeeds, the server invokes onMeow (implemented in [tailcat.go](https://github.com/tailscale/tailcat/blob/main/tailcat.go#L426-L431)) to add the client as a WireGuard peer. Upon successful registration, the server constructs a Meowed acknowledgment using EncodeMeowed from [disco.go](https://github.com/tailscale/tailcat/blob/main/disco.go#L41-L46):
ack := tailcat.EncodeMeowed()
magicSock.SendDERPPacketTo(srcNodeKey, regionID, ack)
Step 3: Receiving the Meowed Acknowledgment
The client blocks on a dedicated channel (c.meowWait) until the Meowed packet arrives. This synchronization logic resides in [tailcat.go](https://github.com/tailscale/tailcat/blob/main/tailcat.go#L80-L85) inside the ping method:
select {
case <-c.meowWait:
// Handshake complete; server has registered us as a peer
return nil
case <-ctx.Done():
return ctx.Err()
}
Once the channel closes, the handshake is complete and the WireGuard interface can begin standard key exchange with the newly configured peer.
Code Examples
Client-Side Explicit Handshake
The following snippet demonstrates how to manually trigger the Meow handshake without relying on the high-level Ping convenience method:
package main
import (
"context"
"fmt"
"time"
"tailscale.com/tailcat"
"tailscale.com/types/key"
)
func ExplicitMeowHandshake(
ctx context.Context,
c *tailcat.Client,
serverKey key.NodePublic,
regionID uint16,
) error {
// 1. Encode the Meow Ping packet
meowPkt := tailcat.EncodeMeowPing(
c.NodePublic(),
c.MagicSock().DiscoPublicKey(),
)
// 2. Send raw DERP packet
sent, err := c.MagicSock().SendDERPPacketTo(serverKey, regionID, meowPkt)
if err != nil {
return fmt.Errorf("send failed: %w", err)
}
if !sent {
return fmt.Errorf("packet queued but not sent")
}
// 3. Wait for Meowed acknowledgment (blocks until server replies)
select {
case <-c.MeowWaitChannel():
fmt.Println("Meow handshake succeeded")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Assumes c is an initialized *tailcat.Client
// err := ExplicitMeowHandshake(ctx, c, serverKey, regionID)
}
Server-Side Handler Implementation
Use this handler inside your DERP receive loop to process incoming Meow Pings and respond automatically:
func (b *Backend) HandleRawDERP(
pkt []byte,
src key.NodePublic,
regionID uint16,
) {
// Early detection of Meow protocol
if !tailcat.IsMeowPacket(pkt) {
// Process standard Disco or WireGuard traffic
return
}
nodeKey, discoKey, ok := tailcat.ParseMeowPing(pkt)
if !ok {
b.logf("invalid Meow ping from %v", src)
return
}
// Register peer and send acknowledgment
if b.onMeow(nodeKey, discoKey) {
ack := tailcat.EncodeMeowed()
b.magicSock.SendDERPPacketTo(src, regionID, ack)
}
}
Key Implementation Files
| File | Purpose | Key Functions |
|---|---|---|
[disco.go](https://github.com/tailscale/tailcat/blob/main/disco.go) |
Defines Meow packet structures and serialization | EncodeMeowPing, ParseMeowPing, EncodeMeowed, IsMeowPacket |
[tailcat.go](https://github.com/tailscale/tailcat/blob/main/tailcat.go) |
Client ping logic and server onMeow handler |
Client.ping, onMeow, meowWait channel handling |
[wire.go](https://github.com/tailscale/tailcat/blob/main/wire.go) |
DERP packet routing and raw frame delivery | SendDERPPacketTo integration |
Summary
- Encode explicitly using
tailcat.EncodeMeowPingto build the raw packet containing your NodePublic and DiscoPublicKey. - Transmit via raw DERP using
MagicSock.SendDERPPacketTorather than wrapped Disco frames. - Validate with
IsMeowPacketon the server to distinguish Meow traffic from standard protocol data. - Register peers via
onMeowand reply withEncodeMeowedto complete the handshake. - Synchronize on
meowWaitin the client to confirm the server has accepted the peer registration.
Frequently Asked Questions
What is the difference between the Meow handshake and a regular WireGuard handshake?
The Meow handshake is a Tailcat-specific prerequisite that exchanges NodePublic and DiscoPublicKey identities over DERP before WireGuard crypto-key routing begins. It uses a custom meow magic prefix and raw DERP packets, whereas the regular WireGuard handshake performs Noise protocol key exchange encapsulated in UDP (or DERP once the peer mapping is established).
Why does Tailcat use raw DERP packets instead of Disco frames for the Meow handshake?
Raw DERP packets bypass the Disco protocol layer to minimize overhead during the initial peer discovery phase. According to the source code in disco.go, the meowMagic constant allows the server to identify bootstrap traffic immediately without parsing Disco headers, enabling faster peer registration before the full WireGuard stack initializes.
How do I know if the Meow handshake succeeded?
The client’s ping method returns successfully only after the meowWait channel receives the Meowed acknowledgment from the server, as implemented in tailcat.go lines 80-85. If this channel closes or times out, the handshake failed. You can also verify server-side by checking the logs inside onMeow to confirm the peer was added to the WireGuard configuration.
Can I trigger the Meow handshake automatically without manual packet construction?
Yes. Calling client.Ping(ctx) automatically performs the handshake internally by invoking EncodeMeowPing and managing the SendDERPPacketTo call and meowWait synchronization. Manual construction is only necessary when you need to interpose custom logic between packet creation and transmission or when debugging DERP connectivity issues.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →