613 lines
40 KiB
Markdown
613 lines
40 KiB
Markdown
# VPNセルフホスト設計相談
|
||
|
||
**User:** Soma Nakamura (namamurasub@gmail.com)
|
||
**Created:** 1/19/2026 21:48:40
|
||
**Updated:** 1/20/2026 0:41:25
|
||
**Exported:** 1/20/2026 0:51:17
|
||
**Link:** [https://chatgpt.com/c/696e2828-1440-8330-b0b7-14ed22c5f05c](https://chatgpt.com/c/696e2828-1440-8330-b0b7-14ed22c5f05c)
|
||
|
||
## Prompt:
|
||
簡単にセルフホスト可能なVPNソフトウェアを作るに当たり、以下のようなものを考えている。
|
||
1. 普通のクライアント同士は、なるべく直接p2p接続を行う。
|
||
2. グローバルIPがある場所に「サーバー」を置く。そいつが設定の管理や鍵の管理、TURNっぽいもの(NAT配下から抜けるために)やTURNS(ファイヤウォール突破のため。この場合のみこのサーバーがリレーを行う?)をやったりする。NAT46みたいなことができるようになれば、サーバーはv6グローバルだけあれば良くなるので、更における場所が広がる。また、これをexit nodeにするような設定もできると良い。あと、このサーバー上の設定として、各クライアントに名前をつけてDNS広報などもできるようになると良い。
|
||
こういうものを作るにあたり、どういうふうにしたらよいか以下の質問に答えてほしい。
|
||
1. 鍵の管理などはtincみたいに分散させるべきか、集中していたほうが(グローバルがある場所にあったほうが)良いか。
|
||
2. 新しいクライアントの参加時には、どのような手法で行うべきか
|
||
3. TURNとか、様々なNAT越えの手法があるが、どれを使うとよいか(どんな環境においてもHTTPSさえ通れば絶対に疎通できることが望ましい。)
|
||
4. 各クライアントがp2p接続する際のプロトコルはwireguardでよいか?適当にやると(フルメッシュにしたいわけで)無数のwireguardクライアントが生えて困りそうだが、多分カスタムのインターフェースを作ってそこに集約するのか?
|
||
|
||
## Response:
|
||
Thought for 11m 26s
|
||
|
||
あなたが考えている構成は、方向性としてはかなり「現代的なMesh VPN(= Control planeは集中、Data planeはP2P、無理ならHTTPSリレー)」で、実装方針も立てやすいです。結論から言うと、
|
||
|
||
* **鍵“そのもの”(秘密鍵)は各クライアントに保持させる(分散)**
|
||
* ただし **鍵配布・ACL・名前/DNS・到達性(NAT越えの候補交換)といった“調停”は中央サーバ(集中)**
|
||
* **NAT越えは ICE/STUN/TURN の考え方を借り、最終フォールバックは 443/TLS 上のリレー(DERP的)**
|
||
* **P2Pの暗号化・仮想L3は WireGuard が最適。ただし「WireGuard単体」ではなく、それを動的にオーケストレーションする常駐デーモンが要る**
|
||
|
||
…という設計が、要件(セルフホスト容易・P2P優先・HTTPSさえ通れば極力なんとかする)に一番素直です。Tailscale/Headscale系がまさにこの分離で動いていて、「Control planeは鍵(公開鍵)とポリシーを配るだけ、Data planeはWireGuardのメッシュ」という整理が定番になっています。 [Tailscale+2GitHub+2](https://tailscale.com/blog/how-tailscale-works?utm_source=chatgpt.com)
|
||
|
||
以下、質問に順に答えます。
|
||
|
||
* * *
|
||
|
||
1\. 鍵の管理は分散(tinc的)か集中か
|
||
----------------------
|
||
|
||
### まず「何の鍵をどこで管理するか」を分解した方がよい
|
||
|
||
Mesh VPNでは鍵が最低でも2種類あります。
|
||
|
||
1. **データプレーン鍵**(実際にトラフィックを暗号化する鍵。WireGuardなら各ノードの秘密鍵)
|
||
2. **コントロールプレーンの認証・承認**(誰がネットワークに参加できて、誰と通信できるか、という権限)
|
||
|
||
このうち、
|
||
|
||
* \*\*(1) データプレーンの秘密鍵は“絶対にクライアントから出さない”\*\*のがベストです。
|
||
そうすると中央サーバが侵害されても「盗聴」まではされにくくなります(ただし後述の“鍵すり替え”リスクは残る)。Tailscaleも「公開鍵だけ交換し、秘密鍵は端末から出ない」と明言しています。 [Tailscale+1](https://tailscale.com/kb/1093/can-tailscale-decrypt-my-traffic?utm_source=chatgpt.com)
|
||
* \*\*(2) 参加管理/ACL/DNS名/経路広告/到達性(候補交換)\*\*は、\*\*集中サーバ(coordination server)\*\*に置く方が「簡単にセルフホスト」要件に合います。 [Tailscale+1](https://tailscale.com/blog/how-tailscale-works?utm_source=chatgpt.com)
|
||
|
||
### 「分散(tinc的)」のメリット/デメリット(あなたの要件に照らす)
|
||
|
||
**分散のメリット**
|
||
|
||
* 中央が落ちても(理想的には)参加/鍵配布が継続できる
|
||
* 中央を完全に信頼しない設計に寄せられる
|
||
|
||
**分散の現実的デメリット**
|
||
|
||
* そもそも **NAT越えの“候補交換/到達性観測”のために結局ランデブーが要る**(完全分散は難しい)
|
||
* オンボーディングが複雑化(誰が新規ノードを承認するのか)
|
||
* 「HTTPSさえ通れば」の最終フォールバックに **443リレーが必要** → 結局“中央サービス”が必要
|
||
|
||
### 推奨(結論)
|
||
|
||
* \*\*鍵管理は「秘密鍵は分散(端末保持)」「公開鍵とポリシー配布は集中(サーバ)」\*\*が最適解です。 [Tailscale+1](https://tailscale.com/kb/1093/can-tailscale-decrypt-my-traffic?utm_source=chatgpt.com)
|
||
* さらに“中央を信頼しない”を少しでもやるなら、オプションで
|
||
**(a) 管理者署名によるノード鍵承認**、または **(b) 透明性ログ(Key Transparency的)** を入れると「サーバ侵害→鍵すり替えMITM」耐性が上がります(実装コストは上がります)。
|
||
|
||
* * *
|
||
|
||
2\. 新しいクライアント参加はどう設計すべきか
|
||
------------------------
|
||
|
||
要件が「簡単にセルフホスト」なので、**現実的に安全で運用しやすいブートストラップ**に寄せるのが良いです。
|
||
|
||
### 推奨フロー:短命の招待トークン(Enrollment / Auth Key)方式
|
||
|
||
1. 管理者がサーバで **招待トークン**を発行
|
||
* TTL(例: 10分〜24時間)
|
||
* 使い捨て or N回まで
|
||
* 付与するタグ/グループ(例: `dev`, `admin`, `no-exit`)
|
||
2. クライアントは初回起動で
|
||
* \*\*デバイス識別鍵(例: Ed25519)\*\*を生成(長期)
|
||
* \*\*WireGuard鍵(X25519)\*\*を生成(ローテーション可能)
|
||
3. クライアントは `https://control.example.com` に接続し、招待トークンを提示して登録
|
||
4. サーバは
|
||
* ノードID発行
|
||
* VPN内IP(v4 /32 と v6 /128)割当
|
||
* ACL適用
|
||
* “通信してよい相手の公開鍵一覧 + ルート + DNS情報” を配布(以後は差分Push)
|
||
|
||
この「中央は公開鍵と設定を配るだけ」の形は、Headscaleの説明(WireGuard公開鍵の交換点、IP割当、ルート広告)とも一致します。 [GitHub](https://github.com/juanfont/headscale?utm_source=chatgpt.com)
|
||
|
||
### 追加で入れると強い安全策
|
||
|
||
* **承認待ちモード**:トークンで「仮登録」→ 管理者がUI/CLIで承認したら有効化
|
||
* **トークンのスコープ**:
|
||
* 「このトークンで参加した端末は exit node になれない」
|
||
* 「このトークンは特定ユーザ/特定タグにのみ」
|
||
* **サーバ真正性**:TLS(Let’s Encrypt等)+ 可能なら **初回にサーバ公開鍵ピン留め(TOFUでも可)**
|
||
“社内Proxyで証明書差し替え”環境もあるので、その場合の扱い(明示的にCAを追加する等)は設計で決めておく。
|
||
|
||
* * *
|
||
|
||
3\. TURNやNAT越えはどれを使うべきか(HTTPSさえ通れば絶対が理想)
|
||
----------------------------------------
|
||
|
||
### 結論:考え方は ICE(STUN+TURN)を採用、実装は「段階的フォールバック」が現実的
|
||
|
||
* \*\*ICE(RFC 8445)\*\*は「NAT越えのために候補(アドレス/経路)を集め、疎通チェックして最適経路を選ぶ」枠組みです。ICEはSTUNとTURNを利用する、とRFC自体が書いています。 [IETF Datatracker+2IETF Datatracker+2](https://datatracker.ietf.org/doc/html/rfc8445?utm_source=chatgpt.com)
|
||
* STUN(RFC 8489)は、NAT越しに「外から見える自分のIP:port」を知ったり、NATバインディング維持のkeepaliveにも使えます。 [IETF Datatracker+1](https://datatracker.ietf.org/doc/html/rfc8489?utm_source=chatgpt.com)
|
||
* TURN(RFC 5766 / 更新版RFC 8656)は、どうしても直接が無理なときに \*\*中継(リレー)\*\*を提供します。 [RFC Editor+1](https://www.rfc-editor.org/rfc/rfc5766.html?utm_source=chatgpt.com)
|
||
|
||
### ただし「VPN用途」だと TURNフル実装が重い
|
||
|
||
TURNは汎用中継として標準ですが、**VPNの“データプレーン”のためにTURNサーバ/クライアントを完全実装**するのは工数が増えがちです。
|
||
この場合、現実解としては2つあります:
|
||
|
||
#### A) 標準準拠でいく:STUN + TURN(必要ならTCP拡張)
|
||
|
||
* UDPが通る: STUNで外部アドレス取得 → UDP hole punching
|
||
* UDPが塞がれる: TURN over TCP/TLS を検討
|
||
TURNのTCP割当拡張はRFC 6062が定義しています。 [IETF Datatracker+1](https://datatracker.ietf.org/doc/html/rfc6062?utm_source=chatgpt.com)
|
||
|
||
#### B) “HTTPSさえ通れば”を最優先:DERP的な「443/TLSリレー」を自作(おすすめ)
|
||
|
||
「HTTPS(443/TCP)が通ればとにかく繋ぐ」を狙うなら、TURNより \*\*DERP(TCP/TLSで暗号化済みパケットを盲目的に転送するリレー)\*\*の方が設計が単純です。
|
||
|
||
* Tailscaleは「DERPはTLS上(多くは443)で動くTCPリレーで、直接繋がらないときに暗号化済みパケットを転送する」と説明しています。 [Tailscale+2Tailscale+2](https://tailscale.com/blog/nat-traversal-improvements-pt3-looking-ahead?utm_source=chatgpt.com)
|
||
* そしてファイアウォール観点でも「制御サーバ/DERPリレーはHTTPSで443を使う」とドキュメントにあります。 [Tailscale](https://tailscale.com/kb/1082/firewall-ports?utm_source=chatgpt.com)
|
||
* HeadscaleのDERP解説も、DERP relayが **HTTPS tcp/443**、STUNが **udp/3478** という“最低限ポート”の切り方になっています。 [headscale.net](https://headscale.net/stable/ref/derp/?utm_source=chatgpt.com)
|
||
|
||
あなたの要件に寄せた “段階的フォールバック” のおすすめ順はこうです:
|
||
|
||
1. **IPv6直**(両者がグローバルv6なら最強。NAT越え問題が激減)
|
||
2. **UDP直(v4/v6)**
|
||
* STUNで各自の外部アドレスを把握(server-reflexive candidate)
|
||
* rendezvous(あなたの中央サーバ)で候補交換
|
||
* 同時送信でhole punching(WireGuardのハンドシェイク自体を“疎通パケット”にしても良い)
|
||
3. **(任意)UPnP/PCP/NAT-PMP**でポートマッピング要求(家ルータでは効くことがあるが期待しすぎない)
|
||
4. **最後の砦:443/TLSリレー(DERP的)**
|
||
* WebSocket/HTTP2ストリーミングなど“HTTPSっぽい見え方”にして通りやすくする
|
||
|
||
### 「HTTPSさえ通れば絶対」についての注意(設計上の現実)
|
||
|
||
“絶対”は難しいです。理由は、ネットワークによっては
|
||
|
||
* HTTPプロキシ必須(CONNECTが要る、認証が要る)
|
||
* TLSインスペクション(証明書差し替え)
|
||
* SNI/宛先制限(特定ドメイン以外禁止)
|
||
* キャプティブポータル
|
||
などがあるためです。
|
||
|
||
ただし、\*\*「443/TCPで外向きTLSが通る」\*\*という前提が満たされる範囲では、DERP的リレーは非常に強いアプローチです。 [Tailscale+1](https://tailscale.com/blog/nat-traversal-improvements-pt3-looking-ahead?utm_source=chatgpt.com)
|
||
|
||
* * *
|
||
|
||
4\. P2PのプロトコルはWireGuardでよいか?フルメッシュで無数に増えないか?
|
||
--------------------------------------------
|
||
|
||
### 結論:WireGuardで良い。増えるのは「インターフェース」ではなく「peerエントリ」
|
||
|
||
WireGuardは「インターフェース(例: wg0)1つ」に対して、複数peerをぶら下げるモデルです。
|
||
WireGuard公式のQuick Startでもインターフェースを作ってIPを付ける流れが説明されています。 [WireGuard](https://www.wireguard.com/quickstart/?utm_source=chatgpt.com)
|
||
またWireGuardはNoiseフレームワーク等の現代的暗号を使う、という説明とホワイトペーパーもあります。 [WireGuard+1](https://www.wireguard.com/?utm_source=chatgpt.com)
|
||
|
||
### フルメッシュでの基本設計(典型)
|
||
|
||
* 各ノードに **VPN内アドレスを1つだけ**割り当てる
|
||
* IPv4: /32(例: 100.64.0.10/32)
|
||
* IPv6: /128(例: fd7a:115c:a1e0::10/128)
|
||
* 各peerのAllowedIPsは「そのノードの/32(/128)だけ」にする
|
||
→ **AllowedIPsが重ならない**のでルーティングが破綻しない
|
||
* WireGuardのAllowedIPsは「送信時の宛先IPからpeerを選ぶ/受信時のソースIPをフィルタする」用途を兼ねる、という説明がホワイトペーパーにあります。 [WireGuard](https://www.wireguard.com/papers/wireguard.pdf?utm_source=chatgpt.com)
|
||
|
||
この形だと、フルメッシュでも「wg0 1つ + peerがN-1個」です。
|
||
|
||
### ただし WireGuard“単体”ではNAT越え・経路切替ができない
|
||
|
||
WireGuard自体は「UDPで相手のEndpointへ投げる」だけで、
|
||
|
||
* STUNして外部アドレスを発見
|
||
* 経路が変わったらEndpointを更新
|
||
* UDPが無理なら443/TLSに逃がす
|
||
みたいなことは“外側のソフト”がやる必要があります。
|
||
|
||
なのであなたが言っている「カスタムのインターフェースを作って集約するのか?」はだいたい合っていて、実装の形は2択です:
|
||
|
||
#### 方式A:カーネルWireGuard + 制御デーモン(netlinkでpeer更新)
|
||
|
||
* パフォーマンス良い
|
||
* でも \*\*“TCP/TLSリレーでWireGuardパケットを運ぶ”\*\*の統合が難しくなる(WGは基本UDP)
|
||
|
||
#### 方式B:userspace WireGuard(wireguard-go相当)を内蔵し、送受信ソケットを自前で差し替える(おすすめ)
|
||
|
||
* 「UDP直」と「443/TLSリレー」を \*\*同一の“パケット送受信抽象”\*\*で切り替えやすい
|
||
* Tailscaleの `magicsock` はまさに「通信経路を動的に変えられるソケット」を提供する、という説明があります。 [Go Packages+1](https://pkg.go.dev/tailscale.com/wgengine/magicsock?utm_source=chatgpt.com)
|
||
* 443/TLSリレーは「WireGuardで暗号化済みのデータをリレーが盲目的に転送する」形にしやすいです。 [Tailscale+1](https://tailscale.com/kb/1232/derp-servers?utm_source=chatgpt.com)
|
||
|
||
あなたの「HTTPSさえ通れば」要件が強いなら、**方式B寄り**が実装コストと到達性のバランスが良いと思います。
|
||
|
||
* * *
|
||
|
||
追加:あなたの「v6グローバルだけでサーバを置きたい(NAT46的)」について
|
||
---------------------------------------
|
||
|
||
ここは誤解が起きやすいので先に釘を刺します。
|
||
|
||
* **IPv4しかないクライアント**は、**IPv6-onlyのサーバへは基本的に到達できません**(間に“IPv4を受けられる翻訳/プロキシ装置”が必要)。
|
||
* NAT46は「IPv4クライアント→IPv6サーバ」を実現する翻訳の一種ですが、翻訳ゲートウェイが“受け口としてIPv4側”を持つ必要があり、サーバが純IPv6だけだとどこか別の場所に翻訳器が要ります。 [Palo Alto Networks TechDocs+1](https://docs.paloaltonetworks.com/content/techdocs/en_US/prisma-sd-wan/administration/nat-protocol-translation/nat-supported-protocol-translation-types/nat46-connect-ipv4-clients-to-ipv6-servers?utm_source=chatgpt.com)
|
||
|
||
「IPv6だけで置ける場所を増やしたい」という狙いは理解できますが、**“誰でもどこでも(IPv4-only含む)”を満たすなら、結局どこかにIPv4の到達点が必要**です。
|
||
一方で日本だとIPv6普及は高いので、実用上は「v6優先 + v4はリレーで吸う」でも十分成立するケースは多いです(ただし“絶対”は言えない)。
|
||
|
||
* * *
|
||
|
||
まとめ(あなたの4問への短い指針)
|
||
-----------------
|
||
|
||
1. **鍵管理**:
|
||
* **秘密鍵は端末保持(分散)**
|
||
* \*\*公開鍵配布・ACL・設定・名前は中央(集中)\*\*が最も簡単で強い。 [Tailscale+2Tailscale+2](https://tailscale.com/kb/1093/can-tailscale-decrypt-my-traffic?utm_source=chatgpt.com)
|
||
2. **参加手法**:
|
||
* **短命/制約付きの招待トークン** + **端末生成鍵** +(任意で)**管理者承認**が運用しやすい。 [GitHub](https://github.com/juanfont/headscale?utm_source=chatgpt.com)
|
||
3. **NAT越え**:
|
||
* 枠組みは **ICE(STUN+TURN)** が教科書。 [IETF Datatracker+2IETF Datatracker+2](https://datatracker.ietf.org/doc/html/rfc8445?utm_source=chatgpt.com)
|
||
* ただし「HTTPSさえ通れば」重視なら \*\*最終フォールバックは 443/TLSリレー(DERP的)\*\*が現実解。 [Tailscale+2Tailscale+2](https://tailscale.com/blog/nat-traversal-improvements-pt3-looking-ahead?utm_source=chatgpt.com)
|
||
4. **P2Pプロトコル**:
|
||
* **WireGuardでOK**。インターフェースは基本1つでpeerを増やす。 [WireGuard+1](https://www.wireguard.com/quickstart/?utm_source=chatgpt.com)
|
||
* “経路切替・NAT越え・リレー”のために **WireGuardの外側に常駐オーケストレータが必須**。 [Go Packages+1](https://pkg.go.dev/tailscale.com/wgengine/magicsock?utm_source=chatgpt.com)
|
||
|
||
* * *
|
||
|
||
必要なら次に、あなたの要件に合わせて \*\*最小MVP(サーバ1台・ポート最小・クライアント実装の部品分割)\*\*を、
|
||
|
||
* プロトコル境界(Control API、NetMap差分、Relay framing)
|
||
* データ構造(ノードID、鍵、ACL、DNS名、経路広告)
|
||
* 推奨ポート設計(443/tcp、3478/udp 等)
|
||
まで具体化して提案できます。
|
||
|
||
## Prompt:
|
||
あと、L2的にするか、L3的にするかという問題(tailscaleのSubnet Routingみたいなものをやるかどうか?)もあります。
|
||
|
||
## Response:
|
||
Thought for 13m 4s
|
||
|
||
L2/L3は「見た目がそれっぽい」以上に、**アーキテクチャ全体(到達性・スケール・運用・セキュリティ)を決める分岐**なので、ここは最初に方針を固めた方がいいです。結論としては、あなたの要件(セルフホスト容易 / P2P優先 / 最悪はHTTPSで通す)だと、
|
||
|
||
* **基本は L3(TUN)で作る**
|
||
* **“LANをまるごと繋ぐ”は L3の「Subnet Routing(経路広告)」で実現**
|
||
* **L2(TAP/ブリッジ)は「どうしても必要なユースケースだけ」のオプション**(MVPでは避ける)
|
||
|
||
が一番きれいです。
|
||
|
||
* * *
|
||
|
||
1) まず整理:Subnet Routingは「L2にするか?」とは別問題(L3の拡張)
|
||
--------------------------------------------
|
||
|
||
TailscaleのSubnet routersは、やっていることは **L3の経路広告**で、L2ブリッジではありません。
|
||
|
||
* Subnet router側で **IP forwardingを有効化**して、`--advertise-routes=192.0.2.0/24,...` みたいに **到達させたいプレフィックスを広告**する(=Control planeに「このサブネットはこのノード経由で届く」と登録)。 [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
* 管理コンソール(またはautoApprovers)で **その経路を承認**して有効化する。 [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
* 複数広告がある場合は **Longest Prefix Matchで選ぶ**(/16より/24が優先、みたいな)。 [Tailscale+1](https://tailscale.com/kb/1019/subnets)
|
||
* デフォルトルート `0.0.0.0/0` / `::/0` を流すなら “exit node” にせよ、という整理。 [Tailscale+1](https://tailscale.com/kb/1019/subnets)
|
||
|
||
つまり \*\*Subnet Routingを実装するかどうかは「L3でルーティング機能を足すか」\*\*の話で、**L2ブリッジ(同一ブロードキャストドメイン化)とは別**です。
|
||
|
||
* * *
|
||
|
||
2) L3(TUN)で作る場合の性質:WireGuardと相性が良い
|
||
----------------------------------
|
||
|
||
あなたが前に挙げたWireGuardを採るなら、L3ベースは自然です。
|
||
|
||
* WireGuard自体が **Layer 3のトンネル**として設計されている(公式ペーパーのAbstractで明言)。 [WireGuard](https://www.wireguard.com/papers/wireguard.pdf)
|
||
* Tailscaleも「主としてネットワーク層のソリューションで、WireGuardを使う」と整理しています。 [Tailscale](https://tailscale.com/kb/1456/osi)
|
||
|
||
### L3(TUN)Mesh + Subnet Routingのメリット
|
||
|
||
* **スケールが楽**:各ノード/32(/128)で、ルーティングは「宛先IP→次ホップpeer」になり、ブロードキャストの洪水が起きにくい
|
||
* **フルメッシュでも管理可能**:WireGuard peerは増えるが、基本は `wg0` 1つにpeerをぶら下げる設計で済む(あなたの前提とも整合)
|
||
* **セキュリティ境界が作りやすい**:ACLはL3/4(IP/Port)で表現しやすい
|
||
* **“HTTPSさえ通れば”の最終フォールバック**(443/TLSリレー)も、L3パケット単位で運びやすい
|
||
※Tailscaleは「UDPが無理ならTCP(HTTPS)で暗号化トラフィックをリレーする」と説明しています。 [Tailscale](https://tailscale.com/compare/zerotier)
|
||
|
||
### L3のデメリット(ここが「L2が欲しくなる」ポイント)
|
||
|
||
* **L2ブロードキャスト/マルチキャストに依存する発見系が素直に動かない**
|
||
例:mDNS/Bonjour、SSDP、NetBIOS Name Service、ゲームのLAN探索、など。
|
||
これは(Tailscaleでも)課題として継続的に話題になります。 [GitHub+1](https://github.com/tailscale/tailscale/issues/11134?utm_source=chatgpt.com)
|
||
|
||
* * *
|
||
|
||
3) L2(TAP/仮想Ethernet)にすると何が起きるか
|
||
-------------------------------
|
||
|
||
L2にする=**「全ノードが同一のEthernetセグメントに繋がっている」ように見せる**です。
|
||
|
||
### L2のメリット
|
||
|
||
* DHCP・ARP・mDNS・SSDPなど、**L2に依存するプロトコルが“そのまま”動く**可能性が高い
|
||
* “VPN越しに同一LANっぽく”という体験は作りやすい
|
||
|
||
### ただし、あなたの狙い(簡単にセルフホスト、P2P優先、HTTPSフォールバック)と衝突しやすい
|
||
|
||
L2はインターネット越しにやると、現実のつらさが出ます。
|
||
|
||
* **ブロードキャスト/ARPの扱い**
|
||
フルメッシュで“同一セグメント”をやると、結局「ブロードキャストを全peerへ複製」する必要がある
|
||
→ ノードが増えるほど雑に増える(O(N²)寄りになりがち)
|
||
→ P2P優先のメリットを削りやすい
|
||
* **ループ/ストーム/MTU問題**
|
||
L2はSTP相当の設計や、フレームの洪水対策をちゃんとやらないと事故りやすい
|
||
* **アドレス空間の衝突が地雷**
|
||
“同一L2”を複数拠点の既存LANに伸ばすと、同じRFC1918が被って終わる、が典型
|
||
* **セキュリティ境界が曖昧になりやすい**(L2は攻撃面が増えやすい)
|
||
|
||
参考として、ZeroTierは「Virtual Layer 2をVXLAN的に作る」と説明していますが、これはそれ相応の設計領域を背負う、ということでもあります。 [Tailscale](https://tailscale.com/compare/zerotier)
|
||
|
||
* * *
|
||
|
||
4) あなたのプロダクトとしての推奨方針
|
||
--------------------
|
||
|
||
### 推奨:L3をコアにして、Subnet Routingを“必須級のオプション”にする
|
||
|
||
MVPとして一番強いのはこれです。
|
||
|
||
* 通常ノード:/32(/128)だけを持つ
|
||
* **Subnet routerノード**:
|
||
* 「このノードが `192.168.1.0/24` を背負う」みたいな経路広告
|
||
* 管理者承認(勝手にLANが公開されないように) [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
* ルート衝突はLPMで選択 [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
* **Exit nodeノード**:
|
||
* `0.0.0.0/0`, `::/0` を背負う(全トラフィック経由) [Tailscale](https://tailscale.com/kb/1103/exit-nodes)
|
||
* 利用側/提供側/管理者の“オプトイン”を要求する(勝手に出口化しない) [Tailscale](https://tailscale.com/kb/1103/exit-nodes)
|
||
|
||
### Subnet Routing実装でハマる要点(ここを仕様に落とすと良い)
|
||
|
||
Tailscaleのドキュメントが現実に寄り添っていて、あなたの実装にもそのまま使えます:
|
||
|
||
* ルータ側は **IP forwarding必須**、firewallは転送拒否がデフォルトの前提で説明されている [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
* **SNATの扱い**が超重要
|
||
* デフォルト:SNAT(masquerading)=LAN側から見ると送信元がルータになる → **設定が簡単** [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
* SNAT無効:送信元がVPN内IPのまま → **戻り経路(return route)が必要**(例としてTailscaleは `100.64.0.0/10` をサブネットルータへ向ける静的経路が要る、と明記) [Tailscale](https://tailscale.com/kb/1019/subnets)
|
||
→ あなたの実装でも同じで、\*\*「簡単モードはSNATあり」「正確モードはSNATなし+return route必須」\*\*が王道になります。
|
||
|
||
* * *
|
||
|
||
5) 「L2っぽさ」が欲しい時の現実的な落としどころ
|
||
--------------------------
|
||
|
||
L2に振り切る前に、L3のまま“困りがちポイントだけ”解決する手があります。
|
||
|
||
### A. 発見系(mDNS/SSDPなど)だけを中継する
|
||
|
||
* いわゆる **mDNSリフレクタ/ゲートウェイ**(224.0.0.251)や **SSDPプロキシ**(239.255.255.250)を、サブネットルータ上で動かす
|
||
* VPN全体をL2にせず、必要なプロトコルだけを中継
|
||
* セキュリティポリシー(誰がどの発見情報を見ていいか)が書きやすい
|
||
|
||
### B. “名前で解決”に寄せる(DNSの作り込み)
|
||
|
||
あなたが元々言っていた「名前つけてDNS広報」は、L3モデルの弱点をかなり潰します。
|
||
Subnet Routingの文脈でも「内部DNSへのルーティング」やsplit DNSの話が出ます。 [Tailscale+1](https://tailscale.com/kb/1019/subnets)
|
||
|
||
* * *
|
||
|
||
6) それでもL2が必要なら:どう設計するのがマシか
|
||
--------------------------
|
||
|
||
もし「L2がないと成立しない」ユースケースが明確にあるなら、僕ならこうします:
|
||
|
||
* **コアはL3のまま(WireGuard/TUN)**
|
||
* L2は **“L2セグメント”という別機能**として提供(デフォルトOFF)
|
||
* 例:特定グループだけが参加する“仮想スイッチVNI”を作り、そのVNI参加ノード間だけEthernetフレームを運ぶ
|
||
* 実装は VXLAN/Geneve 的な発想(ただしメンバ管理・フラッディング制御・MTU・暗黙のブロードキャスト複製など、考えることは増える)
|
||
* さらに、HTTPSフォールバック上でL2を流すなら、**TCP-over-TCPの性能劣化**やフレーム断片化に注意が必要(ここは設計で割り切る)
|
||
|
||
これを最初からやると重いので、MVPでは避けるのが無難です。
|
||
|
||
* * *
|
||
|
||
まとめ:あなたのプロジェクトに対する実装優先度
|
||
-----------------------
|
||
|
||
1. **L3 Mesh(WireGuard)をコアにする**(最も自然・簡単・堅い) [WireGuard+1](https://www.wireguard.com/papers/wireguard.pdf)
|
||
2. **Subnet Routing(経路広告+承認+SNAT制御)を実装**(“VPNを実用にする”機能) [Tailscale+1](https://tailscale.com/kb/1019/subnets)
|
||
3. **Exit node(デフォルトルート)を実装**(必要なら) [Tailscale](https://tailscale.com/kb/1103/exit-nodes)
|
||
4. それでも足りないときだけ
|
||
* 発見系の中継(mDNS/SSDP)
|
||
* さらに必要なら L2セグメント(オプション)
|
||
|
||
* * *
|
||
|
||
必要なら次は、あなたのControl plane設計に合わせて、
|
||
|
||
* 「経路広告(subnet/exit)をnetmapにどう持つか」
|
||
* 「クライアント側でAllowedIPsとOSルートをどう同期するか」
|
||
* 「SNATあり/なしの両モードのユーザ体験(設定手順)をどう分岐させるか」
|
||
|
||
を、擬似コード/データ構造まで落として提案します。
|
||
|
||
## Prompt:
|
||
L3にしようと思います。もう一つの問題は、クライアントソフトウェアの設計です。個人的には、一つのクライアントのマシンがいくつかのネットワークに接続できたほうが望ましいと思っている(tailscaleはこれができない!)のですが、どうでしょうか。ただ、問題は適当にやるとIP帯がかぶるところですね。
|
||
|
||
## Response:
|
||
Thought for 14m 19s
|
||
|
||
L3で行くなら、「1台の端末が複数ネットワークに同時参加できる」は**かなり価値のある機能**です(仕事用・個人用・検証用・顧客環境など)。実際、Tailscaleも“複数アカウントを素早く切り替える”機能はありますが、**同時に複数tailnetへパケット送信はできない**と明記しています。 [Tailscale](https://tailscale.com/kb/1225/fast-user-switching)
|
||
(なお組織が複数tailnetを持てる機能も出ていますが、これは主に管理側の話で、クライアントが同時接続できる話とは別です。 [Tailscale+1](https://tailscale.com/kb/1509/multiple-tailnets))
|
||
|
||
あなたが作るなら、**同時参加を最初から設計に入れる**のはアリです。ただし、言う通り「IP帯がかぶる」問題が出るので、そこを**仕様で潰す**のがポイントになります。
|
||
|
||
以下、設計のおすすめを「現実に運用できる線」でまとめます。
|
||
|
||
* * *
|
||
|
||
1) まず分けて考えるべき「かぶるIP」の種類
|
||
-----------------------
|
||
|
||
### A. オーバレイ内(VPN端末同士)のIP
|
||
|
||
これは**あなたのVPNが配るアドレス**なので、設計でほぼ完全に回避できます。
|
||
|
||
### B. Subnet Routingで流す“向こう側LAN”のIP
|
||
|
||
こっちは現実世界のLANが `192.168.0.0/16` とかで乱立するので、**高確率で衝突**します。
|
||
同時に2つ以上のネットワークへ参加するなら、衝突が起きたときの挙動が実質“仕様の核”になります。
|
||
|
||
* * *
|
||
|
||
2) 「複数ネットワーク同時参加」を成立させるクライアント設計の基本方針
|
||
------------------------------------
|
||
|
||
### 結論(おすすめ)
|
||
|
||
* **ネットワークごとに“独立したルーティングドメイン”を持つ**発想で作る
|
||
* ただしOSの通常ルーティングテーブルは“宛先IPだけ”で決めるので、
|
||
**衝突する経路を“そのまま”同居はできない**
|
||
→ だから、衝突を避ける(オーバレイ)+衝突したら方針を決める(サブネット) が必要
|
||
|
||
* * *
|
||
|
||
3) 実装アーキテクチャ:一番ラクなのは「マルチインスタンス」
|
||
-------------------------------
|
||
|
||
あなたのソフトを「同時に複数ネットワーク参加可能」にする最短ルートはこれです。
|
||
|
||
### 方式A:**1ネットワーク = 1デーモン(推奨)**
|
||
|
||
* `clientd --profile netA` が `tun-netA` を作る
|
||
* `clientd --profile netB` が `tun-netB` を作る
|
||
* UI/CLIだけ “1つのクライアント” に見せれば良い
|
||
|
||
**メリット**
|
||
|
||
* 実装が単純(状態・鍵・netmap・ルート注入が分離)
|
||
* バグっても片方に閉じ込めやすい
|
||
* “このネットワークだけ落とす/再接続” が容易
|
||
|
||
**デメリット**
|
||
|
||
* DNS統合(後述)が少し面倒
|
||
* OSごとにTUN複数の扱いが違う(Windows/macOS周りは要検証)
|
||
|
||
### 方式B:1デーモンで複数ネットワーク管理
|
||
|
||
* 中長期ではUXが良いけど、最初は事故りやすい(状態機械が重なる)
|
||
|
||
**MVPは方式A**が堅いです。Tailscaleでも“複数同時接続したい”需要は昔からIssueになっていて、回避として複数デーモンを立てる話が出ます。 [GitHub](https://github.com/tailscale/tailscale/issues/183)
|
||
|
||
* * *
|
||
|
||
4) オーバレイIPの衝突を「設計で潰す」方法
|
||
-----------------------
|
||
|
||
ここはあなたが自由に決められるので、**衝突が起きない割当**にしてしまうのが最強です。
|
||
|
||
### 推奨:ネットワークIDからプレフィックスを決定する
|
||
|
||
ネットワークには必ず一意な `NetworkID`(例:コントロールサーバの公開鍵、またはネットワーク作成時に生成するUUID相当)を持たせて、そこから
|
||
|
||
* IPv6:ULA /48(RFC 4193の範囲 `fd00::/8`) [IETF Datatracker](https://datatracker.ietf.org/doc/html/rfc4193?utm_source=chatgpt.com)
|
||
* IPv4:Shared Address Space `100.64.0.0/10`(RFC 6598)から /24 など [IETF Datatracker](https://datatracker.ietf.org/doc/html/rfc6598?utm_source=chatgpt.com)
|
||
|
||
を**決定的(deterministic)に生成**します。
|
||
|
||
#### 例(イメージ)
|
||
|
||
* `overlay_v6_prefix = fd00::/8 + H(NetworkID)の先頭40bit → /48`
|
||
* `overlay_v4_prefix = 100.64.0.0/10 の中から H(NetworkID)で /24 を選ぶ`
|
||
|
||
これにより、**別ネットワーク同士でもオーバレイがかぶりにくい**(実質“ほぼ起きない”)状態を作れます。
|
||
|
||
> 補足:IPv6 ULAは「ローカルで使うがグローバル一意性を期待できる」用途として定義されています。 [IETF Datatracker](https://datatracker.ietf.org/doc/html/rfc4193?utm_source=chatgpt.com)
|
||
> IPv4の `100.64/10` はCGN向けの共有アドレス空間としてIETFが予約したブロックです。 [IETF Datatracker](https://datatracker.ietf.org/doc/html/rfc6598?utm_source=chatgpt.com)
|
||
|
||
### ルート注入も衝突しにくくする
|
||
|
||
* 各ネットワークのオーバレイは **そのネットワーク固有のプレフィックスだけ**を `tun-netX` に向ける
|
||
* 端末同士は /32(/128)でもいいし、プレフィックスまとめても良い(規模次第)
|
||
|
||
WireGuard自体もL3トンネルとして整理されているので、この方向は素直です。 [WireGuard+1](https://www.wireguard.com/papers/wireguard.pdf?utm_source=chatgpt.com)
|
||
|
||
* * *
|
||
|
||
5) 本題:Subnet Routingがあると“衝突が必ず起きる”問題
|
||
------------------------------------
|
||
|
||
ここは「何を保証するか」を決めないといけません。おすすめは段階的です。
|
||
|
||
### レベル1(推奨デフォルト):**衝突したサブネット経路は“入れない”**
|
||
|
||
クライアントが受け取った advertised routes を、OSのルートテーブルへ入れる前に
|
||
|
||
* 既存ルート(ローカルLAN含む)
|
||
* 他のVPNネットワークから入っているルート
|
||
|
||
と照合して、**プレフィックスが重なるものは拒否**します。
|
||
|
||
ユーザ体験としては:
|
||
|
||
* “netB の `192.168.1.0/24` は netA(またはローカル)と衝突するので無効化しました”
|
||
* どのルートが衝突してるか、解決策(後述)を提示
|
||
|
||
これで \*\*「同時参加はできるが、衝突サブネットは使えない」\*\*という一貫した挙動になります。
|
||
MVPとしては非常に現実的です。
|
||
|
||
### レベル2:手動優先度(メトリクス)でどちらか一方を採用
|
||
|
||
* 衝突した2つの `192.168.1.0/24` のうち、**片方だけ**を採用
|
||
* UIで “この経路はnetA優先/netB優先” を選ばせる
|
||
|
||
ただし、同時に両方は使えません(OSルーティングは宛先IPで決まるため)。
|
||
|
||
### レベル3(上級者向け):**サブネットのアドレス変換(route translation / netmap)**
|
||
|
||
「同時に両方の `192.168.1.0/24` を使いたい」を満たす現実解はこれです。
|
||
|
||
#### 発想
|
||
|
||
* netBの `192.168.1.0/24` を、そのネットワーク内では **仮想プレフィックス `172.31.200.0/24` にマッピング**して提供する
|
||
* クライアントは `172.31.200.0/24` 宛に送る
|
||
* Subnet Router(netB側)が **1:1のプレフィックス変換**で `172.31.200.x ⇔ 192.168.1.x` を行う
|
||
|
||
こうすると、
|
||
|
||
* ローカルLANの `192.168.1.0/24`
|
||
* netAの `192.168.1.0/24`
|
||
* netBの(変換後)`172.31.200.0/24`
|
||
|
||
が**同時に共存**できます。
|
||
|
||
**ただし仕様の重さ**
|
||
|
||
* 変換後のアドレスをユーザにどう見せるか(DNSで変換後を返す、など)まで決める必要がある
|
||
* ルータだけでなく、クライアント側も「この経路は変換モード」を理解する必要がある(実装は可能)
|
||
|
||
MVPではレベル1(拒否)で十分、要望が出たらレベル3を足すのが良いです。
|
||
|
||
### レベル4(Linux限定の最終手段):VRF / network namespace
|
||
|
||
* OS側でルーティングテーブルを分離して“同じ宛先プレフィックス”を共存させる
|
||
* ただし「そのネットワークを使うアプリ」を名前空間に入れる必要が出て、透明性が落ちます
|
||
* クロスプラットフォームでは厳しい
|
||
|
||
* * *
|
||
|
||
6) “同時に複数ネットワーク参加”を入れるなら、追加で決めるべき仕様
|
||
-----------------------------------
|
||
|
||
### (a) Exit node(デフォルトルート)は同時に1つ、が基本
|
||
|
||
複数ネットワークで同時に `0.0.0.0/0` を入れるのは混乱のもとです。
|
||
|
||
* 「どのネットワークのexitを使うか」を明示選択
|
||
* もう一方はexit機能をOFFにする(またはアプリ単位の選択にする)
|
||
|
||
### (b) DNS名の衝突
|
||
|
||
netAにもnetBにも `db` みたいな名前があると衝突します。
|
||
解決策は素直に
|
||
|
||
* ネットワークごとにDNSサフィックスを持つ(例:`db.netA.vpn`, `db.netB.vpn`)
|
||
* OSへはsplit DNS(or ローカルスタブDNSで集約)
|
||
が堅いです。
|
||
|
||
### (c) ネットワーク間の“意図しない中継”を防ぐ
|
||
|
||
同じホストが2ネットワークに入ると、事故でルーティング(転送)してしまう可能性があります。
|
||
|
||
* デフォルトで **IP forwardを有効化しない**
|
||
* 「サブネットルータ/exit node」は明示的に有効化+承認制
|
||
|
||
* * *
|
||
|
||
まとめ:あなたの状況へのおすすめ
|
||
----------------
|
||
|
||
あなたが「複数ネットワーク同時接続」を売りにしたいなら、こう決めると破綻しにくいです。
|
||
|
||
1. **クライアントはマルチインスタンス(1ネットワーク=1デーモン)でMVP**
|
||
2. **オーバレイIPは “NetworkIDから決定的に生成”**
|
||
* IPv6はULA /48(RFC 4193) [IETF Datatracker](https://datatracker.ietf.org/doc/html/rfc4193?utm_source=chatgpt.com)
|
||
* IPv4は100.64/10内(RFC 6598) [IETF Datatracker](https://datatracker.ietf.org/doc/html/rfc6598?utm_source=chatgpt.com)
|
||
3. **Subnet Routingは衝突検出して、衝突した経路はデフォルト拒否(レベル1)**
|
||
4. どうしても必要になったら \*\*“サブネット変換(レベル3)”\*\*を追加
|
||
|
||
* * *
|
||
|
||
もしよければ、次は「衝突検出ロジック」と「ネットワークIDからのプレフィックス生成」の具体案(ビット割り当て、衝突時のリトライ規則、サーバ側のネットワーク作成API仕様、クライアントのルート適用順序)を、実装に落とせる粒度で提案できます。
|