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

はじめに

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

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

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

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

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

前回のおさらい

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

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

現時点の進捗

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

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

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

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

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

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

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

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

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

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

改修前準備

accel-ppp の特徴

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

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

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

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

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

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

改修の詳細

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

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

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

改修後の通信シーケンス

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2:CHAP 認証情報格納

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

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

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

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

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

3:CHAP 認証情報取得

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

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

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

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

4:CHAP 認証

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

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

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

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

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

5:CHAP 認証結果転送

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

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

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

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

6:Conf-Req 転送

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

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

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

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

7:Conf-Ack 転送

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

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

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

おわりに

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