homelab notes ポート開放できない家の自宅サーバー

Tailscale Funnel で1ページだけインターネットに出す

tailnet の中身は閉じたまま、選んだ1つだけを公開する。 このページがまさにそれで配信されている。

仕組み

Funnel は Tailscale が持つ公開イングレスで TLS を終端し、 そこから tailnet を通してノードに転送する。 ルータ側では何も開けない

インターネット
  → Tailscale の公開イングレス (TLS 終端)
    → tailnet
      → ノードの tailscaled
        → 127.0.0.1:8082 (nginx)

使えるポートは 443 / 8443 / 10000 に限られ、独自ドメインは使えない。 公開名は <ノード名>.<tailnet>.ts.net に固定される。

公開範囲は思ったより狭い

気になったので実測した。Funnel を有効にしていない ts.net の名前は、 公開 DNS から引けない。

$ dig +short @1.1.1.1 grafana.<tailnet>.ts.net
(応答なし)

$ dig +short @1.1.1.1 <funnelしたノード>.<tailnet>.ts.net
103.84.155.153

つまり1ノードを公開しても、同じ tailnet の他のノードは外から名前解決すらできない。 公開されるのは Funnel で明示的に指定した経路だけ。

既定の ACL は全デバイスに公開権限を与えている

Funnel にはノード属性が要る。tailnet の初期ポリシーを開いたら、既に入っていた。

"nodeAttrs": [
    {
        "target": ["autogroup:member"],   ← 全メンバーの全デバイス
        "attr":   ["funnel"],
    },
],
autogroup:member は、そのメンバーが持つ全デバイスを指す。 うちはリバースプロキシがサービスごとに Tailscale デバイスを作る構成なので、 Grafana も Ollama も「公開できる状態」で並んでいたことになる。 実際に公開するにはコマンドを叩く必要があるとはいえ、事故の余地は小さいほうがいい。

公開したい1台の Tailscale IP に絞った。

"nodeAttrs": [
    {
        // 既定は ["autogroup:member"](=全デバイスが Funnel 可能)。
        // プロキシが作った他のデバイスまで公開できてしまうため1台に絞る。
        "target": ["100.x.y.z"],
        "attr":   ["funnel"],
    },
],

なお、管理コンソールの「Add rule」ボタンは acls(送信元→宛先の通信許可) を作るための UI で、nodeAttrs はそこからは作れない。 JSON エディタで直接書く。

公開するもの側

既存のサービスを Funnel に向けるのではなく、公開専用の静的サイトを1つ立てた。 公開してよいものと、してはいけないものが同じプロセスに同居しないようにする。

services:
  www:
    image: nginx:1.31.5-alpine
    ports:
      - "127.0.0.1:8082:80"   # LAN にも出さない。外向きは Funnel だけ
    volumes:
      - ./site:/usr/share/nginx/html:ro
$ sudo tailscale funnel --bg --https=443 http://127.0.0.1:8082

踏んだ罠: DNS のネガティブキャッシュ

有効化したのに、公開 DNS から引けない状態が10分近く続いた。

$ dig +short @1.1.1.1 <node>.<tailnet>.ts.net
(応答なし)
$ dig +short @8.8.8.8 <node>.<tailnet>.ts.net
103.84.155.217 103.84.155.153     ← こっちは引ける

原因は自分だった。Funnel を有効にする前に dig で確認していたので、 「レコードなし」がネガティブキャッシュに乗っていた。 SOA の最小 TTL のあいだ保持される。

判断に使うなら権威サーバーに直接聞く。キャッシュを経由しないので今の真の状態が出る。
$ dig +short @ns3.dnsimple.com <node>.<tailnet>.ts.net A

公開後に確認したこと

「見えた」だけでは検証にならない。tailnet に繋がった端末のブラウザだと、 Funnel を経由せず内部経路で表示されてしまう。 公開イングレスの IP を明示して取得する。

$ curl -o /dev/null -w '%{http_code} %{remote_ip}\n' \
    --resolve <node>.<tailnet>.ts.net:443:103.84.155.153 \
    https://<node>.<tailnet>.ts.net/
200 103.84.155.153

公開されている経路も確認しておく。

パス応答
/ /index.html200
/nope /../etc/passwd404

止め方

$ sudo tailscale funnel --https=443 off   # 公開だけ止める(サイトは動いたまま)
$ tailscale funnel status                 # 今なにが公開されているか