それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

iptablesでソース ルーティング(入ってきたインタフェースから返す)

入ってきた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を使っている。