FRRでPath Attributeを改造して、実際にコマンドでfilterを作ってみる

FRRを改造しました。あくまで個人用です。
とりあえず動くものを作れるようになるまでを目標としています(topotestから動かせるようになるまで!)

BGP ATTRの構成について

BGPのattr構成は大体四種類に分けられています

BGP attrには属性みたいなのがあって

Well-known mandatory( 既知必須 ):全てのBGPルータで識別できて、全てのUpdateメッセージに含まれる

Well-known discretionary: 全てのBGPルータで識別できるが、Updateメッセージに含まれるかは任意

Optional transitive( オプション通知 ):全てのBGPルータで識別できない可能性があるが、BGPネイバーへは通知する

Optional non-transitive(オプション非通知):全てのBGPルータで識別できない可能性があり、BGPネイバーへは通知しない

となっていて

決められたType codeと一緒に送信することになっている 例えばpath attrの一つのORIGINあれば

flags:Well-known mandatory  
type code:1  

という決まりになっている

次にattrのデータの長さを定義するフィールドが存在していて、最後にデータが付けられる。

またORGIN attrを例にすると

len = 0x01  
data = 0x00  

例:NEXT HOP attr  
flags:0x40  
type code:0x03  
len:0x04  
data:0x02,02,02,02(2.2.2.2)

www.infraexpert.com

目標

今回はコマンドを自作して、FRRにPath Attributeとして機能させることを目標としています。
さらに具体的な動きを説明すると
・unko filterというコマンドを設定すると、ebgp限定で自作attrがbgp attrとして組み込まれます。
・そしてBGP Path Attributeとして機能する。
をゴールにします

今回作るunko filterで追加される自作attrの構成

属性flag:Optional non-transitive(0x80) 
type code:NISEBGP(0x42)  
len:0x05
data:"toire"(文字列型)  

実装編

必須アイテム:zlog_debug()

気になる所は全部zlog_debugすること

step1 データを定義する

1:FRRではBGP attrの属性はbgp_attr.hで以下のように定義されています https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.h#L45

/* BGP Attribute flags. */
#define BGP_ATTR_FLAG_OPTIONAL  0x80    /* Attribute is optional. */
#define BGP_ATTR_FLAG_TRANS     0x40    /* Attribute is transitive. */
#define BGP_ATTR_FLAG_PARTIAL   0x20    /* Attribute is partial. */
#define BGP_ATTR_FLAG_EXTLEN    0x10    /* Extended length flag. */

2:bgpd.hにtype codeの定義がされています https://github.com/FRRouting/frr/blob/master/bgpd/bgpd.h#L1733

/* BGP4 attribute type codes.  */
#define BGP_ATTR_ORIGIN                          1
#define BGP_ATTR_AS_PATH                         2
#define BGP_ATTR_NEXT_HOP                        3
....

3:FRRではattrのデータを管理するためにbgp_attr.hにattrという構造体を作り管理しています。 https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.h#L142

/* BGP core attribute structure. */
struct attr {
...

本格的に実装するとhashを作っていくことになります

1で属性でどんなものが定義されているかを確認します。

2でtype codeを書きます。
今回はtype codeとして NISEBGP(0x42)を追加する予定です。
bgpd.hにマクロを定義します。

...
#define BGP_ATTR_SRTE_COLOR                     51
#define NISEBGP                                 42 // 追加した場所

3で構造体の中に作りたいデータを定義します。
構造体に送信する用の文字列型を定義しておきます

struct attr {
    /* AS Path structure */
    struct aspath *aspath;
        
        char gehin[8];  // 追加した場所

step2 定義したものを整える

1:type codeのロギングをするための構造体 https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.c#L60

/* Attribute strings for logging. */
static const struct message attr_str[] = {
    {BGP_ATTR_ORIGIN, "ORIGIN"},
...

2:恐らく不正なtype codeを排除するためのもの https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.c#L1244

switch (args->type) {
...

3:type codeと属性を結び合わせています https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.c#L1363

const uint8_t attr_flags_values[] = {
    [BGP_ATTR_ORIGIN] = BGP_ATTR_FLAG_TRANS,
...

1は先程type codeを定義したため、ロギング用に追加しておく必要があります。

static const struct message attr_str[] = {
    {BGP_ATTR_ORIGIN, "ORIGIN"},
    {BGP_ATTR_AS_PATH, "AS_PATH"},
    {BGP_ATTR_NEXT_HOP, "NEXT_HOP"},
    {BGP_ATTR_MULTI_EXIT_DISC, "MULTI_EXIT_DISC"},
    ...
    {BGP_ATTR_PREFIX_SID, "PREFIX_SID"},
    {BGP_ATTR_IPV6_EXT_COMMUNITIES, "IPV6_EXT_COMMUNITIES"},
    {NISEBGP, "NISEBGP"}, // 追加した場所
    {0}};

2はとりあえず追加しておきます

 switch (args->type) {
    ....
    case BGP_ATTR_ORIGIN:
    case BGP_ATTR_AS_PATH:
    case BGP_ATTR_NEXT_HOP:
    case BGP_ATTR_MULTI_EXIT_DISC:
    case BGP_ATTR_LOCAL_PREF:
    case BGP_ATTR_COMMUNITIES:
    case BGP_ATTR_EXT_COMMUNITIES:
    case BGP_ATTR_IPV6_EXT_COMMUNITIES:
    case BGP_ATTR_LARGE_COMMUNITIES:
    case BGP_ATTR_ORIGINATOR_ID:
    case BGP_ATTR_CLUSTER_LIST:
        case NISEBGP:                                    // 追加した場所
        return BGP_ATTR_PARSE_WITHDRAW;
    case BGP_ATTR_MP_REACH_NLRI:
    case BGP_ATTR_MP_UNREACH_NLRI:
        bgp_notify_send_with_data(peer, BGP_NOTIFY_UPDATE_ERR, subcode,
                      notify_datap, length);
        return BGP_ATTR_PARSE_ERROR;
    }

3ではtype codeと属性を結び合わせる必要があるので、書きます。

const uint8_t attr_flags_values[] = {
    [BGP_ATTR_ORIGIN] = BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_AS_PATH] = BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_NEXT_HOP] = BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_MULTI_EXIT_DISC] = BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_LOCAL_PREF] = BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_ATOMIC_AGGREGATE] = BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_AGGREGATOR] = BGP_ATTR_FLAG_TRANS | BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_COMMUNITIES] = BGP_ATTR_FLAG_TRANS | BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_ORIGINATOR_ID] = BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_CLUSTER_LIST] = BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_MP_REACH_NLRI] = BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_MP_UNREACH_NLRI] = BGP_ATTR_FLAG_OPTIONAL,
    [BGP_ATTR_EXT_COMMUNITIES] =
        BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_AS4_PATH] = BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_AS4_AGGREGATOR] =
        BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_PMSI_TUNNEL] = BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_LARGE_COMMUNITIES] =
        BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_PREFIX_SID] = BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [BGP_ATTR_IPV6_EXT_COMMUNITIES] =
        BGP_ATTR_FLAG_OPTIONAL | BGP_ATTR_FLAG_TRANS,
    [NISEBGP] =
        BGP_ATTR_FLAG_OPTIONAL,  // 追加した場所

};

step3.1 送信する準備

1:ここからattrの送信を行います。 https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.c#L3748

/* Make attribute packet. */
bgp_size_t bgp_packet_attribute(struct bgp *bgp, struct peer *peer,
...

小ネタ FRRでのパケットを送信する際の管理方法

FRRではパケットを送信する際、オリジナルな関数から送信する仕組みになっています。

https://github.com/FRRouting/frr/blob/master/lib/stream.h#L182

/* steam_put: NULL source zeroes out size_t bytes of stream */
extern void stream_put(struct stream *, const void *, size_t);
extern int stream_putc(struct stream *, uint8_t);

凄く厳重に決められていて、大体はそれぞれの型にあった関数を使って送信することになっています。 オリジナルで送信したい場合はsizeを調整出来て自由に型を入れることが出来るstream_put()を使っていきます。

step 3.2 送信する準備

とりあえず属性とtype codeを決めたので条件式を作って、その中に属性、決めたtype codeをbgp_attr.cのbgp_packet_attribute()の中でstream_putc()します。

アドレスファミリで条件式を作ったり、相手がIBGP,EBGPだったら、、みたいな感じで条件式を作っていくと楽しいと思います。
EBGPのみで反応させたかったので、peerがEBGPだった場合という条件式を作りました。

        if (peer->sort == BGP_PEER_EBGP) {
          if (attr->gehin){
            stream_putc(s, BGP_ATTR_FLAG_OPTIONAL);
            stream_putc(s, NISEBGP);

https://github.com/FRRouting/frr/blob/master/bgpd/bgpd.h#L897

typedef enum {
    BGP_PEER_UNSPECIFIED,
    BGP_PEER_IBGP,
    BGP_PEER_EBGP,
...

FRRではBGPピアの状態をbgp_peer_sort_tとして管理しています。

step 4 データを書き込む場所の確認

もう結構ノリで作っていきます、とりあえず動けば勝ちです。

実際にデータを作成する場合、bgp_attr.cでは値を入れていません。 bgp_attr.cではあくまで定義したり、送受信の役割を持っているだけなので実際にデータを入れたりするのはよく分かっていません。

それぞれ追うのが大変ですが、今回はRRの際に入ってくるattr、ORIGINATOR_ID属性を元にして実装します。

bgp_route.cにはrrが有効になっているかをチェックをして、値を入れたりする関数がsubgroup_announce_check()という関数で管理されています。 今回は同じような仕様でこの場所に関数を入れていきます。

参考例:

github.com

ここに書いていくのですが、ここでそろそろコマンドを作っていきます。

step 5 vtyshにコマンドを作成する

FRRではvtyshというデーモンを総括するシェルが存在します。 ここでconfigしたり、ピアが通っているかなど色んな確認が出来ます。 bgpdではvtyshのコマンドを管理しているのはbgp_vty.cです。

github.com

DEFUN_YANG()みたいな名前の関数を使っていきます。 なんかここはめっちゃ種類がありますが、よく分かってません。

今回、unko filterという名前のコマンドを作りたいので、bgp_vty.cに書き込んでいきます。
コマンドの内容としては、bgpという名前の構造体にbool型のオリジナルな値を入れてFRRがコマンドを確認すると、trueになってCMD_SUCCESSを返すだけにしました。

DEFUN_YANG(ogehin_unko,
    ogehin_unko_cmd,
    "unko filter",
    "unko_test"
    )
{
   VTY_DECLVAR_CONTEXT(bgp, bgp);
   bgp->unko = true;
   vty_out(vty, "gehinha dame\n");
   return CMD_SUCCESS;
}
/* BGP instance structure.  */
struct bgp {
        ...
        bool unko;

vty_out()などでコマンドが動くか確認しておくと便利だと思います。

DEFUN〜()の中に_cmdみたいな感じで書いたコマンドをinstall_element()でFRRに認識させます。 第一引数にどのタイミングでコマンドを入れるかを教えてあげます。 今回はrouter bgp ASを入れた後にコマンドを入れたいので、BGP_NODEと書いてあげます。

        ...
        install_element(BGP_NODE, &ogehin_unko_cmd);
        ...

これでコマンドの導入が出来ました

step6 値を入れる

step4で紹介したsubgroup_announce_check()に内容を書いていきます。

自分が作ったfilterはコマンドが確認されるとbgpという名前の構造体の中のオリジナルな値がtrueになるという仕組みでした。この仕組みを使って条件式を作っていきます。

        ...
    int transparent;
    int reflect;
        int nise_check;
        ...

        if (bgp->unko)
                nise_check = 1;
        else
                nise_check = 0;

そして、nise_checkが確認されると、データが代入されます。

 if (nise_check
        && (!(attr->flag & ATTR_FLAG_BIT(NISEBGP)))) {
            strcpy(attr->gehin, "toire");
            SET_FLAG(attr->flag, NISEBGP);
        }

これで送信する事が可能になりました。

step7 データの送信

step 3.2で書いた条件式の中にデータの長さと、データをstream_putします。 上から書いた順番順にstream_putされます。

        if (peer->sort == BGP_PEER_EBGP) {
          if (attr->gehin){
            stream_putc(s, BGP_ATTR_FLAG_OPTIONAL);
            stream_putc(s, NISEBGP);
            stream_putc(s, 5);
            stream_put(s, &attr->gehin, 5);

          }

送信した値はzlog_debug()で確認するといいと思います。

zlog_debug("%d", attr->gehin);

step8 パースする

FRRではこのようにtype codeでcaseを作ってパースをしていきます。

https://github.com/FRRouting/frr/blob/master/bgpd/bgp_attr.c#L3166

switch (type) {
        case BGP_ATTR_ORIGIN:
            ret = bgp_attr_origin(&attr_args);
...

そのため、NISEBGP君にも同じことをやってもらいます。

     case NISEBGP:
            ret = nise_parse(&attr_args);
            break;

パースする関数は周りに合わせて書きます。

static bgp_attr_parse_ret_t
nise_parse(struct bgp_attr_parser_args *args)
{

        struct attr *const attr = args->attr;
    struct peer *const peer = args->peer;
        struct stream *s;

        s = BGP_INPUT(peer);

        stream_get(&attr->gehin, s, 5);
    attr->flag |= ATTR_FLAG_BIT(NISEBGP);
        zlog_debug(":parse:%s", attr->gehin);
    return BGP_ATTR_PARSE_PROCEED;
}

zlog_debug()で値を確認するといいと思います。 これにて実装は終わりです。

小ネタ2 FRRのattrパース

FRRでは送信する場合と同じようにstream_get()みたいな関数を使って受信します。

https://github.com/FRRouting/frr/blob/master/lib/stream.h#L208

extern void stream_get(void *, struct stream *, size_t);
extern bool stream_get2(void *data, struct stream *s, size_t size);

topotestを作って、ログを見る

導入方法!使い方!

docs.frrouting.org

FRRでは様々なケースに合わせてネットワークトポロジーを作る事が出来ます。それがtopotestsと呼ばれます。

以下に様々なケースがあると思います。

github.com

デバッグしたり、設定やトポロジーはどうやって構成されているかなどを確認出来る、FRRの心強い味方です。

今回はシンプルに r1<->r2でebgpでピアを取って経路広報するだけのネットワークを作ります。 構成は

init.py r1 r2 r3 r4 test_ebgp.py

それぞれ機器として設定するフォルダを用意して、その中に

bgpd.conf
zebra.conf

といった感じで設定します。

ここで、bgpd.confの中に実装したフィルターを入れてみることが出来るので、書き込みます

!
debug bgp updates in
debug bgp updates out
!

router bgp 65001
  no bgp ebgp-requires-policy
  unko filter
  bgp router-id 1.1.1.1
  neighbor 192.168.10.1 remote-as 65000
  network 10.0.1.0/24
!

zebra.confでIPアドレスなどの設定も可能です

!
interface r1-eth0
 ip address 192.168.10.2/24
!

!
interface r1-eth1
 ip address 10.0.1.1/24
!
ip forwarding
!

といった感じで設定します

test_○○.pyの書き方について

注意することとして実行する際にtest_○○.pyではないと反応してくれません。 以下は今回のケースでのtestの書き方です。

import os
import sys
import json
import time
import pytest
import functools

CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(CWD, "../"))

# pylint: disable=C0413
from lib import topotest
from lib.topogen import Topogen, TopoRouter, get_topogen
from lib.topolog import logger
from mininet.topo import Topo


class TemplateTopo(Topo):
    def build(self, *_args, **_opts):
        tgen = get_topogen(self)
        tgen.add_router("r1")
        tgen.add_router("r2")
        tgen.add_router("r3")
        tgen.add_router("r4")
        tgen.add_link(tgen.gears["r1"], tgen.gears["r2"], "r1-eth0", "r2-eth0")
        tgen.add_link(tgen.gears["r1"], tgen.gears["r3"], "r1-eth1", "r3-eth0")
        tgen.add_link(tgen.gears["r2"], tgen.gears["r4"], "r2-eth1", "r4-eth0")


def setup_module(mod):
    tgen = Topogen(TemplateTopo, mod.__name__)
    tgen.start_topology()

    router_list = tgen.routers()

    for i, (rname, router) in enumerate(router_list.items(), 1):
        router.load_config(
            TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
        )
        router.load_config(
            TopoRouter.RD_BGP, os.path.join(CWD, "{}/bgpd.conf".format(rname))
        )

    tgen.start_router()
    tgen.mininet_cli()

def teardown_module(mod):
    tgen = get_topogen()
    tgen.stop_topology()

def test_shutdown_check_stderr():
    if os.environ.get("TOPOTESTS_CHECK_STDERR") is None:
        pytest.skip("Skipping test for Stderr output and memory leaks")

    tgen = get_topogen()
    # Don't run this test if we have any failure.
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    logger.info("Verifying unexpected STDERR output from daemons")

    router_list = tgen.routers().values()
    for router in router_list:
        router.stop()

        log = tgen.net[router.name].getStdErr("bgpd")
        if log:
            logger.error("BGPd StdErr Log:" + log)
        log = tgen.net[router.name].getStdErr("zebra")
        if log:
            logger.error("Zebra StdErr Log:" + log) 

if __name__ == "__main__":
    args = ["-s"] + sys.argv[1:]
    sys.exit(pytest.main(args))

tgen.mininet_cli()でmininetのシェルに入ることが出来ます。 ここで、r1 vtyshなどと打ち込むと、vtyshに入ることが出来ます。

make、実行

全てが終わったらmakeします。

frr/bgpdでmake

frr/ でmake install

systemctlでfrrを管理しているなら systemctl restart frrで更新をします

実際に実行すると、/tmp フォルダにtopotestという名前のログファイルが作成されます。

topotestに入ると自分の実行したtestのフォルダを見つけて、機器ごとにフォルダが並んでいるのを確認します。

機器のフォルダに入るとそれぞれのログファイルが存在します。 bgpd.logを見ればzlog_debug()の結果などがログに残されているので、値を確認することが出来ます。

ここでパースされているかを確認することが出来ます。 コマンドを入れた時
f:id:eniyEniy:20210628010757p:plain
送信時
f:id:eniyEniy:20210628010926p:plain
パース時
f:id:eniyEniy:20210628010950p:plain

終わり!

これはあくまで改造しただけなので、めちゃくちゃ実装です。
参考に、、出来ないと思います。