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のために使っているわけではありません。