systemdに全部賭けろ(第一部)

Linuxでルータを作る(フレッツ光 + v6プラス、下流にホームゲートウェイあり)

ルータの稼働状況
ルータの稼働状況

まえが

systemd森羅万象を統べるようになって千年ほどが経った。

ダイソン球の姿勢制御から、マスドライバーの運行管理まで、あらゆるプロセスがかれの傘下に入った。政治systemd-politicsd)、経済systemd-economyd)、文化systemd-cultured)など、名だたるサブシステムがデーモン置き換えられ、社会構造は大きく改変されていった。ときには抵抗もあったが、systemdがPID 1」と叫べば、ひれ伏さぬものはなかった。

そのように強力であるため、systemdはご家庭のルータになることもできる(ここからは本当)。そこで、この記事ではsystemd-networkd使い、Linuxルータを作る。最終的には、

環境を実現する。


記事の構成について

記事は三部に分かれている。

第一部では、ネットワーク構成とルータの環境構築について扱う。


背景

とはいえ、なぜそもそもこんな手間をかけるのか、背景を補足しておこう。

なぜ自前ルータ?

すでにあちこちで書かれているように、フレッツ光の回線にひかり電話オプション付くと、/56IPv6プレフィックスが払い出される。このとき同時に貸し出されるHGWの挙動は以下のようになっている。

つまり残念なことに、与えられたアドレス空間の1/2561/256ないし1/161/16しか有効に利用されない1

そのほかにも、HGWを上流に置くことの弊害はいろいろある(性能の限界、自由度の低さなど)。そこで代わりに自前のルータを上流に置きたい、という欲が出てくる。

なぜLinux?

ところがひかり電話を使う場合、基本的にHGWを最も上流に置かなければならない。いうのも、HGWがひかり電話ルータとして機能するには、以下の二つが必要だからだ。

この制約に対し、二つほど解決策が知られている。

法1: 選手交代

一つ目の法は、HGWが嫌なら、そもそも自前の市販ルータで置き換えてしまえ、というものだ。これはNTTお墨付き法だが、デメリットもいくつかある。

  1. ひかり電話対応ルータしか使えない
    • NVR500、NVR510、NVR700Wなど
  2. HGW特有の機能(例:SIPサーバ機能)を捨てることになる
  3. そこまで自由度は増えない
  4. 優等生すぎて面白くない

法2: IPv4ブリッジ

もうひとつの法は、IPv4を選択的にブリッジすることだ。これについては下の記事が詳しい。

IX2215直下にHGWを置いてひかり電話をする【2024年夏版】 - Qiita
概要だよ UNIVERGE IX2215直下に繋いだHGWと同一リンクにDHCPv6サーバを置くことでNTTからのDHCPv6パケットを偽装するといいよ 従来の方法は最新HGWのファームウェアに対応できていなかったよ NVR500・510を使うほうが楽だよ。ひかり電話で...
qiita.com

ざっくりいえば、ルータの配下にHGWを置き、

いう構成である。これはかなりうまく動く一方で、まだ以下の難点を残している。

  1. IPv4ブリッジに対応したルータしか使えない2
    • UNIVERGE IXシリーズなど
  2. 別途DHCPv6サーバが必要
  3. 別途外部からパケットキャプチャが必要

特に 2. と 3. は重要だ。市販ルータが扱えるDHCPv6オプションは少ないため、別途DHCPv6サーバを立てる必要がある。そのうえ、配るオプションの内容は、HGWのパケットを外からキャプチャして得なければならない3

しかしLinuxでルータを作れば、これらすべての機能──IPv4のブリッジ、DHCPv6サーバ、DHCPv6オプションの受信──を一台で担える。そのうえ適当な端末(たとえば1-3万円程度のミニPC)さえあれば済む。ちょっとしたDIYで、シンプルかつ多機能な構成を実現できるのだ。

環境

使用した回線・機器などは以下の通り。

回線契約

事情あってVDSLだが、それ以外はごく一般的な契約内容だと思われる。

例示のため、プロバイダから割り振られたIPv6プレフィックスを3fff:0:0:ab00::/56仮定する。

IPv4 over IPv6の接続に必要な情報は、あらかじめプロバイダから得るか、さもなくば計算機などで算出しておく。ここでは以下のように仮定する。

項目
CE3fff:0:0:ab00::ce
IPv4アドレス192.0.2.1
PS(= ポートセット)ID171(10進)/ ab(16進)
option peeraddr(= BR)2001:db8::1/64

機器

項目機種用途・備考
ミニPCGMKtec NucBox M6ルータ用。LANポートが二つ以上あればよい5UEFIで電力モードを"Quiet"にしている
L2スイッチTP-Link TL-SG105PEポート数を増やすために必要。タグVLAN(802.1Q)が切れればよい
HGW沖電気 RX-600KIひかり電話ルータ
VDSLモデム住友電工 VH100-4ESFTTH回線であれば、単独型ONU、しくは「UNI出し」したHGWの下半分(ONU部分)がこれにあたる

ルータ用には、AliExpressで拾った適当なミニPC6使った。ミニPCだけではポート数が足りないため、別途タグVLAN対応のL2スイッチも用意した。

ネットワーク設計

今回自作ルータが担う役割を整理すると、以下のようになる。

  1. 基本的なルータの機能
    • DHCPv6クライアントを動かしフレッツ網からIPv6プレフィックスを受け取る
      • 同時に、ひかり電話関連情報(DHCPv6オプション)も受け取る
    • IPv4 over IPv6のトンネルを通す
    • VLANで宅内機器側のセグメントを作り、IPv6・IPv4を配る
    • その他、ファイアウォールなども設定
  2. ひかり電話ルータ(HGW)の世話
    • VLANでHGW側のセグメントを分離する
    • フレッツ網-HGW間のIPv4パケットをブリッジする
    • DHCPv6-PDサーバを動かし、HGW向けにフレッツ網に似たIPv6環境を整える
      • IPv6プレフィックスの一部をHGWに再委譲する
      • ひかり電話関連情報をHGWに配布する

このうち太字の項目に関連して、いくつか考慮すべき点がある。

DHCPv6クライアントを動かすインターフェースについて

ルータのWAN側インターフェースは、その性質上、フレッツ網とHGWを繋ぐブリッジのメンバーになる。このため、当該インターフェースではDHCPv6クライアントを動かすことができない。代わりに、ブリッジデバイスでDHCPv6クライアントを動作させる7

IPv4 over IPv6のポート振り分けについて

IPv4 over IPv6(MAP-E)に関しては、以下の記事が注目に値する。

LinuxルーターのMAP-Eで良い感じにNATポートを使い回す方法 - turgenev’s blog
OpenWRTなどLinuxを使用してMAP-E(v6プラス、OCNバーチャルコネクトなど)接続を行う方法は様々なサイトで紹介されていますが、iptablesやnftables(バックで動いているのはnetfilter)のNATでは使用ポート範囲を複数(1000-2000, 3000-4000みたいな感じで)指定できないため、MAP-Eの利用可能ポートをうまく使い回すのが難しいという問題があります。具体的には、多くの接続を同時に行ってポートが不足気味のときに、実際には利用可能なポートが余っているにもかかわらずそれが割り当てられる対象になっていないため新規接続が失敗するという現象が発生する可能性…
turgenev.hatenablog.com

執筆時点では、この記事に示されているようなtc使ったポート振り分けの実用例はインターネット上に見つからなかった。今回はこの法を実環境で試してみる。

ひかり電話関連情報の取得・配布について

ひかり電話関連情報の受け渡しは意外に難しい。

理想としては、フレッツ網から受け取ったDHCPv6オプションを、そのままHGWに「中継」できればよいが、あいにくそうはいかない。代わりに、DHCPv6のリースを受けるたびに、リース内容をもとにDHCPv6サーバの設定を更新する仕組みを作る。

鍵となるのは、systemd-networkdのD-Bus APIと、Kea(DHCPv6サーバ)のManagement APIだ。systemd-networkdは、リースを受けるたびにD-Busシグナルを飛ばしてくれるうえ、リースの内容自体もD-Busで取得できる。一方、KeaのManagement APIを使うと、HTTPで動的に設定を投入できる。両者を合わせると8以下のような処理が可能になる。

  1. DHCPv6リースを受けた際のD-Busシグナルを検知
  2. D-Busを叩き、リース情報を取得
  3. リース情報を元に、KeaのAPIを叩いて設定を更新

お、実装はPythonで行うことにした。こんな処理のためにコンパイルはしたくないし、かといってシェルスクリプトには荷が重いからだ。D-Busとのやり取りにはpython-sdbusいうライブラリを使った。

ネットワーク構成

以上を実際の接続に落とし込んだ図がこちら

物理接続

青色は光ファイバー・LANケーブル、灰色は電話線による接続を指す。

物理接続

論理接続

黒色はIPv6 IPoE接続、赤色はIPv4 over IPv6接続、水色はHGW専用のIPv4接続を指す。

論理接続

補足

ルータの物理インターフェースは以下の2つある。

これに加えて、仮想インターフェースとして以下の4つを作成する。

L2スイッチ側でも、サブインターフェースに対応したタグVLAN(ID = 10, 100)を設定する9

手順1:環境構築

免責

手元では、以下の構成は安定して動作しており、実用上も問題は出ていない。しかしあくまでも実験的な構成であり、事業者の想定していない状態であることは留意ねがいたい。したがって動作は保証できないし、この記事の内容を実践した結果としていかなる損害が生じても、私は一切責任を負わない。

#ArchLinuxInstallBattle

まず、ミニPCにArch Linuxをインストールする(I use Arch, BTW)

使用するLinuxディストリビューションは自由だが、systemd-networkdを使う都合上、systemdのバージョンが新しいほうが良い。解説記事が無数にあるため、詳細なインストールプロセスは省略する。

あらかじめ、後で必要になるソフトウェアをインストールしておく。

Terminal window
pacman -S ethtool nftables iptables-nft ndisc6 kea jq
paru -S python-sdbus

NICのチューニング

続いてNICのチューニングをする(端末によっては不要)。

使用する端末には、RTL8125BというRealtek 🦀 の2.5GbE NICが載っている。しかし初期状態では汎用のドライバr8169)で動いており、本来の機能を発揮できない。

そこで製造元から配られている専用ドライバをインストールする。Arch Linuxの場合、AURにあるパッケージ使うと楽だ。

デフォルトのMakefileはかなり保守的になっていて、とえばマルチキューなどの機能が有効化されていない。そのため、適宜書き換えてビルドする10

r8125-rss-dkms/PKGBUILD
# (前略)
_pkgname=r8125
pkgname=${_pkgname}-dkms
pkgname=${_pkgname}-rss-dkms
pkgver=9.016.00
# (中略)
package() {
# (中略)
sed -e "s/@_PKGNAME@/${_pkgname}/g" \
-e "s/@PKGVER@/${pkgver}/g" \
-i "${pkgdir}/usr/src/${dir_name}/dkms.conf"
sed -e 's/ENABLE_RSS_SUPPORT = n/ENABLE_RSS_SUPPORT = y/' \
-e 's/ENABLE_MULTIPLE_TX_QUEUE = n/ENABLE_MULTIPLE_TX_QUEUE = y/' \
-e 's/CONFIG_ASPM = y/CONFIG_ASPM = n/' \
-i "${pkgdir}/usr/src/${dir_name}/Makefile"
}

そして汎用ドライバを読み込まないようにする。

/etc/modprobe.d/blacklist-r8169.conf
blacklist r8169

次回の起動時にr8125ドライバがロードされ、マルチキューが有効化されるはずだ。

Terminal window
> ethtool -i wan0
driver: r8125
version: 9.016.00-NAPI-RSS
firmware-version:
expansion-rom-version:
bus-info: 0000:03:00.0
supports-statistics: yes
supports-test: no
supports-eeprom-access: no
supports-register-dump: yes
supports-priv-flags: no
Terminal window
> ethtool -l wan0
Channel parameters for wan0:
Pre-set maximums:
RX: 4
TX: 2
Other: n/a
Combined: n/a
Current hardware settings:
RX: 3
TX: 2
Other: n/a
Combined: n/a

せっかくマルチキューが使えるようになったので、irqbalanceでキューを各CPUコアに振り分けるようにしておく(おまじない)。

Terminal window
pacman -S irqbalance
systemctl enable --now irqbalance

ついでにNICの送受信バッファも大きくする(おまじない)。適当にRX = 512、TX = 1024とした。

/etc/systemd/network/01-wan0.link
[Match]
PermanentMACAddress=de:ad:be:ef:00:01
[Link]
Name=wan0
RxBufferSize=512
TxBufferSize=1024
# MACアドレスのランダム化
MACAddressPolicy=random
/etc/systemd/network/01-lan0.link
[Match]
PermanentMACAddress=de:ad:be:ef:00:02
[Link]
Name=lan0
RxBufferSize=512
TxBufferSize=1024
Terminal window
> ethtool -g wan0
Ring parameters for wan0:
Pre-set maximums:
RX: 1024
RX Mini: n/a
RX Jumbo: n/a
TX: 1024
TX push buff len: n/a
Current hardware settings:
RX: 512
RX Mini: n/a
RX Jumbo: n/a
TX: 1024
RX Buf Len: n/a
CQE Size: n/a
TX Push: off
RX Push: off
TX push buff len: n/a
TCP data split: n/a

カーネルパラメータのチューニング

ArchWiki教えに従い、TCP / UDPのさまざまなパラメータを調整する。すべておまじないだ(世界の再魔術化)。

/etc/sysctl.d/99-tuning.conf
net.ipv4.tcp_slow_start_after_idle = 0
net.core.default_qdisc = cake
net.ipv4.tcp_congestion_control = bbr
net.core.netdev_max_backlog = 16384
net.core.rmem_default = 1048576
net.core.rmem_max = 16777216
net.core.wmem_default = 1048576
net.core.wmem_max = 16777216
net.core.optmem_max = 65536
net.ipv4.tcp_rmem = 4096 1048576 2097152
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.udp_rmem_min = 8192
net.ipv4.udp_wmem_min = 8192
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_mtu_probing = 1

もちろんルータとして使う以上、IPv4 / IPv6の転送設定は必須だ。

/etc/sysctl.d/99-ip-forwarding.conf
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.ipv6.conf.all.forwarding = 1

あとでIPv4 over IPv6トンネルを張る際に、不要なトンネルデバイスip6tnl0)が作成されないような設定もしておく。

/etc/sysctl.d/99-no-fb-tunnel.conf
net.core.fb_tunnels_only_for_init_net = 2

ここまでで一通りの環境構築は済んだ。第二部では端末をフレッツ網へ接続し、基本的なルータとしての機能を実現する。


  1. もちろん、これでもアドレス数は十分なのだが、せっかく与えられたものが無駄になるのは気に入らない

  2. とはいえIPv4のブリッジ程度であれば、他社のルータでも実現できるかもしれない

  3. 詳しくは第三部で述べるが、DHCPv6リクエストに適切なオプション(Option 16, Vendor Class)を含めれば、ルータで直接ひかり電話関連情報を受信できる。しかしこれに対応した市販ルータは(おそらく)少ないし、よしんば受信できたとして、その情報を下流に送信できるとは思えない

  4. この記事では、VDSLモデム(ないしONU)とHGWが独立していることを暗黙の前提としている。両者が一体化しているタイプのHGWの場合、いわゆる「UNI出し」をするとこの状態にできる。小型ONUの場合は、Linuxルータに別途SFP+ポートが必要になる

  5. VLANを駆使すれば、ポートが一つでもなんとかなるかもしれない

  6. なんとRyzen 5 6600H(6C12T)と16GBDDR5が載っている。本来はもっと省電力な機種を使うべきだ

  7. MACVLANでWAN側インターフェースを二つの仮想インターフェースに分割し、片方をブリッジに組み込む構成も試したが、うまく機能しなかった(下りのパケットを適切に処理できなかった)

  8. systemd-networkdにはDHCPv6サーバ機能がないため、併用が必要

  9. お、もう一つVLANを切ってHGWのLAN側と接続しておくと、ルータ経由でHGWのWeb設定画面にアクセスできて便利だが、今回は省略した

  10. OpenWrtではデフォルトでマルチキューが有効化されているため、それに倣った