APRESIA Technical Blog

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

評価構成

  • 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 のカプセル化、カプセル化解除の動きを追跡した。