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 .

protovalidateにIPプリフィックスをバリデーションするためのカスタム関数を追加した話

この記事は BBSakura Networks Advent Calendar 2023 の 12/20 の記事です。

こんにちは。BBSakura Networksで全社のテックリードとモバイルコアの開発をするチームのリーダーを兼務している、日下部(@higebu)です。

はじめに

BBSakuraではシステム間通信にProtocol Buffersを多用しています。例えば、さくらのセキュアモバイルコネクトでは、さくらのクラウドのバックエンドとの連携やモバイルコアのコンポーネント間の通信に使用していて、OCXのバックエンドでも使用しています。

どちらのサービスもネットワークを制御するシステムで、入力されたパラメータのバリデーションや整合性のチェックに苦労しています。OCXでの苦労話はJANOG51での川畑の発表や下記の記事で紹介しています。

blog.bbsakura.net

今回、protovalidateにIPプリフィックスをバリデーションするための機能を追加したので、追加した機能の紹介、使い方、今後の話をしたいと思います。

protovalidateとは

protovalidateはBufが開発している、Protocol Buffersで定義されたメッセージのバリデーションのためのライブラリ群で、protoc-gen-validate(PGV)の後継とされています。 Common Expression Language(CEL)ベースになっているため、複雑なバリデーションルールを簡潔に記述することが可能です。 各言語用のライブラリはGo、C++、Python、Javaのベータ版がリリースされており、TypeScriptにも対応予定となっています。

詳しくは下記のBufの記事や公式リポジトリのREADMEを参照してください。 buf.build

CELはGoogleが作った式の評価のためのシンプルな言語で、Protocol Buffersを使って実装されています。 最近ではKubernetesのCRDのバリデーションAdmission Policiesのバリデーションでも使われているため、知っている方もいるかもしれません。

追加した機能の紹介

CELのカスタム関数として isIpPrefix() という関数を追加しています。ドキュメントにも同じことが書いてあるのですが下記のような仕様になっています。

  • string.isIpPrefix() -> bool
  • string.isIpPrefix(4) -> bool
  • string.isIpPrefix(6) -> bool
  • string.isIpPrefix(true) -> bool
  • string.isIpPrefix(4,true) -> bool
  • string.isIpPrefix(6,true) -> bool

引数には4/6に加えて、true/falseを取るようになっています。4/6はIPv4/IPv6のことで、trueの場合はネットワークアドレスかどうかをチェックしています。

これを使うと、下記のようにIPv4のプレフィックスかどうかをチェックするバリデーションルールを書くことができます。

message IPv4Route {
  string ipv4_prefix = 1 [(buf.validate.field).cel = {
    id: "ipv4_route.ipv4_prefix",
    message: "value must be a valid IPv4 prefix",
    expression: "this.isIpPrefix(4,true)"
  }];
}

ただ、毎回このように書くのはだるいのと、 isIp() と雰囲気を合わせるため、下記のように書けるようにもしています。

message IPv4Route {
  string ipv4_prefix = 1 [(buf.validate.field).string.ipv4_prefix = true];
}

先ほど説明した、isIpPrefix() の引数のパターンに合わせて、 string.ip_with_prefixlen string.ipv4_with_prefixlen string.ipv6_with_prefixlen string.ip_prefix string.ipv4_prefix string.ipv6_prefix を定義しています。

詳しくは buf/validate/validate.proto を見てください。

使い方

ここからはBufによるprotoファイルからのコード生成についてある程度知っているという前提で説明していきます。 あまり知らないという方は Bufのドキュメントを見に行っていただければと思います。

まず、 buf.yamldepsbuf.build/bufbuild/protovalidate を足す必要があります。例としては下記のような感じです。

version: v1
name: buf.build/higebu/example
deps:
  - buf.build/bufbuild/protovalidate
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

buf.gen.yaml は好きな言語のコード生成の設定をしていただければ良いです。ここではGoのコードを生成したいので下記のようにします。

version: v1
plugins:
  - plugin: buf.build/protocolbuffers/go:v1.31.0
    out: gen/go
    opt: paths=source_relative
  - plugin: buf.build/grpc/go:v1.3.0
    out: gen/go
    opt:
      - paths=source_relative

次に、protoファイルを作ります。今回は、雑に proto/example/v1/example.proto とします。例として、IPv4/IPv6アドレスの追加、IPv4/IPv6ルートの追加を行うAPIを持ったサービスを定義しています。 IPアドレスの追加は、インターフェースへのIPアドレスの追加、つまり ip addr add のイメージで、ルートの追加は ip route add のイメージでお願いします。

syntax = "proto3";

package higebu.example.v1;

import "buf/validate/validate.proto";

option go_package = "github.com/higebu/protovalidate-ip-prefix-example/gen/go/higebu/example/v1;examplev1";

service ExampleService {
  rpc AddIPv4Address(AddIPv4AddressRequest) returns (AddIPv4AddressResponse) {}
  rpc AddIPv6Address(AddIPv6AddressRequest) returns (AddIPv6AddressResponse) {}
  rpc AddIPv4Route(AddIPv4RouteRequest) returns (AddIPv4RouteResponse) {}
  rpc AddIPv6Route(AddIPv6RouteRequest) returns (AddIPv6RouteResponse) {}
}

message AddIPv4AddressRequest {
  string name = 1;
  string address = 2 [(buf.validate.field).string.ipv4_with_prefixlen = true];
}

message AddIPv4AddressResponse {
  string name = 1;
  string address = 2;
}

message AddIPv6AddressRequest {
  string name = 1;
  string address = 2 [(buf.validate.field).string.ipv6_with_prefixlen = true];
}

message AddIPv6AddressResponse {
  string name = 1;
  string address = 2;
}

message AddIPv4RouteRequest {
  string name = 1;
  string prefix = 2 [(buf.validate.field).string.ipv4_prefix = true];
  string nexthop = 3 [(buf.validate.field).string.ipv4 = true];
}

message AddIPv4RouteResponse {
  string name = 1;
  string prefix = 2;
  string nexthop = 3;
}

message AddIPv6RouteRequest {
  string name = 1;
  string prefix = 2 [(buf.validate.field).string.ipv6_prefix = true];
  string nexthop = 3 [(buf.validate.field).string.ipv6 = true];
}

message AddIPv6RouteResponse {
  string name = 1;
  string prefix = 2;
  string nexthop = 3;
}

AddIPv4AddressRequest では、 ipv4_with_prefixlen を使い、 192.168.100.5/24 のようにプレフィックス長が付いているIPアドレスのみ受け付けるようにしています。 また、 AddIPv4RouteRequest では prefixipv4_prefix を使い、 192.168.100.0/24 のようにアドレス部分がネットワークアドレスになっていて、プレフィックス長が付いている値のみ受け付けるようにしています。 IPv6についても同様です。

protoファイルができたら、buf generate でGoのコードを生成します。問題なければ gen/go ディレクトリ配下にコードが生成されるはずです。

次にサーバのrpcの実装ですが、特に特殊なことをする必要はありません。本当はインターフェースにIPアドレスを付けるなど何かすごい処理をするはずですが、例として、リクエストの中身を返すだけにしています。

func (*Server) AddIPv4Address(ctx context.Context, req *examplev1.AddIPv4AddressRequest) (*examplev1.AddIPv4AddressResponse, error) {
    return &examplev1.AddIPv4AddressResponse{
        Name:    req.GetName(),
        Address: req.GetAddress(),
    }, nil
}

バリデーションを行えるようにするにはサーバ起動時の処理を少し追加する必要があります。具体的には下記のようになります。

import (
    "log"
    "net"

    protovalidate_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
    "google.golang.org/grpc"

    "github.com/bufbuild/protovalidate-go"
    examplev1 "github.com/higebu/protovalidate-ip-prefix-example/gen/go/proto/example/v1"
)


func main() {
    lis, err := net.Listen("tcp", "localhost:9000")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    validator, err := protovalidate.New()
    if err != nil {
        log.Fatal(err)
    }
    s := New()
    g := grpc.NewServer(grpc.UnaryInterceptor(protovalidate_middleware.UnaryServerInterceptor(validator)))
    examplev1.RegisterExampleServiceServer(g, s)
    g.Serve(lis)
}

普段と違うところは、 protovalidate.New() しているところと、生成したvalidatorを protovalidate_middleware.UnaryServerInterceptor() に渡しているところだけだと思います。 github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidateを使うことで、サーバの各メソッドでバリデーションに関する処理を書かなくても良くなるため、使うことをおすすめします。

紹介したコードは下記のリポジトリに置いています。

github.com

置いているコードにはmain関数がないため、サーバを起動することはできませんが、同様の処理をテストコードに書いているため、 go test -v ./server を実行すると雰囲気がわかると思います。

今後の話

今後ですが、IPプレフィックスに関する、より高度なバリデーションを行えるようにしたいと思っています。具体的にはIPアドレスが指定したプリフィクスの範囲に含まれるかどうかなどをバリデーションルールとして記述できるようにしたいです。 また、protovalidateはまだプロダクション環境に導入していないため、導入を進めていきたいと思っています。

最後に

機能追加自体にご興味がある方は下記のプルリクエストから、ソースコードを追っていただければ雰囲気がわかると思います。

github.com

protovalidate にはProtocol Buffersの定義、ドキュメント、Examples、テストツールがあり、まずは、その辺りにコードを追加することになります。

特に、テストツールとして、protovalidate-conformance という各言語の実装が仕様を満たしているかどうかチェックできるツールがあり、これに対してテストケースを足しておかないと、各言語の実装を行っても最終的なテストができないので注意が必要です。

その後、各言語のリポジトリ(protovalidate-goprotovalidate-pythonprotovalidate-javaprotovalidate-cc)に対して仕様を満たすようにコードを追加していけば良いです。

protovalidate-conformanceですが、JavaとPythonは修正中の自分のprotovalidateリポジトリをベースにテストが可能ですが、GoとC++については、protovalidateの修正がマージされた後でないとテストできません。これはメンテナの方にやっていただくしかないので、ある程度できたらメンテナの方にお願いしましょう。

GoとPythonにはIPプリフィックスをパース、操作するための標準ライブラリが存在するため比較的簡単でしたが、Java、C++にはないので大変でした。

ちなみにC++については弊社のpainaさんに実装していただきました。ありがとうございました!

実は、IPプリフィックスをバリデーションするための機能は過去にpainaさんがPGVに追加していました。プルリクエストも出しています。 しかし、PGVへの機能追加がフリーズされていたためか、Goのみに対応していたためかわかりませんが、反応がありませんでした。。。 そのような状況で、どうしようかなと思っていたところにprotovalidateの発表があったため、それに乗っていこうとしている話でした。

OCXはなぜ全国にアクセスポイントを作っているのか?

はじめに

こんにちは。BBSakura Networks株式会社の佐々木です。 この記事は BBSakura Networks Advent Calendar 2023 - Adventar の18日目の記事です。

以前、以下のポストでも書いておりますが、OCXでは全国のデータセンター内にアクセスポイントを設置し、全国カバーのネットワークを構築しております。

blog.bbsakura.net

本日は我々がなぜ全国にアクセスポイントを構築しているのか?を中心に紹介させてください。

OCXのネットワークトポロジー

OCXは親会社であるBBIXのIXネットワークトポロジーをベースに構築されております。 昨今のネットワークは、IPネットワーク上に仮想化したオーバーレイネットワークを構築するのが主流であり、IXもOCXも多分に漏れずこのような技術を採用しております。 詳細については、弊社川畑の以下のポストをご参照ください!

blog.bbsakura.net

なぜ全国にアクセスポイントを設置しているのか?

OCXでは、2023/12/18現在、20ヶ所のデータセンターでサービスを提供しており、毎月のように展開拠点を拡大しており、国内でも最大級のアクセスポイントを持ったNaaSサービスとなっております。

なぜ、全国にこんなにアクセスポイントが必要なのか?というところですが、

  • ネットワークの効率化(トラフィック処理の地産地消)によるコストや遅延の削減
  • 快適な NW インフラを整備することによりトラフィックの地産地消と DX を推進
  • インターネットと異なるレベルで品質を担保したNWの実現

などを実現していきたいと思ってます。

OCXを検討する上で出てきたデジタル田園都市国家構想には会社としても大変共感をしており、いろいろなインスピレーションを受けており、本構想の実現に向けて、弊社もさまざまな取り組みをしていきたいと考えており、特にインフラ整備の部分において民間企業の力を結集して寄与したいと考えたのが地域企業との協業によるアクセスポイントの全国整備でした。

デジタル田園都市国家構想 デジタル田園都市国家構想とは、「デジタル実装を通じて地方が抱える課題を解決し、誰一人取り残されずすべての人がデジタル化のメリットを享受できる心豊かな暮らしを実現する」という政府が推進している構想です。

デジタル田園都市国家構想を表した図。1. デジタル基盤の整備、2. デジタル人材の育成・確保、誰一人取り残されないための取り組み、3. 地域課題を解決するためのデジタル実装、の 3 項目が記載されている
デジタル田園都市国家構想サマリー

サービス、プラットフォームのクラウド化が進む中、柔軟な対応ができるネットワークインフラが必須になってきております。 OCXの構成要素はアンダーレイとなる全国網とVNFを動かすコンピューティング基盤、制御するオーケストレーター(API群)の3つとなります。 OCXの構成要素はアンダーレイNW、コンピューター、APIの3つとなる。

これらを分散配置することで、特定のエリアへのトラフィック集中の回避や、遅延の低減、また最適なトラフィック分散の結果としてコストの削減効果も付随的に現れてきます。遅延が短くなればさまざまな新しいアプリケーションのユースケースを作り出すこともでき、まずはインフラを作ることで、そういったユースケースを生み出す基盤にできればと考えてます。

最後に

Advent Calendarで登録したものの、何を書こうかギリギリまで決めておらず、先日routeviewsにあるbgpdataから事業者/国別のIPアドレス保有数を分析するツールを作っていたので、その紹介記事を書き掛けてたのですが、検算したらどこか計算おかしそうで、慌てて別の記事を書いてみました。 IPアドレスもどこかでゆっくり検算したら結果共有したいと思います。

BBSakuraが参加しているSAJについて

はじめに

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

adventar.org

こんにちは、BBSakura Networksで取締役を務めている山口と申します。2018年8月に会社を設立してから、早いもので5年目を迎えています。

当社では、OCX(Open Connectivity eXchange)というキーとなるプロダクトが立ち上がりました。これを強化する機能の構築が順調に進んでおり、今年は新しい分野の事業も手がけるなど、会社として新たなステージに入った感があります。これからもさらに研鑽を積んでいかなければと思っています。

今回は久しぶりにブログを書かせていただいて、当社が参加している一般社団法人ソフトウェア協会(SAJ)についてとそこでの活動についてお話しします。

SAJとは?

SAJとは、一般社団法人ソフトウェア協会といいまして、ソフトウェア製品に係わる企業が集まり、ソフトウェア産業の発展に係わる事業を通じて、我が国産業の健全な発展と国民生活の向上に寄与することを目的に、政策提言や人材育成、参加している会社同士の交流まで10の委員会(図の左側の青い色の部分)、更にその下部に多数の研究会を置いて会員企業が活発に活動をしつつ、社団法人としてもPSマークの認証、U22プログラミングコンテスト、CEATECなど数多くの事業をやっています。

U22プログラミングコンテストCEATECなどはご存じの方も多いのでは無いでしょうか? 全部のSAJのメールを受けていると、活動への参加お誘いや、新しく入られた企業さんの紹介、活動報告のメールがひっきりなしにやってくるのでもう大変なくらいです。

SAJの組織図を表している、大きく4つの組織を表しており、左半分に理事会など組織運営をする組織、協会事業に関わる組織、外部の委員会が表示されている。右半分にはSAJ内部にある委員会が表示されており、青い色で囲まれている。
SAJの組織図

この団体は、現在当社の親会社であるさくらインターネット株式会社の代表の田中が会長を務めており、当社のもう一つの親会社BBIX株式会社の親会社であるソフトバンク株式会社の創業者孫正義さんが1982年に「日本パソコンソフトウェア協会」として設立、その後3回の名称変更を経て、現在の「ソフトウェア協会」となった歴史ある団体で、現時点ではなんだかすごく縁(えにし)を感じているところです。

SAJでBBSakuraは何をしているのか?

当社は数ある委員会の中でも、技術委員会、地域デジタル推進委員会、アライアンスビジネス委員会、プロジェクトみらい、に参加させていただいております。 技術委員会では、参加各社の技術課題や興味のある技術分野について議論したり、共同で各分野の先端の企業の方をお呼びしたり、社内の技術や技術的な課題をシェアしたりしています。 アライアンスビジネス委員会では、自社ソリューションの紹介や仲間を集めたりしていて、この場で新しい商材などの情報を入手したりできたこともあります。 プロジェクトみらいは、少し毛色が違っていて次世代の経営リーダーが集まっている集団で、経営に関わるリーダーだからこその悩みだったりどうしていきたいかという決断だったりを話すことによって、ゆるくて強固なつながりを得られるようなイメージがあります。

最後に地域デジタル推進委員会ですが、ここではSAJの活動を全国に拡大するため、全国でセミナー、勉強会などの企画、運営を行う体制を整備し、地場産業界や各地域経産局、各地域団体と連携し、ビジネスマッチング商談会への参加や展示会の企画、運営なども行い、各地域の交流や連携をより強め、地域でのビジネス成功モデルを積極的に発信し、地方創生に対して貢献しようという目的の委員会です。

どこの団体もそうなのですが、団体の参加企業は関東近円にあることが多く、会合も常に関東のみで行われるということになりがちで、その地域のみの意見にまとまりがちです。そういうなかで、地域のビジネスにもしっかり目を向けることによって、日本全体の力につなげていこうという取り組みを多く実施しているのが、地域デジタル推進委員会です。

当社はこの委員会で、いわゆるNaaS≒ネットワーククラウド研究会というものに参加させていただいて活動をさせていただいております。

今後何をしようとしているのか?

「活動させていただいております」と記載しましたが、実はネットワーククラウド研究会は、本日2023/12/19がメンバーを集めて研究会活動をする第一回目のできたてほやほやの会になります。

www.saj.or.jp

当社BBSakuraは、OCXというNaaSサービスを提供させていただいており、その本質的な価値は当社がコアになって、地方のネットワーク事業者の仲間と一緒になってサービスを組み上げていくものなのですが、OCXだけではなく、NaaS的なビジネス活動に関する研究を全国各地に広がっているSAJ参加企業の方々と一緒になって業界全体を押し広げていきたいなと期待しているところです。

地方独自のビジネス活動を別の地方や関東近郊などでもできるようになったり出来るようにするお手伝いができたら良いなぁと心底思います。 今日はNaaS関係の取り組みということで、当社以外のネットワーククラウド提供事業者の方、とメガクラウドの方のお話も聞ける予定で、個人的にも楽しみにしておりました。

ちょっと大きなスコープで業界を眺められるかも?

ちなみになのですが、こういった一般社団法人の活動は、会社によっては特定の部局の方のみが参加するもので一般の社員の方にはあまり関係ないかも?っていう方もおられると思います。 もちろんそういった企業もあると思いますが、もし参加するチャンスがあったらぜひ参加してみてください。 現在の社会情勢や政治の世界で何を話しているのか、業界の中でビジネス的な自社の立ち位置はどの辺りかなとか、そんな産業が存在していて、そんなところに、そんなソフトウェアやサービスがあるなんて!?みたいな新鮮な驚きがたくさんありたくさんの刺激を受けることができますし、そこになにかのビジネスを作れるチャンスがあるかもしれません。

一緒に成長しませんか?

最後になりますが、SAJでは自分自身の考え方に新しい視点や視座を加えつつ、周りにも変化を与えられるような企業さんをいつでも募集しております。 実際にはそんなにハードルは高くないので、ちょっと気になるなぁくらいで構いませんのでぜひお声がけいただければと思います。

あ、BBSakura Networksでももちろんメンバー募集しています、カジュアル面談できるのでいつでも声かけてください〜! www.bbsakura.net

どちらもご連絡をお待ちしています!