OpenBSD で MAP-E 方式の IPv4 over IPv6 を動作させるためのメモ。前回はカーネルにパッチをあて PF の動作に修正を加えていたが、PF ルールを工夫することでおおむね MAP-E 動作を実現する。

PF のオプション probability を利用して、NAT で使用するポート番号を MAP-E の port set に制限する。なお、カーネルの動作が原因で、この方法では一部の ICMP が利用できない。

オプション probability の必要性

MAP-E 動作には NAT で使用されるポート番号をクライアント毎に排他的に割り当てられるポート集合に制限する必要がある。ポート集合は複数のポート区間(5376-5439, 9472-9535, …, 62720-62783 など)であり、通常の PF ルールでは使用ポート番号を複数の区間に制限することはできない。

通常の PF ルールでも単一のポート区間に制限することはできる。単一のポート区間では使用できるポート数が少ないため、同一アドレスに並列接続が必要なサイトなどとの通信に不具合が発生する可能性がある。

そこで複数ポート区間毎に NAT ルールを作成して、各 NAT ルールが等確率で適用されるように PF を設定できれば、利用可能なポート集合を全て使うことができる。このルールが適用される確率を設定する際にオプション probability を利用する。

具体的な probability 設定方法

ここでは MAP-E で利用可能なポート集合が15区間で、各区間64ポートであるとする。

このポート区間それぞれに対して NAT ルールを作成して、それが適用される確率が 1/15 となるように設定すれば良い。

ただし、PF では最後に match したルールが有効になるので、単純に各ルールの match 確率を 1/15 としては、そのルールが適用される可能性は 1/15 とならない。実際、全てのルールの match 確率を 1/15 (約 6.7%)とすると最初のルールが適用される(最初のポート区間が使用される)確率は約 2.5% となる。これは最初のルールが適用されるには最初のルールに match し、かつ以降全てのルールに unmatch する必要があるからである。

1
return 1.0/15 * (14.0/15) ** 14 * 100, 1.0/15 * 100
2.5376026178153164 6.666666666666667

このように match 確率の設定は一見難しそうではあるが、実は N 個のルールがあり、 各ルールが適用される確率を 1/N としたい場合、 i 番目のルールの match 確率を 1/i と設定すれば良い。

確率設定の考え方

次が成立し、i 番目のルールに着目すると i 番目以降のルールの match/unmatch のみを考慮すれば良いことに着目する。

i 番目のルールが適用される if and only if i 番目のルールに match し、 i+1,..,N 番目のルールに unmatch

そこで次のように帰納的に考える。今 (i+1) 番目から N 番目のルールのどれかに match する確率が (N-i)/N となるように (i+1) 番目から N 番目のルールの match 確率が設定できているとする。

このとき i 番目の match 確率を p_i とすると i 番目が適用される確率は

p_i * (1 - (N-i)/N) = p_i * i / N

となる。よって p_i = 1/i と設定すれば i 番目が適用する確率は 1/N となる。

また、このとき i 番目から N 番目のルールのどれか match する確率は

p_i + (1 - p_i) * (N-i) / N = (N-i)/N + p_i * i/N = (N-(i-i)) / N

となり、次のステップの帰納仮説が成立する。

なお、最初のステップの i = N は自明に成立する。

PF ルール の生成と適用

多数の NAT ルールが必要となるのでスクリプトでルールを作成して PF の Anchor として適用する。

ルール生成

生成スクリプト

次は PSID offset = 4, PSID length = 6, PSID = 20 として PF ルールを生成するスクリプトである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  #!/bin/sh

  psid_offset="4"
  psid_length="6"
  psid="20"
  map_e_if="gif0"

  port_bit_length="16" # port number is a 16 bit integer

  base=$((1 << ($port_bit_length - $psid_offset)))
  range_length=$(($port_bit_length - $psid_offset - $psid_length))
  range_size=$((1 << $range_length))
  range_num=$(((1 << $psid_offset) - 1))
  offset=$(($psid << $range_length))

  echo "# PSID offset=$psid_offset, PSID length=$psid_length, PSID=$psid, IF=$map_e_if"
  for i in $(seq 1 $range_num)
  do
          port_start=$(($i * $base + $offset))
          port_end=$(($port_start + $range_size - 1))
          prob=$(bc -l -e "scale=8; 1/$i" -e "quit")
          rule="match out nat-to $map_e_if port ${port_start}:${port_end}"
          if [ "$i" -eq 1 ]
          then
                  echo "$rule"
          else
                  echo "$rule probability 0$prob"
          fi
  done

生成ルール例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# PSID offset=4, PSID length=6, PSID=20, IF=gif0
match out nat-to gif0 port 5376:5439
match out nat-to gif0 port 9472:9535 probability 0.50000000
match out nat-to gif0 port 13568:13631 probability 0.33333333
match out nat-to gif0 port 17664:17727 probability 0.25000000
match out nat-to gif0 port 21760:21823 probability 0.20000000
match out nat-to gif0 port 25856:25919 probability 0.16666666
match out nat-to gif0 port 29952:30015 probability 0.14285714
match out nat-to gif0 port 34048:34111 probability 0.12500000
match out nat-to gif0 port 38144:38207 probability 0.11111111
match out nat-to gif0 port 42240:42303 probability 0.10000000
match out nat-to gif0 port 46336:46399 probability 0.09090909
match out nat-to gif0 port 50432:50495 probability 0.08333333
match out nat-to gif0 port 54528:54591 probability 0.07692307
match out nat-to gif0 port 58624:58687 probability 0.07142857
match out nat-to gif0 port 62720:62783 probability 0.06666666

ルール適用

生成したファイルを別ファイル /etc/pf-map-e-nat.conf などに保存して、メイン設定ファイル pf.conf から anchor として読み込む。

1
2
anchor map-e-nat out on gif0 inet from { gif0, vport0:network } to !<martians>
load anchor map-e-nat from "/etc/pf-map-e-nat.conf"

Anchor を利用することで多数の NAT ルールを別ファイルに管理でき、また anchor 適用時に一箇所でフィルターオプションを指定することで各ルールにフィルターオプションを指定する必要がなくなる。

ICMP について

今回の方法では IPv4 ICMP Echo 通信(ping)ができない。これはそもそものカーネルの動作として ICMP Echo パケットはルールで指定したポート制限が反映されないためである。

次はカーネルソースコードの該当箇所である。

1
2
3
4
5
if (pd->proto == IPPROTO_ICMP) {
	if (pd->ndport == htons(ICMP_ECHO)) {
		low = 1;
		high = 65535;
	} else

ICMP Echo の場合はポートの範囲が 1 から 65535 に固定される。この動作を変更するにはカーネルに手を加える必要がある。