ipftrace2を用いてGTP-Uパケットの処理の流れを見る
はじめに
- free5GC(※)の UPF では gtp5g というカーネルモジュールを用いて、
GTP-U パケットのカプセル化、カプセル解除を行っています。
UPF で問題が起きた際にカーネル内部でのパケットの流れを追う必要が出るときがありますが、
それを効率的に行うツールとして、ipftrace2 というツールを試してみました。
(※) https://github.com/free5gc
- ipftrace2とは
下記で開発が行われており、eBPF(Extended Berkeley Packet Filter)等を用いることで、
Linuxカーネルのソースコードに変更を加えることなしに、
カーネル内部でパケットバッファーを処理している関数を追跡することができるツールです。
https://github.com/YutaroHayakawa/ipftrace2
インストールと基本的な使い方
- インストールは以下で行います。
curl -OL https://github.com/YutaroHayakawa/ipftrace2/releases/download/v0.5.1/ipftrace2_amd64.tar.gz tar xvf ipftrace2_amd64.tar.gz sudo cp ipft /usr/local/bin/ipft
- ipftrace2の利用方法として、まず、トレースしたいパケットにマークを付与するよう設定し(後述)、
その後、ipftrace2を起動し、トレース対象のパケットを流します。
-
ツールの起動は、下記のように実行します。
マーク番号はトレース対象のパケットに付与したマークの番号になります。
ipft -m <マーク番号> (その他のオプション)
-
コマンドを実行した直後は以下のように表示され、トレースの準備が行われます。
succeeded の後ろの数字が増加し、total の後ろの数字と同じになるまで待ちます。
Attaching program (total xxx, succeeded yyy, failed 0, filtered: 0)
- その後パケットを流します。パケットがトレースされた場合は以下のように表示されます。
Got xxx traces
- パケットを流した後は、ctrl-cで終了するとトレース結果が出力されます。
-
例えば、あるコンソールで下記を実行します。
sudo ipft -m 0x55555555
また別のコンソールで下記を実行します。
sudo ping -m 1431655765 -c 1 127.0.0.1
これにより、カーネル内でのpingパケットの流れがトレースできます。
Ubuntu 20.04.4 LTS の 5.4.0-105-generic でのトレース結果は下記の通りになります。
- pingのトレース結果
nishi@GTP-U:~$ sudo ipft -m 0x55555555 Attaching program (total 1084, succeeded 1084, failed 0, filtered: 0) Trace ready! Got 46 traces^C Timestamp CPU Function === 917884795104 001 ip_send_skb 917884814724 001 ip_local_out 917884819637 001 __ip_local_out 917884824472 001 nf_hook_slow 917884839236 001 ip_output 917884843650 001 nf_hook_slow 917884852284 001 ip_finish_output 917884857798 001 __cgroup_bpf_run_filter_skb 917884863155 001 __ip_finish_output 917884867649 001 ip_finish_output2 917884872609 001 dev_queue_xmit 917884877230 001 __dev_queue_xmit 917884881928 001 netdev_core_pick_tx 917884887504 001 validate_xmit_skb 917884891925 001 netif_skb_features 917884896551 001 skb_network_protocol 917884901215 001 validate_xmit_xfrm 917884906637 001 dev_hard_start_xmit 917884911809 001 loopback_xmit 917884917056 001 skb_clone_tx_timestamp 917884921474 001 sock_wfree 917884926532 001 eth_type_trans 917884931291 001 netif_rx 917884935715 001 netif_rx_internal 917884940179 001 enqueue_to_backlog 917884947487 001 __netif_receive_skb 917884952809 001 __netif_receive_skb_one_core 917884958668 001 ip_rcv 917884968577 001 nf_hook_slow 917884974476 001 ip_rcv_finish 917884979879 001 ip_local_deliver 917884983819 001 nf_hook_slow 917884989853 001 ip_local_deliver_finish 917884994918 001 ip_protocol_deliver_rcu 917884999447 001 raw_local_deliver 917885004463 001 icmp_rcv 917885011009 001 __skb_checksum_complete 917885016734 001 icmp_echo ←← ICMP echoの処理 917885022206 001 __ip_options_echo 917885028023 001 fib_compute_spec_dst 917885035105 001 security_skb_classify_flow 917885059864 001 consume_skb ←← 応答パケットは新たにバッファを確保するので受信したパケット自体は開放される 917885065807 001 skb_release_all 917885069796 001 skb_release_head_state 917885074072 001 skb_release_data 917885078414 001 skb_free_head 917885082819 001 kfree_skbmem nishi@GTP-U:~$
- ソケットからpingパケットが出力されloopbackインターフェイスを通り、IPの受信処理からICMP echoの処理にいたるところでまでトレースができています。
ICMPレイヤーでトレースが途切れる原因ですが、ICMP echo replyの送信時は新たに確保したパケットバッファーを用いるために、マークが初期化されるためであります。
パケットのマークについて
- パケットのマークは以下のような方法で設定できます。
- ソケットから送信する際に SO_MARK オプションを設定することで指定できます。
例えば、pingコマンドの場合は上の例にあるように -mオプション で指定可能ですが、10進数でしか指定できません。既存のソフトウェアすべてに SO_MARK を設定する機能があるわけではないので、柔軟性には欠けますが、他の方法に比べて早いタイミングから出力パケットをトレース可能です。
- iptablesを使用する場合はアクションとして “-j MARK –set-mark <マーク番号>” を使用するとフィルターにマッチしたパケットに対しマークを付与可能です。
- これはソフトウェアによらず使用できることや柔軟かつ分かりやすくトレース条件を設定できる利点はありますが、他の方法に比べて遅いタイミングでのマーク付加になるためにトレースできない部分が多くなります。
- さらに受信パケットに対しては tcコマンド のアクションで “skbedit mark <マーク番号>” を使用することで設定可能です。受信の場合こちらの方が iptables よりも早いタイミングで設定可能になります。
- なお、このパケットのマークは netns(network namespace) を超える際にはクリアされるので、netns を超えるトレースの場合は境界のインターフェイスの受信側で再度マークを付与する必要があります。
環境整備
- gtp5g の GTP-Uのカプセル化動作をトレースすることを試みます。
- OSは Ubuntu 20.04 を用います。
- gtp5g のようなカーネルモジュールをトレースする場合、
カーネルオプション CONFIG_DEBUG_INFO_BTF_MODULE を設定し、
BTF(BPF Type Format)形式のデバッグ情報をカーネルモジュールに埋め込む必要がありますが、
Ubuntu 20.04 の標準カーネルには設定されていないので、
このオプションが設定されている Hardware Enablement Stack (HWE) 版のカーネルを使用します。
-
まず、下記のように、HWE版カーネルをインストールします。
インストール後再起動し、HWE版のカーネルを起動してください。
apt -y install linux-generic-hwe-20.04
- さらにカーネルモジュールのビルド時に対象のカーネルのvmlinuxファイルにアクセスする必要があるために、以下のような操作で利用可能にする必要があります。
apt -y install ubuntu-dbgsym-keyring lsb-release echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | ¥ sudo tee -a /etc/apt/sources.list.d/ddebs.list sudo apt -y update sudo apt -y install linux-image-$(uname -r)-dbgsym sudo ln -s /usr/lib/debug/boot/vmlinux-$(uname -r) /usr/src/linux-headers-$(uname -r)/vmlinux
gtp5gとツールのビルド
-
以下の手順で gtp5g をインストールします。
ここでインストールしている dwarves は BTF の作成に必要となるツールです。
sudo apt -y install git build-essential dwarves cd git clone -b v0.5.2 https://github.com/free5gc/gtp5g.git cd gtp5g make sudo make install
-
さらに gtp5g にトンネルを作らせるためのツールを得るために、以下の操作で free5GC の UPF をビルドします。 先に go言語 のコンパイラを利用可能にしてください。
sudo apt -y install git build-essential cmake autoconf automake libtool pkg-config libmnl-dev libyaml-dev cd git clone --recursive -b v3.0.7 https://github.com/free5gc/free5gc.git cd free5gc make upf
- ビルド後 ./NFs/upf/updk/src/third_party/libgtp5gnl/tools に gtp5g の制御コマンド gtp5g-link と gtp5g-tunnel が作成されるので、PATH を通します。
- Bシェル系の場合
PATH=$PATH:`/bin/pwd`/NFs/upf/updk/src/third_party/libgtp5gnl/tools ; export PATH
- Cシェル系の場合
setenv PATH ${PATH}:`/bin/pwd`/NFs/upf/updk/src/third_party/libgtp5gnl/tools
- Bシェル系の場合
評価構成
- GTP-Uの動作を見るために、 Linux network namespace を3個作成しています。
- nsUPF で gtp5g を動作させ、 nsDN から nsgNB に向かうパケットは GTP-U でカプセル化され、
反対向きのパケットはカプセル解除されます。
- tcコマンド により veth1 、および、veth2 に入力されるパケットにマークを付けることで gtp5g でのパケットのカプセル化、および、カプセル解除の処理をトレースします。
評価手順
① Network namespace を作成し、相互で通信を行うための仮想ネットワークの設定を行います。
sudo ip netns add nsgNB sudo ip netns add nsUPF sudo ip netns add nsDN sudo ip link add veth0 type veth peer name veth1 sudo ip link add veth2 type veth peer name veth3 sudo ip link set veth0 netns nsgNB sudo ip link set veth1 netns nsUPF sudo ip link set veth2 netns nsUPF sudo ip link set veth3 netns nsDN sudo ip netns exec nsgNB ip link set veth0 up sudo ip netns exec nsUPF ip link set veth1 up sudo ip netns exec nsUPF ip link set veth2 up sudo ip netns exec nsDN ip link set veth3 up sudo ip netns exec nsgNB ip addr add 20.0.0.1/24 dev veth0 sudo ip netns exec nsUPF ip addr add 20.0.0.2/24 dev veth1 sudo ip netns exec nsUPF ip addr add 20.0.1.1/24 dev veth2 sudo ip netns exec nsDN ip addr add 20.0.1.2/24 dev veth3
② gtp5g のドライバーを起動します。
このコマンドはgtp5gを使用している間起動させ続ける必要があります。
sudo ip netns exec nsUPF `which gtp5g-link` add gtp5gtest
③ 次に、gtp5g ドライバーでのカプセル化のための設定を行います。
sudo ip netns exec nsUPF ip route add 60.0.0.0/24 dev gtp5gtest sudo ip netns exec nsDN ip route add default via 20.0.1.1 sudo ip netns exec nsUPF `which gtp5g-tunnel` add far gtp5gtest 1 --action 2 sudo ip netns exec nsUPF `which gtp5g-tunnel` add far gtp5gtest 2 --action 2 --hdr-creation 0 78 20.0.0.1 2152 sudo ip netns exec nsUPF `which gtp5g-tunnel` add pdr gtp5gtest 1 --pcd 1 --hdr-rm 0 --ue-ipv4 60.0.0.1 --f-teid 87 20.0.0.1 --far-id 1 sudo ip netns exec nsUPF `which gtp5g-tunnel` add pdr gtp5gtest 2 --pcd 2 --ue-ipv4 60.0.0.1 --far-id 2
④ tcコマンド によるパケットのマーキング設定を行います。
veth1から入るパケットがマーク番号1、veth2から入るパケットがマーク番号2になります。
sudo ip netns exec nsUPF tc qdisc add dev veth1 ingress sudo ip netns exec nsUPF tc filter add dev veth1 parent ffff: protocol ip matchall action skbedit mark 1 Sudo ip netns exec nsUPF tc qdisc add dev veth2 ingress Sudo ip netns exec nsUPF tc filter add dev veth2 parent ffff: protocol ip matchall action skbedit mark 2
カプセル化のトレース
-
veth2 → veth1 の流れになるので、下記でトレースします。
sudo ipft -m 2
-
パケットの印加は、下記で行います。
sudo ip netns exec nsDN ping -c 1 60.0.0.1
カプセル化のトレース結果
nishi@GTP-U:~$ sudo ipft -m 2 Attaching program (total 1524, succeeded 1524, failed 0, filtered: 0) Trace ready! Got 29 traces^C Timestamp CPU Function === 8908654355294 002 ip_rcv ← IP受信 8908654429834 002 sock_wfree 8908654435720 002 ip_rcv_finish 8908654440743 002 ip_route_input_noref 8908654449048 002 ip_forward ← IP中継 8908654454330 002 pskb_expand_head 8908654458994 002 skb_free_head 8908654463408 002 skb_headers_offset_update 8908654468186 002 ip_forward_finish 8908654472224 002 ip_output ← IP出力 8908654476202 002 nf_hook_slow 8908654481407 002 apparmor_ipv4_postroute 8908654495598 002 ip_finish_output 8908654500514 002 __ip_finish_output 8908654504460 002 ip_finish_output2 8908654511037 002 neigh_direct_output 8908654515006 002 dev_queue_xmit 8908654519246 002 __dev_queue_xmit 8908654523832 002 netdev_core_pick_tx 8908654528214 002 validate_xmit_skb 8908654532710 002 netif_skb_features 8908654536947 002 skb_network_protocol 8908654541302 002 validate_xmit_xfrm 8908654545619 002 dev_hard_start_xmit 8908654550569 002 gtp5g_dev_xmit ← gtp5gのカプセル化時の入り口 8908654557844 002 gtp5g_fwd_skb_ipv4 8908654565984 002 ip_rt_update_pmtu 8908654641009 002 skb_push ← gtpヘッダ付与のために先頭に領域を確保 8908654649043 002 udp_set_csum ← gtpヘッダ外側のUDPチェックサムの処理、本来はこの手前にudp_tunnel_xmit_skbの呼び出しがあるはずだがトレースされない 8908654654509 002 skb_scrub_packet ← トンネリング処理用にパケットバッファに付属するフラグ類のリセットを行う関数、ここでマークも削除されているのでこの後はトレースされなくなる ^Cnishi@GTP-U:~$
カプセル化のトレース結果について
- IP の入力、中継、出力と転送されて、gtp5g にパケットが流れているが、その後の処理がトレースされていない。これについては gtp5g でのカプセル化の最終段階で、 Linux カーネル側の UDP トンネル処理の共通関数( udp_tunnel_xmit_skb )にパケットを引き渡すが、その際に network namespace 越えであるというフラグを不必要に追加しているために UDP ヘッダーを付与するところでマークがクリアされるためと考えられる。
- なお、この共通関数 udp_tunnel_xmit_skb がトレースに出てこない理由は現在不明。別途カーネルバージョン 5.15.31 をソースから独自にビルドしたものでも試したが、うまくいかなかった。
カプセル解除のトレース
-
veth1 → veth2 の流れになるので、下記でトレースします。
sudo ipft -m 1
-
パケットの印加は nsgNB でパケットジェネレータの OSS である scapy を起動し、
下記で nsDN宛 の GTP-U カプセル化された pingパケット を投げることで行います。
load_contrib("gtp") p=IP(src="20.0.0.1",dst="20.0.0.2")/UDP()/GTP_U_Header(teid=87)/IP(src="60.0.0.1",dst="20.0.1.2")/ICMP() send(p)
カプセル解除のトレース結果
nishi@GTP-U:~$ sudo ipft -m 1 Attaching program (total 1524, succeeded 1524, failed 0, filtered: 0) Trace ready! Got 70 traces^C Timestamp CPU Function === 11393770779522 000 ip_rcv 11393770809123 000 sock_wfree 11393770815598 000 ip_rcv_finish 11393770823002 000 udp_v4_early_demux 11393770828046 000 ip_route_input_noref 11393770835330 000 ip_local_deliver 11393770840308 000 ip_local_deliver_finish 11393770844814 000 ip_protocol_deliver_rcu 11393770852480 000 raw_local_deliver 11393770858333 000 udp_rcv 11393770866028 000 __udp4_lib_rcv 11393770871785 000 __skb_checksum_complete 11393770880721 000 udp_queue_rcv_skb 11393770885684 000 udp_queue_rcv_one_skb 11393770890728 000 gtp5g_encap_recv 11393770899797 000 __iptunnel_pull_header 11393770904695 000 skb_pull_rcsum 11393770909493 000 skb_scrub_packet 11393770914187 000 netif_rx 11393770918610 000 netif_rx_internal 11393770922698 000 enqueue_to_backlog 11393770928006 000 __netif_receive_skb 11393770932245 000 __netif_receive_skb_one_core 11393770936609 000 ip_rcv 11393770940696 000 ip_rcv_finish 11393770945351 000 ip_route_input_noref 11393770954027 000 ip_forward 11393770961066 000 ip_forward_finish 11393770965813 000 ip_output 11393770971701 000 nf_hook_slow 11393770979457 000 apparmor_ipv4_postroute 11393770984023 000 ip_finish_output 11393770988796 000 __ip_finish_output 11393770993629 000 ip_finish_output2 11393771000657 000 neigh_resolve_output 11393771004982 000 __neigh_event_send 11393771011167 000 skb_push 11393771015599 000 dev_queue_xmit 11393771019863 000 __dev_queue_xmit 11393771024020 000 netdev_core_pick_tx 11393771029082 000 validate_xmit_skb 11393771033590 000 netif_skb_features 11393771037874 000 passthru_features_check 11393771041952 000 skb_network_protocol 11393771046398 000 validate_xmit_xfrm 11393771050883 000 dev_hard_start_xmit 11393771055046 000 veth_xmit 11393771060350 000 skb_clone_tx_timestamp 11393771064407 000 __dev_forward_skb 11393771068472 000 __dev_forward_skb2 11393771072722 000 skb_scrub_packet === 11393771259174 000 ip_rcv 11393771266948 000 sock_wfree 11393771271190 000 ip_rcv_finish 11393771275526 000 ip_route_input_noref 11393771280694 000 ip_local_deliver 11393771285051 000 ip_local_deliver_finish 11393771289561 000 ip_protocol_deliver_rcu 11393771294154 000 raw_local_deliver 11393771298402 000 icmp_rcv 11393771303251 000 __skb_checksum_complete 11393771309604 000 icmp_unreach 11393771314954 000 icmp_socket_deliver 11393771319409 000 raw_icmp_error 11393771325319 000 udp_err 11393771330351 000 __udp4_lib_err 11393771335455 000 consume_skb 11393771339536 000 skb_release_all 11393771343552 000 skb_release_head_state 11393771347689 000 skb_release_data 11393771351862 000 kfree_skbmem nishi@GTP-U:~$
カプセル解除のトレース結果について
- 結果に 「===」 が含まれるが、これは 2個 のパケットがトレースされたことを示している。
前者は目的の GTP-U カプセル解除処理であり、後者は試験パケットが nsDN 宛 の ping であるために、その返答が GTP-U カプセル化されたものの nsgNB 側にそれを受け取るものがないために ICMP error が返っているためである。
- こちらは outer IP の受信処理から gtp5g でのカプセル解除ののち IP forward され、 veth2 へ出力されるところまでトレースができている。
まとめ
- ipftrace2 というツールを用いて Linuxカーネル内部 でのパケット処理の流れを追跡した。
- 5GC で使用される GTP-U に対応したカーネルモジュールである gtp5g を ipftrace2 に対応した形でビルドし、それを用いて、 GTP-U のカプセル化、カプセル化解除の動きを追跡した。