はじめに
この記事は 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 アドレスを取得する処理が未実装の状態です。
PPP ネゴシエーション情報中継機能 → 完了
PPP クライアントと網終端装置間で発生する CHAP 認証情報、IPCP の通信を中継させて、網終端装置が管理する IP アドレスを PPP クライアントに割り当てる処理は実装済みです。
PPP セッション間通信中継機能 → 未実装
セッション確立時に生成されたネットワークインターフェースを利用して PPPoE クライアントからの通信を網終端装置に転送する機能です。こちらがまだうまく動作しない状態です。
改修前準備
accel-ppp の特徴
accel-ppp の大きな特徴はモジュールの組み合わせによりサーバとしての機能を柔軟に変更できる点です。 デーモン起動時にコンフィグファイルに指定したモジュールをロードされて、その機能が有効化されます。代表的な機能は下図のとおりです。
今回採用したモジュールの組み合わせ
疑似フレッツを実現するため、役割ごとに以下のようにモジュールを組み合わせます。
- accel-ppp デーモン(フレッツ収容ルータ相当)
- アクセス:PPPoE による接続
- 認証方式:CHAP MD5 認証を選択
- 認証情報参照方法:RADIUS (ユーザ情報を管理する RADIUS サーバと通信)
- NCP:IPCP を選択( IPv4 のみを利用して IPv6 は利用しない)
- accel-ppp デーモン(網終端装置相当)
- アクセス:L2TP による接続
- 認証:CHAP MD5 認証を選択
- 認証情報参照方法:RADIUS (網終端装置情報を管理する RADIUS サーバと通信)
- NCP:IPCP を選択(同上)
改修の詳細
モジュールの改修箇所(構造体・関数)
上手の赤枠の機能・モジュールに以下の改修をしました。
- 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 が受信
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 パケットの送受信 )
改修された 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
- 改修による処理として、ICCN パケットに以下の AVP をセットする
- PPP セッション生成
- 改修による処理として、
l2tp_create_tunnel_exec
関数経由で取得した対向セッションのポインタ情報をセットする
- 改修による処理として、
3:CHAP 認証情報取得
sse 側の l2tp_send_ICCN
関数が実行された後、nte は ICCN パケットを受信します。
改修された l2tp_recv_ICCN
関数は、このタイミングで以下の処理を実施します。
- ICCN パケットから AVP にセットされた CHAP 認証情報を取得
- PPP セッション生成時に取得した CHAP 認証情報をセット
4:CHAP 認証
こちらの処理に到達するまでに下記の処理が完了しています(図中青枠の処理)。
- nte から sse に ZLB パケットを送信(= L2TP セッションの生成完了 )
- LCP ネゴシエーション完了
通常であれば 、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 セッションのステータスを「認証完了」に変更します。
この後、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 を送信するようにできます。
この機能のほか、改修した ppp_unit_read
関数の機能により、届いたパケットを対向の PPP セッションに転送するようにします。
これにより home → sse → nte → sse → home の順で Conf-Req パケットを転送することができます。
7:Conf-Ack 転送
sse から Conf-Req を受信した home は、Conf-Ack パケットを sse に送信します。
先程と同様に home → sse → nte → sse → home の順で Conf-Ack パケットが送受信されます。Conf-Ack を受け取った段階で各 IPCP のネゴシエーションが完了し、home に Conf-Ack が届いたタイミングで、すべての PPP ネゴシエーションが完了した状態となります。
おわりに
BBSakura Networks では Go 言語を利用する機会が多く、C 言語で記述されたこの規模のソースコードを編集するのは久しぶりでした。メモリレイアウトやポインタに対する深い理解が求められ、動作を把握するのに多くの時間を費やしました。今回セッション確立後のデータ転送処理の完成に至らなかったことが残念ですが、普段あまり意識しないレイヤ(ソケット関連のシステムコールなど)を調査する機会を持つことができたことは今後のネットワークプログラミングをしていくための良い経験になったと思います。