P2P通信の仕組み、NATタイプ、Tailscaleで別のIPを割り当ててサブネットを公開する方法、などについて

最近の自宅ネットワーク環境の更新に伴って前々回前回と記事を書いてきましたが(特に前回記事はオススメ!)、それに引き続いて自宅までVPNで接続できる設定などをしていたので、今回はそれに関する記事です。

本来はTailscaleで別のIPを割り当ててサブネットを公開する方法について書きたかっただけだったのですが背景となるP2Pの仕組みとかについて調べて書いていたら結局そっちの方が長くなってしまいました。しかしもともと筆者がそれほど詳しい分野ではなく、説明が間違っている可能性も十分ありますので注意してください。

インターネット接続とポートの動作

まず、ポートの動作について基本的なところを説明します。

普通、インターネットを通じて接続を確立しようと思ったら、接続に関して、そこにたどり着くためのグローバルIPアドレスとその上の固定的なポートが必要です。例えば、Googleのトップページであるgoogle.comがインターネットで閲覧できるのは、(google.comに関連づけられた)IPアドレスがあり、その80番ポートにサーバーが立っているからです。

一方、インターネットへの接続に関しては、固定的なポートを割り当てるのではなく、接続に際して一時的なポートエフェメラルポート)を開放して(割り当てて)、必要な応答が返ってきたらすぐに閉じる(割り当て解除する)、という方法で接続を行うのが普通です。いつなんどき世界のどこから接続が来るかわからない接続先サーバーでは常に固定のポートが必要ですが、自分から接続して応答を待つだけの接続元では毎回違うポートを使っても大丈夫ですし、そのほうが複数の接続を捌くには便利です。

例えば、PCが直接インターネットに接続していて1.1.1.1というグローバルIPを持っていたとします。そのPCでgoogle.comを見たい場合、PCはまず空いているエフェメラルポートを一つ探します。例えばそれが50000だったら、1.1.1.1:50000からgoogle.com:80にむけて通信を行い、その応答が1.1.1.1:50000に返ってきます。ページの読み込みが終わったら(ブラウザで言えば、読み込み中のぐるぐるマークが消えたら)、もう50000番ポートを使う必要はないので解放(開放ではない)して、未使用になります。もし、google.comの読み込み中に別のサイト(例えばamazon.com)に接続するとしたら、空いている別のエフェメラルポート(例えば50001とか)を使って同じようなことをすることになります。ちなみにPC側の使用ポートはアクセス情報【使用中のIPアドレス確認】などを使うとわかります。

ちなみに、このようにグローバルIPを持った端末なら、何かのポートを使用するアプリケーションを普通に起動すれば(&適宜ファイアウォールを開ければ)ただちにそれはインターネット上の誰でも使える接続先として機能するようになります。

しかし実際には、世界中の接続端末ごとに一つずつグローバルIPアドレスを割り当てるにはIPv4アドレスが足りないので、通常はルーターだけをインターネットに直接接続し(=グローバルIPを持たせる)、ルーターNAT(NAPT)という仕組みを用いて自身の配下の端末のインターネット接続を"代行"するという方法が使われます。この際は、端末(PCなど)と全く同様にルーター自身も一時的なポートを使うことになります。

例えば、先ほどの例にルーターを追加してみましょう。1.1.1.1というグローバルIPを持ったルーターがあって、その配下に10.1.1.1というプライベートIPを持ったPCがあるとします。この場合、PCからgoogle.comに接続したければ、まずPCは空いているエフェメラルポート(例えば50000)を使って、「10.1.1.1の50000番に、google.com:80の内容を持ってきてくれ」とルーターに依頼します。するとルーターは、自身のエフェメラルポート、例えば60000(このように50000から60000へとポート番号まで変換されうることを指してNAPTと呼んでいます)を使って、google.com:80と通信します。google.comからの応答が1.1.1.1:60000に返ってくるので、その内容をもともとの依頼元である10.1.1.1:50000に返せば、最終的にPCからgoogleのサイトが見られるというわけです。この接続の途中でamazon.comも見たくなったら、PCは50001番ポートを使い、ルーターは60001番ポートを使ってそれをamazonに転送し…という感じになります。先ほどと同じくアクセス情報【使用中のIPアドレス確認】ルーター側の使用ポートが確認できます。

ルーターが外から受け入れるのはあくまで上記のような「自身の配下の端末に依頼された内容への応答」だけなので、この状況だと、PCで特定のポートを使うアプリケーションを起動するだけでは外から通信できません。なぜなら、外から見えているのはルーターだけであり、ルーターの適当なポートにいきなり外からアクセスしたとしても、それは配下の端末からの依頼への応答ではないので拒否されてしまうからです。そこで、ルーターの設定で、「ルーターの20000番ポートは常に開けておいて、そこに来た通信は無条件で全て10.1.1.1の30000番ポートに転送」というように先ほどのNAPTを静的に(常にポート番号・LAN側の宛先を固定して)行うようにします(これがいわゆるポート開放設定)。その上で10.1.1.1において30000番ポートを使うアプリケーションを起動すれば、ようやくPCが接続先として機能するようになります。

ただ、ポートを開放するということはセキュリティ的なリスクを伴います。また、インターネットの契約によってはこのルーターの設定にあたる部分が外部(マンションの管理室やプロバイダ側設備)にあって手出しできない場合も多くあります。LAN配線方式の集合住宅やモバイル回線、あるいは(光配線方式・VDSL方式であっても)DS-Lite方式のIPv4通信などはこれに該当します。この場合だと、自宅へとつながるグローバルIP上のポートを確保することができません。

一旦内から外への接続が確立してしまえば外から内へと固定的に通信することは可能(例: SSHのリモートポートフォワード、各種VPN)(概念的には、モバイル端末であってもメールやLINEやTwitterのメッセージを外から受信できるのと同様)なので、他のサーバーを起点としてそこから家に通信が入るようにするという回避策はあります。VPSやngrok, Cloudflare Tunnelなどのサービスは実際このような目的でよく使われます。ただ、コストがかかることもありますし、中継するわけなのでそのサーバーの品質に全体の通信品質が左右されます。あるいは別の方法として、自宅に接続しようとしているまさにそのPCが接続として使える可能性もあります。例えば、外出先で使っているPCにグローバルIPが割り当てられていたり、あるいは経由している(グローバルIPが割り当てられた)ルーターの設定をいじれたり(親戚の家にいる等)する場合は、自宅からそのグローバルIPに接続するように(別ルートで)命令することで自宅との通信を確立できます。しかしそういった環境にいる(そういう場合にのみ使えればよい)ことは稀です。それに、いずれにしてもどこかしらではポート開放が必要なのでセキュリティリスクが若干増えます。

長くなりましたが、まとめると、インターネットから自宅に接続しようと思ったらグローバルに接続可能な公開ポートを自宅に設置する必要があるが、金銭コスト・品質・セキュリティリスクの面でそれが難しい場合もある、ということです。

P2P通信とUDPホールパンチング

しかし実は、この「インターネットから自宅に接続しようと思ったら自宅に接続可能な公開ポートを設置する必要がある」というのはあくまで原則論で、実は回避する方法が存在します。つまり、インターネットに公開されたポートがどこにもなくても、ルーター(NATを行う機器)の内側にいる離れた端末同士でインターネットを介して接続を確立できる技術が存在します。それがUDPホールパンチングです。これにより、中継サーバーを介しない端末同士の通信、いわゆるP2P(Peer to Peer)通信が実現します。グローバルIPを持っている自宅と通信したい場合に限ったとしても、静的なポート開放が不要というメリットがあります。

UDPホールパンチングについての詳細な解説はしませんが、要は、固定的(静的)なポート開放設定ができなくても、どれかのポートを一時的に開放させることはできるので、その一瞬の隙をついて(穴を開けて)互いにそのポートを使って通信してしまおう、ということです。

ただし、どんな場合でもUDPホールパンチングが成立するわけではありません。NATの動作にはいくつか分類があり、それによって、どれくらいその「隙」を突きやすいかが異なります。

この分類は、RFC 4787という規格において、「Mapping」「Filtering」という2つの特性がそれぞれ「Endpoint-Independent」「Address-Dependent」「Address and Port-Dependent」のどれに当てはまるかという3x3=9種類が定められています。3つの中では、後にいくほど「隙」が少ない(UDPホールパンチングが難しい)ものになります。これらに加えて、ポート番号の偶奇がNATで維持されるかといった細かい性質もあるのですがそれにはここでは触れません。

9種類といっても、まずMappingがどれであるかで3種類に大別されて、その上でFilteringがどれであるかで細分化されるというイメージがよいでしょう。メジャーなのは、まず「Endpoint-Independent Mapping」のもの(最もホールパンチングしやすいグループ)です。この中では3種類の「Filtering」方式である「Endpoint-Independent Filtering」「Address-Dependent Filtering」「Address and Port-Dependent Filtering」がいずれもよく利用されており、これらは以前の規格(RFC 3489)で「Full Cone NAT」「(Address-)Restricted Cone NAT」「Port-Restricted Cone NAT」と呼ばれているものにそれぞれ対応します。また、最もホールパンチングが困難な「Address and Port-Dependent Mapping」は、RFC 3489で「Symmetric NAT」と呼ばれているものに対応するようです。「Address-Dependent Mapping」はあまり使われていないようです。

実際には、今でもFull Cone NATやSymmetric NATなどの旧来の用語が使われることも多く、これらで十分な場合もあるのですが、一応新しい分類のほうが正確とされているようです。しかし以降では便宜的に旧来の用語を使用することもあります。

これらの中で、もっとも隙の大きいFull Cone NATだと、実質的には内側からポートを一時的に全世界にむけて開放することができるような状態なので、ほぼ確実にUDPホールパンチングが成功します。一方、制限が厳しいSymmetric NATだと、通信を行いたい特定の相手(ポートとアドレスの組み合わせ)に対してしかポートを開けることができないので、成功率が下がります。

http://toremoro21.world.coocan.jp/study/voip2008/NATTraversal.pdfのP.21によると、通信を行うどちらか一方がFull ConeあるいはRestricted Cone NATである場合、あるいは双方がPort Restricted Cone NATである場合は成功するが、Port Restricted Cone NATとSymmetric NAT、あるいは双方がSymmetric NATである場合は不可能、などとされています。英語で調べてもこのような情報が最も多いです。一方、開発者ドキュメント|SkyWay(スカイウェイ)では、Restricted ConeとSymmetricの組み合わせでもダメと書いてあります。また、この後で扱うVPNサービスであるTailscaleのサイトにも How NAT traversal works · Tailscale という大変詳しい記事があるのですが、こちらではEndpoint Independent Mappingの中での区別にはあまり言及されておらず、どちらか片方でもEndpoint Independent MappingであればUDPホールパンチングが成立する(つまり、Port-Restricted Cone NATとSymmetric NATの間でも成立する)という意味にも読めなくはありません(しかし後述のように自分の環境ではその通りにはなりませんでした)。このへんの細かいところはサイトによっても違いがあり、よくわかりません。Symmetric同士であっても次に解放されるポートを予測するなどの方法によって確率的にUDPホールパンチングが成立する場合もあるようです。

NATというのは一応ファイアウォール的な役割も果たしており、その「隙」を突くというのはイケナイことのようにも思われるかもしれませんが、あくまでこれはNATの内側の端末の協力がないと成立せず、外からいきなり内側に侵入できるようなものではありません。言い換えれば、既に内側に「侵入者」がいないと侵入できないという点で安全といえます。ただし、それでもセキュリティ的に厳しい組織などであればそれっぽい通信がブロックされたり偉い人に怒られたりという可能性も一応なくはなさそうなのでそこは注意してください。

これより詳しい解説は以下のサイトなどを参照してください(他にもいろいろあります)。

UDPホールパンチング - Wikipedia

Hamachi - Wikipedia

NAT Traversalって知ってますか | Cerevo TechBlog

https://www.janog.gr.jp/meeting/janog30/doc/janog30-v64-pre-stun-ryosato-01.pdf

WebRTCの裏側にあるNATの話 / A talk on NAT behind WebRTC - Speaker Deck

Cloud NAT の Endpoint-independent Mapping とは? | by Seiji Ariga | google-cloud-jp | Medium

インターネット環境のNATタイプの確認・変更

UDPホールパンチングが成立しそうかどうか確かめるために、自分のインターネット接続環境のNATタイプを確認したくなります。これについてはあまり情報がなく探すのに苦労したのですが、PCからだとStuntman - open source STUN serverを使うのが良さそうです。

Stuntmanの使い方はport - How do i check my nat type using stun? New on this - Stack Overflowにある通りで、MappingとFilteringに関してそれぞれ結果を表示できます。また、IPv4またはIPv6(デフォルトはIPv4)、プロトコルとしてUDPまたはTCP(デフォルトはUDP)、送信元IPアドレス・ポートなどをオプションで指定できます。

自分が試したところ、以下のような多くの環境では、UDPについてはEndpoint Independent MappingAddress and Port Dependent FilteringTCPについてはAddress and Port Dependent MappingEndpoint Independent Filteringとそれぞれ判定されました。従来の用語でいえば、UDPについてはPort-Restricted Cone NAT、TCPについてはSymmetric NATということになります。

・自宅のOCNバーチャルコネクトによるMap-e接続(ルーターはRX-600KI、UPnP無効、プロバイダはOCN。詳しくは前々回前回も参照。)

・自宅のPPPoE接続(ルーターはBuffaloのWSR-1166DHPL2、ほか同上)

テザリング経由のスマホdocomo回線

・コンビニのd-WiFi

・渋谷駅の「SHIBUYA Wi-Wi-Fi

一方で、筆者の所属大学のWi-Fiや、東京メトロが提供するMetro_Free_Wi-Fiでは(注: 駅によるかも)、TCPについては上記と同様でしたが、UDPについてはAddress and Port Dependent MappingAddress and Port Dependent Filteringと判定されました。つまりUDPに関してもSymmetric NATであるということになります。

他にはpythonで書かれたPyStun3というのもありました。OCNバーチャルコネクトではRestric NAT(これはAddress-Restricted NATの意味らしい)、モバイル回線にするとSymmetric NATになりました。この辺はよくわかりません。新しい用語を使っているStuntmanのほうが良さそうな気がします。

あとはNAT Test: Am I behind a Symmetric or Normal NAT?というサイトもありますが、Normal/Symmetricの2通りしかない上に、RX-600KIでもSymmetric NATと表示されたのでちょっと微妙そうです。HTTPで見ているわけなので、TCPに関してしか判定してくれていないかも?GitHub - nthack/NatTypeChecker: A useful NAT type checker with STUN protocol, base JS. Using Google stun server, available. 一个使用STUN协议且基于JS的可用的NAT类型检查工具. 使用Google的stun服务器,可用,记得看看自己是否能访问Google。も同様でした。

P2P通信はゲーム機でも使われるので、ゲーム機にも判定機能が付いている場合があります。NintendoSwitchではNATタイプA/B/C/D/Fといった形で判定してくれるようです。自分は持っていないので確かめていません。XBoxにもあったりするようです。

一般には、普通の家庭用ルーターUDPに関してSymmetric NATであることはあまりない(参考:【Switch】Splatoon2/スプラトゥーン2 S帯スレ49【質問/雑談/愚痴】など)ようです。

NATタイプを変更するための直接の設定項目は普通ありませんが、UPnPを有効にするとFull Cone NATになるという話があるようです(ただしセキュリティ的にはやはりあまり良くなさそう)。また、ゲーム機に関しては、ルーターUDPポートをゲーム機にむけて全開放すると解決することもあるようです。例えばスプラトゥーン2などのオンラインゲームが遊べない「NAT越え失敗」の原因と解決方法【Switch】では、全ポートは使えないOCNバーチャルコネクトですがうまくいったようで、さらにポート開放の宛先ではない別のSwitchからもいけるようになったとのこと。。不思議なものです。Nintendo Switchに関してはNintendo Switch の「NATタイプ」判定条件 #Network - QiitaSwitchに「NAT越え失敗(2618-05**)」のエラーが出てフレンドさんと遊べない原因と対策も詳しいです(全体的にNATタイプに関する情報はゲーム界隈で豊富に出回っているようです)。

あるいは、2重ルーターになっている場合は実質的にSymmetric NATになってしまっている可能性もあるようなので、内側のルーターをブリッジモードにしましょう。ちなみに自宅で2重ルーターにしてPyStun3を試しましたが変わらずRestrictedでした。Stuntmanは未調査。NAT Testのほうでは何ならSymmetricからNormalに変わりました。意味不明。ただ、2重ルーターにしたからといってただちにSymmetricになるということは無さそうです。

ところで、LAN配線方式のマンションで使われているルーターがどのNAT方式なのかというのは実用上も非常に気になるところですが、これに関してはあまり情報がなくわかりませんでした。

また、Linuxであれば、以下のコマンドによって自分自身の環境をSymmetric NATに変えられるようです。(eth0のところは各自の環境にあわせて変更)

sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE --random

正直原理はそこまでよくわかっていませんが、ホールパンチングが成立するかどうかのテストに使えそうです。

「ポートセービングIPマスカレード」関連について

続・v6プラスの“怪しいウワサ”は本当か? ファクトチェックQUICとNATで確かめたいと思うこと (MAP-EでのNATとNAT64) #nat - QiitaYAMAHA RTX1200でMAP-Eした(けどやめた) | 愚行録 the Next GenerationUbuntu / Debian でIPv4 over IPv6 (OCNバーチャルコネクト, v6プラス), systemdによる設定, ルーター化, VPNおよび自宅サーバー可能な固定グローバルIPv4アドレス #RaspberryPi - Qiitaなどにあるように、「ポートセービングIPマスカレード機能」というのがある場合、ルーターは接続先ホストが異なる場合に同じポートを使いまわすことがあります。これによって、v6プラスのように240ポートしか使えないような状況でも円滑に通信を行うことが可能になります。

で、よくわからないんですが、これがSymmetric NATに対応する動作ということになる?というような話があります。おそらく、あるホストとの通信に使っているポートに別のホストから通信が来たとしても、それが全く別の通信という扱いになる(それに対応する通信がなければ破棄される)からでしょうか。。。

ちなみに「ポートセービングIPマスカレード機能」というのはYAMAHA用語で、最近のYAMAHAルーターではTCP通信のみに関してデフォルトでこの動作が選択されると明記されているものがあるようです。他社ルーターでもv6プラス(使えるポートが特に少なく、240しかない)やtransixなどのIPv4 over IPv6で通信する場合は暗黙的にこれと同等の機能が使用されている可能性が高いのではないかと思います。実際、自宅では、IPv4 over IPv6のほうだけでなくPPPoE通信(Buffalo)のほうでもTCPがSymmetric、UDPがPort-Restrictedと判定されたわけなので、この機能が原因である可能性もあるでしょう。UDPに関しても「ポートセービングIPマスカレード機能」的なものを設定できるルーターもあるようです。この場合ホールパンチングがうまくいかないかもしれないので設定を変更してください。

参考になりそうなサイト: YAMAHA ルータと Splatoon 2 - mura日記 (halfrack)

ヘアピンNAT問題

多くのNAT機器(ルーター)では、ルーターのWAN側のIP(グローバルIPとか)をLAN側から指定しても正しくルーティングしてくれません。例えば1.1.1.1というグローバルIPをもつルーター側で8080番ポートを開放して「1.1.1.1:8080」へのアクセスが「192.168.1.2:8080」に転送されるようにしていたとしても、このルーター内側にあるPCで「1.1.1.1:8080」にアクセスした場合は接続できません。

このようなルーティングを可能にする機能がヘアピンNATです。イメージ的にはNAT機器のところでヘアピン状にルーティングが折り返している感じです。NAT機器によってはこれをサポートしているものもあります。

で、これが今までの話と関係してくることがあり、それは同じNAT機器の内側でP2P通信したい場合です。詳しくは筆者自身よくわかっていないのですが、さっきのTailscaleの記事とかを読んだ感じだと、このときにNAT機器にヘアピンNAT機能がないとP2P通信がうまくいかない可能性がありそうです。単純なケース(同じルーターの配下で同じサブネットに属している2つの端末の間)では(手元で試した限りでも)うまくいきそうですが、CGNATのような大規模なNATの配下(同一会社のモバイル回線を使っている2つの端末間)や、IPv4 over IPv6で同じIPv4アドレスが割り当てられた家の間とかでP2P通信をする場合はうまくいかないことがあるのかもしれません。未調査。

STUN/TURN/ICEとかについて

UDPホールパンチングをするには最初に一時的にポートを開けるための仮の通信先が必要です。そのために一時的に使われるサーバー(あるいはそれを使う手法?)をSTUNと呼びます(さっきからSTUNという言葉は出てきていますが)。また、上記のように一般にはUDPホールパンチングが成立するとは限らないので、P2Pを謳う多くのサービスでは、UDPホールパンチングが失敗したらリレーサーバーを用います。このリレーサーバー(のプロトコル?)がTURNと呼ばれるようです。で、STUNがダメだったらTURNにフォールバックするというこの仕組み全体を指す言葉がICE?のようです。

見ての通り詳しくはよくわかってないので調べてください。

P2PVPN

やっとVPNの話に入ります。UDPホールパンチングの仕組みを活かしてVPNを構築するのがP2PVPNと呼ばれるサービスで、Hamachi(先ほどもリンクを載せましたが)、ZeroTier、そして今回使うTailscaleなどがあります。STUNサーバーにあたるものが要るのと、UDPホールパンチングが必要なので、(P2P型ではないVPNサーバーを立てることに比べると)なかなか個人でやるのは難しそうです。

TailscaleはWireguardというVPNソフトウェアを使用していて、これのパフォーマンスがいいのでTailscaleもパフォーマンスがいいらしいです。一方、Zerotierは独自プロトコルを使っていて、TailscaleがL3(IPレベル)のVPNであるのに対して、より低レイヤであるL2(Ethernetレベル)のVPNが構築できる(直接LANケーブルでつないでいるのと同じ扱いになるので、NetBIOSとかDLNAとかがインターネット越しに使えるようになる)という強みがあるようです。

あとこれはVPN一般に当てはまるのかTailscaleだけなのかわかりませんが、VPNが上手い具合に接続状況を仮想的に維持(=単にしばらくパケットロスが続いたのと同じ動作をする)してくれるので、Wi-Fiがちょっと切れてまたつながったりとかアクセスポイントを切り替えたりしてもVPN経由のssh接続はそのまま維持されるのが良い感じです。

Tailscaleの基本的な設定

設定は簡単で、Googleアカウントなどを使ってアカウントを作って、各クライアント端末にTailscaleソフトを入れてログインしておくだけで、クライアント同士がVPN接続されます。他にも色々サイトがあるので参考にしてください。

TailscaleでUDPホールパンチングができているか確認

TailscaleではUDPホールパンチングを勝手にやってくれますが、うまくいかなかった場合はリレーサーバーを使います(さっきのICEみたいな感じだが、DERPという独自プロトコルを使っている模様)。

tailscale pingコマンドによって実際にUDPホールパンチングが成立しているか確認できます(参考: Direct vs relayed connections · Tailscale Docs)。ホールパンチングが未成立の状態では

pong from my-device-name (100.x.y.z) via DERP(tok) in 190ms

というような応答が連続して返ってきます(上記の"tok"は東京にある中継サーバーという意味)。成立すると、

pong from my-device-name (100.x.y.z) via xxx.yyy.zzz.www:60760 in 99ms

というような応答が返ってきて、そこでコマンドが終了します。

自分が試した限りだと、(UDPに関して)双方がPort-Restricted Cone NATである場合にはホールパンチングが(数回のDERP応答の後に)成立しますが、Port-Restricted Cone NATとSymmetric NAT(例えばMetro_Free_Wi-Fiに接続した端末から自宅の端末)だとホールパンチングが成立しませんでした。

先ほどのリンクにありますが、ホールパンチングがなかなか成立しない場合、自分でポート開放できる環境であれば、41641番(設定ファイルで変更可)のポートを開放してUDPを通すとそこを使ってくれる(この場合、ホールパンチングではなくただの「中継サーバーを使わない直接接続」とでも呼べばいい)ようです。自宅やクラウドなどがSymmetric NATである場合、あるいはPort-Restricted Cone NATであるものの大学などのSymmetric NATの内側から接続したいことが多いという場合はこちらを選ぶのもよいでしょう。なお、セキュリティのためにはポートの開放をしなくて済むほうがよいと今まで述べてきましたが、UDPTCPと違って接続指向のプロトコル(ACKが返ってくる的な)ではないため、仮にUDPポートを開放していたとしても外部からそれを判断するのは(通信を傍受などしない限り)難しいというのをこの記事を書いているときに知りました。いわゆるポート開放確認ツールが原則的にTCPにしか対応していないのはそういうことだったようです。SSHのポートを外部公開していると怪しい国からログイン試行が来て気分が悪いみたいな話がありますが、そういうことはどうやら無さそうです。ということで、結局筆者もUDPポートを一つ解放する運用にしました。

ただし、Linuxで普通にポート開放すると、Tailscaleが起動していない時にそのポートが「閉じている」ことが分かってしまう(他のポートと挙動が異なるのが見える)という問題があります。これを回避する方法などに関してはDROP vs REJECT論争、そしてWindowsとLinuxのファイアウォールの動作の違いについて - turgenev’s blogで詳しく述べたのでお読みください。

設定ができたら、先ほどのようにSymmetric NATを模倣したLinuxからつながるかどうか試してみましょう。

ちなみに、大学・企業の設備などでは全てのUDP通信がブロックされているネットワーク環境だと何をどうやっても(ポート開放をしても)直接接続はできず、全てDERP経由になります。

サブネットの公開

Tailscaleでは、subnet router機能というのがあり、Tailscaleをインストールしている端末から見えている他のネットワーク機器をサブネット単位でまとめて公開(シェア)することができます。例えば、tailscaleの入っていないPCが192.168.1.12だとして、プリンター(もちろんtailscaleをインストールするのは無理)が192.168.1.6だとすると、PCのほうで

tailscale up --advertise-routes=192.168.1.0/24

とすることで、(そのPCが自宅LAN内で起動していれば)外出先の別のtailscale端末からでも192.168.1.6を指定すれば自宅のプリンターにアクセスできるようになります。

ちなみにFreeプランだと以前はサブネットを1つしか登録できませんでしたが、2023年4月くらいの変更で制限がなくなりました(ちなみに接続可能デバイスも100個に増えました、すごい)。

別のIPを割り当ててサブネットを公開

ただしこれだと若干問題があり、サブネットとして公開されたIP範囲は(tailscaleを使っている端末では)最優先で使用されるので、LAN内アクセスができなくなってしまうことがあります。

例えば先ほどの(192.168.1.0/24を公開している)PCとは別に、同じ192.168.1.0/24に所属するもう一つのPC2があってそこにTailscaleがインストールされている場合、PC2から192.168.1.0/24への通信は(実際にはその必要がないのに)Tailscale経由になります。普通のLANアクセスに比べると(僅かですが)オーバーヘッドが生じます。さらに、これは2つの「192.168.1.0/24」が実際には別のサブネットだった場合でも同じです。この場合、PC2がLAN内の端末に接続できなくなってしまいます。またサブネットを公開しているPCの電源が切れているとPC2から192.168.1.0/24につながらなくなります。かといっていちいちルーティングの優先度を変えるのは面倒です。

同様に、別々の場所にある192.168.1.0/24をどっちもTailscale上に公開しようとするとconflictしてしまうのでうまくいきません(ちなみに、subnet routerの冗長化のために同じsubnetを複数端末から公開する機能は存在します)。

この問題はNAT (DNAT) conflicting subnets in advertise-routes · Issue #826 · tailscale/tailscale · GitHubWhen local route is available to a subnet, bypass tailscale subnet relay · Issue #1227 · tailscale/tailscale · GitHubTailsacle accepted subnet routes should not not have a higher priority than main table · Issue #6231 · tailscale/tailscale · GitHubなどで扱われていますが、実は良い感じの(部分的な)解決策があります。それは、192.168.91.0/24のような「ダミー」のサブネットを公開し、そこへのアクセスをPC上で192.168.1.0/24に転送するという方法です。これなら、LAN経由の192.168.1.0/24と同時に使うことができます。また、複数の192.168.1.0/24を同時に公開することもできるようになります。もちろん、元のPCが常時稼働している前提です。

以下、PCとしてはLinuxUbuntu)を使っていると想定します(常時稼働のサーバーなのでLinuxを使うことが実際多いはずです)。まず、先ほどのようにadvertise-routesで192.168.91.0/24が公開された状態にしましょう。次に、IP転送を有効にします。具体的にはecho 1 > /proc/sys/net/ipv4/ip_forwardをします。rootが必要ですがリダイレクトがあるのでsudoを付けるだけではだめで、sudo suしてからこれを実行しましょう。

次に以下のようにtailscale0に対してDNATルールを設定します。

sudo iptables -t nat -A PREROUTING -i tailscale0 -d 192.168.91.4 -j DNAT --to-destination 192.168.1.4

これで、tailscaleをインストールした(他の)デバイスから192.168.91.4にアクセスすると、192.168.1.4につながるようになります。また、これだと機器ごとに転送設定が必要ですが(そのほうがいいときもあるかもしれませんが)、ネットワーク単位で一括で転送したければ以下のように「NETMAP」を使います。

sudo iptables -t nat -A PREROUTING -i tailscale0 -d 192.168.91.0/24 -j NETMAP --to 192.168.1.0/24

ちなみにローカル(このコマンドを実行しているまさにそのPC)から192.168.91.4でアクセスするのは、今までの設定だけではできず、以下のようにPREROUTINGではなくOUTPUTルールを使う必要があります。

sudo iptables -t nat -A OUTPUT -d 192.168.91.4 -j DNAT --to-destination 192.168.1.4

このままだと再起動後に設定がリセットされてしまいます。永続化するには、(tailscaleの起動後にこれを実行しなければいけないことので)以下のようなスクリプトを/etc/network/if-up.dに作成しましょう。

#!/bin/sh
if [ "$IFACE" = "tailscale0" ]; then
  iptables -t nat -A PREROUTING -i tailscale0 -d 192.168.91.0/24 -j NETMAP --to 192.168.1.0/24
  iptables -t nat -A OUTPUT -d 192.168.91.0/24 -j NETMAP --to 192.168.1.0/24
fi

ちなみに試していませんが、192.168.1.1/32のように個々の機器の単位でサブネットを公開することもできるはずで、これと今紹介したDNATを組み合わせるというやり方もありそうです。

追記: nftablesによる永続化

上記の方法だとtailscaleを再起動するたびにルールが増えていってしまいます。筆者が使っているubuntuではiptablesのバックエンドとしてnftablesという後継システムが使われており、これを使って転送設定を永続化させることもできます。以下のようにnftables.confに設定すればよいです。

table ip my_ip_table {
        chain prerouting_nat {
                type nat hook prerouting priority dstnat; policy accept;
                iifname "tailscale0" ip daddr 192.168.91.0/24 dnat ip prefix to ip daddr map { 192.168.91.0/24 : 192.168.1.0/24 }
        }
        chain output_nat {
                type nat hook output priority -100; policy accept;
                ip daddr 192.168.91.0/24 dnat ip prefix to ip daddr map { 192.168.91.0/24 : 192.168.1.0/24 }
        }
}

このip prefixを使った書き方は比較的新しく、あまり情報がないので注意が必要です。(参考:linux - IPTables how to nat 10.8.a.b to 10.0.a.b? - Super User

一時的にこの設定を行うには、上記の表の内容をnftコマンドで直接入力するだけです。これは難しくないので省略します。必要に応じて関連記事DROP vs REJECT論争、そしてWindowsとLinuxのファイアウォールの動作の違いについて - turgenev’s blogを参照してください。

他の方法・他のVPNサービスの状況

先ほど挙げたIssueにも載っていますが、192.168.1.0/23のような少し広いサブネットを--advertise-routesすることで、Tailscaleを介しない192.168.1.0/24へのルーティングより優先度を下げるという方法もあるようです。

またTailscaleの類似サービスであるZeroTierでも似たような手法でサブネットを公開できるようです。

Route between ZeroTier and Physical Networks - ZeroTier Knowledge Base - Confluence

Routing traffic to ZeroTier’s subnet from all devices on the LAN – Chris Tech Blog

その他Tailscaleでできること

自宅に常時稼働のTailscale有効PCがあると色々なことができます。例えば、前回紹介したような方法で、自宅のIPoE/PPPoE回線をそれぞれ経由するプロキシサーバーを立ててブラウザから使うことができます。

また、ProxyJumpを用いてsshを自宅のTailscale経由(ここでは紹介しませんが自分でsshサーバーを立てなくてもTailscale自体にssh機能があります)にすることで、ネットワーク環境が不安定(あるいはPCのスリープ中)でもコネクションがいちいち切断されないssh環境が手に入ります。moshやEternal Terminalもありますが、これらはリモート先のサーバーでroot権限やポート開放などのネットワーク設定ができる権限が必要です。

もちろん、自宅を経由するので、自宅から遠く離れた2箇所の間でsshしたいときなどは不便でしょう。

まとめ

Tailscaleを使えばポートを開放しなくても中継サーバーによるオーバーヘッドがない自宅までのVPN環境を整備でき、サブネットも自由自在に公開できるので大変便利です。おすすめです。