旅行中に新しく撮った写真を長沙の自宅にある Immich サーバーにアップロードしようと試みていました。以前は Cloudflare Tunnel を使ってリバースプロキシを設定していましたが、中国の特有なネットワーク環境のために接続速度が遅く、頻繁に中断し、アップロードが失敗することが常態化していました。その後、Tailscale — WireGuard を基にした内部ネットワーク透過ツールを使い始めると、全国どこにいても安定して高速に自宅の NAS や写真ライブラリにアクセスできるようになりました。
しかし新たな問題が発生しました:Tailscale が Android スマートフォンで動作する際、システムのトラフィックを VPN サービスとして制御する必要がありますが、私が普段使用している Clash も VPN インターフェースに依存して分岐しています。Android システムの制限(一つの VPN サービスのみ有効)のためにこれらは共存できず、「家庭内サービスへのアクセス」と「セキュアなインターネット接続」とを選択しなければならない状況になりました。
この記事では、Tailscale の原理から始まり、独自の DERP サーバーを構築して接続品質を最適化する方法、さらに ShellCrash へトラフィックを転送するために iptables を用いた Tailscale Exit Node の流量の転送方法について解説します。NAS、写真ライブラリのアクセスや不慣れなネットワークでのデータ保護を目指す方にとって、この記事が実用的で安定した解決策を提供します。
Tailscale とは
Tailscale は WireGuard プロトコルを基にした設定不要の仮想 LAN ツールです。これにより、異なるネットワーク環境にあるデバイスが同じセキュアな内部ネットワーク内にいるかのように互いに通信できます。NAT やファイアウォールなどのネットワークの障壁を自動的に透過し、公衆 IP やポート転送なしで自宅の NAS、個人サーバー、開発環境などの内部リソースに簡単にアクセスできます。その主な利点は、シンプルで安全、安定しており、データ送信が全面的に暗号化されている点で、個人開発者、リモートワーカー、家庭ユーザーなどのいろいろなシーンに適しています。
Tailscale の技術実装は非常に巧妙です:WireGuard 暗号化プロトコルに基づいていますが、従来の VPN の IP 割り当てのロジックを覆しました。デバイスは SSO/OAuth2 で認証後、それぞれが生涯にわたって結びつけられたノードキーを取得します。この身分に基づくネットワーキングモードにより、「長沙の NAS」と「香港のスマートフォン」が仮想ネットワーク内で直接通信できるようになります。
Tailscale の接続プロセスの原理
中央制御サーバー(Control Server)
Tailscale の各クライアントが起動すると、まず中央制御サーバー(control plane)に接続し、身分認証を行い、ネットワーク内の他のノードの情報を取得します。これには、各デバイスの公衆 IP、ポート、NAT のタイプなどが含まれます。このステップは「友達と知り合う」ようなものです。
Tailscale の制御サーバーはデータ転送には関与せず、接続の調整のみを行います。これは一種のディスパッチセンターの役割です。
DERP サーバー
Tailscale が高い接続成功率を維持できる秘訣については、Tailscale 独自の中継プロトコル DERP を挙げることができます。Tailscale のネットワークアーキテクチャー内で、DERP(Designated Encrypted Relay for Packets)は非常に重要でありながら通常は必要時にのみ介在するコンポーネントです。簡単に言えば、これは HTTP ベースの暗号化リレーサーバーで、二台のデバイスが直接通信できない時に「中継所」として機能します。
クライアント間の全ての接続は最初に DERP モード(中継モード)で選ばれ、これによってすぐに接続が確立でき、ユーザーは待つ必要がありません。その後、接続両者は同時にパス探索を開始し、通常数秒後に Tailscale はより優れたパスを発見し、既存の接続を透明にアップグレードします。これにより、点対点の直接接続へと移行します。1
注意が必要な点は:
- DERP を通じる全てのデータは端から端まで暗号化されており、DERP サーバーは内容を見ることができません;
- Tailscale はできるだけ少なく DERP を使用し、直接接続が成功したら自動的に切り替えます;
- 公式には複数の分散 DERP ノードがデプロイされており、クライアントは自動的に最低遅延のものを選びます;
- 自国内に DERP ノードを構築することもできます(中国など)、これにより高遅延や不安定な接続の問題を解決できます。
DERP は兜底策として理解でき、性能は直接接続に劣りますが、接続の透通性を保証します。
NAT 透過(NAT Traversal)
相手のアドレス情報を手に入れた後、Tailscale は NAT 透過を使って点対点(P2P)接続を試みます。この過程では STUN プロトコルを使用し、双方が探索パケットを相互に送り、NAT ルーター上に直接通路を開くことを試みます。双方のネットワーク条件が許せば、UDP の直接トンネル接続が成功し、データは直接通路を通じて、速度が速く、遅延が低くなります。
この部分の詳しい原理について興味があれば、Tailscale 公式の「How NAT traversal works」を参照してください。
完全な接続の流れを示す図
flowchart TD A[デバイス A が Tailscale を起動] --> B[DERP サーバーを通じて初期接続を確立] B --> C[ネットワーク情報と WireGuard キーを交換] C --> D[NAT タイプの検査を並行して実施] D --> E{直接接続は可能か?} E -- はい --> F[P2P 直接トンネルを確立] F --> G[接続品質を定期チェック] G --> H{直接接続は DERP より優れているか?} H -- はい --> I[大部分のトラフィックを直接通路に切り替え] H -- いいえ --> J[トラフィックを DERP 経由で転送し続ける] E -- いいえ --> J %% スタイル設定 style B fill:#e3f2fd,stroke:#2196f3,color:#000 style F fill:#e8f5e9,stroke:#4caf50,color:#000 style J fill:#fff3e0,stroke:#ff9800,color:#000
自前の DERP を構築する
Tailscale のインストールは各プラットフォームで比較的簡単で、公式文書が詳細な操作ガイドを提供しています。この記事ではインストールプロセスには触れませんが、以下の内容はあなたが関連するデバイス上で Tailscale を既に成功的にインストールしてログインしていることを前提としています。
なぜ自前の DERP を構築するのか?
Tailscale は世界中に多数の DERP 2中継サーバーを配置しています。これは NAT の穿通に失敗した際にトラフィックの中継を担当します。しかし、よく知られている理由で、中国本土には公式の DERP ノードが配置されていません。これは以下を意味します:
- NAT の穿通に失敗すると、すべてのトラフィックが海外の DERP ノードを経由する必要があります。これにより遅延が高くなり、速度が遅くなり、体験が悪化します;
- いくつかの公式 DERP ノードは GFW によって干渉されやすく、接続が中断されたり、ハンドシェイクが失敗することがあります;
- 穿通に成功しても、Tailscale は依然として DERP を通じてルーティング情報と WireGuard キーを交換する必要があります。もし DERP が接続不可能だと、接続品質も影響を受けます。
したがって、中国のネットワーク環境では、地元の DERP ノードを自前で構築することで接続の安定性と転送性能を大幅に向上させることができ、ネットワークのブロックによる予期せぬ問題を回避することができます。
準備作業
先に述べたように、DERP は HTTP ベースですので、HTTP リバースプロキシサービスの準備と SSL 証明書の発行などの基本問題を解決する必要があります。この記事では Docker を使用してデプロイします。デプロイ開始前にサーバーに Docker および Docker Compose などの追加コンポーネントをインストールしておく必要があります。設定ファイルの編集には nano などのエディタを使用する知識も必要です。最良の結果を得るために、サーバーは静的な公衆 IPv4+IPv6 デュアルスタックアドレスを持っていることが望ましいです。
すべての条件が整っていれば、デプロイを開始できます。
|
|
docker-compose.yml
|
|
network_mode: host
は重要な設定であり、コンテナはホストマシンのネットワークスタックと共有します。もし Docker のデフォルトの bridge ネットワークモードを使用すると、コンテナのネットワークは Docker 内部ネットワークを経由して転送され、DERP の STUN サービスは Docker 172.17.0.0/16
アドレス範囲のアドレスを識別できません。そのため、クライアントの正しい外部アドレスを識別できず、Tailscale クライアントが正しく接続できません。
|
|
先に述べたように、DERP を通じるすべてのデータは端から端まで暗号化されており、DERP は誰が使用しているかを知りません。この意味は、対策を講じない場合、あなたの DERP サーバーのアドレスとポート番号を知っている誰でもそれを使用することができます。この設定の目的は、ホストマシンの Tailscale ソケットファイルをコンテナにマウントして、DERP_VERIFY_CLIENTS=true
を使用することで、認証を行うことができ、あなたの DERP ノードが他人に無断で使用されることを防ぎます。
重要な注意点は以下の通りです:
- ホストマシンには Tailscale クライアント(tailscaled)がインストールされ、ログインされている必要があります。このファイルが存在しない場合、コンテナはエラーを報告します;
- tailscaled は root 権限で実行される必要があり、この sock ファイルを作成できます。
リバースプロキシの設定
Caddy を例にして、4433 ポートをリバースプロキシし、SSL 証明書をデプロイする方法を示します。
|
|
Caddy と dnsproviders を使ってワイルドカードデジタル証明書を申請する場合、Tailscale の MagicDNS が Caddy のローカル証明書検証に障害をもたらす可能性があるため、resolvers
パラメータを手動で設定する必要があります。
設定されたノードにアクセスすれば、以下のページが表示されれば、設定が正しいことが確認できます。
ACL ポリシーの設定
Tailscale のコントロールパネルの「」ページにて ACL ポリシーを設定し、設定した DERP を追加します。
Tailscale の ACL ポリシーは HuJSON3で書かれており、VSCode で編集する場合は言語設定を「JSON with Comments(jsonc)」に設定することができます。以下は設定例です:
|
|
Tailscale は RegionID 1-899 を公式ノードとして予約していますが、自営ノードの RegionID は 900 以上である必要があります。
接続のテスト
ACL の設定が完了し保存されると、Tailscale は自動的にすべてのクライアントに設定を同期し、少し待ってからクライアントでtailscale netcheck
を使用して接続をテストします。
戻り値の IP が実際の公衆 IP であるかを確認する必要があります。もし172.17.0.0/16
アドレス範囲のアドレスが戻ってきた場合、Docker の設定が間違っている可能性があります。
Exit Node でのトラフィックを Clash コアにハイジャックする:科学的なインターネット接続の共存
Exit Node の宣言
家では 24 時間稼働するデバイスを用意します。これはラズベリーパイでも MacMini でもかまいません。そこに Tailscale をインストールし、それを Exit Node として宣言します。また、必要に応じて Tailscale の内部ネットワークで家庭内ネットワークセグメントを宣言し、その後コントロールパネルでこのデバイスを Exit Node として有効にします。これにより無料の VPN が手に入り、見知らぬネットワーク環境でも安全を保ちながら接続することができます。
|
|
ルーティングが完了すると、どこにいても Tailscale ネットワークに接続できる限り、家庭内のすべての内部デバイスにアクセスすることができます。
IP フォワーディングの有効化と UDP GRO の無効化4
ラズベリーパイなどのデバイスが Exit Node として機能するために必要な設定です。この例ではラズベリーパイを使用しますが、使用しているデバイスに応じて Tailscale 公式ウェブサイトのチュートリアルを参照してください。
|
|
Tailscale 公式の文献によると5、UDP GRO6を無効にすることで転送性能が向上するとされていますが、公式の永続的なチュートリアルはラズベリーパイでは効果がありません。しかし、手動での設定が可能です。
|
|
持続化のために手動で systemd 設定ファイルを作成します:
|
|
ファイルの内容は以下の通りです:
|
|
その後、サービスを起動します:
|
|
Exit Node でのトラフィックハイジャック
私の Exit Node はラズベリーパイで、ラズベリーパイで Exit Node の設定が完了したら、最後のステップ、つまりスマートフォンから Exit Node に送信されるトラフィックをハイジャックして、セキュアなインターネット接続を実現します。安定性を考慮して、家庭のメインルーターで直接プロキシソフトウェアを実行することは避けています。この目的を実現するために、ラズベリーパイで直接トラフィックをハイジャックすることが唯一の選択肢です。
まず ShellCrash をインストールし、必要に応じて設定ファイルをインポートし、自動タスクを設定してください:
|
|
その後、サービスを起動し、ファイヤーウォールの運用モードを純粋モードに変更することを個人的にお勧めします。SNI スニッフィングをオンにし、DNS モードをfake-ip
からredir-host
7に切り替え、IPv6 透過プロキシを有効にします。
純粋モードを設定する目的は、iptables でより精密なトラフィックハイジャックを手動で構成することです。私たちは、Tailscale のネットワークインターフェースtailscale0
を介して Exit Node に転送されるすべてのトラフィックを、ローカルの Clash コアがリッスンしているポート7892
(静的ルートポートと呼ばれる)にハイジャックします。また、不可解なネットワーク問題に直面しないよう、IPv6 もハイジャックします。
|
|
進階:TProxy を使用して UDP トラフィックをハイジャックする
iptables の REDIRECT は TCP トラフィックのみリダイレクト可能で、UDP は接続状態を持たない(コネクションレスプロトコル)ため、REDIRECT では目的アドレスを保持できず、透明プロキシが元の目的アドレスを知ることができません。
したがって、以下のように UDP トラフィックをハイジャックする場合:
|
|
このルールは効果がないか、プロキシの動作が正常ではありません。
では、UDP トラフィックをプロキシする方法はあるのでしょうか?はい、方法はありますが、前提条件があります:
- コアが UDP 透明プロキシをサポートしている必要があります(Clash Premium や Mihomo が対応しています);
- TProxy モードを使用し、REDIRECT ではなく;
- iptables mangle テーブルと policy routing が正しく設定されていること;
- プロキシ構成ファイルで UDP プロキシが有効になっていること(例:mode: rule + udp: true)。
最初の 3 つの条件を満たしている場合、以下に例を示します8:
|
|
もちろん、IPv6 を忘れないでください:
|
|
ルーティングルールの永続化
ip rule
およびip route
で作成されるルールは再起動後失われますので、手動で永続化する必要があります。最も簡単かつ直接的な方法はスクリプトを作成し、それを crontab に追加することです。
スクリプトを作成:
|
|
以下の内容で編集:
|
|
実行権限を付与した後、Crontab を編集:
|
|
crontab ファイルの最後に以下の内容を追加:
|
|
原理解説:TProxy はどのように UDP トラフィックを転送するのか?
ここまで来ると、疑問が湧くかもしれません:なぜプロセス全体で--to-ports
を指定していないのに、iptables でも目的地が書き換えられていないのに、UDP トラフィックが不可解にもプロキシされたのでしょうか?これはどうやって実現されるのでしょうか?
この問題を解明するために、まず TProxy と REDIRECT の基本的な違いを見てみましょう:
REDIRECT モード:
- iptables nat テーブルを使用;
- 目的アドレスをローカルアドレス(例えば 127.0.0.1:7892)に書き換え;
- 主に TCP トラフィック用;
- 実際の目的アドレスを保持できない;
--to-ports
を指定する必要がある。
flowchart TB A[クライアントデバイス
Tailscaleを通じてTCPリクエストを開始] B[tailscale0 インターフェースがトラフィックを受信] C[iptables NAT PREROUTING
REDIRECT --to-ports 7892] D[ShellCrash ローカルでリッスン
127.0.0.1:7892] E[ShellCrash が新しい TCP リクエストを開始
→ 目的サーバー] F[ネットワークからの応答が戻る
ShellCrashが応答を転送] G[応答回クライアントデバイス] A --> B --> C --> D --> E --> F --> G style C fill:#f9f,stroke:#aaa,stroke-width:1px,color:#000 style D fill:#bbf,stroke:#aaa,stroke-width:1px,color:#000
TPROXY モード:
- iptables mangle テーブルを使用;
- 目的 IP を変更せず、元の目的アドレスを保持;
- fwmark と policy routing を使用してパケットを
lo
へルーティング; - プロキシプログラムが特定のポート(例えば 7893)をリッスンし、
IP_TRANSPARENT
を有効に; - UDP および TCP をサポート;
- NAT ではなく、マーク + ルーティングのため、iptables 内で
--to-ports
を指定する必要はない。
flowchart TB A[クライアントデバイス
Tailscaleを通じてTCP/UDPリクエストを開始] B[tailscale0 インターフェースがトラフィックを受信] C[iptables MANGLE PREROUTING
fwmark 233を適用] D[ip rule: fwmark 233
使用 routing table shellcrash] E[ip route: local 0.0.0.0/0
dev lo table shellcrash] F[ShellCrash は lo:7893 でリッスン
IP_TRANSPARENT モード] G[ShellCrash が元の目的アドレスを取得
プロキシ接続を開始] H[ネットワークからの応答が戻る
ShellCrashが応答を転送] I[応答回クライアントデバイス] A --> B --> C --> D --> E --> F --> G --> H --> I style C fill:#f9f,stroke:#aaa,stroke-width:1px,color:#000 style F fill:#bbf,stroke:#aaa,stroke-width:1px,color:#000
TProxy は DNAT/REDIRECT を使用せず、mangle テーブルでパケットに mark をつけ、policy routing(ip rule
+ ip route
)によってこれらのパケットを lo
インタフェースに送ります。プロキシプログラム(例:Clash / ShellCrash)は lo
9 でポート(例えば 7893)をリッスンしており、IP_TRANSPARENT
オプションを有効にすることで、パケットの元の目的 IP とポートを読み取り、代理転送を行います。
端的に言うと、TProxy モードは以下の 3 つのステップで動作します:
iptables
でデータパケットに mark をつける;ip rule
+ip route
でこれらのパケットをlo
に送る;- プログラムが
lo
上のポートをリッスンし、IP_TRANSPARENT
を有効にする。
したがって、iptables 内で --to-ports
を指定する必要はなく、なぜなら目的 IP とポートは変わらないため、代理プログラムが自身で感知し処理することができるからです。
iptables ルールの永続化
iptables-persistent
をインストール:
|
|
インストール中に現在の IPv4 および IPv6 設定を保存するかどうかを尋ねられますので、「はい」を選択してください。新しいルールを追加した後は、保存コマンドを実行してください:
|
|
保存されたルールファイルのパス:
- IPv4:
/etc/iptables/rules.v4
- IPv6:
/etc/iptables/rules.v6
必要に応じて上記のrules.v4
/rules.v6
ファイルを直接編集できます。
最終的な効果
このセットアップの使用体験は、あなたの家の上り帯域幅に依存します。私の家のネットワークは下り 500M、上り 60M で、一度も打ち破れないことはありませんでした。したがって、ほぼ全速力で走ることができ、遅延も許容範囲内です。また、外出先でいつでも家の Immich や OpenWRT ルーターなどのデバイスにエンドツーエンドで暗号化されたアクセスを提供し、科学的なインターネットを実現できるため、全体としては非常に満足しています。
HuJSON について: https://github.com/tailscale/hujson ↩︎
参照: https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes ↩︎
UDP GRO(汎用受信オフロード)は Linux カーネル内のネットワーク最適化技術で、複数の小さなパケットを一つにすることで処理効率を上げるものです。しかし、デバイスがネットワーク転送ノードとして使用される場合、これにより転送遅延が増加し、パケットロスが高い環境でのスループットが低下する可能性があります。 ↩︎
fake-ip
に比べて、redir-host
は互換性が高く、問題が発生する可能性が低くなります。また、プロキシのオン/オフ後にfake-ip
が残ってインターネット接続が一時的に中断されることがなくなるため、一般的にはredir-host
を GeoSite 分流ルールと組み合わせて使用することをお勧めします。私のラズベリーパイ上の DNS はを使用しており、DNS 汚染がなく、快適に動作しています。 ↩︎参考: https://blog.zonowry.com/posts/clash_iptables_tproxy/ ↩︎
lo
は Linux システムのデフォルトの「ループバックインターフェース」として機能しますが、透明プロキシではそれが単に localhost トラフィックを処理するだけでなく、外部世界からのネットワーク接続を受け取り、外部トラフィックをローカルでハイジャックして転送するために使用されます。 ↩︎