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.cl.378-379で代入されます。 書かれている通りfrr_vtydirの値が使われています。

1
2
3
void frr_init_vtydir(void){
    snprintf(frr_vtydir, sizeof(frr_vtydir), DAEMON_VTY_DIR, "", "");
}

ref. lib/libfrr.c#L320-L323

この DAEMON_VTY_DIRここで決定されて代入されています。

実験:メッセージの確認

VTYSHはデーモンへNUL終端文字列を投げているらしいです。 確認します。まず事前準備として socat でプロキシを挟むようにしました。

1
2
sudo mv /var/opt/frr/staticd.vty /var/opt/frr/staticd.vty.orig
sudo socat -x -v UNIX-LISTEN:/var/opt/frr/staticd.vty, UNIX-CONNECT:/var/opt/frr/staticd.vty.orig

別のターミナルでVTYSHで設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ sudo vtysh
% Can't open configuration file /etc/frr/vtysh.conf due to 'No such file or directory'.

Hello, this is FRRouting (version 8.3-dev-MyOwnFRRVersion-gde416e678).
Copyright 1996-2005 Kunihiro Ishiguro, et al.

This is a git build of frr-8.3-dev-388-gde416e678
Associated branch(es):
        local:master
        github/frrouting/frr.git/master

bullseye# configure
bullseye(config)# ip route 100.0.35.0/24 10.0.2.100
bullseye(config)# exit
bullseye# exit

こんな感じでコマンドがそのまま流れています。またMarkerとStatusCodeを合わせた4バイトの0x00も確認できました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
> 2022/05/06 14:42:10.163439  length=7 from=0 to=6
 65 6e 61 62 6c 65 00                             enable.
--
< 2022/05/06 14:42:10.164441  length=4 from=0 to=3
 00 00 00 00                                      ....
--
> 2022/05/06 14:42:16.305447  length=11 from=7 to=17
 63 6f 6e 66 69 67 75 72 65 20 00                 configure .
--
< 2022/05/06 14:42:16.305631  length=4 from=4 to=7
 00 00 00 00                                      ....
--
> 2022/05/06 14:42:35.168397  length=34 from=18 to=51
 69 70 20 72 6f 75 74 65 20 31 30 30 2e 30 2e 33  ip route 100.0.3
 35 2e 30 2f 32 34 20 31 30 2e 30 2e 32 2e 31 30  5.0/24 10.0.2.10
 30 00                                            0.
--
< 2022/05/06 14:42:35.169482  length=4 from=8 to=11
 00 00 00 00                                      ....
--
> 2022/05/06 14:42:37.422039  length=5 from=52 to=56
 65 78 69 74 00                                   exit.
--
< 2022/05/06 14:42:37.426787  length=4 from=12 to=15
 00 00 00 00                                      ....
--
> 2022/05/06 14:42:38.509946  length=5 from=57 to=61
 65 78 69 74 00                                   exit.
--
< 2022/05/06 14:42:38.510113  length=4 from=16 to=19
 00 00 00 00                                      ....
--

プロセス内部構造: イベントループ

プロセスアーキテクチャの章に詳しいです。

イベントループを中心としたアーキテクチャになっています。 詳細はドキュメントを読みましょう。 他のライブラリやフレームワークでイベント・タスクと呼ぶものをFRRoutingではスレッドと呼びます。 そして、OSが提供するスレッドはpthreads経由で利用しているため、こちらをpthreadと呼んでいます。

イベントループで各ステップが別れて処理されるためスタックトレースをしても互いの繋がりが読みにくかったです。 このイベントループはプロトコルデーモンの中で複数動いています。 互いにスケジューラ経由でイベントを登録できます。またメインスレッドへのリンクを持っています。ErlangのActorモデルに似てる印象を受けました。

そして、イベントループが1つのpthreadに対応するわけではありません。 イベントには種類があり、イベント登録する際に種類を明示します。その際、ブロックされるイベントではpthreadが確保されていました。 なので単一のイベントループから複数のaccept()のようなブロックを伴う処理を並列に走らせることができます。

たとえばvty_event(VTY_READ, vty)を呼び出すと内部でthread_add_read(...)が呼ばれています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static void vty_event(enum vty_event event, struct vty *vty)
{
        switch (event) {
#ifdef VTYSH
        case VTYSH_READ:
                thread_add_read(vty_master, vtysh_read, vty, vty->fd,
                                &vty->t_read);
                break;
        case VTYSH_WRITE:
                thread_add_write(vty_master, vtysh_write, vty, vty->wfd,
                                 &vty->t_write);
                break;

プリプロセッサで下のように _thread_add_read_write() に展開されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

#define _xref_t_a(addfn, type, m, f, a, v, t)                                  \
        ({                                                                     \
                static const struct xref_threadsched _xref                     \
                                __attribute__((used)) = {                      \
                        .xref = XREF_INIT(XREFT_THREADSCHED, NULL, __func__),  \
                        .funcname = #f,                                        \
                        .dest = #t,                                            \
                        .thread_type = THREAD_ ## type,                        \
                };                                                             \
                XREF_LINK(_xref.xref);                                         \
                _thread_add_ ## addfn(&_xref, m, f, a, v, t);                  \
        })                                                                     \
        /* end */

#define thread_add_read(m,f,a,v,t)       _xref_t_a(read_write, READ,  m,f,a,v,t)
#define thread_add_write(m,f,a,v,t)      _xref_t_a(read_write, WRITE, m,f,a,v,t)
#define thread_add_timer(m,f,a,v,t)      _xref_t_a(timer,      TIMER, m,f,a,v,t)
#define thread_add_timer_msec(m,f,a,v,t) _xref_t_a(timer_msec, TIMER, m,f,a,v,t)
#define thread_add_timer_tv(m,f,a,v,t)   _xref_t_a(timer_tv,   TIMER, m,f,a,v,t)
#define thread_add_event(m,f,a,v,t)      _xref_t_a(event,      EVENT, m,f,a,v,t)

_thread_add_read_write()pthreadが確保されています。 ブロックされるスレッドはイベント登録時にイベントタイプを明示することで専用スレッドを確保してるように読めました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void _thread_add_read_write(const struct xref_threadsched *xref,
                            struct thread_master *m,
                            void (*func)(struct thread *), void *arg, int fd,
                            struct thread **t_ptr)
{
    if (dir == THREAD_READ)
        frrtrace(9, frr_libfrr, schedule_read, m,
                xref->funcname, xref->xref.file, xref->xref.line,
                t_ptr, fd, 0, arg, 0);
    else
        frrtrace(9, frr_libfrr, schedule_write, m,
                xref->funcname, xref->xref.file, xref->xref.line,
                t_ptr, fd, 0, arg, 0);

    frr_with_mutex(&m->mtx) {
            thread = thread_get(m, dir, func, arg, xref);

ルーティング設定のデバイスモデルに関わるコード

FRRoutingでネットワーク設定がどう実装されているか確認します。ファイルはこのように分類できます。

矢印でモジュール間の依存を表しています。 特に staticd/static_nb_configlib/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コードが生成されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ find ./yang/ -name '*.yang'
./yang/frr-isisd.yang
./yang/frr-ospfd.yang
./yang/frr-filter.yang
./yang/frr-route-map.yang
./yang/frr-bgp-types.yang
./yang/frr-bgp-peer-group.yang
./yang/frr-bgp-common-structure.yang
./yang/frr-ospf6-route-map.yang
./yang/frr-test-module.yang
./yang/frr-deviations-bgp-datacenter.yang
./yang/frr-bgp-rpki.yang
./yang/frr-ripngd.yang
./yang/frr-route-types.yang
./yang/frr-bgp.yang
./yang/frr-bgp-route-map.yang
./yang/frr-module-translator.yang
./yang/frr-bgp-common-multiprotocol.yang
./yang/frr-bgp-filter.yang
./yang/frr-bgp-bmp.yang
./yang/frr-pim-rp.yang
./yang/frr-pim.yang
./yang/ietf/ietf-routing-types.yang
./yang/ietf/frr-deviations-ietf-rip.yang
./yang/ietf/frr-deviations-ietf-routing.yang
./yang/ietf/frr-deviations-ietf-interfaces.yang
./yang/ietf/ietf-bgp-types.yang
./yang/ietf/ietf-interfaces.yang
./yang/frr-bgp-neighbor.yang
./yang/frr-vrf.yang
./yang/frr-eigrpd.yang
./yang/frr-routing.yang
./yang/frr-nexthop.yang
./yang/confd/confd.frr-ripd.yang
./yang/confd/confd.frr-ripngd.yang
./yang/frr-ospf-route-map.yang
./yang/frr-zebra-route-map.yang
./yang/frr-staticd.yang
./yang/frr-bfdd.yang
./yang/frr-gmp.yang
./yang/frr-ripd.yang
./yang/frr-pathd.yang
./yang/frr-bgp-common.yang
./yang/frr-interface.yang
./yang/frr-zebra.yang
./yang/frr-vrrpd.yang

YANGオブジェクトモデルの実装

YANGのノード操作にlibyangを使っています。 このライブラリではスキーマノードやデータノードをXPathから特定したりバリデーションを行う機能が提供されています。 これに加えて複数のYANGモジュールを管理する機能を提供するAPIが*lib/*yang**にまとめられています。

設定変更はデータノード操作に紐づいたコールバックとして実装されています。コールバック機構によってデータノードへの操作を実際に処理しています。 このコールバック機構と低レベルなイベント・命令の実装は lib/northbound.c に集まっています。 これらを組み合わせてコミットやロールバック、ペンディングなど高度な仕組みを提供しているのが lib/northbound_cli.c です。

モジュールやコールバックは、コミット前のコールバックのキューなどは赤黒木で管理されています。 モジュールの読み込みは下の関数で行います。ここで RB_INSERT()は赤黒木へ要素を挿入するマクロです。 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct yang_module *yang_module_load(const char *module_name)
{
        struct yang_module *module;
        const struct lys_module *module_info;

        module_info =
                ly_ctx_load_module(ly_native_ctx, module_name, NULL, NULL);
        if (!module_info) {
                // ...
                exit(1);
        }

        module = XCALLOC(MTYPE_YANG_MODULE, sizeof(*module));
        module->name = module_name;
        module->info = module_info;

        if (RB_INSERT(yang_modules, &yang_modules, module) != NULL) {
                // ...
                exit(1);
        }

        return module;
}

赤黒木

モジュールやコールバックの管理に赤黒木を使っています。赤黒木は lib/openbsd-tree.c,h で定義されています。 マクロを使って複数の型に赤黒木を提供します。

それぞれの型に対応した関数とデータ構造と及びそのインスタンスを作成するためRB_PROTOTYPE(),RB_HEAD()マクロが定義されてます。 YANGモジュールを赤黒木で挿入するにはRB_INSERT(yang_modules, )を用い探索にRB_FIND(type, ...)を使います。 挿入を基本操作として赤黒木が実装されています。ノードの削除は実装されていません。rbe_rotate_left(), rbe_rotate_right()などいつもの内部操作が実装されてます。

1
2
RB_HEAD(yang_modules, yang_module);
RB_PROTOTYPE(yang_modules, yang_module, entry, yang_module_compare);

YANG関連では lib/yang.h で下のように呼ばれています。

1
2
3
4
5
6
7
struct yang_module *yang_module_find(const char *module_name)
{
        struct yang_module s;

        s.name = module_name;
        return RB_FIND(yang_modules, &yang_modules, &s);
}

northbound

クライアント(VTYSH)向けインタフェースをnorthboundインタフェースと呼びます。YANGを利用して実装されます。 YANGは単にデバイスモデルを定義するだけですが lib/northbound.c でコールバック機構を提供しています。 たとえば staticd ではstaticd/staticd_nb.c のなかでYANGのデータノードへの変更にコールバックを設定しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const struct frr_yang_module_info frr_staticd_info = {
        .name = "frr-staticd",
        .nodes = {
            //...
                {
                    .xpath = "/frr-routing:routing/control-plane-protocols/control-plane-protocol/frr-staticd:staticd/route-list/src-list/path-list/frr-nexthops/nexthop",
                    .cbs = {
                            .apply_finish = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_src_list_path_list_frr_nexthops_nexthop_apply_finish,
                            .create = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_src_list_path_list_frr_nexthops_nexthop_create,
                            .destroy = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_src_list_path_list_frr_nexthops_nexthop_destroy,
                            .pre_validate = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_path_list_frr_nexthops_nexthop_pre_validate,
                            .cli_show = static_src_nexthop_cli_show,
                            .cli_cmp = static_nexthop_cli_cmp,
                    }
                },
            //...
        }

このようにコールバックなどとの対応づけを定義したfrr_yang_module_info構造体のインスタンスは、 プログラム開始時に lib/northbound.cnb_init()によって利用されます。 登録されたコールバックはnb_transaction_process()関数経由で呼び出されます。 下のコード内でtransaction->changesがコールバックのリストです。nb_callback_configuration()eventが少なくともNB_EV_APPLYの時はコールバックが実行されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static int nb_transaction_process(enum nb_event event,
                                  struct nb_transaction *transaction,
                                  char *errmsg, size_t errmsg_len)
{
    struct nb_config_cb *cb;
    // ...
    RB_FOREACH (cb, nb_config_cbs, &transaction->changes) {
        struct nb_config_change *change = (struct nb_config_change *)cb;
        int ret;
    ret = nb_callback_configuration(transaction->context, event,
                                    change, errmsg, errmsg_len);
    // ...
    }
    return NB_OK;
}

ref. lib/northbound.c#L1462-L1474

このnb_transaction_process()staticdでどのように呼ばれるか確認します。 &transaction->changesに代入しているのはnb_transaction_new()nb_candidate_commit_prepare()で呼ばれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// in lib/northbound.c
int nb_candidate_commit_prepare(struct nb_context *context,
                                struct nb_config *candidate,
                                const char *comment,
                                struct nb_transaction **transaction,
                                /* ... */) {
    // ...
    nb_transaction_new()
    // ...
    nb_transaction_process(NB_EV_PREPARE, ...)
}

int nb_candidate_commit(struct nb_context *context, struct nb_config *candidate,
                    bool save_transaction, const char *comment,
                    /* attrs */) {
    // ...
    nb_candidate_commit_prepare()
    // ...
    nb_candidate_commit_apply()
}

void nb_candidate_commit_apply(struct nb_transaction *transaction, 
                               /* attrs */) {
    nb_transaction_process(NB_EV_APPLY, ...)
    nb_transaction_apply_finish()
    // ...
}

これらの関数がlib/northand_cli.cで利用されています。

1
2
3
4
5
6
// in lib/northbound_cli.c
static int nb_cli_classic_commit(struct vty *vty) {
    // ...
    nb_candidate_commit()
    // ...
}

staticdのコマンドはまず nb_cli_enqueue_change() が呼ばれ&vty->cfg_changesを更新します。 そしてnb_cli_apply_changes()nb_cli_apply_changes_internal()からnb_cli_classic_commit()が呼ばれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void nb_cli_enqueue_change(struct vty *vty, const char *xpath,
                           enum nb_operation operation, const char *value)
{
        struct vty_cfg_change *change;
        // ...
        change = &vty->cfg_changes[vty->num_cfg_changes++];
        strlcpy(change->xpath, xpath, sizeof(change->xpath));
        change->operation = operation;
        change->value = value;
}

ref. lib/northbound_cli.c#L135-L152

デーモンプロセス初期化での挙動

YANGのコールバック機構の初期化がプロセス起動時にどこから実行されるか確認します。 場所は lib/libfrr.c です。 プロセス初期化はファイル中のfrr_init()です。northboundの初期化はnb_init()関数にまとまっています。 呼び出しは下のようになっていて di->yang_modulesという部分が読み込まれるモジュールになっています。

1
nb_init(master, di->yang_modules, di->n_yang_modules, true);

nb_init()へのパラーメータ渡し: staticd

実際に staticd でどのように初期化するかを追ってみます。staticd/staticd_main.c の中は下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static struct frr_daemon_info staticd_di;

static const struct frr_yang_module_info *const staticd_yang_modules[] = {
        &frr_filter_info,
        &frr_interface_info,
        &frr_vrf_info,
        &frr_routing_info,
        &frr_staticd_info,
};

FRR_DAEMON_INFO(staticd, STATIC,
                .yang_modules = staticd_yang_modules,
                .n_yang_modules = array_size(staticd_yang_modules),
);

int main(int argc, char **argv, char **envp) {
    frr_preinit(&staticd_di, argc, argv);
    frr_init();
}

このfrr_preinit()&static_dilib/libfrr.c内のdiに代入します。 static_diFRR_DAEMON_INFO(static, ...)で代入されます。

このマクロは lib/libfrr.h で定義され下のようになっています。 特に __VA_ARGS__の箇所を経由して.yang_modulesのフィールドにstaticd_yang_modulesが入ります。

1
2
3
4
5
6
7
8
9
#define FRR_DAEMON_INFO(execname, constname, ...)                              \
        static struct frr_daemon_info execname##_di = {.name = #execname,      \
                                                       .logname = #constname,  \
                                                       .module = THIS_MODULE,  \
                                                       __VA_ARGS__};           \
        FRR_COREMOD_SETUP(.name = #execname,                                   \
                          .description = #execname " daemon",                  \
                          .version = FRR_VERSION, );                           \
        MACRO_REQUIRE_SEMICOLON() /* end */

というわけでstaticd_yang_modulesを確認すればXPAATHに対応したコールバックを知ることができます。 たとえば …:staticd/route-list/src-list/path-list/frr-nexthops/nexthop に関するコールバックは下のように定義されてます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const struct frr_yang_module_info frr_staticd_info = {
        .name = "frr-staticd",
        .nodes = {
            //...
                {
                    .xpath = "/frr-routing:routing/control-plane-protocols/control-plane-protocol/frr-staticd:staticd/route-list/src-list/path-list/frr-nexthops/nexthop",
                    .cbs = {
                            .apply_finish = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_src_list_path_list_frr_nexthops_nexthop_apply_finish,
                            .create = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_src_list_path_list_frr_nexthops_nexthop_create,
                            .destroy = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_src_list_path_list_frr_nexthops_nexthop_destroy,
                            .pre_validate = routing_control_plane_protocols_control_plane_protocol_staticd_route_list_path_list_frr_nexthops_nexthop_pre_validate,
                            .cli_show = static_src_nexthop_cli_show,
                            .cli_cmp = static_nexthop_cli_cmp,
                    }
                },
            //...
        }

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を使ってディスパッチされます。

1
2
3
4
5
6
7
void (*const zserv_handlers[])(ZAPI_HANDLER_ARGS) = {
    // ...
        [ZEBRA_ROUTE_ADD] = zread_route_add,
        [ZEBRA_ROUTE_DELETE] = zread_route_del,
        [ZEBRA_REDISTRIBUTE_ADD] = zebra_redistribute_add,
        [ZEBRA_REDISTRIBUTE_DELETE] = zebra_redistribute_delete,
    // ...

RIBへのエントリー登録

ルート追加のコマンドはzread_route_add()関数に対応している。 このなかで RIB のエントリ(struct route_entry)を組み立てて rib_add_multipath_nhe() を呼び出している。 rib_add_multipath_nhe()の中でネクストホップを解決してFIB更新を行う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// in zebra/zapi_msg.c
static void zread_route_add(ZAPI_HANDLER_ARGS)
{
    // ...
    struct route_entry *re;
    // reを組み立てる
    ret = rib_add_multipath_nhe(afi, api.safi, &api.prefix, src_p,
                                re, &nhe, false);
    // ...
}

RIB/FIB

FRRoutingでは VRF(Virtual Routing and Forwarding) をサポートしている。 VRF./zebra/zebra_vrf.h で定義されたstruct zebra_vrfのオブジェクトに対応している。 この構造体は用途別に route_tableを複数保持していた。他にもプロトコルごとにhashで関連情報を保持してもいる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* Routing table instance.  */
struct zebra_vrf {
        struct vrf *vrf;

        /* Description.  */
        char *desc;

        /* FIB identifier.  */
        uint8_t fib_id;
// ...
        uint32_t table_id;

        /* Routing table.  */
        struct route_table *table[AFI_MAX][SAFI_MAX];

        /* Recursive Nexthop table */
        struct route_table *rnh_table[AFI_MAX];
        struct route_table *rnh_table_multicast[AFI_MAX];

このroute_tableRIB に対応していて ./lib/table.hで定義されています。 この辺りの構造体をまず眺めてみます。関連する型の参照関係です。

ref. src

オブジェクト 大雑把な説明
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 の二分木です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct route_node;
struct route_table;
// ...
struct route_table {
        struct route_node *top;
        struct rn_hash_node_head hash;

        /*
         * Delegate that performs certain functions for this table.
         */
        route_table_delegate_t *delegate;
        void (*cleanup)(struct route_table *, struct route_node *);

        unsigned long count;
        void *info;
};

struct route_nodeは下のように定義されていて、ルートはプレフィックスを持っています。またvoid *infoが宛先を持ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define ROUTE_NODE_FIELDS                                                      \
        /* Actual prefix of this radix. */                                     \
        struct prefix p;                                                       \
                                                                               \
        /* Tree link. */                                                       \
        struct route_table *table_rdonly(table);                               \
        struct route_node *table_rdonly(parent);                               \
        struct route_node *table_rdonly(link[2]);                              \
                                                                               \
        /* Lock of this radix */                                               \
        unsigned int table_rdonly(lock);                                       \
                                                                               \
        struct rn_hash_node_item nodehash;                                     \
        /* Each node of route. */                                              \
        void *info;                                                            \


/* Each routing entry. */
struct route_node {
        ROUTE_NODE_FIELDS

#define l_left   link[0]
#define l_right  link[1]
};

このinfoフィールドはこのプレフィックスの向き先情報になります。 取り出すインライン関数がzebra/rib.hで次のように定義されてます。

1
2
3
4
static inline rib_dest_t *rib_dest_from_rnode(struct route_node *rn)
{
    return (rib_dest_t *)rn->info;
}

rib_dest_troute_entryの連結リストとnhtの連結リストを持ち採用されたselected_fibなどのメタ情報を含んでいます。

1
2
3
4
5
6
7
8
9
// l.198 in zebra/rib.h
typedef struct rib_dest_t_ {
    struct route_node *rnode;
    struct re_list_head routes;
    struct route_entry *selected_fib;
    struct rnh_list_head nht;
} rib_dest_t;
DECLARE_LIST(rnh_list, struct rnh, rnh_list_item);
DECLARE_LIST(re_list, struct route_entry, next);

rib_dest_tstruct 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()を呼びます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int rib_add_multipath_nhe(afi_t afi, safi_t safi, struct prefix *p,
                          struct prefix_ipv6 *src_p, struct route_entry *re,
                          struct nhg_hash_entry *re_nhe, bool startup)
{
    // ...
    rn = srcdest_rnode_get(table, p, src_p);
    // 同じルートノード(prefix, src_prefix)に紐づくroute_entryの数を確認したり
    RNODE_FOREACH_RE (rn, same) {
        if (rib_compare_routes(re, same)) {
            // ROUTE_ENTRY_REMOVEDなのをスキップ
            // 全く同じエントリであるか確認している
            if (rib_compare_routes(re, same)) {
                if (first_same == NULL) first_same = same;
            }
        }
    }

    // kernel/connectedを特別扱いするコード
    SET_FLAG(re->status, ROUTE_ENTRY_CHANGED);
    rib_addnode(rn, re, 1); // 中でrib_link()を呼び出す
}

ref. zebra/zebra_rib.c#L3540

rib_link()は対象のルートノードに新しくルートエントリーを追加して、rib_queue_add(route_node)をしてます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static void rib_link(struct route_node *rn, struct route_entry *re, int process)
{
    
    dest = rib_dest_from_rnode(rn);
    if (!dest) {
        // route_nodeに向き先: route_entryのリストがなかったら作成する
        dest = zebra_rib_create_dest(rn);
    }
    // 今回の route_entryを登録する
    re_list_add_head(&dest->routes, re);

    afi = (rn->p.family == AF_INET)
          ? AFI_IP
          : (rn->p.family == AF_INET6) ? AFI_IP6 : AFI_MAX;
    if (is_zebra_import_table_enabled(afi, re->vrf_id, re->table)) {
    } else if (process)
        rib_queue_add(rn);

ref. zebra/zebra_rib.c#L3163

これにより別スレッドとして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更新スレッドに処理が渡ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static void rib_process(struct route_node *rn)
{
    // VRFを特定したり優先度を特定する
    best = rib_choose_best(new_fib, re)
    if (new_fib && best != new_fib) {
        new_fib = best
    }
    // 
    if (new_fib && old_fib)
        rib_process_update_fib(zvrf, rn, old_fib, new_fib);
    else if (new_fib)
        rib_process_add_fib(zvrf, rn, new_fib);
    else if (old_fib)
        rib_process_del_fib(zvrf, rn, old_fib);
}

FIB操作

dplaneの処理ループはdplane_thread_loop()で駆動されます。変更操作はkernel_dplane_process_func()が呼ばれています。 limit数だけproviderにタスク(ctx)を割り当てて逐次実行しています。(TAILQ_XXX()はキューの操作です)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static void dplane_thread_loop(struct thread *event)
{
    // 最初のproviderのためにwork_listを準備している
    TAILQ_INIT(&work_list);
    TAILQ_INIT(&error_list);
    error_counter = 0;

    DPLANE_LOCK(); // DPLANEの操作受付を一旦止める
    prov = TAILQ_FIRST(&zdplane_info.dg_providers_q);

    // 最大で limit 数の操作の塊(ctx)を上で取得したproviderに割り当ててwork_listに登録する
    for (counter = 0; counter < limit; counter++) {
        ctx = TAILQ_FIRST(&zdplane_info.dg_update_ctx_q);
        if (ctx) {
            TAILQ_REMOVE(&zdplane_info.dg_update_ctx_q, ctx,
                         zd_q_entries);

            ctx->zd_provider = prov->dp_id;

            TAILQ_INSERT_TAIL(&work_list, ctx, zd_q_entries);
        } else {
            break;
        }
    }
    DPLANE_UNLOCK(); // DPLANEへの操作要求を受け付け開始
    while (prov) {
        TAILQ_FOREACH_SAFE(ctx, &work_list, zd_q_entries, tctx) {
            if (dplane_ctx_get_status(ctx) ==
                ZEBRA_DPLANE_REQUEST_SUCCESS) {
                ctx->zd_provider = prov->dp_id;
            } else {
                // エラーなら扱うべきじゃない
                TAILQ_REMOVE(&work_list, ctx, zd_q_entries);
                TAILQ_INSERT_TAIL(&error_list,
                                  ctx, zd_q_entries);
                error_counter++;
            }
        }

        dplane_provider_lock(prov);
        // providermにwork_listを登録する
        if (TAILQ_FIRST(&work_list))
            TAILQ_CONCAT(&(prov->dp_ctx_in_q), &work_list,
                         zd_q_entries);
        // 色々やって
        (*prov->dp_fp)(prov); // (*provider->dp_fp)はkernel_dplane_process_func

        // 後処理 ...
        // 次のprovのためにwork_listを準備
        prov = TAILQ_NEXT(prov, dp_prov_link); // provに次のproviderを入れる
    }
    // 処理後のcallbackを登録している (失敗, 成功)
    (zdplane_info.dg_results_cb)(&error_list);
    TAILQ_INIT(&error_list);
    (zdplane_info.dg_results_cb)(&work_list);
    TAILQ_INIT(&work_list);

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がセットされています。

1
2
3
4
5
nl = kernel_netlink_nlsock_lookup(dp_info->sock);
n->nlmsg_seq = dp_info->seq;
n->nlmsg_pid = nl->snl.nl_pid;
if (netlink_send_msg(nl, n, n->nlmsg_len) == -1)
    return -1;

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.cvty_read() がREPLをやっていて、改行でそれまでの入力をコマンドとして処理しています。

1
2
3
4
5
6
7
8
9
vty_read() {
 	// 入力処理
 	switch {
 	case '\n':
 		// まずvty->wfdに書き込みバッファをフラッシュしてから受け取ったコマンドを実行する
 		buffer_flush_available(vty->obuf, vty->wfd);
 		vty_execute(vty);
 	}
 }

細かくことほどのことはないですがいくつか触れておく。

  • 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は全て渡してくる。

クライアントの状態管理

プロトコルデーモンは受け取った入力文字列をコマンドに対応させている。これはプロセス起動時に下のように登録しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void static_vty_init(void)
{
        install_node(&debug_node);

        install_element(CONFIG_NODE, &ip_mroute_dist_cmd);

        install_element(CONFIG_NODE, &ip_route_blackhole_cmd);
        install_element(VRF_NODE, &ip_route_blackhole_vrf_cmd);
        install_element(CONFIG_NODE, &ip_route_address_interface_cmd);
        install_element(VRF_NODE, &ip_route_address_interface_vrf_cmd);
        install_element(CONFIG_NODE, &ip_route_cmd);
        install_element(VRF_NODE, &ip_route_vrf_cmd);

        install_element(CONFIG_NODE, &ipv6_route_blackhole_cmd);
        install_element(VRF_NODE, &ipv6_route_blackhole_vrf_cmd);
// ....
}

VTYSH からnetlinkへのsendmsg()までを追っかけました。 読んでいて zebra_srv6_init() など気になる関数もみつけましたがとりあえず読まずに放置しました。おしまい。

comments powered by Disqus