#show tech >>log

Raspberry Pi 3B で自宅のテレビをどこでも見る ── 改修編③ 外へ出す

>> はじめに - 外へ出したい

改修編② までで,壊れても映り続けるところまでは作った.

ここまでは 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.16LAN の 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 合鍵あり       2008090 合鍵なし・映像      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件壊した.本体より道具の不具合の方が多かったというのが,終わってみての正直な感想だ.