deepsecのscan/processとLocal LLM運用を整理する

はじめに

deepsec は、AI agent を使って既存のコードベースから脆弱性を探すためのツールである。単純な静的解析ツールというより、まずローカルで怪しい箇所を広く拾い、その後に LLM へ調査させる二段構えのセキュリティレビュー基盤に近い。

この記事では、deepsec を読みながら確認した内容をもとに、重要な機能、scanprocess の違い、コードが外部に送られるタイミング、Local LLM や LiteLLM を使った閉域運用の可能性を整理する。

deepsecの中心機能

deepsec の核は、以下のようなパイプラインである。

scan -> process -> revalidate -> enrich -> report/export/metrics

scan は、コードベースをローカルで走査し、regex matcher によって「調査候補」を見つける。process は、その候補を LLM に読ませて、本当に脆弱性として成立するかを調査する。revalidate は既存の finding を再検証し、false positive や修正済みを判定する。enrich は git 履歴や ownership 情報を付与する。最後に reportexport で人間や CI が扱いやすい形に整える。

対応範囲は特定の言語に限定されていない。JavaScript / TypeScript、Python、Go、PHP、Ruby、Terraform、Dockerfile、Kubernetes、GitHub Actions、AI agent 関連のコードなどを対象にしている。ただし、言語や framework ごとに matcher の厚みには差がある。Next.js や JS/TS、Python、Go、Rails、Laravel などは比較的手厚く、Rust、JVM、.NET は tech detection はあるものの、専用 matcher はまだ発展途上という印象だ。

また、deepsec は plugin 機構を持っている。built-in matcher だけで足りない場合は、社内 framework や独自 API のパターンに合わせて matcher や agent backend を追加できる。

scanとprocessの違い

scanprocess は混同しやすいが、役割はかなり違う。

scan はローカルで動く機械的な候補抽出である。ファイルを glob し、tech detection を行い、matcher を走らせて candidates を作る。この時点では脆弱性と確定していない。

例えば、以下のようなコードがあるとする。

export async function GET(req: Request) {
  const id = new URL(req.url).searchParams.get("id");
  const user = await db.query(`SELECT * FROM users WHERE id = ${id}`);
  return Response.json(user);
}

scan はこのコードを見て、Next.js の route handler らしい、raw SQL らしい、外部入力が絡んでいるかもしれない、といった候補を記録する。

{
  "filePath": "app/api/users/route.ts",
  "candidates": [
    {
      "vulnSlug": "js-sql-raw",
      "lineNumbers": [3],
      "matchedPattern": "raw SQL string interpolation"
    },
    {
      "vulnSlug": "all-route-handlers",
      "lineNumbers": [1],
      "matchedPattern": "Next.js route handler"
    }
  ],
  "status": "pending"
}

一方、process は LLM による実調査である。scan が作った候補ファイルを batch にし、Codex や Claude などの agent backend に読ませる。LLM は、入力が本当に attacker-controlled か、認証や validation があるか、ORM が parameterize していないか、脆弱性として実害があるかを判断する。

本当に問題があると判断されると、candidates から findings に昇格する。

{
  "filePath": "app/api/users/route.ts",
  "findings": [
    {
      "severity": "HIGH",
      "vulnSlug": "js-sql-raw",
      "title": "SQL injection in user lookup route",
      "description": "The id query parameter is interpolated directly into SQL.",
      "lineNumbers": [3],
      "recommendation": "Use a parameterized query instead.",
      "confidence": "high"
    }
  ],
  "status": "analyzed"
}

逆に、scan が候補にしても、process が安全だと判断すれば finding は出ない。例えば次のように parameterized query になっていれば、候補には残っても、最終 finding にはならない可能性が高い。

await db.query("SELECT * FROM users WHERE id = ?", [id]);

つまり、scan は「安く広く拾う」段階であり、process は「高コストだが深く判断する」段階である。

データは外部に送られるのか

通常の deepsec scan 単体では、ソースコードや候補データは外部に送られない。実装上は、ローカルファイルを読み、tech detection と regex matcher を実行し、結果を data/<projectId>/ 配下に書くだけである。

一方で、以下の操作では外部送信が発生する。

  • process: 候補ファイルや repo context を LLM provider に送る
  • revalidate: 既存 finding と関連コードを LLM provider に送る
  • triage: finding の説明や project context を Claude に送る
  • sandbox scan / sandbox process: Vercel Sandbox に repo や data の tarball をアップロードする
  • plugin: 外部通信する plugin を読み込んだ場合、その plugin 次第で送信される

したがって、閉域で運用したい場合に重要なのは、scan ではなく processrevalidatetriage をどう扱うかである。

Local LLMで外部送信を避けられるか

Local LLM を使えば、processrevalidate でコードを外部 provider に送らない構成は可能である。ただし、現状の deepsec にそのまま差し替えられるかは、Local LLM の API 互換性に依存する。

一番近い経路は --agent codex である。Codex backend は OPENAI_BASE_URLOPENAI_API_KEY を見て、OpenAI 互換 endpoint に向けられる。ただし、ここで要求されるのは一般的な /v1/chat/completions ではなく、OpenAI Responses API の /v1/responses である。

そのため、Local LLM サーバが Responses API に十分対応していれば、以下のような形で試せる。

OPENAI_API_KEY=local
OPENAI_BASE_URL=http://127.0.0.1:8000/v1
pnpm deepsec process --project-id <id> --agent codex --model <local-model-name>

しかし、多くのローカル OpenAI-compatible server は /v1/chat/completions だけを実装している。その場合、環境変数を差し替えるだけではうまく動かない可能性がある。

より堅い方法は、deepsec の plugin 機構で Local LLM 用の AgentPlugin を追加することである。AgentPlugininvestigate()revalidate() を実装すればよいので、内部で /v1/chat/completions 互換の Local LLM を呼び、deepsec が期待する JSON finding に変換すればよい。

LiteLLMを挟む場合

LiteLLM を使うと、この問題はかなり現実的に解決できる。LiteLLM Proxy は OpenAI-compatible な入口を提供し、背後に Ollama、vLLM、llama.cpp、各種 cloud provider などを接続できる。さらに Responses API 互換の入口として使える場合、deepsec の Codex backend から見て OpenAI 互換 endpoint として扱いやすくなる。

例えば、Ollama のローカルモデルを LiteLLM 経由で使うなら、概念的には次のような構成になる。

litellm --model ollama/qwen2.5-coder:32b --api_base http://localhost:11434

deepsec 側は LiteLLM Proxy を OpenAI endpoint として指定する。

OPENAI_API_KEY=anything
OPENAI_BASE_URL=http://127.0.0.1:4000
pnpm deepsec process --project-id <id> --agent codex --model qwen2.5-coder:32b

この構成では、deepsec から見た送信先は localhost の LiteLLM になる。さらに LiteLLM の upstream も Ollama や vLLM などのローカル実行にすれば、コード断片は外部に出ない。

ただし、注意点もある。

  • LiteLLM の upstream を cloud provider にすると、当然コードは外部に送られる
  • observability callback や logging を外部サービスに向けると、そこから漏れる可能性がある
  • Local LLM の精度次第で false negative が増える
  • triage は現状 Claude SDK 直呼びなので、完全ローカル化するなら別途対応が必要
  • Codex backend と LiteLLM の Responses API 互換性の細部で詰まる可能性はある

最初に試すなら、scan は通常通りローカルで実行し、process だけを LiteLLM 経由の --agent codex に向けるのがよい。

運用イメージ

閉域寄りの運用を考えるなら、次のような流れになる。

# 1. ローカルで候補抽出
pnpm deepsec scan --project-id my-app --root .

# 2. LiteLLM + Local LLM にだけ調査させる
OPENAI_API_KEY=anything \
OPENAI_BASE_URL=http://127.0.0.1:4000 \
pnpm deepsec process --project-id my-app --agent codex --model qwen2.5-coder:32b

# 3. 必要なら結果を export
pnpm deepsec export --project-id my-app --format md-dir --out ./findings

この場合、scan の段階では外部送信なし、process の段階でも LiteLLM と Local LLM が localhost に閉じていれば外部送信なし、という構成にできる。

まとめ

deepsec は、regex matcher による候補抽出と LLM による深い調査を分離している。この設計により、scan はローカルで安く広く実行でき、process は必要な箇所だけを高精度に調査できる。

外部送信を避けたい場合、問題になるのは scan ではなく processrevalidatetriage である。processrevalidate については、Codex backend の OPENAI_BASE_URL を LiteLLM に向ける構成が現実的な第一候補になる。LiteLLM の upstream もローカルに閉じれば、コードを外部 provider に送らずに運用できる可能性がある。

ただし、Local LLM の精度は商用の強い reasoning model と同等とは限らない。セキュリティレビュー用途では false negative が最も怖いので、閉域性と検出精度のトレードオフを理解したうえで使う必要がある。

tmuxのステータスバーにClaude Codeの動作状況を表示する

はじめに

Claude Code を日常的に使っていると、「今 AI が考えているのか、待っているのかわからない」という瞬間が出てくる。特に別のペインで作業しながら Claude Code を動かしているときに、ちらっとステータスバーを見るだけで状況が把握できると快適だ。

この記事では、Claude Code のフック機能と tmux の status-right を組み合わせて、AI エージェントの動作状況(idle / working / blocked)をリアルタイムで表示する仕組みを作る手順を整理する。

構成の概要

Claude Code フック
  → ~/.claude/agent-state (状態ファイル)
  → tmux refresh-client -S

tmux status-right
  → ~/.claude/tmux-agent-status.sh
  → 状態ファイルを読んでカラー表示

Claude Code には、ツール実行前・停止時・セッション開始・終了などに任意のシェルコマンドを実行できる「フック」機能がある。これを使って状態ファイルを更新し、tmux がその内容をポーリングして表示する。

状態は 3 種類:

状態意味表示
workingツール実行中・回答生成中シアン ⬤ claude
blocked権限確認待ちオレンジ ⊘ claude
idleセッション起動中・応答待ちグレー ○ claude

セッション終了時(release)は状態ファイルを削除し、tmux の表示からも消える。

セットアップ手順

1. フックスクリプトを作成

~/.claude/hooks/tmux-agent-state.sh を作成する。

#!/bin/sh
state="${1:-}"
state_file="${HOME}/.claude/agent-state"

case "$state" in
  working|blocked|idle) printf '%s\n' "$state" > "$state_file" ;;
  release) rm -f "$state_file" ;;
  *) exit 0 ;;
esac

tmux refresh-client -S 2>/dev/null || true
chmod +x ~/.claude/hooks/tmux-agent-state.sh

tmux refresh-client -S によって、フック実行直後にステータスバーが即時更新される。status-interval を待たずに反映されるのがポイント。

2. tmux 表示スクリプトを作成

~/.claude/tmux-agent-status.sh を作成する。

#!/bin/sh
state_file="${HOME}/.claude/agent-state"
[ -f "$state_file" ] || exit 0
state=$(cat "$state_file" 2>/dev/null) || exit 0
case "$state" in
  working) printf '#[fg=#8bd5ca,bold]⬤ claude#[nobold]' ;;
  blocked) printf '#[fg=#f5a97f,bold]⊘ claude#[nobold]' ;;
  idle)    printf '#[fg=#7d8996]○ claude' ;;
esac
chmod +x ~/.claude/tmux-agent-status.sh

カラーコードは Catppuccin Macchiato ベースのテーマに合わせている。自分の tmux テーマに合わせて #8bd5ca(シアン)と #f5a97f(オレンジ)と #7d8996(グレー)を調整すること。

3. Claude Code の hooks に登録

~/.claude/settings.jsonhooks セクションに以下を追加する。各イベントに対応した state を渡す。

{
  "hooks": {
    "PermissionRequest": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' blocked",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "PreToolUse": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' working",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' release",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' idle",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' idle",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "command": "bash '~/.claude/hooks/tmux-agent-state.sh' working",
            "timeout": 5,
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ]
  }
}

既存の hooks がある場合は、各イベントの hooks 配列に追記する形でマージする。

4. tmux.conf を更新

~/.tmux.confstatus-right の先頭に #(~/.claude/tmux-agent-status.sh) を追加する。

# 変更前
set -g status-right '#[fg=#9aa7b4,bg=#101419] CPU #{cpu_percentage} ...'

# 変更後
set -g status-right '#(~/.claude/tmux-agent-status.sh) #[fg=#9aa7b4,bg=#101419] CPU #{cpu_percentage} ...'

status-right-length が短いと表示が切れるため、必要に応じて広げる。

set -g status-right-length 120

設定を反映する。

tmux source-file ~/.tmux.conf

動作確認

スクリプト単体でテストできる。

# working 状態のテスト
echo "working" > ~/.claude/agent-state && ~/.claude/tmux-agent-status.sh

# idle 状態のテスト
echo "idle" > ~/.claude/agent-state && ~/.claude/tmux-agent-status.sh

# セッション終了(表示消去)のテスト
rm ~/.claude/agent-state && ~/.claude/tmux-agent-status.sh

tmux のステータスバーに ⬤ claude○ claude⊘ claude と表示されれば完成。

メリット

コンテキストスイッチの削減 Claude Code が作業中かどうかを確認するために Claude Code のペインにフォーカスを移す必要がなくなる。ステータスバーの隅を見るだけで把握できる。

blocked 状態の即時検知 ⊘ claude の表示で権限確認が必要なことがわかり、見逃しが減る。

シンプルな実装 状態ファイルを介した疎結合な設計なので、Claude Code のバージョン依存がなく壊れにくい。ファイルを読むだけなのでパフォーマンスへの影響もほぼない。

既存フックとの共存 herdr などの他のエージェント管理ツールのフックと並列で動かせる。settings.json の同じイベントに複数フックを追加するだけでよい。

まとめ

Claude Code のフックと tmux の #() コマンド展開を組み合わせることで、AI エージェントの状態をステータスバーに常時表示できる。実装はシェルスクリプト 2 本と JSON の設定変更のみで、既存の tmux 設定を大きく変える必要はない。

tmux refresh-client -S による即時更新により、status-interval の間隔(デフォルト 15 秒)を待たずにリアルタイムで状態が変わる点も実用的だ。

Obsidian Web Clipperの代わりにクリップボードからノートを作るzsh関数

はじめに

Obsidian はローカルの Markdown ファイル群をノートとして扱うエディタで、公式の Web Clipper ブラウザ拡張を使うと、見ているページのタイトル・URL・本文を所定のテンプレート(frontmatter 付き Markdown)として Vault に保存できる。

ただ「ブラウザ拡張を開いて保存」という導線が、ターミナル中心の作業だと少し重い。コピーした文章をターミナルから一発でノート化したい。この記事では、Web Clipper と同じ出力フォーマットを zsh 関数で再現する過程をまとめる。題材は投資ニュースを raw/news/ に貯める用途だが、保存先とテンプレートを差し替えれば汎用的に使える。

ゴールはこうだ:

  • 既定はクリップボードから本文を取得して即保存(ブラウザでコピー → ターミナルでコマンド実行だけ)
  • -p を渡したときだけ、ターミナルへの貼り付けを待ち受ける
  • パイプ(pbpaste | clip-news)でも動く
  • 出力は Web Clipper のテンプレート(type: clip / status: inbox などの frontmatter)と一致させる

出力したいフォーマット

Web Clipper 側のテンプレートに合わせ、こういうノートを YYYY-MM-DD-タイトル.md で生成する。

---
type: clip
status: inbox
title: "(ファイル名から)"
source: ""
clipped: "2026-06-10"
ticker:
theme:
---

> [!info] raw clip — 直接編集せず、後で要約・統合する。

(本文)

ファイル名は入力で指定でき、空 Enter なら 2026-06-10-ab3xy.md のようにランダムな5文字を付ける。

ハマりどころ: 複数行ペーストと read

最初の素朴な実装は「本文を1行 read する」だったが、これは複数段落の記事で破綻する。

ターミナルの行入力(cooked mode)では、ペーストした文字列に含まれる改行がそれぞれ確定として扱われる。read 1回では1行目しか取れず、残りの行はシェルのプロンプトに流れ込み、最悪そのままコマンドとして実行される。これは地味に危険だ。

そこで「最初の1行は待つ → 残りはまとめて吸い出す」方針にする。最初の版ではタイムアウト付き read で残りをドレインした。

IFS= read -r line; body="$line"
while IFS= read -r -t 0.4 line; do   # 入力が 0.4 秒止まったら確定
  body+=$'\n'"$line"
done

ペーストは一気に届くので、0.4 秒の猶予で2行目以降を回収でき、溢れも防げる。ただし末尾で必ず 0.4 秒待つのが気になる。

read -t 0 で待ち時間をゼロにする

「Enter を押した瞬間に保存したい(末尾の待ちを消したい)」という要望に対し、zsh の read -t 0 が効いた。

zsh のドキュメント上 -t 0 は「入力があるか判定するだけ(消費しない)」と読めるが、手元の zsh 5.9 で試すと 入力があれば即座に1行読み、なければ即座に偽を返す挙動だった。つまりこれをドレインのループにそのまま使える。

IFS= read -r line; body="$line"
while IFS= read -r -t 0 line; do   # 残りを待たずに吸い出す
  body+=$'\n'"$line"
done
  • 単一行: 残りが無いのでループは即終了 → 待ち時間ゼロ
  • 複数行: ペーストはまとめて届いているので -t 0 が次々に回収

トレードオフとして、-t 0 は「今この瞬間に入力があるか」で判断するため、巨大なペーストが複数チャンクに分割して届くとごく稀に末尾を取りこぼす。ニュースのスニペット程度なら一括で届くので実用上は問題ない。長文は後述のパイプを使えば確実だ。

3つの入力モードを分岐する

最終形は「既定=クリップボード / -p=待ち受け / パイプ=stdin」の3経路にした。判定は [[ -t 0 ]](stdin が端末か)と -p フラグの2つだけ。

clip-news() {
  emulate -L zsh
  local VAULT="$HOME/path/to/vault"
  local DIR="$VAULT/raw/news"
  local DATE; DATE="$(date +%Y-%m-%d)"
  mkdir -p "$DIR"

  local wait_input=0
  [[ "$1" == "-p" ]] && wait_input=1

  local name="" body="" line
  if [[ ! -t 0 ]]; then
    body="$(cat)"                          # パイプ/リダイレクト: stdin 全部
  else
    printf 'ファイル名(空Enter で自動: %s-XXXXX): ' "$DATE" >&2
    IFS= read -r name
    if (( wait_input )); then              # -p: 貼り付けを待ち受け
      printf '本文を貼り付けて Enter:\n' >&2
      if IFS= read -r line; then
        body="$line"
        while IFS= read -r -t 0 line; do
          body+=$'\n'"$line"
        done
      fi
    else
      body="$(pbpaste)"                     # 既定: クリップボードから取得
    fi
  fi

  local base title
  if [[ -z "${name//[[:space:]]/}" ]]; then
    base="${DATE}-$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 5)"
    title=""
  else
    base="${DATE}-${name//\//-}"            # '/' はファイル名に使えないので '-'
    title="${name//\"/\\\"}"               # YAML 用に '"' をエスケープ
  fi

  local file="$DIR/${base}.md" i=2
  while [[ -e "$file" ]]; do                # 同名は連番で回避
    file="$DIR/${base} ${i}.md"; ((i++))
  done

  {
    print -r -- "---"
    print -r -- "type: clip"
    print -r -- "status: inbox"
    print -r -- "title: \"${title}\""
    print -r -- "source: \"\""
    print -r -- "clipped: \"${DATE}\""
    print -r -- "ticker:"
    print -r -- "theme:"
    print -r -- "---"
    print -r --
    print -r -- "> [!info] raw clip — 直接編集せず、後で要約・統合する。"
    print -r --
    print -r -- "$body"
  } > "$file"

  print -r -- "✓ 保存: ${file#$VAULT/}" >&2
}

ポイント:

  • プロンプトやメッセージは >&2(標準エラー)へ。標準出力をパイプで汚さないため。
  • pbpaste は macOS のクリップボード読み出し。Linux なら xclip -o 等に置き換える。
  • ランダム5文字は tr -dc 'a-z0-9' < /dev/urandom | head -c 5
  • emulate -L zsh で関数内だけ素の zsh 挙動に固定し、ユーザー環境の setopt に左右されないようにする。

スクリプトではなく関数で、dotfilesに置く

当初は Vault 内に .zshrc から呼ぶ外部スクリプトを置いていたが、最終的に dotfiles リポジトリの .zshrc に関数として直書きする形にした。~/.zshrc が dotfiles 内のファイルへの symlink になっているため、編集対象は実体である dotfiles 側になる。こうすると外部ファイル依存がなくなり、シェル設定がバージョン管理下で self-contained になる。

~/.zshrc -> ~/ghq/github.com/<user>/dotfiles/.zshrc   # symlink

まとめ

  • Obsidian Web Clipper の出力(frontmatter 付きノート)は、テンプレートを固定すれば zsh 関数で十分再現できる。
  • ターミナルでの複数行ペーストは read 1回だと取りこぼし&プロンプトへの溢れが起きる。read -t <timeout> で残りをドレインするのが定石。
  • 末尾の待ち時間が嫌なら read -t 0 が使える(手元の zsh では「あれば即読み・無ければ即終了」)。ただし巨大ペーストの分割到着には弱いので、確実さが要るならパイプ経由にする。
  • 入力経路は [[ -t 0 ]] と1つのフラグで「クリップボード / 待ち受け / パイプ」に素直に分岐できる。
  • 単発スクリプト+alias より、dotfiles の .zshrc に関数として置くほうが依存が減って管理しやすい。

Headroom × Context Mode:コンテキスト効率化ツールの比較と実装

はじめに

LLM エージェント(Claude Code, OpenCode など)を使い込むと、すぐにコンテキスト問題に直面する。ファイル読み込み、ツール出力、ログ検索、git diff — 全部が context window を圧迫し、30 分も作業すると 4 割前後がツール出力で埋まる。

この問題に対して、レイヤーの異なる 2つのアプローチがある:

  1. Headroom — LLM に送る情報(会話履歴・tool 出力・RAG)を送信直前に圧縮(API トランスポート層)
  2. Context Mode — ツール出力をローカル処理して、そもそもトランスクリプトに入れない(エージェント/hook 層)

この記事では、2つのツールの設計思想の違い、実装レベルでの違い、そして組み合わせるときの実践的な手順を整理する。

まず:Claude Code のようなエージェントはどう動くのか

後で出てくる図を読むには、Claude Code のような「LLM エージェント」がどう動くかを先に押さえる必要がある。ここだけは前提知識として、できるだけかみ砕いて説明する。

ポイントは1つ。LLM(AI の本体)は、前の会話を覚えていない。だから毎回、「これまでの会話ぜんぶ」をもう一度まとめて渡してあげないと話が通じない。

たとえるなら、LLM は記憶力がゼロの天才だ。とても賢いが、1回答えるたびに記憶がリセットされる。だから話しかけるたびに、これまでのやりとりを書いたノートを丸ごと最初から読ませるしかない。エージェント(Claude Code)は、その「ノートを毎回渡す」係だと思えばいい。

動きを番号付きで追うとこうなる。

① あなたが指示する(例:「このログのエラー原因を調べて」)

② これまでの会話を“ぜんぶ”まとめて LLM に送る      ← 「送信」の瞬間

③ LLM が答えを返す or「道具を使いたい」と言う

④ 道具(ツール)を実行する
   (ファイルを読む / コマンドを走らせる / 検索する)

⑤ 道具の出力が“ノート(会話)”に追記される        ← ここがどんどん膨らむ

   (②に戻ってくり返す)

ここで2つの言葉を覚えておく。

  • コンテキスト(ノート):②で毎回送る「これまでの会話ぜんぶ」。LLM の作業机のようなもの。
  • コンテキストウィンドウ:その机の広さ(=1回に送れる上限)。無限ではない。

問題は だ。kubectl logs やコード検索のように、道具の出力が巨大だと、それがそのままノートに貼り付けられて机を埋めていく。机が埋まれば、本来の作業に使えるスペースが減る。「30 分で 4 割がツール出力」というのは、この⑤がたまり続けた結果だ。

ここまで分かると、この記事の2ツールはこの図のどこに割り込むかの違いだと一言で言える。

  • Context Mode … ④〜⑤に割り込む。道具の出力が巨大なら、そもそもノートに貼らずに要約だけ残す。
  • Headroom … ②の直前に割り込む。送る瞬間に、ノート全体をぎゅっと圧縮してから渡す。

以降の各ツールの「タイミング」は、この①〜⑤の番号を指していると思って読んでほしい。

1. ツール概要と比較テーブル

Headroom(chopratejas/headroom)

何か:LLM エージェント向けのコンテキスト圧縮エンジン

動作原理

messages → ContentRouter → 
  ├─ JSON → SmartCrusher (50-90% reduction)
  ├─ Code → CodeCompressor (AST解析, 40-70%)
  └─ Text → Kompress-base (ML model, 30-80%)

  CacheAligner (prefix stabilization)

  CCR reversible layer (originals in SQLite)

  LLM

主な特徴

  • タイミング②の直前(LLM送信直前 / post-execution)
  • 圧縮単位:メッセージシーケンス全体
  • デプロイ方法:Proxy, Library, MCP, CLI wrap
  • 復元性:CCR で完全復元可能(SQLite TTL 付き)
  • 可用性:言語・プラットフォーム非依存

CCR / SQLite TTL とは:Headroom の圧縮は原文を「捨てる」のではなく「畳んで隠す」方式だ。削った原文はローカルの SQLite に退避し、本文側にはプレースホルダだけを残す。この原文を退避して後から巻き戻せる可逆レイヤーが CCR(図の "CCR reversible layer")で、必要になればプレースホルダから原文をそのまま復元できる(lossless)。退避した原文には **TTL(Time To Live=保持期限)**が付いていて、一定時間で自動的に消える。これにより「あとから完全復元できる」一方で、SQLite が無限に肥大化しないようにしている。

(注:本記事では "CCR" を可逆レイヤーの呼称として扱う。正式な略称の展開は未確認なので、公開前に Headroom の README で確認のこと。)

実績

場面圧縮率
Code search (100結果)92%
SRE incident debugging92%
GitHub issue triage73%
Codebase exploration47%

Context Mode(mksglu/context-mode)

何か:Claude Code プラグイン + MCP サーバ。ツール出力の自動ルーティングと session tracking

動作原理

tool call

[PreToolUse hook] 

raw output ← tool execution

[PostToolUse hook] 判定:
├─ small → context に
├─ large → ctx_execute で処理
│          ↓
│        script 実行
│          ↓
│        ctx_index (FTS5)
│          ↓
│        summary だけ context に

└─ compacting → FTS5 search で復元

主な特徴

  • タイミング④〜⑤(ツール呼び出しの前後。hook で routing を強制 + イベント記録)
  • 処理単位:ツール単位のローカルスクリプト実行("think in code")
  • デプロイ方法:Claude Code plugin / OpenCode plugin など hook 対応エージェント + MCP
  • 復元性:FTS5 BM25 検索で復元(キーワードベース)
  • 可用性:16 プラットフォーム対応(Claude Code, OpenCode, Gemini CLI, Cursor など)

注意:Context Mode は Headroom のように「出力されたバイト列を透過的に圧縮する」のではない。hook が raw な Bash/Read/WebFetch を抑制してモデルに ctx_execute(スクリプト実行)を 書かせる よう仕向け、結果のサマリーだけを context に入れる。つまり「モデルの振る舞いを変える」アプローチである。

実績(README より):

場面削減
Playwright snapshot56 KB を context に入れない
GitHub Issues 20件59 KB → 集計サマリーのみ
session 全体315 KB → 5.4 KB(98%)
"think in code" 例47×Read (700 KB) → 1×ctx_execute (3.6 KB)
OpenCode セッション~98% saved(README 計測値)

比較テーブル

項目HeadroomContext Mode
圧縮対象既にcontextにある情報全体tool出力(contextに入る前)
圧縮アルゴリズムML model (Kompress-v2) + AST決定的なスクリプト実行
復元方法CCR tool で完全復元FTS5 search でキーワード検索
依存性MCP 不要(Proxy/Library で可)hooks が必要(自動化のため)
使用対象全LLM API (Anthropic/OpenAI/Bedrock)hook 対応エージェント 16種 (Claude Code / OpenCode 等)
output削減Verbosity steering + Effort routing— (prose-style は強制しない方針)
Session memoryCross-agent memorySQLite + hooks tracking
設定難易度低(headroom wrap 一発 or Proxy起動)低(plugin install)
圧縮率47-92% (場面依存)~98% (raw tool出力が大きい場合)

圧縮率は測定対象が異なるため直接比較できない。Headroom は「送信リクエスト全体のトークン削減率」、Context Mode は「raw tool 出力を context に入れないことによる削減率」を指す。レイヤーが違うので、後述の通り組み合わせると相補的に効く。


どちらをいつ使うか

Headroom が向いている場面

  • 複数ツール間で context を共有する必要がある
  • API response が JSON で深い(SmartCrusher 向き)
  • Code review や long diff を読ませたい
  • 複数のLLM provider を使い分けたい

Context Mode が向いている場面

  • rg --json, kubectl logs, git log など raw 出力が巨大
  • 同じ調査・デバッグを何度も繰り返したくない
  • session 中に「最後の編集ファイル」を自動検索したい
  • Claude Code での日常作業

両方組み合わせるべき場面

  • 長期 debugging session(Context Mode で即座に削減 + session 記憶)
  • Conversation compacting 後の resume(Context Mode で FTS5 検索 + Headroom で再圧縮)
  • multi-agent 運用(Headroom で cross-agent memory + Context Mode で Claude Code local routing)

2. 実践的な実装手順

2.1 Claude Code での Context Mode セットアップ

前提条件:Claude Code v1.0.33+

手順

# 1. Plugin install
/plugin marketplace add mksglu/context-mode
/plugin install context-mode@context-mode

# 2. Reload
/reload-plugins

# 3. Verify
/context-mode:ctx-doctor
# ✅ RuntimeCheck, HooksCheck, FTS5Check all pass

自動的に有効になること(plugin が 4 hook を登録):

  • SessionStart hook:routing 指示を runtime で注入(プロジェクトにファイルは書かない)
  • PreToolUse hook:raw な tool 呼び出しを routing(ctx_* へ誘導)
  • PostToolUse hook:実行結果(編集・git・error・決定)を session DB に記録
  • PreCompact hook:Compacting 直前に FTS5 へ index 化して resume 用 snapshot を準備
  • 11 個の MCP tools:6 sandbox(ctx_execute, ctx_search, ctx_index など)+ 5 meta(ctx_stats, ctx_doctor など)

設定ファイル(optional)

{
  "statusLine": {
    "type": "command",
    "command": "context-mode statusline"
  }
}

2.2 OpenCode での Context Mode セットアップ

OpenCode は TypeScript プラグイン方式に対応している。Claude Code のような /plugin マーケットプレイスではなく、opencode.json に plugin を登録する。

前提条件:Node.js >= 22.5(または Bun), OpenCode installed

手順

# 1. opencode.json に追記
#    (プロジェクト直下、またはグローバルは ~/.config/opencode/opencode.json)
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["context-mode"]
}

この plugin エントリだけで、11 個の ctx_* tool が in-process で登録され、hook が有効になる(OpenCode が context-mode の TS プラグインを直接呼ぶので、stdio MCP の子プロセスは立たない)。

# 2. (任意) routing rules ファイルをコピー
#    モデルがどの tool を使うべきか・どのコマンドがブロックされるかを認識する
cp node_modules/context-mode/configs/opencode/AGENTS.md AGENTS.md

# 3. OpenCode を再起動

# 4. Verify — セッション内で `ctx stats` と入力

注意点

  • OpenCode には本物の SessionStart hook が無い(sst/opencode#14808)。プラグインは experimental.chat.system.transform を代替として使い、routing block と前回セッションの snapshot を system prompt に注入する。
  • routing 強制は tool.execute.before / tool.execute.after で行われる。
  • 既存 config に plugin: ["context-mode"]mcp.context-mode の両方があると ctx_* tool が 0 個になる。context-mode upgrade で legacy MCP エントリを削除する。

2.3 Headroom の設定

Headroom は API のリクエストを透過的に圧縮するプロキシとして動く。ルーティングは HTTP プロキシ(http_proxy)ではなく、ANTHROPIC_BASE_URL / OPENAI_BASE_URL をローカルプロキシに向ける方式である点に注意。

前提条件uv。以下のいずれの方法でも、まず Headroom 本体を入れておく。

uv tool install "headroom-ai[all]"
# → headroom コマンドが PATH に入る
#   インストール確認: headroom --version

方法D(Library)でプロジェクトに直接組み込む場合は、ツールではなく依存として uv add "headroom-ai[all]" を使う。

以降の方法A〜Dは、この headroom が入っている前提で進める。

方法A:Claude Code を wrap(最も簡単)

# wrap で起動(proxy 起動 + ANTHROPIC_BASE_URL 設定を自動でやる)
headroom wrap claude
# → 内部で `ANTHROPIC_BASE_URL=http://127.0.0.1:8787` 相当を設定し Claude Code を起動

headroom wrap の対応エージェント:claude, codex, cursor, aider, copilot, gemini

方法B:Proxy を手動起動して任意のエージェントを向ける

# 1. Proxy 起動
headroom proxy --port 8787

# 2. エージェント側で provider の base URL をプロキシに向ける
#    Claude Code を手動で向ける場合:
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787

OpenCode は headroom wrap の対応リストに無い。OpenCode は OpenAI 互換クライアントなので、headroom proxy を立てて OpenCode の provider 設定(opencode.json の provider baseURL)をプロキシに向ける方式になる。この経路は本記事の検証対象(シナリオ参照)— 正確な設定は検証後に追記する。

方法C:MCP(Claude Code 向け)

headroom mcp install
# → Claude Code 再起動で headroom_compress / headroom_retrieve / headroom_stats が使える

方法D:Library(Python/TypeScript apps に直接組み込み)

from headroom import compress

messages = [...]
compressed = compress(messages, model="claude-sonnet-4-6")
# → 圧縮後のメッセージを Anthropic SDK に送信

2.4 組み合わせ運用(Context Mode + Headroom)

シナリオ:大規模コードベース調査

┌─ Codebase scanning task

├─ [Claude Code / OpenCode]
│  └─ User: "Find all uses of API X in the codebase"

├─ [Context Mode PreToolUse hook]
│  └─ Detects: rg --json output (5MB)
│  └─ Routes: ctx_execute で filter script に
│     ↓
│     (JavaScript)
│     const results = JSON.parse(input)
│       .filter(r => r.path.includes('src'))
│       .slice(0, 50)  // Top 50 only
│     console.log(`${results.length} matches found`)
│     results.forEach(r => console.log(`${r.path}:${r.line}`))
│     ↓
│     Output: 2.4 KB

├─ [ctx_index hook]
│  └─ FTS5に索引化(full results も保存)

├─ [次ターン送信前: Headroom proxy が request を圧縮]
│  └─ 圧縮対象は「送信リクエスト=これまでの会話履歴+残った tool 出力」
│  └─ SmartCrusher: JSON を 50-90% / CodeCompressor: AST を 40-70%
│  └─ CacheAligner: provider KV cache のヒット率向上

└─ [Output token reduction (optional, HEADROOM_OUTPUT_SHAPER=1)]
   └─ Verbosity steering / Effort routing でモデルの「書き戻し」を削減

結果(2つは別レイヤーなので相補的):
  - context に入る raw 量: Context Mode が削減(2.4 KB に)
  - 送信リクエストのトークン: Headroom がさらに圧縮
  - 出力トークン: Headroom output shaper が削減
  - Session DB: full results は FTS5 に indexed(ctx_search で後から復元)

ポイント:Context Mode はトランスクリプトに入る情報を減らし、Headroom は API へ送る/返るバイト列を圧縮する。動作レイヤーが違うため二重圧縮にならず、片方が削り残した分をもう片方が拾う。ただし両者とも "think in code" 的な思想を持ち、Headroom は内部で RTK(shell 出力書き換え)を同梱するため、シェル出力まわりは役割が一部重なる。

実装手順

  1. Context Mode 起動(Claude Code plugin)

    /context-mode:ctx-stats  # 確認
  2. Task 実行

    User: "Find all imports of ./utils in this repo"
    
    Claude Code (自動的に):
    → $ rg --json "import.*utils" src/
    
    Context Mode (自動):
    → PreToolUse: 出力サイズ > threshold → ctx_execute へ
    → ctx_execute で集計スクリプト実行
    → 結果 (2.3 KB) + indexed summary
  3. Compaction 後の resume

    Context Mode (自動):
    → PreCompact hook: FTS5 index を最新化
    
    User: "中断した調査を続けて"
    
    Claude Code:
    → ctx_search("utils imports") で過去の結果を検索
    → Session DB から前回の編集ファイル等を復元
  4. Headroom での最終圧縮(optional)

    # Context Mode plugin を有効にしたまま、Claude Code を Headroom 経由で起動
    headroom wrap claude
    # → ANTHROPIC_BASE_URL がプロキシに向き、送信リクエストが圧縮される
    #   Context Mode の hook/MCP はそのまま動く(レイヤーが違うため共存可能)

    共存時の注意:両方とも RTK/shell 出力の取り扱いに触れるため、headroom wrap 側の HEADROOM_CONTEXT_TOOL と Context Mode の routing が二重に効いていないか、/context-mode:ctx-statsheadroom perf の両方で実測して確認すること(検証シナリオ参照)。


まとめ(現時点)

ここまでを整理すると、2つのツールは競合ではなく別レイヤーで効く補完関係だ。

  • Context Mode は「そもそも context に入れない」。tool 出力が巨大なとき(rg --json, kubectl logs, CI ログなど)に直撃で効く。plugin を入れるだけで動き、Claude Code の日常作業で一番手軽。
  • Headroom は「送信直前に圧縮する」。会話履歴も残った tool 出力もまとめて削るので、Context Mode が削り残した分を拾える。headroom wrap claude で両立する。
  • 迷ったら まず Context Mode、効果が頭打ちになったら Headroom を被せる、という順番が分かりやすい。

一方で、本文中の数値(圧縮率・対応プラットフォーム数・ツール数・バージョン要件など)は、どちらのツールも更新が速く変わりやすい。公開前に各 README で最新の値を確認してほしい。

次章には、これらを実際に自分の環境で測るための検証シナリオを置いておく。数値は追って追記する。


3. 動作検証シナリオ(後で試験・更新用)

以下のシナリオで、実際の context 削減量を測定する。結果は追って本記事に追記する。

シナリオ 1:Kubernetes ログ調査(Context Mode メイン)

目的kubectl logs が大きい場合、Context Mode がどこまで削減できるか

検証環境

cluster: 任意の Kubernetes 環境(local k3d でも OK)
pod: エラーが頻出している pod(CrashLoopBackOff 推奨)

テスト手順

  1. Raw log サイズを記録

    kubectl logs pod-name > /tmp/raw.log
    wc -c /tmp/raw.log  # Size in bytes
  2. Claude Code で Context Mode 経由で分析

    User: "このpodのログを分析して、エラーパターンを教えて"
    
    Claude Code (自動):
    → $ kubectl logs pod-name
    
    # Context Mode が自動 intercept
  3. 削減前後を比較

    /context-mode:ctx-stats
    # per-tool breakdown, tokens consumed, savings ratio を記録

期待される結果

  • Raw log:5-50 MB
  • Context に入る情報:< 5 KB
  • 削減率:96-98%
  • Session DB に indexed

記録すべき数値

  • Raw log size (bytes)
  • Context token estimate (before)
  • Context token estimate (after)
  • Compression ratio (%)
  • FTS5 index size (bytes)
  • Session resume 時の search レスポンス時間

シナリオ 2:Codebase 検索 (Context Mode + Headroom)

目的:大型 codebase を検索するときに、Context Mode と Headroom が組み合わさるとどこまで削減できるか

検証環境

repo: 自分のプロジェクト(50+ files, 10k+ lines)
search: 特定の API / function の全呼び出し箇所

テスト手順

  1. 検索を実行(両方 OFF / ベースライン)

    # Context Mode plugin を外し、Headroom も経由しない素の Claude Code
    /plugin disable context-mode
    # ANTHROPIC_BASE_URL を設定せず、`headroom wrap` も使わずに起動
    
    User: "FunctionX を使っているすべてのファイルを見つけて"

    記録:

    • Tool calls count
    • Total input tokens
    • LLM response length (tokens)
  2. Context Mode のみ ON

    # Context Mode plugin は有効のまま
    
    # 同じ質問を繰り返す(session を reset)
    User: "[別の会話で] FunctionY を使っているすべてのファイルを見つけて"

    記録:

    • Tool calls count (削減されるか)
    • Input tokens estimate
    • ctx_index 処理時間
    • FTS5 index データ (bytes)
  3. Context Mode + Headroom proxy ON

    # Context Mode plugin を再有効化した上で、Headroom 経由で起動
    /plugin enable context-mode
    headroom wrap claude   # ANTHROPIC_BASE_URL がプロキシに向く
    
    User: "[別の会話で] FunctionZ を使っているすべてのファイルを見つけて"

    記録:

    • Tool calls count
    • Input tokens (Headroom 圧縮後)
    • Output tokens (Headroom output reduction)
    • Total savings %

期待される結果

項目OFFContext Mode+ Headroom
Tool callsNN (削減)N (削減)
Input tokensX0.3X0.15X
Output tokensYY0.7Y
Session DB

記録すべき数値

  • 各段階での tokens estimate
  • 圧縮率の差分(Context Mode vs Headroom)
  • Compaction trigger までの会話ターン数
  • Resume 時の search hit rate

シナリオ 3:CI ログ(複数 job)の原因抽出

目的:複雑な CI ログセットを、Context Mode で効率化できるか

検証環境

CI: GitHub Actions / GitLab CI
build: 失敗している複数 job のログセット(30-300MB 総量)

テスト手順

  1. 全 job log を fetch

    # GitHub Actions なら
    gh run download <run-id> --dir /tmp/logs
    du -sh /tmp/logs  # Total size
  2. Claude Code で分析(Context Mode OFF)

    # Context Mode を一時的に無効化(plugin を外す or 別プロファイルで起動)
    #   ※ HEADROOM_CONTEXT_TOOL は Headroom 側の CLI ツール選択変数であり
    #     Context Mode の ON/OFF とは無関係なので使わないこと
    /plugin disable context-mode   # ベースライン計測用
    
    User: "このCI runの失敗原因を分析して"
    # Claude Code が各ログを生で read(ベースライン)

    記録:

    • Tool invocations (read count)
    • Context token usage
    • Analysis 完了までの時間
    • Accuracy(指摘された根本原因が正しいか)
  3. Context Mode ON で再実行

    # session reset
    
    User: "このCI runの失敗原因を分析して"
    # Context Mode が自動 filtering

    記録:

    • Tool invocations count (削減率)
    • ctx_execute script の実行内容
    • Indexed log fragment count
    • Analysis 完了までの時間
    • Accuracy(結果は変わったか)

期待される結果

  • Raw logs:30-300 MB
  • Context に入る summary:< 10 KB
  • Tool calls:10+ → 2-3 に削減
  • Analysis 時間:短縮(ctx_search が高速)
  • 精度:変わらない(summary に問題の root cause は含まれているはず)

記録すべき数値

  • Raw log total size
  • Summary size after ctx_execute
  • Tool invocation reduction %
  • Time to analysis (before/after)
  • Accuracy score (1-10)
  • FTS5 search query examples

検証後の更新ポイント

試験後、以下を記事に追記してください:

  • 実測トークン数:理論値 vs 実測値(グラフ化推奨)
  • 圧縮率の内訳:Context Mode での削減 vs Headroom での削減を分離
  • 組み合わせ効果:単独 vs 組み合わせで、相加的か相乗的か
  • パフォーマンス影響:tool invocation time, script exec time
  • 精度への影響:削減後の LLM 判断精度は低下したか
  • 実用上の課題:予期しなかった問題点、回避策
  • 推奨運用:どの場面でどちらを使うべきか(更新版)
  • 限界ケース:圧縮で失敗するケース、workaround

DoS攻撃に役立つdnsdist設定集

はじめに

この記事は、富士通クラウドテクノロジーズ Advent Calendar 2023の9日目の記事です。8日目では、o108minminさんのN100で楽しい自宅インフラでした。

DoS攻撃に役立つdnsdist設定集

最近、ランダムサブドメイン攻撃(DNS水責め攻撃)がかなり活発的に発生している。この記事では、dnsdistというツールを用いた攻撃対策について、ざっくり解説する。

※1 dnsdistについて、もっと詳しい情報は、DNS Summer Day 2019で発表されたdnsdist - DNSトラフィックに特化したオープンソースの多機能ロードバランシンサーをご覧ください。 ※2 ランダムサブドメイン攻撃について、もっと詳しい情報や対策全般については、DNS Summer Day 2023で発表されたランダムサブドメイン攻撃についてをご覧ください。 ※3 記事は、断片的な情報が記載されているので、公式ドキュメントを読むことを推奨する。

必要な基礎知識

dnsdistの設定はluaで書くことができるため、非常に柔軟な処理を実現することができるが、この記事では、複雑なlua設定を紹介せず、一旦組み込み関数の活用方法について、簡単にご紹介したい。組み込み関数とは、dnsdistに最初から予め用意された関数だ。これらの関数をluaで呼び出したり、組み合わせたりするイメージで設定を記述する。DoS攻撃に有効な対策として、個人的には以下の2つあると思う。

  • cache: cacheを使うことによって、データベース参照処理を大幅に削減することできるため、攻撃には全く同じクエリを効率的に処理することができる
  • block:
    • ランダムサブドメイン攻撃では、ランダムな値が使われるため、cacheの有効性が下がる場合がある。それを補うことができるのはblockだ。
    • dnsdistはロードバランサーの役割をしているため、自分が受付した全体パケットを集計し、特定のルール(Packet Policies)に該当するパケットに対し、指定された処理(Packet Action)を行う。
    • ルールは静的ルールと動的ルールがある。

Packet Action

ルールでマッチングされたパケットに対し、様々処理(Packet Action)を適用することができる。イメージしやすいようにいくつの例を記載しておく。

処理の関数名処理の概要
DelayActionレスポンスを指定したミリ秒だけ遅延させる
DropActionパケットをDROPする
RCodeAction指定されたrcodeでクエリをレスポンスに変えて即座に返信する

上はほんの一部に過ぎないので、詳細はドキュメントを参照すること。

静的ルール

MaxQPSIPRule

  • クライアントからの流量を一定固定する場合に有効である。

  • 例えば、下の例では、IPv4アドレスごと、およびIPv6の/48ごとのトラフィックを測定し、そのようなアドレス(範囲)のトラフィックが5qpsを超えた場合、100ms遅延させる。

    addAction(MaxQPSIPRule(5, 32, 48), DelayAction(100))
  • 静的ルールなので、クライアントの条件に柔軟な対応が難しい場合があるので、無難な値を設定する必要がある。

  • しかし、無難な値である反面、防御効果は下がっしてしまうことがある。

  • 後述のDynBlockRulesGroup:setQueryRate()等と組み合わせるとこの弱点を改善することができるであろう。

MaxQPSRule

  • 上で紹介したMaxQPSIPRuleの適用範囲は、クライアント毎であるのに対し、この MaxQPSRule の適用範囲は、全体である。全体の流量の上限を指定できる。
  • 例えば、以下の例では、全体の流量を5qpsで固定する。5qpsから溢れたパケットは全部DROPされる。
    • MaxQPSRule(5) : 5qpsでパケットをマッチングする
    • NotRule(MaxQPSRule(5) でNOTを取り、5qps以上のパケットをマッチングする
addAction(NotRule(MaxQPSRule(5)), DropAction())
  • この設定は適用範囲が広いため、防御効果は高いものの、スケールできないので、工夫する必要がありそう。

RegexRule

  • クエリを正規表現でマッチングする

  • 正規表現なので、様々なPacket Actionと組み合わせると、かなり柔軟なことができる

    • DropActionで特定のクエリのパケットを即座にDROPする
    addAction(RegexRule("[0-9]{4,}\\.example$"), DropAction())
    • RCodeAction で特定のクエリのパケットを即座にレスポンスする
    addAction(RegexRule("[0-9]{4,}\\.example$"), CodeAction(dnsdist.REFUSED))
  • blacklistのクエリを作って防御したり、データベース参照処理なしで即座レスポンスしたりすることができる

QPSAction

  • 流量制限を超えたパケットをDROPする

  • MaxQPSIPRuleとMaxQPSRuleよりもやることがシンプルなので、様々な組み合わせ方で防御効果を図れる

  • 例えば、RegexRuleと同じようにblacklistの作成を実現できる

    addAction("h4xorbooter.xyz.", QPSAction(10))
    addAction({"130.161.0.0/16", "145.14.0.0/16"} , QPSAction(20))
    addAction({"nl.", "be."}, QPSAction(1))

動的ルール(Dynamic Rule)

静的ルールによるPacket Actionと違い、動的ルールによるPacket Actionは生存時間が短く、ルールによって生成された後、またルールによって削除される。平常時静的ルールで守り、DoS攻撃のような一時的な異常時は、一時的な防御措置を適用することできる。

maintenance() 関数

maintenance()はdnsdistにより1秒おきに呼び出される関数である。この関数の中で動的ルールを定義することで、毎秒動的ルールでパケットがマッチングされる。マッチする場合は、そのルールの定義通りにdnsdistは処理する。

function maintenance()
  -- 動的ルール
end

DynBlockRulesGroup クラス

dnsdist 1.3以降では、動的ルールは基本的に DynBlockRulesGroup クラスを使ってまとめて定義することが推奨される。

-- dbrを作成する
local dbr = dynBlockRulesGroup()
-- このdbrに必要な動的ルールをどんどん追加していく
dbr:setQueryRate(30, 10, "Exceeded query rate", 60)
dbr:setRCodeRate(DNSRCode.NXDOMAIN, 20, 10, "Exceeded NXD rate", 60)
dbr:setRCodeRate(DNSRCode.SERVFAIL, 20, 10, "Exceeded ServFail rate", 60)
dbr:setQTypeRate(DNSQType.ANY, 5, 10, "Exceeded ANY rate", 60)
dbr:setResponseByteRate(10000, 10, "Exceeded resp BW rate", 60)

--- maintenance関数ではapply関数だけかけば良い
function maintenance()
  dbr:apply()
end

上の例で示されたように、 DynBlockRulesGroup クラスには set*() メソッドがたくさんある。ランダムサブドメイン攻撃に役に立つメソッドをいくつかピックアップし紹介する。

setRCodeRate()

このメソッドはレスポンスのコード(rcode)をベースにマッチングすることができる。ランダムサブドメイン攻撃では、ランダムな値が使われるため、殆どの場合では、NXDOMAINやREFUSEDのrcodeになる。これらのrcodeがある程度の流量で、引き起こているクライアントからパケットをマッチングすると、効果的に防御することができる。

dbr:setRCodeRate(DNSRCode.NXDOMAIN, 20, 10, "Exceeded NXD rate", 60)
dbr:setRCodeRate(DNSRCode.SERVFAIL, 20, 10, "Exceeded ServFail rate", 60)

setRCodeRatio()

  • ランダムサブドメイン攻撃の場合、各クライアントからは 1 qpsで、断続的攻撃されると、setRCodeRate() では、指定した流量が極端に小さい値になってしまう。
  • 値が小さい場合は、攻撃元だけではなく、一般ユーザも巻き込まれてしまう可能性がある。
  • この弱点を克服できるのは、setRCodeRatio() メソッド。
  • setRCodeRatio() メソッドは流量をqpsではなく、割合(ratio)でマッチングする。
  • 例えば、ランダムサブドメイン攻撃の場合、1 qpsで攻撃してくるクライアントでも、NXDOMAINのrcodeの割合が 80% 以上になる。一方で、一般クライアントは正常なクエリで問い合わせするため、この割合は50%より低い可能性が高いと考えられる。
  • このように割合で動的ルールを設定すると、攻撃はのクライアントと正常なクライアントの区別の精度がより高まる。
dbr:setRCodeRatio(DNSRCode.NXDOMAIN, 0.8, 10, "Exceeded NXD rate", 60)

まとめ

dnsdistを使ってランダムサブドメイン攻撃等DoS攻撃の防御方法について、浅く紹介した。静的ルールと動的ルールを使えば、ある程度攻撃の影響を限定することができる可能性があるが、もっと防御力を高めるためには、SaaS等を使用し、多重防止を構築することが重要だと思う。

最後に

10日目では、@ktakaakiさんのSwitchBotのBLEでデータを取ってみたです。お楽しみに!