令和の時代にネットワークデバイス名をeth*にする

命名をeth\*にすることで発生する問題により包括的に対処する方法を見つけました。この記事は原因の経緯や前提知識として必要なため残しておきます。

https://qiita.com/liqsuq/items/c8f3fdd45e772d175a26

----

# はじめに

(組み込み除く)Linuxでのネットワークデバイスの命名がeth\*の様な認識順に命名する方法から、eno\*やenp\*s\*の様な**予測可能な名前**がおおむね標準になってしばらく経ちました。しかし「ネットワークデバイスは従来通りeth\*で命名しろ！」「eth\*で命名すると命名が予測不可能になる！なんとかしろ！」というケースは存在します。

「NIC eth 変更」なんかで検索するとおよそ以下の2点の方法が出てくるかと思います。

- カーネルパラメータに`biosdevname=0 net.ifnames=0`を設定する
- /etc/udev/rules.d/70-persistent-net.rulesでMACアドレスで固定する様に設定する

ですが、これらの方法は命名を完全に固定するという用途では十分でなく、上手くいかない場合が出てきます。本記事ではその原因について少し深堀りしていきます。

udevの挙動とネットワークデバイス命名の歴史については以下が詳しいです。

http://blog.keshi.org/hogememo/2014/12/28/debian-vs-ubuntu-network-interface-names

# 原因1：udevルールの優先順位
ディストリビューションやバージョンによる差異や特殊なデバイスの場合の例外はありますが、udevでのネットワークデバイス命名ルールは概ね以下の通りになります。

|ルール                        |概要|
|:----------------------------|:---|
|(lib)/60-net.rules           |ifcfg-*にHWADDR行がある場合はDEVICE行に命名変更する|
|(etc)/70-persistent-net.rules|75-persistent-net-generator.rulesから生成され固定の命名を提供する|
|(lib)/71-biosdevname.rules   |biosdevname=1の場合に独自の命名を行う|
|(lib)/80-net-setup-link.rules|net.ifnames=0でない場合に[systemd.link(5)](https://manpages.ubuntu.com/manpages/xenial/man5/systemd.link.5.html)による命名を行う(eno\*やenp\*s\*といった命名を含む)|

勘違いされがちですがudevは**辞書順(若い番号から)で適合するルールが優先**です。なので70-persistent-net.rulesはRHEL系だと既存のルールを上書きする上にifcfg-*に書いたHWADDR設定の方が優先されます。

今回の様にユーザ側で完全に命名を固定したい場合、下手に他のudevルールと調停を図るより、特定のデバイスだけ既存のルールよりも優先されるルールファイルを作成する方が賢明です。例えば以下の様に作成します。

```txt: /etc/udev/rules.d/50-net-fixed.rules
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="aa:aa:aa:aa:aa:aa" , NAME="eth0"
:
```

# 原因2：initramfsが更新されていない
大前提としてネットワークデバイスの命名変更は既に存在する命名を使用することは出来ず、互いを入れ替える様な変更はできません(例：eth0↔eth1)。変更すると以下の様なログを吐きます。

```txt
... systemd-udevd: error changing net interface name eth0 to eth1:File exists
... systemd-udevd: error changing net interface name eth1 to eth0:File exists
```

:::note
udevでは入れ替える様な変更をする時、変更先のデバイスを一時的な命名に変更する様なアップデートを行う気配がありますが、それでもeth\*はいつカーネルが使うか分からないので一般的に非推奨です。

> Note that specifying a name that the kernel might use for another interface (for example "eth0") is dangerous because the name assignment done by udev will race with the assignment done by the kernel, and only one interface may use the name. Depending on the order of operations, either udev or the kernel will win, making the naming unpredictable. It is best to use some different prefix, for example "internal0"/"external0" or "lan0"/"lan1"/"lan3".

https://www.freedesktop.org/software/systemd/man/systemd.link.html#Name=
:::

そしてudev(及びsystemd)はinitramfsにも組み込まれており、ルートファイルシステムのルールを変更しただけではinitramfsにまで反映されません。更に厄介なことに、PXEやNFSブートのために一部のネットワークデバイスドライバはinitramfsに組み込まれています。特にIntel製のデバイスで顕著です。

:::note
ドライバが組み込まれているかどうかは`lsinitramfs`コマンドを使ってinitramfsファイルを確認してみてください。
:::

ここまでの要因が揃うと、「initramfsでデバイスが認識→udevが変更前のルールで命名→ルートマウント後にudevが変更後のルールで命名」といった具合でudevが2回命名を行います。

冒頭の2点の方法を行ってinitramfsの更新を行わなかった場合、次の様なフローが発生して命名が失敗するケースがあります。

- initramfsでデバイスが認識(eth0)
- `biosdevname=0 net.ifnames=0`でudevが変更前のルールなので命名変更なし
- initramfsでデバイスが認識(eth1)
- `biosdevname=0 net.ifnames=0`でudevが変更前のルールなので命名変更なし
- (eth0, eth1, eth2…とデバイスが並んでいく)
- ルートマウント
- udevが変更後のルールで命名しようとするが既にeth\*は存在するので変更出来ない

これを防ぐためにはinitramfsから該当するドライバを削除するか、作成したudevルールをinitramfsに反映させる必要があります。前者だと(使わないにせよ)ネットワークブートが出来なくなってしまうので後者を選ぶのが妥当でしょう。

# 対処法

原因1及び2への適切な対処法をまとめます。まず先述の通り固定したいネットワークデバイスに対して既存のルールよりも優先されるルールを作成します。

```txt: /etc/udev/rules.d/50-net-fixed.rules
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="aa:aa:aa:aa:aa:aa" , NAME="eth0"
:
```

次に作成したudevルールをinitramfsに反映させます。initramfsはカーネルアップデートの際に自動生成されるので、各ディストリビューションが採用するツールチェーンで設定が反映される様にする必要があります。RHEL系の場合はdracutコマンド、Debian系の場合はinitramfs-toolsパッケージが提供するupdate-initramfsコマンドがデファクトスタンダードです。

## RHEL系の場合
```
$ uname -srm
Linux 4.18.0-305.19.1.el8_4.x86_64 x86_64
$ cat /etc/rocky-release
Rocky Linux release 8.4 (Green Obsidian)
```

/lib/dracut/modules.d/以下のdracutモジュールを見た限り自作のudevルールは自動ではinitramfsに書き込まれない様なのでdracut.confで設定を行います。

```txt: /etc/dracut.conf.d/net-fixed-rules.conf
install_optional_items+="/etc/udev/rules.d/50-net-fixed.rules"
```

以下のコマンドを実行して起動中のカーネルに対するinitramfsを更新します。他にカーネルをインストールしている場合はそれぞれ起動してコマンドを実行するか引数を渡して実行する必要があります。

```
$ sudo dracut -f
```

## Debian系の場合
```
$ uname -srm
Linux 5.15.0-71-generic x86_64
$ lsb_release -sd
Ubuntu 20.04.6 LTS
```

以下のコマンドを実行してインストールされた全てのカーネルに対するinitramfsを更新します。

```
$ sudo update-initramfs -u -k all
```

:::note
initramfs-tools-coreパッケージの/usr/share/initramfs-tools/hooks/udevフックによれば、/etc/…にudevルールを書いても/lib/…の方のルールと同名だとinitramfsに書き込まれない点に注意してください。
:::

優先順位的に固定にしたいデバイスには適用されないため`biosdevname=0 net.ifnames=0`は設定してもしなくても構いません。それらは新規デバイスの命名を制御するために決めてください。

# 原因ex：どうしようもない時もある
「カーネルがデバイスを認識→udevが命名を変更」という処理がデバイス毎に一揃いに進んでくれればありがたいのですが、残念ながら保証されていません。「カーネルがデバイスを認識(eth0)→カーネルがデバイスを認識(eth1)→udevが命名を変更(eth0↔eth1)」の様に処理が進んでしまうことが頻繁にあります。これは割とどうしようもありません。以下の方法を試してみて上手く認識してくれる様に祈るしかありません。

# 対処法ex1：ネットワークデバイスのスロットを変える

読んで字の如くです。デバイスの認識順が変わるのでうまくいく可能性があります。

# 対処法ex2：ドライバのロード順を調整する

ドライバのロード順をユーザ側で制御します。modprobe.dに設定を書き込みます。以下はe1000e→tg3→ixgbe→igbの順にロードしたい場合の例です。

```txt: /etc/modprobe.d/net-load-sequence.conf
softdep e1000e                       post: tg3 ixgbe igb
softdep tg3    pre: e1000e           post:     ixgbe igb
softdep ixgbe  pre: e1000e tg3       post:           igb
softdep igb    pre: e1000e tg3 ixgbe
```

:::note
`lspci -v`を実行すると認識されているPCI(e)デバイスを一覧表示します。該当するデバイスを探して`Kernel modules:`行で使用ドライバを確認してください。
:::

その後initramfsを更新します。/etc/modprobe.d/以下はdracutでもupdate-initramfsでもデフォルトで参照される様です。

記事末尾に書きましたが、ドライバのロードを制御したところでデバイスの認識がその順に行われる訳ではありません。ですので飽くまでうまくいく可能性がある、というだけです。

# 対処法ex3：故意にudev命名を2段階で行う
「initramfsを更新しないとudevが2回命名変更を行う」と書きました。であれば逆に命名を2段階に分けて「initramfsで一時的な命名に変えて、ルートマウント後に本番の命名にすれば…?」というハックです。`tmp=a; a=b; b=tmp;`的な奴です。

まず対処法ex2と同じ様にmodprobe.dで使用予定の全てのドライバに対してsoftdepを設定します。こうすると依存関係が発生するので元々initramfsに無かったドライバも含まれる様になります。

次にinitramfs用に一時的な命名に変更するudevルールを作成します。この時に認識されうる全てのデバイスに対して設定してください。そうでないとeth\*のデバイスが残ってしまいます。

```txt: /etc/udev/rules.d/50-net-fixed.rules
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="aa:aa:aa:aa:aa:aa" , NAME="tmp0"
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="bb:bb:bb:bb:bb:bb" , NAME="tmp1"
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="cc:cc:cc:cc:cc:cc" , NAME="tmp2"
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="dd:dd:dd:dd:dd:dd" , NAME="tmp3"
:
```

各ディストリビューションに対応した方法でinitramfsを更新したらルートファイルシステム用に上記のファイルを書き換えます。

```txt: /etc/udev/rules.d/50-net-fixed.rules
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="aa:aa:aa:aa:aa:aa" , NAME="eth0"
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="bb:bb:bb:bb:bb:bb" , NAME="eth1"
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="cc:cc:cc:cc:cc:cc" , NAME="eth2"
ACTION=="add", SUBSYSTEM=="net", DRIVERS=="?*", ATTR{address}=="dd:dd:dd:dd:dd:dd" , NAME="eth3"
:
```

これ以降は**initramfsの更新は行いません**。このままであればinitramfs上ではtmp\*に命名変更するルールが残り、ルートファイルシステムではeth\*に命名変更するため、eth\*→tmp\*→(ルートマウント)→eth\*といったルートを辿るため、命名の衝突を防ぐことが出来ます。

```
... kernel: ixgbe 0000:01:00.0 tmp3: renamed from eth1
... kernel: ixgbe 0000:01:00.1 tmp4: renamed form eth4
... kernel: tg3 0000:03:00.0 tmp1: renamed from eth2
... kernel: tg3 0000:03:00.1 tmp2: renamed from eth3
    :
... systemd[1]: Starting Remount Root and Kernel File Systems...
    :
... kernel: ixgbe 0000:01:00.0 eth3: renamed from tmp3
... kernel: tg3 0000:03:00.1 eth2: renamed from tmp2
... kernel: tg3 0000:03:00.0 eth1: renamed from tmp1
... kernel: ixgbe 0000:01:00.1 eth4: renamed form tmp4

```

上記では本来であればeth1→eth3と変更し、eth3が既にtg3で使われているので失敗するはずですが、eth1→tmp3→eth3とすることで回避できています。

ただしこの様なエキセントリックな方法は**技術上可能というだけで全く推奨できません**。保守性が無く、カーネルアップデートやデバイスドライバのインストール、もしくは気まぐれにinitramfsを更新しただけで破綻します。

# 結び
命名をeth\*にするだけでこれだけ考えなければなりません。ここまでやっても問題は完全に解決した訳ではありません。恐ろしいことにこの問題が顕在化するのは「デバイスの認識順的にたまたま命名を入れ替える操作をした時」です。認識順は経年劣化や個体差によって変化することがあるので、一度ここまで考えずにうまくいったとして未来永劫うまくいく保証はありません。ひどいと「起動時に20回に1回デバイス名がおかしくなる」みたいなバグとして表出します(経験済)。

これらの問題はカーネルデフォルトの命名であるeth\*を同じ命名で上書きすることに起因しています。別にeno\*やenp\*s\*といった命名に変更しろとまでは言いません。「こっちのカードはenp59でこっちはenp134で…」なんて初見では確かに混乱してしまいますから。しかし良識があれば**eth\*ではなくnet\*とかether\*で命名するだけでこの問題がずっと単純になる**ことに気づくはずです。ユーザによるeth\*への命名は**カーネルが予約している名前空間への侵害行為**です。

どうかこの記事が多くの人の目に届き、「ネットワークデバイスの命名はeth\*とする」みたいな時代錯誤でともすれば有害な仕様が見直されることを祈ります。

# 参考：デバイス認識の実例
以下は検証用に異種のネットワークデバイス(オンボード/Intel X520-DA/Intel I350/Broadcom BCM5720)を挿して起動した場合のドライバのロード順及びデバイスの認識順(eth\*)を示したものです(括弧内はポート番号)。

|ロード順|ドライバ|デバイス|認識順|
|:--:|:----|:---|:--:|
|0|e1000e|オンボード  |2|
|1|tg3   |BCM5721(1)|0|
|1|tg3   |BCM5721(2)|1|
|2|ixgbe |X520-DA(1)|5|
|2|ixgbe |X520-DA(2)|8|
|3|igb   |I350(1)   |3|
|3|igb   |I350(2)   |4|
|3|igb   |I350(3)   |6|
|3|igb   |I350(4)   |7|

e1000eとtg3のケースからドライバのロードが先だからといってデバイスの認識が先だとは限らず、またixgbeとigbのケースからデバイスの認識がオーバーラップしていることが確認出来ます。特にオーバーラップする場合、その挙動は不安定で再起動すると異なる重なり方をします。(だからこそ予測可能な名前にしようって話になる訳ですね)

こうなってしまうと命名の再配置には厳密な順序定義が必要となり、通常のudevルールでは対処不可能ということになります。