! This post is also available in the following language. 英語

【インターンレポート】FRRouting IS-IS SRv6 Extension 設計と実装に関して


こんにちは,9月6日から6週間,Verda室のネットワーク開発チームでインターンを行っていた橘直雪といいます.インターンのテーマとしてLINEがデータセンターで活用しているオープンソースのルーティングプラットフォームであるFRRのIS-IS daemonにSRv6機能の拡張を開発しました. この記事ではその成果についてお話します.

背景

LINEでは,数多いサービスのほとんどをVerdaというプライベートクラウドに収容しています.Verdaでは,ネットワーク分離のためにSRv6を利用しています.詳細については,以下のブログを御覧ください.

VerdaのSRv6はneutronのコントロールプレーンを用いて利用されていましたが,neutron側の負荷も大きく,ルーティングプロトコルでSRv6 BGP Control Planeを利用できるようにしたいというモチベーションがありました.SRv6のルーティングプロトコル拡張はオープンソース実装がなかったため,ネットワーク開発チームではFRRというオープンソースルーティングプラットフォームにSRv6 Data Planeの管理機能とBGP SRv6 Extensionを開発する貢献を行いました(詳細は FRRouting/frr/pull/5865 を御覧ください).我々が直近で採用するルーティングプロトコル拡張はBGPのL3VPN機能ですが,これに限らずSRv6自体のControl PlaneのOSS実装を増やしていくことで,SRv6のコミュニティに網羅的に貢献しつつ,盛り上げていきたいモチベーションがあります.これはコミュニティ活性だけでなく,SRv6の根本的な課題発見や改善につなげるような長期的な目標です.

そこで,今回私はSRv6の各社実装状況(draft-matsushima-spring-srv6-deployment-status-11#section-2)の中でBGPに次いで利用されているプロトコルであるIS-ISのSRv6 Extensionを実装しました.(参照しているドラフトはExpiredですが,公開情報の中では最も正確なものです)

IS-IS SRv6 Extension

SRv6とは

SRv6はSegment RoutingをIPv6で実現したものです.Segmentとはパケットに対する指示を表します.例えば「特定のノードを経由して転送する(Node Segment)」や「特定のルーターから隣接するルーターに転送する(Adjacency Segment)」などがあります.送信者がこのようなSegmentをパケットに対して自由に付与することで,パケットの流れる経路を自由に制御することができます.

ネットワーク内でSegmentを一意に識別するために,SID(Segment ID)と呼ばれるIDが割り当てられています.SRv6の場合,SIDはIPv6アドレス形式でエンコードされます.通るべきSegmentのSIDは,Segment Routing Headerと呼ばれるIPv6拡張ヘッダーの中に格納されます.SIDはLocator, Function, Argumentの3つのフィールドに分割されます.

Locatorは,SRv6 nodeを一意にするためのものです.Functionは,nodeでの処理を一意にするためのものです.SRv6 FunctionはRFC8986にてWell-KnownなFunctionが定義(以下表参照)されていますが,それに限らず定義が可能です.独自のFunctionを作成することもできます.Argumentは,Functionに対する引数を格納するためのものです.

Function概要用途
EndDestinationとSRHを書き換えて、Next-hopをRIBから探して送るNode Segment
End.XDestinationとSRHを書き換えて、決められたNext-hopへ送るAdjacency Segment
End.TDestinationとSRHを書き換えて、Next-hopを「指定されたRIB」から探して送るMulti VRF Operation
End.DT4SRHを外して、IPv4 Next-hopを「指定されたRIB」から探して送る(NH=4)VPNv4
End.DT6SRHを外して、IPv6 Next-hopを「指定されたRIB」から探して送る(NH=6)VPNv6

Locator, Function, Argumentの各bit長はRFC上では可変ですが,NOS側で一部制限がかかっていることもあるので注意が必要です.

参考: https://www.cisco.com/c/en/us/td/docs/iosxr/ncs5xx/segment-routing/72x/b-segment-routing-cg-72x-ncs540/configure-srv6.html#id_95420

IS-ISとは

IS-IS(Intermediate System to Intermediate System)は,Link Stateルーティングプロトコルであり, OSPF同様にIGPとして利用されています. IS-ISでは,ルーターのことをIS(Intermediate System)といいます.IS-ISは日本ではあまり馴染みのないプロトコルですが,海外のISPでは採用されている他,新しい拡張機能などはOSPFよりもIS-ISのほうが実装スピードが早いなど,今後避けては通れないプロトコルになってくることが予想されます.

このセクションでは,IS-IS SRv6 Extensionを理解する上で重要なIS-ISのパケットフォーマット及びPDU(Protocol Data Unit)の種類,TLV(Type Length Value)の概念に関して少し説明します.IS-ISのパケットフォーマットは以下のようになっています.

IS-ISのPDUは大きく分けて4種類あります.

  • Hello PDU…IS同士でNeighbor確立に利用するためのPDU
  • LSP(Link State PDU)…Reachabilityを交換するためのPDU
  • CSNP(Complete Sequence Numbers PDU)…LSPの更新を認識するためのPDU
  • PSNP(Partial Sequence Number PDU)…LSP要求や確認応答などで使うためのPDU

基本的なIS-ISのNeighbor確立の流れは以下のようになります.

  1. Hello PDUを交換してNeighborを確立する
  2. LSPで経路情報を交換し,情報をLSDBに登録する
  3. CSNP,PSNPを用いてLSDB(Link State Data Base)の同期を定期的に行う

IS-ISでは,PDUの可変長フィールドに,IS固有の情報をTLV形式で付加して送信します.TLVで広告される情報は,例として以下のようなものがあります.その他のTLVに関しては,IANAのCode Points(https://www.iana.org/assignments/isis-tlv-codepoints/isis-tlv-codepoints.xhtml)を御覧ください.

TypeNameDescriptionReference
1Area addressISの所属するエリアアドレスISO 10589
2IS NeighborsこのISと隣接するISISO 10589
10Authentication LSPの認証情報ISO 10589
14LSPBufferSize受け取ることのできるPDUのサイズISO 10589
132IP Interface AddressIPv4インタフェースアドレスRFC1195
232IPv6 Interface Address IPv6インタフェースアドレスRFC5308

IS-IS SRv6 Extensionとは

IS-IS SRv6 Extensionは,IS-IS ネットワークで SRv6を利用する際に必要な拡張機能のことです.ドラフト(draft-ietf-lsr-isis-srv6-extensions)に詳細な説明が記載されています.思い切って要約すると「IS-ISネットワーク内でSRv6を利用するには,Locator,Node Segment,Adjacency Segmentの情報をPDUにのせて送信する必要がある.」ということになります.Locator TLV, End SID Sub-TLV(Node Segment), End.X SID Sub-TLV(Adjacency Segment)についてのフォーマットはドラフト内で定義されており,以下のようになっています.

SRv6 Locator TLV

SRv6 Locator情報をIS-ISネットワークに広報するには, SRv6 Locator TLVを用います, TLVのデータフォーマットを以下に示します. このTLVのTypeは27で, LSPのTLVとして広報されます. このTLVは複数のSRv6 Locatorを広報することができます.

一つ一つのLocatorは以下のデータフォーマットで広報されます. 実際のSRv6 LocatorのPrefixや, IGP内部でどのようなポリシーで利用されるかのアルゴリズム番号などが含まれます.

End SID Sub-TLV

Sub-TLVはTLVに付随して広報したい情報がある際に利用されるTLVです.Sub-TLV Typeは5, Locator TLVのSub-TLVとして広報されます.

End.X SID Sub-TLV

Sub-TLV Typeは43です.End.X SIDはAdjacency Segmentにつくものなので,IS reachability TLVのSub-TLVとして広報されます.

実装

ここでは,FRRの機能開発や拡張の参考になることを期待し,実際にどのような流れで開発を行ったかを紹介します.開発は以下の流れで行いました.

  1. FRRの設計を理解する
  2. ドラフトから完成状態を整理する
  3. テスト用トポロジーを開発する
  4. CLIを作成する
  5. 実際に中身を作る
  6. TLVの交換ロジックを実装する

それぞれのセクションについて詳解します.

FRRの設計を理解する

FRRのプロセス構造について

FRRは単一のプロセスで動作しているのではなく,コアとしてzebraが存在し,その上に各ルーティングプロトコルプロセス,そのすべてをCLI操作するためのvtysh…といったように,複数のプロセスで構成されています.zebraと各ルーティングプロトコルプロセスのやりとりには,ZAPIと呼ばれる専用のAPIが用いられています.ZAPIを用いる際,Zebra側をzserver, ルーティングプロトコルプロセス側をzclientと表現します.

SRv6 ManagerのLocator管理について

冒頭でも少し紹介しましたが,SRv6 Managerは,Zebra(FRRのコア部分)でSRv6情報を管理する機能です.SRv6 Managerは一つのLocatorを論理的にLocator Chunkとして分離し,それぞれのLocator Chunkごとに所有権を設定します.ルーティングプロトコルプロセスは複数存在しますが,Locator Chunkごとの所有者管理がZebraで行われているため,一つのLocatorを複数のルーティングプロトコルで共有することができます.

ルーティングプロトコルプロセスからのLocator利用要求は新たに追加されたZAPIを用いますが,ここではその説明を省略します.代わりにPRで追加されたソースコードもそこまで多くないので読んでみるとスッキリわかると思います.

参考: https://github.com/FRRouting/frr/pull/5865

ドラフトから完成状態を整理する

実装する機能についてのドラフトを読み,目指すべき状態を確認します.今回の場合は,LSP(Link State PDU)によってSRv6 Locator ,およびそこから確保されたEnd/End.X SIDが共有され,各routerからすべてのISの情報がLSDBから参照できるようになる状態を目指します.

このタイミングで,IS-IS SRv6 Extensionを実装したFRRの表面上の挙動(投入するコンフィグ,その結果期待される出力)もある程度想定しておきましょう.

  • 投入するコンフィグ
segment-routing
 srv6
  locators
   locator loc1
    prefix 2001:db8:f:1::/64
   !
  !
 !
!
router isis 1
 segment-routing srv6
  locator loc1 ! このisisdはloc1のprefixを用いてsidを切り出す
 !
!
  • 期待される出力(一部抜粋)
// RIB
R1> show ipv6 route
I*> 2001:db8:f:1::100/128 End
I*> 2001:db8:f:1::200/128 End.X 2001:db8:1:2::2
I*> 2001:db8:f:1::300/128 End.X 2001:db8:1:3::3
 
R1> show ipv6 route json
[
  "2001:db8:f:1::100/128": {
     "seg6local_action": "End"
  },
  "2001:db8:f:1::200/128": {
     "seg6local_action": "End.X",
     "nexthop6": "xx"
  },
  "2001:db8:f:1::300/128": {
     "seg6local_action": "End.X"
     "nexthop6": "xx"
  },
]
 
// LSDB
R1> show isis database detailed
r1-00-0. 
 SRv6 Locator: 2001:db8:f:1::/64
    End SID: 2001:db8:f:1::100/128 End
 Ext IS Reachability: xxx
    End.X SID: 2001:db8:f:1::200/128 (2001:db8:1:2::2)
    End.X SID: 2001:db8:f:1::300/128 (2001:db8:1:3::3)

テスト用トポロジーを開発する

テスト環境のトポロジーを作成します.FRRは,FRR同士のトポロジーと検証したいテストを登録すると,構成と検証を行ってくれるtopotestという機能を提供しています.FRR自体の機能を新鮮に保つために日々多数のテストトポロジーが追加されています. 

参考: https://github.com/FRRouting/frr/tree/master/tests/topotests

今回のIS-IS SRv6 Extensionでも,その機能を確認するために以下のようなトポロジーを作成します.R1-4でSRv6 Locatorを設定します.

CLIを作成する 

まずは中身を作る前に,コマンドをFRR側に認識させるために空のコマンドを実装します.

R1(conf)> router isis 1
R1(conf-isis)> segment-routing srv6 // これを表示できるようにする
R1(conf-isis-srv6)> locator loc1 // これを表示できるようにする

IS-ISのCLIはyang形式で管理されており,yangのpathに情報を追加することでshow CLIでの表示,設定の反映を可能にしています.

例えば,今回のSRv6の設定の場合はこのような形でyangファイルを追加します.

~~~~~
container segment-routing {
  description ""Segment Routing global configuration.";
  container srv6 {
    description "srv6 global configuration.";
    leaf locator {
      type string;
      dafault "default";
      description "locator name";
    }
  }
~~~~~

その上で,このyang treeを操作するためのインターフェースを定義します.これで,空のコマンドは完成です.

{
    .xpath = "/frr-isisd:isis/instance/segment-routing/srv6",
    .cbs = {
        .cli_show = cli_show_isis_sr_srv6, // show running-config でsegment-routing srv6を表示する
        .cli_show_end = cli_show_isis_sr_srv6_end,// show running-configでsrv6の設定項目を表示し終わったあとに,exitを表示する
        .create = isis_instance_sr_srv6_create,// yang treeにcontainer srv6を追加する
        .destroy = isis_instance_sr_srv6_destroy,// yang treeから container srv6を削除する
    },
    .priority = NB_DFLT_PRIORITY - 1,
},
{
    .xpath = "/frr-isisd:isis/instance/segment-routing/srv6/locator",
    .cbs = {
        .cli_show = cli_show_isis_sr_srv6_locator,// show running-config でlocator nameを表示する
        .modify = isis_instance_sr_srv6_locator_modify,// yang treeにleaf locatorを追加する
    },
},

あとは,それぞれのインターフェースを操作するための関数を作っていきます.

TLVの交換ロジックを実装する

まず,TLVのデータフォーマットを構造体としてFRRに登録します.以下に今回追加した3種のTLVのデータフォーマットと,構造体を示します.

Locator TLV:

struct isis_srv6_locator_info {
    struct isis_srv6_locator_info *next; // 次のLocatorへのポインタ
       
    uint32_t metric;
    uint8_t flags;
    uint8_t algorithm;
    uint8_t loc_size;
    struct in6_addr locator;
 
    uint8_t sub_tlv_len;
    struct isis_srv6_loc_subtlvs *subtlvs;
};

End SID Sub-TLV:

struct isis_srv6_sid_end {
    struct isis_srv6_sid_end *next;
 
    uint8_t flags; // type,lengthはTLV情報の送信の時に付加される
    uint16_t endpoint_behavior;
    struct in6_addr sids[SRV6_MAX_SIDS];// 現在のFRR SRv6にはSIDは16個まで取り付けられるという制約がある.
};

End.X SID Sub-TLV:

struct isis_srv6_sid_end_x {
    struct isis_srv6_sid_end_x *next;
 
    uint8_t flags; // type,lengthはTLV情報の送信の時に付加される
    uint8_t algorithm;
    uint8_t weight;
    uint16_t endpoint_behavior;
    struct in6_addr [SRV6_MAX_SIDS];
};

次はインターフェースの実装です.FRRのIS-ISにおいてTLVを広報するためには,5つのインターフェースを実装する必要があります.これらを実装すれば,あとはFRRのIS-ISの実装によって自動的に必要に応じてLSPにTLVを乗せて広報してくれます.

  • pack…TLVの情報をstreamに追加する
  • unpack…streamからTLVの情報を受け取る
  • copy…TLVの情報を持つ構造体を複製する
  • free…TLVの情報を持つ構造体のメモリ領域を開放する
  • format…TLVの情報をdatabase datailed CLIに表示する

ここでは例としてLoctator TLVのapack,unpackの実装の一部を紹介します.

pack:

static int pack_item_srv6_locator_info(struct isis_item *i, struct stream *s, size_t *min_len)
{
    struct isis_srv6_info *r;
~~~~~  
    stream_putl(s, r->metric); // metric情報をstreamに追加する.putlのlはlong.
    stream_putc(s, r-flags); // flags情報をstreamに追加する.putcのcはchar.
    stream_putc(s, r->algorithm); // algorithm情報をstreamに追加する.
    stream_putc(s, r->loc_size); // loc_size情報をstreamに追加する.
    uint8_t spl = (r->loc_size + 7) / 8;
    stream_put(s, r->locator, spl); // locator情報をstreamに追加する.
~~~~~
}

unpack:

static int unpack_item_srv6_locator_info(uint16_t mtid, uint8_t len, struct stream *s, struct sbuf *log, void *dest, int indent)
{
    struct isis_srv6_locator_info *rv;
    rv = XCALLOC(MTYPE_ISIS_TLV, sizeof(*rv)); // isis_srv6_locator_infoのサイズ分メモリ領域を確保する.
~~~~~
    rv->metric = stream_getl(s); // metric情報をstreamから受け取る.putlのlはlong.
    rv->flags = stream_getc(s); // flags情報をstreamから受け取る.putcのcはchar.
    rv->algorithm = stream_getc(s); // algorithm情報をstreamから受け取る.
    rv->loc_size = stream_getc(s); // loc_size情報をstreamから受け取る.
    uint8_t spl = (r->loc_size + 7) / 8;
    stream_get(&rv->locator, s, spl); // locator情報をstreamから受け取る.
~~~~~
}

動作検証

今回の動作検証では,検証用トポロジーのR1-R4に実際にLocatorを設定するコンフィグを投入し,RIBを参照した際に,SIDが確認できるかをtopotestで検証します.ここでは,R1を例に設定と検証内容を紹介します.

R1のコンフィグ例(一部抜粋)

// isisd.conf
router isis 1
 segment-routing srv6
  locator loc1
 
//zebra.conf
segment-routing
 srv6
  locators
   locator loc1
    prefix 2001:db8:f:1::/64

FRRには,RIBをJSON形式で出力する機能があり,topotestではその出力結果と,あらかじめ用意しておいた期待される出力結果を比較し,その内容が一致しているかどうかでテストのPass/Failを決定します.テストのためには,期待される出力のJSONが必要です.今回の場合は,以下の内容が期待されます.

  • 1つのPrefixに seg6local action Endが設定されていること(End SID)
  • 2つのPrefixに seg6local action End.Xが設定されていること (End.X SID)

テスト結果:

また, 今回実装したTLVが実際にPDUの一部として広報されているかも見てみましょう.topotestには,テストを行わずにテストトポロジーだけを展開するオプションがあります.このオプションを用いてトポロジーを展開し,ip linkコマンドを用いてR1のeth0に擬似的なリンク障害を発生させ,疎通復旧時に流れるLSPをpcapして覗いてみます.

まずはLocator. ドラフトに示されるとおり,TLV number 27として広報されていることがわかります.

次にEnd SID.SRv6 Locator TLVのsub-TLV 5として広報されています.また,R1 のLocatorである2001:db8:f:1::/64からSIDを確保していることもわかります.

最後にEnd.X SID. MT IS Reach TLVのsub-TLV 43として広報されています.また,R2とR3へのEnd.X SIDそれぞれがR1 のLocatorである2001:db8:f:1::/64から確保されていることがわかります.

今後の展望

今回開発したIS-IS SRv6 Extensionによって,IS-ISネットワーク内でSRv6の設定とその情報の広報をできるようになりました.しかし,まだいくつか改良できる点があります.例えば,IS-IS SRv6 Extensionには関連する2つのドラフトがあります.

これらを導入することによって,SRv6のネットワーク分離, トラフィック制御をより柔軟にすることが可能になるので,ぜひとも追加したいです.そして,このExtensionをupstreamにマージしたいと考えています.

開発において気をつけたこと

このインターンに参加するにあたって,コードは実験用に数百行を書く程度で大規模の開発経験はなく,またFRRの仕組みや内部構造,SRv6などは全く知らない状態から期間内に開発,検証までを終わらせる必要がありました.あまりこのような状況になることはないかもしれませんし,結局つまるところはよく言われている話になってしまいますが,この開発フローで意識したことなどを共有したいと思います.

開発前に,事前知識の習得も含めた自分のToDoを細かに整理する

開発前に,開発と検証にはどんな知識とどんな準備が必要かをリスト化し,整理します.これはあくまで,自分が想像して必要そうなものをピックアップするだけでOKです.もちろん,リストに従って技術習得や開発を行っていく過程で,ToDoのアップデートがあるかと思いますので,そのタイミングでリストしましょう.とにもかくにも,開発の全体像を俯瞰し,自分がどこまで進んでいるかを可視化することが重要です.

ToDoに優先度をつける

ToDoを進めていく上で,やるべきことを思いついた時に,それが完成するためにどれくらい重要かを考えて,今する必要がないのであればリストに追記して他のことをやるなど,ToDoの優先度付けが必要です.例えば,コードの見た目を綺麗にする,などです.これはOSS貢献などの他人のコードに手を加える開発で特に顕著ですが,既存コードはライブラリの構造体などを使って綺麗に書かれていることが多く,そこに乗っかって開発してしまいたくなりがちです.しかし,そのせいで時間を取られてしまっては本末転倒です.綺麗にするのは最後にやればいいので,「まずは外側だけ作ってあとから中身を作っていく」「適当なグローバル変数を作ってそれで値をやり取りする」など,動くものができることを最優先に開発していきます.

質問は積極的に共有する

個人的には,これが一番大事だった気がします.何かわからない点があったとき,できるだけ人数が多いところで質問をします.こうすることによって,以下のメリットがあります.

  • メンターの方以外の方も質問に対応してくれる可能性がある
  • 一見基礎的な質問でも,他のメンバーの知見になり得る

質問するとき,ただ「〜が分かりません」という形で質問するのでなく,15分前後考えて自分なりに出した仮説を共有し,これであっているかどうか,というのを基準に質問すると,自分の頭も整理されて非常に建設的な議論になります.とにかく,「この程度で聞くのもなぁ…」という躊躇いを捨てるのが大事かな,と思いました.

最後に

1ヶ月で技術の習得からロードマップの作成,設計,実装,検証までやりきる,という今までに体験したことのないスピードでの開発でしたが,本当にあっという間で充実した時間でした.また,実際にチームとしてオープンソースのルーティングプラットフォームに貢献するモチベーションなどを知るなど,非常に貴重な体験をさせていただきました.また,休憩時間などで同じチームメンバーの方と議論させていただいた技術の話も,今後の研究/開発に役立ちそうなものばかりでした.指導/議論してくださったネットワーク開発チームのみなさん,特にFRR周りのエコシステムの説明やロードマップ作成など,様々な局面でアシストしてくださった城倉さんに改めて謝意を述べたいです.