How to Publish Tailcat Addresses Using DNS TXT Records: A Complete Guide

Tailcat resolves server addresses from public DNS TXT records by looking for a tailcat= prefix, enabling you to connect using human-readable hostnames instead of raw Tailcat addresses.

Tailcat, Tailscale's peer-to-peer networking tool, supports DNS-based address discovery through specially formatted TXT records. This feature lets you publish a Tailcat address in DNS and connect using a simple hostname like example.com. This article explains exactly how the mechanism works, how to configure it, and the security considerations you must address.

How DNS TXT Record Resolution Works in Tailcat

The DNS lookup logic resides in cmd/tailcat/tailcat.go. When you supply a hostname to any Tailcat sub-command, the client performs the following steps:

  1. Calls net.Resolver.LookupTXT to fetch all TXT records for the hostname
  2. Scans each returned string for the prefix tailcat=
  3. Extracts and parses the remainder as a standard Tailcat address
  4. Proceeds with the connection using the resolved address

The core lookup implementation appears at lines 709-722:

// Simplified excerpt from cmd/tailcat/tailcat.go
txt, err := net.Resolver.LookupTXT(ctx, dnsName)
for _, t := range txt {
    if s, ok := strings.CutPrefix(t, "tailcat="); ok {
        // s contains the Tailcat address
        addr = s
        break
    }
}

If no tailcat= record exists, the client aborts with a fatal error (lines 720-722).

Creating a DNS TXT Record for Tailcat

To publish your Tailcat address, add a TXT record with the exact prefix tailcat= followed by your full address.

BIND Zone File Example

example.com.    IN  TXT  "tailcat=tc-1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h5i6j7k"

Obtain Your Tailcat Address

Generate or retrieve your address using:


# Generate a new key and address

tailcat genkey --key=default

# Or start a server to see its address

tailcat serve ssh

# Server prints: tc-xxxxxxxxxxxxxxxx...

Verify the Record

dig TXT example.com +short

# Expected output: "tailcat=tc-1a2b3c4d5e6f..."

Using DNS-Resolved Addresses with Tailcat Commands

Once published, use the hostname with any command that accepts a <tc-addr> argument:


# SSH via hostname

tailcat ssh example.com

# List directory contents

tailcat ls example.com:/var/log

# Copy files

tailcat cp localfile.txt example.com:/remote/path/

The client transparently resolves example.com to the embedded Tailcat address before establishing the connection.

Critical Security Considerations

The source code contains explicit warnings about the risks of public DNS TXT records. In tailcat.go at lines 1071-1076, a comment states:

A TXT record is public, so the contained address should be considered secret unless additional authentication is used.

Why This Matters

Risk Mitigation
Address enumeration — Anyone can query your DNS and discover the Tailcat address Always require client authentication
Unauthorized connections — No-auth services exposed via DNS are easily found and abused Never publish --no-auth addresses in DNS
Traffic interception — Passive observers see the address in DNS queries Use in combination with Tailcat's built-in encryption

# Require client authentication (safer for public DNS)

tailcat serve --allow=$(tailcat printpub) ssh

# NEVER do this with DNS-published addresses:

# tailcat serve --no-auth ssh  # DANGEROUS with public TXT records

The CLI prints warnings when a DNS-based address is detected without proper authentication (lines 328-336).

Programmatic DNS Resolution

Implement the same lookup logic in your own Go applications:

package main

import (
    "context"
    "fmt"
    "net"
    "strings"
)

func resolveTailcatTXT(name string) (string, error) {
    ctx := context.Background()
    r := net.Resolver{}
    
    txts, err := r.LookupTXT(ctx, name)
    if err != nil {
        return "", err
    }
    
    for _, t := range txts {
        if s, ok := strings.CutPrefix(t, "tailcat="); ok {
            return s, nil
        }
    }
    
    return "", fmt.Errorf("no \"tailcat=\" TXT record found for %q", name)
}

This mirrors the implementation in cmd/tailcat/tailcat.go at lines 715-718.

Key Source Files

File Purpose
cmd/tailcat/tailcat.go DNS-TXT lookup implementation (LookupTXT call, prefix parsing)
tailcat.go Core address handling; security warnings for public TXT records
cmd/tailcat/ssh.go DNS hostname support for SSH connections
cmd/tailcat/ls.go DNS hostname support for directory listing

Summary

  • Prefix requirement: TXT records must start with tailcat= followed by the full address
  • Lookup location: Resolution happens in cmd/tailcat/tailcat.go using Go's net.Resolver.LookupTXT
  • Security imperative: Public DNS means public addresses; always require client authentication via --allow
  • Broad compatibility: Works with ssh, ls, cp, and any other command accepting <tc-addr>

Frequently Asked Questions

What happens if multiple TXT records contain tailcat=?

Tailcat uses the first matching record found. The implementation at lines 715-718 iterates through TXT strings and returns immediately upon finding a tailcat= prefix. Publish only one Tailcat address per hostname to avoid ambiguity.

Can I use a CNAME record instead of publishing directly on my domain?

Yes. CNAME records work transparently because Tailcat resolves the canonical name before performing the TXT lookup. Point your alias to a host that carries the tailcat= record, or ensure both the CNAME and target have appropriate records.

Is there a length limit for the Tailcat address in DNS?

Standard DNS TXT records support 255 characters per string and multiple strings per record. Tailcat addresses typically fit within a single string. If your address exceeds 255 characters, your DNS provider should concatenate multiple strings automatically; Tailcat joins them before parsing.

Does Tailcat cache DNS TXT record results?

No built-in caching is implemented in the current source code. Each Tailcat command performs a fresh LookupTXT call. For frequent connections, consider using the raw Tailcat address directly or implementing local caching in wrapper scripts.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →