Linux 6.5のnetfilterのNATはTCPのTIME_WAIT状態のポートを使いまわす

TCPでは、自分から接続を切断する際に、外向きのFIN(+ACK)→内向きのFIN+ACK→最後の外向きのACK、という順でパケットがやりとりされます。この外向きのACKを送ったあとしばらくはソケットを使い回さないようにTIME_WAITという状態が設定されます(理由は他のサイトを見てください)。

Linuxのnetfilter(conntrack)によるNATでも、(自端末あるいは他端末からの)TCP接続を管理するにあたってこのTIME_WAITの状態が発生します。TIME_WAIT状態になっているポートはその間はNATのために使用されることはなくなります。

例えば192.168.1.1:2000からのTCP通信が1.2.3.4:3000にNATされて1.1.1.1:80に出ていって、そのTCP通信が終了すると、1.2.3.4:3000はしばらくTIME_WAIT状態になり、その間に他のポート(例えば192.168.1.1:4000)が1.1.1.1:80に通信しようとすると1.2.3.4:3000が使われることはありません(行き先が1.1.1.1:80以外なら普通に使われる)。

このタイムアウト時間はカーネルパラメータのnet.netfilter.nf_conntrack_tcp_timeout_time_waitにて設定されておりデフォルト値は120となっていますが、変更することができます。net.ipv4.tcp_tw_recycle は廃止されました ― その危険性を理解する #Linux - Qiitaにある通りTCPソケット自体に関するTIME_WAITの時間を原則的に変更できないのとは対照的です。

このタイムアウトを短く(例えば5秒程度)変更すると、接続が終了したTCPポートを即座に使い回すことができるようになるため、利用可能なTCPポートが非常に少なくても(例えば16個とか)、ポートを多く消費するサイト(「ニチバンベンチ」で知られるスピール膏™ワンタッチEX|うおのめ・たこ|ニチバン株式会社:製品情報サイトなど)をそれなりに実用的な速度で閲覧できるようになります。

ここまではいいのですが、ここからが本題です。

Linux 6.5で試した結果、上記のようにTIME_WAIT状態になっているTCPポートでも即座にNATに使い回されるという現象が発生しました。例えば、宛先ポート3478の通信のNAT先ポートを1つだけに絞ってStuntman - open source STUN serverTCPモードで使用して試してみると、6.5以前のバージョンであればTIME_WAITの120秒が経過するまで一切通信が通らないのに、6.5以降だと120秒が経過する前に通信が通ります。conntrack -Lで見てみると既存の通信が置き換えられている(NAT先ポートも宛先のIP・ポートも変わらないのにNAT前のポートの部分だけが変わっている)ことがわかります。

しかも、必ず通るというわけでもなく、3回に1回など、確率的に通るようです。

試したカーネルのバージョンは以下の通りです。太字が、今回の現象が発生したものです。ディストリビューションLinux Mint 21.3で、(関係あるかわかりませんが)iptablesはv1.8.7 (nf_tables)、nftは1.0.2、libnftnlは1.2.1-1build1とかそんな感じです(カーネル以外は変えていません)。

5.15.0-102
5.19.0-50
6.2.0-39
6.3 (mainline)
6.4 (mainline)
6.5 (mainline)
6.5.0-14
6.5.0-27
6.8.6 (mainline)

mainlineというのは概念がよくわかっていませんが、GitHub - bkw777/mainline: Install mainline kernel packages from kernel.ubuntu.comを普通に使って入れられるやつのことです。

この挙動は、試した限りでは、以下のようなそれっぽいカーネルパラメータを変更しても変化しませんでした(6.5以降のものが使い回さないようになることも、6.5より前のものが使いまわすようになることもなかった)。

net.ipv4.tcp_timestamps
net.ipv4.tcp_tw_reuse
net.netfilter.nf_conntrack_tcp_be_liberal
net.netfilter.nf_conntrack_tcp_loose
net.netfilter.nf_conntrack_timestamp

Changelogなどを読んでもそれっぽい原因が全く思い当たらないので、superuser.comで質問してみたところ、ドンピシャな関連コミットの情報を教えてもらうことができました。

https://superuser.com/questions/1839024/linux-6-5-netfilter-nat-reuses-tcp-ports-in-time-wait-status
コミットはこちらです。
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=4589725502871e77d06464f731f92fd9173e2be6
これによると、128回の試行のうち、最後の32回は、シーケンス番号が増加していれば既存のTIME_WAITエントリでも構わず再利用するようです。

GCPのCompute EngineのFree Tier(無料枠)の外部静的IPには本当に課金されないのか?

ドキュメント読んでもわかりづらいのでメモを残しておきます。

2024/4現在、GCPの無料枠(Free Tier)ではメモリ1GBくらいの仮想マシンを永久無料で使えることになっていますが、これに割り当てられる外部IPv4アドレス(インターネットと接続するために必要)に果たして課金されるのか?ということが結構わかりづらいです。

いや、まあ、Google Cloud の無料プログラムには「Compute Engine の無料枠では外部 IP アドレスに対する課金は発生しません。」と書いてあって、また結論としては事実上(?)課金はされないということになったんですが、一応もう少し詳しく書きます。

自分も無料枠で使い始めてみて、しばらくの間は3万くらいのクレジットがあるので特に何も気にしなくてよかったんですがこの期間が切れていざ本格的な課金状態になってみて、しばらくすると無料のはずのマシンから1円くらいの課金額が発生していました。それで色々調べてこんなツイートもしました。

これを見ると、2021年の一時期は実際に課金されていた時期があったようです。で、この期間の英語の文面が「Google Cloud Free Tier does not include external IP addresses.」です。現在の「Compute Engine free tier does not charge for an external IP address.」(=「Compute Engine の無料枠では外部 IP アドレスに対する課金は発生しません。」)は、なんかこれと文の見た目が似てるのに意味としては逆になっていて、不安な感じがします。

不安だったのでとりあえず全部IPアドレスを外しました。しかしインターネットには接続したいので、確実に無料っぽいIPv6アドレスを割り当ててしばらく使ってみました。

ちなみにさっきの1円の課金額は本当にクレカから引かれました。

その後、また思い立ってIPv4を有効にしてみましたが、結局とりあえず課金額は1円くらいのオーダーには収まるようです。

「料金明細」(ここが一番詳しそうです)をみてみると、Compute Engineの計算コスト(メモリ・CPU)に関しては、「E2 Instance Ram running with free tier discount」として-326円といった感じの表記があります。

(一部消してあります。)これはわかりやすい感じです。

一方で、ネットワークに関しては、以下のような感じです。

同額の負の金額なのは同じですが、「free tier discount」という表現がなく、上と全く同じ文字列です。しかも、先ほどのfree tier discountとは違って、こちらはクレジットの扱いのようで、横にある「割引」(あるいは「支出に基づく割引(契約による)」)のチェックボックスを消すとこの負の金額のほうは表示されなくなります。free tier discountのほうは引き続き表示されています。ちなみに、無料期間は既に終了していて、「クレジット」の画面を開いても何もないので、なんでクレジットのありなしで金額に差が出るの?ってところも混乱しました。

どうやら、結果的に無料にはなりますが内部的に扱いが違うようです。

まあ普通に考えて「無料tierのインスタンスに紐づいているIP」かどうかを判別するのって結構ダルそうですし、その辺で多分あんまり綺麗じゃない実装になってしまっているのでしょう。その結果として計算に誤差が出て1円課金とかされてしまっているんじゃないでしょうか。

というわけでGCPの外部IPv4(ちなみに自分が使っていたのは静的のほうです)には、ほとんど課金はされないという結論になりました。

しかしIPv4の枯渇は激しく、最近はAWSでもIPへの課金が始まったみたいな話を聞いたので、こまめに課金条件とか請求額を確認しましょう。

ファイルをディスク上で暗号化し、ログイン時に自動で復号する方法まとめ(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版のバイナリが提供されています。

3proxyはAndroid(termux)でもそのままコンパイルが通り、TCP/UDPともにちゃんと動きます。danteは、./configureに--build=armを付けるのと、sockd/mother_util.cでwait3関数がないというエラーが出るのでこのへんに書いてあるようにwaitpid(-1, &status, WNOHANG)と書き換えるとコンパイルが通ります。しかしそのままだとBad system callで即落ちます(Android 14で発生、8で発生せず)。printfデバッグの結果、sockd/privileges.cにあるif文のsetegid(sockscf.uid.unprivileged_gid)で落ちているようだったのでこのif文全体をコメントアウトしたら(この記事で扱う範囲では正しく)動くようになりました(その直後にあるよく似た式seteuid(sockscf.uid.unprivileged_uid)はそのままでも大丈夫だった)。 backtrace_symbols()が見つからないというエラーのときは、pkg search backtraceとかするとlibandroid-execinfoが出てくるのでそれを入れるとよいです(うまくいかなければmake distcleanして./configureからやり直すこと)。

dante・3proxyの設定

/etc/dante.confの内容(最低限の内容のみ載せています)は以下のような感じです。

internal: eno1 port=3129
external: eno1
internal: eno2 port=3129
external: eno2
external.rotation: same-same
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
    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)(DNSリクエストがSOCKSによってプロキシされるわけではない)。

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