BBSakuraの今と今後

こんにちは。BBSakura Networks社長の佐々木です。

本日はBBSakura Networksの今と今後について書かせていただきます。

この記事は BBSakura Networks Advent Calendar 2024 - Adventar 最終日として書いております。

今年の6月からBBSakuraの親会社であるBBIXの社長も兼任させていただくことになりまして、BBSakuraで開発をしているNaaSサービスである、OCXの展開をさらに加速していく所存です。

さて、本日はBBSakura設立から5年目となる今年、会社設立時の想定と今、そして今後を比較してみたいと思います。

BBSakura Networksの理由と目的

全ての「モノ」がつながる社会を支えるテクノロジーカンパニーを目指して設立した当社ですが、2019年に設立して今年で5年目になりました。

元々、コンピューターの汎用化が進む中で、通信に関わるコンポーネントについても、よりソフトウェアによる実装が進むと考え、この部分の開発能力を持ったエンジニアを抱えたさくらインターネットさんと会社を設立させていただいた形です。

以下はBBSakura 設立時の発表会で使った資料ですが、通信インフラ主体のBBIXと、クラウドコンピューター主体のさくらインターネットそれぞれの出身母体の強みを活かし、「国産のソフトウェア」を実装しながら世界へ展開かけていくことを考えておりました。

BBIXとさくらインターネットの強みを融合し国産のソフトウェアを生産

会社設立の経緯などは過去の記事でも書いておりますのでご参照ください。 blog.bbsakura.net

5年間の振り返り

過去5年間を振り返ると最初の3年はコロナ禍もあり、採用とチームの拡大に大変苦労しておりました。

当初は即戦力を求めて中途採用を狙ってましたが、なかなか採用が進まず、この2年ほどはソフトバンク採用の新卒をソフトウェアエンジニアとして育てていく方向に舵を切っておりまして、開発のエンジニアも急激に増えてきました。

対外トピック 社内トピック 開発社員数
2019 会社設立 さくらとBBIX混合チーム発足 約10人
2020 さくらのSMS開始 コロナ禍に入りフルリモートの働き方が本領発揮 約15人
2021 N/A OCXに関わる開発に没頭 約15人
2022 OCXリリース OCXの開発体制を拡充 約20人
2023 「革新的情報通信技術(Beyond 5G(6G))基金事業」に採択 新卒採用→開発エンジニア育成を開始 約25人
2024 OCXの拠点が40拠点へ・海外展開のパートナーとも合意 開発チーム拡大方法を模索 約40人

人数が増えてくるとできることも増えてきまして、リリースできるサービスが増えてきた反面、チームをスケールさせるところに力を割かねばならないと感じており、 大きくなりつつある会社のオンボーディングを整える - BBSakura Networks Blog みたいな動きもしてもらってます。

サービス面では、昨年「革新的情報通信技術(Beyond 5G(6G))基金事業」に採択いただけたことは、我々としても大変勇気付けられましたし、今年、初のOCXの海外進出に合意ができたことも、世界に向けてMade in Japanのプラットフォームを広げていくという我々の野望の第一歩にできるかと思っております。

今後のBBSakuraについて

OCXはおかげさまで順調に拡大を続けており、さらなる機能拡充・展開に向けてより開発体制の拡充をかけたいと考えております。

OCXでは、従来のネットワークの概念を大きく変え、より柔軟で拡張性の高いネットワーク環境を実現したいと思ってます。BBSakuraでは、このOCXを基盤に、IoTデバイスから大規模データセンターまで、あらゆるものをシームレスに接続するプラットフォームへ進化させていきたいと考えており、お客様は、より低コストで、高品質なネットワークサービスを利用できるようになります。

日本で開発したサービスを世界に広げながら、新しい価値提供を広げていきたいと思っており、OCXを技術にとどまらず、新しい世界観を創造していけるようなプラットフォームにしていきたいです。

アドベントカレンダー2024、盛り上がってますか?

はじめに

この記事は BBSakura Networks アドベントカレンダー 2024 の 18 日目の記事です。

adventar.org

こんにちは。開発本部 モバイル開発グループの みずき @n0mzk です。

モバイルコアの開発・運用がメインの業務ですが、人事や技術広報もやっています。

11 日目には人事として行っている取り組みについての記事を書きました。

blog.bbsakura.net

今日は技術広報の話を書きます。

技術広報チームは社内有志のエンジニアが集まってこのブログの運営・登壇支援・社外イベントへのスポンサー検討などを行っています。

アドベントカレンダーも、Adventar にカレンダーを作って、参加者を募って、レビューして、記事を公開して、SNS で宣伝して、というのをやっています。

ブログ運営については去年のアドベントカレンダーの記事で書いたので、具体的な運用方法・工夫していることはこちらの記事を参照ください。

去年のアドベントカレンダー以降どうやって会社技術ブログの更新を続けているか - BBSakura Networks Blog

アドベントカレンダー 2024、盛り上がってますか?

ここ 3 年ほどアドベントカレンダーの運用をやってきて、今年はあまり盛り上がっていないように感じています。ブログへのアクセス数も去年に比べだいぶ少ないです(具体的な数字は控えます)。

盛り上がらなさには社内の要因と SNS の要因があると思っていて、今回は SNS の要因について考えます。

社内の要因

社内の要因についても軽く書きます。

会社として年を重ねるごとに、ありがたいことにやるべき業務は増えていて、みんなそれぞれ忙しくなってきています。余裕があればブログ書いてみようかなと思えても、業務に追われているとなかなか参加しようと踏み切れないだろうと思います。今年のアドベントカレンダーは去年に比べてだいぶ参加者の集まりが悪く、埋まらなかった日を遅刻しながらでも埋めようと協力してもらっています(この記事も遅刻しながら後から書いています……)!

また、12/11 の記事に書いたとおり、会社が大きくなるにつれ「越境しづらくなっている」のも要因のひとつだと思っています。

SNS の要因

X の仕様・規約変更で Twitter だったころのような雰囲気ではなくなったことはみなさん感じていると思います。

新しい SNS もたくさん登場し、X から他の SNS に移る人が増えています。私も長年 "住んで" いた Twitter ( X ) の殺伐とした雰囲気にうんざりし、平和を求めてメインで使う SNS は Bluesky に移行しました。

公式アカウントでブログの更新などを発信したときにいちばん反応をもらえる SNS は今でも X です。でも X からの流入は明らかに減っていて(Google アナリティクスで見ると、t.co からの流入は昨年同時期と比べて -82.26 % でした!)、X での発信で読者を獲得するのは難しくなっているのではないかと感じています。

BBSakura では SNS 開始当初は X ( @bbsakuranet )Facebook ( @bbsakuranet ) を運用していましたが、2023 年 12 月からは Bluesky ( @bbsakura.net ) も始め、今年の 12 月からは流行りに乗って mixi2 ( @bbsakuranet ) にも公式アカウントを作ってみました。

Bluesky は、まだまだ X より反応の数は少ないものの、いいね・リツイートしてくれる方、フォローしてくれる方は、記事をしっかり読んでくれていそうという印象を受けています。

mixi2 は、ユーザと交流するわけではない企業アカウントが一方的な発信に使うことが適切な場なのか、つかみきれていません。公式アカウントの投稿を個人アカウントでリポストするのが嫌われる行為なのかそうでないのかなどの感触をつかもうとしているところです。

去年までは私個人の X アカウントでも毎日公式アカウントの投稿をリポストしたりして盛り上げようとしていたのですが、X から足が遠のき、それもしなくなってしまいました。より個人的な使いかたをしている Bluesky では X でやっていたのと同じような盛り上げをしていないことも、"アドベントカレンダー盛り上がってなさ" を感じる要因になっているとは思っています。

SNS 広報、どうしていますか?

X にみんな集まっているという状況ではなくなりつつある今、企業の広報のみなさん、SNS での広報どうしていますか?

やっぱりまだ X が強いのかも。でも他の SNS でファンを獲得できている企業もたくさんあるとも思う。

各 SNS での引用ポストなどで、最近の SNS 運用どうしているのか、苦労話や悩み・工夫・考えていることなど教えていただけたら嬉しいです。

Proxmox VE + CML2.8(無償版)でお手軽ラボ環境を作ってみた

この記事は、BBSakura Networks アドベントカレンダー 19日目の記事となります。

こんにちは、BBSakura Networksにてモバイルコアの開発・運用をしている清水です。

先月、Ciscoから、CML2の無料版のリリースが発表されました。

developer.cisco.com

同時に起動できるのは5ノードまで、などの制限はいくつかあるものの、CCNAの勉強や簡単な検証に使用する分には十分そうです。 そこで本記事では、 Proxmox + CML2.8 Free TierをミニPC上に構築するための方法について説明します(n番煎じではございますが...)。

構築に使用したPCについて

構築に使用したミニPCの外観。13cm四方程度の大きさで非常にコンパクト。
写真. 構築に使用したミニPC

しばらく前にAmazonのセールで購入したPCです。45,000円くらいで買いました。*1 スペックは下表の通りです。

要素 スペック
CPU AMD Ryzen 7 5700U
メモリ 32GB
ストレージ M.2 SSD 1TB
ネットワーク 2.5GbE LAN, Wifi 6E

このPCにオープンソースの仮想化環境プラットフォームであるProxmox VEを導入し、その仮想環境上でCML2を動かします。 Proxmoxを使用せず、直接CML2をインストールすることも可能ですが、ミニPCを別の用途でも同時に利用したかったため、今回は仮想環境での構築を行っています。

Proxmox VEの導入

www.proxmox.com

公式ページの手順に従い、Proxmox VEを導入していきます。

ISOイメージの入手

公式ページからISOイメージをダウンロードします。今回はProxmox VE 8.3 ISO Installer をダウンロードしました。

Proxmox公式ページにある、ISOイメージのダウンロードページ

USBからイメージを起動する

ダウンロードしたISOイメージをUSBに書き込みます。書き込みにはRaspberry Pi ImagerやRufusなどをを使います。今回はRaspberry Pi Imagerを使用して書き込みました。 Raspberry Pi ImagerでISOイメージをUSBに書き込むときの画面

書き込みが終了したら、ミニPCにUSBメモリを挿して、Proxmoxのインストーラーを起動します。

USBブートで起動したProxmoxインストーラーのトップ画面

インストール

インストーラーの指示に従い、タイムゾーンや管理者アカウントの設定、ネットワーク設定を進めていきます。

地域とタイムゾーンの設定 管理アカウントのパスワード・メールアドレスの設定 ネットワークの設定

インストーラーを進めていき、最終的にこのような表示になったらインストール完了です

インストールが完了し、再起動した後に表示されるCLIのログイン画面

管理画面へのログイン

ミニPC以外のPCからProxmoxの管理ページへログインします。 http://<インストーラーで設定したIP>:8006/にアクセスし、IDとパスワードを入力しログインします Proxmox VEのログイン画面

ログインに成功し、下図のようなダッシュボードが表示されればOKです

Proxmoxのダッシュボード

CML2 Free Tierのインストール

事前準備

https://software.cisco.com/download/home

上記のリンクから、CML2のインストールに必要なISOイメージを入手します(Ciscoのアカウントが必要です

CML2のダウンロードページ

2.8.0 Free Tier からcml2_2.8.0-6_amd64-32-iso.zip と、refplat-20241016-supplemental-iso.zipをダウンロードします(赤枠で囲ってあるもの)。

Proxmox からCML2用のVMを作成

事前準備でダウンロードしたISOイメージをProxmoxにアップロードします

Proxmoxの管理画面から、ノード名local(ノード名)ISOイメージ と遷移していき、アップロードをクリックし、2つのイメージをアップロードします

Proxmox ISOイメージの登録

ISOイメージのアップロード画面

VMを作成します。全般〜ネットワークまで順番に設定していきます。 以下、重要な設定のみ抜粋

  • OS
    • ISOイメージとしてcml2_2.8.0-6_amd64-32.isoを指定(もう一方のイメージは後で使用します)

VMの作成(OSイメージの指定)

  • システム
    • BIOS: OVMF (UEFI)

VMの作成(システム設定)

  • CPU
    • 種別(Type)を規定値のx86-64-v2-AESからhostに変更します。
    • 規定値のままだとCML2のインストーラーが失敗します(参考

VMの作成(CPUの設定)

CML2 インストール

管理画面からコンソールをクリックし、インストーラーの指示に従い設定を進めていきます

CML2インストーラーの起動時の画面

CML2のシステム設定の画面

最後に、以下の画面がコンソールで表示されればインストール作業は完了です

CML2のインストール完了後に表示されるCLIの画面

ノード用イメージの登録

作成したVMを一旦停止し、ProxmoxのVMの設定画面からのハードウェアCD/DVDドライブ(ide2)をクリックした後、編集をクリックし、ISOイメージをrefplat-20241016-freetier.isoに変更します

ProxmoxのVMのハードウェア設定の画面

Proxmoxの管理画面でCML2のVMのISOイメージを入れ替える設定画面

https://<CML2のIPアドレス>:9090にアクセスし、でcml-controllerにログインします。

cml-controllerのログイン画面

ログイン後に表示される画面から、Copy Refplat ISOをクリックし、ノード用のイメージを登録します

cml-controllerのトップメニュー

これでCML2の導入作業は完了です。

CML2の動作確認

CML2のインストール作業が終わったので動作確認を行います

https://<CML2のIPアドレス>にアクセスし、ログインします。

CML2のログイン画面

ダッシュボードが表示されるので画面右上のADDをクリックしワークベンチを追加します。

CML2ログイン後に表示されるダッシュボード

画面上部のツールバーにあるAdd Nodeをクリックし、数種類のノードが出てくる状態になっていることを確認します

ワークベンチ上でAdd Nodeをクリックした際の画面。ルーターやPC含む様々な機器が登録されていることを確認する

後は自由にノードを配置し、シミュレーションを行うだけです。 シミュレーション環境なので、実際の環境では試しにくいことも気軽に試せます。

ブロードキャストストームのシミュレーションを行うために2つのUnmanaged Switchに2本のケーブルを繋ぎパケットをループさせている様子
ブロードキャストストームのシミュレーション

終わりに

本記事では、ミニPC上にProxmox VEで仮想環境を構築し、その上にCML2の無償版をインストールする方法について紹介しました。 PC1台あれば環境を作れるため、低予算でラボ環境を作ることができ大変満足しています

本記事が、手元にPCが余っている方や、低予算でラボ環境を作りたい方の助けになれば幸いです。

*1:ちょっとお高めなので、CML2 無償版を動かすだけならばIntel N100搭載の2万円前後のミニPCでも問題ないと思います

疑似フレッツ光網内の通信を accel-ppp で再現できるか検証する(実装編1)

はじめに

この記事は BSakura Networks Advent Calendar 2024 の 22 日目の記事です。

こんにちは、BBSakura Networks 株式会社の佐藤です。

余力があれば今後「実装編」を書く予定です。

前回の投稿で上記の文言を残して一年が経過しました。PPPoE 熱が戻ってきましたので、こちらの活動を再開します。 ただ、実際に実装を開始した結果、Advent Calendar 期間中の完成が難しいことがわかったため、今回の記事では「実装編1」というタイトルとし、改修済みの箇所の紹介に留めたいと思います。

※ すべての改修が完了した時点でソースコードを公開する予定です。

前回のおさらい

前回の投稿では下記のことを述べました。

  • オープンソースでフレッツ光網に似た「疑似ネットワーク」を再現してみたい
  • PPP 関連の機能が一式揃っている accel-ppp を改修するのが近道だと判断した
  • 再現のためには以下の機能を追加する必要があると考えた
    • L2TP セッション動的生成機能
    • PPP ネゴシエーション情報中継機能
    • PPP セッション間通信中継機能

現時点の進捗

現時点の進捗は下記のとおりです。

L2TP セッション動的生成機能 → 8 割完了

PPPoE クライアントとの PPP セッション生成をトリガーに L2TP 側に PPP セッションを生成する機能です。 クライアントからユーザ名を受け取ったタイミングで L2TP セッションを生成する機能の実装は完了しており、受け取ったユーザ名の @ 以降の文字列を用いて網終端装置の IP アドレスを取得する処理が未実装の状態です。

PPPoEおよびL2TPセッションのネットワーク構成を示した図。 左側に「宅内ルータ相当」としてPPPoEクライアントが配置されており、次に「accel-pppデーモン(フレッツ収容ルータ相当)」が続く。このデーモン内にはPPPoEサーバとL2TPクライアントが含まれている。L2TPクライアントは右側の「振り分けサーバ」と接続され、最終的に「accel-pppデーモン(網終端装置相当)」へ到達。このデーモン内にはL2TPサーバが含まれている。  図内では以下の内容が説明されている:  PPPoEセッション(宅内ルータからフレッツ収容ルータへの接続) L2TPセッション(フレッツ収容ルータから網終端装置への接続) ユーザー情報の受信をトリガーに実行される以下の処理: 接続先となる網終端装置のIPアドレスを「振り分けサーバ」から取得(未実装)。 取得したIPアドレスに対してL2TPセッションを生成(実装済み)。

PPP ネゴシエーション情報中継機能 → 完了

PPP クライアントと網終端装置間で発生する CHAP 認証情報、IPCP の通信を中継させて、網終端装置が管理する IP アドレスを PPP クライアントに割り当てる処理は実装済みです。

PPPoEおよびL2TPセッションを利用したネットワーク構成図で、ネゴシエーション情報のやり取りを説明している。左側に「宅内ルータ相当」としてPPPoEクライアントが配置され、中央に「accel-pppデーモン(フレッツ収容ルータ相当)」が存在。このデーモン内にはPPPoEサーバとL2TPクライアントが含まれる。さらに右側に「accel-pppデーモン(網終端装置相当)」があり、L2TPサーバが含まれている。図内には以下の要素が含まれる:PPPoEセッション(宅内ルータからフレッツ収容ルータへの接続)L2TPセッション(フレッツ収容ルータから網終端装置への接続)ネゴシエーション情報のやり取り: PPPoEとL2TP間でネゴシエーション情報を転送し、宅内ルータと網終端装置を両端とした仮想的な1つのPPPセッションを生成する仕組み。図の下部には次の説明が付記されている:ネゴシエーション情報を中継することで、(実際は2つのセッションだが)宅内ルータと網終端装置を両端とした仮想的な1つのPPPセッションを生成する。

PPP セッション間通信中継機能 → 未実装

セッション確立時に生成されたネットワークインターフェースを利用して PPPoE クライアントからの通信を網終端装置に転送する機能です。こちらがまだうまく動作しない状態です。

PPPoEおよびL2TPセッションを利用したネットワーク構成図で、セッション間のデータ転送機能の未実装部分を示している。左側には「宅内ルータ相当」としてPPPoEクライアントが配置され、中央に「accel-pppデーモン(フレッツ収容ルータ相当)」があり、PPPoEサーバとL2TPクライアントが含まれている。右側には「accel-pppデーモン(網終端装置相当)」があり、L2TPサーバが含まれる。図内には以下の要素が含まれる:PPPoEセッション(宅内ルータからフレッツ収容ルータへの接続)。L2TPセッション(フレッツ収容ルータから網終端装置への接続)。デバイス間のデータ転送機能:図の中央に「/dev/ppp0」と「/dev/l2tp0」が示されており、両者の間にデータを転送する処理が必要であることが記載されている。現時点では未実装の部分が「?」マークで示されている。図の下部には次の説明が付記されている:一方から出力されたデータを対向のセッションに転送する機能の実装がまだ完了していない。

改修前準備

accel-ppp の特徴

accel-ppp の大きな特徴はモジュールの組み合わせによりサーバとしての機能を柔軟に変更できる点です。 デーモン起動時にコンフィグファイルに指定したモジュールをロードされて、その機能が有効化されます。代表的な機能は下図のとおりです。

accel-pppデーモンの構成を示す図で、主に以下の4つのカテゴリに分かれています:アクセス手段PPPoE (server) L2TP CLI (telnet) PPP Core機能 session管理 lcp auth ncp ipcp ipv6cp 認証方式 auth_pap auth_chap_md5 auth_mschap_v1 auth_mschap_v2 認証情報参照方法 radius (client) chap-secrets また、図内では各モジュールがどの機能や方式と接続されているかが矢印で示されています。たとえば、認証方式(auth_papやauth_chap_md5など)は認証情報参照方法(radiusまたはchap-secrets)と接続されています。また、ip_poolはIPアドレス管理を担当しています。

今回採用したモジュールの組み合わせ

疑似フレッツを実現するため、役割ごとに以下のようにモジュールを組み合わせます。

  • accel-ppp デーモン(フレッツ収容ルータ相当)
    • アクセス:PPPoE による接続
    • 認証方式:CHAP MD5 認証を選択
    • 認証情報参照方法:RADIUS (ユーザ情報を管理する RADIUS サーバと通信)
    • NCP:IPCP を選択( IPv4 のみを利用して IPv6 は利用しない)
  • accel-ppp デーモン(網終端装置相当)
    • アクセス:L2TP による接続
    • 認証:CHAP MD5 認証を選択
    • 認証情報参照方法:RADIUS (網終端装置情報を管理する RADIUS サーバと通信)
    • NCP:IPCP を選択(同上)

PPPoEクライアント、accel-pppデーモン(フレッツ収容ルータ相当および網終端装置相当)、RADIUSサーバの構成を示した図。赤枠は重要なモジュールや機能を強調している。左側はPPPoEクライアントが接続されている。中央のaccel-pppデーモン(フレッツ収容ルータ相当)は、pppoe (server)やl2tpのアクセス手段を持ち、コア機能としてsession管理(赤枠)、lcp、auth、ncp → ipcp(赤枠)が含まれ、認証方式としてauth_chap_md5(赤枠)を採用し、radius (client)を通じて認証情報を参照する。一方、右側のaccel-pppデーモン(網終端装置相当)はl2tp(赤枠)のアクセス手段、session管理(赤枠)、lcp、auth、ncp → ipcp(赤枠)のコア機能、auth_chap_md5(赤枠)の認証方式を持ち、radius (client)およびip_poolを通じて認証情報を参照する。左右に接続されたRADIUSサーバは、フレッツ収容ルータ相当のデーモンが「網終端装置情報」を参照し、網終端装置相当のデーモンが「ユーザ情報」を参照している。図全体では、PPPセッションとL2TPセッションを用いて通信を管理するフレームワークが示されている。

改修の詳細

モジュールの改修箇所(構造体・関数)

上手の赤枠の機能・モジュールに以下の改修をしました。

  • session 管理(機能)
    • ap_session 構造体:受け渡しのためのメンバ変数を追加
  • auth_chap_md5 モジュール
    • chap_recv_response 関数:認証処理をせずに L2TP トンネル・セッションを生成
    • chap_start 関数:通常の処理に移行せず特有の処理をサーバごとに実行
    • chap_recv 関数:ペアとなるセッションにパケットを転送する処理を実行
  • l2tp モジュール
    • l2tp_create_tunnel_exec 関数:PPP セッション同士を参照する仕組みを追加
    • l2tp_send_ICCN 関数:認証情報を格納する処理を追加
    • l2tp_recv_ICCN 関数:認証情報を取り出す処理を追加
  • ipcp(機能)
    • ppp_unit_read 関数:ペアとなるセッションにパケットを転送する処理を実行

改修後の通信シーケンス

改修後の通信シーケンスは以下のようになります。以降では図面の関係上、略称を使用します。

  • 宅内ルータ相当 → home
  • accel-ppp デーモン(フレッツ収容ルータ相当)→ sse
  • accel-ppp デーモン(網終端装置相当) → nte
  • ユーザ認証用 RADIUS サーバ → isp-radius
  sequenceDiagram
      participant HR as 宅内ルータ<br>(home)
      participant SR as フレッツ収容<br>(sse)
      participant N as 網終端装置<br>(nte)
      participant RA as RADIUS<br>(isp-radius)
      Note over HR,SR: PPPoE ネゴシエーション開始〜完了
      Note over HR,SR: LCP ネゴシエーション開始〜完了
      Note over HR,SR: CHAP 認証(開始)
      Note over SR: CHAP( Challenge 生成 )
      SR->>HR: CHAP Challenge
      HR->>SR: CHAP Response
      rect rgb(249, 194, 112)
        rect rgba(255, 0, 255, 0.2)
          Note over SR:  1:L2TPトンネル・セッション生成
          rect rgb(191, 223, 255)
            Note over SR : ユーザ名から<br>網終端装置の IP アドレスを取得<br>(今回未実装)
          end
        end
      end
      rect rgb(191, 223, 200)
        Note over SR,N: L2TP トンネル生成開始〜完了
        Note over SR,N: L2TP セッション生成開始
        SR->>N: ICRQ
        N->>SR: ICRP
        rect rgba(255, 0, 255, 0.2)
          Note over SR: 2:CHAP 認証情報格納
        end
        SR->>N: ICCN
        rect rgba(255, 0, 255, 0.2)
          Note over N: 3:CHAP 認証情報取得
        end
        N->>SR: ZLB
        Note over SR,N: L2TP セッション生成完了
      end
      rect rgb(249, 194, 112)
        Note over N,SR: LCP ネゴシエーション開始〜完了
       Note over SR,N: CHAP 認証開始
        rect rgba(255, 0, 255, 0.2)
          Note over N: 4:CHAP 認証
        end
        Note over N,RA: RADIUS(CHAP 認証)

        N->>SR: CHAP Success/Failure
        rect rgba(255, 0, 255, 0.2)
          Note over SR: 5:CHAP 認証結果転送
        end
       Note over SR,N: CHAP 認証完了

      end
      SR->>HR: CHAP Success/Failure 
      Note over HR,SR: CHAP 認証完了
     Note over HR,SR: NCP(IPCP) ネゴシエーション開始
      HR->>SR: Conf-Req
      rect rgb(168, 168, 255)
        rect rgba(255, 0, 255, 0.2)
          Note over SR: 6:Conf-Req 転送
        end
       Note over SR,N: NCP(IPCP) ネゴシエーション開始
        SR->>N: Conf-Req
        N->>SR: Conf-Req
        rect rgba(255, 0, 255, 0.2)
          Note over SR: 6:Conf-Req 転送
        end
      end
      SR->>HR: Conf-Req
      HR->>SR: Conf-Ack
      rect rgb(168, 168, 255)
        rect rgba(255, 0, 255, 0.2)
          Note over SR: 7:Conf-Ack 転送
        end
      SR->>N: Conf-Ack
      N->>SR: Conf-Ack
        rect rgba(255, 0, 255, 0.2)
          Note over SR: 7:Conf-Ack 転送
        end
       Note over SR,N: NCP(IPCP) ネゴシエーション完了
      end
      SR->>HR: Conf-Ack
      Note over HR,SR: NCP(IPCP) ネゴシエーション完了
      rect rgb(191, 223, 255)
        Note over HR,N: 仮想的な PPP セッションを利用したデータ通信(今回未実装)
      end

ポイントは home ↔ sse 、sse ↔ nte のそれぞれの区間で PPP セッションを確立するための 3 つのフェーズが完了している点です。

  • LCP ネゴシエーション
  • CHAP 認証
  • NCP(IPCP)ネゴシエーション

改修箇所(構造体・関数)の詳細

今回改修した構造体・関数の動作の詳細を通信シーケンスと対応付けて説明します。

共通:セッション構造体(ap_session)の改修

accel-ppp は、各 PPP セッションの状態を管理するため ap_session 構造体を有しています。 こちらに以下のメンバ変数を追加します。

  • 対向セッションの管理
    • peer_session:対となる ap_session 構造体のポインタを格納
  • CHAP 認証情報の格納
    • username:ユーザ名(username@domain)を格納
    • challenge:CHAP Challenge を格納
    • response:CHAP Response を格納

1:L2TPトンネル・セッション生成

こちらの処理に到達するまでに下記の処理が完了しており、PPP は「認証中」のステータスとなっています。

  • home ↔ sse 間の PPPoE ネゴシエーション
  • home ↔ sse 間の LCP ネゴシエーション
  • sse が送信した CHAP Challenge に対する home の CHAP Response を sse が受信

図は「home」と「sse」間のPPPoEセッションにおける処理の流れを示しています。ap_session構造体にusername、challenge、responseといった情報を格納し、情報取得後(ステップ2)にl2tp_create_tunnel_exec関数を実行してL2TPトンネルを生成する処理(ステップ3)が行われます。また、ステップ1では、ユーザ名の「@」以降を利用して「nte」のIPアドレスを取得する未実装の機能が示されています。図内にはlcp、auth、ipcpの各処理のステータスも表示され、「完了」と「処理中」の状態遷移が強調されています。

CHAP Response を受信したタイミングで通常は chap_recv_response 関数がコールされ CHAP 認証を実施しますが、今回の改修により下記の処理を実施します。

  • 取得したユーザ名( @ 以降)を利用して nte の IP アドレスを取得(今回未実装
  • home から受信した CHAP 認証情報を引数にセットした ap_session 構造体のポインタを引数に指定した l2tp_create_tunnel_exec 関数を実行

2:CHAP 認証情報格納

こちらの処理に到達するまでに l2tp_create_tunnel_exec 関数が実行され下記の処理が完了しています。

  • L2TP トンネルの生成
  • L2TP セッションの生成処理( ICRQ、ICRP パケットの送受信 )

PPPoE クライアント(home)から L2TP クライアント(sse)を経由して L2TP サーバ(nte)に接続する仕組みを示し、ap_session で認証情報(username, challenge, response)を管理し、l2tp_create_tunnel_exec 関数でトンネル・セッションを生成、対向セッションのポインタをセットし、認証情報を含む ICCN パケットを送信してセッションを確立するプロセスを表しています。

改修された l2tp_send_ICCN 関数は、このタイミングで以下の処理を実施します。

  • ICCN パケットの送信
    • 改修による処理として、ICCN パケットに以下の AVP をセットする
      • Proxy Authen Name(Type 30): ユーザ名
      • Proxy Authen Challenge(Type 32):CHAP Challenge
      • Proxy Authen Response(Type 33):CHAP Response
  • PPP セッション生成
    • 改修による処理として、l2tp_create_tunnel_exec 関数経由で取得した対向セッションのポインタ情報をセットする

3:CHAP 認証情報取得

sse 側の l2tp_send_ICCN 関数が実行された後、nte は ICCN パケットを受信します。

PPP セッション作成後、ICCN パケットで認証情報(username, challenge, response)を送信し、nte 側で l2tp_recv_ICCN を通じて認証情報を ap_session 構造体にセットし、セッション間の関連付けを行うフローを示しています。

改修された l2tp_recv_ICCN 関数は、このタイミングで以下の処理を実施します。

  • ICCN パケットから AVP にセットされた CHAP 認証情報を取得
  • PPP セッション生成時に取得した CHAP 認証情報をセット

4:CHAP 認証

こちらの処理に到達するまでに下記の処理が完了しています(図中青枠の処理)。

  • nte から sse に ZLB パケットを送信(= L2TP セッションの生成完了 )
  • LCP ネゴシエーション完了

CHAP 認証開始後、sse 側は認証結果を待機し、nte 側が RADIUS サーバに認証処理を依頼して結果を受信し、成功または失敗のステータスを sse 側に送信する流れを示しています。

通常であれば 、LCP ネゴシエーション完了後の処理として、CHAP 認証開始のための CHAP Challenge をクライアントに送信しますが、chap_start 関数を改修して以下の動作をさせます。

  • sse の L2TP 側の PPP セッション
    • PPP セッションのステータスを強制的に「認証開始」から「認証結果待ち」に変更する
    • 何もしない (CHAP 認証結果の到着を待つ)
  • nte の PPP セッション
    • ap_session に格納された CHAP 認証情報から RADIUS のリクエスト用パケットを生成して isp-radius に CHAP 認証を依頼、その結果を受け取る
    • 受け取った CHAP 認証の結果が「成功」であれば、PPP セッションのステータスを強制的に「認証開始」から「認証完了」に変更する
    • sse に CHAP 認証結果を送信する

5:CHAP 認証結果転送

nte から受信した CHAP 認証結果が成功であれば、chap_recv 関数の通常の動作として sse の L2TP 側の PPP セッションのステータスを「認証完了」に変更します。

peer_session から取得した構造体の送信処理を呼び出し、受信データを home に転送するフローを示した図です。

この後、chap_recv 関数に追加した改修後の動作として、以下の処理を実施します。

  • peer_session メンバ変数から、対となる ap_session 構造体を取得する
  • ap_session 構造体に紐づく ppp_chan_send 関数に nte から受信した CHAP 認証結果をセットして home 側に転送する
  • 構造体の認証ステータスを「認証結果待ち」から「認証完了」に変更する

6:Conf-Req 転送

CHAP 認証結果を受信した home は、IPCP ネゴシエーションのフェーズに移行して、Conf-Req パケットを sse に送信します。

一般的な IPCP のネゴシエーションでは、双方から同時に Conf-Req パケットが送信されますが、accel-ppp コンフィグファイルで ipv4=allow を指定することで、対向からのパケットの受信をトリガーにして Conf-Req を送信するようにできます。

届いた Conf-Req パケットを ap_session の peer_session を参照して受信データを転送するフローを示した図です。

この機能のほか、改修した ppp_unit_read 関数の機能により、届いたパケットを対向の PPP セッションに転送するようにします。 これにより home → sse → nte → sse → home の順で Conf-Req パケットを転送することができます。

7:Conf-Ack 転送

sse から Conf-Req を受信した home は、Conf-Ack パケットを sse に送信します。

届いた Conf-Ack パケットを ap_session の peer_session を参照して受信データを転送するフローを示した図です。

先程と同様に home → sse → nte → sse → home の順で Conf-Ack パケットが送受信されます。Conf-Ack を受け取った段階で各 IPCP のネゴシエーションが完了し、home に Conf-Ack が届いたタイミングで、すべての PPP ネゴシエーションが完了した状態となります。

おわりに

BBSakura Networks では Go 言語を利用する機会が多く、C 言語で記述されたこの規模のソースコードを編集するのは久しぶりでした。メモリレイアウトやポインタに対する深い理解が求められ、動作を把握するのに多くの時間を費やしました。今回セッション確立後のデータ転送処理の完成に至らなかったことが残念ですが、普段あまり意識しないレイヤ(ソケット関連のシステムコールなど)を調査する機会を持つことができたことは今後のネットワークプログラミングをしていくための良い経験になったと思います。

BGP Monitoring Protocol を RFC とパケットキャプチャで勉強してみた

はじめに

この記事は BBSakura Networks Advent Calendar 2024 の 21 日目の記事です。

こんにちは! BBSakura で基盤となるネットワークを開発している酒井です。

昨年の BBSakura Advent Calender では BGP Flow Spec について書いていました。今年も去年と同様に BGP ネタで書いていこうと思います。

今年の BGP ネタとしては、 BGP Monitoring Protocol (以降は BMP と呼びます)について RFC 7854 を読んで内容を理解し、実際にパケットキャプチャをして中身を眺めてみます。

BMP とは簡単に言うと、 BGP セッションの状態や交換される経路情報を効率的に収集するためのプロトコル になります。

過去には、JANOG 33.53452 でも解説されていますので、興味のある方はこれらも確認するとよいかと思います。

本記事では私が学習した内容について書いていきます。

RFC 7854

BMP は 2016 年に RFC 7854 が発行されて仕様が決定しました。その後、現時点で RFC 8671 (Adj-RIB-Out への拡張) ・ RFC 9069 (Loc-RIB への拡張) ・ RFC 9515 (BMP に関わる一部の IANA レジストリ登録ポリシーの変更) によってアップデートされています。ただ、これらの RFC のアップデート内容はあくまで拡張がメインであり、 BMP の基礎をおさえるのであれば RFC 7854 を読めば OK です。

RFC 7854 の導入部分であるセクション 1 には、 BMP の概要について次のように書かれています。

BMP provides access to the Adj-RIB-In of a peer on an ongoing basis and a periodic dump of certain statistics the monitoring station can use for further analysis. 
From a high level, BMP can be thought of as the result of multiplexing together the messages received on the various monitored BGP sessions.

Adj-RIB-In は BGP-4 プロトコル自体の RFC である RFC 4271 で定義されており、ローカルの BGP スピーカーがピア先より受信した経路情報のことです。つまり、 BMP はこの受信した経路情報を解析するために、ダンプ情報としてエクスポートをすることを目的としている、と言えるかと思います。あくまで、 BMP 自体はルータなどの BGP スピーカーの情報をエクスポートするための技術であり、情報の解析・収集を行なうコレクタ( monitoring station )は別に用意する必要があるわけです。

BMP で観測・提供できるものとして、 BMP Messages がセクション 3.1 で定義されています。

Message Type番号 メッセージ内容
Route Monitoring (RM) 0 ピアから受信した経路情報 (初回ダンプ後の ongoing な update を含む)
Stats Reports (SR) 1 ongoing な統計情報
Peer Down Notification 2 ピアダウンの情報
Peer Up Notification 3 ピアアップの情報
Initiation 4 BMP セッションを開始する時に通知 (BGP スピーカーのベンダー、ソフトウェアバージョンなどの情報通知)
Termination 5 BMP セッションを終了する時の通知
Route Mirroring 6 BGP スピーカーが受信した BGP メッセージをそのまま複製して送信

これらの BMP Messages はセクション 4 で定義されたフォーマットで BGP スピーカーからコレクタへ送信されます。なお、この BMP の通信は TCP で行なわれます。

セクション 4 のフォーマットの詳細はボリュームがあるため割愛しますが、 Per-Peer Header が定義されており、 BGP スピーカーから見たピア単位での情報の観測が可能です。

セクション 5 では、 BMP でもっとも使用されるであろう Route Monitoring の動作について解説されています。 BGP スピーカーとコレクタ間で BMP セッションがアップした直後に、まずはその時点での各ピアごとの Adj-RIB-In がダンプされて Route Monitoring メッセージが送信されます。その後は、新たにピアから Update を BGP スピーカーが受信すると、随時コレクタへその分の Route Monitoring メッセージを送信します。また、 Adj-RIB-In がポリシーやフィルター適用前か適合後のものかを表す Per-Peer Header 内の L フラグによって、 Route Monitoring メッセージは判別されるともセクション 5 では書かれています。

続く、セクション 67 ではそれぞれ Route Mirroring と Stat Reports の動作ついて簡単に触れられていますので、興味のある方は確認してみてください。

また、セクション 11 の Security Considerations では、 BMP 自体には認証などの機能がないことから、セキュリティ上の懸念がある場合は BGP スピーカーとコレクタ間の通信に IPSec を使用することを推奨しています。

パケットキャプチャ

ここまで RFC 7854 の内容について説明してきました。次からは実際の動作をパケットキャプチャしながら見ていきます。

環境の構築

今回は BGP スピーカーとして GoBGP、 BMP コレクタソフトウェアとして goBMP を使用します。

今回は BMP のパケットをキャプチャすることを目的にしているので、下図のように 2 つの GoBGP 間で BGP の Update を発生させて、その結果を goBMP 側で見てみましょう。

左からgoBMP、AS65001のGoBGP、AS65002のGoBGPと順に接続されている環境構成図。
環境構成図

GoBGP はバージョン 3.32.0 を使用しています。 GoBGP をインスール後に gobgpd -f <設定ファイル> & で次の内容の設定ファイルを読み込ませます。

[AS65001 GoBGP]

[global.config]
  as = 65001
  router-id = "192.168.110.1"

[[neighbors]]
  [neighbors.config]
    neighbor-address = "192.168.110.2"
    peer-as = 65002

[[bmp-servers]]
  [bmp-servers.config]
    address = "192.168.10.70"
    port=5000
    route-monitoring-policy = "post-policy"
    statistics-timeout = 30

[AS65002 GoBGP]

[global.config]
  as = 65002
  router-id = "192.168.110.2"

[[neighbors]]
  [neighbors.config]
    neighbor-address = "192.168.110.1"
    peer-as = 65001

これで 2 台の GoBGP 間の BGP ピアが Up するはずです。

続けて、 goBMP をビルド して、バイナリを ./bin/gobmp --dump=console で実行します。 このコマンドではポート番号を指定していないので、 goBMP はデフォルトの 5000 番の TCP ポートを使用していることになります。

これで環境は整いました!とても簡単に環境構築ができますね。

パケットキャプチャ結果

ここから実際に BMP 通信のパケットをキャプチャして Wireshark で見ていきます。今回のパケットキャプチャは goBMP 側で取得しています。注意点として、今回の環境だと BMP コレクタは TCP 5000 番ポートで BMP 通信のパケットを待ち受けていますが、この状態でパケットキャプチャした pcap ファイルを Wireshark で確認しても、別のプロトコル ( RSL ) として認識されており正しくデコードされません。そのため、 Wireshark では当該パケットエントリで右クリック→[Decode as...]の設定画面から BMP を選択して、正しくデコードする必要があります。

最初に BGP スピーカーとコレクタ間の BMP 通信の TCP コネクションが確立した直後に発行される Initiation Message を見ます。

Wireshark画面でのBGP Monitoring Protocolパケットでタイプがinitiation Messageのキャプチャのスクリーンショット。
BMP 通信開始時のパケットキャプチャ

sysName で BGP スピーカーが GoBGP であること、 sysDescr で GoBGP のバージョンが 3.32.0 であることの情報がエンコードされています。

次に BGP ピアがアップ/ダウンした時を見ていきます。具体的には AS65002 側の GoBGP を起動/停止して BGP ピアをアップ/ダウンさせてみます。

BGP ピアがアップした時の BMP 通信パケットでは Peer Up Notification が発行されていることがわかります。

Wireshark画面でのBGP Monitoring ProtocolパケットでタイプがPeer Up Notificationのキャプチャのスクリーンショット。
BGP ピアアップ時のパケットキャプチャ

Per Peer Header からピア先の情報もわかりますし、アップしたピア間の情報が GoBGP 間でやり取りされた BGP OPEN Message として格納されていてわかりやすいです。

続けて、下の図が BGP ピアダウンした時の Peer Down Notification です。

Wireshark画面でのBGP Monitoring ProtocolパケットでタイプがPeer Down Notificationのキャプチャのスクリーンショット。
BGP ピアダウン時のパケットキャプチャ

NOTIFICATION Message から、ピアダウンした理由が読み取れます。このように実際の BGP のメッセージが情報として付いてくるのは嬉しいですね!

次に実際に BGP Update のやり取りが GoBGP 間で発生した時の BMP パケットを見てみます。 AS65002 から gobgp global rib add 192.0.2.X/32 で Prefix を 5 個ほど広報してみます。

Wireshark画面でのBGP Monitoring ProtocolパケットでタイプがRoute Monitoringのキャプチャのスクリーンショット。
BGP Update 発生時のパケットキャプチャ

Route Monitoring が発行され、実際に今回 GoBGP 間でやり取りされた BGP Update Message も含まれています。

これによって定期的に発行される Stats Reports でも BGP スピーカーが受信した Adj-RIBs-In の Prefix 数 ( Number of routes )が 5 にカウントされていました。

Wireshark画面でのBGP Monitoring ProtocolパケットでタイプがStatistic Reportのキャプチャのスクリーンショット。
定期的に発行される Stats Report のパケットキャプチャ

正しく統計情報も反映されていそうです!

以上となります。

おわりに

今回の記事では RFC 7854 とパケットキャプチャを通した BMP の調査内容について書きました。 BGP の定点監視を可能にするプロトコルが正式に定義されていて、実装がちゃんとあることは全世界の BGP オペレータにとって喜ばしいことですね!

BBSakura Networks Advent Calendar 2024 もそろそろ終盤ですが、引き続きお楽しみください。

来年のアドベントカレンダーでも BGP ネタで書きたいな〜。

eBPF/XDPでパケット処理をするときにやってるテスト方法の紹介

この記事は BBSakura Networks Advent Calendar 2024 の 24 日目の記事です。

こんにちは。BBSakuraでソフトウェアエンジニアをしています、早坂(@takemioIO) と申します。

先日、eBPF Meetup Japan #2 をさくらインターネット東京支社から配信しました。今回はオーガナイザーとして参加し、50名を超える参加者を集めたミートアップカンファレンスを実現できました。 ご参加いただいた皆さんありがとうございました。 本記事では、さらに前に開催された第一回のMeetupでお話しした内容のフォローアップとして、実装上のテストテクニックに関する落穂拾いをしていきたいと思います。

ちなみに前回発表のスライドはこちらです。よろしければご覧ください。 speakerdeck.com

eBPFヘルパーとは何か?

eBPFヘルパーと呼ばれる、eBPFプログラム内部から呼び出せる便利な関数群があります。これらは多くの場合、カーネルに対するAPI的な側面を持ち、UAPIの一部として定義されます。そのため、カーネルバージョン間でのコンパチビリティが担保されています。
もしコンパチビリティを特に考慮する必要がない場合は、KFuncと呼ばれる仕組みを用いてカーネルとやり取りするインターフェースを作ることも可能です。

eBPFヘルパーが実際にどのように定義されているかは、カーネルのコードを見るとわかります。
github.com

たとえば bpf_fib_lookup というヘルパー関数は、引数としてIPアドレスなどを渡すことで、Nexthopに接続するインターフェースとそのMACアドレスを取得できます。XDPなどを用いたパケット処理においては、ARPパケットをカーネルのプロトコルスタックに通すことで、ARPによるMACアドレス解決が可能です。

この結果はLinuxカーネル内部でFIBエントリとして保持されていますが、bpf_fib_lookupを利用することでその結果を取得できます。
詳しくは以下のリンクをご覧ください。
docs.ebpf.io

しかしながら、eBPFヘルパーを利用したプログラムをテストするには、Linuxカーネル内部にあるような情報をどこかで保持する必要があります。これはeBPFプログラムに対するテストの大変さの一つでもあります。

eBPFヘルパーを利用したeBPFプログラムをテストするには

eBPFプログラム自体をテストする方法として、主に以下の2つが考えられます。

1. QEMUを用いて実機相当の環境を再現したテスト

この方法では、実際のトポロジーを組むことで、eBPFヘルパーを利用したプログラムもテスト可能です。 また最も確実でもあります。実際のカーネル上で動作させ、virtio-netドライバレベルでの不具合検出なども可能です。
しかし、テスト環境を構築する手間やオーバーヘッドが大きいため、開発イテレーションを素早く回すには不向きです。

たとえば、ciliumのような大規模プロジェクトでは当然この手法が利用されており、参考資料として以下を挙げます。
Yutaro Hayakawaさんの資料

また、弊社環境でも同様のアプローチを採用しており、弊社CTOの日下部による資料が参考になります。

www.docswell.com

補足として、最近ではcilium/ebpfがvirtmeではなくvirttoというものに置き換わっています。
こうしたツールチェーンの変遷も興味深いです。

2. eBPF Helperの関数をモックする

BPF_PROG_TEST_RUNを使うと、eBPFプログラムに対してユニットテストを書くことができます。

一方で、BPF_PROG_TEST_RUNだけではeBPFヘルパー利用部分をテストするのは難しいです。なぜならカーネル内部からデータを取得する操作は、多くの場合副作用を伴うためです。

そこで、コンパイル時にC言語のプリプロセッサを用いてeBPFヘルパーをモック実装と切り替え可能にすることで、BPF_PROG_TEST_RUNを利用した簡易なユニットテストが実現できます。この方法は、特定の入力に対するテストを手軽に記述でき、環境を模すのに必要だったVMのオーバーヘッドがなく、開発のフィードバックループを高速化することが可能です。

BPF_PROG_TEST_RUN自体に関しては以下をご覧ください。
docs.ebpf.io

eBPFヘルパーのモックを使ったユニットテストの実装例

ここからは実際のサンプルコードを交えながら実装例を示します。
サンプルコードは以下のリポジトリにあります。XDPを用いたパケット処理実装の際に便利に使えるテンプレートです。
github.com

まず、テスト実行の例を抜粋します。全体は以下のリンクから参照してください。
github.com

このコードでは、eBPFプログラムに対して期待する入力を渡し、期待する出力が返ってくるかをテストしています。retが処理後のパケットデータ、gotが戻り値、errがシステムコール実行結果となります。これらを用いて期待通りのパケットかどうかをチェックします。

   ret, got, err := objs.XdpProg.Test(generateInput(t))
    if err != nil {
        t.Error(err)
    }

    // return code should be XDP_TX
    if ret != 3 {
        t.Errorf("got %d want %d", ret, 3)
    }

    // check output
    want := generateOutput(t)
    if diff := cmp.Diff(want, got); diff != "" {
        t.Errorf("output mismatch (-want +got):\n%s", diff)
    }

パケットはgopacketを用いると簡単に生成できます。以下はVXLANパケットの例です。

func generateInput(t *testing.T) []byte {
    t.Helper()
    opts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}
    buf := gopacket.NewSerializeBuffer()
    iph := &layers.IPv4{
        Version: 4, Protocol: layers.IPProtocolUDP, Flags: layers.IPv4DontFragment, TTL: 64, IHL: 5, Id: 1212,
        SrcIP: net.IP{192, 168, 10, 1}, DstIP: net.IP{192, 168, 10, 5},
    }
    udp := &layers.UDP{SrcPort: 4789, DstPort: 4789}
    udp.SetNetworkLayerForChecksum(iph)
    vxlan := &layers.VXLAN{VNI: 0x123456}
    err := gopacket.SerializeLayers(buf, opts,
        &layers.Ethernet{DstMAC: []byte{0x00, 0x00, 0x5e, 0x00, 0x53, 0x01}, SrcMAC: []byte{0x00, 0x00, 0x5e, 0x00, 0x53, 0x02}, EthernetType: layers.EthernetTypeIPv4},
        iph, udp, vxlan,
        &layers.Ethernet{DstMAC: []byte{0x00, 0x00, 0x5e, 0x00, 0x11, 0x01}, SrcMAC: []byte{0x00, 0x00, 0x5e, 0x00, 0x11, 0x02}, EthernetType: layers.EthernetTypeIPv4},
        &layers.IPv4{
            Version: 4, Protocol: layers.IPProtocolICMPv4, Flags: layers.IPv4DontFragment, TTL: 64, IHL: 5, Id: 1160,
            SrcIP: net.IP{192, 168, 100, 200}, DstIP: net.IP{192, 168, 30, 1},
        },
        gopacket.Payload(payload),
    )
    if err != nil {
        t.Fatal(err)
    }
    return buf.Bytes()
}

今回は前述した bpf_fib_lookup に関してモック化しています。
モック化したC関数を示します。中を見ると、eBPFマップでモックとして返すべき値を取得して返しています。
これらはコンパイル時にifdefマクロで切り替えています。

#ifdef XDP_TEST
// bpf_fib_lookupのモック実装
static __always_inline bool ipv4_fib_lookup(struct xdp_md *xdp, struct bpf_fib_lookup *params,
                                            struct iphdr *iph, __u32 ifindex, __u32 flags)
{

    struct bpf_fib_lookup_mock map_key = {};

    // tos, tot_len, sport, dport, l4_proto, ifindexを無視
    map_key.family = AF_INET;
    map_key.ipv4_src_or_ipv6_src[0] = iph->saddr;
    map_key.ipv4_dst_or_ipv6_dst[0] = iph->daddr;
    map_key.ifindex = ifindex;

    DEBUG_PRINT("ipv4_fib_lookup (mock) called iph->saddr %x", iph->saddr);
    DEBUG_PRINT("ipv4_fib_lookup (mock) called iph->daddr %x", iph->daddr);
    DEBUG_PRINT("ipv4_fib_lookup (mock) called ifindex %d", ifindex);
    struct fib_lookup_mock_result *mockres = (struct fib_lookup_mock_result *)bpf_map_lookup_elem(&fib_lookup_mock_table, &map_key);
    if (!mockres)
    {
        DEBUG_PRINT("ipv4_fib_lookup (mock) failed, return false");
        return false;
    }
    if (mockres->status != BPF_FIB_LKUP_RET_SUCCESS)
    {
        DEBUG_PRINT("ipv4_fib_lookup (mock) success, but status is not success, return false");
        return false;
    }

    __builtin_memcpy(params, &mockres->params, sizeof(struct bpf_fib_lookup));
    DEBUG_PRINT("ipv4_fib_lookup (mock) success");
    return true;
}
#else
// 通常時のbpf_fib_lookup呼び出し
static __always_inline bool ipv4_fib_lookup(struct xdp_md *xdp, struct bpf_fib_lookup *params,
                                            struct iphdr *iph, __u32 ifindex, __u32 flags)
{
    params->family = AF_INET;
    params->tos = iph->tos;
    params->l4_protocol = iph->protocol;
    params->sport = 0;
    params->dport = 0;
    params->tot_len = bpf_ntohs(iph->tot_len);
    params->ipv4_src = iph->saddr;
    params->ipv4_dst = iph->daddr;
    params->ifindex = ifindex;
    int rc = bpf_fib_lookup(xdp, params, sizeof(struct bpf_fib_lookup), flags);
    if (rc != BPF_FIB_LKUP_RET_SUCCESS)
        return false;
    return true;
}
#endif

モック時はGoのユニットテスト側で事前にマップへ期待結果を設定します。
たとえば以下のように設定します。

if err := UpdateIPv4FibLookUpMockMap(
    objs,
    IPv4FibLookUpMockKey{
        IPv4Src: netip.AddrFrom4([4]byte{192, 168, 100, 200}),
        IPv4Dst: netip.AddrFrom4([4]byte{192, 168, 30, 1}),
        Ifindex: 1,
    },
    &xdpFibLookupMockResult{
        Status: xdptool.BPF_FIB_LKUP_RET_SUCCESS,
        Params: xdpBpfFibLookupMock{
            Dmac: byteArrayToUint8Array([6]byte{0x00, 0x00, 0x5e, 0x00, 0x11, 0x11}),
            Smac: byteArrayToUint8Array([6]byte{0x00, 0x00, 0x5e, 0x00, 0x11, 0x12}),
        },
    },
); err != nil {
    t.Fatal(err)
}

モック化しない場合は実際のbpf_fib_lookupが呼ばれます。

テスト実行時は以下のようにコンパイル時フラグを付与してからテストします。

# モック有効化ビルド
CEXTRA_FLAGS="-DXDP_DEBUG -DXDP_TEST" make

# ユニットテスト実行
make test

このように、モックをうまく使うことで実環境の模擬なしにユニットテストが可能になります。

まとめ

今回は発表時のフォローアップとして、XDPを用いたパケット処理実装時のテストテクニックを紹介しました。
eBPFヘルパーを含むプログラムでも、コンパイル時マクロを用いたモック化によってユニットテストを容易にできることがお分かりいただけたかと思います。
もし、より良い手法やアイデアがありましたら、ぜひ教えてください。

BBSakura Networksでは、このようなパケット処理技術やカーネル内での処理に興味のあるエンジニアを募集しています。

マイクロカーネル本輪読会に参加してみた

この記事はBBSakura Networksアドベントカレンダー2024の17日目の記事です。

こんにちは。BBSakura Networksでモバイルコアのソフトウェア開発を行っている楽野です。今回はBBSakuraの任意のメンバーで活動している勉強会について書いてみようと思います。

輪読会やる?

以前からBBSakuraでは自社の事業領域であるネットワークやモバイル通信に関連する勉強会が不定期に開催されています。 そんな中、同じチームの早坂さん(@takemioIO | @gtpv2)、金井さん(@masu-mi | @masu-mi.bsky.social)がマイクロカーネルOS勉強会を輪読形式で始めると聞きました。

私も昔からOSのカーネルなど、低レイヤに興味を持って一人で技術書を読んだりはしていたのですが、強いエンジニアのお二方の楽しい勉強会のお邪魔になるといけないと思い参加を躊躇しました。輪読会というものに参加したことがなく、得体の知れない怖さを感じていたのもあります。

でも結局、思い切って参加してみることにしました。折角のこんな機会逃してはいけないと思ったのです。

輪読会でやったこと

今回の輪読会では「自作OSで学ぶマイクロカーネルの設計と実装」という書籍を持ち回りで読み進めることになりました。

使用したテキスト

基礎知識、カーネル、ユーザランド、発展的話題の4部で構成されており今回の輪読会では基礎知識とカーネルの部分に取り組みました。

  • 基礎知識

    • マイクロカーネル入門
    • 教育用マイクロカーネルOS「HinaOS」
    • RISC-V入門
  • カーネル

    • プロセス・スレッド
    • メモリ管理
    • 割り込み例外
    • メッセージパッシング
    • システムコール

この書籍は教育用に開発されたシンプルなマイクロカーネルOS「HinaOS」をベースにカーネルの各機能が解説されているのでかなり読み進めやすかったです。 その他、教育用のHinaOSに加えて実用されているマイクロカーネルOS「MINIX3」「seL4」「Mach」も題材として取り上げられています。

輪読会の進め方

各章を前半と後半に分け、担当者が事前に準備した資料をベースに勉強会当日に読み合わせを行う形で実施しました。 社内での初めての輪読会(3GPP輪読会をやろうとして頓挫した過去あり・・・)だったので、なるべく継続できるゆるい勉強会を目指しました。

例えば、勉強会当日までに資料が間に合わなければその場で全員で読み進める、週一で実施するが月に一週はバッファとして置いておく、勉強会の日が都合が悪ければ なるべく同じ週内でリスケする、などが会の継続に関して意識した点です。

こちらは任意だったのですが1000行で作るOSを実際に手を動かしながら読み進めることも推奨されました。HinaOSのソースコード読む際の手がかりにもなって取り組んでみて良かったと思います。

輪読会はどうだった

当初想像していたよりずっと楽しめた、というのが第一の感想です。資料を準備して、どのように話せば伝わりやすいかを考えることで理解がより深まりました。また読み合わせを行う中で脱線も交えながら色々と会話することで自分一人では気づけなかった面も知ることもできて、モチベーション向上に繋がったと思います。あと実際に題材のOSを動かしてみたり、ビルドに苦労してみたり、勝手なシステムコールを足す改造をしてみたりとそういった詳細に没頭する時間を持てたのはとても有り難かったです。

それなりに準備は大変でしたが想像して恐れていたより理解出来たし、それを自分なりに噛み砕いて話すこともできたと感じています。今後も他の題材でも輪読会をやりたいと感じています。(3GPP輪読会リベンジも)

おわりに

今後はBBSakuraの低レイヤ系勉強会活動として、CPUに関する勉強会を行おうといった話も出てきています。どのような勉強会をやっていけるのか今から楽しみです。

このような勉強会活動は業務の遂行へダイレクトに影響する部分でもありませんが、自分が普段生息しているレイヤの上下でどのような営みが行われているのか意識することは間接的に見える世界を広げることになり、技術的な選択肢を増やしてくれるのではと考えています。

あれこれ書きましたが、単純に技術について色々話せる仲間がいることが嬉しいというのが一番大きいです。今後も積極的に社内外の勉強会に参加していこうと思います。