OpenBSD で MAP-E 方式の IPv4 over IPv6 を動作させるためのメモ。

自宅ネットワークは OpenBSD をルータとした IPoE 方式の IPv6 と PPPoE 方式の IPv4 のデュアルスタック環境である。 最近、IPv4 接続の速度が劣化していると感じたため、品質が改善する可能性がある MAP-E 方式へ変更した(結局、有意な差はなかったのだが…)。

OSの標準機能や既存のOSSでは MAP-E 動作(MAP CE 機能)を実現できなかったため Kernel にパッチ をあてソースからビルドした。NAT で使用されるポート番号を MAP-E の port set に制限するオプションを pf(4) に追加した。3ヶ月程度、問題なく動作し続けている。

Mapping of Address and Port with Encapsulation (MAP-E) について

IPv4 over IPv6 を実現する方式の一つ。ユーザルータ(MAP Customer Edge (CE))とISPルータ(MAP Border Relay (BR))の間でIPv4 over IPv6トンネルを構築する。MAP CE では NAT によりIPv4プライベートアドレスをグローバルアドレスに変換し、それを IPv6 にカプセル化する。MAP BR でデカプセル化する。MAP BR でグローバルアドレスは複数のユーザで共有され、ポート番号でユーザを識別する。つまり、MAP CE で NAT する際に使用できるポート番号が制限される。正しいポート番号に NAT しないと返りのパケットが別ユーザにルーティングされてしまう。

RFC 7597 で説明されている。解説は次の文献が分かりやすい:

Kernel にパッチをあてる理由

NAT で使用するポート番号を MAP ルールに従い制限する上手い方法がないため。

MAP CE に必要な機能

MAP CE として必要な機能は次の3つである。

  1. IPv4 over IPv6 トンネル
  2. IPv4 NAT
  3. NAT で使用する port 番号制限

OpenBSD は 1. は gif(4), 2. は pf.conf(5)nat-to で実現できる。3. で使用できるポート番号を制限しつつ、使用可能なポート番号を全て使えるように設定するのが難しい。

pf.conf のNATポート範囲の指定

例えば pf.confport 2000:2004 のように一つの区間ならば指定できる。しかし、一般に MAP-E で使用されるポート番号は複数の区間となる。実際、私の環境では63個の区間となった(各区間は16個のポート番号で構成されている)。

この方法を使う場合63個の区間から一つ選び、それをNATで使用することになる。ポート番号が16個しかないのでNATセッションが多い状況では使用できるポート番号が枯渇する恐れがある。

pf.conf の複数NATルール

使用可能なポート数を増やすために、送信元ポート番号などに基づき63個のNATルールを作成し、それぞれ使用可能なポート区間を指定することも考えられる。しかしながら、各ルールが使用可能なポート番号は16個のままなのでマッチルールやクライアントのリクエストによっては、やはり、ポート番号が枯渇する恐れがある。

ラウンドロビンでマッチするアクションを選ぶようなランダムにルールを選択仕組みがあればある程度は緩和可能かもしれないが、現状、そのような仕組みは pf にはない。

(ただ、うまくマッチルールを作成すれば、例えば、送信元ポート番号の下位6ビットとポート区間を紐付けるなどでランダムに近い挙動でポート区間を選べれば、この方法で問題ないかもしれない)


追記 [2022-08-21 Sun]

ランダムでルールを適用する probability オプションが存在することに気がついた。このオプションを使いうまく確率を設定すれば、複数のルールからいずれかを適用する、という動作が可能である。一部制限はあるが PF ルールで MAP-E 動作が実現できる。

追記おわり


変更箇所

Kernel に手を入れて NAT で使用するポート番号を制限できるように変更した。設定ファイル pf.conf で MAP ルールで定まる集合から選択するためオプション map-e-portset を設定できるようにした。例えば次でPSID offset (a bits の長さ), PSID length, PSID をそれぞれ 4, 6, 20 に指定できる。

1
  match out on gif0 inet nat-to (gif0) map-e-portset 4/6/20

パッチは次で公開している。

MAP-E 動作のための設定例

Kernel にパッチをあてビルド、インストールすることで MAP-E 動作が可能となる。

MAP-E 情報の取得

まず、MAP-E設定に必要な情報(MAP BR のアドレスや PSID など)を次から取得する(このサイト非常に便利で助かった)。

必要なのは次の4つ。

  • MAP BR のIPv6アドレス
  • MAP グローバル IPv4 アドレス
  • PSID offset
  • PSID length
  • PSID

また、MAP CE の IPv6 アドレスも必要である。

以降、それぞれの値は次の表の通りであるとして説明する。

MAP CE IPv6 address 2001:0db8:dead::1/64
MAP BR IPv6 address 2001:0db8:beef::1/64
MAP global IPv4 192.0.2.1
PSID offset 4
PSID length 6
PSID 20

IPv4 over IPv6 トンネル

あるインターフェイスに 2001:0db8:dead::/64 のアドレスが設定されており、このインターフェイスを使いインターネットへ IPv6 接続されているとする。

MAP BR へ IPv4 over IPv6 トンネルを作成するには、例えば /etc/hostname.gif0 を次のように編集し sh /etc/netstart gif0 を実行する。

1
2
3
4
  tunnel 2001:0db8:dead::1 2001:0db8:beef::1
  inet 192.0.2.1 255.255.255.255
  mtu 1460
  !route add -inet default 192.0.2.1

IPv6ヘッダが40バイトなので MTU は 1460 (=1500-40) に設定する。

MAP ルールに従う NAT ポート選択

次を pf.conf に追加する。

1
2
  match on gif0 inet all scrub (no-df random-id max-mss 1420)
  pass out on gif0 inet nat-to (gif0) map-e-portset 4/6/20

一行目は MAP-E とは関係ないが、 Windows が MTU 1500 で通信しようとするので TCP の MSS を 1420 に設定する必要がある。 二行目で MAPルールに従うようにポート選択する。

実装上のメモ

  • FreeBSD で MAP-E を有効にするパッチ を参考にした
  • NAT処理を行なう pfpf の設定を読む pfctl に変更を加えた

    • pf 関連のファイルはディレクトリ sys/net/ の中の pf がプレフィックスになっているもの
    • pfctl 関連のファイルはディレクトリ sbin/pfctl, share/man/man5/pf.conf, regress/sbin/pfctl に配置されている
  • NATで使用されるポート番号は sys/net/pf_lb.c の関数 pf_set_sport で決定される

    • この関数は nat-toaf-to のルール処理時に呼び出される
    • オリジナルの pf_set_sport はポート番号の下限 low と上限 high を定めて、そこからランダムに未使用のポート番号を選択する
    • 新しい関数 pf_set_sport_rangepf_set_sport_mape を加えた

      • 関数 pf_set_sport_range はオリジナルの pf_set_sport と同様に下限と上限からランダムに未使用ポートを選択する
      • 関数 pf_set_sport_mape は MAP-E の port-set 設定に従い未使用ポートを選択する

        • ランダムに a bits を選択し、 pf_set_sport_range を呼びだすことを繰り返す
      • 関数 pf_set_sport 設定に応じて pf_set_sport_range もしくは pf_set_sport_mape を実行する
    • MAP-E port-set のルールは pfvar.hstruct pf_mape_port として新しく定義し struct pool のメンバ変数として追加した
  • pfctl による設定ファイル pf.conf の読み取り、出力は sbin/pfctl/parse.y, sbin/pfctl/pfctl_parser.c で決定される

    • parse.yyacc で記述されている

      • まず %{%} の間に C の struct の定義、関数宣言
      • 次に %token, %type が定義され %% で終端
      • そして 文法 が定義され %% で終端
      • 最後に C の関数が定義
    • トークンとして map-e-portset を追加して値を struct pf_mape_port に保存
    • pfctl_parser.cmap-e-portset 情報を出力するように変更
    • regress/sbin/pfctl には成功すると失敗する pf.conf のルールを出力とともに置く

      • 例えば pf1.inpf1.okpfail2.inpfail2.ok

おわりに

Kernel と Base system をビルドするのは数時間かかるので大変である。また、IPv4 のゲートウェイが PPPoE と MAP-E の2つできたので policy routing で使い分けができるようになったが、計測してみたところ、私の環境ではどちらの経路も大差ないようで残念であった。