入ってきたNICから戻らない。
listen 0.0.0.0
していると、入ってきたパケットが応答で出ていくとき、別の経路を通ってしまう。
TCPならコネクション状態でなんとかなるが、UDPなら如何ともしがたい。これを単純になんとかしたい。
今回はWireguardで別経路を作りたかったので、UDPなのでコネクション状態を識別するのが大変。
実験環境を作る
次のような実験環境を作る。
準備
## コンテナ作成 lxc launch ubuntu:22.04 t01 lxc config set t01 security.nesting true lxc restart t01 lxc shell t01 ## デバイス追加 ip link add name veth0 type veth peer name veth0-c01 ip netns add c01 ip link set veth0-c01 netns c01 ip netns exec c01 ip link set veth0-c01 name eth0 ip link set veth0 name eth1 ## アドレス追加 ip netns exec c01 ip addr add 10.2.0.2/24 dev eth0 ip addr add 10.2.0.1/24 dev eth1 ## リンクアップ ip netns exec c01 ip link set eth0 up ip link set eth1 up ## ルーティング ip netns exec c01 ip route add default via 10.2.0.1 dev eth0
2本目の経路を追加
## デバイス追加 ip link add name veth1 type veth peer name veth1-c01 ip link set veth1-c01 netns c01 ip netns exec c01 ip link set veth1-c01 name eth1 ip link set veth1 name eth2 ## アドレス追加 ip netns exec c01 ip addr add 10.2.0.4/24 dev eth1 ip addr add 10.2.0.3/24 dev eth2 ## リンクアップ ip netns exec c01 ip link set eth1 up ip link set eth2 up ## ルーティング ## 10.2.0.3宛は eth1 から出す。 ip netns exec c01 ip route del 10.2.0.0/24 dev eth0 ip netns exec c01 ip route del 10.2.0.0/24 dev eth1 ip netns exec c01 ip route add 10.2.0.3 dev eth1 src 10.2.0.4 metric 10 ip netns exec c01 ip route add 10.2.0.0/24 dev eth0 src 10.2.0.2 metric 100
経路は、デフォルト
ただ、ネットワークを繋いだだけなので、通信はできるが、経路がややこしい。
## ルーティング・テーブル default via 10.17.238.1 dev eth0 proto dhcp src 10.17.238.47 10.2.0.0/24 dev eth1 proto kernel scope link src 10.2.0.1 10.2.0.0/24 dev eth2 proto kernel scope link src 10.2.0.3
応答は、routing tableにより、eth1から出ていく
ncat をTCPでつなぐと、応答の受信でエラーになる。送信元からもて、宛先IP以外のIPから応答戻ってくるためで。要はFROMが不一致でコネクションを作れない。
Ncat: Connection refused.
UDPでもおなじになる。(UDPも宛先IPとポートでコネクションを張るのでちょっとね)
入った箇所から応答する( recent 活用)
上記のように、入ったインタフェースと出ていくインタフェースが異なるので対応が必要
通常であれば、ネットワーク・アドレスを使ってネットワークアドレスで変えればいいんだろうが、それって1対1対応ならいいけど、N対N対応にはならないし、「アドレス」であるので、「インタフェース」ではないのですよね。
そこで、xt_recentを使って、次のようなトリッキーなフィルタリングを考えてみる。
## 入ったIPをメモしておく。 iptables -A INPUT -p udp --dport 80 -m recent --name fromList --set ## OUTPUTでメモに一致したかを見る。 iptables -A OUTPUT -t mangle \ -m recent --name fromList \ --rcheck --rdest \ -j MARK --set-mark 0x3 ## 一致したパケットのルーティングを入れる。 ip rule add fwmark 0x3 lookup 300 ip route add 0.0.0.0/0 dev eth2 src 10.2.0.3 table 300
仕組み
入ってきた「パケット」の「IPアドレス」をメモする。このときに xt_recentに書き込んでいく。
出ていくとき、recent に出現したIPかどうかをマッチングする。マッチしたらマークをする
マークがついたパケットは ip rule によって経路を変更し出ていくインタフェースを切り替える。
ルーティング挿入後
ルーティングを入れたあと、マークとtableによって、パケが入ってきたインタフェースから応答するようになる。
もうちょっと詳細にマッチング
先程の例は、ざっくりしているので、もっと詳細にマッチ条件を書いてみたり。
## iptables recent の利用 ## ## さらに詳細にマッチ条件を書く ## conntrack でもっと詳細にマッチさせる ################ ## 開始時に記録 ## conntrack と nic を条件にする iptables -A INPUT -i eth2 \ -p udp --dport 80 \ -m conntrack --ctstate NEW \ -m recent --name fromList --set ## ############ ## 接続済みへの応答で記録する ## iptables -A OUTPUT -t mangle \ -p udp --sport 80 \ -m conntrack --ctstate RELATED,ESTABLISHED \ -m recent --name fromList \ --rcheck --rdest -j MARK --set-mark 0x3 ## UDPでも RELATEDは使える
記憶している限り経路が切り替わらない
この場合、別の経路を通してくれないので、不意の回線切断などでどちらかの経路だけを使いたいときに、いつまでの前の経路を選んでしまう。
そこで、タイムアウトや、別経路から来たら削除する
## タイムアウト設定 iptables -I INPUT -i eth2 -p udp --dport 80 -m conntrack --ctstate NEW -m recent --name fromList --set iptables -I INPUT -i eth2 -p udp --dport 80 -m conntrack --ctstate RELATED,ESTABLISHED -m recent --name fromList --second 5 --hitcount 1 --rcheck --reap -j ACCEPT ## 別経路を通ってきたら、削除する iptables -I INPUT -i eth1 -p udp --dport 80 -m conntrack --ctstate NEW -m recent --name fromList --delete
これで、recent 機能を使ってIPアドレスを記憶しておき、記憶したものによって経路を変えられることががわかる。
メモ recent テーブルに入ってるアドレスを確認する
iptables recent は xt_recent であり、それは proc ファイルシステムに記憶されている。
recent で記録されたものを見る。
## $name=fromList cat /proc/net/xt_recent/fromList
recent で保存されたテーブル(名前)を一覧する
cat /proc/net/xt_recent/ # 名前を省略すると default が使われている。
recent 追記
編集をして構わない。
echo xxx > /proc/net/xt_recent/fromList
編集をして構わない。これを使えば、「WEB」からポートを開放するためにIPアドレスを自己申告させるような社内PHPを作ることも可能だ。
recentの寿命
無制限に増えるのだが、カーネルモジュールなので、初期上限が設定されているのでメモリを食いつぶして崩壊するようなことはあまりない。が適宜削除するようにiptablesを記述することも大事。
まとめ
recent を動的なフィルタリングのテーブルに使うことで、送信元IPが入ってきたインタフェースへ、ローカルから応答できるソースIPルーティングが可能になった。
たしか業務用のアプライアンスを買わないと実現が面倒な機能だけど、ちゃんと考えればiptablesでも実現が可能だった。
他にも解決方法は数多あるだろうが、たとえば、metricの調整とかもっと間だと思う。
recentは身近でかつ構成が理解しやすいしキャッシュのみで解決する。今回はデフォルトGWを書かない縛りでルールを書いて遊んでるのでRecentを使っている。