Encrypted Client Helloを設定した
このサイトは自作のHTTP/2サーバで運用している。そのサーバにEncrypted Client Helloを試験的に導入してみた。ChromeであればDevToolsのSecurityパネルでECHが有効になっているか調べることが出来る。
自作HTTP/2サーバはTLSのバックエンドを切り替えられるように実装していて、Rustls, OpenSSL, BoringSSLを使えるようにしている。今のところECHが使えるのはBoringSSLだけなので今回はBoringSSLを使った。手順は以下の通り。
- ECHConfigおよび公開鍵暗号の鍵ペアを作る
- サーバ側でBoringSSLにECHConfigと秘密鍵を設定する
- DNSのHTTPSリソースレコードにECHConfigListを設定する
ECHConfigの作成
TLS Encrypted ClientHello(ECH) を BoringSSLで試してみるに書いてある通りにBoringSSLをビルドしてbssl generate-ech
を実行してECHConfig、秘密鍵およびECHConfigListの3つを作る。前者二つはサーバへ設定するプライベートなもので、3つめのECHConfigListはクライアントへ公開するためのもの。なおbssl generate-ech
の使い方はこのコマンドが追加されたコミットのメッセージに書かれている。
サーバの設定
サーバを変更して前段で作ったECHConfigと秘密鍵をBoringSSLのAPIを通して設定する。使うAPIは以下のbssl
のサーバ実装を参照した。
bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
bssl::ScopedEVP_HPKE_KEY key;
if (!keys ||
!EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(),
ech_key.data(), ech_key.size()) ||
!SSL_ECH_KEYS_add(keys.get(),
/*is_retry_config=*/1, ech_config.data(),
ech_config.size(), key.get()) ||
!SSL_CTX_set1_ech_keys(ctx.get(), keys.get())) {
fprintf(stderr, "Error setting server's ECHConfig and private key\n");
return false;
}
自作サーバはRustで書いているのでこれらのAPIをbinding経由で使う。
DNS HTTPSリソースレコードの登録
サーバ側の準備ができたのであとはクライアント(ブラウザ)へECHの設定を公開する。2024年2月の時点では、FirefoxおよびChromeはECHが使えるかどうかを判断するのに提案されているDNS HTTPSリソースレコードのech
パラメータを使っている。このパラメータに最初のステップで作ったECHConfigListをbase64でエンコードした値を設定する。
設定の確認
あとはブラウザからサーバへ接続してECHが有効かどうかを調べる。Wiresharkで見てみるとSNIがbssl generate-ech
で指定した公開用のドメインになっていて、かつ encrypted_client_hello (0xfe0d, 65037) extensionが送信されているのが分かる。
その他
ECHの概要についてはFirefoxのヘルプやCloudflareのブログポストが分かりやすい。
ECHが採用しているHPKEについてはHPKE とは何かが参考になる。
Nuphy Gem80 - 2024/02/11
FF16クリア - 2024/01/29
ちまちまと進めていたFF16をようやくクリアした。感想を少しだけ。
もっと読む
プレイ時間は73時間(放置してる時間も多かったから実際はもっと少なそう)。サブクエストはレベル上げをかねて全部やった。リスキーモブやその他のやりこみは触らず。
ゲーマーには低難易度らしいけど自分にはアクションが難しく進めるのに苦労した。ただこれは常に晩酌しながらのプレイだったせいで、素面でやったら難易度の印象は変わると思う。アクセサリで難易度を調整でき、オートなんちゃらをつけてればボタンをポチポチするだけで進められる。自分は変なプライドがあってオート系はオートトルガルだけ使ってた。ボス戦ではゲームオーバーを繰り返してたけど、再開ポイントが豊富に用意されていて再開後にポーションも補充されるので時間のロスは少ない。ネット上のレビューや感想で書かれていたけどこれらは離脱しないようにする工夫らしく、なるほど、と感心した。召喚獣戦は演出が素晴らしい一方、ゲーム性としてはいまいち。というか酔っていると急に別ゲーに変わられると対応できない。
時間と予算、ターゲットをきっちり決めて作ってるんだろうなあと思わせる作り。レビューによるとこれはFF15の反省らしい。ストーリー、アクション、FFという看板IPに寄せられる期待、古参へのサービス、新規顧客の開拓、コアゲーマーへの配慮。言われてみれば確かに時間と予算を念頭に入れてこれらをバランスよく配分した印象を受ける。
ストーリーや世界観は松野ファンにとっては新鮮味はなく、ちょろっとゲームオブスローンズ的な要素を足したような感じ。個人的には随所に現れるFF3ファンに対するサービスがよかった。FF3は子供のころにやりこんだ思い入れのある作品。アルテマが召喚獣をFF3の名前で呼んだり浮遊大陸がでてきたりしたあたりは胸が熱くなった。
2023年の内に終わらせたかったけどなんとかクリアできてよかった。後半はぐっと楽しくなって終わってしまったのが少し寂しいが、とりあえずここまで。次はゼルダTotKのクリアを目指す。
2023年の振り返り
環境の変化に伴い、まとまった時間をとるのが難しいと感じる一年だった。
仕事
色々と手を出したけどどれも具体的な成果につながらない、さっぱりしない感じだった。プロダクトの成熟に伴い、プロセスは重く、チーム間のやり取りはより政治力を要するようになった。エスカレーションへの対応も多く、割り込み作業に多くの時間を使った。
IETFに参加するようになった。ネットワークプロトコルは学生のころからずっと興味のある分野だし、参加してみたいと思っていたので長年の願いが叶った感じ。ただ、今年やっていたメインのプロジェクトたちはネットワークプロトコルとは関係がなかったので仕事としてはほとんど時間を割けなかった。頑張って上司たちへpitchした結果、次にやろうとしているプロジェクトではDNSやHTTPを触る予定。
今年前半はレイオフ騒ぎがあり浮ついていた。さっぱりしなかったのはそのせいもあるのかもしれない。
生活
適応障害は快癒したと思う。抵抗感を振り払って心療内科に行き、処方された薬を服用したのが良かった。ふとしたきっかけで揺り戻しが来ることもあるけど、月日を重ねれば大丈夫かなという感触がある。家族の世話をする時間が多くなり時間管理が難しくなった。自分の勉強や趣味に使える時間が減ったことに対して四苦八苦している。毎日の晩酌をやめればもっと多くの時間を確保できるはずだけど、中毒者なのでそれは無理なんだよな…。
IETFや出張で海外へ二度行った。コロナ禍以降数年ぶり。プラハとボストン。どちらも良かった。
余暇
余暇時間のほとんどをネットワークプロトコルの理解や動向のキャッチアップに充てていた。 RFCやI-D読んだり既存のコードベース読んだり。 インプットが多くてアウトプットがほとんど無かった。 それでもまだ議論に参加するには全然理解とインプットが足りない。 半年ROMってろと言われた感じだと言えば伝わるだろうか。 ここは積み重ねが必要なんだろう。
趣味コーディングほぼ無し。GitHubに草が生えてないのがそれを物語ってる。Reactを使ってNPRのオーディオを手軽にリピート/ブックマークできる、英語聞き取り練習のためのウェブアプリを作ったぐらい。
読んだと言えそうな本のリスト(除く漫画)。ネットワーク関連以外は隙間時間に読んでいたので自己啓発や娯楽書の類が多い。
- 能力はどのように遺伝するのか 「生まれつき」と「努力」のあいだ (ブルーバックス)
- 英語は10000時間でモノになる ~ハードワークで挫折しない「日本語断ち」の実践法~
- 100 Tricks to Appear Smart In Meetings
- 完全無欠の問題解決: 不確実性を乗り越える7ステップアプローチ
- 私たちはどう学んでいるのか ――創発から見る認知の変化 (ちくまプリマー新書)
- プログラマーのためのCPU入門
- 暗号と認証の仕組みと理解がしっかりわかる教科書
- プロフェッショナルIPv6 第2版
- ピアリング戦記 ― 日本のインターネットを繋ぐ技術者たち
- 世界一流エンジニアの思考法
- The Staff Engineer’s Path
- Bulletproof TLS and PKI
ゲームはゼルダTotKとFF16を細々とやっている。
英語
喋るのと聞き取るのができないのを改めて痛感したのでn度目の英語練習を始めた。
今年はアプリを使って練習し始めた。 基礎力をつけるためにスタディサプリ(新日常英会話)。 喋るのにはELSA Speakとスピーク。 これらを毎日少しづつこなしている。
喋る練習にAIが使えるようになったのが大きい。 スピークのAI会話が自分のレベルには合っていると思う。 覚えたいフレーズや単語の復習をどうやるか悩んでいたけど、Ankiでカードをちまちま作る方向で考えている。
聞き取りの練習はどうしたらいいのか試行錯誤している。 先述の自作ウェブアプリを使って通勤中にNPRを聞き取れるまでリピートするのを続けてたけど認知負荷が高くて挫折してしまった。 星さんが取り組んでいるComprehensible Inputを試してみようかなと思っている。
アプリを使った練習を始めて数か月たった。英語力が上がった実感は今のところない。 学習とは継続なり。日々の生活に無理なく組み込める負荷でやっていきたい。
運動
在宅の時はFit Boxing 2を引き続きやっている。出社したときは30分ぐらいジョギングするようになった。以前と比べると運動量はだいぶ減った。空腹によるストレスをなくすために食事も以前より食べるようになったので体重が増えつつあるが、これはもう気にしないことにした。
2024
振り返り以外のブログエントリを書けるといい。
2022の振り返り
あっという間の一年だった。夏以降に鬱をやらかしてしまって時間が溶けた。だいぶん復調して年末を迎えている。
仕事
年初めからしばらくは去年実装した Early Hints を ship する作業をやっていた。長いことブラウザ開発に関わってきたけど HTML/Fetch の仕様を議論してPRを出すというのは今回が初めてだった。仕様に加筆するというのは実績解除として嬉しかったものの、やっていることは抽象化レイヤをまたぐ情報の plumbing で普段書いているコードを想起してしまい、この手の作業はもういいかな、という気分になった。無事出荷できたし、Shopify に使ってもらったりもしたので達成感のあるプロジェクトになった。
そのあとは HTTP Cache を速くできないかねえとメモリキャッシュを導入する試験実装をやったりしていた。今のところ結果は芳しくないけど、副作用として //services/network の owner になれたのは良かった。
夏以降はほとんど働かず。
生活
夏に子が生まれ生活がガラリと変わった。トラブル続きもあって適応障害みたいになってしまった。長い時間をかけて受け入れていくしかない事柄もあったりして、一時期は全く動けない状態だった。この時期に話を聞いてくれた人たちに感謝している。先生に診てもらって加療を始めたのでだいぶ落ち着いた。
趣味コーディング
仕事のコーディングが面白かったり後半は動けなかったりで、ろくなものは書いていない。小さな VSCode の extension を書いたり、mojom (Chromium の IPC 用 IDL) から C++ の定義や参照を探すコードを書いたりしていた。
運動
激減した。それでもセロトニンを出すべしと朝にステップ運動をしている。あと Fit Boxing 2 も継続してやっている。
ゲーム
コードを書く集中力が無くなってしまったのでゲームをやる時間が増えた。トライアングルストラテジーが面白かった。この手のジャンルがもう少し売れるようになるといいのになあ。年末に出たタクティクスオウガ リボーンもやっている。こちらも面白い。
短いけどこんなところで。来年は状況を受け入れつつ出来る範囲でやっていこうと思う。だいぶ元気になってきたので大丈夫だろう。
Tauri を試す - 2022/07/13
少し前に Tauri という Electron 風ツールキットの 1.0 リリースが出た。WebView を使うので Chromium がくっついてこない。バイナリサイズが小さくて済みそうだ。開発体験はどんな感じかと以前 Electron で作ったツールを Tauri を使って書き直している。
ツールは単純なもので、ドラッグ&ドロップされたファイルの文字コードを変換するだけ。GUI 操作だけで日常作業を手早く済ませたいと要望を受けて作ったもの。単純だけど helloworld よりはやる事がある。手間取ったところいくつかがあるので書いておこうと思う。
まずはドラッグ&ドロップ。Electron だとWeb アプリと同じように drop
を listen すれば良い。
const { ipcRenderer } = require("electron");
const dragEl = document.getElementById("drag-target");
dragEl.ondrop = event => {
const files = event.dataTransfer.files;
let paths = [];
for (let i = 0; i < files.length; i++) {
paths.push(files[i].path);
}
ipcRenderer.send("convert", paths);
};
Tauri では独自の API を使わないとドラッグ&ドロップを処理できない。
// dist/main.js
const tauri = window.__TAURI__;
tauri.event.listen("tauri://file-drop", (event) => {
const paths = event.payload;
tauri.invoke("convert", paths);
});
onFileDropEvent()
を使ってもいい。
次は設定の保存。ウィンドウの位置やサイズ、元ファイルを上書きしたいか、などの情報を保存して次回起動時に復元したい。以下のステップを使って実現している:
- 設定を保存し後で復元するための設定ファイルのパスを求め、
- 実行中にウィンドウの位置やサイズの変更を検知しつつ、
- WebView上で起きたイベント (
<input>
の値の変化) をホストプロセスへ通知、 - アプリが終了する前に情報をファイルへ書き出す。
順番に見ていく。
設定ファイルのパス
Electron で作ったツールではホストプロセスから app.getPath("userData")
を呼んで設定ファイルを保存するディレクトリを取得していた。Tauri は同等の API を公開していない様子。公開しているのかもしれないが自分がざっと見た感じでは見つけられなかった。
ホストプロセスは Rust で書かれたアプリケーションに過ぎないので Tauri が提供する API に縛られる必要はない。適当な crate を使って設定ファイルのパスを決めればいい。依存する crate を減らすために既に Tauri が依存している dirs-next
を使う。
// src-tauri/src/main.rs
fn settings_path() -> std::io::Result<Option<PathBuf>> {
let config_path = match dirs_next::config_dir() {
Some(path) => path,
None => return Ok(None),
};
let config_path = config_path.join("my-tool");
fs::create_dir_all(&config_path)?;
let settings_path = config_path.join("settings.json");
Ok(Some(settings_path))
}
ウィンドウイベント処理
ウィンドウの位置やサイズの変更はホストプロセス、レンダラプロセスどちらからでも検出できる。
ホストプロセスでウィンドウイベントを検出する場合:
let settings = read_settings(settings_path())?;
let settings = Arc::new(Mutex::new(settings)); // (*) 後述
tauri::Builder::default()
.on_window_event(move |global_event| match global_event.event() {
tauri::WindowEvent::Moved(position) => {
settings.lock().unwrap().window_position = Some(position.clone());
}
tauri::WindowEvent::Resized(size) => { /* (omit) */ }
tauri::WindowEvent::Destroyed => {
save_settings(settings.lock().unwrap().clone());
}
_ => (),
})
// ...
on_window_event()
は Send + Sync + 'static
なクロージャを要求する。面倒くさいことは考えたくないので定番の Arc<Mutex<T>>
で settings
を包む。
レンダラプロセスからは "tauri://move"
を listen するか onMoved()
あたりを使う。
WebView 状態の保存
WebView 上でのイベントはレンダラプロセスでしか拾えない。入力フィールドの値を永続化したい場合はレンダラプロセスからホストプロセスへ状態の変化を通知して、ホストプロセス側でその状態を維持する。
レンダラプロセスから emit()
してホストプロセスで listen()
する。
// dist/main.js
const el = document.getElementById("my-field");
el.addEventListener("change", _ => {
tauri.event.emit("my-field-changed", el.value);
});
// src-tauri/src/main.rs
let settings_for_setup: Arc<Mutex<Settings>> = settings.clone();
tauri::Builder::default()
// ...
.setup(move |app| {
let main_window = app.get_window("main").unwrap();
main_window.listen("my-field-changed", move |event| {
let my_field = event.payload().unwrap_or("").to_owned();
settings_for_setup.lock().unwrap().my_field = my_field;
});
})
// ...
起動時の状態の復元
二回目以降の起動ではウィンドウの位置とサイズ、WebView の状態を保存した設定ファイルから復元したい。Electron では BrowserWindow
を作るコールバックで位置とサイズを設定し、did-finish-load
イベントを受け取ったタイミングでページ内の状態を送っていた。これで期待通り動いていた。
Tarui で同じ挙動をさせるのは手間がかかった。
ウィンドウの復元から。最初は tauri::Builder::on_page_load()
で位置とサイズを設定する方法を試した。こうするとウィンドウが初期値の位置とサイズで描画された後にウィンドウが移動してリサイズされてしまう。この挙動は受け入れられない。
次に試したのはアプリを起動する前に WindowConfig
を上書きするというもの。
let mut context = tauri::generate_context!();
let mut window_config = &mut context.config_mut().tauri.windows[0];
if let Some(ref window_position) = settings.window_position {
window_config.x = Some(window_position.x as f64);
window_config.y = Some(window_position.y as f64);
}
手元の環境では挙動が定まらない。たまに期待通りの位置とサイズで表示されることもあるが、変な位置とサイズで表示されることもある。挙動に一貫性がなく、printf デバッグしても原因がつかめない。
結局今は以下のように実装している:
- 起動時にはウィンドウを非表示にして、
tauri::Builder::setup()
に渡したクロージャで位置とサイズを復元し、tauri::Window::show()
を呼んでウィンドウを表示する。
次に WebView の状態を復元する。on_page_load()
はこの用途でも使えなかった。on_page_load()
で emit()
したイベントを WebView 側で listen()
しようと試行錯誤したが上手くいかない。タイミングの問題だろうかと dist/main.js
が読み込まれた直後に listen()
してもイベントハンドラが呼ばれない。
今の実装では tauri::Builder::manage()
を使っている。manage()
を使うとアプリケーションに任意のデータ (Tauri では state と言っている) を紐づけることが出来る。以下のように settings
をアプリケーションに紐づけて:
// src-tauri/src/main.rs
let shared_settings: Arc<Mutex<Settings>> = settings.clone();
tauri::Builder::default()
// ...
.manage(shared_settings)
.invoke_handler(tauri::genrate_handler![get_my_field, /* ... */])
// ...
WebView 側で設定を取得する。
// dist/main.js
const my_field = await tauri.invoke("get_my_field");
設定を取得するコマンドは以下のような感じ。
// src-tauri/src/main.rs
#[tauri::command]
fn get_my_field(settings: State<'_, Arc<Mutex<Settings>>>) -> String {
settings.lock().unwrap().my_field
}
Electron は戸惑うことがほぼなく、作りたいものを素直に書けた印象がある。Tauri はちぐはぐ。挙動、設計、コード、ドキュメントのどれをとっても成熟の差が出ていると思った。
余談: この記事を書いている段階での最新版である 1.0.3 には MacOS でウィンドウのリサイズイベントが検出できないバグがある。安定版を名乗るには致命的な感じがあるが、そのうち直ると思う。
2022/06/24
This site stopped using Google Fonts. FOUT is annoying. GDPR is another concern. This site stopped using Google analytics too.
I still believe that web fonts are valuable. I have a dictionary tool that is built on top of the Web technologies. It shows the IPA nicely thanks to web fonts. However, do they bring similar benefits at a cost of loading additional resource at this site and privacy implications? No.
Let’s prioritize readers.
帰省 - 2022/05/31
便りがないのは無事な証拠、を地でいっているので実家には帰省する時ぐらいにしか連絡しない。年に一度くらいは親に元気にやっている姿を見せようと帰省してきた。
GW 明けだったら安価に帰れるだろうと高を括っていたが同じように考える人が多いのか飛行機の手配に少し手間取る。平日に移動すればチケットが取れそう。趣を変えて空港近くの都市部に滞在してみるのはどうだろうか。経由するだけだった場所をいつもと違う時間と目線で見るいい機会かもしれない。
遠い昔に屋台でラーメンを食べたことを思い出す。屋台の数は年々少なくなっているそうで、たしかに昔はもっと賑やかだったと思う。
会社の福利厚生の一部として申請できると聞いたので宿は良いところにした。
子供のころによく食べていた甘い食べ物たち。久しぶりに食べるとおいしい。
初夏は過ごしやすく、良い旅になった。
縁日 - 2022/05/11
カタカタといつものようにキーボードを叩いていると外から笛の音が聞こえてきた。ドンドンと太鼓の音もする。換気のために開けていた窓を全開にして外を覗き込むと神輿を引く人々がみえた。
掲示板に大祭の案内が掲示されていたのを思い出す。少しづつ下町の夏が戻ってきている。
地元の子であろう元気な男子が「俺ポテトにするー」とはしゃぎながら駆け抜けていった。うなぎ文は台東区には根付いているよう。
Perfetto via Rust - 2022/04/28
Perfetto は Android platform で使われている tracing フレームワーク。Android platform 以外でも単独でアプリケーションに組み込んで使うことも想定して作られていて、例えば Chrome が使っている。この Perfetto を Rust で書いたアプリケーションから使ってみようと試している。
現状のプロトタイプ実装で HTTP/1.1 のリクエストを投げてレスポンスを解釈する async タスクを以下のように書くと:
async fn h1_get_task(host: String, port: u16) -> anyhow::Result<()> {
let track = Track::with_name(&host);
trace_event!(CATEGORY_BENCHMARK, "Request", track, EventType::SliceBegin);
let mut client = Client::connect(host.clone(), port, track).await?;
client.send_request().await?;
let response = client.recv_response().await?;
trace_event!(
CATEGORY_BENCHMARK,
"Request",
track,
EventType::SliceEnd,
|event| {
trace_event_set_string!("host", &host);
trace_event_set_string!("status", response.status().as_ptr());
}
);
Ok(())
}
struct Client { /* ... */ }
impl Client {
async fn connect(host: String, port: u16, track: Track) -> Result<Self> {
let _s = trace_event_span!(CATEGORY_BENCHMARK, "Connect", track);
// ...
}
async fn send_request(&mut self) -> Result<()> {
let _s = trace_event_span!(CATEGORY_BENCHMARK, "Send", self.track);
// ...
}
async fn recv_response(&mut self) -> Result<Response> {
let _s = trace_event_span!(CATEGORY_BENCHMARK, "Recv", self.track);
loop {
match self.state {
State::ReadingHeaders => self.read_headers().await?,
State::ReadingBody => self.read_body().await?,
State::Done => break,
}
}
// ...
}
async fn read_headers(&mut self) -> Result<()> {
let _s = trace_event_span!(CATEGORY_BENCHMARK, "ReadHeaders", self.track);
// ...
}
async fn read_body(&mut self) -> Result<()> {
let _s = trace_event_span!(CATEGORY_BENCHMARK, "ReadBody", self.track);
// ...
}
}
こんな感じのトレースが得られる。
trace_event!
と trace_event_span!
が C++ の TRACE_EVENT()
に相当する。スコープを扱うのに _s
に代入しているのはいびつだけれど、これは Attribute macros で対処できる目途が立っている。
Perfetto には track という概念がある。分散環境を意識した用語でいえば span と同等のもの。 Android での tracing を主な用途とする Perfetto では TRACE_EVENT()
で記録される event はデフォルトでスレッドと1:1に対応する track に紐づけられる。
一方 Async Rust では .await
の前後でスレッドが異なるエコシステム (tokio) で trace を記録したいことがほとんど。 C++ と同じ感じで track を省略して書くと UI が span を期待通りに表示してくれない。プロトタイプでは明示的に track を要求するようにしている。似た様な問題意識を持つ tracing crate はこのあたりを隠蔽しようと悪戦苦闘している様子だが見込みは薄そう。
Event を引数付きで記録したい場合は event にメタデータを付与するコールバックを渡す。TRACE_EVENT()
のように記録したいパラメータを inline で渡せるマクロを提供できるかもしれないが、自分はマクロに詳しくなりたいわけではないので深掘りしていない。
実用には至っていない。困っている点を二つ羅列しておく。
- 動的に決まる key-value ペア (HTTP response headers とか) を trace event として記録する方法が分からない。Perfetto はオーバーヘッドを減らすべく key を internalize する。Internalize するときに使われるのは key のアドレスのようだ。つまり key は静的にアロケートされているか、動的に確保されたときは同じアドレスが使われていてはいけない。スタックに一時的に key を保存するようなナイーブなやり方だとこの前提を崩してしまい、期待した動きにならない。
TRACE_EVENT()
はperfetto::TracedValue
を引数に受け取る lambda を受け付けるように読めるが、マクロを使わずにTraceForCategory()
を直に叩くとコンパイルに失敗する。これは単純に自分の C++ 力が足りないだけ。