Debian 12 (bookworm) ベースのRaspberry Pi OSでUSB-OTGする方法

## 略解

USB-OTG以外の方法で端末へのアクセスが可能な状況を想定．

1. `nmcli dev`で出てくるデバイス一覧をメモっておく
1. (大既出)`/boot/config.txt`と`/boot/cmdline.txt`に **追 記** (`sudo`)
    `/boot/config.txt` :

    ```script
    dtoverlay=dwc2
    ```

    `/boot/cmdline.txt` :

    ```script
    ...rootwait modules-load=dwc2,g_ether ...
    ```

1. 再起動
1. `nmcli dev`でusbとおぼしきデバイスが追加されているのを確認する(典型的には`usb0`、以降`<usb_dev>`と記述)
1. `sudo nmtui` > `接続の編集` > `<追加>` > `Ethernet`で新しいEthernet接続を作成

    - `プロファイル名` : 適当に
    - `デバイス` : `<usb_dev>`
    - `Ethernet` : いじらない
    - `802.1X Security` : いじらない
    - `IPv4 設定` : `<ローカルにリンク>`
    - `IPv6 設定` : `<無効>`
    - `自動的に接続する` : チェックをつける(デフォルト)
    - `全ユーザに使用可能` : お好みで
    で`<OK>`
1. `sudo nmcli con mod USB-OTG connection.autoconnect-priority 0`を実行
1. `udevadm info /sys/class/net/usb0`で`ID_PATH`の値を確認(典型的には`platform-fe980000.usb`、以降`<IDパス>`と記述)
1. `/etc/udev/rules.d/85-usb-otg.rules`を作成(`sudo`)

    ```sh
    SUBSYSTEM=="net", ACTION=="add|change|move", ENV{DEVTYPE}=="gadget",ENV{ID_PATH}=="<IDパス>", ENV{NM_UNMANAGED}=""
    ```

1. シャットダウン
1. USB-OTGする母機に電源を接続
1. `ip addr show usb0`でIPv4アドレス`169.254.~.~`が振られていることを確認
1. 母機と端末で`netcat`や`ssh`などでトランスポート層の通信ができることを確認

成功したら成功

## 動機

Raspberry Pi 4B(raspi)をサーバ用途に使っていて、かつ有線環境がない場合、必然的にWi-Fi経由の`ssh`でアクセスすることになる。これではWi-Fi環境が変化する(例: 転居・帰省)と直ちに世界と断絶した文鎮と化し、緑ランプが付かないことを祈りながら電源ケーブルを抜くという危険な"`shutdown`"を行わざるを得ない[^rp5]。ここから`Wi-Fi`を回復するには別のPCにmicro SDを刺し、ext4領域をマウントして設定をいじるというアクロバティックな操作が必要になる。

[^rp5]: Raspberry Pi 5で漸く電源ボタンが付き、SD破壊の危険からは逃れられたが、Wi-Fiに自動で繋がってくれたりはしない。

実はRaspberry Pi 4BにはUSB Ether Gadgetという、電源ケーブルを通信に兼ねることができ、適当なPCにこれをぶっ刺すとそれがEthernetとして振る舞うという機能がある。世間では専らUSB On-The-Go(USB-OTG)の名で通っているのだが、ともかくこの通信路をraspiとPCの双方でいい感じに認識してもらえれば、ここから`ssh`し放題というわけで、無事上述の問題は解決できる。

USB-OTGの導入方法はあちこちに転がっている(略解の2の部分)。しかし今回Raspberry Pi OSの基盤とするDebianのバージョンが12 (bookworm)に上がるのに伴って、ネットワーク管理が`dhcpcd`から`NetworkManager`に移管されて、既出の方法では正しく認識してもらえなくなった。

今回記述するのは上述のOSアップデートに伴うUSB-OTG設定についての、専ら正攻法による解決策である。略解は上で述べた通り。以下に詳解(機序)を述べる。

## 詳解

### ブートオプションの編集

- 略解の該当箇所 : 2

この部分は`Raspberry Pi USB-OTG`とかで調べればいくらでも湧いてくるが、一応注意を述べておくと、**絶対に呪文を間違えないこと**。ブートオプションでやらかすということはOSの立ち上げでしくるということなので、ほぼ確実に起動が途中で停止してしまう。それ則ち緑ランプお祈り電源ぶっこ抜きなので、なるべく避けたい[^ev]。複数の記事をチェックして整合性をとった上で、タイポがないか確認するべき。特に、**無闇な空白の挿入は引数の意図しない解釈に繋がり危険**なので、自分の判断で書き換えてはいけない。また`/boot/cmdline.txt`の追記についてはちょこちょこ明示しない記事が散見されるが、**すでに入っている引数のうち`rootwait`の直後に、`modules-load=dwc2,g_ether`を両側空白つきで挿入する**という操作である。

[^ev]:SDが傷んでもあまり気にしない、所詮使い捨てのOSだというのなら気にせずバンバンやらかすといい。実際そういう壊していい台があった方が検証とかし易くていいよね。

### `NetworkManager`の設定

- 略解の該当箇所 : 1, 4, 5, 6

上記の操作の後再起動すると、`ip a`とか`nmcli`とかで見えるインタフェース/デバイスにUSB系統のものが増えているはずである(典型的には`usb0`の名が与えられる)。かつて`dhcpcd`の頃には自動でIPを割り当てて起動までしてくれたのだが、`NetworkManager`では自分で設定をいじる必要がある。とはいっても、このレイヤではただのEthernetと代わりがなく、設定もEthernetの設定と大差ない。せいぜい迷うのはDHCPサーバがいないのをどう補うかぐらいである。

`nmcli`でうだうだやってもいいが、面倒なので`nmtui`を使った方が話が早いだろう。設定の書き換えには管理者権限が必要なので`sudo nmtui`とやるとGUI擬きが起動される。ここでの操作はざっくり十字キーで移動、スペースキーで選択/決定である。あとは書いてある通りに`接続の編集` > `<追加>` > `Ethernet`と選んでいって設定詳細の画面に入る。

`プロファイル名`、`デバイス`、`Ethernet`、`802.1X Security`、`自動的に接続する`、
`全ユーザに使用可能`は自明なので省略しよう。問題は`IPv4 設定`と`IPv6 設定`である。正直ここについてはベストプラクティスがまだ見通せていないのだが、Ethernet(仮想的な)で2端末を繋いでいて、しかもどちらもハブではないという状況を考えると、リンクローカルアドレス、各々がめいめいにランダムなアドレスを宣言するのが良いと考えた。DHCPがいないのだから動的アドレスは無理だし、共有アドレスはむしろraspiのネットワークアクセスを母機に共有する形になるから不本意である。他に芽があるとしたら静的アドレス設定だが、あまり理解していないので放棄(知見のある方は編集リクエストからどうぞ)。一旦`<ローカルにリンク>`を選択することにした[^mdns]。またIPv6アドレスがあっても大して嬉しくないので`<無効>`とした。

[^mdns]: このままだとraspiのIPがわからず接続ができないと思われるが、`mdns`が効いていれば`<ホスト名>.local`で接続できる。

略解では省略したが、`IPv4 設定`と`IPv6 設定`の`<表示する>`詳細設定を開き、`このネットワークはデフォルトのルートには使用しない`にチェックを入れておくと良いだろう。リンクローカルアドレスを選んだ以上デフォルトゲートウェイは生えてこないが、今後設定を変えたときに通信が吸われたりしたら困るので、安全弁としていじっておく。

これでほとんど設定は終わりだが、`nmtui`からは弄れない部分が一箇所あり、それは`connection.autoconnect-priority`なる、自動接続の優先度を決める数値である。理由は知らないがEthernetの初期設定だとここに`-999`が入っており(これは`nmcli con show <プロファイル名>`で確認できる)、このままだと`connection.autoconnect`が有効(`はい`/`Yes`)になっていても起動時の自動接続が発生しない。`sudo nmcli con mod <プロファイル名> connection.autoconnect-priority 0`としてやると修正できる。ただこれではWi-Fiの優先度の初期値0とダブってしまい、実際に確認したことはないがWi-Fiが起動されないとかの不具合を起こしそうなので、同様のコマンドでWi-Fiの優先度を上げておくといいだろう。

### `udev`の設定

- 略解の該当箇所 : 7, 8

上記の操作後、`nmcli con up/down <接続名>`でUSB-OTGを立てたり止めたりできるようになり，その上で起動時の自動接続を行うよう設定したが、実はこの段階で再起動しても自動接続は全く行われない。試みられることもない。

この状況で`journalctl -u NetworkManager -b`として`NetworkManager`のログを見ると、`usb0`は

```log
manager: (usb0): new Ethernet device (/org/freedesktop/NetworkManager/Devices/3)
```

みたいな行が1行あるきりで、以降全く触られていないという結果が出るはずである。つまり`NetworkManager`は`usb0`を認識はしているが、何らかの力が働いてその管理を放棄している。

ここで仮定するのは、**`NetworkManager`を制止しているのはデバイス管理のサブシステムだろう**ということだ。というのも`usb0`がそこから生えてくるからである[^udev]。Raspberry Pi OSではデバイスの管理は`udev`が担当しているので、その周りを漁ってみると`/usr/lib/udev/rules.d/`なるフォルダがあり、ここを覗いてみると、

[^udev]: 実際には`journalctl`を見つめたり`udevadm trigger --dry-run`してみたりといった試行錯誤が埋もれている。

```bash
$ ls /usr/lib/udev/rules.d/
10-local-rpi.rules        60-persistent-input.rules         70-touchpad.rules                  77-mm-longcheer-port-types.rules  80-net-setup-link.rules
10-vc.rules               60-persistent-storage-dm.rules    70-uaccess.rules                   77-mm-mtk-port-types.rules        80-noobs.rules
15-i2c-modprobe.rules     60-persistent-storage-tape.rules  71-seat.rules                      77-mm-nokia-port-types.rules      80-udisks2.rules
40-usb_modeswitch.rules   60-persistent-storage.rules       73-seat-late.rules                 77-mm-qcom-soc.rules              81-net-dhcp.rules
50-firmware.rules         60-persistent-v4l.rules           73-special-net-names.rules         77-mm-qdl-device-blacklist.rules  84-nm-drivers.rules
50-udev-default.rules     60-rpi.gpio-common.rules          75-net-description.rules           77-mm-quectel-port-types.rules    85-hwclock.rules
55-dm.rules               60-sensor.rules                   75-probe_mtd.rules                 77-mm-sierra.rules                85-nm-unmanaged.rules
60-autosuspend.rules      60-serial.rules                   77-mm-broadmobi-port-types.rules   77-mm-simtech-port-types.rules    90-alsa-restore.rules
60-block.rules            60-tpm-udev.rules                 77-mm-cinterion-port-types.rules   77-mm-telit-port-types.rules      90-console-setup.rules
60-cdrom_id.rules         60-triggerhappy.rules             77-mm-dell-port-types.rules        77-mm-tplink-port-types.rules     90-nm-thunderbolt.rules
60-dma-heap.rules         64-btrfs.rules                    77-mm-dlink-port-types.rules       77-mm-ublox-port-types.rules      90-pi-bluetooth.rules
60-drm.rules              69-libmtp.rules                   77-mm-ericsson-mbm.rules           77-mm-x22x-port-types.rules       92-libccid.rules
60-evdev.rules            70-camera.rules                   77-mm-fibocom-port-types.rules     77-mm-zte-port-types.rules        95-dm-notify.rules
60-fido-id.rules          70-joystick.rules                 77-mm-foxconn-port-types.rules     78-sound-card.rules               96-e2scrub.rules
60-flashrom.rules         70-memory.rules                   77-mm-gosuncn-port-types.rules     80-debian-compat.rules            97-hid2hci.rules
60-infiniband.rules       70-microbit.rules                 77-mm-haier-port-types.rules       80-drivers.rules                  99-nfs.rules
60-input-id.rules         70-mouse.rules                    77-mm-huawei-net-port-types.rules  80-ifupdown.rules                 99-systemd.rules
60-persistent-alsa.rules  70-power-switch.rules             77-mm-linktop-port-types.rules     80-mm-candidate.rules
```

ここを睨むと、`85-nm-unmanaged.rules`なるいかにも関係がありそうなファイルがある(というのも、`nm`は`nmcli`や`nmtui`のように、`NetworkManager`の略語として頻出である。)。その中身は以下の通り。

```sh
# Do not modify this file, it will get overwritten on updates.
# To override or extend the rules place a file in /etc/udev/rules.d

SUBSYSTEM!="net", GOTO="nm_unmanaged_end"
ACTION!="add|change|move", GOTO="nm_unmanaged_end"

# VirtualBox host networking. Out-of-tree driver that looks like an ordinary
# Ethernet. No parent device (lives in /virtual/), no support for ethtool
# to identify the driver, MAC address defaults to 08:00:27:, but can be
# changed. Interface name will have to do, it's always vboxnet*.
ENV{INTERFACE}=="vboxnet[0-9]*", ENV{NM_UNMANAGED}="1"

# VMWare host networking. Out-of-tree driver that looks like an ordinary
# Ethernet. No parent device (lives in /virtual/), no support for
# ethtool to identify the driver. They have their own MAC prefix that
# can not be changed.
ATTR{address}=="00:50:56:*", ENV{INTERFACE}=="vmnet[0-9]*", ENV{NM_UNMANAGED}="1"

# Parallels Workstation host networking. Out-of-tree driver that looks like
# an ordinary Ethernet. No parent device (lives in /virtual/),  no support for
# ethtool to identify the driver and the interface name is too generic.
# However, they have their own MAC prefix that can not be changed.
ATTR{address}=="00:1c:42:*", ENV{INTERFACE}=="vnic[0-9]*", ENV{NM_UNMANAGED}="1"

# Virtual Ethernet device pair. Often used to communicate with a peer interface
# in another net namespace and managed by libvirt, Docker or the like.
# Generally we don't want to mess with those. One exception would be the
# full system containers, like LXC or LXD. LXC containers run via libvirt
# don't use udev, so this doesn't apply. LXD does, though. To deal with the
# LXD situation, let's treat the devices called eth* as regular ethernet.
ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}!="eth[0-9]*", ENV{NM_UNMANAGED}="1"

# USB gadget device. Unmanage by default, since whatever created it
# might want to set it up itself (e.g. activate an ipv4.method=shared
# connection).
ENV{DEVTYPE}=="gadget", ENV{NM_UNMANAGED}="1"

LABEL="nm_unmanaged_end"
```

……`ENV{DEVTYPE}=="gadget", ENV{NM_UNMANAGED}="1"`なる行がある。`udev`ルールを参照すると、これは環境変数`DEVTYPE`が`"gadget"`であったときに、環境変数`NM_UNMANAGED`を1に設定するという意味で、`NM_UNMANAGED`とはその名の通り`NetworkManager`が該当デバイスの管理を放棄するフラグになっている。今設定しているのはUSB Ether **Gadget**だから、当然このフィルターに引っかかるわけだ。

というわけでこのフラグをへし折ってやればめでたく自動接続が成るわけで、このファイルを書き換えても達成できるのだが、このファイルは`udev`パッケージが管理しているのでそれはよろしくない。最上部にも

```sh
# Do not modify this file, it will get overwritten on updates.
# To override or extend the rules place a file in /etc/udev/rules.d
```

とご丁寧に書いてくれているので、`/etc/udev/rules.d/`下に適当にファイルを作って上書きすれば良いが、ここで注意が必要なのは`udev`ルールファイルはどのフォルダにいるかに関係なく、辞書順に評価されるという点である。迂闊に`10-usb-otg.rules`としたり、`85-ether-gadget.rules`としたりすると、これらは辞書順で`85-nm-unmanaged.rules`より前に来るから、せっかくフラグを折った後に立て直されるという虚無が発生する．かといって他のルールとの干渉も避けたいので、略解では`85-usb-otg.rules`としたわけである。

さてルールファイルの中身だが、

```sh
SUBSYSTEM=="net", ACTION=="add|change|move", ENV{DEVTYPE}=="gadget",ENV{ID_PATH}=="<IDパス>", ENV{NM_UNMANAGED}=""
```

これは`85-nm-unmanaged.rules`で`NM_UNMANAGED`に`1`が入る条件をまず持ってきておいて、`DEVTYPE`が`"gadget"`かつ`ID_PATH`が`<ID_PATH>`である時だけ`NM_UNMANAGED`を空にしておくという挙動をする。これで`<ID_PATH>`に対応するGadget機器は`NetworkManager`の管理下に入るようになる。

それでは`usb0`に対応する`<ID_PATH>`をどう調べるかというと、`udevadm info <デバイスパス>`で対応するデバイスの環境変数を調べられるということ、ネットワークインターフェースが`/sys/class/net/`下にあることを使えば、`udevadm info /sys/class/net/<インターフェース名>`とすれば求まる[^env]。これを使えば`NetworkManager`の制止が解かれ、無事自動接続に成功するはずである。

[^env]: 無論、`ID_PATH`以外の環境変数でデバイスを一意に取ってきても良いが、私の環境では他に使えそうな変数はなかった。

## 終わりに

`systemd`に連座するサブシステムは、大体適切な設計のもとに適切な拡張が可能な形になっており、扱いやすい。場当たり的に凌ごうとする(今回であれば、起動時に`usb0`をアクティベートするサービスを入れる、とか)前に、設定集を当たることをオススメする。