新卒のJANOG53体験記

はじめに

こんにちは、BBSakura Networks株式会社(以降BBSakura)にてOCXのクラウド直接接続まわりを担当してる太田です。

今回は新卒の私が初めてJANOGに現地参加した際の面白かったテーマについて2つ記述してみようかなと思います。

NETCON

www.janog.gr.jp

NETCONとは

上記サイトの文言だとNETCONとは

ネットワークエンジニアのためのコンテストです。JANOG参加者が誰でも参加できるネットワークコンテストを開催します。コンテストでは、実際にトラブルが起きているネットワーク環境を提供します。 それらを実際に解いていただくことで、ネットワークエンジニアとしてトラブルシューティングの勘を高めていこうという企画です。

だそうです。ここで私が引っかかったのが2点。

  1. ネットワークエンジニアのための →まだネットワークエンジニアを名乗るには早すぎる私が参加して大丈夫なのか

  2. JANOG参加者が誰でも参加できる →え、もはやネットワークエンジニアじゃなくても参加できる?

と難易度が全く分からず、参加して大丈夫なのか不安でしたが、「現地問題」という現地でしか解けない問題があったので部屋に寄ってみることにしました。

現地問題

(写真を撮り忘れたのが恐縮ですが)現地問題ではスイッチ、ルーター、光ケーブルなどなど実際の物を用いたトラブルシューティングを行うのが、オンライン問題との大きな違いです。(ちなみに現地問題は合計で4問でしたがオンライン問題は約30問ありました)

私が取り組んだ問題がこちらです。(答えも載っています) note.com

要約すると

  • 機器にSFPも光ケーブルも刺してるのにリンクアップしない。どうにかしてリンクアップさせてください。

という内容です。

私は物理機器を全く触ったことがなかったので、右も左も分からず、とりあえず何をすればいいかをスタッフの方に聞きました。 すると「まずはハードウェアのインターフェースの情報が見れるコマンド」を探してみようと言われたので、ひたすらググり続け、見つけたコマンドを打っては「いや、これ違うな」を繰り返していました。(そもそもベンダーによってコマンド違うのなんとかしてくれ)

そして最終的に見つけたコマンドが

show chassis hardware

です。これをJuniper機器で打つとハードウェアに関する様々な情報が出てきますが、その中にインタフェースに刺さってるSFPモジュールの情報が出てきます。

今回は最初に両ポートに刺さってたSFPモジュールが一致していなかったため、片方を抜いてもう片方に合わせてあげるとリンクアップするという内容でした。

とまあここでは簡単に書きましたが、実際は何も分からない中、ほぼ全てスタッフさんに導いて頂き、なんとか回答に辿り着いています。ありがとうございました。

結果的に誰でも参加できるは正しかったですね(次はもう少し自力で解けるようになりたい)

エンジニアのキャリアパス古今東西 ~エンジニアが経営に参加してみたら見えるようになった世界~

www.janog.gr.jp

JANOGでは、登壇者が大勢の聴衆に向かって特定のテーマを元に語りかけ、聴衆の質問を受けながら議論を深めていくミーティング形式のプログラムが複数開催されています。プログラムの中には難しい技術的な要素を含むものも多いですが、誰にとっても聞きやすい系のものもあります。

今回私が取り上げるのはその中の一つで、エンジニアのキャリアパスについて語られていたものです。登壇者は4名でそれぞれが

  • エンジニアをやっていたら, ~~~~~になっていました

といった方々です。この中で1人次元が違うなあと感じた方がいるのでその方について思ったことを書いていこうと思います

その方のスペックは

  • 34歳時点でCOO
  • 学生時代にCCNPを取得
  • 転職3回

と言った感じで正直凄すぎて参考にならないと感じましたが、その方が仰っていたことの中で以下のような点は実践することができるのではないかと思いました。

  • 予算・納期のことは常に考える
  • 20代のうちはいいマネージャーを探し徹底的に観察
  • CCNPを取得(大変そう)

他にもマインドのことや管理職になって変わったことなど色々仰られていましたが、果たして自分はエンジニアに向いているのか、管理職・経営に向いているのかも考えさせられるプログラムでした。20代もあっという間に終わる気がしているので日々を大切に過ごしたいです。

おわりに

初めてのJANOGでしたが、多くの刺激をもらったと同時に「いつになったらこの方々のレベルに到達できるんだろうか」と虚無感にも駆られました。まずは「ネットワークエンジニア」を名乗れる段階まで成長し、いつかはJANOGで発表する側、そしてこんな記事を書かれる側に回れる日が来るように、日々精進していきます。

OCXを支える技術 #4 Building the OCX Identity Provider System with Kratos and Hydra / Kratos と Hydra で作る認証認可基盤

Japanese follows English.

和訳は英語の次に記載されています。

Hello, I'm Atreya, a full-stack engineer at BBSakura Networks where I lead the UI team and oversee our identity provider's development and maintenance. I also collaborate closely with our API team, lending a hand in backend tasks if required, since I was involved in backend development as well during the early phases of the project.

Identity management has always been an integral component of modern applications and for software engineers seeking to construct a bespoke identity provider system, understanding the underlying architecture is essential. In this article, I'll be sharing insights into how I built our in-house identity provider system using open-source solutions: Kratos and Hydra.

What is an Identity Provider?

At the core of any robust digital solution is an identity provider (IdP) system. An IdP system is essentially the backbone that supports user authentication (verifying who the user is) and authorization (determining what the user can do).

The Core Components of our IdP System

Our IdP system comprises the following critical components:

  1. OCX Identity Provider: This server is at the heart of our system. Not only does it render the UI, but it also operates as a bridge between Hydra and Kratos. With Kratos already offering a configuration option to integrate Hydra, our Golang server focuses on rendering the UI, handling consent and logout requests, and forwarding the generated access token to our frontend console.

  2. Hydra: An open-source OIDC-compliant OAuth2 Server, Hydra's primary responsibility in our setup is to issue access tokens once the user is authenticated through Kratos. This ensures that our backend only needs to validate the access token for each API request.

  3. Kratos: A headless, open-source identity management system, Kratos provides the authentication mechanics. By being headless, it offers the flexibility to design a UI consistent with our system's overall design and functionality.

The following diagram illustrates how the three components talk to each other:

a sequence diagram illustrating how the three components talk each other. From left to right, the four participants are placed: OAuth 2 Client (Browser), OCX Identity Provider, ORY Hydra, and ORY Kratos. The sequence is as follows:  1. the OAuth 2 Client initiates an authorization code flow or an implicit flow request to Hydra. 2. Hydra verifies that there is no user session, i.e. the end user is not authenticated. 3. Hydra redirects to the OCX Identity Provider and sends a request with login challenge. 4. If there is no valid flow ID, the OCX Identity Provider retrieves the authentication details. 5. If there is no user session, the OCX Identity Provider follows link to '/self-service/login/browser?return_to="/login?refresh=true&login_challenge=xxx"'. 6. Kratos creates and stores a new login flow. 7. Kratos returns 'HTTP 302 Found <selfservice.flows.login.ui_url>?flow=<flow-id>' to the OCX Identity Provider. 8. OCX Identity Provider shows the login UI to the client (browser). 9. The user fills out the form and clicks login. 10. OCX Identity Provider submits the form to Kratos with 'POST /self-service/login/browser?flow=<flow-id>'. 11. Kratos validates the form and sets a cookie on the client (browser). 12. OCX Identity Provider redirects to Hydra's redirect URL with the verifier.  13. Hydra redirects to the consent endpoint '/oauth2/auth/requests/consent' with the concent challenge. 14. OCX Identity Provider obtains and accepts detailed information about the requested scope (OpenID, OCX Portal, etc.) 15. OCX Identity Provider redirects to the Hydra redirect URL with the verifier. 16. Hydra verifies grant. 17. Hydra sends an access token to the client (browser).

Why Kratos and Hydra?

Developing an identity provider from scratch is a daunting task. Instead of reinventing the wheel, we decided to stand on the shoulders of giants: Kratos and Hydra. Our choice was strategic. Both tools are open-source and built using Golang, a language we're familiar and comfortable with. This not only ensures a smooth integration with our backend service, but also makes it feasible for our team to contribute back, be it bug fixes or feature enhancements. I've personally fixed a bug (https://github.com/ory/kratos/pull/2507) and proposed a new feature (https://github.com/ory/kratos/issues/3037) to Kratos which I am currently developing.

On the frontend, we are using Next.js along with NextAuth.js. NextAuth.js simplifies the task of setting up custom OIDC-compliant OAuth2 servers with Next.js. All that needs to be done is register an OAuth client ID and client secret in Hydra and setting up the client ID and secret from Hydra into the NextAuth configuration. The result is a seamless bridge between our frontend console with our identity provider system.

It's worth noting that while Kratos is our choice for storing fundamental authentication details, the majority of business-centric data, including user roles, preferences, and more, is safely housed in our backend database. This clear demarcation ensures system clarity and efficiency.

Why not use services like Auth0 or Okta?

You might wonder, why not adopt mainstream services like Auth0 or Okta? Here's why:

  1. Control Over User Data: With our chosen setup, we have complete control over user data.

  2. Transparency: As Kratos is open-source, we're privy to how the data is saved. This transparency is paramount from a security perspective. Being open-source, Kratos affords us a detailed look into its workings. To illustrate, here's the schema of the tables that store user data.

an ER diagram illustrating the schema of the tables that store user data. There are seven tables (networks, identities, identity_credential_types, identity_credentials, identity_verifiable_addresses, identity_recovery_addresses, identity_credential_identifiers ), with columns and relationships for each table.

Final Thoughts

Incorporating existing, robust solutions like Kratos and Hydra allowed us to focus on optimizing our business logic rather than grappling with the foundational challenges of developing an identity provider from scratch. The synergy between these tools and our custom Golang server resulted in an IdP system that is not only efficient but also scalable and maintainable.

For those interested in diving deeper, Kratos and Hydra both boast excellent documentation ( https://www.ory.sh/docs/ecosystem/projects ) . While we chose the self-hosted open-source versions of Kratos and Hydra, it's worth noting that Ory, the company steering the development of these tools, offers a managed cloud service. This might appeal to those who prefer a managed service over the responsibility of hosting by themselves.

Additionally, an open-source example of Kratos-Hydra integration using Golang can be explored here ( https://github.com/atreya2011/go-kratos-test/tree/hydra-consent ). Building on top of proven platforms accelerates development and ensures that we're working with industry-tested security and performance standards. In essence, it's about smart engineering.

Happy coding!

Disclaimer: This article is not an endorsement of Kratos and Hydra.

日本語訳

こんにちは、BBSakura Networksのフルスタックエンジニア、アトレヤです。UIチームをリードしており、認証認可基盤の開発とメンテナンスも行っています。また、プロジェクトの初期段階ではバックエンド開発にも携わっていたため、必要に応じてAPIチームと密接に協力し、バックエンドのタスクにも手を貸しています。

「アイデンティティ管理」はモダンなアプリケーションに不可欠な要素であり、専用の認証認可基盤を構築しようとするソフトウェアエンジニアにとって、基盤となるアーキテクチャを理解することは極めて重要です。この記事では、オープンソースのソリューション、Kratos と Hydra を使用して認証認可基盤を構築した方法についての知見を共有します。

Identity Provider とは何か?

全ての堅牢なデジタル・ソリューションのコアには、認証認可基盤があります。認証認可基盤は基本的に、ユーザー認証(ユーザーが誰であるかを確認すること)と認可(ユーザーが何をできるかを決定すること)をサポートするバックボーンです。

OCXの認証認可基盤のコアコンポーネント

OCXの認証認可基盤は、以下の重要なコンポーネントで構成されています。

  1. OCX Identity Provider: このサーバーはOCXのシステムの中心にあります。UIをレンダリングするだけでなく、HydraとKratos間のブリッジとしても動作します。Kratosは元々Hydraを統合する設定オプションを提供しており、"OCX Identity Provider" はUIのレンダリング、コンセントとログアウト・リクエストの処理、生成されたアクセストークンをフロントエンドに転送することにフォーカスしています。
  2. Hydra: OIDC準拠のオープンソースOAuth2サーバであるHydraの主な役割は、ユーザがKratosを通して認証された後にアクセストークンを発行することです。これにより、OCXのバックエンドはAPIリクエスト毎にアクセストークンを検証するだけでよくなります。
  3. Kratos: オープンソースのヘッドレスなアイデンティティ管理システムであるKratosは認証の仕組みを提供します。ヘッドレスであることで、OCXのシステム全体のデザインと機能性に合致したUIを設計する柔軟性を提供します。

実際の処理の流れは、以下のシーケンス図を参考にしてください。

コアコンポーネント間のやりとりを示すシーケンス図。左から、OAuth 2 Client (Browser), OCX Identity Provider, ORY Hydra, ORY Kratos の 4 つの登場人物が配置されています。シーケンスは以下の通りです。1. OAuth 2 Client は Hydra に対して 認可コードフローまたはインプリシットフローのリクエストを開始します。2. Hydra はユーザーセッションがない、つまりエンドユーザーが認証されていないことを確認します。3. Hydra は OCX Identity Provider にリダイレクトし、認証のためのチャレンジを含むリクエストを送信します。4. 有効なフロー ID がない場合、OCX Identity Provider は認証情報の詳細を取得します。5. ユーザセッションがない場合、OCX Identity Provider は Kratos に対して '/self-service/login/browser?return_to="/login?refresh=true&login_challenge=xxx"' のリンクを辿ります。6. Kratos は新しいログインフローを作成および保存します。7. Kratos から OCX Identity Provider に 'HTTP 302 Found <selfservice.flows.login.ui_url>?flow=<flow-id>' を返します。8. OCX Identity Provider はクライアント(ブラウザ)にログイン UI を表示させます。9. ユーザーがフォームを埋めてログインをクリックします。10. OCX Identity Provider は Kratos に対して 'POST /self-service/login/browser?flow=<flow-id>' でフォーム送信します。11. Kratos はフォームの有効性を検証し、クライアント(ブラウザ)に cookie を設定します。12. OCX Identity Provider はベリファイアとともに Hydra のリダイレクト URL にリダイレクトします。13. Hydra はチャレンジとともに同意エンドポイント '/oauth2/auth/requests/consent' にリダイレクトします。14. OCX Identity Provider は要求されているスコープ(OpenID, OCX ポータルなど)について詳細な情報を取得し、受け入れます。15. OCX Identity Provider はベリファイアとともに Hydra のリダイレクト URL にリダイレクトします。16. Hydra は権限を確認します。17. Hydra はクライアント(ブラウザ)に対してアクセストークンを送信します。

なぜKratosとHydraなのか?

認証認可基盤をゼロから開発するのは大変な作業です。車輪を再発明するのではなく、我々開発陣は巨人(HydraとKratos)の肩の上に立つことに決めました。この選択は戦略的でした。KratosもHydraもオープンソースで、開発陣が慣れ親しんでいるGolang言語を使って開発されています。これは、OCXのバックエンドとのスムーズな統合を保証するだけでなく、バグ修正や機能拡張などにOCXのチームメンバーが貢献することを可能にします。私自身、バグ ( https://github.com/ory/kratos/pull/2507 ) を修正し、現在開発中の新機能 ( https://github.com/ory/kratos/issues/3037 ) をKratosをメンテしているOry社に提案しました。

フロントエンドでは、Next.jsとNextAuth.jsを使用しています。NextAuth.jsでNext.jsにカスタムのOIDC準拠のOAuth2サーバをセットアップする作業が簡単になります。HydraにOAuthのクライアントIDとシークレットを登録し、NextAuth.jsのコンフイグにこのクライアントIDとシークレットを設定するだけです。その結果、OCXフロントエンドのコンソールと認証認可基盤をシームレスにつなぐことが可能になります。

Kratosは基本的な認証情報を保存するために使用されており、ユーザのロールなどを含むビジネス中心のデータの大部分は、違うバックエンドデータベースに安全に格納されています。この明確な区分により、システムの明瞭性と効率性が保証されます。

なぜAuth0やOktaのようなサービスを使わないのか?

Auth0やOktaのような主流のサービスを採用していない理由は以下の通りとなります。

  1. ユーザデータの管理: ユーザデータを独自に管理することができます。

  2. 透明性: Kratosはオープンソースであるため、データがどのように保存されるかを知ることができます。この透明性はセキュリティの観点から最も重要なことです。オープンソースであるため、その仕組みを見ることもできます。例として、ユーザデータを格納するテーブルのスキーマは以下の通りとなります。

ユーザデータを格納するテーブルのスキーマを表す ER 図。7 つのテーブル(networks, identities, identity_credential_types, identity_credentials, identity_verifiable_addresses, identity_recovery_addresses, identity_credential_identifiers )があり、各テーブルのカラムや関係性が書かれています。

最後に

KratosやHydraのような既存の堅牢なソリューションを組み込むことで、認証認可基盤をゼロから開発する基礎的な課題に取り組むよりも、ビジネスロジックの最適化に集中することができました。これらのツールとカスタムGolangサーバであるOCX Identity Providerとの相乗効果により、効率的であるだけでなく、スケーラブルでメンテナブルな認証認可基盤を構築することができました。

KratosとHydraはどちらも優れたドキュメントがあるのでぜひ読んでみてください ( https://www.ory.sh/docs/ecosystem/projects ) 。OCXではKratosとHydraのセルフホスト型のオープンソース版を選びましたが、これらのツールの開発を主導しているOry社がマネージドクラウドサービスも提供しています。これは、自分でホスティングして責任を負うよりも、マネージドサービスが良いというチームには合っていると思います。

さらに、Golangを使用したKratosとHydraのブリッジ実装のオープンソース例をこのレポ ( https://github.com/atreya2011/go-kratos-test/tree/hydra-consent ) で見ることができます。実績のあるプラットフォームの上に認証認可基盤を構築することで、開発を加速し、業界でテストされたセキュリティとパフォーマンスの標準を確実に使用することができます。要するに、スマートエンジニアリングということです。

ハッピー・コーディング!

www.DeepL.com/Translator (無料版)で翻訳し、修正したものです。

海底ケーブル巡りをしてきました!(南大東島編)

はじめに

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

adventar.org

こんにちは、BBSakura NetworksのOCX開発チームに所属している秋葉です。
普段はOCXのバックエンド側の実装をメインに行っています。
趣味でインフラ設備巡りをすることがあるのですが、先日南大東島に海底ケーブルを見に行く機会がありましたので、写真と共にその様子をお届けします!

本編の前に...

そもそも海底ケーブルがどこに引かれているのかですが、当てもなく片っ端から海沿いを歩いて探すわけにはいきません。(それはそれで楽しそうではありますが...)
海洋状況表示システム(海しる)という便利なサイトがあり、こちらにざっくりと海底ケーブルのルートがまとめられています。

海しるで見た沖縄本島周辺の海底ケーブルMAP。
海しるで見た沖縄本島周辺の海底ケーブルMAP
画像のピンク色の線が海底ケーブルです。沖縄の周りだけでも結構ありますね...!
この地図とgoogle mapを見比べて、地形などからおおよその場所を絞って現地に行きます。

本編スタート!

南大東空港の滑走路と駐機中の旅客機。
南大東空港
(南大東空港で記念に1枚パシャリ)

その1

海しるで見た南大東島周辺の海底ケーブル。島の南西に1本海底ケーブルが引かれている。
海しるで見た南大東島周辺の海底ケーブル
海しるで見てみると、島の南西に1本海底ケーブルが引いてあるようです。
早速行ってみましょう。

NTT西日本南大東無線中継局と林の中に設置された2台の巨大なパラボラアンテナ。
NTT西日本南大東無線中継局のパラボラアンテナ
立派なアンテナが!NTTの無線中継局のようです。
ここからさらに奥へ進むと、海底ケーブルが来ているあたりまで行けそうなので、先に進んでみます。

軽自動車がぎりぎり通れそうなくらいの未舗装路が続いている。道の両端には雑草が生い茂っている。
目当ての海底ケーブルまでの道のり1
おや...

未舗装路が途切れ、ごつごつした岩と生い茂る植物の上に木板で作られた人ひとりが通れそうな道が続いている。
目当ての海底ケーブルまでの道のり2
だんだん道が険しく.....

木板で作られた道も途中で無くなり、気を抜けば足を取られそうなほど足場の険しい岩場が海まで続いている。
目当ての海底ケーブルまでの道のり3
ぎりぎりまで海に近づいたのですが、足場が悪くこれ以上は進まない方が良さそうなためここで断念です><
まわりに陸標やそれっぽい目印などがないか探してみたのですが、見当たらず、、
島に来ている唯一の海底ケーブルのため見てみたかったのですが、、残念!

その2...?

海底ケーブル敷設工事中の看板
海底ケーブル敷設工事中の看板
先ほどの海底ケーブルを探している途中、珍しいものを見つけました。

木々の間を抜けて海の方向に伸びている新設予定のケーブルルートを示すワイヤー。
新設するケーブルのルート1
新設するケーブルのルートのようです!!
ちょうど島を訪れたタイミングが工事期間と被っていたようで、なかなか貴重なものが見れました。

険しい岩場の間を抜けて、海の手前で岩に固定されている新設予定のケーブルルートを示すワイヤー。
新設するケーブルのルート2
こちらはしっかりと海まで!!
(ボカシを入れているのは一緒に見に行った会社の方々です。インフラ設備の写真撮ってる集団って、はたから見ると怪しい人たちに見えますね、、

最後に

既設の海底ケーブルや陸標を見つけることはできませんでしたが、かわりになかなかレアなものが見られたので大変満足です。また来年あたりに様子を見に行ってみようと思います!
以上、海底ケーブル巡り(南大東島編)でした!!

最後に南大東島で見つけたインフラ設備の写真を何枚か♪

木の葉が茂る手前に「南大東村」と書かれたパラボラアンテナが 2 つ並んでいる
「南大東村」と書かれたパラボラアンテナ
南大東対空通信局舎と、聳え立つ2本のアンテナ
南大東対空通信局舎
KDDIの携帯基地局と併設されている小型のパラボラアンテナ
KDDIの携帯基地局
聳え立つNTT西日本南大東大池無線中継所の巨大な鉄塔
NTT西日本南大東大池無線中継所
鉄塔に設置されたパラボラアンテナと、命綱をつけた作業員
NTT西日本南大東大池無線中継所

MPTCPのスケジューラーをeBPFで書けるようになった話

この記事は BBSakura Networks Advent Calendar 2023 の 25 日目の記事です。

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

普段はモバイル開発グループという組織でモバイルコアの開発・運用をしていますが、その傍ら技術広報活動もしています。

技術広報に関しては 1日目の記事 で弊社のみずきさん(@n0mzk|@n0mzk.bsky.social)が書いていますのでぜひご覧ください。

そして自分はこのアドベントカレンダーの主宰もやっております。

今年で(学生時代のアルバイトから数えて)3 回目の主宰ですが、なんと今回は誰かが記事を無理やり 2,3 本書かずとも無事達成しました。

いやー人が集まらず毎年 2 本は書いていたので嬉しいですね。3 年目にしてやっと達成です。なので今年は頑張ったし 25 日の大トリをもらってもよいだろうということで最終日をもらうこととしました。来年も 25 日を取りたいですね。そして 1 本の記事にちゃんと労力を掛けられることって最高なんだなと思いましたね。

無事最終日を迎えられてホッとしております。


さて、雑談は程々にしておくとして、この記事は最近の MPTCP に関してのあれこれと、マルチパス技術の肝であるスケジューラーを eBPF で書けるようになったんだぜ?みたいな話をする記事です。しばらくお付き合いください。

MPTCPとは

MPTCP(Multipath TCP) とは TCP のマルチパス化技術のことです。MPTCP は任意のインターフェースに紐づく複数の TCP コネクション(以降 subflow と呼ぶ)をバンドルして、通信の帯域を拡張したり、適切なパスを使ったりすることで、通信を最適化する技術です。例えば任意のパスの通信品質が悪くなった時に流す流量を減らして制御したり・違うパスにフォールバックするなど Active Active, Active Standby なパスの使い方が可能です。

MPTCP という技術はちょうど 10 年前(2013 年)に RFC6824 MPTCPv0 として RFC 化されました。現在では RFC8684 MPTCPv1 と呼ばれるものが現行の最新です。Linuxカーネルのサポート も 5.6 から v1 のサポートに切り替わりました。

MPTCPスケジューラーとは

MPTCPスケジューラーとは、利用している subflow に対してどれくらいパケットを投げるかを決定するために使われます。

例えば A の subflow(A の Interface を持つパスと言い換えても良い)の調子が悪くなったので B の subflow に流量を増やしたりするのはこのスケジューラーのアルゴリズムで決定されます。

このスケジューラーには BLEST1というアルゴリズムをベースにしたのが現在の Linux のデフォルトで使われていたり、他にも Round-Robin や Lowest-RTT-First2など利用するアプリケーションの最適化戦略によって様々なアルゴリズムに変更可能なことが知られています。

最近の MPTCP を取り巻く環境の話

前述した通り MPTCP 自体は RFC 化から約 10 年の歴史があるのですが、現在ではどれくらい使われているのでしょうか。

Multipath TCP Measurement Service3という論文を眺めてみると、数年で 20 倍に増加したという話が書かれています。

この論文を書いた人たちは ZMap を利用して IPv4 の空間全体と IPv6 Hitlist4を使って IPv6 の一部を現在も計測していて、その計測データを mptcp.io というサイトで可視化しています。

IPv4 を見ると現在では MPTCPv0 を 80 番 ポートで動かしているのは 400k Addresses を超える程に達しました。

IPv4 での MPTCP 利用数のグラフ。徐々に増えており現在では 400k Addresses を超えていることが示されている
IPv4でのMPTCP利用数

MPTCP を取り扱うための整備も進み、Linux では MPTCPv1 を前提にした mptcpd というデーモンも実装されています。

これを利用することで path management を行うことができます。更に付随している mptcpize コマンドラインツールを使うと TCP を利用したアプリケーションを書き換えやリビルド等の変更を実施せずに MPTCP を活用できます。 実際に TCP に対応した iperf3 で使うと以下のようになります。

mptcpize run iperf3 -s

これは 、LD_PRELOAD 環境変数を使い glibc をオーバーライドすることで実現しています。そのためアプリケーションの更新は不要になります。Go 言語では glibc に依存させずにビルドが可能です。このケースでは当然ですが環境に依存しない故に動かすことができません。

最近では Go言語v1.21のパッケージの中でMPTCPサポート が入るようになりました。以下のように書くだけで MPTCP 対応が完了するようになりました。これによって Go でも(リビルドは必要ですがほぼ)TCP と同じ利用方法で使えるので簡単に MPTCP が使えて便利だなと思いました。

lc := &net.ListenConfig{}
lc.SetMultipathTCP(true)
ln, err := lc.Listen(context.Background(), "tcp", *addr)

利用用途としても最近では非常に発達しています。よく知られてる Appleの利用例 以外にも OpenMPTCProuter というプロジェクトでは複数の回線を束ねて帯域を広げるのに MPTCP を利用したルーターを作ったりしています。

また、同じような発想の話で 5G のコンテキストでは ATSSS(Access Traffic Steering, Switching and Splitting)5と呼ばれる仕組みがあります。

3GPP 環境 (e.g. モバイル通信)と Non-3GPP 環境(e.g. Wi-Fi)の終端装置を UPF に直接入れることで、適切なリンクに合わせて処理を行います。

この話については むねあきさんが書かれた記事 が詳しいのでオススメします。

ATSSSのアーキテクチャ図。UPFの中にはMPTCPのProxyとPMF(Performance Measurement Function)があり、そのUPFで3GPP 環境と Non-3GPP 環境を終端して、DataNetworkとの通信を行っている
ATSSSのアーキテクチャ図6

それにしても PMF みたいなやつは、MPTCP なんで適切なスケジューラーを利用すれば問題ないのではないだろうか?みたいな気持ちがありますが、L3 の知見を L4 にフィードバックしたいということ何でしょうね。確かに L4 のレイヤで言われるより長い時間の統計を取って利用することで嬉しくなったりするかもなーとかは思うので気になるところです。

eBPFとは

eBPF(extended Berkeley Packet Filter) は、Linux カーネル内で実行されるプログラムを実行するための拡張可能な仕組みです。元々はネットワークパケットのフィルタリングに使用されていた Berkeley Packet Filter(BPF)を拡張したもので、現在ではさまざまな用途に広がっています。

eBPF は、プログラムを動的に挿入し、実行できる柔軟な仕組みです。これにより、カーネルの様々な部分で動作する小さなプログラムを実行できます。

struct_opsについて

BPF_STRUCT_OPS と呼ばれる特定のカーネル内関数ポインタを実装する仕組みがあります。これは、 Linux v5.3から入りました。 現在は TCP の輻輳制御(tcp_congestion_ops)を eBPF で記述する為に使われていることが知られています。 例えば Cubic と呼ばれる有名な輻輳制御アルゴリズムは現在は eBPF で記述されており、その例がkernelのリポジトリツリーに含まれています

このようにカーネルを弄らなくても輻輳制御アルゴリズムの実装をプラグインのように適用できるので、今日ではお手軽にアプリケーションの特性に合わせた通信環境を実装することが可能になりました。

そして、これを応用して今日では MPTCP のスケジューラーが eBPF で書けるようになりました。最初のサポートは Linux v5.19 から入りました(なお完全なサポートや最適化はまだ取り込まれてない)

つまり任意の評価アルゴリズムを書いて任意のサブフローに好きなようにパケットを流し込むといったことが簡単にできるようになったわけですね。

オレオレMPTCPスケジューラーをちょっと作ってみた

ではこのおもしろ技術に触れてみたいと考えるのが今日の本題です。少し触ってみた経験と実装に簡単な解説をつけつつ話して行きます。

今回は Lowest-RTT-First になるバージョンを実装してみました。ここで指す Lowest-RTT-First というのは SRTT が最小の subflow を選択するアルゴリズムとします。また今回は Upstream に入ってない mptcp_net-next の v6.6.0 を対象に説明していきます。

最小構成の例

ひとまず実装物の説明の前に mptcp_net-next という Upstream に入る前のカーネルに置かれている最小構成のスケジューラー実装である mptcp_bpf_first.c を利用して構造と利用法を説明します。

以下が最小構成であるスケジューラーのコードです。これは、ある subflow 1 つに対してのみパケットを投げ続けることができるスケジューラーです。

末尾に書いてある struct mptcp_sched_ops に関数ポインタを登録することで、実際にスケジューラーとして利用できます。

登録が必要なものは次の4つです

  • init: スケジューラーをロードした時にコールされる関数
  • release: スケジューラーをアンロードした時にコールされる関数
  • get_subflow: MPTCP のパケットを投げるのに呼ばれる関数(スケジューラー機能の main 関数部分に当たるようなところ)
  • name: sysctl を利用して利用するスケジューラーを決定するのでその時に渡す名前
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022, SUSE. */

#include <linux/bpf.h>
#include "bpf_tcp_helpers.h"

char _license[] SEC("license") = "GPL";

SEC("struct_ops/mptcp_sched_first_init")
void BPF_PROG(mptcp_sched_first_init, struct mptcp_sock *msk)
{
}

SEC("struct_ops/mptcp_sched_first_release")
void BPF_PROG(mptcp_sched_first_release, struct mptcp_sock *msk)
{
}

int BPF_STRUCT_OPS(bpf_first_get_subflow, struct mptcp_sock *msk,
           struct mptcp_sched_data *data)
{
    mptcp_subflow_set_scheduled(bpf_mptcp_subflow_ctx_by_pos(data, 0), true);
    return 0;
}

SEC(".struct_ops")
struct mptcp_sched_ops first = {
    .init       = (void *)mptcp_sched_first_init,
    .release    = (void *)mptcp_sched_first_release,
    .get_subflow    = (void *)bpf_first_get_subflow,
    .name       = "bpf_first",
};

実際にこれを使う場合はどのようにするかというと、このようにして実行します。

# ebpfのプログラムロード
sudo bpftool struct_ops register mptcp_bpf_first.o
# sudo bpftool prog list とやると確認ができる

# mptcpのスケジューラーのロード
sudo sysctl -w net.mptcp.scheduler=bpf_first

これである任意の subflow 1 つのみにパケットを流すような実装ができました。

この仕組みは現在デフォルト機能になってるスケジューラーでも同様に mptcp_sched_ops に詰めてあげています。

該当箇所は この部分 です。

static int mptcp_sched_default_get_subflow(struct mptcp_sock *msk,
                       struct mptcp_sched_data *data)
{
    struct sock *ssk;

    ssk = data->reinject ? mptcp_subflow_get_retrans(msk) :
                   mptcp_subflow_get_send(msk);
    if (!ssk)
        return -EINVAL;

    mptcp_subflow_set_scheduled(mptcp_subflow_ctx(ssk), true);
    return 0;
}

これは、解説されてると有用なので、ざっくりデフォルトスケジューラーのプログラムの解説をします。

  • data->reinject: ここには今投げようとしてるデータが再送されるデータなのかどうかのフラグが含まれています。
  • mptcp_subflow_get_retrans: 再送の場合に使われる関数
    • アクティブなサブフローの中でTCP 送信待ちのデータがない場合を利用する
    • 利用できなければ、backup flag がついているエンドポイントを選ぶ
  • mptcp_subflow_get_send: 初めて投げられる時に使われる関数
    • linger time(キューがはけるまでの時間)が一番短いsubflowを使います。
      • sk_wmem_queued とソケットに書き込むバッファメモリが queue を示しています
      • このアルゴリズムは blest アルゴリズムを参考にして作られてるらしいです。
      • ペーサー(送信データのレートみたいなものです)なども含まれているので、ここが MPTCP の輻輳制御部分もやってそうです。

細かいことを知りたい場合は ここで呼ばれる mptcp_subflow_get_send 関数 に書いてあるので、MPTCP のスケジューラーを実装したい人は参考になりますので、一度読んでおくと良いです。

Lowest-RTT-First 対応スケジューラー

Smoothed RTT (SRTT)を利用して最小の RTT を見て動く簡単なやつを改造して作ってみました。SRTT というのはローパスフィルタが入った RTT みたいなものです。これの Linux 上での測定アルゴリズムについては このブログ が割とよくまとまっていますので気になる方はどうぞ。

で今回のコードのあるリポジトリは こちら です。これは、mptcp net-next の開発リポジトリに含まれてる mptcp_bpf_burst.c を改造したものです。

ざっくり説明すると、全ての subflow を探索して、利用可能な subflow の中で、なおかつ RTT の最小値を持つ subflow を優先して投げつけるということをしています。これにより RTT が良い subflow を貪欲に選択し続けるみたいな実装が書けました。

(実際には前述したデフォルトのスケジューラーのように、RTTベースだけではなく、ペーシングやqueueの容量などの複合要因を考えるのが良いというのが正解な気もしますが...w)

static int bpf_minrtt_get_send(struct mptcp_sock *msk,
                  struct mptcp_sched_data *data)
{
    struct mptcp_subflow_context *subflow;
    struct sock *sk = (struct sock *)msk;
    __u32 selected_minrtt = 0;
    __u32 selected_subflow_id = 0;
    __u32 minrtt = 0;
    struct sock *ssk;
    int i;

    for (i = 0; i < data->subflows && i < MPTCP_SUBFLOWS_MAX; i++) {
        subflow = bpf_mptcp_subflow_ctx_by_pos(data, i);
        if (!subflow)
            break;

        ssk = mptcp_subflow_tcp_sock(subflow);
        if (!mptcp_subflow_active(subflow))
            continue;

        const struct tcp_sock *tp = bpf_skc_to_tcp_sock(ssk);
        if (!tp){
            continue;
        }
        
        minrtt = tp->srtt_us;
        if (minrtt < selected_minrtt || (selected_minrtt == 0 && selected_subflow_id == 0)){
            selected_minrtt = tp->srtt_us;
            selected_subflow_id = i;
        }
    }
    mptcp_set_timeout(sk);

    subflow = bpf_mptcp_subflow_ctx_by_pos(data, selected_subflow_id);
    if (!subflow){
        return -1;
    }

out:
    mptcp_subflow_set_scheduled(subflow, true);
    return 0;
}

ということで、eBPF を利用して任意のアルゴリズムのスケジューラーを簡単に実装できました。

カーネルのドキュメントを眺めてみると bpf_prog_run に関して BPF_PROG_TYPE_STRUCT_OPS があるので、Unittest とかもしっかり書けそうだなぁというのも嬉しいポイントだと思いました。

引っかかったポイントは、既存実装に含まれていた bpf_tcp_helpers.hstruct tcp_sock のメンバがカーネル本体に含まれる tcp_sock と全然数が違ったことでした。それにより利用できるメンバが制限されていると思って途方に暮れていました。ですが実際に git bleam などで状況確認をしていくと、どうやら構造体の順番や数は関係なく、メンバのフィールド名が勝手に mapping されていたので、普通に利用したいフィールド名を tcp_sock に追加すれば良いとわかりました。この点でしばらくハマってしまっていました。(正直いまだによく分かってない)

それと普段 cilium/ebpf を利用して go で書いて開発してるのですが、その pure go な実装のローダーの上で BPF_PROG_TYPE_STRUCT_OPS が使えず普段使ってるもので動かず悲しい気持ちになりました。maptype はあるんですけどね...誰もやらなかったら試しに改造してパッチを出そうかなと思います。

終わりに

なぜ MPTCP の話をしたのかというと、SFC のとある博士課程の学生と一緒に研究開発をやっていて(thanks @yas-nyan)、その中で MPTCP に関する知見を得たりしたので論文に書かなかったこと含めてちょっと供養をしようと思ったからでした。

僕も博士受けたいと思っていますが、ネタを思いつけず困ってるので迷走してる感じではあります...:innocent:

個人的には次にちゃんと来る技術はマルチパス技術だろうなと思っており、ここ数年で MPTCP 技術の自由度と実装されてる品質が高くなったなと思っています。なので例えばポリシー投入が可能な SDN などの実装で今までにない世界を掘り下げられるのではないかと感じるようになりました。

例えば L3 の知見をもとに L4 を制御するとか、逆に L7 のセマンティクスを L4 にフィードバックするとか。eBPF でスケジューラ実装が書けるので eBPF Map を通じてならアプリケーションとの連携が比較的簡単です。今後こういうのを応用すると、DHCP や BGP 拡張との合わせ技でマルチパススケジューラーのアルゴリズムやトランスポート層の輻輳制御アルゴリズムをサーバーにフェッチさせて、アプリケーションやDCなどのネットワーク特性に合わせてアルゴリズム選択するなど、プロビジョニングの点でも面白いことが出来そうでワクワクしますよね(妄想)

皆さんも eBPF を使っておもしろスケジューラーを書いたりして楽しみましょう:)

参考文献

OSS による模擬フレッツ光網(IPoE・検討編)

この記事は,『BBSakura Networks Advent Calendar 2023』の 23 日目の記事です……が,遅刻しました! :dogeza:

はじめに

初めましての方は初めまして! そうでない方はこんにちは! BBSakura Networks 株式会社 事業本部の @paina こと佐藤です。

みなさんは,Advent Calendar 12 日目の記事「疑似フレッツ光網内の通信を accel-ppp で再現できるか検証する(検討編)」をご覧になりましたか。 その記事について,ひとつ皆様にお知らせしておきたいことがございます。

「疑似フレッツ光網〜」の記事を書かれた佐藤さんは
私とは別の佐藤さんです!

私の友人や技術者のコミュニティ,時には社内の方からも「paina さんの記事いいね!」というお言葉を頂いており,その度にアナザー佐藤さんに申し訳ない気持ちになっています。(笑)

その後,むしろ佐藤と佐藤をセット販売にしたほうが良いのではないか? という悪乗り……じゃない,共著案が立ち上がるまでに至りました。

Slack のスクリーンキャプチャ t-sato:佐藤あるある対応が大変なので、どっかのタイミングで共著にしましょう! (余力があれば)PPPoE IPv6 編、IPoE 編と続く予定です。 y-kusakabe:余計ややこしくなりそうw t-hayasaka: ばら売り不可セット販売 マスター佐藤
佐藤セット販売のお知らせ

というわけで,今回私は将来の佐藤 & 佐藤の共著を目指して,IPoE を中心にフレッツ光網を模擬する環境を検討してみました。

なぜ模擬フレッツ光網が必要か

弊社および BBIX では,「OCX 光 インターネット」という,フレッツ光を利用した法人向けインターネット接続サービスを提供しています(10 ギガ版プレスリリース1 ギガ版プレスリリース)。 私もこのサービスの開発に少し関わっているのですが,IPoE 版の模擬フレッツ光網はその開発過程で立ち上げたシステムです。

本サービスでは CPE (Customer Premises Equipment) として NTT 東日本・西日本のレンタルのホームゲートウェイ(以下,HGW)をお客様にご利用いただく場合があるのですが, この場合に IPoE と IPv4 over IPv6 の通信を実現するには,事業者が HGW のためのソフトウェアを開発する必要があります。 本番環境では,そのソフトウェアは「フレッツ・ジョイント」という NTT 東西のサービスを用いて各お客様宅の HGW に配信され, 回線からの情報とも連携して IPoE・IPv4 over IPv6 の通信を確立します。

しかし,ソフトウェアの開発時にはまだその回線,つまり自社のサービスがなく,HGW のソフトウェアをデバッグすることが難しい段階もありました。 そのため,私たちは都内某所の検証ラボに IPoE 版の模擬フレッツ光網を構築して HGW のソフトウェア開発を行っていました。 今回この記事で述べる模擬フレッツ光網は,その環境を元に再構成したものです。 この事情から,この模擬フレッツ光網は UNI (User-Network Interface) の視点で本物のフレッツ光網に似た動作をできることを主眼としています。

OSS での再構成

検証ラボに構築した模擬フレッツ光網は,その一部にメーカー製品を使用しています。特にルータ等のネットワーク機器です。 そういったプロプライエタリ製品を利用した環境をそのまま解説するだけでは,気軽に作れるものではなくなってしまいます。 そこで,この記事ではほぼ全ての要素を OSS で実現できるよう,改めて構成を考えました。 しかし,今回はその構成を検討するのみで,十分な動作確認はできていません。 今後,実際に CPE を接続してみたりして,本当に模擬できているか確認し,「実装編」をお送りしたいと思います。

フレッツ光の回線種別について

フレッツ光に必要な要素を考える前に,回線種別について整理します。 フレッツ光では,RA (Router Advertisement) で /64 のプレフィクスを配る回線と, DHCPv6-PD で通常 /56 のプレフィクス*1を配る回線があります。 前者を RA 回線・後者を PD 回線と呼んだりします。どちらの回線になるかは,下記の表に従います。

RA 回線・PD 回線の場合分け

フレッツ光ネクスト フレッツ光クロス
NTT 東日本管内 ひかり電話契約なし RA PD
ひかり電話契約あり PD PD
NTT 西日本管内 PD PD

RA 回線は場合分けの上では NTT 東日本管内・ひかり電話契約あり・フレッツ光ネクストの回線のみのパターンではありますが,そのユーザは多いようです。 今回は RA 回線を模擬するかたちで,構成を検討しました。

ネットワーク構成

まず,ネットワークの全体像を示します。

「収容ルータ VyOS」を中心にユーザネットワークが 2001:db8:817:1::/64・2001:db8:817:2::/64・link-local only でつながっている。前者 2 つが USER1・USER2 の UNI (RA) で,後者は USER3 UNI (PD) で今後検討となっている。収容ルータにはさらに IPv6 Internet と,2001:db8:817::/64 のリンクを介して「各サービス用 FreeBSD ホスト」がつながっている。ホストには「VNE DNS キャッシュサーバ : Unbound」・「SNTP サーバ : ntpd」・「DHCPv6 サーバ : isc-dhcp」・「HTTP 経路サーバ : Nginx 」がインストールされている。
模擬フレッツ光 ネットワーク構成

ごらんの通りあまり複雑な構成は必要ありません。 今回は UNI の視点でフレッツ光網を模擬しているので,CPE に対して適切なプロトコルで情報を与えて通信を確立できればよいからです。 必要なものは,何かしらの IPv6 インターネットへのリーチャビリティと,VyOS のホスト,FreeBSD のホストで,それらは仮想基盤の上に構築してもベアメタル PC を組み合わせてもよいです。

ソフトウェア構成・設定例

次に,使用するソフトウェアとそれが担う機能,設定例などを示します。

サービスホスト : FreeBSD

各ソフトウェアを提供するために,Unix 系 OS のホストが必要です。 Linux でもよいのですが,私はちょっとした検証にはネットワークに関する設定が簡単にできる FreeBSD を利用することも多いです。 rc.conf を下記のように設定し,アドレスやサービスの立ち上げを行います。

/etc/rc.conf

hostname="EXP-FLETS-SV01"
ifconfig_vmx0="up"  # vmx0: 検証ネットワークへ接続するインターフェイス
ifconfig_vmx0_ipv6="inet6 2001:db8:817::3/64"  # vmx0 に設定する IPv6 アドレス

ifconfig_vmx1="DHCP"  # マネジメント等へのアクセスは別のインターフェイスで設定

ifconfig_lo0_alias0="inet6 2404:1a8:7f01:b::3/128"  # 模擬 DNS キャッシュサーバのアドレス #1
ifconfig_lo0_alias1="inet6 2404:1a8:7f01:a::3/128"  # 模擬 DNS キャッシュサーバのアドレス #2
ifconfig_lo0_alias2="inet6 2404:1a8:1102::b/128"  # 模擬 SNTP サーバのアドレス #1
ifconfig_lo0_alias3="inet6 2404:1a8:1102::a/128"  # 模擬 SNTP サーバのアドレス #2
ifconfig_lo0_alias4="inet6 2404:1a8:c023:3201::15/128"  # 模擬 HTTP 経路サーバのアドレス

ipv6_route_0="-inet6 2001:db8:817::/48 2001:db8:817::2"  # ユーザ向けのスタティックルート

ntpd_enable="YES"  # ntpd を起動 (模擬 SNTP キャッシュサーバ)
unbound_enable="YES"  # Unbound を起動 (模擬 DNS キャッシュサーバ)
dhcpd6_enable="YES"  # isc-dhcp-server を起動 (DHCPv6 サーバ)

収容ルータ : VyOS

収容ルータは VyOS で模擬します。 まず,IPv6 インターネットへの到達性を RA を受けるなどして確保します。 模擬のユーザに割り当てるアドレスがグローバルなものでない場合は,NAT66 でインターネットに到達できるようにしておくと良いでしょう。

set interfaces ethernet eth0 ipv6 address autoconf
set nat66 source rule 1000 source prefix '2001:db8:817::/48'
set nat66 source rule 1000 translation address 'masquerade'

サービスホストとのリンクのアドレスを設定し,サービスのアドレスへのスタティックルートを設定します。

set interfaces ethernet eth1 address '2001:db8:817::2/64'
set protocols static route6 2404:1a8:7f01:a::3/128 next-hop 2001:db8:817::3
set protocols static route6 2404:1a8:7f01:b::3/128 next-hop 2001:db8:817::3
set protocols static route6 2404:1a8:1102::a/128 next-hop 2001:db8:817::3
set protocols static route6 2404:1a8:1102::b/128 next-hop 2001:db8:817::3
set protocols static route6 2404:1a8:c023:3201::15/128 next-hop 2001:db8:817::3

ユーザ向けのインターフェイスにアドレスをつけ,RA と DHCPv6 でアドレス等の情報を配布するようにします。

set interfaces ethernet eth2 address '2001:db8:817:1::fffe/64'
set interfaces ethernet eth3 address '2001:db8:817:2::fffe/64'
set service dhcpv6-relay listen-interface eth2
set service dhcpv6-relay listen-interface eth3
set service dhcpv6-relay upstream-interface eth1 address '2001:db8:817::3'
set service router-advert interface eth2 prefix 2001:db8:817:1::/64
set service router-advert interface eth3 prefix 2001:db8:817:2::/64

DHCPv6 サーバ : isc-dhcp-server

PD 回線で DHCPv6-PD にてプレフィックスを委譲し,また PD 回線と RA 回線の両方で DNS キャッシュサーバのアドレスなどを配布する,DHCPv6 サーバです。 DHCPv6 サーバには,WIDE Project による実装や,ISC による実装 isc-dhcp,同じく ISC による Kea DHCP Server などがあります。今回は一般によく使われておりシンプルな isc-dhcp を使用しました。 DHCPv6 はユーザ収容を行う VyOS でもサポートしていますが,細かい制御を行うために VyOS は DHCPv6 Relay とし DHCPv6 Server は別途立ち上げました。

今回は RA 回線を模擬しますので,設定は次のようになります。

/usr/local/etc/dhcpd6.conf

option dhcp6.name-servers 2404:1a8:7f01:b::3, 2404:1a8:7f01:a::3;
option dhcp6.domain-search "flets-east.jp", "iptvf.jp";
option dhcp6.sntp-servers 2404:1a8:1102::b, 2404:1a8:1102::a;

subnet6 2001:db8:817:1::/64 {}
subnet6 2001:db8:817:2::/64 {}

模擬 VNE DNS キャッシュサーバ : Unbound

VNE が提供する DNS キャッシュサーバです。キャッシュサーバとして一般的な実装の Unbound を使用しました。 後述する HTTP 経路情報サーバなど,フレッツ光網内のみで利用されるドメイン名を local-data として宣言しておくとよいでしょう。

/usr/local/etc/unbound/local-data.conf

server:
        local-data: "route-info.flets-east.jp 10m AAAA 2404:1a8:c023:3201::15"

SNTP サーバ: ntpd

フレッツ光の DHCPv6 で配布される SNTP サーバです。NTP サーバのリファレンス実装である ntpd の FreeBSD に付属するバージョンを使用しました。 ntpd はフルセットの NTP だけでなく SNTP にも対応しているので,そのまま利用可能です。 FreeBSD の ntpd は rc.confenable するだけでクエリを受け付けるようになるので ntp.conf の設定は不要と思います。

HTTP 経路情報サーバ: Nginx

フレッツ光には,各 VNE が利用しているプレフィックスの一覧を取得する,経路情報サーバが存在します。 実態としては HTTP サーバで,NTT 東日本管内では下記のように curl 等でアクセスできます。

curl -H 'Connection: close' http://route-info.flets-east.jp:49881/v6/route-info

一部の CPE 製品はこれにアクセスして自身に割り当てられたアドレスを検索することがありますので,これも準備しておくとよいです。 模擬 VNE DNS キャッシュサーバに設定した AAAA レコードのアドレスの tcp/49981 で Nginx を立ち上げると模擬できます。

/usr/local/etc/nginx/nginx.conf

http {
    # (snip)
    server {
        listen [::]:49881;
        server_name route-info.flets-east.jp;

        location / {
            root /var/www/route-info.flets-east.jp;
        }
    }
}

あとは curl 等で取得した情報をファイルとして /var/www/route-info.flets-east.jp/v6/route-info に書いて置きます。 デバッグのために適宜編集してもよいでしょう。

まとめ

この記事では,フレッツ光網とその RA 回線を模擬するために,OSS で下記の機能を実現する検討をしました。

  • 回線への RA・DHCPv6 でのアドレス等の配布
  • 模擬 VNE DNS キャッシュサーバ
  • 模擬 SNTP サーバ
  • 模擬 HTTP 経路情報サーバ

今後は下記のようにさらに詳細化を進めて,「実装編」の記事の公開を目指してみます。佐藤 & 佐藤共著で!

  • この環境に実際に CPE を接続して機能不足等なく模擬できているか確認する
  • PD 回線も模擬できる構成を検討する
  • IPv4 over IPv6 も模擬し IPv4 でも通信できるようにする
    • MAP-E や DS-Lite 等のソフトウェアを調査する必要あり

宣伝コーナー

  • 弊社と BBIX では,フレッツ光を利用したインターネット接続サービス「OCX 光 インターネット」を提供しています。ご検討ください。
  • この記事を詳細化したものを,コミックマーケット103で頒布するかもしれません。
    • コミックマーケットへの参加,及び文書の頒布は業務外ですので,詳細は私の X/Twitter アカウントでご確認ください。

参考

*1:一部,もっと短いプレフィクスを配る回線があるそうですが,現存するかは不明です

試行錯誤してProxmox VE上でvJunosEvolvedを動かしたりPyEZを動かしたり

この記事は BBSakura Networks Advent Calendar 2023 の 22日目の記事です。

adventar.org

はじめに

こんにちは、BBSakura Networksでバックエンド開発をしている秋山です。普段はOCXの開発をしており、OCXの機能追加やクラウド事業者とのAPI連携に尽力しています。最近はバックエンド開発の他にもネットワーク開発に興味を持っており、社内ラボ環境の整備を目的とした石狩プロジェクトなどにも顔を出したりしています。最近、石狩プロジェクトのメンバーのおかげでProxmox VEを通じて自由にVMを立てたり消したりできる環境ができたので、自分も便乗してたまに触ったりしています。今回は、その中で得た知見としてVMの立て方やconfig投入自動化の仕方などについて解説したいと思います。

blog.bbsakura.net

今回は

  • NW自動化の勉強がしたいと思い、NW機器上でPythonコードを走らせられるPyEZに興味があった
  • 今年公開された大手ベンダーの新しい仮想イメージを触って見たかった
  • アカウント登録が不要でハードルが低い

などの理由からvJunosEvolvedをProxmox VE上で動かしてみることにしました。

仮想イメージを取得

仮想イメージは Downloads から取得できます。DLした仮想イメージはノード内の任意ディレクトリに移動させます。 ノードの/home/akiyama配下にvJunosEvolved-23.2R1-S1.8-EVO.qcow2が存在している画像

仮想イメージをインポート

まずはvJunosEvolvedを動作させるための最小要件に合わせて空のVMを作成します。

ハードウェアおよびソフトウェアの最小要件 |vJunosEvolved |ジュニパーネットワークス

以下のコマンドを実行しましょう。(1行です)

# qm create 103 --cores 4 --memory 8192 --cpu host --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci --ostype l26

ノードのshellから以下のコマンドを実行し、仮想イメージを取り込みます。

# qm importdisk 103 <your_path>/vJunosEvolved-23.2R1-S1.8-EVO.qcow2 <your_storage_name>
# qm set 103 --virtio0 <your_storage_name>:vm-103-disk-0
# qm set 103 --boot order=virtio0
# qm set 103 --serial0 socket

起動にはSMBIOSの設定も必要になります。

KVMでのvJunosEvolvedの展開と管理 |vJunosEvolved |ジュニパーネットワークス

VMのOptionsからSMBIOS(type 1)の各欄に以下を入力します。

manufacturer: Bochs
product: Bochs
serial: chassis_no=0:slot=0:type=1:assembly_id=0x0D20:platform=251:master=0: channelized=yes

前述のパラメータがSMBIOS(type 1)の各欄に入力されている様子

その後、ノードのshellから/etc/pve/qemu-server/[vmid].confを編集し、argsを2行追加します。

# nano /etc/pve/qemu-server/103.conf
boot: order=scsi1;scsi0;ide2;net0
cores: 4
cpu: x86-64-v2-AES
ide2: none,media=cdrom
memory: 8192
meta: creation-qemu=8.1.2,ctime=1703206221
name: vJunosEvolved
net0: virtio=BC:24:11:90:EA:A2,bridge=vmbr0
numa: 0
ostype: l26
scsi0: local-lvm:vm-103-disk-0,iothread=1,size=32G
scsi1: local-lvm:vm-103-disk-1,iothread=1,size=40G
scsihw: virtio-scsi-single
smbios1: uuid=dca71814-b638-4735-8eae-9a62237a7e2c,manufacturer=Qm9jaHM=,product=Qm9jaHM=,serial=Y2hhc3N>
sockets: 1
vmgenid: bb9c4e40-e285-47f5-bb3c-0fffec77746b
args: -smbios type=0,vendor=Bochs,version=Bochs
args: -smbios type=3,manufacturer=Bochs

VMの起動

VMを起動して動作を確認します。

# qm start 103
# qm terminal 103

ログイン情報を求められた際は「root」と入力すれば入れるはずです。
ログインと同時にエラーログが流れたりして見にくいので、以下のコマンドを実行し、少し待つと止まります。

# cli
# configure
# delete chassis auto-image-upgrade
# set system root-authentication plain-text-password
# commit and-quit

以上でvJunosEvolvedの設定は終わりです。
環境構築は完了したので、これから自動化やアプリケーションの動作検証など頑張ってやっていきたいと思います。
実際にJuniperの機器が手元になくても、config投入を行うスクリプト等の動作検証ができるのは非常に便利だと感じました。
また、Proxmox VEもVMの設定が非常に楽で、慣れれば15~30分で好きな仮想イメージを積んだVMが立てれるようになりそうです。

PyEZでconfigを操作してみる

環境構築ができたということで、config投入の自動化に取り組んでみました。その第一歩としてインターフェースの有効化、無効化を行うconfigをPyEZを用いたPythonコードから投入できるようにします。
PyEZとはJunos OSに対するPythonライブラリであり、これを使用することで、Junos機器への設定や管理が可能になります。

以下のコード(interface_manip.py)をVM内の/var/db/scripts/op/配下にscpなりを使って置きます。

from jnpr.junos import Device
from jnpr.junos.utils.config import Config
from jnpr.junos.exception import *

def generate_interface_config(interface_name, disable=True):
    if disable:
        disable_tag = "<disable/>"
    else:
        disable_tag = "<disable operation=\"delete\"/>"
    return f"""
        <configuration>
            <interfaces>
                <interface>
                    <name>{interface_name}</name>
                    {disable_tag}
                </interface>
            </interfaces>
        </configuration>
    """


def apply_configuration_changes(config_utility, config_changes):
    try:
        config_utility.lock()
        config_utility.load(config_changes, format="xml", merge=True)
        config_utility.commit()
    except (LockError, ConfigLoadError, CommitError) as err:
        print(f"Error: {err}")
        return False
    finally:
        try:
            config_utility.unlock()
        except UnlockError:
            print("Error: Unable to unlock configuration")
            return False
    return True

def main():
    with Device() as dev:
        dev.bind(cu=Config)
        if apply_configuration_changes(dev.cu, generate_interface_config("et-0/0/1", True)):
            print("Configured successfully.")
        else:
            print("Configuration error")

if __name__ == "__main__":
    main()

自分は以下のコマンドでsshを有効化し、ユーザを作り、scpで送りました。
アドレスの設定なども適宜行なって下さい。

# configure
# set system services ssh
# set system login user akiyama authentication plain-text-password
# set system login user akiyama class super-user
# commit and-quit

Pythonをスクリプト言語として設定し、操作スクリプト(op script)の登録をするために、/var/db/scripts/op/interface_manip.pyがある状態で以下のコマンドを実行します。

# configure
# set system services netconf ssh
# set system scripts language python3
# set system scripts op file interface_manip.py
# commit and-quit

CLIモードで以下のコマンドを実行すると、Pythonコードが実行されます

# op interface_manip.py

interface_manip.pyの実行前後で特定のインターフェースが無効化されているのが分かります。 全てのインターフェース(et-0/0/0からet-0/0/3)がuplinkしている et-0/0/1だけがdownlinkしている

終わりに

今まで開発の中でちょくちょく仮想アプライアンスは出てきていたのですが、あまり理解しておらず、今回のProxmox VEの操作を通じて一気に理解が深まった気がします。ただ検証環境を整えたところで終わっては勿体無いので、ネットワーク×アプリケーションでどんなことができるかこれから色々試していきたいです。

百番煎じだけど、チームの Github Actions 活用方法とデプロイの流れを紹介してみる

はじめに

この記事は BBSakura Networks Advent Calendar 2023 の 21 日目の記事です。 adventar.org

こんにちは、BBSakura Networks のシステム管理部に所属している蟹江(@kanix2929)です。普段は BBIX から委託されているシステムなどの開発・運用がメインで、ネットワークエンジニア / オペレーターを手助けするためのシステム構築・自動化に尽力しています。

最近、複数人で同時並行的に 1 つのコードを触る場合が増えてきたことから、あらためてチーム内で開発プロセス(GitHub の運用方法など)についてまとめたので紹介してみます。

環境について

  • 本番環境の構成
    • VM 上で Docker コンテナを起動させている
      • docker-compose.yml でコンテナ管理している
    • コンテナが動いているサーバーはプロキシ配下
  • ステージング環境の構成
    • 本番環境を模している(データだけ違う)
  • CI/CD ツールとして Github Actions を主に使っている
    • コード管理に GitHub を使用しているので運用が楽

開発の流れとルール

ざっと絵にするとこんな感じ

開発者がプルリクを出すことでGitHub Actionsが自動的にステージング用と本番環境用イメージを作成して開発していく様子を描いています
GitHub Actionsを使用した開発フローの図

流れを言語化すると

  1. コードを修正してプッシュし、main ブランチへのプルリクを作成
    • プルリク作成時にブランチ名のタグを打った Docker イメージが自動生成され、Docker Hub にプッシュされる(build-image.yml
    • 後述の Release Please のためにプルリクのタイトルは Conventional Commits に従う必要あり
      • タイトルをチェックするワークフローも作成済み(lint-PR-title.yml
  2. プルリク作成者が自身でステージング環境にて動作確認
    • ステージングのコンテナを入れ替えるときは Slack で一声かける
  3. 自身での動作確認後、チームメンバーにレビュー & 動作確認を依頼する
  4. レビュアーは該当プルリクが問題ないことを確認して、プルリクをマージする
    • 基本的にはレビュアーがマージする
    • プルリクのマージは Squash and merge
    • リポジトリの設定でも制限
  5. マージすると main タグの Docker イメージが生成される(build-image.yml
  6. マージ時に Release Please のワークフローも動いて Bot がプルリクを生成するので確認してマージする
    • GitHub 上で Semantic Versioning に従ってタグが打たれる
    • 上記 Version をタグ名とした Docker イメージが自動生成され、Docker Hub にプッシュされる(build-image.yml
  7. 本番へリリース
    • コンテナを立ち上げるのは基本人手
    • デプロイを自動化しても良いが、結局確認が必要なので
      • みんなが使っているステージング環境において、どのタイミングでコンテナ立ち上げ直していいのかちょっと決めづらいのでそちらも人手

もろもろの採用理由

  • Squash and merge の理由
    • 開発していくときのコミットがある程度適当でも他人に影響が無い
      • 開発する人が開発しやすい粒度でコミットしたい
      • Create a merge commit だと、コミット粒度についてチームで統一しておかないと逆に見づらくなると思っている
      • もしバグ含んだプルリクをマージしてしまった場合は、該当プルリクをまるっと Revert するか Fix のコミットを当てる
    • 「1 機能 1 プルリク」にすることで、「1 機能 1 コミット」となるので、コミット履歴がスッキリして見やすい
    • プルリクのタイトルだけ見れば良いので、release-please.yml でのバージョン管理が楽
  • プルリク作成時にイメージを作成する理由
    • 各人がステージング環境での動作確認をしやすくするため

おわりに

GitHub Actions をメインとした CI/CD についての紹介は今さら感ありますが、まだ GitHub の運用がざっくりしているチームの方たちの参考になればなあ、と思っています。 今回紹介した方法が完成形というわけではないので、これからも柔軟に運用をアップデートしていく予定です。 また、みなさんの開発体制で「もっと良い方法があるよ」ということがあれば、ぜひ教えていただきたいです!

[参考]

参考として yml ファイルをいくつか貼っておきます。

build-image.yml

単純に Docker イメージを生成するための yml ファイルです。

name: build-and-push-to-dockerhub

env:
  IMAGE_NAME: hogehoge

on:
  workflow_dispatch:
  push:
    branches:
      - "main"
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+*'
  pull_request:
    branches:
      - "main"

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set TAG
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            TAG=$(echo "${{ github.head_ref }}" | tr '/' '-')
          else
            TAG=$(echo "${{ github.ref_name }}" | tr '/' '-')
          fi
          echo "TAG=${TAG}" >> $GITHUB_ENV
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${IMAGE_NAME}
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: fugafuga
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: "${{ env.IMAGE_NAME }}:${{ env.TAG }}"
          labels: ${{ steps.meta.outputs.labels }}

ちなみに

次のように、本番環境とステージング環境でイメージビルド時に使う変数だけ変更したい場合には、スクリプトで無理やり埋め込んだりしています。

  • Docker でマルチステージビルドを使っているのでイメージのビルドとコンテナの起動のステージが違う
  • NEXT_PUBLIC_* のようなビルド時に展開される変数を使っている
    • 本番環境とステージングで別の変数を使いたい

参考として yml はここに折りたたんでおきます。

タグごとに変数を指定する Docker イメージを生成するための yml ファイルです。

name: build-image-and-push-to-dockerhub

env:
  IMAGE_NAME: hogehoge

on:
  workflow_dispatch:
  push:
    branches:
      - main
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+*'
  pull_request:
    branches:
      - main

jobs:
  build-image:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v4
      - name: Set TAG
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            TAG=$(echo "${{ github.head_ref }}" | tr '/' '-')
          else
            TAG=$(echo "${{ github.ref_name }}" | tr '/' '-')
          fi
          echo "TAG=${TAG}" >> $GITHUB_ENV
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${IMAGE_NAME}
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username:  fugafuga
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      # 以下で変数を設定している
      - name: SET ENV for Dockerfile
        run: |
          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            TAG=$(echo "${{ github.head_ref }}" | tr '/' '-')
            NEXT_PUBLIC_EXAMPLE_ENV=http://example-staging.com
          else
            TAG=$(echo "${{ github.ref_name }}" | tr '/' '-')
            NEXT_PUBLIC_EXAMPLE_ENV=http://example.com
          fi
          echo "TAG=${TAG}" >> $GITHUB_ENV
          echo "NEXT_PUBLIC_EXAMPLE_ENV=${NEXT_PUBLIC_EXAMPLE_ENV}" >> $GITHUB_ENV
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: hogehoge
          file: ./docker/Dockerfile
          push: true
          tags: "${{ env.IMAGE_NAME }}:${{ env.TAG }}"
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            NEXT_PUBLIC_EXAMPLE_ENV=${{ env.NEXT_PUBLIC_EXAMPLE_ENV }}

DockerfileARG コマンドと ENV コマンドで環境変数を外から指定できるようにしておく必要があります。

ARG NEXT_PUBLIC_EXAMPLE_ENV=http://example.com
ENV NEXT_PUBLIC_EXAMPLE_ENV=${NEXT_PUBLIC_EXAMPLE_ENV}
...

release-please.yml

Release Please の yml ファイルです。Version タグを打った Docker イメージも生成するようにしています。

name: release-please

env:
  IMAGE_NAME: hogehoge

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - name: Run release-please
        id: release
        uses: google-github-actions/release-please-action@v3
        with:
          release-type: go
          package-name: "hogehoge"
      - name: Set TAG from release-please
        if: ${{ steps.release.outputs.release_created }}
        run: |
          echo "Release Tag - ${{ steps.release.outputs.tag_name }}"
          if [ -n "${{ steps.release.outputs.tag_name }}" ]; then
            echo "TAG=${{ steps.release.outputs.tag_name }}" >> $GITHUB_ENV
          else
            echo "TAG=latest" >> $GITHUB_ENV
          fi
      - name: Checkout for tag created
        if: ${{ steps.release.outputs.release_created }}
        uses: actions/checkout@v4
      - name: Docker meta for tag created
        if: ${{ steps.release.outputs.release_created }}
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${IMAGE_NAME}
      - name: Login to DockerHub for tag created
        if: ${{ steps.release.outputs.release_created }}
        uses: docker/login-action@v3
        with:
          username: fugafuga
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push for tag created
        if: ${{ steps.release.outputs.release_created }}
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: "${{ env.IMAGE_NAME }}:${{ env.TAG }}"
          labels: ${{ steps.meta.outputs.labels }}

lint-PR-title.yml

プルリクのタイトルが Conventional Commits に従っているかチェックするための yml ファイルです。 https://github.com/dreampulse/action-lint-pull-request-title をそのまま使っています。

github.com

name: "Lint PR Title"
on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: dreampulse/action-lint-pull-request-title@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

auto-test.yml

テストを実行するための yml ファイルです。このあたりは各プロジェクトでいい感じに。

name: Lint, Test & Build

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  lint-test-build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: ^1.21
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Lint
        uses: golangci/golangci-lint-action@v3
        with:
          version: latest
          args: --timeout=5m
      - name: Test
        run: make test # make コマンドを使用してテストしている場合
      - name: Build 
        run: go build .