>> はじめに - 外へ出したい
改修編② までで,壊れても映り続けるところまでは作った.
ここまでは Tailscale の中だけで見ていた.だが本当の望みは「タブレットやスマホで,Tailscale を繋がずに使いたい」だった.
ただしそのまま外へ出せば,放送そのものが世界に出る.何かの守りが要る.
>> 端末の登録制
思いついたのは合鍵を Cookie に入れておく形だ.
登録 /enroll/ を開く → 256bit の乱数を作り Cookie に10年
★登録できるのは «自宅の中から» だけ
通行 合鍵が無ければ 403(どこから来ても)
取消 一覧から消す → 即座に効く 合言葉にする案もあったが,守るべき秘密を増やさない方を選んだ.合言葉は必ずどこかに書かれ,いつか漏れる.「家におること」は漏れようがない.
判定は nginx の map にやらせる.
map $cookie_tv_token $tv_tok {
default 0;
include /etc/nginx/tv-tokens.map; # "<64桁の16進>" 1;
} Python を通さんのが要だ.HLS のセグメントは毎秒のように飛ぶので,そこにインタプリタを挟むのは筋が悪い.取消しは一覧を書き直して reload すれば即座に効く.
>>> Cookie は «ホスト名» に付く
ここで一つ罠があった.自宅の IP で登録しても,別のドメインには送られない.
http://192.168.11.147:8080/ で登録 → その IP にしか付かん
https://…ts.net/ へは ★送られん そこで /claim?t=<合鍵> を用意した.登録後に出るリンクを同じ端末のまま開けば,そちらのドメインにも合鍵が付く.
>> ★開ける前にログを数えた
さて Funnel を開けよう——というところで手を止めた.確かめておきたいことがあった.
tailscale serve / funnel は 127.0.0.1 へ中継する.ならば nginx から見た送信元は何になるか.
アクセスログを数えた.
今日のアクセス元
4219 127.0.0.1 ← ★Tailscale 経由は全部これ
676 192.168.11.16 ← LAN の iPhone
4 192.168.11.147
100.x(Tailscale の実アドレス)は ★1件も無い 中継で本当の相手が潰れている.そして 8080 は 127.0.0.0/8 を自宅として素通しにしていた——
つまり Funnel を開けた瞬間
★世界中の誰もが «自宅から来た» ことになって素通しする 関所が一切効かん.合鍵も登録も無意味になる.
>>> 入口を分ける
https://…ts.net/ Funnel → 8090 ★合鍵が «必ず» 要る
https://…ts.net:8443/ tailnet → 8080 登録もできる ポートで経路が決まるゆえ偽装しようがない.中継で送信元が潰れても関係ない——8090 に来た時点で「外の者」と決まっている.
8090 には置かないものを決めた.置かねば攻められん.
/enroll/ /enroll-do /devices /revoke 外から端末を増やさせん
/api/ mirakc を直に触らせん
/recover-card ★外からテレビを止めさせん 検証でlocation / の catch-all が登録ページの «静的HTML» を拾っていたのも見つかった.API は無いので押しても登録はできんが,出すべきでない物が見えているのは良くない.
>> nginx で踏んだ3つ
>>> ① map の器に 64文字の鍵が入らん
[emerg] could not build map_hash, you should increase map_hash_bucket_size: 64 これが «全部» の真因だった.合鍵を1つ入れた瞬間に nginx -t が落ちる.
問題はその先だ.
設定が通らん → reload されん → ★古い設定のまま動き続ける
↓
検証が全部 «素通し» に見える
↓
私は sed や if の書き方を疑った——どちらも «結果» で
«原因» ではなかった 遠回りさせたのはこの一行だ.
# ✕ 落ちても «何も言わずに» 次へ進む
nginx -t >/dev/null 2>&1 && systemctl reload nginx エラーを黙らせる書き方が,原因を3手も遠ざけた.
>>> ② if の左辺は «単一の変数» しか取れん
# ✕ «m$lan という名の変数» と読まれる
if ($m$lan = "10") { return 403; }
# ✕ invalid condition
if ("$m$lan" = "10") { return 403; }
# ✅ 場所ごと(location)に置く
location = /enroll-do { if ($lan = 0) { return 403; } proxy_pass …; } 結果としてこちらの方が読める.どこが閉まっているかが一目で分かる.
>>> ③ catch-all が出すべきでない物を拾う
上に書いた登録ページの件.location /enroll { return 404; } で塗った.
>> わざと壊して確かめる
検証では geo から 127.0.0.0/8 を一時的に外し,自分自身を「外の者」に仕立てた.
8080 合鍵なし・視聴 403 ✅ ★8080 合鍵あり 200 ✅
8090 合鍵なし・映像 403 ✅ ★8090 合鍵あり・リモコン 200 ✅
★8090 に登録の口は無い 404 ✅ 外から登録 403 ✅
でたらめな合鍵 403 ✅ /claim は通す 302 ✅
★取り消した合鍵 403 ✅ ★残した方は 200 ✅ 取り消しは «狙った1つだけ» に効くかまで見た.巻き添えが出んことを確かめて初めて信用できる.
そしてここでも測り方で転んだ.一度目は仕立てが効いておらず,11通り全部 200 という無意味な結果を出した.二度目から「仕立てが効いておるかを先に確かめ,効いておらねばその場で止める」形にした.
>> ★後始末で本物を消しかけた
検証の後始末に,こう書いていた.
echo '[]' > /etc/tv/devices.json # ★全部消える 実行する直前に一覧を見て,手が止まった.
msair 07:40
msair-brave 07:41 自分の端末が既に2台登録されていた.「検証用を片付ける」つもりで,本物を巻き込む書き方をしていた.
# ✅ 作った物だけを名指しして消す
for i in $(… if x["name"] in ("検証用","検証A",…)); do curl …/revoke?id=$i; done 消す対象は «作った物だけ» を名指しする.見分けずに「全部」と書くのは,壊す気が無いのに壊す書き方だ.
>> 開けた
tailscale serve --https=443 off
tailscale serve --bg --https=8443 8080 # 内部を 8443 へ
tailscale funnel --bg --https=443 8090 # ★外部を 443 で Cookie はホスト名に付くので,ポートが変わっても登録済みの合鍵はそのまま効く.
NAT でポートを開ける道もあったが,Funnel を選んだ.
ルータに穴を開けん ★ポート開放なし
本物の証明書が付く ★暗号化される(合鍵も映像も平文で流れん)
見る側に Tailscale が要らん >> 残すリスクを正直に
・合鍵が漏れればその端末分は通る → 取り消せる仕組みがある
・放送そのものが «自宅の外» へ出る形にはなる
→ 家族の端末に限る使い方なら私的利用の範囲と考えてよかろう
→ ★合鍵を配る相手は選ぶ
・Funnel の URL は推測しにくいが «秘密ではない»
→ 守っておるのは Cookie の関所だけ >> まとめ
- 守るべき秘密を増やさない.「家におること」は漏れようがない
- 中継されると送信元は潰れる.開ける前にログを数える
- ポートで経路を分ける.偽装しようがない
- 置かねば攻められん.外向けには管理の口を一切置かない
- エラーを黙らせる書き方が原因を遠ざける
- 仕立てが効いておるかを先に確かめる.効いておらねば測る意味がない
- 消す対象は «作った物だけ» を名指しする
引き出しに転がっていた Pi 3B が,外出先からも見られるテレビになった.登録した端末だけが通り,カードが死んでもワンセグで映り続け,何か起きれば画面が理由を告げる.
この2日間で自分の «測り方» を 10件壊した.本体より道具の不具合の方が多かったというのが,終わってみての正直な感想だ.