
Rust製のピアツーピア暗号化メッセンジャー(Noise IK と UDP NAT hole punching 使用)
エンドツーエンド暗号化されたピアツーピアチャット(UDP経由)。アカウント不要、中央サーバーによるメッセージの中継・保存なし、中間者なし。単なる2つのピア、直接接続、Noiseプロトコル暗号化。
https://github.com/user-attachments/assets/939e96d3-45e3-4484-9a27-28c3a0457b05
2人のユーザーが各自のマシンで punchline connect <peer> を実行します。PunchlineはNATを通過し、暗号化ハンドシェイクを実行し、プライベートチャットを開始します。所要時間はわずか数ミリ秒。内蔵のSTUNサーバーとシグナルサーバーが発見を処理した後、その後の通信には関与しません。

cargo build --release
サーバーの起動(両方のピアから到達可能なマシン上で)。または、私が公開しているサーバー(64.225.107.28、STUN: ポート 3478、シグナリング: ポート 8743)を使用することもできます。
punchline-stund # STUNサーバー - ピアに自身の公開IPを通知
punchline-signald # シグナルサーバー - 通信したいピアをマッチング
各ピアのマシンで:
# 自身のID(X25519鍵ペア)を生成
punchline keygen
# 公開鍵を相手と共有
punchline pubkey
# 相手の鍵を保存
punchline peers add alice a1b2c3d4...64_hex_chars
# 接続(両方のピアがこのコマンドを実行し、互いを指定)
punchline connect alice --stun <server>:3478 --signal <server>:8743
TUIが起動し、接続進行状況がリアルタイムで表示されます。
STUN探索 - punchline-stund 経由で自分の外部アドレスを解決
シグナルサーバー - punchline-signald に接続
ピア待ち - シグナルサーバーが両ピアをマッチング
ホールパンチ - 直接UDP経路を確立
Noiseハンドシェイク - 暗号化鍵交換
完了後、チャット画面になります。文字を入力してEnterキーを押して送信。Escキーで終了します。
システム全体は3つのバイナリで構成され、すべてこのリポジトリに含まれています。
| バイナリ | 役割 | 使用タイミング |
|---|---|---|
punchline-stund | STUNサーバー(UDP) - クライアントの外部IP:ポートを返す |
初期セットアップ後、STUNサーバーとシグナルサーバーには再アクセスしません。すべての通信は直接ピアツーピアで行われます。
punchlineグローバルフラグ:
| フラグ | 説明 |
|---|---|
-v | ログの詳細度を上げる(-v = デバッグ、-vv = トレース)。 |
-q, --quiet | すべてのログ出力を抑制。 |
punchline-stundpunchline-signald毎回 --stun と --signal を指定する代わりに、~/.config/punchline/config.toml を作成します。
stun_server = "203.0.113.10:3478"
signal_server = "203.0.113.10:8743"
punchline peers # 一覧表示
punchline peers add alice a1b2c3d4... # 追加
punchline peers remove alice # 削除
エイリアスは ~/.punchline/known_peers.toml に保存されます。生の64文字の16進鍵を直接指定して接続することもできます。
punchline status
自身のID、設定、サーバー到達可能性(実際のSTUNプローブとTCP接続を送信)、ピア数を表示します。
両サーバーとも -v(デバッグ)、-vv(トレース)、-q(クワイエット)、--address、--port をサポートします。
punchline-stund -v --port 3478
punchline-signald -v --port 8743
~/.config/punchline/style.toml でTUIをカスタマイズできます。
動画で使用したスタイル:
[colors]
my_text = "#ebdbb2"
peer_text = "#bdae93"
input_text = "#ebdbb2"
border = "#ebdbb2"
sidebar_key = "#ebdbb2"
sidebar_value = "#bdae93"
[padding]
chat_horizontal = 2
chat_vertical = 1
すべての色は16進RGBです。ファイルがない場合は、端末のデフォルト色が使用されます。
punchline completions bash > ~/.local/share/bash-completion/completions/punchline
punchline completions zsh > ~/.zfunc/_punchline
punchline completions fish > ~/.config/fish/completions/punchline.fish
完全なプロトコル名: Noise_IK_25519_ChaChaPoly_SHA256
| コンポーネント | 役割 |
|---|---|
| Noise IK | ハンドシェイクパターン - イニシエーターはレスポンダーの公開鍵を事前に知っている。2メッセージで完了。 |
| X25519 |
IKパターンでは、イニシエーターはハンドシェイク開始前にレスポンダーの静的公開鍵を知っている。両ピアはすでに互いの鍵を保持している(帯域外またはピアレジストリで交換済み)ため、初回接続時の信頼(TOFU)は不要。
Punchlineは、各ピアの公開鍵の先頭8バイトをビッグエンディアンの u64 として比較し、決定論的にイニシエーターを選択します。値が小さい方がイニシエーターになります。両サイドが独立にこの計算を行います。
IDは32バイトのX25519秘密鍵で、~/.punchline/id_x25519 にUnixパーミッション 0600 で保存されます。公開鍵は読み込み時に導出されます。鍵生成は x25519-dalek と OsRng を使用します。
各UDPパケットの先頭バイトがタイプを識別します。
両方のピアが同時に同じアルゴリズムを実行します。
PROBE(0x00)を送信。PROBE を受信したら、ACK(0x01)の送信に切り替え。ACK を受信したら、最後の ACK を1回送信し、成功を宣言。メッセージ(0x02)はNoise暗号化されたUTF-8ペイロードを運びます。キープアライブ(0x03)は暗号化された空ペイロードで、10秒ごとに送信され、暗号ノンスの同期を維持します。30秒間パケットがないと切断されます。
WebSocket上のJSON:
// PairRequest (クライアント -> サーバー)
{ "external_addr": "203.0.113.5:48291", "public_key": "a1b2...", "target_public_key": "d4e5..." }
// PairResponse (サーバー -> クライアント)
{ "target_external_addr": "198.51.100.7:51003", "target_public_key": "d4e5..." }
RFC 5389(簡略版)に従います: バインディング要求/応答、XOR-MAPPED-ADDRESS。IPv4のみ。
4つのクレートからなるCargoワークスペース:
crates/
├── proto/ # 共有ライブラリ: 暗号、STUN、シグナルタイプ、トランスポートトレイト
├── client/ # P2Pクライアント: CLI、TUI、接続ロジック、ピア管理
├── signald/ # シグナルサーバー: WebSocketピアマッチング
└── stund/ # STUNサーバー: 外部アドレス探索
cargo install punchline # TUIクライアント
cargo install punchline-signald # シグナルサーバー
cargo install punchline-stund # STUNサーバー
前提条件: Rust 2024エディション(rustc 1.85以上)
git clone https://github.com/michal-pielka/punchline.git
cd punchline
cargo build --release
バイナリは target/release/ に配置されます:
punchlinepunchline-signaldpunchline-stundcargo test
テストは暗号操作、STUNエンコード/デコード、シグナルプロトコルシリアライゼーション、設定解析、ピア管理、スタイルテーマ、Noise IKハンドシェイクをカバーします。
MIT - LICENSE を参照してください。
| セットアップ中のみ |
punchline-signald | シグナルサーバー(WebSocket) - ピアをマッチングし、アドレスを交換 | セットアップ中のみ |
punchline | メッセンジャー本体 - CLI、TUI、暗号、ホールパンチ | 常時 |
| コマンド | 説明 |
|---|
keygen [--force] [-i パス] | 新しいX25519 ID鍵ペアを生成します。--force で上書き確認なし。-i で出力パスを指定。 |
pubkey [-i パス] | 公開鍵(64文字の16進数)を表示します。-i で特定の鍵ファイルから取得。 |
connect <peer> [-i パス] [--stun アドレス] [--signal アドレス] | エイリアスまたは生の16進鍵でピアに接続します。-i でID鍵を指定。TUIを起動。 |
peers | 既知の全ピアを一覧表示。 |
peers add <名前> <鍵> | ピアの公開鍵をニックネームで保存。 |
peers remove <名前> | ニックネームでピアを削除。 |
config path | 設定ファイルのパスを表示。 |
config show | 現在の設定値を表示。 |
status | ID、設定、サーバー到達可能性、ピア数を表示。 |
completions <シェル> | シェル補完を生成(bash、zsh、fish)。 |
| フラグ | 説明 |
|---|
--address <addr> | バインドアドレス(デフォルト: 0.0.0.0)。 |
--port <port> | バインドポート(デフォルト: 3478)。 |
-v / -vv | デバッグ / トレースログ。 |
-q | クワイエットモード。 |
| フラグ | 説明 |
|---|
--address <addr> | バインドアドレス(デフォルト: 0.0.0.0)。 |
--port <port> | バインドポート(デフォルト: 8743)。 |
-v / -vv | デバッグ / トレースログ。 |
-q | クワイエットモード。 |
| 楕円曲線Diffie-Hellman鍵交換(RFC 7748)。128ビットセキュリティ、定数時間。 |
| ChaCha20-Poly1305 | メッセージ暗号化のためのAEAD暗号(RFC 8439)。TLS 1.3やWireGuardでも使用。 |
| SHA-256 | Noise内部で鍵導出とハンドシェイクハッシュに使用。 |
| プレフィックス | タイプ | フェーズ | 説明 |
|---|
0x00 | PROBE | ホールパンチ | 200msごとに送信、NATピンホールを開ける |
0x01 | ACK | ホールパンチ | PROBEの受信確認 |
| (なし) | Handshake | ハンドシェイク | 生のNoise暗号化ハンドシェイクペイロード |
0x02 | Message | トランスポート | 暗号化されたチャットメッセージ |
0x03 | Keepalive | トランスポート | 暗号化された空ペイロード(ハートビート) |
| クレート | 目的 |
|---|
snow | Noiseプロトコルフレームワーク(ハンドシェイク + トランスポート暗号化) |
x25519-dalek | X25519鍵生成と導出 |
ratatui | ターミナルUIフレームワーク |
crossterm | ターミナルイベント処理 |
clap | CLI引数解析 + シェル補完 |
tungstenite | WebSocketクライアント/サーバー |
tracing | 構造化ロギング |