ファイルをディスク上で暗号化し、ログイン時に自動で復号する方法まとめ(Windows・Linux)

概要

モバイルPCなどでは、盗難のリスクがあるため、機密性の高いユーザーデータはディスク上に平文で保存せず、適切に暗号化した状態で保存することが望ましいといえます。ただ、復号のために専用のパスワードを入力するような方法は手間がかかるため、この記事では、ユーザーが特に操作しなくても通常のログインに伴ってシームレスに復号が完了するようなシステムを取り扱います。WindowsLinux(・Android)を対象とします。

Windows①: Bitlocker

Windowsでは、まず標準搭載のBitlockerがこの役割を果たします。Windows Homeエディションでは機能が制限されていてBitlockerの有効/無効を直接切り替えることはできませんが、システムドライブ(C:)に関しては「デバイスの暗号化」という名前でBitlockerによる暗号化を有効にすることができます。

Bitlockerはディスク全体の暗号化であり、特定のユーザーに紐付いたものではありません。正規の方法でWindowsにログインすれば、ユーザーが誰であっても復号されます。

Windows②: EFS

EFSもWindows標準の機能で、こちらはディスク全体ではなくファイル・フォルダ単体での暗号化を提供します。エクスプローラーのプロパティの「内容を暗号化してデータをセキュリティで保護」から有効にできます。ただしこれも、WindowsのHomeエディションでは使用できないという難点があります。Homeエディションは痒いところに手が届きませんね…

Windows③: DPAPI

上記以外の方法として、DPAPI (Data Protection API)を使う方法があります。これは、ログインしているユーザーにしか復号できない方法で文字列データなどを扱うことができます。これを用いてパスワードなどを暗号化しておけば、該当ユーザーでログインしている間だけそのパスワードを使って暗号化ファイルシステムを復号しておくことができます。

これをやってくれるソフトウェアの例がCryptomatorです。ローカルディスクやネットワークフォルダ(WebDAV)など複数の形式でのマウントに対応しており、筆者も愛用しています。おすすめです。

自前でやりたい場合は、DPAPIを直接呼んでもいいですが、軽い用途であればPowerShell経由で呼ぶ方法もあります。以下のサイトを参照してください。例えば筆者はこれを使ってRcloneのconfigのパスワードを暗号化して保存しています。(暗号化した状態でpass.txtに保存しておいて、getcredential passみたいなコマンドで平文が取れる)

スケジュールジョブ(PowerShell)でパスワードをセキュアに使う(セキュアストリング編)

GitHub - senkousya/usingEncryptedStandardStringOnPowershell: 🔰Windows PowerShellで文字列の暗号化と復号化

Linux

Linuxでは、保存データ暗号化 - ArchWikiにだいたいのことが書いてありますが、今回の記事の趣旨に最もよく合致するのはecryptfsかと思います。最低限使うだけだったら、ecryptfs-setup-privateというコマンドを実行すれば、ログイン時だけマウントされる~/Privateというフォルダが作成されます(暗号化された本体データは~/.Privateにあります)。もう少し詳細な説明はLinux で eCryptFS を使用してファイルとディレクトリを暗号化する方法などを見てください。ecryptfs以外だとLUKSも有名ですね。こちらは多分Bitlockerに近いと思います。

自前で似たようなことをしたい(つまりDPAPI的なものが欲しい)場合は、Post Exploitation: Sniffing Logon Passwords with PAM · Embrace The Redのようにするとログイン成功時(GUIログイン含む)にログインパスワードが平文で取れるので、それを使って任意データの暗号化・復号を行うことができそうな感じはします。しかしセキュリティ的に問題のない具体的な実装方法は知りません。ecryptfsも、eCryptfs - ArchWikiに「ログイン時に自動マウントさせる場合、ユーザーアカウントにログインするときに使うのと同じパスワードでなくてはなりません。」とか書いてあるので実質同じような仕組みを使っているのかなとはと思います。特段の理由がなければ、ecryptfsをDPAPIがわりに使うので満足しておくのがよさそうな気がします。

.NETの移植版であるMonoではDPAPIに対応する実装がありますが、暗号化キー自体はディスク上で平文で見れてしまうようです(Storing Secrets in Linux - DZone)。

CryptomatorのLinux版とかはkeyring?っぽいのを使っているようですが、なんかGUIがないと動作しないイメージでよくわかんないです。

あとこれも詳しくはわかりませんが最近はsystemd-creds - ArchWikiというのも出てきているようです。

Android

Androidでは、おそらくある程度最近のものであれば、「スマートフォンの暗号化」「端末の暗号化」といった名前で、パーティションの暗号化がデフォルトで有効になっているはずです。スマホは特に盗難のリスクがあるのでディスク上暗号化が重要ですね。

注意点

保存データ暗号化 - ArchWikiにある通り、今回紹介したような方法は、メモリに直接アクセスしたり、あるいはインターネット経由で(ログインユーザーの権限を使って)攻撃したりといった手法には効果がありません。必要に応じて別の防御手段を追加するなどしましょう。

また当然、ログインパスワードはある程度強固なものにしておく必要があります。

まとめ

Cryptomator(DPAPI)とかecryptfsは手軽に使えますし、何もしないよりマシなのは間違いないので使っていきましょう。

あと盗難には気を付けましょう。

特定アプリケーションのTCP・UDP通信を透過的なSocks5プロキシ経由にする方法(Windows・Linux)

概要

ネットワーク上の制約を回避するためにプロキシやVPNのような類のものを使用するにあたって、(必ずしもそれ自体ではプロキシなどに対応していない)特定のアプリケーションを対象に、またTCPだけでなくUDPにも対応したいということがあると思います。

特に「個別アプリケーション対象」「UDP対応」の2条件を満たすのは結構難しいので、そこを中心にやり方を紹介しようと思います。

SOCKS5プロキシ

SOCKSというのはHTTPプロキシなどと同じくプロキシの種類の1つで、ssh -D(ダイナミックポートフォワード)で使用されていることでも知られています。TCPポート上で動作するプロトコルです。

このSOCKSプロトコルのバージョン5(=SOCKS5)から、UDP Associateという、UDP通信をプロキシするための仕組みが追加されました。UDP Associateの基本的な仕組みは、UDP通信を仲介するための(プロキシサーバー自身の)ポートをクライアント側に通知し、クライアント側がそこに送信したUDPパケットをプロキシサーバーがかわりに本来の宛先に送信するといった感じになっているようです。したがって、クライアント側からはプロキシサーバーのTCPポートだけでなく、おそらくエフェメラルポート範囲全て(厳密にはよく理解できていません)UDPポートが見えている必要があります。ここが状況によっては結構ハードルが高い部分かと思いますが、ここに関してはVPNでなんとかすることを想定しています。

注意すべき点として、SOCKS5プロキシとして利用可能なものすべてがこのUDP Associateをサポートしているわけではなく、例えば上記のssh -Dでは使えません。

この記事ではUDP Associateまでサポートしている代表的なSOCKS5プロキシとしてdanteを主に使用します。他には3proxyというのもあり、こちらはWindows版のバイナリが提供されています。

dante・3proxyの設定

/etc/dante.confの内容(変更が必要な部分のみ載せています)は以下のような感じです。

socksmethod: none
clientmethod: none
client pass {
  from: 0.0.0.0/0  to: 0.0.0.0/0
}
socks pass {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    protocol: tcp udp
}
socks pass {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    command: bind connect udpassociate bindreply udpreply
}

「udpassociate」という文字列があるのがわかります。見ての通りセキュリティは何もないので、必要に応じて追加してください。

外部アドレス(出ていくために使用するアドレス)の設定などについてはIPoE/PPPoE併用時(など)に一つの端末から同時に複数の接続経路を利用する - turgenev’s blogを参照してください。3proxyの設定ファイル例もここに載っています。むしろ3proxyのほうが簡単かもしれません。

TCPに関するsocks5の動作確認だけならcurlなどで簡単にできるので、SOCKS5の設定に慣れていなければこの段階でやっておくのを推奨します。

今回は、192.168.31.2:3129というポートにおいてSOCKS5サーバーが稼働しているとして話を進めます。

また、重要な注意点として、UDP AssociateではUDP通信に使用するアドレスとポートがSOCKS5サーバーからクライアント側に通知されますが、これはあくまでSOCKS5サーバーから見たときのアドレスとポートです。NATが介在するなどして、SOCKS5サーバーがリッスンしていないアドレスにおいてSOCKS5が利用可能になっているような場合、クライアント側は誤ったIPにむけてUDPパケットを送信してしまい、UDP Associateはうまく動作しません。(ちゃんとtcpdumpで外向きパケットを監視していればよかったのですが)ここで2日ほどハマったので注意してください。ただし実は(今回紹介する)Windowsでの方法ではIPが一致していなくても動きます(さすがにポートまで変わると動かなさそうですが)。

socks5プロキシ自体には管理者・root権限は不要です。

SOCKS5を透過プロキシとして利用するための設定: Windowsの場合

SOCKS5サーバーを単体で動かすだけでは、まだ透過的なUDP通信はできません。任意のUDP通信をキャッチしてSOCKS5プロキシとやりとりを行う部分にはまた別のソフトウェアが必要になります。

Windowsの場合は、ProxiFyreというのが利用可能です。2023年に公開された新しいOSSですが、かなり良い感じです。

インストールのしかたはREADMEの通りで、「Windows Packet Filter (WinpkFilter)」というのを入れてから、ProxiFyre本体を解凍するだけです。設定ファイルも簡単なのでほとんど迷うところはありません。

ProxiFyreが起動している間は、設定ファイルに記載された文字列に一致する実行ファイルからの通信はSOCKS5プロキシを経由するようになります。ProxiFyreの起動前から実行されていたプログラムでもプロキシ経由になります。ProxiFyreはWindowsのサービス(≒デーモン)として動作することもできます。

初回起動時にダイアログが出るかと思いますが、ProxiFyre.exeをファイアウォールの許可設定に追加するのを忘れないようにしてください。そうしないと正しく動きません。

あと、3proxyを認証無しで使うと動かない不具合があります(報告済み: #27)(←に載せた3proxyの行をhavepass = 0に変えればとりあえず動く)。

SOCKS5を透過プロキシとして利用するための設定: Linuxの場合

Linuxの場合は、Windowsの場合よりむしろ難しく、ソフトウェア自体の設定に加えてLinux側でnetfilter(iptables/nftables)関連の設定を行う必要があります。というよりはnetfilterで設定ができるからこそWindowsでのProxiFyreのような全部やってくれるソフトが無いということかもしれません。

ソフトウェアとしては(試した限り)redsockshev-socks5-tproxyが利用可能です。redsocksのほうは数年間コミットがありませんが、semigodking/redsocksというメジャーなforkがあり、こちらではshadowsocksのサポートなどいくつかの拡張が追加されているようです。基本的にはforkのほうをおすすめします。

(古いほうの)redsocksであれば、以下のようなredsocks.confを用意します。

base {
    log_debug = off;
    log_info = on;
    log = stderr;
    daemon = off;
    redirector = iptables;
}

redsocks {
    local_ip = 127.0.0.1;
    local_port = 22222;
    ip = 192.168.31.2;
    port = 3129;
    type = socks5;
}

redudp {
    local_ip = 127.0.0.1;
    local_port = 22224;
    ip = 192.168.31.2;
    port = 3129;
    udp_timeout = 10;
}
 

udp_timeoutはもっと短く2秒くらいでも良さそうです(どうせクライアント側がそんなに長時間待ってくれないので)。semigodking/redsocksでは少し文法が変わって、

    local_ip = 127.0.0.1;
    local_port = 22222;
    ip = 192.168.31.2;
    port = 3129;

のところが

    bind = "127.0.0.1:22222";
  relay = "192.168.91.12:3129";

となります。

hev-socks5-tproxyでは以下のように設定すると同等の内容になります。

misc:
  log-file: stderr
    log-level: debug

socks5:
  # Socks5 server port
  port: 3129
  # Socks5 server address
  address: 192.168.31.2
  # Socks5 UDP relay mode (tcp|udp)
  udp: 'udp'
  # Socks5 handshake using pipeline mode

tcp:
  # TCP port
  port: 22222
  # TCP address
  address: '::'

udp:
  # UDP port
  port: 22224
  # UDP address
  address: '::

Socks5サーバーのアドレスはまとめて一箇所で指定されています。機能が少ない分、シンプルではあります。「Socks5 UDP relay mode (tcp|udp)」については、ここを変えるとUDP over TCPでやってくれるのかなと思いますが、試した限りでは'udp'でないと動きませんでした。

これで、127.0.0.1の22222番でTCP用、22224番でUDP用の透過プロキシが動作するようになります。

透過プロキシはおそらく生のパケットを生成する都合上、起動にroot権限が必要になります。ただ、hev-socks5-tproxyのREADMEを見ると、どうやらsetcapというコマンドで個別に権限を与えておけばプログラム自体をrootで起動する必要はないようです。

Linuxの場合: iptablesやrouteの設定

では、上記の127.0.0.1上のポートにTCP/UDPの通信を流し込むためにiptablesやipコマンドでの設定を行います。

特に、不特定多数の宛先を対象としたUDPでの透過プロキシには、iptablesのTPROXYというターゲットを使用する必要があり、これはmangleテーブルのPREROUTINGチェインでしか使用できないという特徴があります。つまり、他のコンピュータから転送されてきたパケットにしか適用できないということです。実はそのコンピュータ自体からのパケットにも適用する方法はあるのですが、説明の都合上後回しにします。

また、TCPではTPROXYを使うよりもっと簡単な方法がありますが、semigodking/redsocksとhev-socks5-tproxyはTPROXYでも動かせるので、ついでに設定してみることにします。Support TPROXY for TCP connections · Issue #97 · darkk/redsocks · GitHubの通りオリジナルのredsocksでは動きませんでした。

ということで、以下の内容をテストするために、また別のコンピュータ(場合によってはスマホなどでも可)を用意する必要があります。手元になければVPN経由、あるいはLinuxで一つのパケットに2回(複数回)NATをかけるための2つの方法 - turgenev’s blogで紹介したようにnetnsやダミー的なtunデバイスを利用するのでも構いません。ここでは、その別のコンピュータのIPアドレスが192.168.1.90であるとします。

また、確認用のコマンドとして今回は主にSTUNプロトコルのクライアント(具体的にはStuntman)を使用したため、宛先ポート範囲を3478:3479に限定しています。疎通確認の方法は何でもいいのですが、雑に指定するとプロキシ用の通信がプロキシに向かうような循環が生じて通信できなくなる場合があるのである程度限定的にやっていくのが良いです。まあ今回の例ではそもそも別の端末なので限定しなくても循環は起こりませんが。ちなみに、stunclient自体は特にUDPの疎通確認ツールとして手軽に使いやすく、アドレス・ポートの情報も得られるのでかなりおすすめです(詳細はNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogにも書きました)。

まず、以下のようなiptablesコマンドを実行します。

sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.1.90 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.1.90 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22224

先ほどの透過プロキシのアドレスを指定しています。送信元アドレスも192.168.1.90に限定しています。

一見これだけでも動きそうですが、実はこのままだと192.168.1.90からの通信が外部インターフェイスに向かってFORWARDされてしまい(?)うまくTPROXYが適用されません。あまり理解できていませんが、TPROXYに関するmanページなどにもそういう感じのことが書かれています。

どうすればいいかというと、TPROXYを適用したいパケットはループバックインターフェイス(lo)に向かうようにルーティングを設定する必要があります。

まず、いつものようにrt_tablesなどを編集して新しいルーティングテーブル(ここでは100とします)を追加します。

ここに次のようにしてrouteを追加します。

sudo ip route add local default dev lo table 100

さらに192.168.1.90からの通信がこのテーブルを使用するようにします。

sudo ip rule add from 192.168.1.90 lookup 100

これで、TPROXYが正しく機能するようになるはずです。

外部インターフェイスからloに転送しているのでip_forwardを有効にする必要がありそうですが、試したかぎり無効(=0)のままでも動くようです。あと多分最近の普通のLinuxなら大丈夫だと思いますが、TPROXYに関するモジュールがロードされている(lsmodでxt_TPROXYが表示されればOK?)必要があります。

Linuxの場合: PC自体の通信にTPROXYを使用する

前述の通り、TPROXYはPREROUTINGチェインでしか使えないため、そのままではPC自体で生成された通信(OUTPUTチェインなどで処理される)には適用できません。

しかし、実際には、先ほどと同様にPC自体で生成された通信をloに強制的に送信するようにすると、PREROUTINGチェインが適用されるようになるようです。(この辺は全然理解できておらず、まともな説明を探してもあまり見つかりません。)

ただし、PC自体の通信をloに向けるとなると場合によっては前述のような循環が発生してしまうので、アドレス指定やmarkを使用するなどして回避する必要があります。

ここでは最も簡単な設定例として、loに(他のデバイスでも構いませんが)適当なプライベートアドレスを割り当てて、そこから出ていくものだけをloに向けるように設定します。結果的に先ほどとほとんど同じようになります。

まずアドレスの割り当てを行います。

sudo ip addr add 192.168.5.5 dev lo

そしてこの192.168.5.5を対象に同じようなip ruleとiptablesのルールを設定します。

sudo ip rule add from 192.168.5.5 lookup 100
sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.5.5 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.5.5 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22224

結局アドレス以外何も変わっていませんね。こちらも別にポートを3478-3479に限定するのはやらなくてもいいです。これで192.168.5.5から出ていくパケットがTPROXYを使うようになります。

Linuxの場合: 特定のアプリケーションにTPROXYを使用させる

当初の目的は特定のアプリケーションにTPROXYを使用させることでした。これは、cgroupというものを使って、特定のプロセス(子プロセス含む)が生成したパケットのみが対象になるように今までのip ruleやiptablesルールを設定することで実現できます。

cgroupに関する部分はこれだけでそれなりの長さになってしまうので別の記事に分けました。

cgroupのnet_clsを使って、特定のプロセス(ツリー)に対するiptables/nftablesルールを設定する - turgenev’s blog

これができたら、以下のようにして該当cgroupのパケットにまずmarkをつけて、

sudo iptables -t mangle -A OUTPUT -p udp ! -d 127.0.0.0/8 -m cgroup --cgroup 1 -j MARK --set-mark 0x123

それからip ruleを追加して、

sudo ip rule add fwmark 0x123 lookup 100

最後にTPROXYルールを適用すればよいです。

sudo iptables -t mangle -A PREROUTING -p udp -m mark --mark 0x123 -j TPROXY --on-ip 127.0.0.1 --on-port 22224

UDPだけ載せましたがTCPも同様です。

UDP AssociateのNATタイプ

NATタイプ(EIM/APDFなどの用語)に関してはNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogをご覧ください。

UDP Associateでは、クライアント側のUDPポートとプロキシ側のUDPポートがマッピングされるような動作をすることになるため、実質的にどのようなNATタイプとして機能するのか気になります。また、SOCKS5プロキシと透過プロキシソフトウェアのそれぞれでNAT動作を考える必要があります。

まずMappingに関しては、ここで紹介した全てのソフトウェアでEIMになります。まあ普通に実装する限りはそうなるだろうという感じです。一方、Filteringに関しては、3proxyとProxiFyreとsemigodking/redsocksとhev-socks5-tproxyはEIFですが、danteとredsocksはAPDFになります。この2つは当初の通信先と一致しないUDPパケットのみを通す実装になっているからです。詳しく原因を見ていきます。

まずdanteに関してですが、最終的な目的地へのUDP通信を行う際に、connect関数を使用しているのが原因です。UDPには接続の概念がないので普通はconnect関数はあまり使わないのですが、使うことは可能で、そうすると単一の宛先としか通信できなくなります(他のところからパケットが来るとICMP port unreachableが返る)。ソースファイルとしては主にsockd/dante_udp.cが関係していて、connectに関するコードの付近に「Should we connect to the destination?」から始まるコメントが書いてあり、方針を決めかねている様子がうかがえます。さらによくコードを読むと、lib/config.cで定義されているsockscf.udpconnectdstという変数を1から0に変更することで、connectが使用されなくなることがわかります。実際、手元でこの変更をしてからビルドすることでdanteの動作がEIM/EIFになることを確認しました。

redsocksに関しては、redudp.cのredudp_pkt_from_socks()で、get_destaddr関数の値との比較を行い、アドレスまたはポートが一致しなければ「Socks5 server relayed packet from unexpected address」というメッセージを表示してパケットを破棄していることがわかります。semigodking/redsocksでは(ここで別のファイルに移動された上で)ここでそのコードが削除されているため、EIM/EIFになります。

もしこのSOCKS5を利用してUDPホールパンチングを行うのであれば、余計なフィルタリングのないEIM/EIF(Full Cone NAT)が望ましいかと思います。一方で、クライアント-サーバー型の普通のUDP通信(DNSなど)が利用できれば十分というのであればEIM/APDFでも問題はありません。

EIM/EIFとEIM/APDFを組み合わせれば、全体としては制約が厳しいEIM/APDFと同等の動作をすることになります。また、当然ですが、SOCKS5プロキシと透過プロキシに加えて、SOCKS5が稼働しているサーバーのNAT環境も影響します。

TCPのNATタイプ

TCPに関してはホールパンチングでは普通使わないのでNATタイプが問題になることはありませんが、一応調べてみると、Mappingの時点でConnection Dependent Mappingになります。SOCKS5だけでなくHTTPプロキシでも同じでした。おそらく、新規接続の際に、前回のポートを使いまわすようなことは特に考えていないのではないかと思います。しかるべく実装すればEIMにはできそうです。Filteringに関しては、EIF(他の場所からの接続も受け入れる)にしようとするとリバースプロキシの役目も果たすということになるのでかなり大がかりな変更(SOCKS5の範囲を逸脱するような)が必要になる気がします。まあ必要ないのでいいでしょう。

MTUに関して

通常のUDP通信にプロキシが介在することになるため、MTU関連で問題が起きないか心配になります。しかし、透過プロキシ側でもSOCKS5側でも一旦UDPソケットを介しているので、そこで再構築と分割が行われるため、(効率はよくないかもしれませんが)プロキシが原因で通信ができなくなることはありません。

VPNによるSplit Tunneling

ここまでSOCKS5を用いた方法について解説しましたが、VPNソフトウェア側がアプリケーションごとのスプリットトンネリング(一部の通信だけを選択的にVPNに通すこと)に対応している場合もあります。TunnelBearは対応しているようです。WireguardはTunnlToを使うとできそうです(ProxiFyreの作者がかかわっている?)(というかこれができるなら一般のVPNに対してもできそうな気がする…)。OpenVPNは(少なくともWindowsでは)無理そうです。

TCP限定の方法

TCPに関してはUDPよりも容易で、様々な方法・ソフトウェアがあります。

まずiptablesを使う場合はTPROXYではなくREDIRECTターゲットを使うこともできます。redsocksのREADMEにもあります。REDIRECTもlocalhostにむけて行われるようなので、透過プロキシ側の設定ファイルはそのままで使えるはずです。

tsocksや、その後継(?)のtorsocksは、tsocks firefoxのようなコマンドで起動することでアプリケーションがsocksを使用するようにしてくれるものですが、UDPには対応していないようです。proxychains(-ng)も(こちらはHTTPプロキシなどにも対応していますが)同様です。iptablesルールを設定する必要がなく、root権限が不要な場合が多い(AppleのSystem Integrity Protectionとか、別の障壁に引っかかる可能性はありますが)ので、用途がTCPならこっちを使った方が楽です。ただし、go製のプログラムなど、カーネルの機能を直接呼んでいるプログラムにはLD_PRELOADが効かないので使えません。

OSやブラウザのプロキシ設定について

少し本題とはそれますが、OSやブラウザにもプロキシを設定する機能は標準で備わっており、多くの場合はSOCKSにも対応しています。しかしこれらは基本的にTCPに関してしか機能せず、DNSやWebRTC(Discordなど)のようなUDP通信はプロキシを経由しません。特にインターネット匿名化技術であるTorの界隈ではUDP通信で生のIPが漏洩するとして注意が呼びかけられているのをよく見かけます。なお、UDPをプロキシすることはできないかわりにUDPをブロックすることができる場合はあります(ChromeならWebRTC Network Limiterとか?)。またFirefoxではDNSをSOCKS側に任せるというオプションがあり、この場合ブラウザ側はDNS解決を行わずドメイン名をそのままSOCKS側に投げるようです(How does SOCK 5 proxy-ing of DNS work in browsers? - Stack Overflow)。

単一のホストとのUDP通信をプロキシする場合

UDP通信の相手が一つ(8.8.8.8:53だけとか)しかない場合は、TPROXYを使わなくてももっと簡単にできるようです。redsocksとかにやり方が載っていると思います。これについては解説しません。

UDP Associateの現実的な有効性について

UDP Associateは技術としては面白く、使用感も悪くないですが、UDP Associateが真に有効な場面というのはそれほど多くないような気もします。

まず、最初に述べたように、UDP Associateを使うためにはプロキシサーバーのUDPポートが手元から全部見えている必要があり、そのためには自宅サーバーやVPSなど、かなり自由に扱えるコンピュータが必要になります。

LinuxではPolicy Based Routingが強力なので、TPROXYのルールを設定するくらいだったら同じ条件でパケットにマークをつけてそれに従って(LAN内のPCやVPN経由のVPSに)ルーティングすれば済みます。

Windowsではルーティングが貧弱なので、特定のアプリケーションのUDP通信をプロキシ経由にしたければ、ここで紹介した以外の方法を筆者は知りません。

従って例えば、「UDP通信を使用するWindows用のゲームソフトを使いたいが、自宅ではUDP通信がブロックされているのでVPSを契約しており、VPSの通信量を節約するためにゲーム用の通信だけをVPSに流したい」といった状況であれば、この記事の方法が真に有効である可能性はあります。ちなみに筆者自身の状況としては、使っているVPNUDPがブロックされていてDiscordの通話ができず、Discordの通話サーバーは大量にあるので宛先ベースでのルーティングでも対処が難しく、プロキシを使って何とかならないかと考えていた感じでした。

また、SOCKS5プロキシを起動するにはrootは不要なので、root化されていないAndroidのキャリア回線(大抵はEIM/EIF)をPC側で使いたいという場合もこの記事の手法が有効です。

UDP AssociateのためのUDPホールパンチング

UDP Associateが使えるためにはSOCKS5プロキシ側のUDPのポートが全部見えていなければいけないと言いましたが、UDPホールパンチングが使えれば、ポートが開放されていなくても必要に応じてP2PUDP接続の経路を確立することができます。SOCKS5と透過プロキシの両方を頑張って拡張すればできそうです。

ただ、これも真に有効な場面があるかと言われると…という感じです。

ちなみにsemigodking/redsocksのREADMEにはFull Cone NAT Traversalのサポートがあるというようなことが書いてありますが、これは単にredsocksがFull Cone NATとして動作するというだけで、UDP Associateでホールパンチングしてくれるという話ではないと思います。

試していないもの

Hysteria 2 SOCKS5も透過プロキシ部分も全部入っていそうでかなり強そうですが、設定方法がよくわからなかった(TLSまたはacmeを設定しろとか)のでやめました。TPROXY関連など多くのドキュメントがあります。

Win2Socks - Transparent Proxy for Windows UDPをサポートしていると明記されていますが、有料なので試していません。

Sockscap Tutorial | Sockscap32 Download これはWindows用で、特定アプリケーションをSOCKSに通してくれるようですが雰囲気的にあまりUDPをサポートしていそうに見えないので試していません。

Force an application's traffic through a SOCKS proxy - Unix & Linux Stack Exchange proxyboundというものが紹介されています。

socks - What's the difference between torify, usewithtor, tsocks and torsocks? - Tor Stack Exchange torifyというものが紹介されています。

特定プログラムを対象に、(SOCKS経由ではなく)bindするIPを指定できるというソフトウェアもありました。今回の目的に合うかは不明です。

ForceBindIP | r1ch.net

GitHub - falahati/NetworkAdapterSelector: A simple solution to let you force bind a program to a specific network adapter

関連文献

iptables - What is the purpose of TPROXY, how should you use it and what happens internally? - Server Fault TPROXYとip routeに関する説明。

redsocks/doc/balabit-TPROXY-README.txt at master · semigodking/redsocks · GitHub semigodking/redsocksにあるTPROXYの説明。

TProxy - Hysteria 2 TPROXYの詳しい設定例がある。

IPTables TPROXY - proxy input and output · GitHub OUTPUTパケットにTPROXYを適用する設定例。markの付け方が工夫されている。

networking - How to send all internet traffic to a SOCKS5 proxy server in local network? - Android Enthusiasts Stack Exchange 英語。網羅的で非常に詳しい回答がついている。

SOCKS非対応のアプリケーションをSOCKS経由にする - ふなWiki 非常に古い。

EdgeRouter X に redsocks を導入して透過的にプロキシーを経由させる – ちとくのホームページ redsocksのdnstc、dnsu2tの機能について書いてある。

マルチユーザー&root権限なしの環境で他ユーザーからTCPポートを保護する

sshサーバーなど、多くのユーザーが1つのオペレーティングシステムを共用するマルチユーザー環境では、TCP/UDPのようなポートを使用するアプリケーションを起動するとそれは他のユーザーからもアクセスできるようになります。これはセキュリティ的に望ましくありません。

もっとも、マルチユーザーのssh環境が使われている多くのケースではポートを使用するようなアプリケーションが必要ない(レンタルサーバーなど)あるいは共用するメンバーが信用できる(研究用途のサーバーなど)場合が多いですが、防御手段を考えておくに越したことはありません。

あまり取り上げられることのない話題かと思いますが、Hijacking other user’s TCP tunnelsなどではこの問題について触れられています。

この記事ではこの問題の解決法をちょっとだけ考えてみました。

ソフトウェア的な保護(パスワードなど)

当たり前ですが、まず有効なのが、各ソフトウェアが提供する認証のシステム(パスワードなど)を使用することです。ファイルサーバーなどであれば多くの場合はパスワード認証があります。

Unixドメインソケットを使ってssh転送

ただ、パスワードではなく、もっと根本的に、ポート自体が見えない状態にすることはできないか?とも思います。

そこで有効なのが、Unixドメインソケットを使う方法です。Unixドメインソケットとは、TCP/UDPポートのように動作する特殊なファイルのことです。ファイルなので、他のユーザーからはそもそもアクセスができません。ポートは他人が使っているとAddress in useになる可能性がありますがそれを避けられるメリットもあります。

例えばsshのポートフォワードでは、ドメインソケットを用いた転送(ローカルもリモートもどちらも)ができます(OpenSSH 6.7 will bring socket forwarding and more [LWN.net])。たとえば別のサーバーのポートを中継のsshサーバーに転送してそれをまたローカルに転送したいときなどは中継サーバーでドメインソケットを使用することができます。

Unixドメインソケットを強制的に使わせる?

Unixドメインソケットに対応していないソフト(というか、対応していないものの方が多いと思いますが)はどうすればいいんだ?と思うところですが、既存ソフトウェアに強制的にUnixドメインソケットを使わせることができるというソフトウェアがありました。

GitHub - cyphar/ttu: A small tool that silently converts TCP sockets to Unix sockets.

AppleのSystem Integrity Protectionとかに引っかからなければ一般ユーザー権限でも使えます。ただ、あくまで実験用にちょっと書いてみたという感じで、実用レベルのソフトウェアではなさそうです。listenもconnectもどっちも対応しています。TCPのみ対応です。LD_PRELOADを使っているので、システムコールを直接呼び出すプログラム(go製のものとか)では使えません(tsocksなどと同様)。

試しにPythonのhttp.serverとcurlという組み合わせでやってみたところ、アドレスが取得できないというようなエラーがPython側で出ました。やはり実用には制約が大きそうです。

感想

ちょっとだけになってしまってすみません。

やはりポートを他のユーザーから保護するのは結構難しそうです。そういうもんなんですかね…。ファイルシステムのようにちゃんと権限分離できれば、Webサービスとかでもう少し積極的にマルチユーザー環境を使っていけるような気もするんですが。

cgroupのnet_clsを使って、特定のプロセス(ツリー)に対するiptables/nftablesルールを設定する

概要

Linuxのcgroup機能とは、プロセスをグループに分けて管理し、その単位ごとにCPU/メモリの割り当て等の設定を可能にするものです。

cgroupのサブシステムにはcpu, memoryなどの他にnet_clsというものがあり、これを使うと指定したグループからのパケットに対してタグ(classid)を付加することができ、これを条件にnetfilter(iptables/nftablesコマンド)でフィルタリングなどを行うことができます。

筆者自身深く理解してはいないので言葉遣いは間違っている可能性があります。日本語情報だと他にはLinux 3.14 で net_cls cgroup に追加された netfilter 対応 - TenForwardがあります。

net_clsサブシステムが使えるか確認する

まず、cat /proc/cgroupsを実行します。すると手元(Linux Mint 21.3, Ubuntu 22.04ベース)では以下のような出力が返ってきました。

#subsys_name    hierarchy    num_cgroups    enabled
cpuset    0    170    1
cpu    0    170    1
cpuacct    0    170    1
blkio    0    170    1
memory    0    170    1
devices    0    170    1
freezer    0    170    1
net_cls    0    170    1
perf_event    0    170    1
net_prio    0    170    1
hugetlb    0    170    1
pids    0    170    1
rdma    0    170    1
misc    0    170    1

このようにnet_clsが含まれていればOKです。含まれていなかったらsudo modprobe cls_cgroupとかをすると出てくるかもしれないです(参考:第4回 Linuxカーネルのコンテナ機能[3] ─cgroupとは?(その2) | gihyo.jp

マウントする

では、次に、ls /sys/fs/cgroupを実行します。手元では以下のようになりました。

cgroup.controllers      cpuset.mems.effective  memory.pressure
cgroup.max.depth        dev-hugepages.mount    memory.stat
cgroup.max.descendants  dev-mqueue.mount       misc.capacity
cgroup.procs            init.scope             proc-sys-fs-binfmt_misc.mount
cgroup.stat             io.cost.model          sys-fs-fuse-connections.mount
cgroup.subtree_control  io.cost.qos            sys-kernel-config.mount
cgroup.threads          io.pressure            sys-kernel-debug.mount
cpu.pressure            io.prio.class          sys-kernel-tracing.mount
cpu.stat                io.stat                system.slice
cpuset.cpus.effective   memory.numa_stat       user.slice

(フォルダとファイルが見分けられなくてすみません)

本当はここの中にnet_cls(というフォルダ)が含まれているべきなのですが、ありません。どうやら、マウントする必要があるようです。これは、2.3. 既存の階層へのサブシステムの接続と接続解除 Red Hat Enterprise Linux 6 | Red Hat Customer Portalみたいな感じでmountコマンドを打つと追加できます。しかし自分で打つのは面倒です。

実は、cgroupfs-mountというコマンドをsudoで実行するとこの部分を勝手にやってくれるということがわかりました。これはcgroupfs-mountというパッケージに入っています。実は、cgroups-mountという非常によく似た名前のコマンドもあり、こちらはcgroup-liteというパッケージに入っています。挙動もほぼ同じです。中身はシェルスクリプトで、見た感じ前者のほうが内容が多いので新しそうです。

というわけでcgroupfs-mountを実行するとls /sys/fs/cgroupは以下のようになります。

cgroup.controllers      cpuacct        io.prio.class     proc-sys-fs-binfmt_misc.mount
cgroup.max.depth        cpuset         io.stat           rdma
cgroup.max.descendants  devices        memory.numa_stat  sys-fs-fuse-connections.mount
cgroup.procs            freezer        memory.pressure   sys-kernel-config.mount
cgroup.stat             hugetlb        memory.stat       sys-kernel-debug.mount
cgroup.subtree_control  init.scope     misc              sys-kernel-tracing.mount
cgroup.threads          io.cost.model  net_cls           system.slice
cpu.pressure            io.cost.qos    net_prio          systemd
cpu.stat                io.pressure    perf_event        user.slice

このようにnet_clsなどいろいろ追加されています。

cgroupの作成・設定

cgroupを作成するには、先ほど存在が確認できた/sys/fs/cgroup/net_clsに、testというフォルダを作成します。testの中に移動してみると、cgroup.procsとかnet_cls.classidといったファイルが勝手に作成されていることがわかります。これを編集することでcgroupに関する設定ができます。

ここではnet_cls.classidに1と書き込んでみます。(数字は何でもいいです)

# echo 1 > net_cls.classid

こうするとiptablesで -m cgroup --cgroup 1などと指定できるようになります。

次にプロセスをcgroupに追加します。適当なシェルを起動してecho $$と打ってPIDを取得し、これをcgroup.procsに書き込みます。(ここでは5008とします)

# echo 5008 > cgroup.procs

これで、該当のシェルの子プロセス(シェルで実行したコマンド)はclassidが1として扱われるようになります。あとはiptablesのルールで好き勝手にやるだけです。

cgroup指定は原則的にINPUTチェインでは機能しません(一応特殊な条件下では機能する?詳しくはhttps://ipset.netfilter.org/iptables-extensions.man.html)。

cgroup-tools

cgroupの作成や各種設定はcgcreateやcgsetといったコマンドで行うこともできます。これらはcgroup-toolsというパッケージに入っています。

今回作成したtestというcgroupで特定プロセスを実行するには以下のようにします。

sudo cgexec -g net_cls:test command args

勝手にcgroup.procsのところにcommandのPIDが追加されていることがわかります。

PIDでの指定について

Linux Netfilter iptables — Re: module owner does not work

これによると、Linux 2.6あたりの一時期には、--pid-owner、--sid-owner、および --cmd-ownerといったPIDなどを直接指定するiptablesルールがサポートされていたようですが、なにか問題があったようで現在は利用できません。--uid-ownerと--gid-ownerは引き続き利用可能です。これらはINPUTチェインでは使えません。

Linuxで一つのパケットに2回(複数回)NATをかけるための2つの方法

概要

LinuxでNATなどを担当するiptablesやnftables(中身はnetfilter)の機能では、一つのパケットに2回以上SNATあるいはDNATをかけることはできません。SNATでもDNATでも、パケットにNATをかけると一度決まったらそのパケットのNATに関する処理はそこで完了してしまうようです(いまだにiptables/nftablesのルールの適用規則がよくわからず…)。

この記事では、これを無理やり行うための方法を2つ紹介します。

動機

そもそもなんでそんなことをしようかと思ったかというと、v6プラスやOCNバーチャルコネクトのようなMap-e接続では、割り当てられたグローバル上の使えるポートが複数の範囲(ポートセット)に分散しており、iptablesではNAT先の指定に複数のポート範囲をまとめて指定することができないからです。

よくあるMap-eの設定方法では、変換元ポート番号を特定の数で割った余りなどに基づいて各ポートセットに分散させていますが、変換先が特定ポート範囲に限定されるので、他のポート範囲なら空いているのにそれが使えないという可能性が残ります。

そこで、例えばv6プラスみたいに16ポートx15個に分かれているなら、まず240ポートの範囲にSNATしてから、その結果のポート番号に基づいて16ポートずつの範囲に分割してSNATすれば、最初のSNATですべてのポートを見ることができます。

この記事の手法はこれを達成することを念頭に置いているので、「NATを2回やりたい」と思った動機がこれと別だった場合は、役に立たない可能性もあるかもしれません。

方針

netfilterの基礎部分をいじってどうこうするのは面倒そうなので、方針としては「パケットが2つ(以上)の別々のインターフェイスを通って外と通信する」ような状況を作り出すことを考えます。言ってしまえば、たとえばマシンが2台あればNATを2回やるのは簡単にできるわけですが、それを仮想的に1台のマシンでやってしまおうということです。

方法1: Network Namespace(netns)とvethを使う

おそらくこっちの方がまともな方法です。

Network Namespaceを使うと、メインでいじっているマシンのとは全く別の仮想的な端末のようなものを用意することができ、この2つの間をvethという仮想的なイーサネットケーブルで接続して通信することができます。

ここに関しては多くの解説記事があります。Network namespaceによるネットワークテスト環境の構築 - ビットハイブなどがわかりやすいかと思います。今回であれば物理マシンともう1つのnetnsがあればいいのでnetns関連のメインのコマンドとしてはip netns add ns1ip link add veth0 type veth peer name veth0 netns ns1の2つだけがあればいいと思います。その他もろもろの設定(ruleとかrouteとかiptables/nftablesとか)はすべてip netns exec ns1 xxxxみたいな感じで実行します(xxxxをbashとかにしてシェルを起動すると、毎回打たなくてすむので楽)。

あとその記事には載っていませんがlo(ループバックデバイス)もupしておかないとpingとかがうまくいかないと思います(network namespaceで遊ぶとかにはloに関するコマンドがあります)。

これをやると、別のnamespace(今回ならns1)から、vethケーブル→物理ケーブルと通って外に出ていけるようになるので2回NATができます。

ただns1のほうの127.0.0.53にはそのままだとDNSサーバーが立っていなくて、ここをどうするのがベストかは知りません。個人的には、メインのほうからいったんns1に入ってまた同じところを通ってメインのほうに出てくるという変な構成で遊んでいました。これだとNATができる場所が3回あります。さすがに同じケーブルを通って返ってくるのは…と思ったら、vethを2本にするという手もありそうです。

実は、netnsを作成せずメインの名前空間どうしをvethで接続することもできるのですが、それだと多分お互いのipがlocalテーブルを通じて見えている(loが処理しようとする)せいか、ARPなどがうまくいかなくて通信が通らなかったのでこれはまともなやり方ではなさそうです。以下も参照してください。

linux - network level of veth doesn't respond to arp - Server Fault

networking - Why two bridged veth cannot ping each other? - Unix & Linux Stack Exchange

本当は、tunみたいなPOINTOPOINTモードのケーブルのペアみたいなのを作成できれば良さそうなのですがそういうものは見つけられませんでした。

方法2: そのままパケットを送り返してくるtunを使う

ちょっとネタバレしてしまいましたが2つ目はtunを使う方法です。というかtunを使えばNATも好き勝手にできるので(Linuxで制限コーンNAT(EIM/ADF)を動かす - turgenev’s blogを参照)それ以上解説する必要もないんですが、単に2回iptablesのNATをかけるだけなら実は「送られてきたパケットをすべて送り返す」だけの単純なtunでできますという話です。そのようなtunの実装の例は(上記の記事で紹介してるリポジトリの)rat/reflector.rb at main · kazuho/rat · GitHubにあります。短いですね。

コツとしてはこのデバイスにローカルで生成されたパケットが入っていくときにiptablesでSNATしてローカルに存在しないIPに書き換えておくことです。そうじゃないと、「自分自身を送信元としたパケットが外から送られてくるなんておかしい!」ということでドロップされてしまいます。まあこれはrp_filterとかaccept_localを変更すると一応許可することもできます。

それ以外の設定もtcpdumpとかで丁寧に見ていけば普通にできると思います。

感想など

どっちのパフォーマンスがいいのかはちょっと気になるところです。1つ目はカーネルの機能ですが、ネームスペースまるごとというのはちょっと重そうな気もします。

(特にtunを使う方で)mtuに関する問題があればLinuxで制限コーンNAT(EIM/ADF)を動かす - turgenev’s blogも見てみてください。

あと自分のところでは2回NATできることを試しただけで、実際にMap-eのために使っているわけではありません。

NAT動作をめぐる誤解まとめ

概要

この記事では、P2P通信の快適性などに大きく影響するところでありつつ、メーカー・プロバイダ側からそれほど積極的に情報が提供されているとは言えないNAT機器の動作について、よくある誤解をいくつか取り上げて解説します。

NAT動作に関する前提知識(cone NAT, symmetric NATあるいはEIM/APDFなどの用語)NATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogなども参照してください(多少被っている内容もあります)。

Map-e(v6プラス・OCNバーチャルコネクトなど)ではポート開放ができない?

この記事を読む人でここを勘違いしている人はあまりいないと思いますが、まずは初歩的なところから行ってみましょう。Map-eによるIPv4 over IPv6(IPoE)通信ではIPv4のグローバルアドレスが割り当てられますが、他のユーザーと共有されており、自分に割り当てられた一部のポートしか使うことができません。OCNバーチャルコネクトなら1008個、v6プラスなら240個などと、サービスにより違いもあります。

グローバルIPへのNATはあくまでユーザー側で好き勝手に行えるので、この割り当てられた範囲内のポートであればポート開放などは自由に設定することができます。ただしTP-Link製など一部のルーターはMap-eで動かすとポート開放ができないものもあるようです。Map-eは多分国内の独自規格なので国産のルーターを買ったほうが安全な気がします。

また、一部のプロバイダではIPoE IPv6接続とPPPoE接続を同時に使わせてくれるところもあり、これだとPPPoEの方でポート開放ができます(ただし速度が遅くなるかもしれない)。これに関しては自分の過去記事のフレッツ光関連の設定について(ドコモ光、ひかり電話、IPoE/IPv4 over IPv6とPPPoEの併用など) - turgenev’s blogIPoE/PPPoE併用時(など)に一つの端末から同時に複数の接続経路を利用する - turgenev’s blogを含めて多くの情報があると思います。

あと、IPv6に関してはそもそもNATがないのでファイアウォールに穴をあければ自由にポート開放(と呼ぶのが正しいかわかりませんがそれと同等のこと)ができます。

DS-Lite(transix・クロスパスなど)ではポート開放ができない?

まあこれは基本的には正しいです。DS-Liteではプロバイダ側でNATが行われるため、グローバルIPに来た通信の扱いには手が出せません。

ただし、DS-LiteのNATはキャリア回線などと同じくフルコーンNATで、これはUDPだけでなくTCPもです。つまり、内側のTCPポートからインターネット側に何らかのパケットを送信してグローバルIP上のポートが割り当てられたとすると、その割り当てが有効な間はこのグローバルIP上のポートは任意の場所からの通信を受け入れて内側に通すようになります。つまり一時的にポートが開放された状態になるということです。

ということは、適当なホストにむけて一定間隔でkeepaliveパケットみたいなのを送っておけばそのTCPポートはいつまでも開放されるということになります。まあ検出されて強制リセットされたりするのかもしれませんが、そこから復帰する仕組みをアプリケーション的に実装するのはそこまで大変ではない気もします。誰かやってみてください。

TCPではなくUDPに関して言えば、まさにこれと同じことをやるのがUDPホールパンチングです。ポート開放の目的がVPNであれば、UDPホールパンチングをやってくれるVPNソフトウェア(tailscaleとかzerotierとかSoftEtherとかhamachiとか)を使うことで、ポート開放と同等の(リレーサーバーを介しない)VPN通信環境が実現できます。Tailscaleであれば他ユーザーにデバイスをシェアする機能もあるので友人同士などであればこれを使って(ポート開放が必要な)ゲームのマルチプレイをすることもできます。

まあとはいえ普通にポート開放ができたほうが何かと嬉しいので、(この記事でこのあと説明する色々な要素を加味した上で)自分だったらMap-eを選ぶと思います。

ポートセービングIPマスカレードとSymmetric NATは同じ意味?

違います。これはNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogのAddress Dependent Mappingあたりの説明をちゃんと読めばわかると思うのですが、ポートセービングIPマスカレード(ポートの共有)が行われることとSymmetric NATであることは基本的には完全に独立です。「ポートの共有を行わないSymmetric NAT」も「ポートの共有を許容し、結果として同じ内側(Address, Port)から複数の異なる宛先への通信に同じポートが使われる可能性があるNAT」も何の矛盾もなく可能です。

唯一、確実に言えることは、「ポートの共有が行われるならEndpoint Independent Mapping(cone NAT)ではない」ということです。EIMではないからといってSymmetric NATであるとは限らないというだけの話です。

ただ、ポートの共有を行わないSymmetric NATではEIMよりもかなり多くのポートを消費してしまうので、「Symmetric NATならばポートの共有を行う実装が多い」というのは傾向としては正しいのかもしれません。これに関しては色々なSymmetric NAT実装を見てみないとわかりません。

v6プラスとかはポートが少ないからポートセービングIPマスカレードを搭載しているYAMAHAとかNECルーターを買うべき?

これも間違いです。Linuxのnetfilterのconnection trackingとNAT動作の仕組み - turgenev’s blogでも解説していますが、多くの安物のルーターに搭載されていると思われるLinux(netfilter)のNAT機能はデフォルトでポートセービングIPマスカレードを行います。TCPUDP両方で有効です。家にあるBuffaloとNEC(Aterm)の最安価格帯の機種でも、NTT製のHGWでもそうでした。CentOSでOCNバーチャルコネクト | QuintRokkこのへんでも問題なくポートが使いまわされていることが確認できます。

もちろん他の性能面で差はあれど、少なくともポートの枯渇を避けるためという理由で高級品を買う必要はありません。

念のためですがポートセービングIPマスカレードが搭載されていればv6プラスの240ポートであっても困ることはまずありません。

EIM/ADF(アドレス制限コーンNAT)はNATタイプBになる?

間違いです。Nintendo Switchでは「NATタイプ」をA-Dの4段階で判定でき、EIM/EIF(フルコーン)ならA、EIM/APDF(ポート制限コーン)ならBになるのですが、両者の間にあたるEIM/ADF(アドレス制限コーン)についてはフルコーンと同じくNATタイプAになります。よく読まれているイカスミカフェの「NAT」「NAT越え」「NATタイプ」ってなーに?や、スプラトゥーン2とNATの奥深い関係(2ページ目) | 日経クロステック(xTECH)などでは間違って記述されているため注意が必要です。おそらく、EIM/ADFの実機がほとんどなくてテストしていないのでしょう。Nintendo Switch の「NATタイプ」判定条件 #Network - Qiitaは正しいです。

実際、UDPホールパンチングの成立条件の表を見ていると、EIM/ADFではEIM/EIFと同じく任意の相手とP2Pが成立するので、タイプAと判定されるのは妥当です(厳密には毎回違うアドレスが割り当てられるような変なNATが相手だったら通信は成立しにくくなりますが、そんなものは普通はありません)。

SymmetricPort RestrictedConeRestrictedConeFull ConeFull ConeRestricted Cone×Port RestrictedCone××Symmetric

またEIFと違ってADFは既知のアドレスとの通信しか通さないのでセキュリティ的にもベターです。逆にEIM/EIF以外のNATタイプは、既知のアドレスからの通信しか内側に通さず、かつ既知のアドレスからの通信であれば(相手が適切なポートを使用すれば)内側に通ってしまうという点ですべて同じです(TCPのConnection Dependent Filteringなどは除く)。そう考えるとEIM/ADFは妥協点として一番良さそうな気がします。

ただ、実装が不十分(EIM/ADFを想定していない)だとEIM/ADFとSymmetricの間でのUDPホールパンチングが成立しない可能性も否定はできず、そこは実装ごとに調べるしかないです。

実機を触る機会がなくてPS4/PS5やXBoxでの判定がわからないので、だれか試せた人がいればコメントで教えてください。

DMZするとフルコーンNATになってNATタイプがAとか1になる?

現象としては大体そうですが厳密には違います。

DMZホスト機能とは、(ほとんど)全てのルーターのポートを番号を変えずに特定IPに転送する機能で、言い換えれば特定のIPの全ポートを開放することになります。基本的には直接インターネットに接続されたような格好になりフィルタリングの類もなくなるのでNATタイプはA(あるいはタイプ1、オープン)になることが多いでしょう。任天堂のページでも公式に案内されています。また英語圏でもiptablesでfull cone NATを作る方法などとして紹介されているのを見かけます。

ただ、少なくともnetfilterの挙動に関して言えば、絶対に固定で内側ポートと同じ番号が割り当てられるというわけではなく、他のIPと競合すれば意図したポートが割り当てられない(結果としてEIMにならない)可能性はあります。まあこれは確率的には低いのでUDPホールパンチングが成立しにくくなる心配をする必要はありません。

あとは一応、内側からの通信では全然別のポートが割り当てられる(DNATが固定なだけでSNATはランダム、とか)という動作の可能性もあり、これだとSTUNとかを使おうとするとうまくいかなくなる気がします(外部からの接続を受け入れるだけなら問題ない)。そんな実装はあまりないのかもしれませんが。

それと、DMZホストでは全てのポートが常時開放されています。EIM/EIFはあくまでマッピングが存在する間だけ「一時的にポートが開放される」動作です。ゲーム機に使うなら気にする必要はなさそうですが一応セキュリティ的にはEIM/EIFのほうが少しだけマシです。

余談ですが、DMZホスト的なことをするにしても1-1023の特権ポートを開放するのは危険なので1024-65535(あるいは可能なら32768-65535などエフェメラルポートの範囲のみ)にしておいた方が無難です(40000-65535で十分だったという情報があります)。一時期、任天堂でも間違って案内されていたようです。

これも余談ですがこの「DMZホスト機能」がある時点でそのルーターには何らかの形でポートセービングIPマスカレードが備わっていることが明らかです。さもなければ、全ポートが特定のIPに転送されている状態で別の端末がインターネット通信できるはずがありません。

あとこれも余談ですが、この「DMZホスト機能」というのは、原義の「DMZ」とは全く別物です(参考:ブロードバンドルータのDMZホスト機能 2台のルータでDMZを用いたネットワークを構築し安全に自宅サーバを公開する | 積水成淵日記natテーブルを利用したLinuxルータの作成:習うより慣れろ! iptablesテンプレート集(2)(6/6 ページ) - @ITなど)。

DS-LiteはEIMなのにポートが1024しかないから枯渇すると詰む?

EIMということは家の中の機器が何かポートを使用するごとに1024個のうち1つが消費されるので、まあまあ人数が多かったりすると実際わりと枯渇するようです。それでNATもキャリア側でやってるから手が出せなくて詰み、という主張です。

まあ普通に使っているだけならそうなりますが、むこうのNATが機能不足ならこちらで勝手にポートセービングIPマスカレードをして出口を絞ってやれば解決します。つまり適当なLinuxマシンとかYAMAHA/NECみたいなルーターを用意して、家の全部の機器をその配下にして、出口ポート数を1024に絞ってNATして、それからDS-Liteルーターに食わせれば、そもそも出口が1024個なのでキャリア側では絶対に枯渇しないというわけです。

ちなみに出口を絞ってNATするのは単に使うポートを少なくするだけなので潜在的には安物ルーターでも全然できるはずなんですが、残念ながらこれを自由に設定させてくれるような安物ルーターは無さそうです。もしあったらそれを使うといいと思います。

あとそもそもDS-LiteってプライベートIPがついたLAN内のパケットをそのままIPv6にラップして家の外に出してるので、NATしておいた方がセキュリティ的にも安心感はあるかもしれません。

ただしこれだとDS-LiteのEIM/EIFの旨味がなくなってしまうので、必要に応じて一部の通信(VPNのポートとかNintendo Switchとか)は直接DS-Liteルーターの配下につないでおくのがよさそうです。その場合ポートセービング担当のルーターのほうは500ポートだとかもうちょい絞っておいてもよさそうです。

Map-eはDMZができないからNATタイプAとか1にするのは無理?

v6プラスとかOCNバーチャルコネクトは使えるポートが制限されているからDMZが使えなくてNATタイプAにするのは絶対無理、だからフルコーンNATなDS-Liteを使うべき(あるいはPPPoEパススルーしろ)、といった感じの主張です。

これは間違いです。あくまで市販ルーターの実装がフルコーン/制限コーンNATでないというだけで、出口ポートの数が制限されていてもフルコーン/制限コーンNATはできますし、そうすればちゃんとNATタイプAになります。DMZはフルコーンNATにするための一つの方法であり、必要条件ではありません。ただしこれもやっぱりLinuxYAMAHAルーターNECの高いルーターか、そのあたりが必要です。

Linuxでフルコーン/制限コーンNATを動かす方法についてはLinuxでポートセービングIPマスカレード付きの制限コーン風NAT(EIM/ADF)を動かす - turgenev’s blogを参照してください。YAMAHAとかNECでは、ポートセービングIPマスカレードを設定で無効にするとフルコーンNATになるはずです。

TCPはホールパンチングには基本使わないしフルコーンにすると枯渇のおそれがあるので、P2P通信(ゲーム機など)のUDP通信だけフルコーンにするのがいいと思います(NETルーターでの設定例: https://twitter.com/xpzr4/status/1527318238400757761)。

とはいえ初心者にはかなり設定が難しいと思うので、ネットワークに自信がなくて家族の人数が少なくてサーバー公開も必要なくて対戦ゲームが快適にできることは重要という場合はDS-Liteを選ぶのがいいかもしれません。

RFC 4787のNAT分類ではMappingがEIM/ADM/APDM、FilteringがEIF/ADF/APDFで、3x3=9種類ある?

これどこにもちゃんと書いてないけど多分嘘だと思うんですよね…詳しくはNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogを見てくださいという感じなのですが、例えばマッピングがAPDMなのにフィルタリングはEIFって、少なくともポート共有が有効な場合はどう考えてもありえません(マッピングに存在しない宛先から通信が来たとして、一体どの内側ポートに転送すればいいのか?)。要するにFilteringよりもMappingのほうが細かいのはありえないということです。

結局、EIMならEIF/ADF/APDF、ADMならADF/APDF、APDMならAPDF、という計6種類になると思います。念のためですがnetfilter搭載の多くのルーターはこのどれにも入りません。

RFC 3489の分類はガバガバだ!とRFC 4787は言っていますが、結局大して分ける必要なかったんじゃん?という…

二重ルーターだとSymmetric NATになる?

これもNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogに書きましたが、間違いです。複数あったら最も厳しいポリシーに揃えられるというだけで、EIM/EIFを10個つなげたところで全体はEIM/EIFのままです。

二重ルーターが問題になるとよく言われているのはおそらくUPnPの関係だと思います。UPnPは自分を直接管轄している一つ外側のルーターに対してポートの開放を要求するものであり、それがグローバルIP上のポートじゃなかったらうまくいかないという話です。一応、UPnPブリッジとかいう機能があれば二重ルーターでもちゃんとUPnPできるという話もあるようです。

UPnP自体はセキュリティ的に怪しいらしいので基本的には無効にして、NAT動作を工夫するのとあとは静的ポート開放をちゃんとやるのが良いと思います。

TCPでホールパンチングするのは無理?

これもNATタイプ(ryに書いたものですが、NATタイプが同じであればUDPホールパンチングとTCPホールパンチングの難易度は変わりません。

しかし実際には、TCPに関しては、既知の相手であっても外側からの通信は受け入れないというRFC 4787でいうところのConnection Dependent Filteringの動作をするNATが多く(netfilterがそうなっている)、こうなるとホールパンチングは困難になります。

UDPホールパンチングのためにはNATはEIMでなければいけないのでポートセービングIPマスカレードは不可能?

ここまでちゃんと読めていればわかると思いますが、NATが「ほとんどの場合にEIMのように振る舞う」ためには、必ずしもNATが「厳密にEIMの定義に従っている」必要はありません。

さらに、NATタイプ(ryに書いた通り、宛先アドレスが異なる場合のみに限ってポートセービングIPマスカレードを認める場合は、フィルタリングをADFにすることができます。つまり、以下のようにすれば、EIM/ADF「的」な動作とポートセービングIPマスカレードを両立できます。

  • 通信相手のアドレスのみに関するマッピングを使用する。(ポート番号は含めない)
  •  
  • 内側のポートがインターネットの新規の相手と通信しようとした際、その内側のポートに(別の相手との通信のために)直近で割り当てられたものと同じポートを原則として割り当てる。(=EIM的な動作)
    • (その内側のポートの使用履歴が無ければ、内側のポートと同じ番号、あるいはそれが利用不可ならランダムな番号を割り当てる)
  • ただし、そのポートを通ってその相手(のアドレス)と通信している他の内側ポートがすでに存在して競合する場合は、別のランダムなポートを割り当てる
    • そのポートを通って他の相手と通信している内側ポートが存在する分には競合しないのでそのまま割り当てる(=ポートセービングIPマスカレード
  • マッピングに一致する外側からの通信はすべて内側に通す(既知のアドレスなら、未知のポートからの通信でも通る)

こうすれば、近接したタイミングで同じポートから2つの相手と通信した際には同じポートが割り当てられることが期待され(EIM)、既知のアドレスからの通信はすべて内側に通り(ADF)、かつアドレスが異なる場合はポートセービングIPマスカレードができます。

一般には同じアドレスの異なるポートに大量のコネクションを張ることはあまりない(だいたい53, 80, 443とかしか使わない)ので、ポートも条件に入れたポートセービングIPマスカレードとほぼ同等の効果が得られるはずです。

というわけでこれをLinuxで実際に動かしたという記事がLinuxでポートセービングIPマスカレード付きの制限コーン風NAT(EIM/ADF)を動かす - turgenev’s blogです。実際、手元でもホールパンチングとポートセービングIPマスカレードが両立することを確認しています。

まとめ

思った以上にいろいろな種類の誤解があることがわかります。NAT動作は原理としては単純な話ばかりなので、適当な情報を鵜呑みにせずにちゃんと整理して考えてみるのが大事だと感じます。

元をたどれば、RFC 3489やRFC 4787が網羅的でない雑な分類を提示したために、UDPホールパンチングやポートセービングIPマスカレードという実際の使い勝手に影響を与える部分に関して深く考察されることがないままCone NAT/Symmetric NATという二元論が広まってしまったことが要因にあるでしょう。実は、draft-naito-nat-port-overlapping-01という文書がこの記事で書いたような事実に触れているのですが、残念ながらこれが正式なRFCに反映されることはなかったようです。

時代はIPv6へと移行してきており、IPv4でのNATに脚光が当たることはもはやないのかもしれませんが、少しでもNATに関する正しい理解が広まることを願っています。

質問などあればお気軽にどうぞ。

Linuxのnetfilterのconnection trackingとNAT動作の仕組み

概要

LinuxではNATやファイアウォールなどのパケットの扱いを担当するカーネルの機能としてnetfilterというものが搭載されており、特にそのconnection trackingの機能を理解することがLinuxのNAT動作の理解には不可欠です。また、安価な市販ルーターの多くはLinuxを搭載しているため、netfilterを理解することは市販ルーターの動作を理解することにもつながります。

NAT動作タイプ(cone NAT, symmetric NATあるいはEIM/APDFなどの用語)については以前の記事(NATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blog)などを参照してください。

他のプロトコルに関しても参考になる部分はあるかもしれませんが、この記事では主にUDPTCPのみについて扱います。

conntrackコマンド

以下で説明するconnection trackingの様子はconntrack -Lというコマンドで閲覧できます。接続情報のタイムアウト秒数なども表示されていて大いに参考になります。

conntrackはデフォルトではインストールされていない場合もあるのでパッケージマネージャなどで入れましょう。

connection trackingのエントリ

connection trackingは一言でいえば「現在有効な接続」の一覧を通信の両端点のアドレスとポートに従って管理するものです。何かしら新規の通信が発生すると接続情報がエントリ(マッピング)として登録され、(接続形態に応じて)定められたタイムアウト時間が経過するまでそれが保持されます。

conntrack -Lコマンドの出力から1行取り出して見てみましょう。

tcp      6 118 TIME_WAIT src=192.168.1.12 dst=192.168.1.9 sport=3311 dport=5678 src=192.168.1.9 dst=192.168.1.12 sport=5678 dport=3579 [ASSURED] mark=0 use=1

これの意味は、「TCPプロトコル番号6番)通信で、タイムアウトまであと118秒で、状態はTIME_WAIT、通信を開始したのは192.168.1.12:3111で宛先は192.168.1.9:5678、相手側から見ると送信元は192.168.1.9:5678で送信先は192.168.1.12:3579、確立済みのコネクション(ASSURED)で、マークは0」という感じです。useだけはちょっと意味がよくわかりませんがiptables - "use" column in /proc/net/ip_conntrack or nf_conntrack? - Server Faultによると一種の参照カウンタ?のようです。あまり気にしなくていい気がします。

「相手側から見ると192.168.1.12:3579」なのはなぜ?というのは後で解説しますがNATが介在するとこうなります。

ここでの「マーク」は接続に対するマークであって、パケットに対するマークではありません。iptablesでいえばCONNMARKのほうであってMARKではありません。注意してください。

登録のタイミング、TCPUDPの違い

上記の通り登録のタイミングは「新規の通信が発生したとき」ですが、これはTCPUDPというプロトコルの性質によっても若干違います。

TCPでは明示的な接続の概念があり、基本的にはSYNパケットの送受信に伴ってエントリが作成されます。ただしSYNパケットが送られただけで応答がないときなどは通信が成立したとみなされないので、さっきのTIME_WAITのところはSYN_SENT、[ASSURED]のところは[UNREPLIED]などとなります。この時点ではタイムアウトも数十秒程度など短めに設定されています。ここからhandshakeが完了して通信が確立するとTIME_WAITのところがESTABLISHEDになり、[UNREPLIED]が[ASSURED]に変わって、タイムアウトも数時間程度など大幅に長くなります(もちろん設定で変えることはできます)。接続が終わると先ほどのようにTIME_WAITの状態になり、再びタイムアウトが短めに修正されます。

UDPでは接続状態がないのでもっと単純で、今までのリストに存在しない(src IP, src Port)と(dst IP, dst Port)の間で通信が発生するとエントリが作成されます。こちらも一方向にしか通信がない場合は[UNREPLIED]、双方向に通信が発生した場合は[ASSURED]と区別され、タイムアウト時間にも差があります。一方で、TIME_WAITやESTABLISHEDにあたる部分に関しての区別はないようです。

逆に言えば、新規の通信でないパケット、つまり既存のマッピングに一致する(src IP, src Port)と(dst IP, dst Port)の組み合わせを持つパケットはそのマッピングに従って転送されることになります。

だいたいここまでがconntrackの基礎です。

NATの適用

NATも、新規の通信が発生したとき、つまりconntrackのエントリ作成と同じタイミングで行われます。NATは大きく分けて送信元を書き換えるSNATと送信先を書き換えるDNATの2つがあり、SNATはPCを出て行く直前のパケット(iptablesのPOSTROUTING)、DNATはPCに入ってくる直前のパケット(iptablesのPREROUTING)及びPC内部で生成されて出て行くパケット(iptablesのOUTPUT)に適用されます。masqueradeというのはSNATの一種です。

例えばDNATの例としては192.168.1.1の3000に来たパケットを192.168.1.2(別のPC)の13000に転送するといったものが考えられます。

この場合であれば、192.168.1.1の3000に未知のホスト(例えば1.1.1.1:11111)からSYNパケットが送られてきた際には、

src=1.1.1.1 dst=192.168.1.1 sport=11111 dport=3000 src=192.168.1.2 dst=1.1.1.1 sport=13000 dport=11111 

のようなエントリが作成され、1.1.1.1:11111からすれば192.168.1.1:3000に送ったつもりのところが内側では192.168.1.2:13000から1.1.1.1:11111への通信が行われているという様子が表現されています。

エントリの競合の回避

エントリの新規作成にあたっては、「既存のエントリと競合(衝突)しないか?」ということが考慮されます。競合というのは、同じポートを同じ宛先に対して使用する二つのエントリが存在する状況です。例えば、192.168.1.2:6000が192.168.1.1:8000を使って1.1.1.1:11111と通信している状況で、192.168.1.3:7000も192.168.1.1:8000を使って1.1.1.1:11111と通信する、というようなエントリを作ってしまうと、1.1.1.1:11111からパケットが送信されてきたときにそれを192.168.1.2:6000と192.168.1.3:7000のどちらに送ればいいのかわからなくなってしまいます。

netfilterは、このように転送先が一意に決定できなくなるような状況が発生するのを避けます。例えばNAT先のポートとして8000-8010が指定されていたときにこの状況になったら、8000番以外のポートが選ばれます。もし8000番しか候補がなければ、ポートの割り当ては失敗し、通信が成立しなくなります。これはTCPでもUDPでも同じです。

逆に、通信先が違う場合であれば、同じ8000番を使う複数のマッピングが同時に存在していても問題はありません。この辺に関しては前回記事でも「ポートの共有」「ポートセービングIPマスカレード」のようなキーワードで説明しました。

この動作は、ポートが他のアプリケーションによって使用されている(EADDRINUSE)というエラーとは全く無関係なので注意してください。

ポート番号の選択とNAT動作タイプ

基本的にはDNATもSNATもどちらも、可能な限り元のポート番号を維持しようとします。つまり、変換元のポート番号と同じ番号が利用可能な場合は、上記の競合が起こらない限りは、それが割り当てられます。

同じ番号が選ばれる状況だとnetfilterのNAT動作はcone NAT、つまりEndpoint Independent Mapping (EIM)と一致します。一方で、競合が起これば一つの変換元ポートに対して複数の変換先ポートが存在することになるので、EIMの厳密な定義には当てはまりません。

そうでない(利用不可能or競合)場合は、ランダムなものが選ばれるようです。ただ、条件によっては(変換先のポート範囲が大きくないとき?)、その変換元のポートに対して(別のどこかに接続したときに)前回選ばれた変換先ポート番号と同じものが選ばれることもあるように見えます。

また、マッピングが宛先の(Address, Port)に従って管理されている関係上、未知の(Address, Port)から来た通信が内側に通ることはないので、フィルタリング動作としてはAddress and Port Dependent Filteringになります。ただしTCPに関しては、既知の(Address, Port)からだったとしても外部から来たものは別の接続と見なされるため、内側に通されません。従ってConnection Dependent Filtering(この用語はあまり使われませんが)になります。

結局、UDPに関しては、全体としてポート制限コーンNATPort Restricted Cone NAT)のように動作します。

ちなみに、SNATなどのオプションでrandomなどを指定すれば最初から完全にランダムなものが選ばれます。これはsymmetric NATやEndpoint Dependent MappingあるいはConnection Dependent Mappingといった動作に近くなりますが、あくまでランダムというだけで、必ず別のポートを割り当てるというわけでもないため、厳密にはそれらの定義には当てはまりません。

ファイアウォールによるフィルタリング動作

conntrackはNATを伴わないファイアウォールとしても機能します。典型的にはufw enableで有効になるようなもののことです。これも同じように、既存の接続と一致しないものは通されないので、UDPならAddress and Port Dependent Filtering、TCPならConnection Dependent Filteringの動作になります。

NAT自体が起こらないのでMappingも何もないですが、しいて言えばEIMと考えればいいです。

ポート範囲をずらす形でのNAT

上記の動作により、例えば192.168.1.1の10000-20000と192.168.1.2の10000-20000というように全く同じポート番号範囲での変換であれば同じ番号の間で1対1に変換が行われます。一方で、192.168.1.1の10000-20000と192.168.1.2の30000-40000など、個数は一致していたとしても範囲が異なっていると、変換先ポート番号は予測不能になります。

しかし、nat - Is it possible to map 1:1 port range iptable DNAT rules - Stack Overflowによると、比較的最近(2018年、Linux 4.19あたり)から、ポート範囲をずらす形での1対1での転送ができるようになったようです。

どのように指定するかというと、

iptables -t nat -A PREROUTING -d 192.168.1.1 -j DNAT --dport 10000:19999 -j DNAT --to-destination 192.168.1.1:20000-29999/5000

のようにスラッシュでオフセットを記述します。すると、変換先ポート範囲を0-indexedな配列として見たときに「変換前のポート番号からオフセットを引いたものを変換先ポート範囲の個数で割った余り」をindexとしたポート番号を(優先して)選んでくれるということのようです。この例であれば変換前が12000とするとそこから5000を引いて10000(変換先ポート範囲の個数)で割って0あまり7000なので(0-indexedで7000ということは7001番目であるところの)27000が選ばれるというわけです。

これはmanとか見ても書いていなくて、こことかここにあるコミットログみたいなやつとかソースコードを見るくらいしか手がかりがありませんが、手元ではちゃんと動きました。

ただ、少なくともiptablesではDNATにしか使えないようです。SNATでは同じ構文を使ってもエラーは出ませんが挙動はスラッシュ以降がないときと変わらないように見えます。

nftablesではSNATでもDNATでも使えません(ただしiptablesコマンドから設定するとなぜかちゃんと動く)。Linux Netfilter Devel — Re: [PATCH nftables 0/8] Support for shifted port-ranges in NATのように1年前くらいにパッチを送っている人がいることはわかりますが、大元のソースを見る限りまだ取り入れられていないようです。てかパッチの見方わからん…

netfilterの自体はSNATにも対応していそうに見えるので、カーネルモジュールとか書けば理論上はいける気がします。あるいはLinuxで制限コーンNAT(EIM/ADF)を動かす - turgenev’s blogみたいな自前のNATを使うのもありですね。

もちろん、別にずらした形での転送ではなくても構わないというケースも実際には多そうです。

「ポートの開放」の意味、市販ルーターの挙動

多くの市販ルーターでは「ポート変換」「ポート開放」「静的NAPT」「静的IPマスカレード」といった名前で、ルーターのポートにアクセスした際に内部の機器の別のポートにアクセスが通るように設定することができます。これがnetfilterにおけるどのような設定に対応するかを説明します。

ルーターとして稼働しているlinuxのnetfilterにおいて「ポートの開放」をするといったときに含意される可能性のある操作は概ね以下の3つだと思います。

Destination NAT(DNAT)により、宛先を変更する…たとえば、iptables -t nat -A PREROUTING -p tcp -d 1.1.1.1 --dport 10000:20000 -j DNAT --to-destination 192.168.1.1:10000-20000

②Source NAT(SNAT)により、送信元を変更する…たとえば、iptables -t nat -A POSTROUTING -p tcp -s 192.168.1.1 --sport 10000:20000 -j SNAT --to-source 1.1.1.1:10000-20000

③上記のパケットを許可…たとえば、iptables -A FORWARD -p tcp -d 192.168.1.1 --dport 10000:20000 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT(あるいは、ufw route allow 10000:20000/tcp

このうち①③は必須で、①がないとアクセスの転送が行われず、③がないとパケットがルーターの手前で止まってしまいます。

一方で、②は設定しなくても外側からのアクセスは内側に転送されるため通常のサーバー用途であれば問題なく動作します。ただし実際には手元で使っているルーターでは2つともSNATが行われていました。

例1: NATよりも既存エントリが優先される

netfilterの挙動に慣れるため、例を見てみましょう。

例えば今、1.1.1.1:10000から192.168.1.1:5000にDNATが設定されているとします。このとき、1.1.1.1:10000を使用(バインド)して2.2.2.2:20000に向かってUDPパケットを送信したとします。するとこの2つの間でのエントリが作成されます。このエントリが存在する間に2.2.2.2:20000から1.1.1.1:10000に向かってUDPパケットが送信されると、(仮にアプリケーション上は最初のパケットと無関係だったとしても)それはDNATされずに1.1.1.1:10000にそのまま到達します。他の場所(例えば2.2.2.2:20001)から来たパケットはDNATされます。

SNATでも同様です。例えば1.1.1.1:10000から1.1.1.1:5000にSNATが設定されていたとしても、2.2.2.2:20000から1.1.1.1:10000に向かってUDPパケットが来たら、以後1.1.1.1:10000から2.2.2.2:20000に向かって出て行くパケットはSNATされずそのまま1.1.1.1:10000から出て行きます。

このように、通信相手との間に既にエントリがある場合はNATが使用されなくなることがあります。

TCPでは接続状態があるのでもう少し安定した挙動になります。同様にDNAT設定した上で1.1.1.1:10000から2.2.2.2:20000へ接続したとして、この接続が閉じた後のTIME_WAITの状態のときは、2.2.2.2:20000から1.1.1.1:10000に来た接続は問題なく192.168.1.1:5000にDNATされます。

ESTABLISHEDの状況でもどうなるのか調べたかったのですが、ここでいう2.2.2.2:20000側の動作を再現する(接続中のTCPコネクションと同じポートからその相手に対して全く別の接続要求を送る)のが普通のやり方ではできないのでやめました。明らかに従来の接続と別のパケットだと判別できるので多分DNATされると思いますが、そもそもINVALID扱いで拒否されるのかもしれません。このへんあまり理解しておらず…

例2: 外部からのパケットによるマッピングとの衝突

UDP通信では、未知のhost:portから自分のポートにパケットが送信されてきたときにもエントリが作成されます。さらに、この状態で内側からそのエンドポイントへの通信を開始しようとすると、競合を避けるためこのポートは使えません。

例えば、PC2の30000番から1.1.1.1:40000に対してUDPパケットを送信すると、40000番をリッスンしているアプリケーションが無かった(この場合ICMPのunreachableが返ることもある)としても、[UNREPLIED]という扱いで1.1.1.1:40000に関するエントリが登録されます。このときにPC1をルーターとして使用している別の端末のポート(例えば192.168.1.1:40000)がPC2の30000番にアクセスしようとすると、PC1の40000番は既に1.1.1.1:40000によって使用されているため競合し、40000番以外のポートが使用されます。もちろん、1.1.1.1:40000自体からパケットを送り返す分には(この既存のエントリに従って)そのまま40000番から出ていきます。

これは例えばLinuxルーターとして使用してUDPホールパンチングを行う際に問題になります。STUNサーバーとの通信時に割り当てられたポートを相手に通知しても、先にそのポートに相手からの通信が到達してしまうと、内側の端末がそのポートを通じて相手と通信することができなくなるからです。実際、判定アプリのリモートアクセスチェックツール|DiXiM.NETなどでは、filtering判定を先に行うため、意図した結果が出なくなります。

解決策としては、既存の接続と関連しない外部からのパケットをACCEPTしないDROPREJECTする)ことが有効です。端的に言えばファイアウォールufwなど)を有効にすると解決します。これにより、パケットがポート(上記の例なら1.1.1.1:40000)に到達する前にドロップされるため、エントリが作成されなくなります。

また、PC自体に対してマッピングが作成されてしまうことが問題なので、内側の端末に対して明示的にDNATを指定してやれば、そちらに向けてマッピングが作成されるようになり、内側からの通信がそのマッピングを利用できるようになります。もちろん、DNATを設定できるのは(ポートごとに)1つの宛先だけです。

この問題は以下のツイートで知りました。

分かってしまえば簡単ですが、知らないと結構ハマりそうです。

TCPでは、外部からのパケット(SYN)が来たとしてもこちらがSYN/ACKで応答しなければ接続自体が開始しないのでエントリが作られることもありません。

Linux(netfilter)搭載の市販ルーターでは原則としてここでのファイアウォールにあたる機能が有効なはずなので気にする必要はあまりありません。

暗黙のSNATルール

あまり知られていないと思いますが、明示的なSNATの対象になっていない新規接続でエントリの競合が発生した際にも、同じIPの間で暗黙的にSNATが行われます。感覚的には、iptables -t nat -A POSTROUTING -d 192.168.1.1 -j SNAT --to-source 192.168.1.1のようなルールが全てのIPについて設定されているような感じです。netfilterのドキュメントにもちゃんと記載されています(https://www.netfilter.org/documentation/HOWTO/NAT-HOWTO-6.html)。

例えば192.168.1.1:3000から192.168.1.1:4000にSNATするルールだけを追加し、192.168.1.1:3000から2.2.2.2:10000に通信したとします。このときに192.168.1.1:4000からも2.2.2.2:10000に通信しようとすると、これは特にSNATの対象になっていないにもかかわらず、(ポートの割り当てができずに通信が成立しないのではなく)別のランダムなポートにSNATされた上で外部に送信されます。

TCPのTIME_WAITの再利用と市販ルーターの挙動

この辺はどこまでnetfilterの話でどこまでカーネル本体の話なのかよくわからないところもあるのですが、一応わかるところまで書きます。

再び1.1.1.1でnetfilterが動いているとします。2.2.2.2:20000から1.1.1.1:10000にむけてTCP接続が成立し、それが切断されると、1.1.1.1:10000側には2.2.2.2:20000との間でTIME_WAITのエントリが残っています。

このときに1.1.1.1:10000から2.2.2.2:20000に向けて(つまりさっきと逆方向)TCP接続を開始すると、これは問題なく成立します。conntrack -Lで見てみると、先ほどのTIME_WAITのエントリがそのまま上書きされたような形になっていることがわかります。(一方で他のポートが1.1.1.1:10000を通って2.2.2.2:20000と通信することはできません。)

これはSNAT/DNATが介在していても変わりません。つまり1.1.1.1:10000と192.168.1.1:10000の間でDNAT/SNATが設定されていたとして同様に2.2.2.2:20000からの接続が切断された後に192.168.1.1:10000から2.2.2.2:20000へと接続した場合は問題なく成立します。

しかし、手元の(netfilterが稼働していると思われる)2つのルーター(下記の1と2)ではこのケースで通信が成立しませんでした。エントリが競合するためポートが割り当てられず通信が始まらないという感じの動作です。

原因はよくわかりません。

手元のルーターのNAT挙動

stunclientなどを使用してnetfilterが内部で動いている可能性がありそうな3台のルーターの動作を実際に細かく調べてみました。それぞれの詳細は以下の通りです。

ルーター1…NTTのホームゲートウェイであるRX-600KIで、OCNバーチャルコネクトによるMap-e接続。(多分Linux搭載?)

ルーター2…BuffaloのWSR-1166DHPL2で、PPPoE接続。Linux搭載(搭載しているOSは? | バッファロー)。

ルーター3…NECAterm WX1500HPで、ローカルルーター(WAN側IPをDHCPで取得)として使用。

これらは、以下のように動作しました。

  • デフォルトの割り当て先は、内側ポートと同じ番号が利用可能ならそれを使う
  • 同じ番号が(競合、あるいはMap-eで利用可能な1008個やiptablesのto-sourceの範囲に含まれていないために)使えなければ代替のポートを使用する
  • ルーター1ではポート番号を1008で割ったもの(具体的には、余りがnなら利用可能なポートのうちn番目(ただし最初が0番目とする)のもの)が最優先の代替として使用され、それも空いていなければ連番で増やしていって最初に空いていたものを使用する
  • ルーター2の代替ポートは規則性が不明だが、5000以下など、番号の小さいものが使われることが多い
  • ルーター3では1000番台などかなり小さいものが代替として使われることが多い
  • ルーター2では、競合が起こっていないはずの状況でも代替ポートに割り当てられることも結構ある気がする
  • 特にUDPでは、競合が起こって代替ポートに割り当てられた場合は、その後の同じ内側ポートから他の宛先への通信もそちらに割り当てられるようになる(ことが多い)
  • UDPでは既に送信履歴がある(Address, Port)からでないと通信を通さない(APDF)。TCPでは送信履歴にかかわらず外側からの新規通信(内側からの通信への応答ではないもの)は通さない(Connection Dependent Filtering)
  • TCPにおいては、同じ内側(Address, Port)から外側(Address, Port)へと通信してその終了直後に再び同じ宛先で通信を開始した場合などに、別のポートが使用される場合もある(Connection-Dependent Mapping的な挙動)。特にnetfilterで55500-55599以外のポートから発信した場合に顕著に見られたほか、ルーター2でも発生した。
  • 追記: ELECOMのWRC-1167FS-BをAtermと同様のローカルルーターモードで使用すると、挙動はほぼAtermのものと同等だった。

特にルーター1のポート割り当て動作(1008で割った余りとか連番で増やすとか)はiptablesで設定するのは無理だと思うので、したがって、netfilterを直接いじっているか、あるいはそもそもnetfilterではないという可能性もあるかもしれません。(しかし競合がない限り同じポートを使用するという基本的な挙動はnetfilterに類似しているように見えますが…)

まとめ

このconnection trackingの部分がLinuxルーターとしての性質のほとんどを決定しているにもかかわらず意外と情報の少ないところかと思うので、記事にまとめられてよかったかなと思います。質問などあればコメントにどうぞ。