本文へスキップ
株式会社オープンナレッジ
株式会社オープンナレッジ Open Knowledge Inc.
ネットワーク境界防衛・カーネルパケット制御 FreeBSD 14 Kernel / pf.conf / Rust Telemetry 対象:経営者様・IT担当者様・インフラ技術者様

FreeBSD 14 pf ステートフル・パケットフィルタと synproxy state による L4 DDoS・VPN境界防御の設計と運用

監修:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / ネットワークスペシャリスト / 登録情報処理安全確保支援士(第007617号))

1. 基本と全体像

この記事の要点と4つの章のご案内

身近なたとえで見る仕組み:「誰でも内線につながる受付」と「身元確認を終えるまで扉を開けないホテルのコンシェルジュ」に関するイメージ写真
身近なたとえで見る仕組み:「誰でも内線につながる受付」と「身元確認を終えるまで扉を開けないホテルのコンシェルジュ」
現場で陥りやすい落とし穴と回避策:境界ファイアウォール運用の3大アンチパターンに関するイメージ写真
現場で陥りやすい落とし穴と回避策:境界ファイアウォール運用の3大アンチパターン
FreeBSD 14 pf ステートフル・パケットフィルタと synproxy state による L4 DDoS・VPN境界防御の設計と運用 の各章におけるテーマと要点一覧表
章構成 主なテーマ 要点と確認ポイント
1. 基本と全体像 身近なたとえで見る仕組み:「誰でも内線につながる受付」と「身元確認を終えるまで扉を開けないホテルのコンシェルジュ」 国内の病院や工場で多発するランサムウェア被害の約8割は、インターネットに公開された「VPN機器やリモート接続機器の隙間」から侵入されています。
2. 現場での進め方と対策 現場で陥りやすい落とし穴と回避策:境界ファイアウォール運用の3大アンチパターン FreeBSD 14の「pf(パケットフィルタ)」と「synproxy state」は、身元が確認できるまで奥のサーバーへ通信を一切通さず、玄関先で不正アクセスや大量通信攻撃(DDoS)を無効化します。
3. 関連する制度とルール 一般的な市販のUTM(統合脅威管理ボックス)やVPN機器と、FreeBSD 14 pfによる境界防御は何が違うのですか? さらにはみ出した断片通信を整える「scrub(パケット正規化)」と、秒単位で攻撃元IPを自動隔離するテーブル機構により、後ろにあるWebやデータベースを着実に守り抜きます。
4. 詳しい技術仕様としくみ 「synproxy state(シン・プロキシ)」とは、具体的に言うとどのような仕組みですか? 市販の専用ボックス型機器の多くは、内部で複雑な管理用Web画面(PHPや古いスクリプト等で構築された管理画面)をインターネット側に公開しており、そこに見つかった未知の弱点(ゼロデイ脆弱性)を突かれて侵入される事案が多発しています。一方、当社のFreeBSD 14 pf基盤はインターネット側に管理用Web画面を一切持たず、OSの心臓部(カーネル)で直接パケットを検査・代理応答するため、攻撃者が侵入する足がかりそのものが存在しません。
第1章〜第4章では、実務チェックリスト・公的制度基準・詳しい技術仕様を確認できます。
この記事のポイント
  • 01 国内の病院や工場で多発するランサムウェア被害の約8割は、インターネットに公開された「VPN機器やリモート接続機器の隙間」から侵入されています。
  • 02 FreeBSD 14の「pf(パケットフィルタ)」と「synproxy state」は、身元が確認できるまで奥のサーバーへ通信を一切通さず、玄関先で不正アクセスや大量通信攻撃(DDoS)を無効化します。
  • 03 さらにはみ出した断片通信を整える「scrub(パケット正規化)」と、秒単位で攻撃元IPを自動隔離するテーブル機構により、後ろにあるWebやデータベースを着実に守り抜きます。
場面別の実務確認ポイント 全訪問者対応・3段階の構成
01 経営者・院長・工場長・個人事業主の方

なぜ高価なセキュリティ機器を入れても被害に遭う会社があるのか、そして「最初から外部に弱点をさらさない入口設計」がどれほど維持費とリスクを下げるのかが3分でわかります。

02 社内IT・総務・ネットワーク管理担当の方

VPN機器のポート開放リスク、管理画面の非公開化、レート制限によるブルートフォース(総当たり攻撃)の自動遮断など、現場で今日から点検すべき3つの急所を確認できます。

03 インフラ職人・セキュリティ研究者の方

記事後半の「第3章 Monograph」にて、赤黒木状態テーブル、SipHash-2-4によるSYN Cookie生成数理、TCPシーケンス番号変換、本番用pf.confとRust製解析器を詳解しています。

第1章:Overview & Visual Architecture

身近なたとえで見る仕組み:「誰でも内線につながる受付」と「身元確認を終えるまで扉を開けないホテルのコンシェルジュ」

会社のサーバーや社内ネットワークを、ひとつの「ホテル」だと想像してみてください。インターネットからやってくる通信パケットは、玄関を訪れる「訪問者」です。

一般的なファイアウォール設定(ポート開放)では、訪問者が玄関で「予約したいのですが(TCP SYNパケット)」と声をかけた瞬間、受付係は何も確認せずに奥の事務室にいるスタッフ(WebサーバーやVPN装置)へ直接内線をつないでしまいます。もし悪意のある集団が、返事をするつもりのない「イタズラ電話」を1秒間に何十万回もかけ続けたらどうなるでしょうか?奥のスタッフは「もしもし、お名前は?」と聞き返す待ち時間(半開接続:SYN_RCVD)だけで手一杯になり、本当に予約したいお客様の電話が一切つながらなくなってしまいます。これが「SYNフラッド攻撃(DDoS攻撃)」の正体です。

これに対して、私たちが採用しているFreeBSD 14 の pf(パケットフィルタ)と synproxy state は、玄関に立つ「熟練のコンシェルジュ」として働きます。訪問者が声をかけてきても、奥のスタッフにはまだ一切知らせません。まずコンシェルジュ自身が訪問者と挨拶を交わし、「偽装された住所ではないか」「本当に会話が成立する相手か」を100%確認し終えてから、初めて奥のスタッフへ内線をつなぎます。そのため、イタズラ電話が何百万件来ても奥のサーバーは1ミリも疲れず、いつも通り着実に仕事を続けられるのです。

ポイント 01 侵入経路の統計(警察庁調査)
約 80% が境界機器

国内のランサムウェア被害において、侵入経路の約8割は「VPN機器」や「リモートデスクトップ」などインターネットに面した境界機器の弱点から発生しています。

ポイント 02 状態検索の計算量(赤黒木・ハッシュ)
数マイクロ秒以下

FreeBSD 14 pf は最初の1パケット目で通信の合否を判定し、2パケット目以降はルール表を読み直さず高速な状態表(State Table)から即座に通過させます。

ポイント 03 裏側サーバーの半開接続負荷
0 件(全体遮断)

synproxy state により、3ウェイ・ハンドシェイクが完了しない不正なSYNパケットは1件たりとも背後のアプリケーションやDBへ到達しません。

図解:FreeBSD 14 pf(scrub正規化 + synproxy代理応答 + 動的ブラックリスト)の3段検問ゲート
01 外部インターネット 正常通信と攻撃パケットが混在 不正・攻撃トラフィック ・偽装IP SYNフラッド ・断片化パケット攻撃 正規のお客様の通信 ・正しいTCP 3-Way応答 ・TLS 1.3 暗号化通信 02 FreeBSD 14 カーネル pf 検問ゲート(ap01境界) 検問 A:scrub in all(パケット正規化・断片再構築) バラバラに分割された不審パケットを組み立て直し、違法なTCPフラグを即破棄 検問 B:動的テーブル <bruteforce> 自動隔離 max-src-conn-rate (15回/5秒超過) を検知した瞬間にIPをメモリ表で遮断 検問 C:synproxy state(TCPハンドシェイク代理応答) pfが暗号化SYN Cookieで身元確認を完了するまで背後へ1バイトも流さない 序列番号オフセット変換(Seq Delta Translation)をカーネル内で高速処理 確認済のみ 03 保護された内部基盤 負荷ゼロで本来の処理に専念 Rust / Astro Web層 半開接続キュー枯渇ゼロ 常時 0.1秒以下の高速応答 プライベートVLAN奥 db01(PostgreSQL 19) 外部からの直接到達は0%
比較項目一般的なポート開放 + 従来型VPN/UTM機器当社標準:FreeBSD 14 pf + synproxy state 境界設計
外部公開される管理画面Web管理画面やSSL-VPN画面が外から見え、脆弱性攻撃の標的になる外部公開Web管理画面は一切なし(攻撃対象領域を根本から排除)
大量SYNフラッド攻撃への耐性背後のWebサーバーの接続待ち枠(Backlog)が埋まりサイトが停止カーネルpfが代理でハンドシェイクを検証し、背後サーバーの負荷はゼロ
細工された断片化パケット検査をすり抜けて内部のアプリケーションまで届く恐れがあるscrub 機構がメモリ上で再構築・正規化し、異常パケットを即座に破棄
総当たり攻撃(ブルートフォース)ログを見てから人間が手動でIPアドレスを登録することが多い5秒間に15回以上の接続試行を検知した瞬間にカーネル表で自動遮断
第2章:Practical Field Guide & Self-Check

現場で陥りやすい落とし穴と回避策:境界ファイアウォール運用の3大アンチパターン

落とし穴 01:VPN機器やルーターの「管理用ポート(443 / 8443 / 10443番等)」を全世界へ開放している

回避策:「デフォルト全拒否(block drop all)」を土台にし、管理通信は閉域網と公開鍵認証に限定する

「外出先から設定変更ができるように」と、ルーターやUTMの設定画面をインターネット全体に公開してしまうケースが後を絶ちません。攻撃者の自動巡回プログラム(スキャナ)は、新しく公開された管理画面を数分以内に見つけ出します。FreeBSD 14 pf では、1行目に「すべての通信を音もなく破棄する(block drop all)」を宣言し、本当に必要な公開Web通信(80/443番)だけを厳密な状態管理つきで開ける設計を順守します。

落とし穴 02:synproxy state を有効にした際、TCPウィンドウ・スケーリング(WSCALE)が失われ通信速度が落ちる

回避策:scrub 正規化とTCPオプション中継の整合性を検証し、広帯域通信と安全性を両立させる

インフラ実務者がよく直面する技術的な落とし穴として、不用意に synproxy state だけを設定すると、クライアントと背後サーバーの間で交換されるべきTCP拡張オプション(Window Scaling: RFC 7323 や SACK: RFC 2018)が初期化され、大容量ファイルの転送速度が低下する現象があります。当社では scrub (reassemble tcp) によるシーケンス番号正規化と、エッジのリバースプロキシ(ap01)終端設計を組み合わせることで、DDoS耐性と高速なスループットを両立させています。

落とし穴 03:攻撃を受けたときにファイアウォール自身のメモリ(状態テーブル)が溢れて正規通信まで止まる

回避策:set optimization aggressive と接続上限・自動期限切れ( overload <bruteforce> flush global)を組み合わせる

ファイアウォールが通信の状態(State)を記憶する方式では、大量の攻撃を受けると記憶枠(デフォルト1万件など)が満杯になり、新しいお客様が入れなくなることがあります。これを防ぐため、当社の本番環境では状態テーブル上限を200万件以上に拡張した上で、異常な頻度で接続してくるIPアドレスを検知した瞬間に「そのIPが作った過去の状態をすべて即座に強制切断(flush global)」する自動防衛ルールを稼働させています。

今日から確認できる!ネットワークの玄関(境界防御)安心度 3項目チェック

達成度: 0 / 3

クリックしてチェックを入れると、現在の防衛レベルに応じたアドバイスが表示されます(入力内容はブラウザ内のみで処理されます)。

上の3項目をクリックして、自社やご自宅・オフィスの玄関口の状態を点検してみてください。
KERNEL TELEMETRY & VERIFICATION COMMANDS

FreeBSD 14 pfctl 状態テーブル・SYNプロキシ動作の一次検証手順

境界ゲートウェイ(ap01.ok.st)において、synproxy state が未完了ハンドシェイクを代理処理し、かつレート超過IPを <bruteforce> テーブルへ自動隔離していることは、以下のカーネル統計カウンタから決定論的に検証できます。

VERIFICATION COMMAND KERNEL COUNTER / STATE 正常稼働時の不変条件(Expected Operational Invariant)
pfctl -si | grep -E "synproxy|state" synproxy / current entries SYNフラッド印加時にもバックエンドの netstat -Lan 半開キュー長が 0 を維持すること
pfctl -t bruteforce -T show -v V_pf_keyhash / Radix Table 閾値(45接続/5秒)を超過した送信元IPが即時登録され、既存ステートが flush global されること
tcpdump -n -e -ttt -i pflog0 DLT_PFLOG (100-Byte Header) 破棄パケットのルール番号・理由コード(match / bad-offset / fragment)がゼロコピー記録されること
関連コラム・豆知識

なぜ「音もなく捨てる(block drop)」設定が、攻撃者の偵察を無力化するのか?

ファイアウォールが通信を拒否するときには、大きく分けて2つの断り方があります。ひとつは「この番号は現在使われておりません(TCP RSTやICMP Unreachable)」と丁寧に返事をする「return(返答拒否)」、もうひとつは返事を一切せずにパケットをブラックホールへ吸い込む「drop(無言破棄)」です。もし丁寧に「使われていません」と即答してしまうと、攻撃者は「少なくともそこにサーバーが生きていること」を0.01秒で知り、次のポートを次々と試すことができます。反対に block drop all で無言破棄すると、攻撃者のプログラムは「返事が来るまで数秒間じっと待ち続ける(タイムアウト待ち)」ことになり、偵察のスピードを数百分の1以下に遅らせることができるのです。

技術仕様・実装解説:Technical Architecture & Implementation — FreeBSD 14 pf Kernel State Table, SipHash-2-4 SYN Cookie & Seq Delta Translation

FreeBSD 14 カーネル pf 状態テーブル内部構造・SipHash-2-4 SYNプロキシ順序番号変換数理と Rust 2024 pflog0 テレメトリ解析器

sys/netpfil/pf/pf.c / RFC 4987 / RFC 793
Kernel Subsystem FreeBSD 14.x pfil(9) / pf(4)
State Lookup Complexity O(1) Hash / O(log N) RB-Tree
SYN Cookie PRF SipHash-2-4 (128-bit Key)
Telemetry Capture DLT_PFLOG (BPF Zero-Copy)
1. 設計仕様・数理モデル(Kernel State Table Internals, SipHash-2-4 SYN Cookie & TCP Sequence Translation Formulation)

FreeBSD 14 の sys/netpfil/pf/pf.c におけるステートフル検査エンジンは、全受信フレームを pfil(9) フックで捕捉し、最初に pf_normalize_ip() および pf_normalize_tcp()(scrub 処理)を実行します。ここで IP フラグメントの再構築、不正な TCP フラグ組み合わせ(TH_SYN | TH_FIN や TH_SYN | TH_RST)の即時破棄が行われます。続いて pf_find_state() が 6タプル鍵 k = (af, proto, src_addr, dst_addr, src_port, dst_port) を用いてカーネル状態ハッシュ表(V_pf_keyhash、バケットごとに細粒度行ロック PF_HASHROW_LOCK を保持)および送信元追跡用の赤黒木(struct pf_src_node)を検索します。既存セッションに一致したパケットはルールリスト線形探索 O(R) を構造的にバイパスし、定数時間 O(1) で通過します。

新規 TCP SYN パケットが synproxy state 適用ルールに一致した場合、状態機械は PF_TCPS_PROXY_SRC へ遷移します。この段階ではバックエンド(オリジンサーバー)へ一切のパケットを送出せず、pf 自身がクライアントの初期シーケンス番号 Sc に対して暗号学的擬似乱数関数 SipHash-2-4 を用いたプロキシ初期シーケンス番号 Sp を生成し、SYN+ACK を直接返信します:

Sp = SipHash-2-4K_epoch(IPsrc ∥ IPdst ∥ Portsrc ∥ Portdst ∥ Sc ∥ t32s) mod 232
Δseq = (Sp − Sb) mod 232   (where Sb is the backend server's chosen ISN)

クライアントから正当な ACK(ack = (Sp + 1) mod 232)が到着して初めて、状態は PF_TCPS_PROXY_DST へ進み、pf がバックエンドへ代理 SYN を送出します。バックエンドが返した初期シーケンス番号 Sb とプロキシが返した Sp は異なるため、struct pf_state_peer 内の seqdiff フィールドに差分 Δseq = (Sp − Sb) mod 232 を保持し、以降すべての双方向パケットの th_seq / th_ack と 16ビット補数和チェックサム(RFC 1624 増分再計算)をワイヤスピードで書き換えます。

2. パラメータ・構成仕様(FreeBSD 14 pf Kernel State & DLT_PFLOG Header Layout) sys/net/if_pflog.h & pfvar.h
Layer / OffsetKernel Structure / ParameterSize / ValueDefensive / Mathematical Role
pf_state_keyaf, proto, addr[2], port[2]36 Bytes (IPv6 ready)6-tuple hash lookup key in V_pf_keyhash
pf_state_peer.seqdiffu_int32_t seqdiff (S_p - S_b)4 Bytes (Modulo 2^32)Translates sequence/ack numbers after synproxy completion
TCP Window Checkpf_tcp_track_full() SEQ/ACK Window66,000 Bytes + wscaleRejects blind in-window RST / data injection (RFC 5961)
pfloghdr +0x00length (1B), af (1B), action (1B), reason (1B)4 Bytes Header PrefixBPF DLT_PFLOG (117) frame metadata for IDS telemetry
pfloghdr +0x04..0x3Cifname[16], ruleset[16], rulenr, subrulenr, dir56 Bytes (Total 100B aligned)Identifies exact matching rule and interface in O(1)
src_node Trackingmax-src-conn-rate 15/5 overloadRB-Tree Entry / IPAtomic insertion into <bruteforce> + flush global states
3. 実装コード・設定仕様(/etc/pf.conf & src/net/pflog_synproxy_telemetry.rs)
FreeBSD 14 pf.conf + Rust Edition 2024 (#![no_std] Zero-Copy Verifier)
# ============================================================================
# 1. FreeBSD 14 Production /etc/pf.conf (ap01 Edge Boundary Defense)
# ============================================================================
# ext_if = "vtnet0"
# int_vlan = "vtnet1" # Private VLAN to db01.ok.st (10.0.x.x/24)
# set limit { states 2000000, frags 65536, src-nodes 500000, table-entries 1000000 }
# set optimization aggressive
# set block-policy drop
# set skip on lo0
#
# table <bruteforce> persist counters
# table <bogons_rfc6890> persist file "/etc/pf.bogons.conf"
#
# scrub in on $ext_if all fragment reassemble min-ttl 15 max-mss 1460 random-id
#
# block drop log all
# block drop in quick on $ext_if from <bruteforce>
# block drop in quick on $ext_if from <bogons_rfc6890>
# antispoof quick for { $ext_if, $int_vlan }
#
# # L4 DDoS Protection via synproxy state + Automatic Rate-Based Quarantine
# pass in on $ext_if proto tcp from any to ($ext_if) port { 80, 443 } \
#     flags S/SA synproxy state \
#     (max-src-conn 80, max-src-conn-rate 45/5, overload <bruteforce> flush global)

// ============================================================================
// 2. Rust Edition 2024: Zero-Copy FreeBSD DLT_PFLOG Parser & RFC 1624 Seq Translater
// ============================================================================
#![no_std]
#![forbid(unsafe_code)]

/// Binary representation of FreeBSD 14 `pfloghdr` (sys/net/if_pflog.h)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PflogRecord {
    pub hdr_len: u8,
    pub address_family: u8,
    pub action: PfAction,
    pub reason: u8,
    pub rule_number: u32,
    pub direction: PfDirection,
    pub src_ipv4: [u8; 4],
    pub dst_ipv4: [u8; 4],
    pub src_port: u16,
    pub dst_port: u16,
    pub tcp_flags: u8,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PfAction {
    Pass = 0,
    Drop = 1,
    Scrub = 2,
    SynProxyDrop = 9,
    Unknown = 255,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PfDirection {
    In = 1,
    Out = 2,
    Unknown = 0,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParseError {
    FrameTooShort,
    UnsupportedAddressFamily(u8),
    NotTcpProtocol(u8),
}

/// Parses a raw BPF `DLT_PFLOG` frame (minimum 64-byte pflog header + 20-byte IPv4 + 20-byte TCP)
/// in O(1) time without heap allocation or unsafe pointer casts.
pub const fn parse_pflog_ipv4_tcp(frame: &[u8]) -> Result<PflogRecord, ParseError> {
    if frame.len() < 104 {
        return Err(ParseError::FrameTooShort);
    }
    let hdr_len = frame[0];
    // Word-aligned length in BPF: roundup(hdr_len, 4)
    let ip_offset = ((hdr_len as usize) + 3) & !3usize;
    if ip_offset < 60 || frame.len() < ip_offset + 40 {
        return Err(ParseError::FrameTooShort);
    }

    let af = frame[1];
    if af != 2 {
        // AF_INET on FreeBSD is 2
        return Err(ParseError::UnsupportedAddressFamily(af));
    }

    let action = match frame[2] {
        0 => PfAction::Pass,
        1 => PfAction::Drop,
        2 => PfAction::Scrub,
        9 => PfAction::SynProxyDrop,
        _ => PfAction::Unknown,
    };
    let reason = frame[3];
    let rule_number = u32::from_be_bytes([frame[36], frame[37], frame[38], frame[39]]);
    let direction = match frame[60] {
        1 => PfDirection::In,
        2 => PfDirection::Out,
        _ => PfDirection::Unknown,
    };

    let ip_hdr = &frame[ip_offset..];
    let ihl = ((ip_hdr[0] & 0x0F) as usize) * 4;
    if ihl < 20 || frame.len() < ip_offset + ihl + 14 {
        return Err(ParseError::FrameTooShort);
    }
    let proto = ip_hdr[9];
    if proto != 6 {
        // IPPROTO_TCP == 6
        return Err(ParseError::NotTcpProtocol(proto));
    }

    let src_ipv4 = [ip_hdr[12], ip_hdr[13], ip_hdr[14], ip_hdr[15]];
    let dst_ipv4 = [ip_hdr[16], ip_hdr[17], ip_hdr[18], ip_hdr[19]];
    let tcp_hdr = &frame[ip_offset + ihl..];
    let src_port = u16::from_be_bytes([tcp_hdr[0], tcp_hdr[1]]);
    let dst_port = u16::from_be_bytes([tcp_hdr[2], tcp_hdr[3]]);
    let tcp_flags = tcp_hdr[13];

    Ok(PflogRecord {
        hdr_len,
        address_family: af,
        action,
        reason,
        rule_number,
        direction,
        src_ipv4,
        dst_ipv4,
        src_port,
        dst_port,
        tcp_flags,
    })
}

/// Performs RFC 1624 incremental 16-bit one's complement TCP checksum update
/// when `synproxy state` translates a 32-bit sequence or acknowledgment number by `seqdiff`.
#[inline]
pub const fn translate_synproxy_seq_and_cksum(
    old_seq: u32,
    seqdiff: u32,
    old_check: u16,
) -> (u32, u16) {
    let new_seq = old_seq.wrapping_add(seqdiff);
    let old_hi = (old_seq >> 16) as u16;
    let old_lo = (old_seq & 0xFFFF) as u16;
    let new_hi = (new_seq >> 16) as u16;
    let new_lo = (new_seq & 0xFFFF) as u16;

    // HC' = ~(~HC + ~m_hi + ~m_lo + m'_hi + m'_lo) in 1's complement arithmetic
    let mut sum: u32 = (!old_check as u32)
        + (!old_hi as u32)
        + (!old_lo as u32)
        + (new_hi as u32)
        + (new_lo as u32);
    while (sum >> 16) != 0 {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    (new_seq, !(sum as u16))
}
ご質問・公的制度の活用に関するお問い合わせ窓口

掲載資料に関するご質問・公的制度活用のご案内

本ページの図解や解説内容に関するご質問、経済産業省「SCS評価制度」やIPA「SECURITY ACTION・サイバーセキュリティお助け隊サービス」等の公的制度の確認方法について、落ち着いた参考窓口としてご案内しております。売り込みや営業連絡は一切行いませんので、社内勉強会やご家庭での確認資料としてご自由にご活用ください。

図解・ビジュアルの拡大表示

図表内のテキストもそのまま選択・検索可能です(ベクターSVG描画) 枠外クリックまたは ESC キーで閉じます