Milk-V Vegaを買った

Milk-VからRISC-Vを搭載したL2スイッチが出るということで購入してみた。 10Gポートが2つしかないがとりあえず自分の環境ではサーバとデスクトップ用で十分だ。 購入はAraceを使ったが春節なのとFedexだが途中までは4pxを経由したので、注文してから届くのに1ヶ月程度かかった。

パッケージは非常に簡素で箱にスイッチだけ入っていてマニュアルなども特にない。 電源は付属していないので、12V2AのDC5525コネクタのACアダプタが必要になる。

ドキュメントはオンラインにあるがWeb設定画面へのアクセスやシリアルの使い方ぐらいしかない。 デフォルトではTELNETSSHは無効になっているので、一旦Web設定画面にアクセスして有効にする必要がある。 USBポートがありシリアル経由でメッセージを見たりOSにログインすることはできる。ただ設定用CLIなどはなさそうなので基本はWebで操作することになりそうだ。とはいえL2スイッチなので複雑なことはしなそうだが。

Web設定画面

中身は単なるLinuxソースコードも公開されているので色々いじることもできそうだ。

root@dev:/root> cat /proc/cpuinfo
processor       : 0
hart            : 0
isa             : rv64imafdc
mmu             : sv39

root@dev:/root> free
              total        used        free      shared  buff/cache   available
Mem:         229300       27596      196192         104        5512      201364
Swap:             0           0           0
root@dev:/root> df
Filesystem           1K-blocks      Used Available Use% Mounted on
ubi0:ubifs               32112     17608     14504  55% /
devtmpfs                114576         0    114576   0% /dev
tmpfs                   114648         0    114648   0% /dev/shm
tmpfs                   114648        52    114596   0% /tmp
tmpfs                   114648        32    114616   0% /run
root@dev:/root> lsmod
Module                  Size  Used by    Tainted: G
led_164                 3084  1
xy1000_net             10144  1

iperfでスループットを計測したところ、きちんとワイヤーレート出た。

------------------------------------------------------------
Server listening on TCP port 5001
TCP window size:  128 KByte (default)
------------------------------------------------------------
[  1] local 10.0.0.10 port 5001 connected with 10.0.0.2 port 53732 (icwnd/mss/irtt=14/1448/85)
[ ID] Interval       Transfer     Bandwidth
[  1] 0.0000-10.0032 sec  11.0 GBytes  9.41 Gbits/sec

Reverse Engineering x86 Processor Microcodeを読んだ

この頃、Zenbleed、Inception、Downfall などのプロセッサー脆弱性が話題になっている。 これらの脆弱性にはベンダーからの microcode アップデートで対応できると言われている。

はて、microcode とは何だったか。なんとなく雰囲気はわかっているものの勉強し直そうと思い適当に検索したら出てきた、Reverse Engineering x86 Processor Microcode という論文を読んでみることにした。

この論文は 2017 年に発表されていて、当時としては比較的モダンな AMD の k8/k10 マイクロアーキテクチャの microcode のリバースエンジニアリングを行って、 モダンな商用プロセッサの microcode がどのようなものでどのように実現されているかということを調べている。

まずはプロセッサだが、論理的には大きく2つの部分に分けられる。1つはレジスタファイルや ALU などを持つデータパスと、もう一つは、命令をデコードしデータパスのコントロールシグナルを発行するコントロールユニットだ。 コントロールユニットのデコーダはざっくり言えば命令からコントロールシグナルへのマッピングを行う。このようなマッピングは hardwired で実装することも可能だし、ROM 内に保存された表を引くことでも実装できる。 そして後者の実現方法を microcoded decode unit という。

microcode は大きく2つの構造があり、データパスのユニットのコントロールシグナルを直接発行する horizontal encoding と、データパスのユニットを駆動する RISC 命令のようなものを発行する vertical encoding というものがある。 vertical encoding による microcoded decode unit は ISA を microcode instruction に変換する一種のインタープリタとみなすことができる。 当然 microcode instruction は直接はデータパスを駆動できないので、実際のシグナルを生成するデコーダが必要になる。

AMD の k8/k10 マイクロアーキテクチャでは、vertical encoding の microcoded decode unit を採用しているらしい。 もちろん、すべての命令を microcode でデコードすると遅いので、レイテンシが求めれらるような単純な命令は hardwired なデコーダを使用する。 そのためモダンな x86 CPU ではハイブリッドなデコーダを採用していることになる。

AMD の microcode アップデートは ROM に書かれた microcode decoder の一部を書き換えることで行っている。 この ROM はレイテンシのため、トランジスタの配線の仕方によって実現されているらしく、物理的に書き換えることはできない。 そのため書き換えは ROM のパッチ先アドレスを格納する match register と RAM によって実装している。 つまり、microcode decoder の表引きを行うときに、アドレスが match register に合致するなら RAM を読み、そうでないなら ROM を読むことで書き換えを実現する。 microcode パッチは RAM に格納するので、起動時に BIOS や OS から毎回 CPU にパッチをロードする必要がある。

AMD の microcode アップデータは 2011 年まで暗号化されていなかったらしく(Intel は 1995 年から暗号化している)、筆者らは専用の OS を作成してアップデータを書き換えて挙動を見ることで、 どのような機能が microcode で実現されているか調べている。

microcode は 3オペランドRISC のような命令列で表現されているらしい。 書き換え可能な命令は、microcode で実現された命令に限られ、hardwired でデコードされる命令は書き換え方法がわからないということだった。

論文を読んでみて microcode アップデートによる修正がどのように実現されているか知ることができた。 Zenbleed は一種のロジックミスなので、microcode 更新によって実行される microcode instruction 数が増減しそうだがそれほどペナルティはなさそうだ。 一方で、Inception や Downfall ではキャッシュインバリデートやフェンスが必要になるのでペナルティが多いのだろう。

X470D4U に OpenBMC を移植する

はじめに

ASRock Rack から発売されている X470D4U に対して OpenBMC を移植した。

X470D4U は IPMI 機能を搭載しており、管理用イーサネットポート(またはホスト共有ポート)を通じて Web ベースの管理インターフェースにアクセスできる。 こうした機能は、各種ベンダーのサーバにも搭載されていて、Dell だったら iDRAC、HP だったら iLO などが一般のご家庭では利用されていることと思う。

X470D4U の IPMI 機能は自分で使うには必要十分で動作も軽快なので特に不満はないのだが、後継製品の発売によるものかファームウェア更新がされなくなってしまっている。 当然 IPMI 機能にも脆弱性は存在し得るので、できれば更新されたソフトウェアスタックを利用したい。実際にファーウェアファイルを binwalk でみてみると バージョン 3.14 ベースの Linux kernel を使っていることがわかる。 上述の理由はモチベーションの一つだが、単に OpenBMC が面白そうというのも多分にある。

OpenBMC は Linux Foundation による OSS の IPMI ソフトウェアスタックで、IBM のコードをベースに Facebook なども開発に参加しているらしい。 ちょっと検索すると IBM は Power サーバの IPMI に使っていて、Facebook は DC のサーバの IPMI に使っているようだ。

X470D4U に搭載されている BMC は Aspped の AST2500 で OpenBMC でよくサポートされているようだ。また ASRock のマザーボードはいくつかオフィシャルでサポートされている他、Renze Nicolai 氏によって今回移植を行う X470D4U の後継である X570D4U の移植もある。 更に検索したところ、X570D4U に対する移植を X470D4U で起動したレポート(中国語)もあった。 実際に試したところ、IPMI ファームウェアを保存するフラッシュのサイズが異なることで起動に失敗したが、サイズ指定を修正したところ起動に成功した。

この時点でほぼ目処がついているが、実際にはボードの違いにより電源管理、センサーなどの微調整が必要になる。

準備

シリアルコンソールへのアクセス

OpenBMC が利用しているブートローダである U-Boot とのやり取りやカーネルメッセージを閲覧するためにシリアルコンソールを用意する必要がある。

ボード上の DEBUG と印字されているピンヘッダがあるので、ここに 3.3V レベルのシリアルケーブルを接続する。 X570D4U の情報を Renze 氏が提供してくれていて、ピンアサインはこれと同一になっている。

シリアル接続に詳しくなかったのでいろいろ検索したが、ピンとケーブルの TX と RX をクロスして接続し、GND 同士はそのまま接続する。(GND の接続はなくても大丈夫かもしれないが GND を接続すると電位が安定するのだと思う) 3.3V は対向に電圧を供給するために使うようで接続は不要らしい。

フラッシュの書き込み方

X470D4U に搭載されているのは winbond の 25Q256JVFQ というフラッシュメモリで、SOP 16 300 mil のパッケージのものが使われている。 このフラッシュは観音開きのソケットに収められていて、ソケットを開けるとそのまま取り外すことができる。 フラッシュの書き込みのために適当なプログラマーとソケットを購入しておくと良い。

また先の Renze 氏のページにあるフラッシュ書き込み用のピンヘッダーが X470D4U にもあるのでこちらを使うと、 フラッシュを取り外さずに書き込みができるようだ。 ただこのピンヘッダはハーフピッチ(1.27 mm)で接続するための適当なコネクタが見つけられなかったので実際には使っていない。

実は物理的なプログラマーを利用せずにフラッシュの書き込みができ、実際にはこちらの方法を利用した。 AST2500 はホスト側から任意のアドレスにアクセスするという機能(脆弱性と言われてしまったが)があり、これを使うとホスト側からフラッシュの書き換えができる。 このツールは ASRock のサポートサイトからバイナリをダウンロードできる。

ビルドと書き込み

コードは次のリポジトリx470d4u ブランチに配置してある。

https://github.com/toshipp/openbmc

上記のリポジトリをクローンしたあと、次のようにビルドを行う。

cd ${PATH_TO_REPO}
. ./setup x470d4u ${PATH_TO_BUILD}
bitbake obmc-phosphor-image

ビルドは時間がかかる上にディスクも大量に消費するので注意すること。

ビルドしたアーティファクトは、${PATH_TO_BUILD}/tmp/deploy/images/x470d4u 以下に出力される。 フラッシュに書き込む raw イメージは、image-bmc ファイル(シンボリックリンク)になるのでこれをホストマシンにコピーして socflash を使って書き込む。

socflash_x64 -s image-bmc

書き込む前には、オリジナルイメージを以下のようにバックアップしておくのが良い。

socflash_x64 -b orig.img

書き込みが終わると自動的に BMC がリスタートするが、デバイスの状態が以前起動していたファームウェアによってされていた設定のままの場合があるので、一旦電源を落として起動するのが良い。

次からは、Web インターフェースの Firmware アップデート画面から更新ができる。 このときに選択するファイルは、obmc-phosphor-image-x470d4u.static.mtd.tar になるので注意する。

移植作業

すでにサポートされなくなってしまったみたいだが、移植作業の流れはドキュメントに記載されているので、これを参考に行った。

既存のシステムをコピーして始めるのが良いらしく、今回は同じく ASRock から発売されている E3C246D4I-2T をコピー元とした。

カーネル

X470D4U に必要となるカーネル機能は upstream に入っているので、カーネルに対する特別な開発は必要ない。 ただし、ボードごとにどのようなデバイスが搭載されているか、どのアドレスに設定されているかが異なるためにその設定を行う必要がある。 このような設定は、デバイスツリーを利用して行う。

OpenBMC ではデバイスツリーは Linux kernel のリポジトリに入れる方針になっているが、X470D4U の dts ファイルは現状存在しないので、OpenBMC 側の recipes-kernel に dts ファイルを追加する。 このファイルは Linux 側に含まれている aspeed-g5.dtsi を継承して、必要な部分だけ設定するようになっている。

いくつかの設定については後述するとして、一部の設定を説明する。 プレフィックス& がつくノードは、ラベルを参照していて、aspeed-g5.dtsi に定義されているノードを上書きしている。

  • chosen ノードは標準出力先の設定とカーネルパラメータを指定している。
  • memory ノードは BMC に搭載されたメモリの物理アドレスとサイズを指定している。
  • bmc-ready ノードでは J0 ピン の GPIO の出力設定を行っている。このピンに電圧をかけていないと BIOS 起動が遅くなる。
  • &fmc ノードでは BMC ファームウェアが保存されているフラッシュの設定を行っている。データシート的にはデフォルトでも問題ないはずだが、やや不安定になる感じがあるので、spi-max-frequency をデフォルトから下げている。
  • &uart5 ノードは BMC の DEBUG ヘッダにつながっているので有効にしている。
  • &mac0 ノードは BMC 専用イーサネットポートを表す。
  • &mac1 ノードはホストと共有しているイーサネットポートを表す。use-ncsi をつけることで、NC-SI を利用して通信ができるようになる。

BIOS ポストコード取得機能

OpenBMC では phosphor-host-postdというデーモンがホストの BIOS ポストコードの読み取りを行っている。

このデーモンが利用するデバイスを有効にするために、デバイスツリーで &lpc_snoop ノードを設定している。

SoL 機能

SoL は Serial over Lan の略で、シリアルコンソールを LAN 経由でアクセスするための機能のことをいう。

AST2500 では virtual UART という機能が存在していて、ホストと BMC がシリアルで接続されているように通信ができる。 ホスト側の BIOS にシリアル出力を SoL にリダイレクトするという設定があるので、これを有効にすることでホストのシリアル出力が BMC 側に転送される。

&vuart ノードでこの機能を有効にする。

KVM 機能

KVM はおそらく最も重要な機能の一つで、ネットワーク越しにホストの画面をみたりキーボード入力を行うことができる。

この機能は OpenBMC ではいくつかのコンポーネントが協調して実装されている。

ローレベルなところでは obmc-ikvmVNC プロトコルサーバとして動作している。 このサーバが AST2500 が提供しているビデオキャプチャ機能をつかってホストの画面を取得し、USB ガジェットドライバを利用してホスト側にキーボード入力やマウス入力を行う。

キャプチャ機能は &video ノードで、USB ガジェットドライバは &vhub ノードで有効にしている。

この VNC サーバは BMC のローカルホストの 5900 番ポートで通信を待ち受けている。

Web インターフェースからこのサーバを利用するために、bmcweb が websocket によるプロトコル変換を行っている。 また、ブラウザ上の VNC クライアントとして noVNCwebui-vue に組み込まれている。

余談だが、USB ガジェットではキーボード入力はスキャンコードでやり取りされるが、noVNC は Javascript なのでスキャンコードではなくて実際に入力される文字しか取得できない。(例えば J と入力したときに、shift + j を使う必要がある) そのため obmc-ikvm では入力文字からスキャンコードの組み合わせを変換しているのだが、これは英語キーボードを前提としているので、日本語キーボードを使っていると入力できない文字がある。この問題を解決するには現状パッチするしかないようだ。

インベントリ機能

IPMI の仕様は FRU 情報について定義していて、この情報は EEPROM などに保存されている。

X470D4U は i2c バスの下に EEPROM がぶら下がっている。eeprom ノードがこの EEPROM を定義している。

entity-manager は EEPROM から FRU 情報を読み取って D-Bus に情報を出力する。 entity-manager は後述する dbus-sensors とも協調し、設定ファイルの jsonExposes にセンサーに関する設定を記載すると、読み取った FRU 情報に合致した設定ファイルのセンサー定義を D-Bus に出力する。

これらの設定は次の json ファイルに記載している。

entity-manager に限らず OpenBMC は D-Bus を使って情報のやり取りをしているので、busctl を使うことでデバッグができる。 busctl listbusctl treebusctl introspect などを使うことで、各プログラムが出力する情報を確認できる。 また、各プログラムは D-Bus メソッドを提供していることもあるので、busctl call を呼び出すこともできる。

センサー機能

各種センサーは dbus-sensors が読み取って D-Bus に情報を出力する。

今回は電圧、ファンスピード、温度についてのセンサーの設定を行った。

電圧は、&adc ノードと iio-hwmon ノードを設定してドライバを設定している。 読み取った値がどこの電圧値なのかと実際の電圧値への変換係数は entity-manager の設定の ADC タイプで設定する。 どのインデックスがどこの電圧値なのかという情報は、公式ファームウェアの画面から推測できる。 また、公式ファームウェアSDR.dat というファイルが含まれるので、IPMI の使用を見つつ次のようなスクリプトを使うと、名前とインデックスの対応が取れる。 (インデックスと名前についての情報はおそらく著作権的には問題ないと思われるが、若干グレーかもしれない。)

import sys
data = open(sys.argv[1], "rb").read()
entries = data.split(b"\x5a\x40")
for e in entries:
    print(e[4-1])
    if e[13-1] == 2:
        idlen = e[48-1] & 0xf
        name = e[49-1:49-1+idlen]
        ownerid = e[6-1]
        ownerlun = e[7-1]
        sensnum = e[8-1]
        entid = e[9-1]
        entins = e[10-1]
        assert(ownerid == 32)
        assert(ownerlun == 0)
        if entid != 7:
            continue
        unit1 = e[21-1]
        unit2 = e[22-1]
        unit3 = e[23-1]
        m = e[25-1]
        assert(e[26-1] == 0)
        assert(e[27-1] == 0)
        assert(e[28-1] == 0)
        assert(e[29-1] == 0)
        Rexp = e[30-1] >> 4
        Bexp = e[30-1] & 0xf
        nominal = e[32-1]
        print(f"{name}, sn={sensnum}, entins={entins}, m={m}, {Rexp}, {Bexp}, {nominal}")

電圧の変換係数も公式ファームウェアの画面と hwmon の値からそれっぽい値を作って、 entity-manager のリポジトリに含まれる ASRock のボードの設定ファイルに書かれている近い値を設定した。

いくつかの電圧値はホスト側の電源が落ちていると 0 に近い値が出力される。そのようなものには "PowerState": "On" を設定して電源が入っているときだけ有効にしている。

またバッテリー電圧は、GPIO の G0 ピンをアサートしてやらないと正しい値が読み取れないらしい。 これは BridgeGpio によって設定できる。 この挙動がわからず悩んでいたが、ほかのボードの設定を見ていて気がついた。

ファンスピードは、&pwm_tacho ノードを設定することでドライバを設定している。 自分の環境では CPU ファンしかファンコントローラーに接続していないので、 regfan-tack-ch の値は間違っているかもしれない。

温度センサーは &i2c1 ノードの下に NCT6779 というチップがぶら下がっているらしい。 このチップはホスト側の電源を利用しているので、ホスト側が起動していないとドライバを読み込んでもデバイスが認識できない。 そのため、デバイスツリーでは設定せずに、entity-manager で "PowerState": "BiosPost" を設定して BIOS 起動が終わってから動的に初期化するようにしている。

ちなみに i2c バスのスキャンは i2cdetect コマンドを使って行えるが、実際に何があるかはわからないので、他のボードの設定やボードのマニュアルを見つつ推測した。 なにか良い方法があるのだろうか。

電源管理

電源管理は x86-power-control で行う。 このプログラムは、GPIO を使って電源オンオフやリセットを行う。 また、電源状態や、BIOS ポストの完了の取得も GPIO を使って行う。 つまり GPIO ピンアサインがわからないといけないのだが、マニュアルにはそのような情報が記載されていない。

とりあえず他のボードの設定を参考に、電源オンオフやリセットを試したところ正しく動いた。 電源状態や BIOS ポストの完了はどうもボードによって異なるらしいので、gpiomon コマンドを使って GPIO ピンの監視をしつつ電源をつけ消ししてそれっぽいところを使うようにした。

スクリーンショット

まとめ

今回の移植でおおよそ公式ファームウェアの機能と同等の機能を持った IPMI を OpenBMC で実現できた。

i2c デバイスや GPIOのピンアサインの特定さえできればそこまで作業量は多くないので公式の保守がなくなっていたり、OSS の IPMI が欲しい人は試してみると良いかもしれない。

ところで電源を GPIO でオンオフしたり、画面キャプチャしたり USB ガジェットを繋げば IPMI ってできちゃうからラズパイとかでも良さそうだよなと思ったら、そのものずばりの PiKVMというのがあるんですね…。

ublk ドライバを試す

Linux の 6.0 では Userspace block device driver(ublk driver)という機能が追加された。

名前の通り、ユーザーランドでブロックデバイスを実装するためのドライバになっている。

ドキュメントは以下にある。 https://github.com/torvalds/linux/blob/v6.0/Documentation/block/ublk.rst

このドキュメントによると、既存の仮想ブロックドライバをユーザーベースに移行したいというモチベーションがあるらしい。

利点としては以下が挙げられている。

ユーザースペースプログラムのことを、ublk serverと呼称している。

実装には、blk-mq リクエストベースドライバを利用していて、各 IO に tag が振られる。 ublk serverも同様に tag を割り当てて、それが一対一にマッピングされるらしい。

リクエストのやり取りには io_uring を利用していて、ドライバとのやり取りで使うだけでなく、 ublk server内の IO にも使うことが推奨されている。

ここに実装の一例がある。 https://github.com/ming1/ubdsrv

楽に実装するためのライブラリもある。 https://github.com/ming1/ubdsrv/tree/master/lib

このドライバをコントロールするためのグローバルなコントロール用のデバイスとして、/dev/ublk-controlがある。 上述の実装を見ると、このデバイスとのやり取りも io_uring を使うらしい。

新しいデバイスを追加するためには、ublk serverが新しいデバイスとの IO をやり取りするためのデバイスを用意する必要がある。そのためのコマンドをコントロールバイスに発行すると、/dev/ublkc*が作られる。 その後、ブロックデバイスの追加コマンドを発行すると/dev/ublkb*にブロックデバイスが作られる。

ublk serverはキューごとにスレッドを作成し、io_uringをハンドルする必要がある。 server がUBLK_IO_FETCH_REQを発行すると、ドライバがIOリクエストの転送を始める。 IOはublksrv_io_descエンコードされており、serverが処理を終えたら結果をUBLK_IO_COMMIT_AND_FETCH_REQコマンドを使って返す。このコマンドで次のリクエスト処理も開始される。

将来の開発計画として、コンテナから ublk を使えるようにしたり、zero copy できるようにしたいそうだ。

それでは実際に使ってみる。実行環境は 2022/11/23 時点の最新の arch linux。 デフォルトではキューの数が1だったので、ublk add-qオプションでキューの数を32個に増やしている

sudo pacman -S autoconf-archive
git clone https://github.com/ming1/ubdsrv.git
cd ubdsrv
autoreconf -i
./configure
make
sudo modprobe brd rd_size=4194304
sudo modprobe ublk_drv
sudo ./ublk add -t loop -q 32 -f /dev/ram0
sudo mkfs.ext4 /dev/ublkb0
mkdir -p t
sudo mount /dev/ublkb0 t
cd t
sudo fio --name=rw --size=250M --rw=randrw --nrfiles=8 --ioengine=libaio --iodepth=16 --direct=1 --overwrite=1 --runtime=60 --time_based --numjobs=4 --group_reporting
rw: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=16
...
fio-3.33
Starting 4 processes
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
Jobs: 4 (f=32): [m(4)][100.0%][r=703MiB/s,w=703MiB/s][r=180k,w=180k IOPS][eta 00m:00s]
rw: (groupid=0, jobs=4): err= 0: pid=32492: Wed Nov 23 15:35:29 2022
  read: IOPS=180k, BW=704MiB/s (738MB/s)(41.3GiB/60001msec)
    slat (nsec): min=1150, max=116402, avg=8394.65, stdev=4924.49
    clat (nsec): min=940, max=766996, avg=167269.66, stdev=49871.85
     lat (usec): min=16, max=775, avg=175.66, stdev=52.93
    clat percentiles (usec):
     |  1.00th=[   67],  5.00th=[   77], 10.00th=[   86], 20.00th=[  143],
     | 30.00th=[  159], 40.00th=[  167], 50.00th=[  174], 60.00th=[  178],
     | 70.00th=[  184], 80.00th=[  192], 90.00th=[  206], 95.00th=[  253],
     | 99.00th=[  322], 99.50th=[  343], 99.90th=[  388], 99.95th=[  420],
     | 99.99th=[  498]
   bw (  KiB/s): min=599648, max=1156024, per=100.00%, avg=721015.53, stdev=21708.24, samples=476
   iops        : min=149912, max=289006, avg=180253.88, stdev=5427.06, samples=476
  write: IOPS=180k, BW=703MiB/s (738MB/s)(41.2GiB/60001msec); 0 zone resets
    slat (nsec): min=1200, max=176111, avg=8791.47, stdev=5048.88
    clat (nsec): min=910, max=783474, avg=168873.39, stdev=50225.29
     lat (usec): min=18, max=799, avg=177.66, stdev=53.40
    clat percentiles (usec):
     |  1.00th=[   68],  5.00th=[   77], 10.00th=[   87], 20.00th=[  145],
     | 30.00th=[  161], 40.00th=[  169], 50.00th=[  176], 60.00th=[  180],
     | 70.00th=[  186], 80.00th=[  192], 90.00th=[  208], 95.00th=[  255],
     | 99.00th=[  326], 99.50th=[  343], 99.90th=[  392], 99.95th=[  424],
     | 99.99th=[  502]
   bw (  KiB/s): min=599456, max=1154560, per=100.00%, avg=720465.01, stdev=21605.03, samples=476
   iops        : min=149864, max=288640, avg=180116.25, stdev=5401.26, samples=476
  lat (nsec)   : 1000=0.01%
  lat (usec)   : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.23%
  lat (usec)   : 100=13.17%, 250=81.43%, 500=5.16%, 750=0.01%, 1000=0.01%
  cpu          : usr=8.49%, sys=31.36%, ctx=29876275, majf=0, minf=52
  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=100.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=10813820,10805346,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=16

Run status group 0 (all jobs):
   READ: bw=704MiB/s (738MB/s), 704MiB/s-704MiB/s (738MB/s-738MB/s), io=41.3GiB (44.3GB), run=60001-60001msec
  WRITE: bw=703MiB/s (738MB/s), 703MiB/s-703MiB/s (738MB/s-738MB/s), io=41.2GiB (44.3GB), run=60001-60001msec

Disk stats (read/write):
  ublkb0: ios=10807636/10799329, merge=0/42, ticks=255538/258038, in_queue=513576, util=99.89%

以前作ったNBDベースのメモリーバックエンドのユーザーランドブロックデバイスで同様にベンチマークを取ると以下のようになった。 こちらもublkと同様にサーバは32スレッドで動かしている。

rw: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=16
...
fio-3.33
Starting 4 processes
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
Jobs: 4 (f=32): [m(4)][100.0%][r=655MiB/s,w=653MiB/s][r=168k,w=167k IOPS][eta 00m:00s]
rw: (groupid=0, jobs=4): err= 0: pid=49755: Wed Nov 23 16:16:34 2022
  read: IOPS=166k, BW=648MiB/s (680MB/s)(38.0GiB/60001msec)
    slat (nsec): min=1730, max=664245, avg=10800.29, stdev=5755.01
    clat (nsec): min=1920, max=1455.3k, avg=180044.58, stdev=27830.83
     lat (usec): min=19, max=1472, avg=190.84, stdev=28.91
    clat percentiles (usec):
     |  1.00th=[  120],  5.00th=[  137], 10.00th=[  147], 20.00th=[  159],
     | 30.00th=[  165], 40.00th=[  174], 50.00th=[  180], 60.00th=[  186],
     | 70.00th=[  194], 80.00th=[  202], 90.00th=[  215], 95.00th=[  227],
     | 99.00th=[  255], 99.50th=[  269], 99.90th=[  306], 99.95th=[  326],
     | 99.99th=[  379]
   bw (  KiB/s): min=625760, max=714384, per=100.00%, avg=664019.76, stdev=4227.49, samples=476
   iops        : min=156440, max=178596, avg=166004.92, stdev=1056.89, samples=476
  write: IOPS=166k, BW=648MiB/s (679MB/s)(37.9GiB/60001msec); 0 zone resets
    slat (nsec): min=1660, max=532044, avg=11640.12, stdev=6046.44
    clat (usec): min=12, max=1441, avg=182.55, stdev=27.93
     lat (usec): min=29, max=1460, avg=194.19, stdev=29.04
    clat percentiles (usec):
     |  1.00th=[  124],  5.00th=[  141], 10.00th=[  149], 20.00th=[  161],
     | 30.00th=[  169], 40.00th=[  176], 50.00th=[  182], 60.00th=[  188],
     | 70.00th=[  196], 80.00th=[  204], 90.00th=[  217], 95.00th=[  231],
     | 99.00th=[  260], 99.50th=[  273], 99.90th=[  310], 99.95th=[  334],
     | 99.99th=[  392]
   bw (  KiB/s): min=626976, max=717352, per=100.00%, avg=663490.35, stdev=4212.33, samples=476
   iops        : min=156744, max=179338, avg=165872.59, stdev=1053.08, samples=476
  lat (usec)   : 2=0.01%, 10=0.01%, 20=0.01%, 50=0.01%, 100=0.05%
  lat (usec)   : 250=98.53%, 500=1.42%, 750=0.01%, 1000=0.01%
  lat (msec)   : 2=0.01%
  cpu          : usr=5.53%, sys=34.42%, ctx=23019937, majf=0, minf=44
  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=100.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=9955048,9947240,0,0 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=16

Run status group 0 (all jobs):
   READ: bw=648MiB/s (680MB/s), 648MiB/s-648MiB/s (680MB/s-680MB/s), io=38.0GiB (40.8GB), run=60001-60001msec
  WRITE: bw=648MiB/s (679MB/s), 648MiB/s-648MiB/s (679MB/s-679MB/s), io=37.9GiB (40.7GB), run=60001-60001msec

Disk stats (read/write):
  nbd0: ios=9935485/9927606, merge=0/42, ticks=239988/241003, in_queue=480991, util=99.89%
sudo fio --name=rw --size=250M --rw=randrw --nrfiles=8 --ioengine=libaio       13.68s user 83.45s system 157% cpu 1:01.61 total

ublkは700MiB/s程度出ているが、nbdでは650MiB/s程度なのでublkのほうが少し速い。 またnbdではサーバ内で確保したメモリーに書くだけなので、ublkも同様にすればより速くなるかもしれない。

なお実験は、CPUがAMD Ryzen 9 3950X 16Coreでメモリーは32GiB(DDR4-2133)の環境を使った。

nftables メモ

nftables は iptables と比べて色々変わっているのでメモ。

ドキュメントは manwiki を読んだ。

  • ruleset はカーネル内に存在する nftable のすべての情報を書き出す。iptables-save に相当するもの。
  • table はチェインやセットなどを保持するためのコンテナ。アドレスファミに理紐付いている。iptables と異なり事前定義されてはいない。
  • chain はルールを保持するコンテナ。iptablesと違って事前定義チェインは存在しない。base チェインと regular チェインという二種類ある。base はエントリーポイントで、regular は他のチェインからジャンプしてくるときに使う。 base チェインは type、hook、priority が必須パラメータになっている。
  • rule はパケット処理ルール。expression と statement から作られる。
  • expression は定数値やパケットから得られる値。各種演算子や他の expression と組み合わせて複雑な expression を作れる。 expressionではmetaを使うとパケットのメタデータを取得できる。 インタフェースとかはわかるけど、l4protoとかpalyloadでは?という気もする。
  • statement はアクションを示す。statement は評価を打ち切るか継続するかするものがある。acceptやdropは評価を打ち切る。continueは次のルールを実行する。statement を指定しないと continue 扱い。

構文が明に書いていなくて、イマイチわかりにくいところがある。 expression では、ip saddr 127.0.0.1 と書けるけど、比較演算子を使って ip saddr == 127.0.0.1 みたいにも書けるらしい。

https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes#Rules https://wiki.nftables.org/wiki-nftables/index.php/Building_rules_through_expressions

パーサを見てみると、statementの一部として、match_stmtが定義されていて、match_stmt は relational_expr であるらしい。 この relational_expr を見てみると、OP_IMPLICIT というオペレータがあって、どうやらこいつが、OP_EQ と同じような役割を果たすらしい。

http://git.netfilter.org/nftables/tree/src/parser_bison.y#n2843 http://git.netfilter.org/nftables/tree/src/parser_bison.y#n4650

あと面白いのは、concatenations という機能で、set などとのマッチを行うときに、expression の値を連結したものをマッチすることができる。 ip saddr . ip daddr { 127.0.0.1 . 1.1.1.1} みたいな書き方で、python でいうと (ip saddr, ip daddr) in {(127.0.0.1, 1.1.1.1)} みたいな意味になるようだ。

https://wiki.nftables.org/wiki-nftables/index.php/Concatenations

Arch Linuxでath10kドライバーを使って11acフルチャンネル対応したAPを立てる

Linuxを利用して、11acに対応したAtherosのWiFiモジュールを使って、全対応チャンネルを有効化したAPを建てる方法を記録しておく。 ここで記載する内容は、カーネルバージョンやコンフィグレーションに強く依存するため、他のバージョンなどでは動作が異なる可能性がある。

今回は以下の環境で検証した。

LinuxでAPを立てるには、WiFiモジュールとhostapdを適切に設定する必要がある。 AtherosのWiFiモジュールはカーネルにドライバーが組み込まれているので、基本的にそのまま使うことができる。 ただし、利用可能な電波は各国の規制状況によって異なるため、規制情報データベースが別に用意されている。 このデータベースはArch Linuxではwireless-regdbパッケージとして用意されていて、 cfg80211モジュールを使ってWiFiの設定を行う際にカーネルが読み込むようになっている。

実は規制情報はAtherosドライバー自体にも組み込まれていて、wireless-regdbとドライバーの情報の組み合わせによって、 挙動が変わるのが設定を難しくしている。

cfg80211モジュールは、モジュールオプションやiwコマンドで規制情報を設定することができる。 規制情報はregulatory domainと表現されていて、iwコマンドではiw reg set/getなどで扱うことができる。 基本的に、国によって規制が変わるので、regulatory domainは国コードを使って指定することになる。

次の操作では、iwコマンドではなく、cfg80211モジュールのオプションを使ってregulatory domainを設定する。 iwコマンドだと、cfg80211モジュールや、ドライバーモジュールの初期化が終わってから設定することになり、 理由は後述するが、うまく設定が反映されないからである。

オプションは、/etc/modprobe.d/に適当なファイルをおいて以下のように記載する。

options cfg80211 ieee80211_regdom=JP

あるいは、modprobeコマンドで指定することもできる。 実験時はmodprobeコマンドで指定するのが楽だった。

まずは、オプションを設定しないときの挙動を確認する。 (cfg80211は00をデフォルトのregulatory domainとして利用していて、これを指定しても良い)

このとき、iw reg getの結果は以下のようになった。

global
country 00: DFS-UNSET
        (755 - 928 @ 2), (N/A, 20), (N/A), PASSIVE-SCAN
        (2402 - 2472 @ 40), (N/A, 20), (N/A)
        (2457 - 2482 @ 20), (N/A, 20), (N/A), AUTO-BW, PASSIVE-SCAN
        (2474 - 2494 @ 20), (N/A, 20), (N/A), NO-OFDM, PASSIVE-SCAN
        (5170 - 5250 @ 80), (N/A, 20), (N/A), AUTO-BW, PASSIVE-SCAN
        (5250 - 5330 @ 80), (N/A, 20), (0 ms), DFS, AUTO-BW, PASSIVE-SCAN
        (5490 - 5730 @ 160), (N/A, 20), (0 ms), DFS, PASSIVE-SCAN
        (5735 - 5835 @ 80), (N/A, 20), (N/A), PASSIVE-SCAN
        (57240 - 63720 @ 2160), (N/A, 0), (N/A)

phy#0
country 99: DFS-UNSET
        (2402 - 2472 @ 40), (N/A, 20), (N/A)
        (5140 - 5360 @ 80), (N/A, 30), (N/A), PASSIVE-SCAN
        (5715 - 5860 @ 80), (N/A, 30), (N/A), PASSIVE-SCAN

また、iw phyコマンドの結果は次のようになった。 (ここでは5GHzだけ抜粋している)

                Frequencies:
                        * 5180 MHz [36] (30.0 dBm) (no IR)
                        * 5200 MHz [40] (30.0 dBm) (no IR)
                        * 5220 MHz [44] (30.0 dBm) (no IR)
                        * 5240 MHz [48] (30.0 dBm) (no IR)
                        * 5260 MHz [52] (30.0 dBm) (no IR, radar detection)
                        * 5280 MHz [56] (30.0 dBm) (no IR, radar detection)
                        * 5300 MHz [60] (30.0 dBm) (no IR, radar detection)
                        * 5320 MHz [64] (30.0 dBm) (no IR, radar detection)
                        * 5500 MHz [100] (disabled)
                        * 5520 MHz [104] (disabled)
                        * 5540 MHz [108] (disabled)
                        * 5560 MHz [112] (disabled)
                        * 5580 MHz [116] (disabled)
                        * 5600 MHz [120] (disabled)
                        * 5620 MHz [124] (disabled)
                        * 5640 MHz [128] (disabled)
                        * 5660 MHz [132] (disabled)
                        * 5680 MHz [136] (disabled)
                        * 5700 MHz [140] (disabled)
                        * 5720 MHz [144] (disabled)
                        * 5745 MHz [149] (30.0 dBm) (no IR)
                        * 5765 MHz [153] (30.0 dBm) (no IR)
                        * 5785 MHz [157] (30.0 dBm) (no IR)
                        * 5805 MHz [161] (30.0 dBm) (no IR)
                        * 5825 MHz [165] (30.0 dBm) (no IR)
                        * 5845 MHz [169] (30.0 dBm) (no IR)
                        * 5865 MHz [173] (disabled)

これを見ると、100から140チャンネルは無効化され、その他はno IRフラグやradar detectionフラグが設定されている。 disabledフラグはその名の通り、完全にそのチャンネルが利用できないことを示す。 no IRフラグは、電波の発信を許可されないことを示す。DFSを使ってパッシブスキャンすれば利用できるらしい。 (最終的にうまく行った設定では、no IRフラグ単体の帯域は存在しなかったので本当にそうなのかはわからないが。) またradar detectionはDFSを利用する必要がある帯域らしい。 no IRとradar detectionが何となくかぶっている気がするので詳しい人がいれば教えてほしい。 一応以下のドキュメントも読んだがいまいちわからなかった。 https://wireless.wiki.kernel.org/en/developers/regulatory/processing_rules#post_processing_mechanisms

デフォルトのregulatory domainではhostapdを起動してもうまく動かなかった。 上記のように利用できる電波帯がないので仕方がないだろう。

次にregulatory domainにUSを設定してみる。

iw reg getは以下のようになり、globalとphyがどちらもUSになっている。

global
country US: DFS-FCC
        (902 - 904 @ 2), (N/A, 30), (N/A)
        (904 - 920 @ 16), (N/A, 30), (N/A)
        (920 - 928 @ 8), (N/A, 30), (N/A)
        (2400 - 2472 @ 40), (N/A, 30), (N/A)
        (5150 - 5250 @ 80), (N/A, 23), (N/A), AUTO-BW
        (5250 - 5350 @ 80), (N/A, 24), (0 ms), DFS, AUTO-BW
        (5470 - 5730 @ 160), (N/A, 24), (0 ms), DFS
        (5730 - 5850 @ 80), (N/A, 30), (N/A), AUTO-BW
        (5850 - 5895 @ 40), (N/A, 27), (N/A), NO-OUTDOOR, AUTO-BW, PASSIVE-SCAN
        (57240 - 71000 @ 2160), (N/A, 40), (N/A)

phy#0
country US: DFS-FCC
        (902 - 904 @ 2), (N/A, 30), (N/A)
        (904 - 920 @ 16), (N/A, 30), (N/A)
        (920 - 928 @ 8), (N/A, 30), (N/A)
        (2400 - 2472 @ 40), (N/A, 30), (N/A)
        (5150 - 5250 @ 80), (N/A, 23), (N/A), AUTO-BW
        (5250 - 5350 @ 80), (N/A, 24), (0 ms), DFS, AUTO-BW
        (5470 - 5730 @ 160), (N/A, 24), (0 ms), DFS
        (5730 - 5850 @ 80), (N/A, 30), (N/A), AUTO-BW
        (5850 - 5895 @ 40), (N/A, 27), (N/A), NO-OUTDOOR, AUTO-BW, PASSIVE-SCAN
        (57240 - 71000 @ 2160), (N/A, 40), (N/A)

一方、iw phyは以下のようになっており、00を指定したときと変わっていない。

                Frequencies:
                        * 5180 MHz [36] (23.0 dBm) (no IR)
                        * 5200 MHz [40] (23.0 dBm) (no IR)
                        * 5220 MHz [44] (23.0 dBm) (no IR)
                        * 5240 MHz [48] (23.0 dBm) (no IR)
                        * 5260 MHz [52] (24.0 dBm) (no IR, radar detection)
                        * 5280 MHz [56] (24.0 dBm) (no IR, radar detection)
                        * 5300 MHz [60] (24.0 dBm) (no IR, radar detection)
                        * 5320 MHz [64] (24.0 dBm) (no IR, radar detection)
                        * 5500 MHz [100] (disabled)
                        * 5520 MHz [104] (disabled)
                        * 5540 MHz [108] (disabled)
                        * 5560 MHz [112] (disabled)
                        * 5580 MHz [116] (disabled)
                        * 5600 MHz [120] (disabled)
                        * 5620 MHz [124] (disabled)
                        * 5640 MHz [128] (disabled)
                        * 5660 MHz [132] (disabled)
                        * 5680 MHz [136] (disabled)
                        * 5700 MHz [140] (disabled)
                        * 5720 MHz [144] (disabled)
                        * 5745 MHz [149] (30.0 dBm) (no IR)
                        * 5765 MHz [153] (30.0 dBm) (no IR)
                        * 5785 MHz [157] (30.0 dBm) (no IR)
                        * 5805 MHz [161] (30.0 dBm) (no IR)
                        * 5825 MHz [165] (30.0 dBm) (no IR)
                        * 5845 MHz [169] (27.0 dBm) (no IR)
                        * 5865 MHz [173] (disabled)

次にregulatory domainにJPを設定してみる。 iw reg getは以下のようになる。 globalは98という謎のdomainになり、phyはなぜかUSが設定されている。

iw reg get
global
country 98: DFS-UNSET
        (2402 - 2472 @ 40), (N/A, 20), (N/A)
        (5170 - 5250 @ 80), (N/A, 20), (N/A), AUTO-BW
        (5250 - 5330 @ 80), (N/A, 20), (0 ms), DFS, AUTO-BW
        (5490 - 5710 @ 160), (N/A, 23), (0 ms), DFS
        (57240 - 66000 @ 2160), (N/A, 10), (N/A)

phy#0
country US: DFS-FCC
        (902 - 904 @ 2), (N/A, 30), (N/A)
        (904 - 920 @ 16), (N/A, 30), (N/A)
        (920 - 928 @ 8), (N/A, 30), (N/A)
        (2400 - 2472 @ 40), (N/A, 30), (N/A)
        (5150 - 5250 @ 80), (N/A, 23), (N/A), AUTO-BW
        (5250 - 5350 @ 80), (N/A, 24), (0 ms), DFS, AUTO-BW
        (5470 - 5730 @ 160), (N/A, 24), (0 ms), DFS
        (5730 - 5850 @ 80), (N/A, 30), (N/A), AUTO-BW
        (5850 - 5895 @ 40), (N/A, 27), (N/A), NO-OUTDOOR, AUTO-BW, PASSIVE-SCAN
        (57240 - 71000 @ 2160), (N/A, 40), (N/A)

一方でiw phyの出力は以下のようになり、利用可能なチャンネルが現れた。

                Frequencies:
                        * 5180 MHz [36] (23.0 dBm)
                        * 5200 MHz [40] (23.0 dBm)
                        * 5220 MHz [44] (23.0 dBm)
                        * 5240 MHz [48] (23.0 dBm)
                        * 5260 MHz [52] (24.0 dBm) (no IR, radar detection)
                        * 5280 MHz [56] (24.0 dBm) (no IR, radar detection)
                        * 5300 MHz [60] (24.0 dBm) (no IR, radar detection)
                        * 5320 MHz [64] (24.0 dBm) (no IR, radar detection)
                        * 5500 MHz [100] (24.0 dBm) (no IR, radar detection)
                        * 5520 MHz [104] (24.0 dBm) (no IR, radar detection)
                        * 5540 MHz [108] (24.0 dBm) (no IR, radar detection)
                        * 5560 MHz [112] (24.0 dBm) (no IR, radar detection)
                        * 5580 MHz [116] (24.0 dBm) (no IR, radar detection)
                        * 5600 MHz [120] (24.0 dBm) (no IR, radar detection)
                        * 5620 MHz [124] (24.0 dBm) (no IR, radar detection)
                        * 5640 MHz [128] (24.0 dBm) (no IR, radar detection)
                        * 5660 MHz [132] (24.0 dBm) (no IR, radar detection)
                        * 5680 MHz [136] (24.0 dBm) (no IR, radar detection)
                        * 5700 MHz [140] (24.0 dBm) (no IR, radar detection)
                        * 5720 MHz [144] (24.0 dBm) (radar detection)
                        * 5745 MHz [149] (30.0 dBm)
                        * 5765 MHz [153] (30.0 dBm)
                        * 5785 MHz [157] (30.0 dBm)
                        * 5805 MHz [161] (30.0 dBm)
                        * 5825 MHz [165] (30.0 dBm)
                        * 5845 MHz [169] (27.0 dBm) (no IR)
                        * 5865 MHz [173] (27.0 dBm) (no IR)

このときにhostapdを使って設定してみたが、36から48チャンネルは正しく使えてたものの、 52以降はhostapdがDFSを利用を試みていたが使用できなくてプロセスが終了してしまった。 (144以降は日本では規制されているので試していない)

ここまでで、cfg80211でregulatory domainが正しく設定できないことと、DFSが利用できないことがわかった。

色々検索したところ、ドライバーにno IRを外すパッチを適用するというハックが見つかるが、 規制回避になるためよろしくない。 ただ調べる中で、ath10kでregulatory domainを扱う、drivers/net/wireless/ath/regd.cでregulatory doaminをユーザーが変更可能とするカーネルオプションがあることがわかった。

  • CONFIG_ATH_REG_DYNAMIC_USER_REG_HINTS このオプションが設定されていない場合、ユーザーによるregulatory domainの設定を拒否する
  • CONFIG_ATH_REG_DYNAMIC_USER_CERT_TESTING このオプションが設定されていない場合、USやJPへの設定を拒否する

これらのオプションを有効にしてathモジュールをビルドして、regulatory domainをJPに設定してみる。

iw reg getの結果は以下のとおりとなり、正しくJPが設定されている。

iw reg get
global
country JP: DFS-JP
        (2402 - 2482 @ 40), (N/A, 20), (N/A)
        (2474 - 2494 @ 20), (N/A, 20), (N/A), NO-OFDM
        (4910 - 4990 @ 40), (N/A, 23), (N/A)
        (5170 - 5250 @ 80), (N/A, 20), (N/A), AUTO-BW
        (5250 - 5330 @ 80), (N/A, 20), (0 ms), DFS, AUTO-BW
        (5490 - 5710 @ 160), (N/A, 23), (0 ms), DFS
        (57000 - 66000 @ 2160), (N/A, 10), (N/A)

phy#0
country JP: DFS-JP
        (2402 - 2482 @ 40), (N/A, 20), (N/A)
        (2474 - 2494 @ 20), (N/A, 20), (N/A), NO-OFDM
        (4910 - 4990 @ 40), (N/A, 23), (N/A)
        (5170 - 5250 @ 80), (N/A, 20), (N/A), AUTO-BW
        (5250 - 5330 @ 80), (N/A, 20), (0 ms), DFS, AUTO-BW
        (5490 - 5710 @ 160), (N/A, 23), (0 ms), DFS
        (57000 - 66000 @ 2160), (N/A, 10), (N/A)

一方でiw phyの結果は、100チャンネル以降が無効化されてしまった。

                Frequencies:
                        * 5180 MHz [36] (20.0 dBm)
                        * 5200 MHz [40] (20.0 dBm)
                        * 5220 MHz [44] (20.0 dBm)
                        * 5240 MHz [48] (20.0 dBm)
                        * 5260 MHz [52] (20.0 dBm) (no IR, radar detection)
                        * 5280 MHz [56] (20.0 dBm) (no IR, radar detection)
                        * 5300 MHz [60] (20.0 dBm) (no IR, radar detection)
                        * 5320 MHz [64] (20.0 dBm) (no IR, radar detection)
                        * 5500 MHz [100] (disabled)
                        * 5520 MHz [104] (disabled)
                        * 5540 MHz [108] (disabled)
                        * 5560 MHz [112] (disabled)
                        * 5580 MHz [116] (disabled)
                        * 5600 MHz [120] (disabled)
                        * 5620 MHz [124] (disabled)
                        * 5640 MHz [128] (disabled)
                        * 5660 MHz [132] (disabled)
                        * 5680 MHz [136] (disabled)
                        * 5700 MHz [140] (disabled)
                        * 5720 MHz [144] (disabled)
                        * 5745 MHz [149] (disabled)
                        * 5765 MHz [153] (disabled)
                        * 5785 MHz [157] (disabled)
                        * 5805 MHz [161] (disabled)
                        * 5825 MHz [165] (disabled)
                        * 5845 MHz [169] (disabled)
                        * 5865 MHz [173] (disabled)

ここで、cfg80211モジュールのデバッグ出力を以下のように有効化する。

echo 'file net/wireless/reg.c +p' > /sys/kernel/debug/dynamic_debug/control

カーネルのログには以下のようなメッセージが出る。

ath: EEPROM regdomain: 0x0
ath: EEPROM indicates default country code should be used
ath: doing EEPROM country->regdmn map search
ath: country maps to regdmn code: 0x3a
ath: Country alpha2 being used: US
ath: Regpair used: 0x3a
cfg80211: Disabling freq 2467.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 2472.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 2484.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5500.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5520.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5540.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5560.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5580.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5600.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5620.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5640.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5660.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5680.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5700.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5720.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5865.000 MHz as custom regd has no rule that fits it
cfg80211: Disabling freq 5720.000 MHz
cfg80211: Disabling freq 5745.000 MHz
cfg80211: Disabling freq 5765.000 MHz
cfg80211: Disabling freq 5785.000 MHz
cfg80211: Disabling freq 5805.000 MHz
cfg80211: Disabling freq 5825.000 MHz
cfg80211: Disabling freq 5845.000 MHz
cfg80211: Disabling freq 5865.000 MHz
ath: EEPROM regdomain: 0x8188
ath: EEPROM indicates we should expect a country code
ath: doing EEPROM country->regdmn map search
ath: country maps to regdmn code: 0x40
ath: Country alpha2 being used: JP
ath: Regpair used: 0x40
ath: regdomain 0x8188 dynamically updated by user

このメッセージを手がかりにソースをコードを読むと次のようなことがわかった。 はじめのathモジュールのメッセージは、ドライバー自体がEEPROMをもとにregulatory domainを設定している。 EEPROMには0が書き込まれているらしく、フォールバックとしてUSを国コードとして使うらしい。 また、regulatory domainの情報自体をドライバーが保持しているが、wireless-regdbを参照せず、 最も厳しい規制を適用するようだ。 Disabling freq 2467.000 MHz as custom regd has no rule that fits itのようなメッセージが、 ドライバーによって無効化されたチャンネルらしい。 また残りのDisabling freq 5720.000 MHzのようなメッセージは、cfg80211で指定したJPによる規制のようだ。

https://github.com/torvalds/linux/blob/v5.18/drivers/net/wireless/ath/regd.c#L635-L666 ドライバーの規制情報は、REGULATORY_STRICT_REGとして設定されており、cfg80211で設定する規制で許可されていても、ドライバーが許可していない場合は無効化されてしまう。

ここについては他の回避手段がないので、より弱い規制を使うようにパッチすることにした。 このパッチ各国規制より弱くなる可能性があるので、必ずwireless-regdbを使って規制する必要がある。

diff --git a/drivers/net/wireless/ath/regd.c b/drivers/net/wireless/ath/regd.c
index f15e7bd690b5..5873fec9ac14 100644
--- a/drivers/net/wireless/ath/regd.c
+++ b/drivers/net/wireless/ath/regd.c
@@ -215,8 +215,7 @@ EXPORT_SYMBOL(ath_is_world_regd);

 static const struct ieee80211_regdomain *ath_default_world_regdomain(void)
 {
-       /* this is the most restrictive */
-       return &ath_world_regdom_64;
+       return &ath_world_regdom_60_61_62;
 }

 static const struct

ここまでで、regulatory domainの設定が正しくできるようになった。 ここで、hostapdを起動してみるが、やはりDFSが失敗してしまう。

コードを読んでいると、CONFIG_ATH10K_DFS_CERTIFIEDというカーネルオプションがあるのでこれを有効にする。

以下のような設定をhostapdに行ったところ、116チャンネルを使って通信ができるようになった。 (パラメータが多くて最小化できていないので、不必要なオプションがあるかもしれない)

country_code=JP
ieee80211d=1
ieee80211h=1
hw_mode=a
channel=0
chanlist=36-140
ieee80211n=1
ht_capab=[HT40+][SHIORT-GI-20][SHORT-GI-40]
ieee80211ac=1
vht_capab=[SHORT-GI-80]
vht_oper_chwidth=1
vht_oper_centr_freq_seg0_idx=42

まとめ

ここまで試行したとおり、LinuxWiFi APを立てるのは、法規制、カーネルオプション、ドライバーなどが複雑に関連して難しい。 Arch Linuxでath10kを使う場合少なくとも以下のことを行う必要がある。

  • wireless-regdbパッケージをインストールする
  • カーネルオプションを有効化する
    • CONFIG_ATH_REG_DYNAMIC_USER_REG_HINTS
    • CONFIG_ATH_REG_DYNAMIC_USER_CERT_TESTING
    • CONFIG_ATH10K_DFS_CERTIFIED
  • ドライバーにパッチを当てる
  • hostapdを適切に設定する

おまけ

デフォルト状態でregulatory domainにJPを設定した場合、利用チャンネルが増えたのはなぜか。 cfg80211モジュールは、ドライバーが指定した規制情報とユーザーが指定した規制情報が食い違ったときに、 両者を合体させたような規制情報を作成する。 このときに、98という特別な国コードを利用する。 この食い違いの判定は国コードによって行われるため、USを指定した場合は、ドライバーフォールバックであるUSと食い違わなかったため、ドライバーが指定する規制情報がそのまま利用されていた。 また、00を指定したときは、ユーザー指定がないものとして扱われるので、このときもドライバーの規制情報が利用される。

Archのデフォルトでregulatory domainをJPに設定した場合、利用チャンネルは増えるのだが、 DFSが利用できないので、11acで利用できるフルのチャンネルは利用できない。 また仮に、DFSが不要な36から48チャンネルであっても、電波出力が規制に合致しないので利用すべきではないだろう。

Pin/Unpinについて

Rust の async 周りでは Pin/Unpin というものが使われている。 ユーザーレベルでは、直接使うことは少ないが、ライブラリ使用時にトレイト境界を書くときとか、自前でコンビネータを書こうとすると理解する必要がある。 ということで自分が理解した範囲の Pin/Unpin についてを残しておく。

async 関数について

Rust の async 関数や async block はコンパイラーマジックによって Future traitを実装したステートマシンに変換される。 Future trait は、poll メソッドを実装する必要がある。 このメソッドの self は Pin に包まれており、async は暗黙的に Pin を使用していることがわかる。

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;

まず Pin を考える前に、async がどういう変換が行われるか考えよう。

次の async 関数を考える。

async fn foo() -> () {
  let x = 1;
  let p = &x;
  bar().await
}

これは例えば次のような実装に変換される。 (イメージなのでコンパイルできないが)

struct Foo<F: Future<Output = ()>> {
    init: bool,
    x: u32,
    p: *const u32,
    bar: F,
}

impl Future for Foo {
    type Output = ();
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if !self.init {
            self.as_mut().x = 1;
            self.as_mut().p = &self.as_mut().x;
            self.as_mut().bar = bar();
            self.as_mut().init = true;
        }
        if let Poll::Ready(_) = self.as_mut().poll(cx) {
            return Poll::Ready(());
        } else {
            return Poll::Pending;
        }
    }
}

要するに bar 関数作った Future の poll メソッドが Poll::Ready を返すまでなんども Foo::poll が呼び出されるようなステートマシンに変換される。 この構造だと、async 関数では単なるローカル変数への代入に見えるものが、実際には構造体のフィールドへの代入にする必要がある。 値の場合は単に代入で済むが、ポインタの場合だと構造体へのフィールドへのポインタを取る必要がある。 つまり async により生成されるステートマシンは一般に自己参照構造体を生成することになる。

rust ではこれまで自己参照構造体をうまく扱う方法がなかった。 rust はすべての値を move できるものとして扱う。 自己参照構造体を move してしまうと、内部に保持しているポインタの参照先は変更されないので、 容易にダングリングポインタが作られてしまう。 safe rust ではこのようなものは許容されないので、async を safe な範囲で使えなくなってしまう。 これが原因で async はなかなか導入が進まなかったらしい。 逆に言えば、move できない型を作ることができれば自己参照構造体を扱えるはずだ。 このための型が Pin である。

Pin/Unpin

Pin は自己参照構造体を安全に保持するためのポインタのようなものだ。 ただし、自己参照構造体を指している場合に、Deref traitやポインタを取得するメソッドをは提供できない。 もしできるなら move ができてしまう。

let p = Pin::new(&mut self_referential);
let moved = mem::replace(p.deref_mut(), ...); // OMG

rust は trait を使って選択的に実装を行うことができるので、この機能を使う。 以下の記述では、P をデリファレンスした結果が Unpin であるときにだけ DerefMut が実装されるという意味になる。

https://doc.rust-lang.org/std/pin/struct.Pin.html#impl-DerefMut

impl<P> DerefMut for Pin<P> where
    P: DerefMut,
    <P as Deref>::Target: Unpin

Unpin は Pin に包まれていても move 可能であるという意味を示すトレイトだ。 具体的にいえば自己参照構造体ではないものを示す。 https://doc.rust-lang.org/std/marker/trait.Unpin.html

このトレイトは auto trait として表現されており、基本的にすべての型に対して実装される。 例外は、PhantomPinnedを含む型に対しては実装されない。 また async 関数やブロックで生成されるデータ構造にも実装されていない。コンパイラの実装としては PhantomPinned を使っているかもしれない。 自分で自己参照構造体を作ったときは PhantomPinned をつけておくことで、安全に Pin に入れることができるようになる。 逆にこれを忘れると、Pin に入っているにも関わらず DerefMut が実装されてしまい、move 可能になるので十分に注意する。

さて、ここで振り返ると、そもそも Pin に DerefMut なんか実装しなければ良いのではないかと思える。 それはある意味では正しいのだが、その場合 Future::poll では self が Pin を要求するので、 自分自身を書き換えるという普通のコード自体が safe rust ではかけなくなってしまう。 なのでこのあたりの仕組みは、unsafe を使う範囲を小さくするための設計なのだろう。

Upin の使いどころ

最初にユーザーレベルではあまり使うことはないと書いたが、例えば Future を受け取る関数を書く場合には、Unpin が必要になる。 Future トレイトと Unpin トレイトは直行しているので、関数の引数に Future を受け取る場合、自己参照構造体であるにも関わらず引数に渡すとそのタイミングで move されてしまう。 Future を扱うライブラリではそのようなが起こらないように、Unpin を要求していることがあるので、Unpin をつけておかないとメソッドがないと言われて怒られてしまうことがある。

例えば、future-rs が提供する FutureExt::map が生成する Mapでは poll メソッドで Unpin が要求されている。