Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save kojira/7580ca1d7b48979833a42eadf630e556 to your computer and use it in GitHub Desktop.

Select an option

Save kojira/7580ca1d7b48979833a42eadf630e556 to your computer and use it in GitHub Desktop.

【注意喚起】Nostrリレーへのエフェメラルイベント大量流入 2026-08-28 / kojira (r.kojira.io / x.kojira.io 運用者)

★★★ 重要な訂正 (2026-08-28 追記) ★★★

本文書は当初「攻撃」として書きましたが、その後の調査で原因が特定されました。 攻撃ではなく、WebRTCライブラリ Trystero が公開Nostrリレーをデフォルトの シグナリング基盤として使用しているものです。詳細は末尾の【追記】を参照。

対処法(kindブロック・総量制限)はそのまま有効です。ただし「悪意ある攻撃」 ではないため、この文書を読んで対応を検討される場合は、末尾の追記まで 目を通してから判断してください。以下の本文は当時の観測記録として残します。

■ 症状

  • リレーのCPUが飽和し、書き込み(EVENT)のOK応答が返らなくなる
  • 購読(REQ)は正常に見えるため「読めるが書けない」状態になり原因が分かりにくい
  • リレーログが数時間で10GB超に肥大(エフェメラル削除ログの洪水)
  • プロセスは生きたままなので、supervisor/launchd の自動再起動が効かない

■ 攻撃の内容

  • エフェメラルイベント(kind 20000-29999)を毎秒4,000件以上送信
  • 当方のリレーでは kind 20001 と 22000番台(特に1つのkindに集中)
  • 別のリレーオーナーからは kind 23004 が32,000件という報告あり
  • リレーごとに使用するkindが異なる模様

エフェメラルイベントは「保存されないから無害」と思われがちだが、リレーは 受信・署名検証・一時保存・配信・削除の全処理を行うため、大量に送られると 確実に飽和する。多くのリレーがレート制限からエフェメラルを除外しているため 素通りしてしまう。

■ 攻撃と判断した根拠

一見すると正規ユーザーに偽装されており、静的な分布だけでは見分けがつかない:

  • 送信元IP: 194個に分散(34カ国・115 ASN)
  • pubkey: 201個(使い捨てではなく繰り返し使用、1回限りは4%のみ)
  • IP:pubkey がほぼ1:1(1台=1ユーザーに見える)
  • 住宅ISP中心(Comcast, AT&T, Verizon, KDDI等)でデータセンターがほぼ無い

これらは全て「正規ユーザーの集団」に見える特徴。しかし決定的だったのは時系列:

kindブロックを有効化した瞬間、毎秒4,000件が33件へ激減した。

201人の独立したユーザーが、サイレントなブロック(クライアント側には OK false が 返るのみ)に対して同時刻に一斉停止することはあり得ない。単一の主体が流量を 制御していることの証明。分散した見た目は偽装で、おそらくボットネットを利用。

■ 対処法

IPブロックは無効。194 IP・大半がIPv6で、遮断してもすぐ別のIPから来る。 住宅ISPを巻き込むリスクもある。

(1) 応急処置: kindベースのフィルタ writePolicyプラグインで該当kindを入口で遮断する。strfryなら例えば:

block_kinds = 20001, 22000-22999

効果(有効化60秒後の実測): エフェメラル削除数/回 : 403,776件 -> 2,101件 relay CPU : 51-67% -> 2.3% publish応答 : タイムアウト -> 即時OK

ただしこの方法には弱点がある:

  • 攻撃者がkindを変えれば効かない(実際に別リレーでは別kindが使われている)
  • 22000-22999 を塞ぐと正規のWebRTC通話も巻き添えで使えなくなる

(2) 本命の対処: エフェメラルの「総量」制限 kind(種類)ではなく量で判断すれば、正規用途を殺さずに攻撃だけ止められる。 リレー全体でエフェメラルの流入量に上限を設け、超過分を捨てる方式。

  • 正規のWebRTC通話(数人が普通のレート) -> 通る
  • 攻撃(毎秒4,000件) -> 止まる
  • 攻撃者がkindを変えても量で見ているので無関係に効く

重要: 閾値は必ず自分のリレーの実測値から決めること。 当方の実測では正規のエフェメラルは毎秒0.7件だったので、毎秒5件(バースト30) = 約7倍の余裕、を設定した。「毎秒500件」のような値は攻撃を素通りさせる。

なお per-pubkey のレート制限では今回の攻撃は止められない。201個に分散して おり1個あたり毎秒20件程度で、閾値を下回るため。分散攻撃には「個々の送信者」 ではなく「全体の総量」を見る必要がある。

参考実装: https://github.com/kojira/strfry-ratelimit (strfry用writePolicyプラグイン。kindブロック + エフェメラル総量制限。 設定ファイルのホットリロード対応、relay再起動不要)

■ 他のオーナーへの推奨

  1. エフェメラルイベントの流入量を確認する strfryなら "Deleted N events (ephemeral=N)" のログ。数万〜数十万が 繰り返し出ていれば被害を受けている。

  2. まず自分のリレーの正規エフェメラル流量を実測する 閾値設定はそれから。推測で決めると守れないか、誤爆する。

  3. kindを変えて再攻撃される可能性がある 遮断対象外のkindの流量も監視すること。

  4. 「プロセス生存だがポート応答なし」は自動再起動で拾えない 実際に接続を試すヘルスチェックを推奨。

■ 補足

エフェメラルイベントはレート制限の除外対象にされがちで、そこが突かれている。 設定を見直す価値がある。

情報交換歓迎。同種の被害や、別のkindでの観測があれば共有してほしい。

================================================================================ 【追記 2026-08-28】原因判明: 攻撃ではなく Trystero によるもの

上記本文で「攻撃」と断定しましたが、誤りでした。訂正します。

■ 原因

WebRTCライブラリ Trystero (https://github.com/dmotz/trystero) が、 公開Nostrリレーをデフォルトのシグナリング基盤として使用しています。

Trystero はサーバー不要のP2P接続ライブラリで、「インフラ不要」を売りに しています。そのインフラの実体が、ハードコードされた47個の公開リレーです。

ソースコードで確認した事実 (packages/nostr/src/index.ts, packages/core/src/):

  • defaultRelayUrls に47リレーがハードコード。x.kojira.io も含まれる (2026-03-18 のコミット e470c9d で作者が追加。運営者への確認はなし) yabu.me/v2, staging.yabu.me も含まれる

  • kind = strToNum(topic, 10_000) + 20_000 → ルームIDのハッシュから導出。20000-29999にランダムに散る → 観測された 22587/22668/23003/23044/24421、他リレーの 23004 は全てこれ → kindでのブロックが粗くならざるを得ない構造的理由

  • announceIntervalMs = 5_333 → 各ピアが5.3秒ごとに送信

  • defaultRedundancy = 5 → 1ピアが同時に5リレーへ送信

  • 接続確立後も送信が止まらない checkDeactivate() の停止条件に connectedPeer が「継続する理由」として 入っており、さらに isPassive (デフォルトfalse) でないと関数が即returnする → 通常利用では停止処理が一度も走らない。退室するまで永久に送り続ける

  • 送信失敗してもレートが落ちない (バックプレッシャー不在) 接続レベルの再接続には指数バックオフ+ジッターがあり良く出来ているが、 イベント送信は socket が閉じていれば黙って破棄されるだけで、 5.333秒のタイマーは送信結果に影響されない → リレーが過負荷でもOK falseを返しても、クライアントは減速しない

■ 「攻撃」と誤判定した理由 (同じ誤りを避けるために記録)

本文中の「ブロックした瞬間に流量が激減した=単一主体が制御している証拠」 という推論が誤りでした。全クライアントが同一ライブラリの同一ロジックで 動いているため、一斉に反応したように見えただけです。

また「1ピアあたり毎秒20件=異常」としましたが、これは異なる時点・異なる 計測窓の数字を割り算した無効な計算でした。同一キャプチャ内で数え直すと 1ピアあたり0.15〜0.16/秒で、announce間隔5.333秒(=0.1875/秒)と一致。 クライアントは完全に設計通りの正常動作をしています。

分散して見えた特徴(199 IP・201 pubkey・34カ国・住宅ISP中心・IP:pubkeyが1:1) は偽装ではなく、実際に世界中のブラウザタブが1つずつ鍵を持っている姿でした。

■ 影響範囲の実測 (デフォルトリレー47件、購読のみ・publishなし)

relay.libernet.app 223.8/秒 staging.yabu.me 43.2/秒 nostr.data.haus 42.3/秒 yabu.me/v2 29.7/秒 nostr.sathoarder.com 29.2/秒 relay.mostro.network 28.9/秒 nostr.vulpem.com 28.7/秒 nos.lol 28.2/秒 (100秒個別計測では94.5/秒) relay.agorist.space 27.9/秒 relay.damus.io 4.5/秒

  • 到達可能37件中25件が毎秒1件以上を受信、合計約557/秒
  • 10件は既に到達不能(DNS消失・接続拒否・502/523/530・証明書エラー) 死んだリレーがリストに残り続け、クライアントは接続を試み続けている
  • nos.lol のイベント年齢中央値1.6秒 = 蓄積分の再配信ではなくリアルタイム

つまり自分のリレー固有の問題ではなく、リストに載った多数のリレーが 現在進行形で負担しています。

■ 未解明の点

x.kojira.io のピークが毎秒4,000件だった理由は特定できていません。 1ピア0.1875/秒から逆算すると約21,000ピアが必要ですが、ピーク時のピア数を 記録しておらず、当時のログも切り詰めてしまったため検証不能です。 nos.lol が94.5/秒であることを考えると、x に何らかの集中があったと 推測されますが、確証はありません。

■ 対処法について

本文の対処法は有効ですが、優先順位が変わります。

(1) kindブロックは応急処置にとどめる Trystero の kind はルームIDのハッシュなので、範囲でしか塞げません。 当方は 20001 と 22000-22999 を塞ぎましたが、これは正規のWebRTC通話も 巻き添えにします。かつ 23044/21717/23003 等は範囲外なので素通りします。

(2) 総量制限が本命 kind(種類)ではなく量で判断すれば、正規用途を殺さず、kindを変えられても 効きます。閾値は必ず自分のリレーの実測から決めてください。 当方の実測: 正規のエフェメラルは毎秒0.7件、非エフェメラルのピークは x=4/秒、r=6/秒。エフェメラル総量5/秒(バースト30)、全体50/秒(バースト100) を設定しました。

参考実装: https://github.com/kojira/strfry-ratelimit
(エフェメラル総量制限 + 全kind総量制限を追加。設定ホットリロード対応)

(3) per-pubkeyのレート制限は効きません 201ピアに分散し1ピアあたり0.16/秒なので、どんな閾値でも下回ります。 分散したトラフィックには「個々の送信者」ではなく「全体の総量」を 見る必要があります。

(4) IPブロックも効きません 199 IPに分散、大半がIPv6の住宅ISP。遮断しても切りがなく、 正規ユーザーを巻き込みます。

■ 上流への報告

Trystero に issue を提出しました: dmotz/trystero#192

要望した内容:

  • 接続確立後は announce の頻度を下げる/停止する
  • checkDeactivate を passive モード以外でも有効にする
  • 確立後の冗長度(5リレー同時)を下げる
  • 到達不能なリレーをデフォルトリストから削除する
  • リレーの応答(OK false・送信失敗)を送信レートに反映する
  • デフォルトリスト掲載について運営者の同意を得る仕組み

同じ状況の運営者の方は、issue に実測データを添えて賛同いただけると 状況が伝わりやすくなると思います。

■ 見解

パブリックリレーは誰でも使えるものであり、P2Pのシグナリングという用途も 正当です。悪意はありません。

ただ、リレー運営者は自費でサーバーを運用しており、負荷はそのまま運営コスト になります。「インフラ不要」という利点は、47のリレー運営者がそのコストを 負担することで成立しています。設計を少し変えるだけで、この負担は大きく 減らせるはずです。

遮断すること自体は正当な自衛です。ただし相手は攻撃者ではなく、 善意で作られたライブラリとその利用者である、という点は共有しておきます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment