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つの主要コンポーネントを組み合わせています:

  1. ユーザースペースWireGuard: LinuxカーネルのWireGuardモジュールに触れたり管理者権限を要求したりすることなく、ポイントツーポイント暗号化(Curve25519、ChaCha20-Poly1305)を処理します。
  2. magicsock: Tailscaleのマルチパストランスポートエンジン。STUNを介してUDPエンドポイントを動的にプローブし、直接のピアツーピア接続のためのUDPホールパンチングを実行します。
  3. DERP(Designated Encrypted Relay for Packets): ランデブーチャネルとして機能し、対称型NATによって直接のUDPホールパンチングが妨げられた場合に保証されたフォールバックとなる、HTTPSベースのリレー機構。
  4. 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
}

ハンドシェイクシーケンス

  1. ブートストラップ: クライアントはトークンを解析し、サーバーの公開鍵とDERPリージョンを抽出して、共有DERPリレーに接続します。
  2. ディスカバリーの交換: クライアントは、自身のエフェメラル公開鍵を含む軽量なMeowパケットをDERP経由で送信します。サーバーはクライアントを登録し、Meowedで応答します。
  3. WireGuardハンドシェイク: DERPトンネルを通じて標準のWireGuardハンドシェイクが行われます。
  4. 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アクセスを提供できます:

  1. クライアントキーペアの生成:

    client$ tailcat genkey --client
    # Public key: nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16
    
  2. 認証済みSSHサーバーの実行:

    server$ tailcat genkey --fixed-region
    server$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16
    
  3. DNS経由で公開: 生成されたサーバートークンを指すDNS TXT レコードを作成します:

    bastion.internal.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX"
    
  4. ドメイン経由で直接接続:

    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