LinuxルーターのMAP-Eで良い感じにNATポートを使い回す方法

OpenWRTなどLinuxを使用してMAP-E(v6プラス、OCNバーチャルコネクトなど)接続を行う方法は様々なサイトで紹介されていますが、iptablesやnftables(バックで動いているのはnetfilter)のNATでは使用ポート範囲を複数(1000-2000, 3000-4000みたいな感じで)指定できないため、MAP-Eの利用可能ポートをうまく使い回すのが難しいという問題があります。具体的には、多くの接続を同時に行ってポートが不足気味のときに、実際には利用可能なポートが余っているにもかかわらずそれが割り当てられる対象になっていないため新規接続が失敗するという現象が発生する可能性があります。俗に言うニチバンベンチが成功しない状態です。

既存の手法はnthモードを使って接続ごとに順番にポートセットを切り替えるものとhmarkを使用してソースポートに応じてポートセットを変える(この場合ソースポートが同じなら同じポートセットが使われるのでcone NATになる)ものに大別されますが、いずれにしても全ポートを必ず使いきれるわけではありません。これらの設定方法についてはNanoPi NEO2をv6プラスのルーターにする 後編 - がとらぼCentOSでOCNバーチャルコネクト | QuintRokkフレッツ光クロス:MAP-E ROUTER by Debian Box (iptables)あたりをご覧ください。

この問題の解決策としては、既にブログでも書いたものが2つあります。1つは【Map-EでもNATタイプA】LinuxでポートセービングIPマスカレード付きの制限コーン風NAT(EIM/ADF)を動かす - turgenev’s blogのようにソフトウェア的に解決する方法で、これ自体はNAT動作を細かくカスタマイズすることもできて便利なのですが、今回の目的を達成する手段としては少し大袈裟です。Rust版を使うにしても、ユーザースペースで動作するのでパフォーマンス的に劣る可能性があります。

もう一つはLinuxで一つのパケットに2回(複数回)NATをかけるための2つの方法 - turgenev’s blogのようにnetfilterのNATを複数回行う方法です。しかしこれだとポートセットの個数(v6プラスなら15個、OCNバーチャルコネクトなら63個)分だけルールを書かなければいけないという問題があり、また1:1でのNATにもかかわらずconntrackを使用することになるのでパフォーマンス的にもやはり無駄があるように思えます。

そこで、この記事で紹介するのが、tcコマンドを使う方法です。tcを使うとnetfilterよりもさらに外側(PREROUTINGより前、POSTROUTINGより後)でパケットを処理することができ、またあまり知られていないと思うのですがpedit機能によりパケットの中身を書き換える(定数へのset、定数のaddなど)ことができます。conntrackのようなステートフルなNATはできませんが、逆にステートレスなNATであれば高速に行うことができます。

とはいえポートセットの個数分だけルールを書かなければいけないのは変わらないのでは?という気がしますが、ビット演算をうまく使うとlog(ポートセットの個数)個くらいのルール(つまりv6プラスなら4個、OCNバーチャルコネクトなら6個)で処理できます。

16進数表示と特に相性がいいv6プラスを使って説明します。

v6プラスでは、0xXXXXというポート番号のうち真ん中の2つの桁が固定(=ポートセットID、PSID)で、上一桁が1-F、下一桁が0-Fとなります(15x16=240ポート)。例えばポートセットIDが0x33であれば、

0x1330, 0x1331, 0x1332, ...., 0x133F

0x2330, 0x2331, 0x2332, ...., 0x233F

︙ .......

0xF330, 0xF331, 0xF332, ...., 0xF33F

の240個が利用可能ポートとなります。両端の桁(変化している桁)のみを見ると10..FFと連続する数値になっています。これをうまく利用します。

例えばiptablesで0x0010-0x00FFにSNAT(MASQUERADE)します。その結果、ポート番号が0x00PQになったとしたら、これが最終的に0xP33Qから出ていく(&戻ってくるときは逆向きに変換する)ようにすればいいということです。

下一桁はそのままなので簡単そうですが、問題は上一桁です。イメージとしてはPのところの4ビット分をそのままshiftさせられればいいのですが、見た感じtcのpeditにはshift演算は用意されていません。そこで、Pの中の各ビットごとに、そのビットが立っていたら対応するところにもビットを立てるというふうにします。

つまり、0x00PQが与えられたとして、

  • 0x0010のビットが立っていたら0x1000を立てる
  • 0x0020のビットが立っていたら0x2000を立てる
  • 0x0040のビットが立っていたら0x4000を立てる
  • 0x0080のビットが立っていたら0x8000を立てる

とすれば0xP0PQが得られます。あとは真ん中の2つの桁を33に変えれば0xP33Qになります。ちなみにもちろんPの中のビットを入れ替えるようなことをしても1:1で変換できるかと思いますが、ここに書いた方法が一番普通でしょう(大小関係を維持できる)。

ここではわかりやすくするため0x0010-0x00FFの範囲にしましたが実際にはウェルノウンポート番号を使うのはちょっと気が引けるので0x8010-0x80FF(32784-33023)を使うことにします。

では具体的なコマンドを見ていきます。ipは192.168.1.13、デバイス名はenp1s0としています(実際にMAP-Eで使う場合はip4ip6のトンネルデバイスの名前にする)。

まず外向きについてです。まず以下のようにiptablesで最下位ビット以外を揃えてmark(fwmark)を付けます。適当に0x54と0x55にしておきます。

sudo iptables -t mangle -A POSTROUTING -s 192.168.1.13 -p tcp -j MARK --set-mark 0x54
sudo iptables -t mangle -A POSTROUTING -s 192.168.1.13 -p udp -j MARK --set-mark 0x55

どうせtcが一番最後なのでマークを付けるタイミングはどこでもいいのですがせっかくなのでPOSTROUTINGのところで付けてみます。さらにここでNATも設定してしまいましょう。

sudo iptables -t nat -A POSTROUTING -s 192.168.1.13 -p tcp -j SNAT --to-source :32784-33023
sudo iptables -t nat -A POSTROUTING -s 192.168.1.13 -p udp -j SNAT --to-source :32784-33023

続けてtcのルールを設定します。まず以下のようなコマンドを打ちます(意味はあんまりよくわかっていません)。

tc qdisc replace dev enp1s0 root handle 1: htb

この1:に対してルールを追加していく感じです。以下のようにメインのポート計算のルールを入れます。ip ruleなどと同じく直近に追加したものの優先度が最も高くなるので、実際はここに書いてあるのと逆順にルールが適用されていくことに注意してください。

sudo tc filter add dev enp1s0 parent 1: handle 0x55 fw action csum ip4h udp continue
sudo tc filter add dev enp1s0 parent 1: handle 0x54 fw action csum ip4h tcp continue
sudo tc filter add dev enp1s0 parent 1: handle 0x54/0xfffffffe fw action pedit pedit munge ip sport set 0x0330 retain 0x0ff0 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0010 0x0010 action pedit pedit munge ip sport set 0x1000 retain 0x1000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0020 0x0020 action pedit pedit munge ip sport set 0x2000 retain 0x2000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0040 0x0040 action pedit pedit munge ip sport set 0x4000 retain 0x4000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0000 0x0080 action pedit pedit munge ip sport set 0x0000 retain 0x8000 continue

ポート変換の処理はビットマスクを0xfffffffeとしてTCP/UDPまとめて行って、最後のチェックサムの計算だけ別々にやっています(netfilterのNATでは勝手にやってくれるが、tcだと自分でやる必要がある)。

fwmarkへのマッチは一つ目のようにhandle 0x55 fwなどと書くのが短いのですが、4番目以降のように他のu32 matchと組み合わせるときはmark 0x55 0xffffffffなどとするしかない気がします。

ポートの計算は、特定のビットが立っているか見て対応するところにビットを立てるという先ほどの説明の通りです。retainを使うことで指定したビットのみ変更できます。0x8010-0x80FFを使うことにした関係で、8に関するルールは「0x80が立っていない場合に0x8000を消去する」ものにする必要があり、少し見た目が違います(違いがあるところを太字にしました)。あと、今回は使用する上位ビットと下位ビットの範囲が被らないので気にする必要はないのですが、一応上位ビットから先に処理していったほうが安心です。

最後にcontinueを付けているのは、これがないとルールを一個評価した時点で評価が終了してしまうからです。

なお、peditの加算はbyte-wiseのようです。つまり例えば0x3333に0xffffを加算すると0x3332になるのではなく0x3232になります。(今回は加算は使わないのでこれで困ることはありません。)

では次に内向きの設定です。こちらもまずparentとなるものを追加します。

tc qdisc add dev enp1s0 ingress handle ffff:

次にメインのルールです。

sudo tc filter add dev enp1s0 parent ffff: handle 0x65 fw action csum ip4h udp continue
sudo tc filter add dev enp1s0 parent ffff: handle 0x64 fw action csum ip4h tcp continue
sudo tc filter add dev enp1s0 parent ffff: handle 0x64/0xfffffffe fw action pedit pedit munge ip dport set 0x8000 retain 0xf000 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x8000 0x8000 action pedit pedit munge ip dport set 0x0080 retain 0x0080 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x4000 0x4000 action pedit pedit munge ip dport set 0x0040 retain 0x0040 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x2000 0x2000 action pedit pedit munge ip dport set 0x0020 retain 0x0020 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x1000 0x1000 action pedit pedit munge ip dport set 0x0010 retain 0x0010 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe action pedit pedit munge ip dport set 0 retain 0x0ff0 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match ip protocol 17 0xff match u16 0 1fff at 6 match ip dport 0x0330 0x0ff0 action skbedit mark 0x65 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match ip protocol 6 0xff match u16 0 1fff at 6 match ip dport 0x0330 0x0ff0 action skbedit mark 0x64 continue

まずTCPUDPパケットにfwmarkを付けます。PSIDの判定はなんとなく付けていますが実際のMAP-Eでは必ず成立する条件なので無くてもいいと思います。「0 1fff」のあたりはIP Fragmentationのためで、tc-u32(8) - Linux manual pageの最後の方を参考にしました。fwmarkの変更はskbeditというのを使うとできます。番号は適当に0x64と0x65にしておきました。次にPSIDの部分を0にします。それから今度は先ほどと逆に上位ビットの判定結果を下位ビットに反映していきます。下位ビットから処理していきます。最後に上一桁を0x8000に設定して完成です。

デバッグについては、tcよりも外側(最終的にLinuxから出入りするポート番号)を見たかったらtcpdump、tcとiptablesの間を見たかったらiptablesの-j LOGやconntrackを使うといいと思います。tcpdumpに-Xを付けるとパケットの中身が見れて-vvを付けるとチェックサムの正誤などが見れます。ちなみにv6プラスの割り当てポートはIPv6(MAP-E方式)使用可能ポート確認 | 監視・防犯カメラの設置なら「アイゼック」、OCNバーチャルコネクトはhttps://ipv4.web.fc2.com/map-e.htmlで計算できます。

tc filtlerのルールを一括で消すときはsudo tc filter del dev enp1s0sudo tc filter del dev enp1s0 parent ffff:でできます(つまりparent 1:は省略できるっぽい)。ip ruleと同じようにpref(prio)指定でのルールごとの追加・削除も可能です。

OCNバーチャルコネクトであれば、たとえばPSIDが0x23=0010 0111として同様に考えると、0x0010 - 0x03FF(0000 0000 0001 0000 - 0000 0011 1111 1111)にまずNATしてから、0000 0110 0111 0000 - 1111 1110 0111 1111 に変えればよくて、太字部分のPSIDが固定という感じになります。コマンドにしてみます。実際は0x8010 - 0x83FF (32784-33791)にしているのも同じです。一部さっきと同じところは省略します。

まずSNATです。

sudo iptables -t nat -A POSTROUTING -s 192.168.1.13 -p tcp -j SNAT --to-source :32784-33791
sudo iptables -t nat -A POSTROUTING -s 192.168.1.13 -p udp -j SNAT --to-source :32784-33791

次に外向きです。

sudo tc filter add dev enp1s0 parent 1: handle 0x55 fw action csum ip4h udp continue
sudo tc filter add dev enp1s0 parent 1: handle 0x54 fw action csum ip4h tcp continue
sudo tc filter add dev enp1s0 parent 1: handle 0x54/0xfffffffe fw action pedit pedit munge ip sport set 0x0230 retain 0x03f0
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0010 0x0010 action pedit pedit munge ip sport set 0x0400 retain 0x0400 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0020 0x0020 action pedit pedit munge ip sport set 0x0800 retain 0x0800 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0040 0x0040 action pedit pedit munge ip sport set 0x1000 retain 0x1000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0080 0x0080 action pedit pedit munge ip sport set 0x2000 retain 0x2000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0100 0x0100 action pedit pedit munge ip sport set 0x4000 retain 0x4000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x54 0xfffffffe match ip sport 0x0000 0x0200 action pedit pedit munge ip sport set 0x0000 retain 0x8000 continue

最後に内向きです。

sudo tc filter add dev enp1s0 parent ffff: handle 0x65 fw action csum ip4h udp continue
sudo tc filter add dev enp1s0 parent ffff: handle 0x64 fw action csum ip4h tcp continue
sudo tc filter add dev enp1s0 parent ffff: handle 0x64/0xfffffffe fw action pedit pedit munge ip dport set 0x8000 retain 0xfc00 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x8000 0x8000 action pedit pedit munge ip dport set 0x0200 retain 0x0200 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x4000 0x4000 action pedit pedit munge ip dport set 0x0100 retain 0x0100 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x2000 0x2000 action pedit pedit munge ip dport set 0x0080 retain 0x0080 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x1000 0x1000 action pedit pedit munge ip dport set 0x0040 retain 0x0040 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x0800 0x0800 action pedit pedit munge ip dport set 0x0020 retain 0x0020 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe match ip dport 0x0400 0x0400 action pedit pedit munge ip dport set 0x0010 retain 0x0010 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x64 0xfffffffe action pedit pedit munge ip dport set 0 retain 0x03f0 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match ip protocol 17 0xff match u16 0 1fff at 6 match ip dport 0x0230 0x03f0 action skbedit mark 0x65 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match ip protocol 6 0xff match u16 0 1fff at 6 match ip dport 0x0230 0x03f0 action skbedit mark 0x64 continue

ちなみにもちろん今回紹介したNATは1:1の静的NATでフィルタリングなどもないためフルコーンNATと同様であり、別のフルコーンNAT(前述の自分の記事やx64 Linux ルータのIPoE(map-e by iptables)環境でGame ConsoleをNAT越えさせる -- その1fullconenat module追加有りの場合に載っているGitHub - llccd/netfilter-full-cone-nat: A kernel module to turn MASQUERADE into full cone SNATなど)と組み合わせたら全体もフルコーンNATとして動作するはずです。基本的にはTCPは(NAT動作が重要ではないので)普通にiptablesのSNATでポートセービングさせておけばよく、UDPP2P通信用にフルコーンにして、ポートの消費が多いDNSは8.8.8.8などは使わずIPv6で問い合わせる(参考:ここの後半のほうとか)というのがおすすめです。もっと詳しいことが気になったら過去記事のNAT動作をめぐる誤解まとめとかNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUNも読んでもらえると嬉しいです。

元のiptablesのSNATで割り当てられるポートは(NAT先ポート範囲に入っていない限りは)予測不能なので、この方法で割り当てられるポートも予測不能です。Linuxのnetfilterのconnection trackingとNAT動作の仕組み - turgenev’s blogに書いたように規則的にポートが割り当てられるように見えるルーターもありますが、これらの実装方法は不明です(専用のハードウェアを使っている、あるいはカーネルに手を加えている?)。

ちなみに、手元では普通にHGWを使ってMAP-Eを使っているのでこの方法を実際に試せてはいないのですが、2つのLinux間でIPv4 over IPv6トンネリング - turgenev’s blogのように自前でトンネリングをしてみたところ、ip4ip6のtunデバイスに関しても問題なく動作しました。

以上です。細かい仕様はよくわかっていないのでもっと簡潔なやり方があるかもしれませんが、とりあえずこんな感じで動くはずです。わからなければコメントにどうぞ。

追記: ICMP関連

ICMPに関して何も対応していませんでした。MAP-Eでは、インターネットから IPoE MAP-E ルータにPingをする | KUSONEKOの見る世界RFC 7597(MAP-Eの仕様)にある通り、ICMP Echo Request/Replyのidentifierをポートと同様に変換する必要があり、またICMPの各種エラーメッセージ(おそらく主に使われるのはdestination unreachable、中でも特にICMP Echo Requestに対するhost unreachable・UDPに関するport unreachable・PMTUd用のneed fragmentあたり?)に含まれる元のIPヘッダのデータのポート番号・identifierも正しく書き換える必要があります。

TCP/UDPのポートと異なり、これらのフィールドに関してはtcにおいて専用の構文が用意されていないので、バイト数を指定する必要があります。またIPヘッダーのサイズは20-60バイトの間で可変であり、ヘッダ長さフィールドを見て長さを判断する必要があります。しかし以下の理由で、今回は20バイトに決め打ちして処理することにしました。

  • オプション付きの(20バイトより長い)IPヘッダはIPsecなど限られた用途でのみ使われ、通常のTCP/UDPなどで対応する必要は事実上ほぼないはず。
  • そもそも用途がトラブルシューティングであり、完全対応していないと困るというほどのものではない。
  • ICMPのエラーメッセージでは外側のIPヘッダとICMPのペイロード部分に含まれたIPヘッダの2箇所のサイズが可変であり、処理が複雑になる。
  • u32 matchにおいてオフセットを動的に指定するには、ハッシュテーブルにルールを追加した上でそれを呼び出すというような回りくどいコマンドが必要(参照: tc-u32(8) - Linux manual page)。とはいえ書いてあるだけマシで、この通りにすれば正しく動いた。
  • tc peditにおいてオフセットを動的に指定するための構文として、manにはat AT offmask MASK shift SHIFTと書いてあるが、実際にはat AT MASK SHIFTと書かないとパースが通らず、またそう書いたとしても正しく動いているように見えず(特にshiftの挙動が不可解なのと、showでルールを表示してもat以下に関する情報が表示されない)、全体としてあまりテストされていない雰囲気を感じた。

v6プラスのみ書きますが、OCNバーチャルコネクトでも同じようにできるはずです。ではまずICMP echoです。iptablesルールを追加します。

sudo iptables -t nat -A POSTROUTING -p icmp --icmp-type 8 -j SNAT -s 192.168.1.13 --to-source :32784-33023

次に外向きのtc filterを追加します。

sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x59 0xffffffff action pedit pedit munge offset 24 u16 set 0x0330 retain 0x0ff0 pipe action csum ip4h icmp continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x59 0xffffffff match u16 0x0010 0x0010 at 24 action pedit pedit munge offset 24 u16 set 0x1000 retain 0x1000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x59 0xffffffff match u16 0x0020 0x0020 at 24 action pedit pedit munge offset 24 u16 set 0x2000 retain 0x2000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x59 0xffffffff match u16 0x0040 0x0040 at 24 action pedit pedit munge offset 24 u16 set 0x4000 retain 0x4000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match mark 0x59 0xffffffff match u16 0x0000 0x0080 at 24 action pedit pedit munge offset 24 u16 set 0x0000 retain 0x8000 continue
sudo tc filter add dev enp1s0 parent 1: u32 match ip protocol 1 0xff match ip icmp_type 8 0xff match ip ihl 0x5 0xf match u16 0 1fff at 6 action skbedit mark 0x59 continue

今回はiptablesではなくtc filterでマークを付けています。ICMP(1)のecho request(8)のうちヘッダ長さが20(=4x5)で最初のfragmentであるものに0x59を付けます。そして24バイト(IPヘッダ20バイト+ICMPヘッダの先頭4バイト)以降に書かれているidentifierを書き換えていきます。u16を使ってマッチしていますが、別にip sportでもいいです。最後にchecksumを書き換えています。pipeを使うと1つのfilterに複数のactionを繋げられます。TCP/UDPのほうはchecksumを書き換えなくても通ることもありますがpingはそうはいかないので注意が必要です。

次に内向きのtc filterです。

sudo tc filter add dev enp1s0 parent ffff: handle 0x69 fw action pedit pedit munge offset 24 u16 set 0x8000 retain 0xf000 pipe action csum ip4h icmp continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x69 0xffffffff match u16 0x8000 0x8000 at 24 action pedit pedit munge offset 24 u16 set 0x0080 retain 0x0080 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x69 0xffffffff match u16 0x4000 0x4000 at 24 action pedit pedit munge offset 24 u16 set 0x0040 retain 0x0040 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x69 0xffffffff match u16 0x2000 0x2000 at 24 action pedit pedit munge offset 24 u16 set 0x0020 retain 0x0020 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x69 0xffffffff match u16 0x1000 0x1000 at 24 action pedit pedit munge offset 24 u16 set 0x0010 retain 0x0010 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match mark 0x69 0xffffffff action pedit pedit munge offset 24 u16 set 0 retain 0x0ff0 continue
sudo tc filter add dev enp1s0 parent ffff: u32 match ip protocol 1 0xff match ip icmp_type 0 0xff match ip ihl 0x5 0xf match u16 0 1fff at 6 match u16 0x0330 0x0ff0 at 24 action skbedit mark 0x69 continue

こちらもecho replyに関して似たような条件でマッチさせてidを書き換えています。

では、UDPのport unreachableもやってみます。こちらは内向きのtc filterだけ設定すればいいです。

sudo tc filter add dev ip46 parent ffff: handle 0x79 fw action pedit pedit munge offset 48 u16 set 0x8000 retain 0xf000 pipe action csum ip4h and icmp continue
sudo tc filter add dev ip46 parent ffff: u32 match mark 0x79 0xffffffff match ip dport 0x8000 0x8000 at 48 action pedit pedit munge offset 48 u16 set 0x0080 retain 0x0080 continue
sudo tc filter add dev ip46 parent ffff: u32 match mark 0x79 0xffffffff match ip dport 0x4000 0x4000 at 48 action pedit pedit munge offset 48 u16 set 0x0040 retain 0x0040 continue
sudo tc filter add dev ip46 parent ffff: u32 match mark 0x79 0xffffffff match ip dport 0x2000 0x2000 at 48 action pedit pedit munge offset 48 u16 set 0x0020 retain 0x0020 continue
sudo tc filter add dev ip46 parent ffff: u32 match mark 0x79 0xffffffff match ip dport 0x1000 0x1000 at 48 action pedit pedit munge offset 48 u16 set 0x0010 retain 0x0010 continue
sudo tc filter add dev ip46 parent ffff: handle 0x79 fw action pedit pedit munge offset 48 u16 set 0 retain 0x0ff0 continue
sudo tc filter add dev ip46 parent ffff: u32 match ip protocol 1 0xff match ip icmp_type 3 0xff match ip ihl 0x5 0xf match ip ihl 0x5 0xf at 28 match u16 0 1fff at 6 match ip sport 0x0330 0x0ff0 at 48 action skbedit mark 0x79 continue

このようにICMP(1)のdestination unreachable(3)のうち外側のIPヘッダと内側のIPヘッダ(オフセットは20+8=28バイト)がどちらも20バイトで、かつ最初のfragmentであるものにマッチさせています。そしてオフセット48(20+8+20)にある内部のポート番号を書き換えています。

nc -u xxx.xxx.xxx.xxx xxxx

などで閉じたポートにデータを送信してみて即座に終了するかどうか確認してみましょう。他のICMPエラーに関しても同様にできるはずです。