SR-Policyについて&FRRを改造してSRv6 Policyという機能を作っています

FRRを改造したり、FRRを使用して機能を解析して楽しんだりするのが最近のマイブームなのですが、今回はFRRにSRv6 PolicyというSegment Routingの拡張機能のようなものを作っているのでブログにしてみました。
SR-TEについて解説した後に、SRv6 Policyがどういったもので、FRRにどう実装していったのかを解説します。
私はTrafic Engineeringに非常に興味があって、このSRv6 PolicyもTEの分野で使用される技術です。
分からない事も多くて、今の知識で書いていますがもしなにかあればどんどんご指摘お願いします
この機能がどんな機能なのか、またはどういった動きをするのかを書いていこうと思います。


本記事ではこのドラフトとスライドを参考にしながら作成しています。


datatracker.ietf.org


https://www.segment-routing.net/images/SRTE_TOI_dev_v20a_EXTERNAL.pdf

SR-Policyとは?

Segment Routing(以下 SRと呼びます)を使った拡張機能であり、特定の目的を果たすために作成されたSegment Routing技術の一つです。
特定の目的というのは単純にSRを使用したTraffic Engineeringであったり、SRを使用したネットワークでSDNコントローラーなどを使用してtopologyの情報を収集した後にコントローラーから経路を受け取る際にSR-Policyを使用したりと様々です。(他にはFlex-Algoと呼ばれる技術もパケットを送信する際にSR-Policyを使用したりします。)
SRを使用しているということで、使用されるData-planeはSR-MPLSとSRv6です。それぞれSRの強みを活かした柔軟で自由な経路制御が可能になります。
SR-Policyは様々な要素が組み合わさって上記の様な技術を実現可能にします。その要素については後ほど解説します。
ちなみにSR-TE Policyともいうらしいです。

SR-Policyを構成する要素

ここではSR-Policyを構成する代表的な要素を紹介します。

Head-end

SR-Policyを構成する要素の一つで、Head-endはSR-Policyが作成される場所を示していています。
実際に入るような値ではなく概念のようなものです。

Color

SR-Policyを識別するための数値であり、SR-Policyにとって重要な要素です。任意に決めることもできれば、自動で決めることも出来ます。
広報された経路にSR-Policyを識別させるために色を付けるようなイメージです。
MP-BGPを使用してColor communityとして経路にcolor値として広報することもできれば、Route-mapを使用した経路の色付けが可能になります。
今回使用するFRRにはまだColor communityは実装されていないので後者のやり方になります。

End-point

SR-Policyの宛先として、IPアドレスで指定されます。

Candidate-path

候補パスと呼ばれています。
SR-Policyは必ず一つ以上の候補パスに関連付けられています。ここから紹介する要素は全て候補パスに含まれています。
この候補パスにはSR-Policyを明示的(explicit)なパスにするか、動的(dynamic)なパスにするかを決めることが出来ます。
明示的というのは「この名前のSID-Listを使うぞ!」と管理者自身が決めることです。
動的に決めるのは自動的にSID-LIstを受け取るような選定方法であり、BGP-LSなどが適用されているネットワークでPCEPを用いたコントローラーでSID-Listを作成してもらう場合に使用します。

Binding-SID

Binding-Segmentの識別子です。Binding-segmentとはSRの拡張機能でSRのSegmentの一つです。特定のSIDを含むパケットが流れてくると、その特定のSIDとSID-Listが経路テーブルに登録してある該当するSID-Listをパケットにencapする機能です。SR-Policy単位で管理することが可能なため様々なユースケースに使用されます。
経路を受け取り、SR-PolicyがAvtiveになるとSR-Policyが持っているBinding-SIDとSID-Listが経路テーブルに登録する仕組みになっています。

preference

SR-Policy内のSID-LIstの優先順位を決める値で、大きいほどそのSID-Listは優先されます。

SID-list

SID-listというのはそのままの意味で、SRで使用されるSegmentを識別するために使用されるSIDのリストです。

その他の要素

SID-Listに付けられるWeightという要素やそのパスがどう作られたのかを示すProtocol-Originという値もあります。

これまで紹介したSR-Policyの要素をまとめて、図にまとめるとこうなります。

f:id:eniyEniy:20220404012106p:plain

このCPathの中にSID-LIstやBSID、preferenceなどの情報が含まれています。
またここからは実際にSR-TEというSR-Policyを使用した技術を動かしながらこのSR-Policyがどう動くのかを説明します。

SR Policyがどうやって動くのかを検証する

今回FRRの中にあるtopotestを使用して検証します。topotestというのはFRRの中にあるFRRに実装されてある機能をテストするためにネットワークを作成して自動的にテストしてくれるような機能です。
その機能がどうやって動いているかを確認したり、Debugに使用されます。
github.com

topotestの内容としてはSR-Policyを機能させ、SR-TEを実現するtopotestです。
IS-ISでSRの設定を行いSR-MPLSを動かした後、BGPで受け取った経路を使用します。
github.com

Step 1 IGPを使用してSIDを配布する

以下の図は、IS-ISを使用しSR-MPLSでのルーティングを行うためのPrefix-SIDAdjacency SIDを広報しています。
f:id:eniyEniy:20220405213305p:plain
Prefix-SIDとはSRGB(Segment-Routing Global-Block)で決めたそのSIDが広報された場所に辿りつくためのSIDです。
例えばこの画像では

segment-routing prefix 1.1.1.1/32 index 10

と設定しているのが分かると思います。この部分でPrefix-SIDがどのようなNexthopを持って、どのようなindexなのかをIGP内で共有します。
このPrefix-SIDにはSIDを決める範囲があり、それをSRGBで決定します。SRGBを設定している部分はこれです。

segment-routing global-block 16000 23999

このSRGBの範囲設定と先程のindexの値によりPrefix-SIDが決定されます。
Adjacency SIDとはそのままの意味で、Adjacency(隣接)に対するSIDです。
管理者自身で決める事は出来ずIGP自体がその値を決めることになります。

同じように各ルーターにIS-ISを使用して配布した後の経路テーブルの状態とTopologyの状態を図にまとめるとこうなります。
f:id:eniyEniy:20220405213523p:plain
ルーターに16010, 16020とSIDが割り当てられています。この値がPrefix-SIDになります。
そして経路テーブルにはそれぞれ配布したPrefix-SIDとnexthopの情報が登録されているのが分かります。

Step2 配布されたSIDを使用してSID-Listを作成し、SR-Policyを定義する。

ここで作成するSID-listを使用してSR-Policyに結びつけることになります。
作成するSID-listは存在するSIDで構成しなければいけません。

f:id:eniyEniy:20220405220838p:plain
この図で作成したSID-listは、segment-list testとSID-listに名前を付け、index 10 mpls label 16050index 20 mpls label 16060と定義しています。

segment-list test
index 10 mpls label 16050
index 20 mpls label 16060

次に、SR-Policyを定義するためのcolorとend-pointを指定します。

policy color 1 endpoint 6.6.6.6

今回のケースでは任意にcolorの値を決めるため、1と設定します。end-pointはSR-Policyの宛先です。
end-pointはSR-Policyを使用して、どこまで届くのかを決めるポイントになります。そしてIP アドレスで指定することになっているため6.6.6.6です。
head-endは概念的な要素なため、このように実際に入るような値ではありません。

最後に候補パスを設定します。
SR-PolicyがどんなSID-listを登録しているかを示すBSIDや、パスが明示的か動的かを決め、どのパスを使用するかを決めるpreferenceを設定します。
なおFRRではSR-Policyに名前を付けて管理することが必須です。それがnameという要素なのですが、管理者がデバッグトラブルシューティングを快適に行い、わかりやすく管理してもらうための要素です。
ドラフトにも記述があります。
datatracker.ietf.org

An implementation MAY allow assignment of a symbolic name comprising
of printable ASCII characters to an SR Policy to serve as a user-
friendly attribute for debug and troubleshooting purposes. Such
symbolic names may identify an SR Policy when the naming scheme
ensures uniqueness.

nameという要素は

candidate-path preference 100
name test explicit segment-list test

のname testの部分と

name default

の部分です。

BSIDはSR-MPLSの場合だとlabel形式ということになっているので、labelで指定します。1111というlabelで設定します。
今回のケースではコントローラーなどを使用せずに、そのままのSID-listを使用したいため明示的(explicit)にします。
preferenceは値が大きいほどパスが有効になります。今回は単一で100と設定していますが、例えば200300と設定するとそのパスの方が優先度が高くなり、該当する経路のSID-listが変更されます。

そしてこれまで出来たSR-Policyを図にまとめます。
f:id:eniyEniy:20220405233122p:plain

Step3 BGPから受け取った経路に色を付け、SR-Policyを使用して経路テーブルに登録する。

今回はEnd-pointに指定しているルーターとiBGPで経路を受け取り、その経路に色を付けてSR-Policyを適用していきます。

FRRの場合、まだBGP Color communityが実装されていないのでBGPのroute-mapを使用して経路に色を付けます。
他にもこの経路を効率的に色を付ける方法にODN(On-Demand Nexthop)があります。これはまだ現時点ではFRRに実装されていません。
f:id:eniyEniy:20220408155207p:plain

route-mapを見てみると

route-map SET_SR_POLICY permit 10
set sr-te color 1

とcolorを指定しています。ここでSR-Policyに定義しているcolorと結びつけて広報されてきた経路に色を付けるということが出来ます。

f:id:eniyEniy:20220408163101p:plain

最終的にはこのような流れでSR-Policyが適用され、該当する経路と通信を行うことが出来ます。
このようにSR-Policyは、colorの値やend-pointをPolicyの識別子とし、SID-Listを効率的に管理するような仕組みが出来ています。

SRv6 Policyとは?

SRv6 Policyとは、その名前の通りSRv6版のSR-Policyです。
上記のSR-Policyの例ではSR-MPLSを使用した方法でしたが、SR-PolicyはSR-MPLSもSRv6も使用する事が出来ます。
SR-MPLSの挙動と対して変わることはないのですがSR-MPLSとの違いについて解説します。

Head-endでの経路のカプセル化の違い

SR-MPLSではMPLSのラベルをパケットに含むカプセル化を行っていましたが、SRv6では指定したSID-Listのカプセル化を行うFunctionが存在します。
簡潔に説明すると、H.encapsと呼ばれるfunctionはTranzit Nodeで実行する事が出来るSRv6の機能で、指定したSID-Listを経路に結びつけることが出来る機能です。

www.rfc-editor.org

SR-MPLSではこのように登録されていましたが

9.9.9.2 encap mpls  16050/16060 via 10.0.1.3 dev eth-sw1 proto 186 metric 20

SRv6ではこうなります。

9.9.9.2 encap seg6 mode encap segs [3::3, 5::5, 6::6] via 10.0.1.3 dev eth-sw1 proto 186 metric 20 

ちなみにBSIDもSRv6 SIDに対応したSIDで登録することになっています。
datatracker.ietf.org

When the active candidate path has a specified BSID, the SR Policy
uses that BSID if this value (label in MPLS, IPv6 address in SRv6)

つまりはこんな感じです

9::9 encap seg6 mode encap segs [3::3, 5::5, 6::6] via 10.0.1.3 dev eth-sw1 proto 186 metric 20 

FRRに実装をしてみる

FRRにはそれぞれ、bgpd、ospfdなどといったルーティングデーモンが動いていますがSR-Policyを動かしているデーモンはpathdというデーモンです。

ユーザー向けのdoc
docs.frrouting.org

開発者向けdoc
docs.frrouting.org

この開発者向けdocともうすでに追加されているSR-MPLS SR-Policyを参考にしながら実際にコードを追加していきます。
以下では、実際に動かしながらどういった実装を行っているかを一部解説します。

実装してみる

CLIの追加

SID-Listに新しい要素を追加するため、pathdのCLIにSRv6 SIDに対応したフォーマットを作成します。
SR-MPLSの場合のフォーマットはSID-Listにlabelを追加しますが

# rt1(config-sr-te)# segment-list test
# rt1(config-sr-te-segment-list)# index 10 mpls label 16050

SRv6ではIPv6 address形式のフォーマットで作ります。

# rt1(config-sr-te)# segment-list test
# rt1(config-sr-te-segment-list)# index 10 srv6 fc00:2::1

実際に設定を入れて反映させてみます
f:id:eniyEniy:20220410205311g:plain

pathdのCLIの設計は、Northbound gRPCというYANGを使用してデータを操作する実装が行われています。
Northbound gRPCにはAPIが用意されておりそれぞれのデータを扱う場合はxpathを指定してやり取りすることになっています。
docs.frrouting.org

このようにXpathを指定した後にnb_cli_enqueue_change()という関数で第二引数にpath、第三引数にcallbackの処理を書くようになっています。

if (has_srv6 != NULL) {
		struct prefix prefix_cli = {};
		char buf_prefix[INET6_ADDRSTRLEN];
		str2prefix(prefix_str, &prefix_cli);
		inet_ntop(AF_INET6, &prefix_cli.u.prefix6, buf_prefix,
			  sizeof(buf_prefix));
		snprintf(xpath, sizeof(xpath),
			 "./segment[index='%s']/srv6sid", index_str);
		nb_cli_enqueue_change(vty, xpath, NB_OP_MODIFY, buf_prefix);
		return nb_cli_apply_changes(vty, NULL);
	}

callbackに指定する処理はYANG treeに対する処理であり以下のような定数を指定することが出来ます。

enum nb_operation {
	NB_OP_CREATE,
	NB_OP_MODIFY,
	NB_OP_DESTROY,
	NB_OP_MOVE,
	NB_OP_PRE_VALIDATE,
	NB_OP_APPLY_FINISH,
	NB_OP_GET_ELEM,
	NB_OP_GET_NEXT,
	NB_OP_GET_KEYS,
	NB_OP_LOOKUP_ENTRY,
	NB_OP_RPC,
};

YANG treeを使用して要素を消したり、増やしたりという事が出来ます。
この他にもYANGを追加したり、NB_APIの処理を書いたりします。
FRRの一部の機能ではフロントエンドにこのような機能を追加しています。

候補パスを設定する

CLIの追加が可能になれば、中身を作ります。
SR-Policyなどを定義している構造体に追加したSRv6 SIDを入れるため新しく作ります。
Zebraとは、FRRを動かしているコア的な存在であり、各ルーティングプロセスとのやり取りを支える重要な役割を持っています。
Netlinkを通じて経路テーブルに指示を送っているのもZebraが行っています。
ZebraにはZclient(クライアント側、ルーティングプロセス)とZserver(サーバー側、コアのZebraの部分)がZAPIを使用してやり取りを行っています。
FRRの肝と言える部分なのでとても大切です。
今回は新しく追加した要素を加えたSR-PolicyをZebraを使用して各ルーティングプロセスとやりとりするような処理を書いていきます。

void path_zebra_add_sr_policy(struct srte_policy *policy,
			      struct srte_segment_list *segment_list)
{
	struct zapi_sr_policy zp = {};
	struct srte_segment_entry *segment;

	zp.color = policy->color;
	zp.endpoint = policy->endpoint;
	strlcpy(zp.name, policy->name, sizeof(zp.name));
	zp.segment_list.type = ZEBRA_LSP_SRTE;
	
	//zp.segment_list.local_label = policy->binding_sid;
	zp.segment_list.label_num = 0;
	zp.segment_list.num_seg = 0;
	RB_FOREACH (segment, srte_segment_entry_head, &segment_list->segments)
		zp.segment_list.sid[zp.segment_list.num_seg++] = segment->sid;
	policy->status = SRTE_POLICY_STATUS_GOING_UP;

	(void)zebra_send_sr_policy(zclient, ZEBRA_SR_POLICY_SET, &zp);
}
struct zapi_srte_tunnel {
	enum lsp_types_t type;
	mpls_label_t local_label;
	uint8_t label_num;
	mpls_label_t labels[MPLS_MAX_LABELS];
	struct in6_addr sid[256];
	struct in6_addr local_srv6_sid;
	uint8_t num_seg;
};

struct zapi_sr_policy {
	uint32_t color;
	struct ipaddr endpoint;
	char name[SRTE_POLICY_NAME_MAX_LENGTH];
	struct zapi_srte_tunnel segment_list;
	int status;
};

path_zebra_add_sr_policy()は候補パスが設定された際にZebraにSR-Policyを定義する関数です。
このstruct zapi_sr_policyという名前の構造体はSR-Policyを定義する時に使用する構造体です。この構造体の中にSRv6 SIDとSID-Listの長さの情報がほしいので追加します。
そしてzebra_send_sr_policy(zclient, ZEBRA_SR_POLICY_SET, &zp)ではSR-Policyの要素をパースした後、やり取りを行うStreamに入れた後にZAPIで命令を送ります。
受け取ったzserver側はハンドラを用意しており、ZEBRA_SR_POLICY_SETという定数を元にどんな処理を行えばいいかを決めています。

void (*const zserv_handlers[])(ZAPI_HANDLER_ARGS) = {
...
	[ZEBRA_SR_POLICY_SET] = zread_sr_policy_set,
...
}

そしてハンドラで指定された関数を元に要素のdecodeを行います

static void zread_sr_policy_set(ZAPI_HANDLER_ARGS)
{
	struct stream *s;
	struct zapi_sr_policy zp;
	struct zapi_srte_tunnel *zt;
	struct zebra_sr_policy *policy;

	/* Get input stream.  */
	s = msg;
	if (zapi_sr_policy_decode(s, &zp) < 0) {
          if (IS_ZEBRA_DEBUG_RECV)
			zlog_debug("%s: Unable to decode zapi_sr_policy sent",
				   __func__);
          return;
       }

このようにしてFRRはZAPIでのやり取りを行っており、ZebraはSR-Policyを管理する事が可能になります。
このdecodeした要素を上手く使用してSRv6でも動くように中身を作っていきます。

次は色付き経路をMPLS形式ではなく、H.encapsで対応させていきます。
FRRでは、経路を決定する際にNexthopを定義する構造体を使用して経路テーブルに登録する事になっています。

例えばその中にはinterfaceのindexを指定するのは

struct nexthop {
...
   ifindex_t ifindex;
...
}

という要素があったり、経路に対するNexthopのアドレスを指定する要素ももちろんあります。
このnexthopの構造体の中にSRv6の情報を作っていけば良いので既存にある構造体を少し変えてZAPIでやり取りを行うようにします。

struct nexthop {
...
   struct nexthop_srv6 *nh_srv6;
...
}
struct nexthop_srv6 {
	/* SRv6 localsid info for Endpoint-behaviour */
	enum seg6local_action_t seg6local_action;
	struct seg6local_context seg6local_ctx;

	/* SRv6 Headend-behaviour */
	struct in6_addr seg6_segs;
	struct in6_addr seg6_multisegs[256];
	uint8_t num_segs;
};

seg6_multisegsとnum_segsという要素を追加します。
後はBGPの色付き経路に対するZAPIの処理側の関数を少し変えて、H.encapsするようになればそれっぽくなるはずです。

通信してみる

さてここまで色々やってきましたが実際に動かしてみます。
ここからはtopotestを使用して、SRv6っぽい通信をしてみます。
今回はこのようなtopologyを使用してrt4にEnd-actionを設定します。

f:id:eniyEniy:20220411151555p:plain

今回はdstのloである9.9.9.2/32の経路をend-pointからiBGPで広報し、rt1で色付けを行います。
まずはHead-end側、rt1で候補パスの設定をします。
使用するSID-Listはfc00:6::10だけのSID-Listで通信してみます。


f:id:eniyEniy:20220411152952g:plain


候補パスを設定してSR-PolicyがActiveになっているのが分かります。
FIBを見てみるとiBGPで受け取った経路がH.encapsされているのが分かります。
dstにdefault routeを追加して、rt1から9.9.9.2/32に向かってpingをしてみます。

f:id:eniyEniy:20220411153540p:plain


fc00:6::10に対するend-actionが実行されてpingが出来ています。
(帰りはちなみにSRv6ではないです…)

次にfc00:2::1とfc00:6::20を含んだSID-Listを作成して通信してみます。
rt2にSIDを上手くtranzitするように設定を投入し候補パスを設定します。


f:id:eniyEniy:20220411154429g:plain


これはrt2のeth-sw1に対するパケットキャプチャですが、SRHの中にfc00:2::1, fc00:6::20が含まれているのが分かります。


f:id:eniyEniy:20220411155030p:plain


rt4でend-actionが実行されて、pingが出来ています。


f:id:eniyEniy:20220411155707p:plain



最後にfc00:3::1とfc00:6::30を含んだSID-Listを作成します。
同じように候補パスを設定して、今回はprefrenceの値を変えて既存のSID-Listが変わるのを見てみます。

f:id:eniyEniy:20220411160644g:plain


また上記と同じようにrt3でパケットキャプチャをしてみるとrt3にfc00:3::1, fc6::30が含まれています。


f:id:eniyEniy:20220411161757p:plain

pingを送信します。


f:id:eniyEniy:20220411161848p:plain


おわりに

SR-Policyの解説から実装までざっくりと書いてみました。
SRv6 Policyもどきを実装してみて、FRRの既存のSR-Policy in SR-MPLSの実装であったり、FRRの実装を読み解きながら実装していくのはとても面白いです。
小さなtopotestで動かす事が可能になっている訳ですが、まだこのSRv6 Policyは仮の状態で実はBSIDの処理などが全然進んでいなかったり、リファクタリングや動的なパスでの動きは全然出来てないので
まだまだ課題はあります。
ここまで読んでくださりありがとうございました!