Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rust(Aya)入門 カーネルレベルでTwitterをブロックする

Avatar for fuse fuse
August 26, 2026
65

Rust(Aya)入門 カーネルレベルでTwitterをブロックする

Avatar for fuse

fuse

August 26, 2026

Transcript

  1. 動機 08 なぜやろうと思ったか ① ② 記事への憧れ 積んでいた本 「eBPFでTwitterをブロックしてみた」系の記事を見か eBPFは大学生のときに教えてもらった けた

    興味があって本まで買った 面白そう。我々もやりたい ……そして積んでいた 今日は積読を回収する話です
  2. 前提 ① 10 カーネルとは ユーザー空間 アプリケーション ブラウザ / curl /

    あなたのコード ↓ システムコール(お願いする窓口) カーネル空間 カーネル プロセス管理 / メモリ管理 / FS / ネットワーク デバイスドライバ ハードウェア(CPU / NIC / SSD) アプリはハードウェアを直接触れない。必ずカーネルに代行してもらう。 パケットの送受信も、当然カーネルの仕 事。
  3. 主役の登場 12 eBPF とは カーネルに手を入れずに、カーネル内でコードを実行できる技術 BEFORE AFTER アプリ カーネル(触れない) アプリ

    カーネル ← 自作コードをその場で注入 再コンパイルもモジュールも再起動も不要。動いたまま注入・取り外しできる 安全性は verifier(カーネル内の検証器)が担保。危ないコードはロードを拒否される
  4. 前提 ② 13 パケットの中身 届いたパケットは、ただのバイト列です。 Ethernet14B IPv4ヘッダ TCP/UDPヘッダ ← 先頭

    本文(HTTPなど) 末尾 → 層 分かること Ethernet MACアドレス IPv4 IPアドレス ← 今日使うのはここ TCP/UDP ポート番号 HTTP / TLS ドメイン名・URL
  5. 前提 ③ XDP とは eXpress Data Path NIC(物理的にパケットが届く) XDP ドライバが受け取った直後にフック/まだ「ただのバイト列」の状態

    ネットワークスタック(sk_buff生成、TCP/IP処理) アプリケーション スタックに入る前なので、カーネルの仕事が始まる前に決着をつけられる 14
  6. 道具の説明書 15 XDPのアクションコード コード 意味 XDP_PASS そのまま通す XDP_DROP 黙って破棄(エラーも返さない) XDP_TX

    受信したNICへ送り返す XDP_REDIRECT 別NICやAF_XDPソケットへ転送 XDP_ABORTED 例外扱いで破棄 実体はただの u32 定数(XDP_DROP=1, XDP_PASS=2)。これを返すとカーネルが実際にその通り動く
  7. 作戦マップ ① 16 どこで殴るのか NIC 受信 XDP ★今日はここ スタックに入る「前」。一番深い、一番早い ネットワークスタック開始(sk_buff生成)

    netfilteriptables / nftables ufw / firewalld もここ(薄いラッパー) アプリケーション ufwより下、というより、ufwが動き出す前に終わらせる
  8. 前提 ④ 17 Aya とは Rust で eBPF を書くためのフレームワーク C

    / libbpf(従来) Aya 言語 C Rust(型安全) ビルド clang / Makefile / bpftool cargo build 一発 依存 カーネルヘッダなどが必要 Rustツールチェインだけ あと、選んだ理由:Rustが書きたかったから
  9. 入門編 19 公式ガイドに沿ってやってみた Aya公式ドキュメントに XDP の章がある https://aya-rs.dev/book/programs/xdp $ cargo generate

    --name denytwitter -d program_type=xdp ¥ https://github.com/aya-rs/aya-template テンプレートを生成し、ガイドの手順をなぞる形で開始。日本語の入門記事も参考にした。 まずは「公式ガイドの通りに動かす」がスタート地点
  10. 入門編 20 プロジェクト構成(テンプレートのまま) denytwitter/ ├── denytwitter --- userspace(司令塔) ├── denytwitter-common

    --- 共有する型定義 └── denytwitter-ebpf --- eBPF本体(カーネル側) userspace 通常のRust。DNS解決もできる ←→ kernel (eBPF) no_std。パケットが来るたび実行 共有マップ(bpf()システムコール経由)でやり取りする 2つに分かれるのは eBPF 全般の定石。eBPF側は #![no_std] +特殊ターゲットなので、同じクレートに混在できない
  11. コア判定ロジック(eBPF側) 21 #[map] static BLOCKLIST: HashMap<u32, u32> = HashMap::with_max_entries(1024, 0);

    fn try_denytwitter(ctx: XdpContext) -> Result<u32, ()> { // ① 先頭14バイト = Ethernetヘッダ。IPv4以外は即PASS let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? }; match unsafe { (*ethhdr).ether_type() } { Ok(EtherType::Ipv4) => {} _ => return Ok(xdp_action::XDP_PASS), } // ② その次がIPv4ヘッダ。送信元IPを取り出す let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? }; let source = u32::from_be_bytes(unsafe { (*ipv4hdr).src_addr }); // ③ ブロックリストと照合してアクションを決める xdp_action::XDP_DROP } else { xdp_action::XDP_PASS }; Ok(action) let action = if block_ip(source) {
  12. 転換点 Twitter、IPいっぱいある問題 $ dig +short twitter.com 162.159.140.229 ← Cloudflare $

    dig +short x.com 151.101.2.146 ← 別のCDN 同じ「X」なのに twitter.com と x.com で別IP・別CDN 以前確認したときは両方同じIPだった。DNS解決の結果は固定ではなく、変動する 公式ガイドの「IPを1個直書き」では、まったく歯が立たない。 23
  13. 反省 24 正直に言います。ちょっとかえた。 let twitter_addrs: [u32; 1] = [ Ipv4Addr::new(162,159,140,229).into()

    手で観測した4つのIPを、直書き しています ]; let x_addrs: [u32; 3] = [ Ipv4Addr::new(151,101,2,146).into(), DNSの再解決による自動更新は入っていない Ipv4Addr::new(151,101,66,146).into(), Ipv4Addr::new(172,66,0,227).into(), ]; 我々は失敗した。Twitterの影響はあまりにも広く拡散し、ありふれたものになってしまった。我々にはたった一 つの選択肢が残されたのだ、諸君。私は 最終手段IP全書きを実行する。これはやり遂げられねばならない。 今日勝っているのは、たまたま今日IPが変わっていないからだ。
  14. 今日一番の学び 25 ドメインでは殴れなかった Ethernet やりたかったこと: x.com というドメイン名でブロックす IPv4 る TCP

    できたこと:IPアドレスでブロックする ↑ XDP が見えるのはここまで 本文(HTTP / TLS) XDP も connect4 も、扱えるのは IPアドレスとポート(L3 / L4)。ドメイ ン名があるのは TLSのSNI や HTTPのHostヘッダ、つまり L7。 ↑ ドメイン名はここにしかない。しかも暗号化済み 選んだフックポイントの構造上、原理的に見えない
  15. 応用編 27 cgroup/connect4 という別の武器 connect() システムコールの発行時点にフックする XDP connect4 フック位置 NICドライバ直後

    connect() 呼び出し時 対象 届いたパケット これから行う接続 アタッチ先 ネットワークインターフェース /sys/fs/cgroup アクションコード XDP_PASS / DROP など5種 1(許可)/ 0(拒否)の二値 できること 高速なDROP 接続先の透過的な差し替え eBPFはプログラムタイプごとに、扱えるデータもアクションコードも全部違う
  16. 作戦マップ ② 28 アプリのところにも差し込める NIC 受信 XDP デモ①でここに刺した ネットワークスタック(sk_buff) netfilter

    アプリケーション connect4 connect() を呼んだ瞬間に横取り XDPは「届いたパケット」、connect4は「これから出ていく接続」 ★デモ②
  17. 接続先を書き換える 29 fn try_forwardtwitter(ctx: SockAddrContext) -> Result<i32, i32> { let

    sock_addr = unsafe { &mut *ctx.sock_addr }; let dst_addr = u32::from_be(sock_addr.user_ip4); let redirect_addr = unsafe { REDIRECT_IP.get(&0) }; let redirect_port = unsafe { REDIRECT_PORT.get(&0) }; if block_ip(dst_addr) { if let (Some(&redirect_addr), Some(&redirect_port)) = (redirect_addr, redirect_port) { sock_addr.user_ip4 = redirect_addr.to_be(); // → 127.0.0.1 sock_addr.user_port = redirect_por } Ok(1) // 1 = 接続を許可(ただし宛先はもう別物) } 構造体のフィールドに代入するだけで、接続先が変わる
  18. まとめ 結局どこで殴っていたのか XDP デモ①。パケットを黙って捨てる ネットワークスタック(sk_buff) netfilteriptables / nftables / ufw

    / firewalld connect() デモ②。接続そのものを書き換える 冒頭で「甘い」と言ったufwは、全部この図の真ん中にいました 31
  19. みなさんも、 絶対にやってください 公式ガイドがあります → 迷いません cargo build 一発です → 環境構築で死にません

    カーネルは、思っているより開かれています 意志力で勝てないものは、 カーネルで殴れ
  20. 付録 ③ A3 環境構築・実行操作編 借用チェッカー(borrow checker)エラー(複数回発生) error[E0499]: cannot borrow `ebpf`

    as mutable more than once at a time ebpf という1つの変数に対して、program(XDP)と blocklist(マップ)、あるいは redirect_addr と redirect_port のように、複数の 書き換え可能な参照を同時に持とうとしてエラーになった。片方の使用を完全に終えてから次を取得する、という順序に直す必要が あった .get() が返すのは値そのものではなく Option マップから値を取り出す .get() は Option<&u32> を返す。「登録されていないかもしれない」ことを if let Some(...) で扱う必要があ る。複数の Option を同時に判定する際、タプルにまとめる書き方でつまずいた
  21. 付録 ④ A4 eBPF/Aya特有の設計理解(1) エントリポイントと実処理を統合し忘れる warning: function `try_xdp_firewall` is never

    used 実際に呼ばれる try_denytwitter(空の雛形)と、実ロジックが書かれた try_xdp_firewall(呼ばれない)という2つの別関数が並存し てしまった。実質、何も処理されていなかった。ビルド時の unused 警告が手がかりになった #[map] は特殊な型にしか付けられない static REDIRECT_IP: u32 = ...; のように、普通の u32 変数に #[map] は付けられない。userspaceと値をやり取りするには HashMap や Array のようなeBPF専用マップ型が必須。この制約に気づくまで時間がかかった
  22. 付録 ⑤ A5 eBPF/Aya特有の設計理解(2) エンディアン変換の方式がプログラムタイプによって違う XDP側 connect4側 元のデータ src_addr :

    [u8; 4] user_ip4 : u32 変換関数 u32::from_be_bytes(...) u32::from_be(...) 理由 バイト配列から組み立てる u32として書かれた値を読み直す ポートの型不一致(u32 vs u16) REDIRECT_PORT を HashMap<u32, u32> にしていた時は動かなかった。HashMap<u16, u16> に変更してから正常に動作した。 ポート番号は本来16bitの値。型を実態に合わせたことが解決の鍵だった