VTYSHでstaticルートを設定した時の動き
VTYSHでstaticルートを設定した時の動き
VyOSを支えるFRRoutingの動きを調べました。 動きを追うためにスタティックルートを VTYSH で追加しました。 そのためデーモン固有の話は staticd で確認しています。
- プロセス間通信
- プロセス内部構造: イベントループ
- ルーティング設定のデバイスモデルに関わるコード
- RIB/FIB
- VTYSHのREPL
プロセス間通信
アーキテクチャ
まず公式からとってきた図がこれです。ZebraがDプレーンとのやりとりを引き受けています。
+----+ +----+ +-----+ +----+ +----+ +----+ +-----+
|bgpd| |ripd| |ospfd| |ldpd| |pbrd| |pimd| |.....|
+----+ +----+ +-----+ +----+ +----+ +----+ +-----+
| | | | | | |
+----v-------v--------v-------v-------v-------v--------v
| |
| Zebra |
| |
+------------------------------------------------------+
| | |
| | |
+------v------+ +---------v--------+ +------v------+
| | | | | |
| *NIX Kernel | | Remote dataplane | | ........... |
| | | | | |
+-------------+ +------------------+ +-------------+
ref. FRRouting.overview
デーモンのインターフェース
プロセス間通信はUnixドメインソケットを使っています。 このソケットのファイルパスはFRRoutingのビルド時に決定されます。(自分の環境では /var/opt/frr でビルドされました) そのためビルド時のオプションに差があるとデーモン間で通信ができません。 例えば aptで入れたstaticdとソースからビルドしたvtyshは話せません。設定オプションもありません。 (もちろん socat などでプロキシすることはできます。)
UNIXドメインソケットのパス探し
どこでUNIXドメインソケットのパスが決定されるか探しました。
VTYSHはvtysh_connect(struct vtysh_client *vclient)
関数で接続しています。
この関数は ./vtysh/vtysh.c で定義されています。
ここを読むとUNIXドメインソケットのパスは {{ vtydir }}/{{ vclient->name }}.vty とわかります。
この擬似テンプレート表記内の vclient->name
はデーモンの名前が入っています。たとえば、zebra, staticd
が代入されます。
定義されているのは同じファイル(vtysh/vtysh.c)の中です。
次に vtydir
の定義箇所についてです。こちらは vtysh/vtysh_main.c のl.378-379で代入されます。
書かれている通りfrr_vtydir
の値が使われています。
|
|
この DAEMON_VTY_DIR
はここで決定されて代入されています。
実験:メッセージの確認
VTYSHはデーモンへNUL終端文字列を投げているらしいです。 確認します。まず事前準備として socat でプロキシを挟むようにしました。
|
|
別のターミナルでVTYSHで設定します。
|
|
こんな感じでコマンドがそのまま流れています。またMarkerとStatusCodeを合わせた4バイトの0x00
も確認できました。
|
|
プロセス内部構造: イベントループ
プロセスアーキテクチャの章に詳しいです。
イベントループを中心としたアーキテクチャになっています。 詳細はドキュメントを読みましょう。 他のライブラリやフレームワークでイベント・タスクと呼ぶものをFRRoutingではスレッドと呼びます。 そして、OSが提供するスレッドはpthreads経由で利用しているため、こちらをpthreadと呼んでいます。
イベントループで各ステップが別れて処理されるためスタックトレースをしても互いの繋がりが読みにくかったです。 このイベントループはプロトコルデーモンの中で複数動いています。 互いにスケジューラ経由でイベントを登録できます。またメインスレッドへのリンクを持っています。ErlangのActorモデルに似てる印象を受けました。
そして、イベントループが1つのpthreadに対応するわけではありません。
イベントには種類があり、イベント登録する際に種類を明示します。その際、ブロックされるイベントではpthreadが確保されていました。
なので単一のイベントループから複数のaccept()
のようなブロックを伴う処理を並列に走らせることができます。
たとえばvty_event(VTY_READ, vty)
を呼び出すと内部でthread_add_read(...)
が呼ばれています。
|
|
プリプロセッサで下のように _thread_add_read_write()
に展開されています。
|
|
_thread_add_read_write()
でpthreadが確保されています。
ブロックされるスレッドはイベント登録時にイベントタイプを明示することで専用スレッドを確保してるように読めました。
|
|
ルーティング設定のデバイスモデルに関わるコード
FRRoutingでネットワーク設定がどう実装されているか確認します。ファイルはこのように分類できます。
矢印でモジュール間の依存を表しています。 特に staticd/static_nb_config は lib/northbound 内の関数を利用しつつコールバックも実装しています。
カテゴリ | ファイル名 | 説明 |
---|---|---|
model | lib/northbound_cli.* | commit/rollbackなど高度なtxを提供するマネジメントAPI |
model | lib/northbound.* | YANGモデルと実際の処理を紐づける枠組みを提供する |
model | lib/yang.* | 複数のYANGモジュールの管理とYANGのノード操作を提供する |
model | lib/openbsd-tree.* | 赤黒木 |
operation | staticd/static_nb_config.* | lib/northbound.* にコールバックとして登録されるサブルーチン |
operation | staticd/static_*.* | staticd固有のエンティティ |
operation | staticd/static_zebra.* | Zebraのクライアントのstaticd固有ラッパー |
operation | lib/zclient.* | Zebraのクライアント |
VTYSH向けのAPI(northbound)でデバイスモデル(YANG)と処理(コールバック)を対応させていて、内部で使う探索木として赤黒木を使っている。という感じです。
YANGによるオブジェクトモデルの定義
YANG(RFC7950)はネットワーク機器を管理するためのリソースモデルです。 FRRoutingでは内部でYANG対応を進めているようです。YANGはデバイスモデルのスキーマを定義します。FRRoutingでの定義は ./yang/ 配下に置かれています。 YANGは関連する定義をmoduleとしてまとめているようで 実際に各ファイルがモジュールに対応します。この定義ファイルからPython使ってcコードが生成されています。
|
|
YANGオブジェクトモデルの実装
YANGのノード操作にlibyangを使っています。 このライブラリではスキーマノードやデータノードをXPathから特定したりバリデーションを行う機能が提供されています。 これに加えて複数のYANGモジュールを管理する機能を提供するAPIが*lib/*yang**にまとめられています。
設定変更はデータノード操作に紐づいたコールバックとして実装されています。コールバック機構によってデータノードへの操作を実際に処理しています。 このコールバック機構と低レベルなイベント・命令の実装は lib/northbound.c に集まっています。 これらを組み合わせてコミットやロールバック、ペンディングなど高度な仕組みを提供しているのが lib/northbound_cli.c です。
モジュールやコールバックは、コミット前のコールバックのキューなどは赤黒木で管理されています。
モジュールの読み込みは下の関数で行います。ここで RB_INSERT()
は赤黒木へ要素を挿入するマクロです。
|
|
赤黒木
モジュールやコールバックの管理に赤黒木を使っています。赤黒木は lib/openbsd-tree.c,h で定義されています。 マクロを使って複数の型に赤黒木を提供します。
それぞれの型に対応した関数とデータ構造と及びそのインスタンスを作成するためRB_PROTOTYPE(),RB_HEAD()
マクロが定義されてます。
YANGモジュールを赤黒木で挿入するにはRB_INSERT(yang_modules, )
を用い探索にRB_FIND(type, ...)
を使います。
挿入を基本操作として赤黒木が実装されています。ノードの削除は実装されていません。rbe_rotate_left(), rbe_rotate_right()
などいつもの内部操作が実装されてます。
|
|
YANG関連では lib/yang.h で下のように呼ばれています。
|
|
northbound
クライアント(VTYSH)向けインタフェースをnorthboundインタフェースと呼びます。YANGを利用して実装されます。 YANGは単にデバイスモデルを定義するだけですが lib/northbound.c でコールバック機構を提供しています。 たとえば staticd ではstaticd/staticd_nb.c のなかでYANGのデータノードへの変更にコールバックを設定しています。
|
|
このようにコールバックなどとの対応づけを定義したfrr_yang_module_info
構造体のインスタンスは、
プログラム開始時に lib/northbound.c のnb_init()
によって利用されます。
登録されたコールバックはnb_transaction_process()
関数経由で呼び出されます。
下のコード内でtransaction->changes
がコールバックのリストです。nb_callback_configuration()
でevent
が少なくともNB_EV_APPLY
の時はコールバックが実行されています。
|
|
ref. lib/northbound.c#L1462-L1474
このnb_transaction_process()
がstaticdでどのように呼ばれるか確認します。
&transaction->changes
に代入しているのはnb_transaction_new()
でnb_candidate_commit_prepare()
で呼ばれます。
|
|
これらの関数がlib/northand_cli.cで利用されています。
|
|
staticdのコマンドはまず nb_cli_enqueue_change() が呼ばれ&vty->cfg_changes
を更新します。
そしてnb_cli_apply_changes()
でnb_cli_apply_changes_internal()
からnb_cli_classic_commit()
が呼ばれます。
|
|
ref. lib/northbound_cli.c#L135-L152
デーモンプロセス初期化での挙動
YANGのコールバック機構の初期化がプロセス起動時にどこから実行されるか確認します。
場所は lib/libfrr.c です。
プロセス初期化はファイル中のfrr_init()
です。northboundの初期化はnb_init()
関数にまとまっています。
呼び出しは下のようになっていて di->yang_modules
という部分が読み込まれるモジュールになっています。
|
|
nb_init()へのパラーメータ渡し: staticd
実際に staticd でどのように初期化するかを追ってみます。staticd/staticd_main.c の中は下のようになります。
|
|
このfrr_preinit()
が&static_di
をlib/libfrr.c内のdi
に代入します。
static_di
はFRR_DAEMON_INFO(static, ...)
で代入されます。
このマクロは lib/libfrr.h で定義され下のようになっています。
特に __VA_ARGS__
の箇所を経由して.yang_modules
のフィールドにstaticd_yang_modules
が入ります。
|
|
というわけでstaticd_yang_modules
を確認すればXPAATHに対応したコールバックを知ることができます。
たとえば …:staticd/route-list/src-list/path-list/frr-nexthops/nexthop に関するコールバックは下のように定義されてます。
|
|
RIB/FIB
ZebraはRIB(Routing Information Base)とFIB(Forwarding Information Base)の管理をしています。 RIBへの変更を受け付け必要であればFIBの変更を行います。Zebra内のRIB/FIBの更新操作についてはYANGはほとんど出てきません。
コマンドディスパッチ
プロトコルデーモンからのコマンドは zebra/zapi_msg.c の(*const zserv_handlers[])(ZAPI_HANDLER_ARGS
を使ってディスパッチされます。
|
|
RIBへのエントリー登録
ルート追加のコマンドはzread_route_add()
関数に対応している。
このなかで RIB のエントリ(struct route_entry
)を組み立てて rib_add_multipath_nhe()
を呼び出している。
rib_add_multipath_nhe()
の中でネクストホップを解決してFIB更新を行う。
|
|
RIB/FIB
FRRoutingでは VRF(Virtual Routing and Forwarding) をサポートしている。
VRFは ./zebra/zebra_vrf.h で定義されたstruct zebra_vrf
のオブジェクトに対応している。
この構造体は用途別に route_table
を複数保持していた。他にもプロトコルごとにhash
で関連情報を保持してもいる。
|
|
このroute_table
が RIB に対応していて ./lib/table.hで定義されています。
この辺りの構造体をまず眺めてみます。関連する型の参照関係です。
オブジェクト | 大雑把な説明 |
---|---|
route_table | RIBでtopのroot_nodeを保持する |
route_node | ルートノード: ルートのprefixを保持した二分木を形成する, route_node, rnhへのリストを持つ,選択中のroute_entryへのリンクも持つ |
rib_dest_t | ルートの宛先を管理する、ルートノードに1:1対応, rnh, route_entryのリストを持つ |
rnh | nexthopを表す, 関連するroute_node, route_entryへのリンクを持つ |
route_entry | 登録されているルート情報を表す, nhg_hash_entry, next_hopgroupを持つ |
nexthop_group | ECMPに対応するように複数のnexthopをリスト形式で持つ |
ngh_hash_entry | nexthopに関わる依存してるconnectedへのツリーを持つ, nexthop_groupへのリンクも持つ |
route_table
はメタ情報付きの route_node
の二分木です。
|
|
struct route_node
は下のように定義されていて、ルートはプレフィックスを持っています。またvoid *info
が宛先を持ちます。
|
|
このinfo
フィールドはこのプレフィックスの向き先情報になります。
取り出すインライン関数がzebra/rib.hで次のように定義されてます。
|
|
rib_dest_t
はroute_entry
の連結リストとnht
の連結リストを持ち採用されたselected_fib
などのメタ情報を含んでいます。
|
|
rib_dest_t
はstruct rnh_list_head nht
フィールドを持っている。
これは親になるroute_node
で採用されているselected: route_entryに紐づくnexthop_group
のエントリのnexthop
を解決したものになる。
確認したところzebra_rnh_evaluate_entry()
の呼び出しを契機にnexthopの解決が行われてnht
に記録されていました。
ref. zebra/zebra_rnh.c#L730-L739
prefixへルートエントリを登録する
rib_add_multipath_nhe()
からzebra_rnh_evaluate_entry()
やsendmsg()
の呼び出しを探します。
VRFやテーブルを特定したり、ネクストホップを解決しrib_addnode()
経由でrib_link()
を呼びます。
|
|
rib_link()
は対象のルートノードに新しくルートエントリーを追加して、rib_queue_add(route_node)
をしてます。
|
|
これにより別スレッドとしてrib_process()
が呼び出されて処理が進みます。
* |-> rib_link or unset ROUTE_ENTRY_REMOVE |->Update kernel with
* |-------->| | best RE, if required
* | |
* static_install->|->rib_addqueue...... -> rib_process
* | |
* |-------->| |-> rib_unlink
* |-> set ROUTE_ENTRY_REMOVE |
* rib_delnode (RE freed)
ref. zebra/zebra_rib.c#L3111-L3118
その様子をgdbで確認しました。まだRIBの処理です。
Thread 1 "zebra" hit Breakpoint 1, __libc_write (fd=6, buf=buf@entry=0x7fff6cbbd420, nbytes=nbytes@entry=1) at ../sysdeps/unix/sysv/linux/write.c:26
26 in ../sysdeps/unix/sysv/linux/write.c
(gdb) bt
#0 __libc_write (fd=6, buf=buf@entry=0x7fff6cbbd420, nbytes=nbytes@entry=1) at ../sysdeps/unix/sysv/linux/write.c:26
#1 0x00007ff5932fc3c9 in _thread_add_timer_timeval (xref=xref@entry=0x7ff5933b0060 <_xref.10>, m=0x56222c859770, func=func@entry=0x7ff593308410 <work_queue_run>, arg=arg@entry=0x56222ca3f040,
time_relative=time_relative@entry=0x7fff6cbbd470, t_ptr=t_ptr@entry=0x56222ca3f048) at lib/thread.c:1090
#2 0x00007ff5932fd6d3 in _thread_add_timer_msec (xref=xref@entry=0x7ff5933b0060 <_xref.10>, m=<optimized out>, func=func@entry=0x7ff593308410 <work_queue_run>, arg=arg@entry=0x56222ca3f040,
timer=<optimized out>, t_ptr=t_ptr@entry=0x56222ca3f048) at lib/thread.c:1129
#3 0x00007ff5933082e6 in work_queue_schedule (delay=<optimized out>, wq=0x56222ca3f040) at lib/workqueue.c:139
#4 wo rk_queue_schedule (wq=0x56222ca3f040, delay=<optimized out>) at lib/workqueue.c:128
#5 0x00007ff593308992 in work_queue_add (wq=<optimized out>, data=data@entry=0x56222ca3ee40) at lib/workqueue.c:165
#6 0x000056222c48cdf3 in mq_add_handler (mq_add_func=<optimized out>, data=0x56222cbf6210) at zebra/zebra_rib.c:2685
#7 rib_queue_add (rn=0x56222cbf6210) at zebra/zebra_rib.c:2704
#8 0x000056222c48fb35 in rib_link (process=1, re=0x56222cbf5150, rn=0x56222cbf6210) at zebra/zebra_rib.c:3163
#9 rib_addnode (process=1, re=0x56222cbf5150, rn=0x56222cbf6210) at zebra/zebra_rib.c:3180
#10 rib_add_multipath_nhe (afi=afi@entry=AFI_IP, safi=<optimized out>, p=p@entry=0x7fff6cbbd630, src_p=<optimized out>, re=re@entry=0x56222cbf5150, re_nhe=re_nhe@entry=0x7fff6cbbd5b0, startup=false)
at zebra/zebra_rib.c:3540
#11 0x000056222c48fdfd in rib_add_multipath_nhe (afi=afi@entry=AFI_IP, safi=<optimized out>, p=p@entry=0x7fff6cbbd630, src_p=<optimized out>, re=re@entry=0x56222cbf5150,
re_nhe=re_nhe@entry=0x7fff6cbbd5b0, startup=<optimized out>) at zebra/zebra_rib.c:3568
#12 0x000056222c457e6d in zread_route_add (client=0x56222cbd4970, hdr=<optimized out>, msg=<optimized out>, zvrf=<optimized out>) at zebra/zapi_msg.c:2148
#13 0x000056222c45bfbf in zserv_handle_commands (client=client@entry=0x56222cbd4970, fifo=fifo@entry=0x56222ca3e110) at zebra/zapi_msg.c:3846
#14 0x000056222c4b9b90 in zserv_process_messages (thread=<optimized out>) at zebra/zserv.c:523
#15 0x00007ff5932fe6be in thread_call (thread=thread@entry=0x7fff6cbc32a0) at lib/thread.c:2002
#16 0x00007ff5932bb490 in frr_run (master=0x56222c859770) at lib/libfrr.c:1196
#17 0x000056222c42e006 in main (argc=1, argv=0x7fff6cbc35a8) at zebra/main.c:471
Thread 1 "zebra" hit Breakpoint 1, __libc_write (fd=16, buf=buf@entry=0x7fff6cbc2e8f, nbytes=nbytes@entry=1) at ../sysdeps/unix/sysv/linux/write.c:26
26 in ../sysdeps/unix/sysv/linux/write.c
(gdb) bt
#0 __libc_write (fd=16, buf=buf@entry=0x7fff6cbc2e8f, nbytes=nbytes@entry=1) at ../sysdeps/unix/sysv/linux/write.c:26
#1 0x00007ff5932fd7dc in _thread_add_event (xref=xref@entry=0x56222c5397e0 <_xref.67>, m=0x56222cbc0760, func=func@entry=0x56222c45c9d0 <dplane_thread_loop>, arg=arg@entry=0x0, val=val@entry=0,
t_ptr=t_ptr@entry=0x56222c580db8 <zdplane_info+280>) at lib/thread.c:1170
#2 0x000056222c45d45e in dplane_provider_work_ready () at zebra/zebra_dplane.c:5260
#3 dplane_provider_work_ready () at zebra/zebra_dplane.c:5252
#4 dplane_update_enqueue (ctx=ctx@entry=0x56222cbfcce0) at zebra/zebra_dplane.c:3207
#5 0x000056222c45d5c9 in dplane_route_update_internal (rn=rn@entry=0x56222cbf6210, re=re@entry=0x56222cbf5150, old_re=old_re@entry=0x0, op=op@entry=DPLANE_OP_ROUTE_INSTALL) at zebra/zebra_dplane.c:3303
#6 0x000056222c460426 in dplane_route_add (rn=rn@entry=0x56222cbf6210, re=re@entry=0x56222cbf5150) at zebra/zebra_dplane.c:3376
#7 0x000056222c48b393 in rib_install_kernel (rn=rn@entry=0x56222cbf6210, re=re@entry=0x56222cbf5150, old=old@entry=0x0) at zebra/zebra_rib.c:651
#8 0x000056222c48e651 in rib_process_add_fib (new=<optimized out>, rn=<optimized out>, zvrf=<optimized out>) at zebra/zebra_rib.c:918
#9 rib_process (rn=0x56222cbf6210) at zebra/zebra_rib.c:1359
#10 0x000056222c48ef15 in process_subq_route (qindex=<optimized out>, lnode=0x56222cbf3420) at zebra/zebra_rib.c:2463
#11 process_subq (qindex=<optimized out>, subq=0x56222ca3ec90) at zebra/zebra_rib.c:2505
#12 meta_queue_process (dummy=<optimized out>, data=0x56222ca3ee40) at zebra/zebra_rib.c:2538
#13 0x00007ff5933084e2 in work_queue_run (thread=0x7fff6cbc32a0) at lib/workqueue.c:292
#14 0x00007ff5932fe6be in thread_call (thread=thread@entry=0x7fff6cbc32a0) at lib/thread.c:2002
#15 0x00007ff5932bb490 in frr_run (master=0x56222c859770) at lib/libfrr.c:1196
#16 0x000056222c42e006 in main (argc=1, argv=0x7fff6cbc35a8) at zebra/main.c:471
dplane更新スレッドへ伝達
rib_process()
ではRIBへの変更要求に対応しています。
これがRIB関係の処理の本体です。この中のrib_choose_best(new, re)
が登録されるルートエントリと今までのベストエントリを比較しています。
その後、rib_process_add_fib()
などを呼び出します。これらの関数がdplane_update_enqueue()
を呼び出すことdplane更新スレッドに処理が渡ります。
|
|
FIB操作
dplaneの処理ループはdplane_thread_loop()
で駆動されます。変更操作はkernel_dplane_process_func()
が呼ばれています。
limit
数だけproviderにタスク(ctx
)を割り当てて逐次実行しています。(TAILQ_XXX()
はキューの操作です)
|
|
ref. zebra/zebra_dplane.c#L5996-L6182
この中の(*prov->dp_fp)(prov);
が関数ポインタ経由でkernel_dplane_process_func()
を呼んでいます。
処理内容で分岐しているわけではなくテストで実際にネットワーク設定を変更しないようにするためのようです。
このkernel_dplane_process_func()
からnetlink_send_msg()
を経てsendmsg()
が呼ばれます。
#0 __libc_sendmsg (fd=12, msg=msg@entry=0x7ff592424a10, flags=flags@entry=0) at ../sysdeps/unix/sysv/linux/sendmsg.c:28
#1 0x000056222c43ed21 in netlink_send_msg (nl=0x56222ca75b58, buf=0x7ff58c006ce0, buflen=52) at zebra/kernel_netlink.c:791
#2 0x000056222c43f833 in nl_batch_send (bth=bth@entry=0x7ff592424ba0) at zebra/kernel_netlink.c:1359
#3 0x000056222c4409a7 in kernel_update_multi (ctx_list=ctx_list@entry=0x7ff592424c40) at zebra/kernel_netlink.c:1557
#4 0x000056222c4621e0 in kernel_dplane_process_func (prov=0x56222ca3f170) at zebra/zebra_dplane.c:5664
sendmsgはzebra/kernel_netlink.c#L864で確認できます。 ここの宛先ソケットは下のようにnetlinkがセットされています。
|
|
ref. zebra/kernel_netlink.c#L1167-L1169
ルートノードのrnh
の更新
FIB が変更を受けるといくつか通知が必要になります。前述の zebra_rnh_evaluate_entry()
によるrnh
の更新はここで必要になるようです。
後述の3つの関数からzebra_rib_evaluate_rn_nexthops()
の呼び出しを通してzebra_rnh_evaluate_entry()
が呼ばれていました。
int rib_gc_dest(struct route_node *rn)
static void rib_process_result(struct zebra_dplane_ctx *ctx)
static void rib_process_dplane_notify(struct zebra_dplane_ctx *ctx)
VTYSH のREPLやコマンド特定は簡単なので置いておくとYANGに従って設定変更が行われ、RIB 更新が FIB の変更につながるまでを眺めました。
VTYSHのREPL
lib/vty.c の vty_read()
がREPLをやっていて、改行でそれまでの入力をコマンドとして処理しています。
|
|
細かくことほどのことはないですがいくつか触れておく。
cmd_make_strvec(cmd_exec);
で入力文字列をトークン列に変換しているstatus = command_match(cmdgraph, vline, &argv_list, &matched_element);
でVTYSHの状態と入力列で実行コマンドを判別して取得しているret = matched_element->func(matched_element, vty, argc, argv);
で取得したコマンドに紐づく関数を実行している
これが staticd で動いていてVTYSHは全て渡してくる。
クライアントの状態管理
プロトコルデーモンは受け取った入力文字列をコマンドに対応させている。これはプロセス起動時に下のように登録しています。
|
|
VTYSH からnetlinkへのsendmsg()
までを追っかけました。
読んでいて zebra_srv6_init()
など気になる関数もみつけましたがとりあえず読まずに放置しました。おしまい。