macOSでDS-Lite (transix)

# 結論
以下のサイトを参考にmacOSでDS-Liteを使えるようにすると、ブラウザは動くがCLI等でDNSが正しく動作しない。

https://techlog.iij.ad.jp/contents/dslite-macosx

https://blog.goo.ne.jp/rabbit5151/e/628a73893085bd932c55c110c0d370a8

対策としてはまずシステム環境設定でIPv4を手入力にし、ルーターを192.0.0.1に設定する（他は適当でOK）。
![ip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703555/f5f75d0e-b14c-e8e4-3182-fcec01e82107.png)

その後上記サイトのスクリプトを実行するとブラウザ以外も正しく動作する。
スリープからの復帰後等でたまにルーティングテーブルが書き換えられてしまうが、その場合は以下のコマンドを実行すれば良い。
```
$ sudo route delete -inet default
$ sudo route add -inet default 192.0.0.1
```

インターネット共有を使う場合は共有を有効化した後に以下を実行するとMacがDS-Lite対応ルータになる。
```
$ cat <<'EOF' | sudo pfctl -a com.apple.internet-sharing -f -
nat on gif0 from bridge100/24 to any -> 192.0.0.1
nat-anchor "base_nat66" all
rdr-anchor "base_nat66" all
EOF
```

動作確認はmacOS 11.7 Big Surで行った。

# 蛇足
結構手こずったので作業ログを残しておく。

### モチベーション
DS-Lite方式の光コラボ回線を契約したが、フレッツ・ジョイントはサポートされていないのでNTTのホームゲートウェイが使えず、正攻法ではONUにルータを接続する必要がある。
普段はMac一台しか使わないのにルータを挟むのも癪なのでONUにMacのイーサネットを直結し、インターネットに接続したい。
冒頭に示した参考サイトのスクリプトを使うと図のような接続を実現できる。gif0というインタフェース名でローカルからAFTRにトンネルが作成され、それぞれの端点に192.0.0.2, 192.0.0.1のIPv4アドレスを（勝手に）設定する。IPv4のデフォルトゲートウェイを192.0.0.1に設定すればカーネルがIPv6にカプセル化してAFTRにIPv4パケットを送信してくれるらしい。
![dslite.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703555/6c5eab70-0a88-1918-3c41-e0f72a5e70fd.png)

### 症状
参考サイトの動作確認にあるようにブラウザは問題なく動作するが、他のアプリケーションは動作しないものがある。
例えばcurlは以下のエラーを返す。
```
$ curl http://httpbin.org/get
curl: (6) Could not resolve host: httpbin.org
```
他、SSH等も動作しない。CLIを使わなくてもMicrosoft Exchangeへのログインが失敗してメールが受信できなかったりもする。
ところが、nslookupは動いている。
```
$ nslookup httpbin.org
Server:		2001:a7ff:5f01::a
Address:	2001:a7ff:5f01::a#53

Non-authoritative answer:
Name:	httpbin.org
Address: 34.224.50.110
Name:	httpbin.org
Address: 52.200.117.68
Name:	httpbin.org
Address: 52.1.93.201
Name:	httpbin.org
Address: 34.205.150.168
```
また、/etc/hostsに直接IPアドレスを書いた場合もちゃんと動く。

### macOSのDNS
どうやらnslookupではDNSサーバに直接問い合わせを行うが、通常はmDNSResponderに問い合わせを行うらしい。

https://qiita.com/set0gut1/items/100385f55061db76ac4f

curlを叩く際にWiresharkでパケットをとってみると、AAAAレコードを問い合わせ、IPv6アドレスが返ってこないとAレコードは問い合わせず諦めている。

mDNSResponderのソースコードを見ると、IPv4のルータ情報を取得しているコードが確認できる。
https://github.com/apple-oss-distributions/mDNSResponder/blob/806254210edec4cab01f794f6fd28658aa6ba59d/mDNSMacOSX/mDNSMacOSX.c#L4256
ドキュメントを見る限りここで使われている定数`kSCPropNetIPv4Router`はシステム環境設定から情報を持ってくるためのキーに思える。
https://developer.apple.com/documentation/systemconfiguration/kscpropnetipv4router?language=objc
ここからは状況証拠からの憶測だが、おそらくmDNSResponderはシステム環境設定においてIPv4のルーター（デフォルトゲートウェイ）アドレスが設定されていない場合にAレコードを問い合わせない（実際にDNSの問い合わせを行う部分のコードは公開されていないようで見つけられなかった）。
デフォルトゲートウェイが設定されていないならインターネット接続はまずできないのでDNSを問い合わせる必要はない。この仕様自体は真っ当だと思うが、問題はデフォルトゲートウェイの設定をカーネルのルーティングテーブルではなくシステム環境設定から持ってきている点である。
真面目にルーティングテーブルを読むのは大変だし、普通はシステム環境設定の値=ルーティングテーブルの値になっているので合理的な実装ではある。

単純にDHCPを使うとゲートウェイのアドレスは降ってこないので、こんな感じでシステム環境設定のルーター欄は空欄になってしまう。結果、mDNSResponderがIPv4のインターネット接続は存在しないと判断して名前解決を諦めていると解釈すればつじつまが合う。
![syspref.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703555/0135866a-c725-8f37-8bae-00d879751fc2.png)

### 解決策
システム環境設定でIPv4を手入力にしてルーターのアドレスを無理矢理設定する。
![manual.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703555/719083ae-a64e-8bde-6c73-7a40a4389b05.png)
ところが、適用を押すとルーティングの設定が上書きされ、interfaceがイーサネットのもの（ここではen0）になってしまう。
```
$ route get -inet default
   route to: default
destination: default
       mask: default
    gateway: 192.0.0.1
  interface: en0
      flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1500         0 
```
そこで、もう一度ルーティング設定を上書きするとシステム環境設定の値を保ったままinterfaceをgif0にできる。
```
$ sudo route delete -inet default
$ sudo route add -inet default 192.0.0.1
$ route get -inet default
   route to: default
destination: default
       mask: default
    gateway: 192.0.0.1
  interface: gif0
      flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1500         0 
```
これでmDNSResponderを騙せるようになる。
```
$ curl http://httpbin.org/get
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.64.1", 
    "X-Amzn-Trace-Id": "Root=1-63f9ac06-16fa40b0395911566573437c"
  }, 
  "origin": "xxx.xxx.xxx.xxx", 
  "url": "http://httpbin.org/get"
}
```

### インターネット共有
インターネット接続を有効にするだけでMacがDS-Lite対応ルータになってくれれば良かったのだが、試してみるとIPv6しか通じないルータになってしまった。
最初に試した対策はプロキシサーバを立てる方法である。Squidを使うとすぐにHTTPプロキシが用意できる。
```
$ brew install squid
$ brew services run squid
```
これでポート3128にHTTPプロキシが立つ。あとはインターネット共有で接続した機器でプロキシを設定すればIPv4が通る。この方法は気楽で良いのだがプロキシに対応していないアプリ（Steamとか）が動かない。

どうにかできないかと調べていたところ、macOSはBSD系のOSなので、Berkeley Packet Filter (pf)と呼ばれる機構がついていることを知った。pfを使うとファイアウォールやNATが設定できるというので、試しに設定を読んでみた。
```
$ sudo pfctl -v -sr
No ALTQ support in kernel
ALTQ related functions disabled
scrub-anchor "com.apple/*" all fragment reassemble
  [ Evaluations: 9202096   Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 92 ]
scrub-anchor "com.apple.internet-sharing" all fragment reassemble
  [ Evaluations: 95        Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 3021 ]
anchor "com.apple/*" all
  [ Evaluations: 4106409   Packets: 0         Bytes: 0           States: 0     ]
  [ Inserted: uid 0 pid 92 ]
anchor "com.apple.internet-sharing" all
  [ Evaluations: 34        Packets: 9         Bytes: 1273        States: 6     ]
  [ Inserted: uid 0 pid 3021 ]
```
com.apple.internet-sharingの設定が存在する。間違いなくインターネット共有である。manをちゃんと読んでNATの設定を確認する。
```
$ sudo pfctl -a com.apple.internet-sharing -s nat
No ALTQ support in kernel
ALTQ related functions disabled
nat-anchor "base_nat4" all
nat-anchor "base_nat66" all
nat-anchor "base_v4" all
rdr-anchor "base_v4" all
rdr-anchor "base_nat66" all
```
多分v4とついている設定を全部外して自分でNATを設定してやればいい。NATルールの書き方はここを参考にした。

https://www.dragonflybsd.org/~aggelos/pf/ja/nat.html

bridge100（インターネット共有のブリッジインタフェース名）からのパケットを192.0.0.1に送るgif0上のNATを設定するルールは以下のように書くので合っている…と思う。
```
nat on gif0 from bridge100/24 to any -> 192.0.0.1
```
というわけで、記事冒頭のコマンドを使ってNATのルールを書き換えてやればめでたくインターネット共有でもIPv4が使えるようになる。わかるかこんなん。