Tailcat: コントロールプレーンなしで実現するポイントツーポイントWireGuardとNATトラバーサル
Tailcatは、中央の調整サーバーを必要とせず、TailscaleのmagicsockとWireGuardデータプレーンを軽量なユーザースペースCLIツールにもたらします。
Tailscaleは、WireGuardと巧妙なNATトラバーサル(magicsock)、暗号化された中継フォールバック(DERP)を組み合わせることでメッシュVPNに革命をもたらしました。しかし、一般的なTailscaleのセットアップでは、中央の調整サーバー(コントロールプレーン)にノードを登録し、システムのルーティングテーブルや仮想ネットワークインターフェース(TUN/TAP)を変更する特権デーモンを実行する必要があります。
Tailcatはそのパラダイムを変えます。Tailscaleチームによって開発されたTailcatは、Tailscaleのコアデータプレーンコンポーネントを、スタンドアロンのユーザースペースCLIおよびGoライブラリとしてパッケージ化しています。netcatのように機能しますが、すべての接続がWireGuardでエンドツーエンド暗号化され、自動的にNATを越え、root権限もTailscaleアカウントも必要としません。
仕組み: ルート権限不要のユーザースペースアーキテクチャ
Tailcatは、Tailscaleのコントロールプレーンを完全に排除しています。中央のコントロールサーバーを通じてピアの状態を調整する代わりに、コンパクトな接続トークンを使用してすべてのランデブーメタデータをアウトオブバンドで渡します。
プロセス内部で、Tailcatは4つの主要コンポーネントを組み合わせています:
- ユーザースペースWireGuard: LinuxカーネルのWireGuardモジュールに触れたり管理者権限を要求したりすることなく、ポイントツーポイント暗号化(Curve25519、ChaCha20-Poly1305)を処理します。
magicsock: Tailscaleのマルチパストランスポートエンジン。STUNを介してUDPエンドポイントを動的にプローブし、直接のピアツーピア接続のためのUDPホールパンチングを実行します。- DERP(Designated Encrypted Relay for Packets): ランデブーチャネルとして機能し、対称型NATによって直接のUDPホールパンチングが妨げられた場合に保証されたフォールバックとなる、HTTPSベースのリレー機構。
- gVisor
netstack: ユーザースペースメモリ内で実行される完全なTCP/IPスタック。netstackがプロセス内で接続を終端するため、TailcatはOSのルーティングテーブルやローカルネットワークインターフェースを変更することなく、着信ストリームを受信したり送信トラフィックを転送したりできます。
+-------------------------------------------------------------+
| Tailcat CLI |
| +-------------------------------------------------------+ |
| | gVisor netstack (Userspace TCP/IP) | |
| +-------------------------------------------------------+ |
| | Userspace WireGuard | |
| +-------------------------------------------------------+ |
| | magicsock | |
| +---------------------------+---------------------------+ |
| | STUN / UDP Hole Punch | DERP Encrypted Relay | |
+--+---------------------------+---------------------------+--+
接続トークン(ConnBlob)の構造
Tailcatサーバーが起動すると、プレフィックスtcが付いたエフェメラルなアドレストークンを出力します(例: tcomFwWCCcjS5nKNqAod034...)。
内部的には、この文字列は以下を含むBase64エンコードされたCBOR blobです:
- サーバーの32バイトCurve25519 WireGuard公開鍵(
nodekey:...)。 - DERPランデブーメタデータ(整数のリージョンID、または完全なノードホスト名とIP)。
トークンはCLIを使用して検査できます:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"RegionID": 302
}
ハンドシェイクシーケンス
- ブートストラップ: クライアントはトークンを解析し、サーバーの公開鍵とDERPリージョンを抽出して、共有DERPリレーに接続します。
- ディスカバリーの交換: クライアントは、自身のエフェメラル公開鍵を含む軽量な
MeowパケットをDERP経由で送信します。サーバーはクライアントを登録し、Meowedで応答します。 - WireGuardハンドシェイク: DERPトンネルを通じて標準のWireGuardハンドシェイクが行われます。
- NATトラバーサル: 並行して、両方のエンドポイントがDERP discoメッセージを介して候補UDPアドレス(STUNエンドポイントとローカルIP)を交換します。UDPホールパンチングが成功すると、データストリームは透過的に直接のUDP通信へアップグレードされます。
実践的なユースケース
1. 標準入出力のパイプ(暗号化Netcat)
受信側のマシン:
$ tailcat
# 🐈 Server listening with new address: tcXXXXXXXXX
送信側のマシン:
$ tar -czf - ./project | tailcat tcXXXXXXXXX
2. ファイアウォール背後にあるローカルポートの転送
ポートフォワーディング、UPnP、ngrokを使わずにローカルサービス(ステージングWebサーバーなど)を公開:
# サーバー側
$ tailcat --serve=8080
# 🐈 Server listening with new address: tcXXXXXXXXX
# クライアント側
$ tailcat tcXXXXXXXXX 8080
GET / HTTP/1.1
Host: localhost
3. DNS TXTレコードによるWireGuard認証SSH
Tailcatを使用すると、SSHデーモンが公開される前にWireGuardキーで保護されたSSHアクセスを提供できます:
クライアントキーペアの生成:
client$ tailcat genkey --client # Public key: nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16認証済みSSHサーバーの実行:
server$ tailcat genkey --fixed-region server$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16DNS経由で公開: 生成されたサーバートークンを指すDNS
TXTレコードを作成します:bastion.internal.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX"ドメイン経由で直接接続:
client$ tailcat ssh bastion.internal.example.com
未認証の接続試行はWireGuardレイヤーでドロップされるため、ポートスキャナーはSSHデーモンが実行されていることすら検知できません。
4. SOCKS5プロキシとイグジットノード
CLIトラフィックをリモートピア経由でルーティング:
# リモートホスト
$ tailcat --serve=exit-node
# ローカルマシン
$ tailcat socks <token> curl http://internal-service.local/
GoライブラリとしてのTailcatの使用
Tailcatは、Goサービスに直接組み込めるように構成されています。
サーバーの例
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) {
defer c.Close()
fmt.Fprintf(c, "Connected to in-process service on port %d\n", port)
}
},
}
if err := s.Start(); err != nil {
log.Fatal(err)
}
// このトークンをクライアントと共有
fmt.Println("Connection Token:", s.ConnBlob())
select {}
}
クライアントの例
package main
import (
"context"
"io"
"log"
"os"
"github.com/tailscale/tailcat"
)
func main() {
token := tailcat.ConnBlob(os.Args[1])
cl := tailcat.NewClient(token)
defer cl.Close()
conn, err := cl.DialTCPPort(context.Background(), 80)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
io.Copy(os.Stdout, conn)
}
リレーの持ち込み(BYO Relay): パブリックインフラなしでの運用
TailcatはデフォルトでパブリックDERPリレー(https://tailcat.dev/derpmap.json)を使用しますが、これに縛られるわけではありません。サーバートークンをセルフホストしたDERPサーバーに直接バインドできます:
server$ tailcat genkey --region=derp.myinfra.net
# derp.myinfra.netを埋め込んだトークンが生成される
DERPノードアドレスはトークン自体の中に保存されているため、接続するクライアントはローカルフラグを設定することなく自動的にランデブーリレーを解決します。
主なポイント
- ゼロコンフィグレーション: アカウント、APIトークン、中央コーディネーターは不要。
- ユーザースペースの安全性: 非特権で動作。仮想ネットワークアダプター、DNSの上書き、ファイアウォールの変更は不要。
- WireGuardのパフォーマンス: 自動NATトラバーサルにより、トンネルはDERPリレーからピアツーピアUDP接続へとアップグレード。
- 高い構成力: スタンドアロンのコマンドラインツール、アドホックなプロキシ、あるいは分散サービス向けの組み込みGoライブラリとして機能。
ソース
tailscale/tailcat: like netcat, but over Tailscale's data plane, without Tailscale's control plane