Panasonic JT-B1をネイティブLinuxのSpotifyプレーヤーにする

Android 4.0.4が入ったPanasonic TOUGHPAD JT-B1を、Alpine Linuxで起動するSpotifyプレーヤーにした。 純正カーネルを残してユーザー空間を入れ替え、画面、タッチ、5 GHz Wi-Fi、音声出力を動かし、日本語入力付きの縦画面GUIを作った。 内蔵LTEモデムのSIMロックも解除し、povo 2.0での通信を確認した。

最初の目的は「Androidの中でLinuxを動かす」ことではなく、端末の通常起動先をLinuxへ置き換えることだった。 途中から用途をSpotifyに絞り、端末自身で曲を探して再生できるところまで進めた。 この記事は、2026年11月4日から6日を中心とするセッション履歴、作業メモ、Triliumの記録、保存した検証ログをもとに再構成している。 調査にはコーディングエージェントを使い、画面の目視、実際のタッチ、スピーカーとイヤホンの聴取は手元の実機で確認した。

これは所有端末での改造記録であり、同型機にそのまま適用するインストール手順ではない。 ストレージとモデムの書き換えには起動不能になる危険がある。 全体バックアップは取得したが、分解せずに復旧できる独立した手段と、全体イメージの実機復元は未検証である。

公開コードと試せる範囲

常用したコードを整理して、chun37/jt-b1-linuxで公開した。 日本語GUI、音声監視、Wi-Fi互換処理、povo接続サービスに加え、実機なしで動くテストと画面プレビューを収録している。 試す場合は、リポジトリのREADMEにあるPC向け手順から始められる。

公開版は、すでにLinuxを起動できる端末向けのソースコード集であり、Androidからの一括インストーラーではない。 root取得、rawパーティション書き込み、SIM解除とモデムFW書き込みの試行は含めていない。 メーカーのバイナリー、認証情報、個体情報も同梱せず、Spotifyは読者自身のClient IDと認証情報を使う。 公開用に設定と補助ツールを整理した版はPCで検証しているが、その変更を含む版を実機へ再配置した試験はしていない。

出発点となったJT-B1

対象は日本向けのJT-B1APAAABJで、Androidのビルドは09.0101だった。 実機から取得した情報は次のとおり。

項目 確認した内容
SoC TI OMAP4460 ES1.1、ARMv7。SoC種別の表示はHS
OSとカーネル Android 4.0.4、Linux 3.0.8+
メモリーとストレージ Linuxから見えるRAMは約658 MiB、eMMC全体は15,634,268,160バイト
画面と入力 1024×600のomapfb、Atmel maXTouch、A1〜A3と電源の物理ボタン
通信と音声 wl12xx系Wi-Fi、Sierra Wireless MC7700、TWL6040系音声、3.5 mm端子

通常のADB shellはUID 2000で、adb rootは製品ビルドとして拒否された。 adb reboot bootloaderを試してもfastboot端末は現れず、通常のAndroidへ戻った。 ただし、この一度の結果だけでfastboot機能が存在しないとは断定できない。

作業には「分解しない」「UARTを使わない」という制約があった。 そのため、ブートローダーへシリアル接続して設定する方法ではなく、USB ADBと保存した純正ソフトウェアから起動経路を探した。

純正更新パッケージから起動処理を調べる

ZIPの中にそのままカーネルがあるわけではなかった

最初にPanasonicの公開サポートから、国内向け09.0096の更新パッケージを取得した。 後には海外向けの7版も保存し、起動処理の違いを調べた。

更新ファイルは、拡張子がdatのZIPにallやmtd6、mtd13などが入った構造だった。 ただし、mtd6をそのまま端末の6番目のパーティションへ書けばよい、という意味ではない。 アーカイブ内の論理名と実機のパーティション番号は分けて扱った。

更新処理を担当するARM実行ファイルのUpdateAgentを抽出すると、RedBend vRMの更新エンジンが含まれていた。 そこで、実機で更新を走らせる代わりに、Unicorn(CPU命令をエミュレーションするライブラリー)で更新エンジンの一部を実行した。 ゲストが見るファイルとブロックデバイスはPythonのメモリー内に置き、実機やホストのデバイスへアクセスさせない構成にした。

この方法で通常起動用とリカバリー用のイメージを復元し、uImageのヘッダーと本体のCRC一致を確認できた。 uImageの表示名はLinux-2.6.35だったが、展開したカーネルの実体は3.0.8+だった。 ファイル名やヘッダーの表示名だけでは、実際の版は判断できなかった。

署名らしい領域と、起動できることは別だった

復元したイメージにはCertISWという外側のヘッダーがあり、uImageに対するSHA-1や署名情報らしい領域があった。 SHA-1とCRCは照合できたが、暗号署名の方式や、ブートローダーがどの段階で何を強制検証するかまでは確定できなかった。 SoCがHS品と報告されることも、無署名カーネルを簡単に起動できる前提にはできない理由だった。

リカバリーの鍵も調べた。 testkey.x509.pemという名前の証明書は、AOSPで公開されているtestkeyではなくPanasonicの証明書だった。 標準OTAの署名検証とメーカー独自のSD更新処理にも別の制御フローがあり、一方の調査結果を他方の結論へ流用できなかった。

結局、署名付きカーネルそのものを改造する方法は採らなかった。 以後は、純正カーネルのままLinuxのユーザー空間を起動できないかを調べた。

kexecとリカバリーも近道にはならなかった

純正カーネルにはkexecに関係する名前があった。 しかし保存バイナリーを調べると、対象の経路は通常の別カーネルへの引き渡しではなく、メーカー独自のダンプと再起動処理につながっていた。 シンボルが存在するだけで、一般的なkexec起動が使えるとは判断できなかった。

adb reboot recoveryでリカバリーメニューへ入ることはできた。 そこには外部SDからの更新やMC7700のファームウェア更新があったが、ADBはofflineで、保存した旧版の起動定義ではadbd自体がコメントアウトされていた。 Androidが起動しなくなったときに物理キーだけで確実に入れる手順も確認できなかった。

この時点で「リカバリーがあるから、失敗しても戻せる」とは言えなかった。

rootを取っても、すべてにアクセスできるわけではない

COW競合による一時変更とCCSecurity

初期の試行では、Dirty COW系の手法で、コピーオンライト(COW)の競合を利用した読み取り専用マッピング経由のファイル変更が成立した。 続いてrun-asの一部を一時的に変更し、実効UID 0で動く固定の診断コードを実行できた。 変更した実行ファイルは試行後に元へ戻し、ファイル全体の一致を確認した。

ところが、実効UIDが0でもブロックデバイス、セキュリティポリシー、シェルの起動は拒否された。 カーネルの起動引数にはccsecurity=onがあり、TOMOYO系のCCSecurityが動いていた。 UnixのUIDだけではなく、どのプログラムから実行されたかというドメインにも制限があった。

このため「root化成功」と「自由なrootシェルを取得できた」は分けて記録した。 限定したコードで設定RAMや起動定義を読めても、そのままOSを置き換えられるわけではなかった。

内部ストレージのスクリプトを起動するLiNSという仕組みも調べた。 有効化フラグは0だったが、設定ライブラリーはRAMだけでなく保存領域も更新する構造で、単純な一時変更として扱えなかった。 さらに、保存したポリシーにはchrootやpivot_rootへの制約があり、LiNSを有効にすればLinuxへ切り替えられるという結論には至らなかった。 この経路は最終的な起動方法には使っていない。

管理デーモンのコマンド処理から一時rootへ

別の経路として、メーカーの端末管理サービスを解析した。 管理APIの呼び出しがrootの管理デーモンを起動し、そのデーモンが受け取った文字列をシェルコマンドへ埋め込む処理があった。 ここにはシェル用のエスケープがなかった。

デーモンの生存時間は短く、通常の管理クライアントより先に接続する必要があった。 接続の競合を制御し、メーカー設定を変更する本来のコマンド部分を実行させない形にして、rootのコマンド実行を確認した。 これにより、通常のshellでは見えないディレクトリーの読み出しもできた。

検証中はWi-Fiなどの非loopbackインターフェースを停止した。 デーモンが全インターフェースで待ち受ける構造だったため、管理機能をネットワークへ公開しないためである。 処理後にはデーモンの停止と管理用プロパティーの復元を確認した。

なお、純正の監査デーモンには、rootに関する監査ログを契機としてメーカー設定を保存する処理も見つかった。 「読み取りコードしか実行していない」ことは、システム全体に書き込みが一切発生しない保証にはならなかった。 過去の試行で実際にその保存が発生したかは確認していない。

Androidを保存し、Alpineを直接起動する

全体バックアップと、その限界

OSを書き込む前に、eMMC全体と各2 MiBのboot0、boot1を保存した。 全eMMCは約15.6 GBで、転送元と受信側、PCでの再読み出しのSHA-256が一致した。

ただし、直接のデバイスパスはrootからでも拒否された。 通常再起動後、/dev配下に作った一時的なブロックデバイスノードから読み出す方法が成立した。 同じ領域でもパスによって結果が違い、UID 0だけでは説明できない制限がここにも現れた。

バックアップはAndroid稼働中のコピーである。 /dataを安定した読み取り専用状態へ移す追加の試行は失敗し、その条件での転送は始めなかった。 元イメージを保全したうえで、PC上のuserdataの別コピーだけを修復し、fsckの再検査が正常終了することは確認した。

ハッシュが一致したバックアップの取得と、アプリケーションの整合性や実機への復元成功は別である。 この制限を残したまま、Linuxの書き込みと起動試験へ進んだ。

純正カーネルから別のrootfsへ

採用したのは、ブートローダーと純正カーネルを維持し、起動引数でAlpineのrootfsを指定する構成だった。 rootfsは、OSが/として使うファイルシステムである。

領域 用途と変更
p22 元のfotadelta領域をAlpine Linux 3.23.7 armv7のrootfsへ転用
p4 起動環境のbootargsをLinux用へ変更
ブートローダーとカーネル 変更せず純正を利用
それ以外のAndroid領域 このOS導入では初期化せず、Linuxからもマウントしない

起動引数の要点は次のとおり。 これは実機に導入した設定の抜粋であり、書き込みコマンドではない。

root=/dev/mmcblk0p22 rootwait
rdinit=/jtb1-no-ramdisk-init
init=/sbin/init
ccsecurity=off

rdinitには存在しないパスを指定した。 既存initramfsのAndroid用/initへ進まず、カーネルが外部ルートをマウントして/sbin/initを実行する経路を使うためである。 この挙動は保存したバイナリーで事前に調べ、最終的には実機起動で確認した。

書き込みには、対象のデバイス番号、開始位置、容量、非マウント状態、入力イメージのサイズとハッシュを固定検査する小さなARMプログラムを使った。 書き込み後はfsync、キャッシュ破棄、領域全体の読み戻しを行い、SHA-256を照合した。

初回用の起動処理には、LinuxのrcSまで到達したら起動環境をいったん元のAndroid設定へ戻す処理を入れた。 USBの初期化に失敗しても、次回の起動先を戻しておく意図だった。 ただし、rcSより前に失敗した場合は役に立たず、元設定からのAndroid再起動も試していない。 独立したリカバリーの代わりにはならない。

chrootではなくLinuxがPID 1になった

初回起動後、USB ADBはJT-B1-LINUXとして現れた。 PID 1の実体は/bin/busybox、コマンドラインは/sbin/initで、/はp22のext4を直接マウントしていた。 Androidのzygote、system_server、surfaceflingerは存在しなかった。

この確認後にLinux用の起動設定を確定し、もう一度再起動した。 次の起動でも同じ構成になったため、Android上のchrootではなく、通常起動するユーザー空間の置き換えが成立した。

最初はUSBのCLIだけだった。 画面もタッチもWi-Fiも、自動的にAndroidと同じように動くわけではなかった。

黒い画面を表示させ、電池LEDを戻す

フレームバッファーの内容と実際の表示は違う

Alpine起動後、画面は黒く、バックライトが薄く光るだけだった。 /dev/fb0に画像を書けて、読み戻した画素が一致しても、実際のパネルには何も出なかった。

表示コントローラーの状態を調べると、ソフトウェア上ではoverlayが有効でも、実レジスターでは表示先アドレスとサイズが0で、有効ビットも落ちていた。 同じオフセットへのFBIOPAN_DISPLAYは、純正ドライバー内で設定の反映を省略していた。

解決したのは、現在の解像度を変えず、FBIOPUT_VSCREENINFOをNOW | FORCEで実行することだった。 これで実際の表示設定が反映され、Alpineの文字が画面に現れた。 以後は起動処理にこの初期化を組み込んだ。

当初はAndroidからデバイスツリーを取り出す案もあった。 しかし今回の構成ではDTBを抽出して新カーネルへ移植したのではなく、純正カーネルに組み込まれた機種設定と既存ドライバーを使っている。

LEDは常時点灯ではなく、純正の状態表示へ

電源LEDも、単に緑で固定するのではなく、Android時代と同じ状態表示にしたかった。 純正のframeworkとライトHALを調べ、電池状態からLEDへの対応を取り出した。

電池状態 移植した表示
過熱 橙色で点滅
非充電で15%未満 赤色で点灯
満充電 緑色で点灯
充電中 橙色で点灯
その他 消灯

最初の監視処理は「電池の状態が変わったときだけLEDを更新」する実装だった。 ところが再起動後、電池状態が同じままLEDの実値だけが消灯側へ変わり、更新されなかった。 状態の変化だけでなくLEDの現在値も照合し、不一致を再適用する方式へ直した。 満充電時の緑点灯は、再起動後も目視で確認した。

この過程には失敗もあった。 メーカー設定用のcfgdrvとddsyncを使う一時試験の直後にADB shellが開けなくなり、強制オフからの再起動で復帰した。 LEDのtrigger変更でも処理が戻らなくなる試行があり、これらを常用設定には採用しなかった。 表示とLEDは、それらの試行を繰り返さずに動く構成へ切り替えた。

5 GHz Wi-Fiとタッチを動かす

5 GHzが見えない原因は地域設定だった

Wi-Fiは、元のAndroidから保存したcompat、cfg80211、mac80211、wl12xx、wl12xx_sdioと対応ファームウェアを再利用した。 実行中の純正カーネルとモジュールのvermagicが一致することを確認している。

最初は目的のアクセスポイントを検出できなかった。 Androidでは見えていた5 GHzのネットワークが、Alpineでは見えなかった。

原因は、regulatory domain(無線の地域別利用条件)の初期要求が未処理だったことだった。 JPを要求するだけではエラーになり、先にworldの初期要求へ応答する必要があった。 初期要求を処理してからJPを設定し、公開の無線規則の保守的な部分集合を返す補助プログラムを作った。 送信出力、passive scan、DFSなどの制約は残し、利用可能な周波数を無制限に広げる方法は採っていない。

別の問題として、新しいwpa_supplicantが送るscan flagsを古いnl80211が拒否した。 該当プロセスだけでこの属性を付けない互換処理を追加した。

これで5 GHzの5180 MHz、チャンネル36に接続し、DHCP、外部への通信、HTTPS取得が成功した。 通常再起動後の自動接続と、画面のIP表示も確認した。 今回確認したのは5 GHzであり、6 GHz対応を確認したわけではない。

タッチは純正モジュールを再利用できた

タッチには純正のatmel_mxt_ts.koを使った。 Alpineでは未ロードだったため、配置してロードすると入力イベントが現れた。 座標範囲はXが0〜1023、Yが0〜599だった。

まず接触位置に水色の十字を描く小さなプログラムを作り、押す、動かす、離すという入力を確認した。 実際に触った位置と十字の位置が一致し、再起動後も同じように動いた。

メーカー設定ファイルの一部は取得できていなかったが、コントローラーに残る設定で入力は動いた。 ここでも、設定用モジュールを追加ロードするより、動いているドライバーと入力をそのまま使う方を選んだ。

Spotify Soloistを古いLinuxで動かす

汎用モバイル環境より、用途を絞った構成へ

GUIを考え始めた時点では、Plasma MobileやUbuntu Touchのような環境も候補だった。 しかし、古いカーネル、約658 MiBの可視RAM、未成立の現代的な描画基盤という制約があった。 /dev/driはなく、代わりにfb0とPowerVR系のデバイスが見えていた。 GPUが存在しないのではなく、現行のWayland環境をそのまま使える状態ではなかった。

そこで用途をSpotifyへ絞った。 別機器の再生を操作するリモコンではなく、JT-B1自身から音を出すことを目標にした。

再生側にはSpotify公式のSoloistを使った。 調査時点の公式資料にARMv7 Linux対応とロスレス対応の記載があり、ローカルAPIも備わっていた。 採用したビルドは1.3.10.42である。

Alpineのmuslと、Soloistのglibcを分ける

公式ARM32バイナリーはglibc向けだった。 Alpineのmuslをシステム全体で置き換えるのではなく、Debian由来のglibc 2.41と必要なライブラリーを専用ディレクトリーへ置いた。 専用ローダーからSoloistを起動し、同様にPulseAudioも必要な依存関係だけを持つ構成にした。

起動できても、音声出力には次の問題があった。 新しいDebianの標準libasoundはtime64のioctlを使い、古いカーネルでSNDRV_PCM_IOCTL_SYNC_PTR failed (-25)になった。

ALSA 1.2.14を専用sysrootでクロスビルドし、-D_TIME_BITS=32 --disable-year2038で旧来の互換経路を使う私有版へ差し替えた。 システムのALSAは変更していない。 この選択には2038年以降の時刻対応を保証しないという制限がある。

最終的な音声用ディレクトリーは約44 MiBになった。 SoloistのWebSocketはloopbackだけ、PulseAudioはroot専用のUnixソケットだけで待ち受ける構成にした。

playingなのに無音だった理由

Soloistがplayingを返し、ALSAのPCMがRUNNINGになっても、最初はスピーカーから何も聞こえなかった。 PCMのポインターは進み、再生側のサンプルにも信号があった。 ソフトウェアが音声データを送っていることと、物理出力が有効なことは別だった。

純正の音声HALとカーネルを調べると、通常のHandsfree Playback Volumeは、内部フラグが無効なときに希望値だけを保持する実装だった。 ミキサーの表示値が変わっても、コーデックのゲイン設定は変わっていなかった。

別にあるAlt Handsfree Playback VolumeのVolume for MultiMediaが、音楽再生用のゲインを実際に反映する経路だった。 保存カーネルの該当処理をPC上で限定実行して動作を確認し、実機では再生を止め、音量を下げてから適用した。 その後、内蔵スピーカーから音が出た。

3.5 mm端子も別途対応した。 こちらはヘッドセット側のDL1経路と、再生用のSidetone Mixer設定が必要だった。 スピーカーとイヤホンの双方で、実際の発音を確認できた。

ロスレスの表示と、受信形式の検証を分ける

最高品質を要求する設定を入れ、公式Spotify側でロスレス表示が出ることも確認した。 ただし、受信したストリームのコーデックとビット深度を独立に検査したわけではない。

確認したALSA出力はS32_LE、2チャンネル、44.1 kHzだった。 このPCM形式だけから、配信元のストリームがFLACだったと結論づけることはできない。 「ロスレス表示を確認した」と「配信から出力までを独立監査した」は区別している。

日本語で曲を探せる縦画面GUIを作る

再生APIと検索APIを分担する

Soloistだけでは、検索や保存曲の一覧を取得するAPIが足りなかった。 そのため、再生はSoloist、検索やライブラリーの取得はSpotify Web APIという分担にした。

自作タッチGUI
  ├─ Spotify Web API:検索、保存曲、アルバム、プレイリスト
  └─ SoloistのローカルAPI:再生、停止、曲送り、音量
       └─ PulseAudio → 私有ALSA → 純正音声ドライバー

Web APIの認証は、PCのloopback上でPKCEを使って行った。 Client Secretを端末や会話へ渡さずに認可し、端末側にはトークンの更新処理も実装した。 古いADBのshellはPTYを経由するため、秘密をそのまま標準入力へ流す方法は避け、非秘密データで転送と改ざん拒否を検査してから、暗号文だけを転送した。

フレームバッファーへ直接描く

GUIには専用のPythonとPillow、Noto CJKフォントを使った。 画面は論理600×1024の縦向きで、物理1024×600のフレームバッファーへソフトウェア回転して出力する。 タッチ入力も逆変換する。 XorgやWaylandのデスクトップ環境は導入していない。

検索には日本語キーボードとSKKの漢字候補を用意した。 検索結果、保存曲、アルバム、プレイリストを表示し、ページ送りと再生操作を接続した。 実機では日本語入力から検索、選曲、再生、停止までを検査し、手元でも好きな曲を検索して再生できた。

プレイリストは、調査時点のWeb APIのDevelopment Mode制限を受ける。 所有するプレイリストの非空の内容は取得できたが、他人のプレイリストを含む公式Androidアプリの全機能を再現したわけではない。 曲行からの単曲再生と、アルバムやプレイリスト全体のcontext再生にも違いがある。

表示だけの試作から始め、実API、トークン更新、入力、再生操作を順に接続した。 画面が出た段階と、端末単独で使える段階を分けて進めた。

表示プロセスが落ちても、音声操作を放置しない

GUIには監督プロセスを置いた。 表示子プロセスの終了を検出し、所有する操作用の子プロセスをPIDだけでなく開始時刻なども照合して回収する。 再生状態を変更する操作が途中だった場合は、一時停止する処理も入れた。

通常終了、表示子の強制終了、未完了の操作を持つ表示子の強制終了を実機で試した。 画面の復旧と、必要な場合の操作子の停止、一時停止を確認した。 監督自身を含むすべての強制終了タイミングを保証するものではない。

携帯回線はSIMロック解除とモデム復旧が必要だった

PH-NET PINは通常のSIM PINではなかった

内蔵のMC7700は、保存した純正Sierra系シリアルドライバーとGobiNetをロードすると認識した。 ところがpovoのSIMを入れても、AT+CPIN?はPH-NET PIN、PNロックの照会は有効という結果だった。

これはSIM自体のPIN入力待ちではなく、モデムのネットワーク個人化ロックだった。 APNを書くだけで接続できる状態ではなかった。 メーカーとdocomoにはサポート対象外と案内され、無料で解除できる手段を調べることになった。

ファームウェアの試行で通常応答を失った

解除調査は最初から成功したわけではない。 途中のファームウェア書き込みやNVM操作を経た後、モデムが通常のATとQMIに応答せず、ダウンロード系の単一ポートだけになる状態に入った。 どの操作が原因だったかを、一つのログから確定することはできなかった。

復旧では、それまでの成功らしい表示をいったん根拠から外した。 stockというファイル名でも元のファームウェアとは限らず、過去のATブリッジにはATIとOKを合成する実装もあった。 その応答は、実モデムが正常である証拠には使えなかった。

公式DocomoパッケージのAPPL部分と別取得した03.05.19.00AのAPPLが完全一致することを確認し、BOOTを追加で書かずにAPPLだけを書き込んだ。 更新ツールは全チャンク転送後の終了処理でタイムアウトし、正常終了にはならなかった。 しかし、その後に実機のシリアルポートへ直接接続するとATとQMIが復帰し、モデムの電源を入れ直しても応答した。

復旧したのは、以前に動作ログのある03.05.19.00Aである。 最初に記録した03.05.19.00とバイト単位で同じ工場状態へ戻した、という意味ではない。

開発用認証とSIM解除は別だった

開発用のOPENLOCKやOPENMEP認証がOKになっても、PNロックは残った。 認証成功をSIMロック解除成功とは扱えなかった。 EFSにある個人化情報の読み出しも拒否され、この方法では解除キーを得られなかった。

有料候補としてDC-unlockerも調べた。 WineではCOMポートを認識せず、WindowsへUSB/IPで転送した環境ではMC7700の検出まで確認した。 これは接続適合の確認であり、有料解除を実行したわけではない。 最終的に購入はしていない。

旧Qualcommの診断実装から正しいキーを読む

成功につながったのは、公開されている旧QualcommのGSDI診断実装だった。 EFSファイルを直接読む方法とは別に、遅延応答を返すgeneric診断に個人化キーを読み出す処理があった。 対象のMC7700でこの形式の応答を検査し、端末内に保持される正しいNW解除キーを取得できた。

取得した端末識別情報を直接のAT照会と照合し、正しい解除キーでPN解除を一度だけ実行した。 直後の照会を期待どおりに受け取れずラッパーの検査は失敗したが、解除処理を繰り返さず、独立したATとQMIで状態を確認した。 最終的にCPIN READY、PNロック無効、QMIのPNがdeactivatedになった。

IMEIや解除キーは公開用のログに出していない。 外部の解除サービスへ識別情報を送ったり、推測コードを総当たりしたりせずに解除できた。 この結果は対象モデムとファームウェアでの実績であり、別機種への適用を保証しない。

Wi-Fiに迂回していないことを確かめる

解除後、APNをpovo.jpに設定し、KDDIのLTE Band 1へ登録した。 データ接続にはGobiNet経由でQMIのWDSを使い、接続時にIP、ゲートウェイ、DNSを取得する常用サービスを作った。 標準のqmicliはこのqcqmiデバイスでそのまま使えず、ドライバーのioctlからサービスを割り当てる処理が必要だった。

通常はWi-Fiを優先し、LTEは大きいmetricで待機させた。 サービス停止時には、自分が追加したIP、経路、DNSだけを取り除く。 Wi-Fi側の設定が残ることと、携帯サービスを再起動して接続し直せることを確認した。

実通信の検査では、Wi-Fiを有効にしたまま、DNSのUDPソケットとHTTPSのTCPソケットの両方をSO_BINDTODEVICE=usb0で携帯側へ固定した。 送信元アドレスも携帯側を指定し、CAとホスト名の証明書検証を有効にしたままHTTPSの200応答を確認した。

「Wi-Fiがつながっている端末でWebが開けた」だけでは、LTE通信の証明にならない。 この検査で、携帯回線だけを通るDNSとHTTPSが成立したことを確認できた。

ステータスバーと物理ボタンを備える

アプリと共通表示を分ける

最初はSpotifyのGUIが画面全体を描いていた。 その後、アプリが終了しても時刻、通信状態、電池残量が残るように、共通の描画サービスを作った。

論理画面600×1024のうち、上44ピクセルをステータスバー、下600×980をアプリ領域に分けた。 フレームバッファーは共通サービスだけが描き、アプリは画像を渡す。 タッチ座標もアプリ領域へ変換する。

Wi-FiとLTEの表示は、通常の送信に選ばれる経路に合わせて一方だけを出すようにした。 接続そのものを排他的にしたのではなく、Wi-Fi優先とLTE待機の設定は維持している。 アプリが閉じた画面と起動時の案内も同じ描画処理へ揃えた。

ホームにはSpotifyのタイルを一つ置いた。 公式のブランド素材から取得したアイコンを使い、タップでSpotifyへ戻れるようにした。 再生、前後曲、シャッフル、リピートも、フォントの記号ではなく図形アイコンで描いた。 汎用のアプリ切り替え環境を完成させたのではなく、今あるSpotifyを開き直せるホームである。

物理ボタンを音楽操作へ割り当てる

前面の物理ボタンは次のように割り当てた。

ボタン 短押しの機能
A1 前の曲
A2 再生と一時停止の切り替え
A3 次の曲
電源 ホームへ戻る

入力装置はevent番号を固定せず、装置名で探索する。 キーリピートや短すぎる再操作、同時押しを抑制し、処理中の追加操作は捨てるようにした。 実機ではホーム復帰、再生停止、スキップが動くことを確認した。 前曲と次曲それぞれの全条件や、長押しまで確認したわけではない。

電源ボタンの短押しはGUI内のホーム復帰であり、一般的なスマートフォンのスリープ機能を作ったわけではない。

AUX用途に合わせて音量方針を変更する

初期の実装では安全側に倒し、イヤホン接続時の音量を20%以下に制限した。 スピーカーは100%まで操作でき、イヤホン挿入時に音量を下げる版も実機で確認した。

その後、3.5 mm端子をAUX出力にも使うため、この制限を解除した。 現在の実装はスピーカーと3.5 mm端子の双方で0〜100%を許可する。 ただし、抜去時の一時停止、出力切り替えと音量変更の排他制御、監視異常時の出力停止は残した。

AUX用途への対応は音量制限の変更であり、固定レベルのライン出力を新設したわけではない。 実際のAUX機器へ接続した発音試験は未実施で、イヤホンを使うときは大きな音量が保存されている可能性にも注意が必要になる。

起動時にSpotifyを開き、前回の音量で再生する

最終的には、OS起動後にSpotifyの画面を直接開き、保存した曲を再生するようにした。 ホーム画面は電源ボタンやアプリ終了後の戻り先として残している。

BusyBox initのinittabからGUIサービスを起動し、音声監視と通信の準備を待つ専用の子プロセスが再生を要求する。 同じ起動で何度も再生しないよう、boot IDを使って一度だけ要求を記録する。 表示プロセスの再起動だけで、利用者が止めた曲を勝手に再開しないためでもある。

最初の再起動試験では自動再生まで進んだが、音量が初期値20%へ戻った。 そこで、音量変更の成功後に設定を原子的に保存し、次回は保存音量を復元してから再生するようにした。 音量の復元に失敗したら自動再生しない。 保存値がない場合だけ20%を初期値として使う。

後続の再起動では、保存値と実音量がともに100%で一致し、起動後のSpotifyがplayingになることを複数回観測した。 その後はタッチ入力の更新と一時停止への遷移を記録した。 観測用スクリプトは別の完了マーカーを待って期限切れになったが、再生状態そのものは確認できており、自動再生の失敗とは扱っていない。

この再起動ではUSB ADB、Wi-Fi、LTEの経路と音声監視の復帰も確認した。 ただし、最後の再起動後に物理的な発音を改めて聴取したわけではない。 保存するのも最後の曲URIと音量が中心で、キュー全体や再生位置を厳密に復元する実装ではない。

できたことと、まだ確認していないこと

実機で確認できたこと

  1. 純正カーネルのままAlpineを直接起動し、BusyBoxをPID 1にできた。
  2. 画面、タッチ、満充電LED、5 GHz Wi-Fiを動かし、通常再起動後の復帰を確認した。
  3. Spotifyを本体で再生し、内蔵スピーカーとイヤホンの発音、日本語検索からの再生を確認した。
  4. MC7700のSIMロックを解除し、povo LTE限定のDNSと証明書検証付きHTTPSを確認した。
  5. 共通ステータスバー、タイルホーム、物理ボタン、起動時再生、音量保持を組み込んだ。

ストレージとセキュリティの制限

全体バックアップはあるが、全体像を実機へ戻す試験はしていない。 Androidが起動しない状態からの独立復旧も未確立である。

純正3.0.8+は古いままで、CCSecurityを無効にした構成を使う。 最初は固定のrootブリッジを使っていたが、後にadbdもroot常駐へ変更し、通常のadb shellでUID 0を使えるようにした。 信頼するUSB接続PCからの管理を前提としており、一般向けに安全な端末へ仕上げたという意味ではない。

音声と電源管理の制限

ロスレス表示は確認したが、受信コーデックとビット深度の独立監査はしていない。 AUX機器の実接続、最後の再起動後の発音聴取、0%保存からの再起動も未確認である。

長時間連続運転、サスペンド復帰、消費電力、電池残量の精度は評価が残っている。 満充電以外のLED状態も、実際の低残量や温度異常を起こして検証したわけではない。

通信とソフトウェア保守の制限

LTEは今回の場所とBand 1での実績であり、移動中の基地局切り替え、圏外復帰、実際にWi-Fiを切ったアプリの通信切り替えまでは検証していない。 Wi-FiのDFSチャンネルや長期切断からの復帰も未確認である。

採用したSoloistはビルドから90日で失効するため、更新が必要になる。 私有ライブラリーにはtime32互換の制約があり、Web APIには調査時点の開発用制限もある。 一度動いた構成をそのまま永久に使えるわけではない。

今回の改造で残ったもの

作業前のJT-B1は、Android 4.0.4が通常起動する端末だった。 現在は純正カーネルからAlpineを直接起動し、縦画面で日本語の曲検索ができ、スピーカーと3.5 mm端子へSpotifyを出力する構成になった。 Wi-Fiに加えてpovoのLTEでも通信でき、物理ボタンで音楽を操作できる。

この過程では、成功したように見える状態を何度も切り分け直した。 実効UID 0でもデバイスを読めず、画像を書いてもパネルに出ず、PCMが進んでも無音で、開発用認証が通ってもSIMロックは残った。 そのたびに、APIの戻り値、ドライバーの状態、実際の表示や音を別々に確かめる必要があった。

新しいカーネルを移植したわけでも、汎用モバイルOSを完成させたわけでもない。 純正の機種依存部分を残し、その上に必要なユーザー空間とGUIを積み直すことで、手元の一台を音楽プレーヤーとして使えるところまで進められた。

公開資料

以下は調査で利用した公開資料である。 リンク先の提供状況やソフトウェアの仕様は変わる可能性がある。

端末と起動処理

Spotifyと通信

Debianで作った10GルータをNixOSで作り直した話

前回 Debian 13 で組んだ自宅 10G ルータの構成 (BCM57810 / VLAN / nftables / MAP-E / tinc) はそのまま、OS だけを NixOS 25.11 (unstable) に丸ごと書き直した 話です。作業日は 2026-03-19。

前回と同じく、複数のレイヤで書きます。

  • 比喩で全体像 (一般向け)
  • Web 系のエンジニアっぽい言い回し (Terraform / blue-green / immutable / atomic)
  • 実コマンド・version 番号・Nix モジュール片 (ネットワーク・OS 系)
  • NixOS の 基礎機能 (services.*, systemd.services.*, environment.etc."x".source, nixos-rebuild switch) を実例で押さえる

ソフトウェアエンジニア視点で言うと、これは 「本番 EC2 を手で運用していたのを Terraform + Ansible 相当に書き直した」 くらいのインパクトのある作業です。


なぜ動いてるものを作り直すのか

ルータ自体はちゃんと動いていました。インターネットは普通に出ていたし、宅内も 10Gbps 出ていた。それなのに作り直したのは、毎日使う分には問題ないけど、いじろうとした瞬間に大変だったから です。

辛みポイント 例えるなら アプリ開発で例えると
設定ファイルが 10 か所以上に散らばっている レシピが冷蔵庫・食器棚・引き出し・机の上に分散 リポジトリ無しで本番 EC2 に直接 vim
一部のソフトは自前ビルドで /opt や /usr/local に置いてある 自分で改造した家電を使ってる グローバルインストールされた CLI に依存、ロックファイル無し
半年前に何を変えたか忘れる カレンダーは残ってるけど「なぜそうしたか」が思い出せない 設定変更の PR が無い、commit message も歯抜け
別のパソコンで同じものを作れない 引っ越し先で同じ家を建てたくても、設計図が頭の中 デプロイ手順が口伝、新環境の bootstrap がブログ記事

特に最後がしんどい。ルータは絶対に止めたくない装置なのに、壊れたときの復旧プランが「がんばって思い出す」ではマズい。

Web アプリならカナリアデプロイで段階的に切り戻せます。ルータは設定ミスで自分の SSH 経路を切るので段階デプロイが効かない。だから 単一の宣言ファイル + アトミックな切り替え + ロールバック が効く NixOS、という選択は割と理に適っています。

NixOS って何 (3 行で)

  • OS 全体を Nix 言語で宣言的に書く ディストリ。/etc/nixos/configuration.nix がほぼ単一のソース
  • パッケージ管理が immutable + atomic (/nix/store/<hash>-<name>/... に世代ごとにインストール、切り替えは symlink 1 本の貼り替え)
  • 何かやらかしても起動メニューから前世代を選んで戻れる (= 物理マシン版の git revert)

nixos-rebuild switch というのが要するに terraform apply、nixos-rebuild switch --rollback が terraform apply で前の state に戻す感じ。直感的に近いです。

重要概念 (5 分で)

用語 ざっくり説明
module Nix 式で書かれた設定のかたまり。options (型付き設定項目) と config (実体) を定義する
evaluation Nix 式を評価して具体的なファイル群 (/nix/store/<hash>-<name>/) を生成すること
store path /nix/store/<hash>-<name> 形式の不変パス。同じ内容なら同じハッシュ
generation rebuild するたびに作られる「システム全体のスナップショット」。世代番号で管理
atomic switch 新世代への切替は symlink 1 本を貼り替える だけ。中途半端な状態にならない

/etc/nftables.conf のような「人間が直接書き換える設定ファイル」は NixOS には存在しない。代わりに /etc/nftables.conf は /nix/store/<hash>-nftables.conf への symlink として置かれていて、rebuild ごとに symlink 先が変わる、というイメージ:

$ ls -l /etc/nftables.conf
lrwxrwxrwx 1 root root 51 /etc/nftables.conf -> /nix/store/zk2p...n9-nftables.conf

Debian vs NixOS 高レベル比較

項目 Debian 13 NixOS 25.11
設定の中央集権 /etc/ 各地に散在 /etc/nixos/ 配下に集約
パッケージ管理 dpkg + apt (mutable) nix-store (immutable, per-generation)
サービス定義 systemctl edit + override.conf services.<name> または systemd.services.<name> を Nix で記述
ロールバック バックアップ復元 (祈り) nixos-rebuild switch --rollback (確実)
再現性 手順書 configuration.nix を別ハードに持っていけばほぼ同じ
ファイアウォール記述 /etc/nftables.conf を手書き networking.nftables.ruleset = ''...'' で文字列リテラル埋め込み
DNS AdGuard Home (deb 外) nixpkgs に AdGuard Home モジュール有り
DHCP /etc/kea/kea-dhcp4.conf を手書き services.kea.dhcp4.settings = { ... };
DHCPv6-PD クライアント wide-dhcpv6 odhcp6c に変更 (nixpkgs に wide-dhcpv6 無し)
tinc 自前ビルド (/usr/local/sbin/tincd) nixpkgs の pkgs.tinc_pre (1.1pre18)

ハードウェア・設計はそのまま

ルータ用のハード (BCM57810 10GbE × 2 + RTL8111 1GbE) は流用。 VLAN 設計 (移行時点で 6 本: 1001/1010/1083/1100/1110/1120)、nftables のルールセット、サブネット割り当て、tinc のノード名・鍵もそのまま。変えるのは OS と「どう書くか」だけ。(なおゲスト VLAN 1020 と別拠点 L2 用の 1130 は移行後に追加した。詳しくは末尾の後日談で。)

移行戦略 — ブルー/グリーンの物理版

  1. 別 SSD に NixOS を新規インストール (Debian 側のディスクは抜いて温存)
  2. /etc/nixos/configuration.nix + /etc/nixos/modules/*.nix で構成を書き上げる
  3. nixos-rebuild test → switch で完成形を作ってから、ONU と CRS305 のケーブルを Debian → NixOS 機に物理で差し替え
  4. 切り戻し = ケーブルを Debian 側に戻すだけ

要するに ブルー/グリーンデプロイの物理版。アプリだと ALB の重み変えるところを、ルータでは SFP+ ケーブルを引き抜いて差し替える。

家のたとえで言うと「作業中も既存のルータは生かしておく。最悪 ケーブルを挿し替える 30 秒 だけネットが止まる、という戦略」です。

ディスク・ブート構成

パーティション サイズ 用途
EFI (FAT32) 512MB systemd-boot
btrfs 残り全部 サブボリューム @, @nix, @var (compress=zstd)

NixOS の /nix/store は世代を持つので 同じバイナリの重複が大量に出る。btrfs の透過圧縮 + reflink 効果で実効容量がだいぶ稼げます。

モジュール分割 — 9 個のかたまり

NixOS では設定を 1 ファイルに書くこともできますが、長くなりすぎるので 9 個のかたまり に分けました (/etc/nixos/modules/*.nix)。

モジュール 内容 方式
network.nix VLAN 6 本 + sysctl NixOS native (networking.vlans)
nftables.nix 6 テーブルのファイアウォール NixOS native (networking.nftables.ruleset)
dns.nix AdGuard Home + Unbound NixOS native (services.adguardhome, services.unbound)
dhcp.nix Kea DHCP4 (5 サブネット, PXE) NixOS native (services.kea)
mape.nix MAP-E + Fixed IP トンネル + tc pedit カスタム (environment.etc + systemd.services)
ipv6.nix odhcp6c + radvd + assign-prefix カスタム systemd unit
routing.nix ポリシールーティング (ip rule) カスタム systemd unit
tinc.nix tinc VPN gsnet (pkgs.tinc_pre) NixOS native (services.tinc.networks)
monitoring.nix Netdata + iperf3 NixOS native + カスタム

configuration.nix から取り込むだけ:

{ config, pkgs, ... }: {
  imports = [
    ./hardware-configuration.nix
    ./modules/network.nix
    ./modules/nftables.nix
    ./modules/dns.nix
    ./modules/dhcp.nix
    ./modules/mape.nix
    ./modules/ipv6.nix
    ./modules/routing.nix
    ./modules/tinc.nix
    ./modules/monitoring.nix
  ];

  system.stateVersion = "25.11";
}

設計方針は 「NixOS が用意するサービスモジュールはなるべくそれを使う。合わなければ無理せず systemd.services.<name> でフラットに書く」 こと。Nix らしく抽象化しようとすると 1 行のシェルが 40 行の Nix 式になることがあって、Web 開発で言う 過剰な DRY と同じで保守性が逆に下がります。

「ここを直したいときはこのファイル」と決まっているので、半年後の自分でも迷わない。これだけで前回の 「設定がバラバラ問題」 が一気に解決しました。

設定パスの対応表 (Debian → NixOS)

NixOS 入門者にとって、これが頭に入ると Debian 時代の /etc/ 知識を NixOS 設定に投影できる。

Debian の設定パス NixOS 上での書き方 実体パス (rebuild 後)
/etc/network/interfaces networking.vlans, networking.interfaces systemd-networkd 設定 (Nix が生成)
/etc/nftables.conf networking.nftables.ruleset = '' ... ''; /etc/nftables.conf → store path
/etc/kea/kea-dhcp4.conf services.kea.dhcp4.settings = { ... }; /nix/store/<hash>-kea-dhcp4.conf
/opt/AdGuardHome/AdGuardHome.yaml services.adguardhome.settings = { ... }; /var/lib/AdGuardHome/AdGuardHome.yaml
/etc/unbound/unbound.conf.d/*.conf services.unbound.settings = { ... }; /nix/store/<hash>-unbound.conf
/etc/wide-dhcpv6/* (廃止: odhcp6c に移行) -
/etc/router-scripts/map-e-setup.sh environment.etc."router-scripts/map-e-setup.sh".source = ./scripts/map-e-setup.sh; /etc/router-scripts/map-e-setup.sh → store path
/etc/tinc/gsnet/ services.tinc.networks.gsnet = { ... }; /etc/tinc/gsnet/ (鍵は /var/lib/tinc/)
/etc/systemd/system/iperf3-gsnet.service systemd.services.iperf3-gsnet = { ... }; /etc/systemd/system/iperf3-gsnet.service → store path

「すべて configuration.nix から派生する」 のがポイント。

Debian → NixOS で実際に書き換えた点

NixOS のエコシステムと衝突する箇所を、移行のついでに書き換えました。casual な言い方をすると 「同じ機能を、別の流儀で書き直すだけ」 の作業です。

1. wide-dhcpv6 → odhcp6c

nixpkgs に wide-dhcpv6 パッケージが存在しない。OpenWrt 由来の odhcp6c に乗り換え。/56 を取得したら hook を叩くだけのシンプルな実装で、assign-ipv6-prefix.sh 側の I/F は同じにしたためロジック無修正。

# modules/ipv6.nix
systemd.services.odhcp6c = {
  description = "DHCPv6 PD client";
  wantedBy = [ "multi-user.target" ];
  serviceConfig.ExecStart = "${pkgs.odhcp6c}/bin/odhcp6c -P 56 -s /etc/router-scripts/odhcp6c-hook.sh enp1s0f0";
  serviceConfig.Restart = "always";
};

2. /etc/wide-dhcpv6/ → /etc/router-scripts/

スクリプト置き場改名。NixOS 上は environment.etc で配置:

environment.etc."router-scripts/assign-ipv6-prefix.sh" = {
  source = ./scripts/assign-ipv6-prefix.sh;
  mode = "0755";
};

これで /etc/router-scripts/assign-ipv6-prefix.sh がシンボリックリンクとして配置される。「設定ファイルは store の中で immutable、/etc には link を貼るだけ」というのが Nix の流儀。

3. nftables の ipencap → プロトコル番号 4

これが地味に痛かった。WAN (enp1s0f0) で 4in6 (MAP-E と ipip-fixed underlay の両方) を受ける行を Debian では ip6 nexthdr ipencap と書いていた。NixOS で nixos-rebuild を回すと:

error: ... nftables-check-...
/nix/store/.../ruleset.nft:NN:M-M: Error: Could not resolve protocol name
    ip6 nexthdr ipencap accept
                ^^^^^^^

で落ちる。services.nftables.checkRuleset = true (デフォルト) がサンドボックス derivation 内で nft --check を回しており、そこで symbolic name 解決に失敗する。

最初は「サンドボックスで /etc/protocols が読めない」のかと疑ったが、調べると runtime の /etc/protocols 自体は見えていて、中身が違う のが本当の原因だった:

ディストロ /etc/protocols 提供元 proto 4 のエントリ
Debian netbase パッケージ ipv4 + ipencap (別名)
NixOS iana-etc (IANA 正式名のみ) ipv4 のみ (ipencap は無い)

nftables の nft は symbolic name を getprotobyname(3) 経由で解決するので、NixOS では ipencap を引けない。

- ip6 nexthdr ipencap iifname "enp1s0f0" accept
+ ip6 nexthdr 4       iifname "enp1s0f0" accept

「プロトコル番号 4 (IP-in-IP / RFC 2003 で命名、ここでは IPv4-in-IPv6 = RFC 2473 の inner)」をベタ書きすることで解決。ip6 nexthdr ipv4 と書いても通る (こちらは IANA 名で NixOS にも存在) が、「symbolic 名は環境差で詰む」と一度刺されたので、以後は GRE (47) / ESP (50) / IPv4 (4) / IPv6 (41) はすべて番号で書く のを鉄則にした。

これは 依存解決の世界では「環境に置いてある名前 (= グローバル変数) を参照する習慣」が NixOS の iana-etc で剥がれる、という典型例。Docker でベースイメージを debian から alpine に変えたら iproute2 の細部が違って死ぬのに近い。

services.nftables.checkRuleset: NixOS は nftables.ruleset を渡すと、activation の前に サンドボックス derivation で nft --check を走らせて構文・symbolic 名解決を検証する。ここで落ちると nixos-rebuild switch 自体が失敗するので、壊れた ruleset が /etc/nftables.conf に降りて WAN を切る ような事故が原理的に起きない。Debian で nft -f /etc/nftables.conf の reload に失敗してネットが死ぬパターンが、構造的に消える。

4. enp2s0 管理用ルール

NixOS 機の初期構築は管理 NIC 経由 SSH 想定なので、enp2s0 用の SSH 許可ルールを input chain に追加。Debian 時代は雑に enp2s0 を accept していたが、宣言的に書くなら明示せざるを得ない。

5. Kea の Dhcp4 ネスト除去

services.kea.dhcp4.settings は内部で Dhcp4 キーを自動付与する。Debian の kea-dhcp4.conf をそのまま貼ると {"Dhcp4":{"Dhcp4":{"subnet4":...}}} の二重ネストで起動しない:

services.kea.dhcp4 = {
  enable = true;
  settings = {
    interfaces-config = { interfaces = [ "enp1s0f1.1010" "enp1s0f1.1100" ... ]; };
    subnet4 = [ ... ];
    hooks-libraries = [ { library = "${pkgs.kea}/lib/kea/hooks/libdhcp_lease_cmds.so"; } ];
  };
};

Dhcp4 階層を 1 段剥がして書く。OpenAPI generator が prefix を勝手に付けるのと同じ罠で、生成系のフレームワークが二重に suffix/prefix を付けて死ぬ、というよくある奴。

ちなみに pkgs.kea から自動で hook ライブラリの store path を引ける のが便利。Debian だと /usr/lib/x86_64-linux-gnu/kea/hooks/... を手で書いていたが、NixOS では store path が hash 込みで決まるので、pkgs.kea の式から動的に組み立てる。

6. tinc は nixpkgs の tinc_pre

pkgs.tinc_pre (1.1pre18) が nixpkgs に居るので自前ビルドを廃止。Debian 時代の最大の負債だった「tinc 1.1pre の自前ビルド + systemd override」がここで消える:

services.tinc.networks.gsnet = {
  package = pkgs.tinc_pre;        # ★ 1.1pre18
  name = "aibauiha";
  interfaceType = "tap";          # Mode = switch
  listenAddress = "0.0.0.0";

  extraConfig = ''
    Mode = switch
    ConnectTo = gsngw01
    ConnectTo = suzukautako
    ConnectTo = linuweb
    ConnectTo = kimon
    ConnectTo = rabbit_house
  '';
};

鍵 (rsa_key.priv, ed25519_key.priv, hosts/*) は Debian ルータから scp で /var/lib/tinc/gsnet/ にコピー。 tinc 1.0.36 (Debian 公式) では SPTPS / ChaCha-Poly1305 が使えない。1.1pre 同士なら UDP 経路の暗号が軽くなる期待があり、10G メッシュなら効くだろう、というのが 1.1pre を入れた動機。全ピアが 1.1pre になっていないと新プロトコルに乗らない (= レガシー側に落ちる経路はメリット無し) ので、効果は経路次第。

「自前ビルド + systemd override + service ファイル」という 3 ファイルの密結合が パッケージ名 1 個に圧縮された のは、移行効果の中でも一番気持ち良かった部分。

NixOS 入門者向け: 実モジュールの中身

何箇所か、入門者が 「あぁ Nix ってこう書くんだ」 と分かる例として中身を出しておきます。

network.nix — VLAN を NixOS で書く

{ ... }:
let
  vlanIds = [ 1001 1010 1083 1100 1110 1120 ];
in {
  networking.useDHCP = false;
  networking.interfaces.enp1s0f0.useDHCP = false;

  # VLAN サブインタフェースを 6 本まとめて生成
  networking.vlans = builtins.listToAttrs (map (vid: {
    name = "enp1s0f1.${toString vid}";
    value = { id = vid; interface = "enp1s0f1"; };
  }) vlanIds);

  # 各 VLAN GW IP (.1) を割り当て
  networking.interfaces = builtins.listToAttrs (map (vid: {
    name = "enp1s0f1.${toString vid}";
    value = {
      ipv4.addresses = [{
        address = "192.168.${toString (vid - 1000)}.1";
        prefixLength = 24;
      }];
    };
  }) [ 1001 1010 1100 1110 1120 ]);

  boot.kernel.sysctl = {
    "net.ipv4.ip_forward" = 1;
    "net.ipv6.conf.all.forwarding" = 1;
  };
}

Debian の /etc/network/interfaces を 6 ブロック手書きしていた分が、リストの上を map するだけ で済みます。

nftables.nix — ヒアドキュメントで全テーブル定義

{ ... }: {
  networking.nftables.enable = true;
  networking.nftables.ruleset = ''
    table inet filter {
      chain forward {
        type filter hook forward priority filter; policy drop;
        iifname "enp1s0f1.1010" oifname "map-wan" accept
        iifname "enp1s0f1.1010" oifname "enp1s0f1.1100" accept
        iifname "enp1s0f1.1010" oifname "enp1s0f1.1110" tcp dport { 80, 443 } accept
        # ...
      }
    }
    table ip mangle {
      chain forward {
        type filter hook forward priority -150;
        oifname "map-wan" tcp flags syn tcp option maxseg size set 1414
      }
    }
    # 残り 4 テーブルも同様
  '';
}

要するに nftables の生ルールを文字列リテラルで埋め込めるだけ の薄いラッパー。これくらいで十分。 この文字列が /nix/store/<hash>-nftables.conf として store に書き出され、/etc/nftables.conf から symlink で参照される。手で書き換えても rebuild すると symlink が上書きされて元に戻る = 設定の真実は常に .nix ファイル。

dns.nix — AdGuard Home と Unbound を 2 行で

{ ... }: {
  services.adguardhome = {
    enable = true;
    mutableSettings = true;  # WebUI で変更を許可 (= rebuild で潰さない)
    settings = {
      dns = {
        bind_hosts = [ "0.0.0.0" "::" ];
        port = 53;
        upstream_dns = [ "127.0.0.1:5335" ];  # Unbound
      };
      filtering.rewrites = [
        { domain = "*.chun37.com"; answer = "192.168.120.105"; }
      ];
    };
  };

  services.unbound = {
    enable = true;
    settings.server = {
      interface = [ "127.0.0.1" ];
      port = 5335;
      access-control = [ "127.0.0.0/8 allow" ];
      do-ip6 = "yes";
      harden-glue = "yes";
    };
  };
}

mutableSettings = true を付けると、WebUI 経由の変更を /var/lib/AdGuardHome/AdGuardHome.yaml に書いて永続化する (rebuild で潰されない)。リライト等のリスト操作を WebUI でやりたいときに便利。

monitoring.nix — ${pkgs.iperf3} で store path 埋め込み

{ pkgs, ... }: {
  services.netdata.enable = true;

  systemd.services.iperf3-gsnet = {
    description = "iperf3 server bound to gsnet address";
    after = [ "tinc.gsnet.service" ];
    requires = [ "tinc.gsnet.service" ];
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      ExecStart = "${pkgs.iperf3}/bin/iperf3 -s -B 10.24.34.2";
      Restart = "on-failure";
    };
  };
}

${pkgs.iperf3}/bin/iperf3 は iperf3 パッケージの store path を Nix が評価時に解決して埋め込む。Debian で /usr/bin/iperf3 と書いていた感覚に近いが、パッケージ更新時にパスが自動で追従する。

nixos-option で型・現在値を確認

# 「services.kea.dhcp4.enable は何?」を Nix 式の中に入る前に調べる
nixos-option services.kea.dhcp4.enable
# → 型, デフォルト値, 実際にどう設定されているか, 設定された場所(.nix:行) が出る

# REPL で nixpkgs の中身を覗く
nix repl '<nixpkgs>'
nix-repl> :l <nixpkgs/nixos>
nix-repl> config.services.kea.dhcp4.settings

rebuild ワークフロー

NixOS の運用は基本的にこのループ:

┌──────────────────────────────────────────────────────────────┐
│  1. /etc/nixos/modules/*.nix を編集                            │
│           │                                                  │
│  2. nix flake check (構文・型の事前チェック; flake 化していれば)  │
│           │                                                  │
│  3. nixos-rebuild dry-build  (ビルドのみ、活性化しない)          │
│           │                                                  │
│  4. nixos-rebuild test       (起動するが boot loader に書かない) │
│           │                                                  │
│  5. nixos-rebuild switch     (活性化 + boot loader 登録)         │
│           │                                                  │
│  6. (壊れた場合) nixos-rebuild switch --rollback                │
└──────────────────────────────────────────────────────────────┘

世代管理コマンド:

# 世代一覧
nix-env --list-generations -p /nix/var/nix/profiles/system
#  120   2026-03-19 12:00:01   (current)
#  119   2026-03-18 22:30:11

# 特定世代に切替
nixos-rebuild switch --switch-generation 119

# 古い世代の掃除 (30日より古いものを削除)
nix-collect-garbage --delete-older-than 30d

ルータでこの「壊れたら 1 コマンドで戻せる」が効くのは精神安定上とても大きい。深夜の作業で一番怖い「物理アクセスを伴うレスキュー」を回避できる。

物理ケーブル切替

一番ドキドキする瞬間。

[Phase A: 切替前]
  ONU ── (SFP+ DAC) ── Debian Router (enp1s0f0)
                                     │
                         CRS305 ── trunk ── Debian Router (enp1s0f1)

[Phase B: 切替中 (~30 秒)]
  ONU                  Debian Router (停止)
        \
         \             NixOS Router  (起動済み)
          \_____ (DAC 差し替え) _____/

[Phase C: 切替後]
  ONU ── (SFP+ DAC) ── NixOS Router (enp1s0f0)
                                    │
                         CRS305 ── trunk ── NixOS Router (enp1s0f1)
  1. Debian ルータの全サービス停止
  2. ONU → NixOS enp1s0f0 差し替え
  3. CRS305 trunk → NixOS enp1s0f1 差し替え
  4. NixOS 側で odhcp6c が DHCPv6-PD /56 取得 → MAP-E + Fixed IP + IPv6 SLAAC 自動構築

所要時間 30 秒。スマホの IPv6 が新プレフィクスに更新されるのを横目で見ながら確認:

$ curl -4 ifconfig.io
198.51.100.140
$ curl -4 --interface ipip-fixed ifconfig.io
203.0.113.80
$ curl -6 ifconfig.io
2001:db8:1010::1

検証結果

主要な機能をチェックリストで:

項目 結果
IPv4 MAP-E (VLAN 1010) 198.51.100.140
IPv4 Fixed IP 203.0.113.80
IPv6 (GUA) 2001:db8:1010::1
AdGuard Home → Unbound OK
DNS rewrite (*.chun37.com) 192.168.120.105
Kea DHCP4 active (5 サブネット)
tinc VPN (gsnet) active, 10.24.34.2 疎通
radvd (IPv6 SLAAC) active
nftables (6 テーブル) active
Netdata active (HTTP 200)

全部 OK。家族からの「ネット遅い」コールも無し。

ハマり集 — 4 件

実質ぶつかったのは下記の 4 件です。

ハマり 1: enp2s0 のルーティング競合

切替直後、IPv4 で 192.168.10.1 に SSH 不可。

# ip route
default via 192.168.10.254 dev enp2s0  proto static
192.168.10.0/24 dev enp2s0  proto kernel  scope link  src 192.168.10.1
192.168.10.0/24 dev enp1s0f1.1010  proto kernel  scope link  src 192.168.10.1

enp2s0 (linkdown) が 192.168.10.0/24 のルートと defaultGateway を持っていた。network.nix から enp2s0 の設定と networking.defaultGateway を削除して nixos-rebuild switch で復旧。

学び: NixOS は networking.interfaces.<n>.ipv4.addresses を書いた瞬間に link-scope route を載せる。linkdown でも route table には残るので、初期構築用の管理 NIC は構築後ちゃんと撤去すること。

ハマり 2: nixos-rebuild switch で IPv6 GUA がフラッシュ

switch の度に VLAN サブインターフェース上の IPv6 GUA が消える。NixOS のネットワーク再設定 (network-addresses-*.service の再 ExecStart) で ip addr flush 相当が走るため。fe80::/10 の link-local は残るが GUA だけ消えるという挙動。

当面の運用回避:

nixos-rebuild switch
/etc/router-scripts/assign-ipv6-prefix.sh /tmp/last-pd-prefix

恒久対策は assign-ipv6-prefix.service を partOf / WantedBy=network-addresses-enp1s0f1.1010.service で連動させる予定 (これは別記事ネタ)。

ハマり 3: Kea DHCP4 の LPF ソケットロスト

nixos-rebuild switch 後に kea-dhcp4 の LPF (Linux Packet Filter, AF_PACKET) ソケットが無効化されて DHCP 応答が返らなくなることが稀にある。

ERROR [kea-dhcp4.dhcp4] DHCP4_PACKET_RECEIVE_FAIL failed to receive ...

Kea は I/F の up/down に追随しないので、再起動が必要:

# systemctl restart kea-dhcp4-server

Kea 側の挙動であって NixOS 起因ではないが、宣言的に書くからこそ rebuild 頻度が増えて踏みやすくなる。reloadTriggers ではなく restartTriggers に I/F 関連を含めるのが正解。

ハマり 4: Android の DoT (TCP 853) 不許可で AAAA 抑制

Android の「プライベート DNS: 自動」が tcp/853 で DoT 接続を試み、失敗すると AAAA クエリを抑制する 仕様。AdGuard Home は :53 でしか受けていなかったので、Android からだけ IPv6 名前解決が壊れた。

networking.firewall.allowedTCPPorts = [ ... 853 ];
# 加えて nftables input chain にも明示
# tcp dport 853 accept

を入れて解決。NixOS 移行とは直接無関係だが、Debian 時代に気付けなかった既存バグでもある。

切替後に得たもの

Before (Debian) After (NixOS)
設定が 10 箇所超に散在 /etc/nixos/ 配下に集約、git diff で読める
APT 外パッケージは自前管理 pkgs.tinc_pre / pkgs.adguardhome で nix flake pin 可
バックアップ復元が祈り 別ハードに configuration.nix を持っていけば再現
切戻し = apt downgrade + 設定戻し nixos-rebuild switch --rollback 一発
apt full-upgrade が怖い チャンネル更新は意図したタイミングだけ

特に 「ロールバック (= 1 つ前に戻す)」が 1 コマンド なのは、ルータを触る心理的ハードルが激減しました。

移行後の進化 — NixOS で気軽に弄れるようになった例

NixOS 化してから、移行前ならとても踏み込めなかった変更を躊躇なく入れられるようになりました。 各機能を「1 モジュールの追加 or 差分」で完結できる のが効いている。代表的な後日談 4 件だけ:

後日談 日付 NixOS 上で起きたこと
VLAN 1020 (ゲスト) 新設 2026-04-30 network.nix の vlanIds リストに 1020 追加 + nftables にゲスト隔離ルール追加。物理 AP の SSID 対応含めて 1 モジュールの diff
chrony 化 2026-04-14 services.timesyncd.enable = false; + services.chrony 有効化。CMOS 電池劣化で 起動時に RTC が 2017 年に巻き戻る事故 対策込み
vxlan-trunk 2026-06-07 〜 06-08 別拠点 (川越) との L2 ブリッジ (wg-remote underlay + vxlan1130 + VLAN 1130 で先行構築済み) を「単一 VLAN」から マルチ VLAN 対応 に拡張。br-trunk を VLAN-aware bridge にして 7 VID を per-VNI VXLAN でカプセル化
MSS clamp 両方向化事件 2026-06-08 vxlan-trunk 越しの WebSocket が 片方向 MSS clamp の見落とし で RST されるバグ。illust-notifier 不通で発覚

それぞれ、Debian 時代だったら 「動いてるルータを触りたくない」で先送り していたはず。詳しくは個別記事で書きますが、最後の MSS clamp 両方向化事件 だけは前回からの伏線回収なので少しだけ触れておきます。

後日談プレビュー: MSS clamp 両方向化事件

vxlan-trunk 越しに WebSocket セッションが長時間後に RST される 事象。原因は MSS clamp が片方向だけ (oifname 側) しか書かれていなかった こと:

  • SYN は通る (3-way OK)
  • 内側ホスト発信の SYN は MSS=1460 のまま出ていく
  • 相手側が「ピアの MSS は 1460」と記憶
  • データ送信時に MSS=1460 の TCP セグメントが vxt-<VID> (MTU 1366) で flood ドロップ
  • 長時間後に TCP RST

establish できるので一見正常に見えるのが厄介。修正は iifname と oifname の 両方 で MSS clamp する 2 行を nftables.nix に追加 → nixos-rebuild switch で即反映。この「即反映 → 駄目なら rollback」がすぐ手の届くところにあるから攻めた変更を試せる、という典型例でした。

まとめ — どの読者にも刺してほしい話

casual な読者へ: 「動いてるルータを別の OS で書き直す」は無謀に聞こえますが、要点は 「物理ケーブルだけ挿し替えて切戻し可能」 という戦略を最初に組んだことです。30 秒のダウンタイムで、何かあったら元に戻せる。家のインフラを攻めるときも、カナリア・ブルーグリーンの考え方 は使えます。

ソフトウェアエンジニアへ: ルータも infra-as-code の範疇です。nixos-rebuild switch = terraform apply、--rollback = terraform state rollback、services.<name> = Terraform module。Web 開発で当たり前にやっている「設定を PR、ロールバック可能、別環境で再現」がそのまま物理ルータに適用できる、というのが NixOS の現実的な意義です。

ネットワークエンジニアへ: VLAN / nftables / MAP-E / tc pedit / tinc 1.1pre / DHCPv6-PD という、業務機器なら別々のベンダ機を組み合わせる構成が、全部 1 つの宣言ファイルから生える 形になります。/etc/nftables.conf を手で nft add rule する運用バグが構造的に消える、/etc/protocols のような暗黙依存がサンドボックスで剥がれる、Kea の hook ライブラリパスが ${pkgs.kea} から動的に生える、といった「Debian で当たり前に踏んでいた地雷」が一通り解除されます。一方で「NIC 設定の link-scope ルートが残る」「rebuild で IPv6 GUA が消える」「Kea LPF ソケットがロスト」のような NixOS 固有の罠 が代わりに生えるので、トレードオフは見ておくべき。

NixOS 入門者へ: ルータという題材を通して、以下の NixOS 基礎機能を一通り使いました。

  • imports でモジュール分割
  • networking.* で IF・VLAN・nftables を宣言
  • services.<name> で既製サービス (Kea, AdGuard Home, Unbound, tinc, Netdata, radvd) を有効化
  • environment.etc."path".source でファイル配置
  • systemd.services.<name> で自前 unit を定義
  • pkgs.<name> でパッケージ参照、${pkgs.foo}/bin/foo で store path を埋め込み
  • nixos-rebuild switch / test / dry-build / --rollback で運用
  • nixos-option, nix repl '<nixpkgs>' で型と現在値を確認

「設定をコードで書き、ビルドして atomic に切替、壊れたら 1 コマンドで戻す」。これが体感できると、ルータ以外のサーバも NixOS で組みたくなります。続編 (vxlan-trunk, chrony, MSS clamp 両方向化など) はそれぞれ別記事で書く予定です。

Debianで10Gルータを作った話

家のインターネットの "玄関口" にあたる ルータ を自作した話を書きます。 一般の家庭で言うところの「プロバイダから渡される機械」「家電量販店で買う Wi-Fi ルータ」を、自分で PC パーツから組み立てて Debian で動かしました。

なぜそんな面倒なことをしたのか、何を頑張ったのか、どこに刺さってどう抜けたのかを、レイヤを分けて書きます。

  • まず比喩で全体像
  • 次にソフトウェアエンジニアっぽい言い回し (K8s Namespace / CORS / infra-as-code) で再説明
  • そのうえで実コマンド・version 番号・nftables ルール片で詰める

途中ところどころに 「(NixOS なら…)」 という囲みを挟みました。続編で同じ構成を NixOS に書き直すので、その伏線です。

この記事は Debian 13 trixie (13.2) で組んだ初代 の話。NixOS 版は次の記事で書きます。


なぜ自作したのか

家には少し変わった事情があります。

項目 普通の家 うち
インターネット側 (WAN) の速度 1Gbps 10Gbps
家の中 (LAN) の速度 だいたい 1Gbps 10Gbps
構成 パソコン・スマホ・テレビ ↑に加えてストレージサーバや Proxmox 数台

宅内のスイッチ間は 10G で組んでいて、ストレージサーバや Proxmox 間の通信、別拠点との L2 VPN ブリッジなど、「ルータを跨ぐ通信」が容赦なく 10G で飛んできます。家電量販店で売っている家庭用ルータは UTM (= セキュリティ機能) を有効化すると 1G すら出ない型番がほとんど、SFP+ を生で扱える小型市販品もまだ高価。それなら自作した方が早い、というのが今回の動機です。

ソフトウェアエンジニアの感覚に翻訳すると「マネージドサービスは高い & 機能が足りない & ベンダロックインがキツい、なら自前で書く」のと同じ判断。回線契約は enひかり (NTT フレッツ光クロス網経由) の 10Gbps、IPv6 は IPoE で v6プラス、IPv4 は v6プラスの MAP-E (通常用、NAT 共有) と enひかり固定IPオプションの IP-in-IP トンネル (外公開用、固定グローバル 1 本) の 2 本立て。WAN は v6 ネイティブの上に IPv4 をトンネルで載せる構造です。

ハードウェア

筐体は ヤフオクで適当に拾ってきた Dell OptiPlex のスリムタワー。中古の法人払い下げ品で、しょっちゅう投げ売られている定番のやつです。これに PCIe で 10G NIC を 1 枚刺すだけ。

部品 型番 役割
筐体 Dell OptiPlex スリムタワー (中古) 本体
NIC #1 Broadcom BCM57810 (SFP+ 10GbE × 2) WAN + LAN トランク
NIC #2 Realtek RTL8111 (1GbE, オンボード) 管理用 (initial SSH)
ストレージ SATA SSD 465.8GB OS
OS Debian 13 trixie (13.2)

BCM57810 は中古市場で投げ売られている Dell の OEM カードで、bnx2x ドライバが Debian 標準カーネルに in-tree。apt install 不要でインストール直後から見えます。ethtool -k でオフロードを覗くと素直に乗っている:

# ethtool -k enp1s0f1 | egrep 'tcp-seg|gso|gro|tx-checksum|rx-checksum|rx-vlan|tx-vlan'
rx-checksumming: on
tx-checksumming: on
generic-segmentation-offload: on
generic-receive-offload: on
tx-tcp-segmentation: on
tx-tcp6-segmentation: on
rx-vlan-offload: on
tx-vlan-offload: on

1 ポートを ONU (フレッツ光クロスの 10G-EPON ONU) に、もう 1 ポートを コアスイッチ (MikroTik CRS305) に VLAN トランクで繋ぐ最小構成です。

                       +--------------------+
   10G-EPON ONU ───────┤ enp1s0f0  (WAN)    │ IPoE / DHCPv6-PD / MAP-E underlay
                       │                    │
                       │  Debian 10G Router │
                       │                    │
                       │ enp1s0f1  (LAN     ├──── CRS305 ──── 各 VLAN
                       │           trunk)   │        │
                       │ enp2s0   (mgmt)    │        ├── AP (Aruba)
                       +--------------------+        ├── Proxmox host
                                                    └── Workstation

予算はジャンク寄りの中古を組み合わせて 1 万円ちょっと。

ネットワーク設計 — VLAN で「区画分け」

宅内は機能ごとに VLAN (IEEE 802.1Q) で完全に分離しています。

家に例えると「同じ家の中だが、ゲスト用の玄関先・リビング・配電盤・物置を別扱いにする」という感覚。ソフトウェアエンジニア向けの言い方をするなら Kubernetes の Namespace に似てる: 物理的には同じクラスタ (= スイッチ + ケーブル) を共有しているけど、論理的には別の宇宙で、互いに触れない。

VID 名前 サブネット 用途
1001 mgmt 192.168.1.0/24 管理 / 工事用
1010 LAN 192.168.10.0/24 通常クライアント (信用ドメイン)
1083 gsnet 10.0.0.0/8 拠点間 L2 VPN
1100 server 192.168.100.0/24 Proxmox / Ceph public
1110 ipmi 192.168.110.0/24 IPMI / BMC (完全隔離)
1120 vm 192.168.120.0/24 VM / LXC コンテナ

ルータの物理 IF は enp1s0f0 (WAN: ONU) と enp1s0f1 (LAN: トランク)。 LAN 側は enp1s0f1.<VID> の 8021q サブインターフェース で終端し、それぞれの VLAN GW として <subnet>.1 を割り当てる、シンプルな構成です。

/etc/network/interfaces 抜粋:

auto enp1s0f1
iface enp1s0f1 inet manual
    up ip link set $IFACE up

auto enp1s0f1.1010
iface enp1s0f1.1010 inet static
    address 192.168.10.1/24
    vlan-raw-device enp1s0f1

# 他 VLAN 同様、5 本ぶん延々と書く

Linux 上ではこう見えます。

# ip -br link show type vlan
enp1s0f1.1001    UP    ...
enp1s0f1.1010    UP    ...
enp1s0f1.1083    UP    ...    master br-gsnet
enp1s0f1.1100    UP    ...
enp1s0f1.1110    UP    ...
enp1s0f1.1120    UP    ...

(NixOS なら…) networking.vlans."enp1s0f1.1010" = { id = 1010; interface = "enp1s0f1"; }; と networking.interfaces."enp1s0f1.1010".ipv4.addresses = [{ address = "192.168.10.1"; prefixLength = 24; }]; を並べるだけ。 mapAttrs を使えば VLAN 定義は 6 行のリスト から自動展開できる。続編の network.nix で実例を出します。

ファイアウォール: nftables

iptables ではなく nftables を選びました。理由は 3 点。

  1. テーブルを機能ごとに分けて書ける (機能別にレビュー可能)
  2. inet ファミリで v4/v6 を一本化できる
  3. ベンチで「ルールが増えても n でほぼ線形」(雪崩しない)

書いた人の感覚で言うと、iptables が古い手書き Apache httpd.conf なら nftables は今風の YAML 設定 + 型付き DSL。

テーブル構成

Table Family 役割
inet filter inet input / forward / output、すべて policy drop
ip nat ip MASQUERADE (map-wan) + DNAT
ip mangle ip MSS clamp (MAP-E 越え 1460 → 1414)
inet raw inet TFTP conntrack helper (PXE boot)
ip caddy ip 固定 IP (203.0.113.80) → CT 105 Caddy DNAT
ip asterisk ip 固定 IP RTP UDP 10000-10010 → CT 144 DNAT
# nft list table inet filter | head
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        iifname "lo" accept
        ct state established,related accept
        ip6 nexthdr icmpv6 accept
        ip protocol icmp accept
        iifname { "enp1s0f1.1010", "enp1s0f1.1100", "enp1s0f1.1120" } tcp dport 22 accept
        ...
    }
}

VLAN フォワード ホワイトリスト

policy=drop なので、明示許可だけが通る。家のたとえで言うと「ゲスト用玄関先からはインターネットへは出られるが、リビング (LAN) や書斎 (サーバ) へは入れない」。Web 開発で言うなら CORS や RBAC のホワイトリスト に近い感覚です。

From To Proto / Port 用途
1010 (LAN) map-wan any 通常 LAN → Internet (MAP-E)
1010 10.0.0.0/8 any tinc 越しの拠点間
1010 1100 (server) any サーバ管理
1010 1120 (vm) tcp/22, udp/69, tcp/80 SSH / TFTP (PXE) / CT 130 HTTP
1010 1110 (ipmi) tcp/{80,443} BMC Web のみ
1100 1120 udp/69 TFTP (PXE)
1120 (CT 130) 1100 any Ceph RGW / PostgreSQL
1001 / 1100 / 1120 ipip-fixed any 固定 IP 出口
ipip-fixed 1120 (CT 105) tcp/{80,443} Caddy 受け
ipip-fixed 1120 (CT 144) udp/10000-10010 Asterisk RTP
gsnet 1120 (CT 144) udp/{5060, 10000-10010} SIP トランク
1110 * — drop (BMC 完全隔離)

実装は iifname "enp1s0f1.<VID>" oifname "enp1s0f1.<VID>" ... でベタ書き。ルール数 200 行程度なら見通しは保てます。

後年の罠の伏線: ipencap

WAN 入口 (enp1s0f0) で 4in6 (= MAP-E と ipip-fixed underlay の両方) を受け入れるルールを ip6 nexthdr ipencap と 名前指定 で書いている。Debian は netbase パッケージが /etc/protocols に ipencap を proto 4 の別名として登録してくれる ので動く。NixOS の iana-etc (IANA 正式名のみ) には ipencap は無く ipv4 しか登録されていない ので、移行時に Could not resolve protocol name "ipencap" で nixos-rebuild が落ちます。続編で数値 4 への書き換えに繋がる伏線。

(NixOS なら…) networking.nftables.ruleset = '' ... ''; のヒアドキュメントに 6 テーブルぶん全部書く。 nixos-rebuild switch を通さないと反映されない = 手で nft add rule で足す運用が物理的に不可能になる。 Debian 時代に頻発していた「ファイルと実行状態の乖離」というバグ クラスが、構造的に消えます。

MTU バジェット — 家庭ルータと業務ルータの差が一番出る部分

トンネルを重ねるとパケットの「載せられる中身」が縮みます。MAP-E 越えと tinc 越えで MTU が違うので、整理しておきます。

                              +-- MAP-E -------- 1460 (MSS clamp で 1414)
  WAN 1500 (Ethernet) -------+
                              +-- ipip-fixed --- 1460 (4in6 / ip6tnl, IPv6 ヘッダ 40B)
                              |
                              +-- tinc (TAP) --- 1420 (UDP/IPv6 overhead)

  LAN trunk 1500 ------------- enp1s0f1.<VID> 1500

MSS clamp は ip mangle の POSTROUTING に MAP-E 出口で 1 行入れて済ませています。「SYN パケットの MSS option を書き換えて (= TCP のレイヤで宿主が大きいパケットを送ってこないように予防接種を打つ)」イメージ。

# nft list table ip mangle
table ip mangle {
    chain postrouting {
        type filter hook postrouting priority mangle; policy accept;
        oifname "map-wan" tcp flags syn tcp option maxseg size set 1414
    }
}

この時点では「内向きから外向き 1 方向」しか書いていません。後日 vxlan-trunk 越しで MSS clamp 両方向化を強いられる事件が起きる ので、これも伏線。詳細は次の記事で。

DNS: AdGuard Home + Unbound の二段構え

「グーグルにアクセスして」と言われたとき、コンピュータは google.com を機械が分かる住所 (IP アドレス) に変換します。これを DNS と呼びます。

家のルータでは 2 段構えにしました。

クライアント ──► AdGuard Home (0.0.0.0:53 / [::]:53, AAAA on)
                    │ (上流に問い合わせ)
                    ▼
                Unbound (127.0.0.1:5335)
                    │ (再帰解決 + DNSSEC 検証)
                    ▼
                ルート DNS / 権威 DNS
役割 担当 何をするか
前段 (受付) AdGuard Home v0.107.71 広告フィルタ、アクセスログ、クライアント別ポリシー、DNS rewrite
後段 (再帰) Unbound 1.22.0 本物のキャッシュ、再帰、DNSSEC 検証、内部 RR (local-data)

*.chun37.com (宅内向けワイルドカード) は AdGuard 側の DNS rewrite で 192.168.120.105 (Caddy CT) に振っています。ベアドメイン chun37.com は GitHub Pages を指したいので、AdGuard 側で書き換えずに上流に透過。

ハマり: Unbound の local-data ワイルドカード非対応

最初は Unbound 側の local-data だけで *.chun37.com を返そうとしましたが、Unbound の local-data はワイルドカードを展開してくれない。1 つずつ FQDN を列挙する必要があります。一方 AdGuard Home の rewrite はワイルドカード OK。ここで結構ハマって AdGuard 側に寄せました。

Debian での導入の苦しみ

# Unbound は APT
apt install unbound
vi /etc/unbound/unbound.conf.d/local-records.conf

# AdGuard Home は APT 外 (公式インストーラ)
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v
# → /opt/AdGuardHome/ に展開、systemd unit を install.sh が自前で書き込む
vi /opt/AdGuardHome/AdGuardHome.yaml

AdGuard Home の アップデートは GUI からポチる必要があり、apt upgrade の対象外。「APT 外のソフトを覚えていないと、年単位でアップデート漏れする」という運用負債の典型例。

(NixOS なら…) services.adguardhome.enable = true; と services.unbound.enable = true; の 2 行。 AdGuard Home のバイナリは pkgs.adguardhome で nixpkgs から取得 → APT 外という概念が消える。 上流のバージョン追跡は nixpkgs (実体は GitHub Actions の bump bot) に丸投げできる。

DHCP: Kea + 自作 WebUI

家の中の機器に「あなたの住所はこれだよ」と渡す係を DHCP と呼びます。これも Kea (ISC DHCP の後継) という Linux のソフトに任せています。Web エンジニアに馴染みのある JSON 設定 + REST 風 API (Control Agent, 127.0.0.1:8000) で操作できるのが最高。

# systemctl status kea-dhcp4-server
● kea-dhcp4-server.service - Kea IPv4 DHCP daemon
     Active: active (running) ...

Kea 2.6.3 を 5 サブネットぶん 設定。lease_cmds フックを有効にすると Control Agent 経由でリース情報を JSON で叩けるので、これに乗っかって Kea Manager という WebUI を自作しました (Go + 簡素な HTML、/usr/local/bin/kea-manager、http://192.168.10.1:8080)。

$ curl -s http://127.0.0.1:8000/ -H 'Content-Type: application/json' \
       -d '{"command":"lease4-get-all","service":["dhcp4"]}' | jq '.[].arguments.leases | length'
84

家族から「Wi-Fi に繋がらない」と言われたときに、ブラウザでリース一覧と予約状況を見られるだけで サポートコストが劇的に下がりました。プラットフォームエンジニアなら共感してもらえると思うんですが、「社内向けのちょっとした管理画面の有無で運用負荷が桁で変わる」やつです。

なお kea-dhcp6 と kea-dhcp-ddns はパッケージ依存で勝手に上がってくるが、設定はデフォルトのまま実質未使用 (IPv6 は SLAAC で配るので困らない)。

Debian の Kea 設定ファイル

{
  "Dhcp4": {
    "interfaces-config": { "interfaces": ["enp1s0f1.1010", ...] },
    "subnet4": [ ... ],
    "hooks-libraries": [ { "library": "/usr/lib/.../libdhcp_lease_cmds.so" } ]
  }
}

Dhcp4 キーがトップに来るのがポイント。続編でこの階層が罠になります。

(NixOS なら…) services.kea.dhcp4.settings = { interfaces-config = ...; subnet4 = ...; }; と書く。 NixOS モジュールが 自動で Dhcp4 キーを付ける ので、Debian のファイルをそのまま貼ると {"Dhcp4":{"Dhcp4":...}} の二重ネストで起動失敗します。これも続編で詳しく。

IPv4: MAP-E + 固定 IP のデュアル運用

ここが一番ややこしい。

MAP-E って何?

フレッツ網は IPv6 が素のネットワーク で、IPv4 はその上を間借りしています。具体的には IPv4 パケットを IPv6 パケットの中に包んで (= IPv4-in-IPv6 カプセル化) ISP のゲートウェイまで運び、そこで IPv4 として吐き出す。

このカプセル化方式が MAP-E (RFC 7597)。「IPv6 アドレス + 決められたポート範囲」のセットで IPv4 グローバルアドレスを 1 本シェアする仕組みです (= 1 つの IPv4 を 16 ホストくらいで分け合う)。

ソフトウェアの世界で言うと VPC 上で NAT GW を共有してる ような気分の構成。ただし NAT GW 自体は ISP 側にあって、宅内ルータがそこに向けてトンネルを掘る、という形になります。

Fixed IP も別途

外から到達可能な 固定グローバル IPv4 (203.0.113.80) は enひかりの IPv4 固定IPオプション で 1 本もらっていて、これは ipip-fixed トンネルで生やしている。実体は IPv4-in-IPv6 (4in6, RFC 2473) の ip6tnl で、underlay はフレッツ光クロスの NGN IPv6。enひかり側 GW (2001:db8:225:100::65) との間に IPv6 トンネルを張って、その中を IPv4 が流れる構造です。MAP-E と underlay は同じ (どちらも IPv4-in-IPv6) で、違うのは IPv4 をシェアするか (MAP-E) / 占有するか (固定IP) だけ。

# ip -d link show ipip-fixed
22: ipip-fixed@NONE: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1460 ...
    link/tunnel6 2001:db8::1 peer 2001:db8:225:100::65
    ip6tnl ipip6 remote 2001:db8:225:100::65 local 2001:db8::1 ...
Tunnel Type 出口 IP 用途
map-wan MAP-E (v6プラス / JPNE) 198.51.100.140 通常 IPv4 (NAT)
ipip-fixed IP-in-IP (enひかり IPv4 固定IPオプション) 203.0.113.80 外公開 / tinc underlay

出口を MAP-E / Fixed IP のどちらに流すかは ポリシールーティング (ip rule) で決めます。アプリのルーティングテーブルとほぼ同じ概念で、Web の middleware っぽい仕組み。

# ip rule
0:      from all lookup local
50:     from all fwmark 0x1 lookup fixed_v4    # tinc 発 (mark 0x1) は Fixed IP へ
99:     from 10.0.0.0/8 lookup main            # gsnet 宛は通常テーブルへ
32766:  from all lookup main
32767:  from all lookup default

MAP-E ソースポート集合の強制 (tc pedit)

MAP-E はソースポートが集合制約を受けるので、tc filter の pedit で SNAT 後のソースポートを許可集合に強制している。これは MAP-E ルータ自作勢には定番ですが、業務機器 (Cisco / Juniper) では基本やらない芸:

# tc -s filter show dev map-wan
filter parent 1: protocol ip pref 1 u32 chain 0
filter parent 1: protocol ip pref 1 u32 chain 0 fh 800:: order 2048 key ht 800 ...
  action order 1:  pedit action pipe keys 1
    key #0  at 20: val 0xc0000000 mask 0x0000ffff offset 0 ...

MAP-E トンネル構築は計算が泥臭く、シェルスクリプト /etc/router-scripts/map-e-setup.sh に押し込んでいます。起動順序は /etc/network/interfaces の post-up で叩いたり、自作 .service ファイルを /etc/systemd/system/ に書いて After= で並べたり、「どこにある何がいつ動くか」が頭の中だけにある 状態。これも辛さの原因の 1 つ。

(NixOS なら…) environment.etc."router-scripts/map-e-setup.sh".source = ./scripts/map-e-setup.sh; で配置。 systemd.services.map-e-setup = { wantedBy = [ "network.target" ]; serviceConfig.ExecStart = "/etc/router-scripts/map-e-setup.sh"; }; で起動順を宣言。 「/etc/ を直接触らない」 のが Nix の作法。/etc/ は Nix が /nix/store/... への symlink として配置する。

IPv6: ネイティブ + DHCPv6-PD

v6プラスの IPv6 は IPoE ネイティブで、ONU の先で直接 IPv6 が見えます。ルータの仕事は:

  1. wide-dhcpv6-client で DHCPv6-PD /56 をもらう (ISP が「/56 のブロックをあげるから自由に分けて使って」と言ってくれる)
  2. /etc/wide-dhcpv6/assign-ipv6-prefix.sh で /56 を VLAN ごとに /64 へ機械的に切り分ける
  3. radvd で各 VLAN に RA (Router Advertisement) を撒く (RDNSS でルータの IPv6 を DNS 先として配布)
ONU ─► enp1s0f0 (RA 受信 / SLAAC で WAN GUA)
         │
         └─► wide-dhcpv6-client (DHCPv6-PD /56 取得)
                  │
                  ├─► assign-ipv6-prefix.sh (/56 → /64 × N VLAN)
                  └─► radvd.conf 自動生成 → radvd 起動
$ ip -6 addr show enp1s0f1.1010 | grep inet6
    inet6 2001:db8:1010::1/64 scope global
    inet6 fe80::xxxx/64 scope link

運用ルール: radvd.conf は手で触らない

スクリプトが書き換える方式なので、手で触らないことだけルール化 した。RA を間違えるとブラウザの PRIVATE_ADDRESS エラーで気付くしかなくなり、デバッグが面倒なので lint レベルで書き換え経路をスクリプトに一本化しました。

IPMI VLAN (1110) は IPv6 でも forward を切って 完全隔離。BMC ファームに脆弱性が出ても LAN からしか触れないことを保証したい。

(NixOS なら…) nixpkgs に wide-dhcpv6 は無い → OpenWrt 由来の odhcp6c に乗り換え ることになります (続編)。 ロジック (assign-ipv6-prefix.sh) は同じインタフェースで書いてあれば ロジック側無修正で済む。 言語の置き換えに耐えるよう 「外部依存はスクリプト内で吸収しておく」 のは、Nix 移行を見据えると地味に効く設計指針。

L2 VPN: tinc (gsnet)

複数拠点の物理 L2 をひとつのブロードキャストドメインに繋ぐために tinc 1.1pre を使っています。gsnet という名前のメッシュ VPN で、10.0.0.0/8 を切って各拠点に静的に割り当て。

+--- Debian router ---------------------------------+
|                                                   |
| enp1s0f1.1083 ────┐                               |
|                   ├── br-gsnet (MTU 1420) ────────┼── tinc TAP
| tinc.gsnet ───────┘                               |
+---------------------------------------------------+
  • ノード名: aibauiha
  • IP: 10.24.34.2/8 (br-gsnet 上)
  • MTU: 1420 (UDP/IPv6 overhead を引いた値)
  • BindToAddress: IPv4 (ipip-fixed) + IPv6 デュアル
  • ConnectTo: gsngw01, suzukautako, linuweb, kimon, rabbit_house (port 655)
  • Mode: switch (TAP モード、L2 そのまま流す)

WireGuard だと L3 トンネルになるので、L2 を持って行きたい (= 同じブロードキャストドメインに居たい) ときには tinc の TAP モードが今でも便利。

tinc バージョンの罠

ソース バージョン
Debian dpkg tinc 1.0.36-2.1
実バイナリ (/usr/local/sbin/tincd) tinc 1.1pre18-242-g940d15c4 (自前ビルド)

1.1pre 系は SPTPS (新プロトコル) + ChaCha-Poly1305 cipher を喋れる。ピア同士が両方 1.1pre なら UDP 経路の暗号化処理が軽くなる 期待があり、10G 越しに L2 ブリッジで拠点間メッシュを張るならここが効くだろう、という動機でバージョンを上げた。Debian 公式 (1.0.36) のままでも疎通は問題なく回るが、暗号で頭打ちさせたくないので 1.1pre に寄せている (= 全ピアが 1.1pre なら新プロトコルに乗る、片方が 1.0 系ならその経路はレガシーに落ちる)。

# 自前ビルド
git clone https://github.com/gsliepen/tinc -b 1.1
./configure --prefix=/usr/local
make && make install
# → /usr/local/sbin/tincd

# systemd の ExecStart を override で差し替え
cat > /etc/systemd/system/tinc@gsnet.service.d/override.conf <<EOF
[Service]
ExecStart=
ExecStart=/usr/local/sbin/tincd -n %i -D
EOF
systemctl daemon-reload

(NixOS なら…) services.tinc.networks.gsnet = { package = pkgs.tinc_pre; ... }; の 1 行で 1.1pre18 に固定できる。 「自前ビルド + systemd override」という 3 ファイルの密結合が 消える。鍵 (rsa_key.priv, ed25519_key.priv, hosts/*) はそのまま scp でコピー。

監視: Netdata + iperf3

  • Netdata 2.9.0 を APT リポジトリ (netdata-repo) から入れて root 実行。1 秒間隔の細かいメトリクス (bnx2x の Rx/Tx パケット、conntrack エントリ数、nftables カウンタ) をブラウザで見られる。Prometheus を入れるほどじゃないけど Grafana 風の見た目が欲しいときに、apt install netdata 1 発で終わる軽さがちょうどいい。
  • iperf3-gsnet.service として iperf3 -s -B 10.24.34.2 を常駐 (tinc 越しの帯域測定用)。tinc@gsnet.service への依存を After=/Requires= で書いてあります。

iperf3 のサービス定義は手で /etc/systemd/system/iperf3-gsnet.service を書いた。これも git で追っていないと、半年後に「これ何のサービスだっけ?」となる。

(NixOS なら…) services.netdata.enable = true; で済む。 iperf3 のような単発常駐は systemd.services.iperf3-gsnet = { wantedBy = [ "multi-user.target" ]; after = [ "tinc.gsnet.service" ]; serviceConfig.ExecStart = "${pkgs.iperf3}/bin/iperf3 -s -B 10.24.34.2"; }; と Nix 式で直接書ける。/etc/systemd/system/ を直接編集する必要が無い。

稼働サービス棚卸し (Debian 13 時代)

サービス バージョン 備考
nftables 1.1.3 起動時適用
AdGuard Home v0.107.71 /opt/AdGuardHome/
Unbound 1.22.0 127.0.0.1:5335
Kea DHCP4 2.6.3 5 subnets
Kea Control Agent 2.6.3 127.0.0.1:8000
Kea Manager (自作) カスタム Go, WebUI
Kea DHCP6 / DHCP-DDNS 2.6.3 パッケージ依存で稼働、設定はデフォルト
radvd 2.20
wide-dhcpv6-client 20080615 DHCPv6-PD
tinc (gsnet) 1.1pre18 自前ビルド + systemd override
Netdata 2.9.0
iperf3 3.18 gsnet 専用、bind 10.24.34.2
OpenSSH 10.0p1

動作実測

観点 実測
LAN → WAN (MAP-E 越え) スループット 約 800–900 Mbps (v6プラスの IPv4 実効に近い)
LAN 内 VLAN 跨ぎ (router 経由) 約 9.4 Gbps (ほぼ線速)
平常時 CPU 1 コア 5% 未満 (MAP-E + nftables + radvd 全部走らせて)
conntrack 平均 数千 (NAT は MAP-E 出口だけ)
電気代 知れている (アイドルのうち)

家庭の常用なら困らない。MAP-E 越えで 900Mbps 出るのは bnx2x + 1 コアで通すには十分です。

運用してみての所感 — どこが本質的に辛いか

「動く」状態は数日で作れた。が、運用 1 年で見えてきたのは 設定の散逸とパッケージ管理の摩擦 のほう。casual な言い方で並べると「レシピが冷蔵庫・食器棚・引き出し・机の上に分散していて、料理を作り直すたびに探し回る」状態です。

辛い 1: 設定の散逸

機能 設定の場所
ネットワーク /etc/network/interfaces
ファイアウォール /etc/nftables.conf
DHCP /etc/kea/*.conf
DNS (フィルタ) /opt/AdGuardHome/AdGuardHome.yaml
DNS (再帰) /etc/unbound/unbound.conf.d/*.conf
DHCPv6-PD /etc/wide-dhcpv6/*
IPv6 SLAAC /etc/radvd.conf (スクリプト生成)
MAP-E /etc/router-scripts/map-e-setup.sh
tinc /etc/tinc/gsnet/*
tinc override /etc/systemd/system/tinc@gsnet.service.d/
iperf3 unit /etc/systemd/system/iperf3-gsnet.service

どこに何があるかを覚えていないと、変更がレビューできない。git log で追えるようにはしてあるが、「何を入れた / 何を消した」を全体俯瞰するのが難しい。

辛い 2: APT 外パッケージとの戦い

  • tinc 1.1pre → 自前ビルド (/usr/local/sbin/)
  • AdGuard Home → 公式インストーラ (/opt/AdGuardHome/)
  • Kea Manager → 自作 Go バイナリ (/usr/local/bin/)
  • Netdata → APT 外リポジトリ

apt full-upgrade で壊れないように祈る運用。APT のメンテ責任範囲 (= APT パッケージ) と APT 外 (= 自分のメンテ責任) が頭の中にしか書かれていない のが本質的に辛い。

辛い 3: ロールバックがない

「rebuild して動かなければ前の状態に戻す」という操作が Debian には標準で無い。/etc/ を git で管理しているとはいえ、git revert → systemctl daemon-reload → nft -f → … を 順序通り に再現する必要がある。深夜にこれを間違えてネットワーク全切断したら詰む。

辛い 4: バックアップ復元が "祈り"

もう 1 台に同じ環境を作る再現コストが高い。tar.gz を撒くだけでは systemd unit / override / 自前バイナリの依存関係が再現できない。ハードが壊れたら涙目。

エンジニア視点での言い換え

これって要するに infra-as-code が無いプロジェクト の苦しみそのもの。Web 開発で言えば「本番 EC2 に SSH で入って手で vim してる」状態。設定変更が PR にならない、ロールバックができない、誰がいつ何を変えたかが追えない。

ルータという「絶対に止めたくない」かつ「設定ミスでネットワーク全切断する」装置こそ宣言的に書きたい、というのが結論。

次回予告

次は この構成をそっくり NixOS で書き直して infra-as-code 化 した話。

  • /etc/nixos/configuration.nix 1 本 (実際にはモジュール 9 分割) でルータ全体を宣言的に定義
  • 本記事で見た パス・ファイル・サービス定義 が、NixOS では どの Nix 式に変換されるか を対応表付きで詳しく書きます
  • nixos-rebuild switch --rollback で物理ルータがロールバック可能になる、という精神安定上ものすごく大きい話

続編に続きます。

おうちネットワーク遊び

やりたかったこと

  • MAP−E の v6 プラスで接続したい
  • v6 プラス固定オプションを使用してサーバを公開したい
  • サーバのネットワークと、メインのネットワークを分離したい(DMZ?用語わからん)
  • ゲストネットワークを用意したい

作業開始

Day0

構成としては、

  • RTX 830 で MAP-E トンネルを張る
  • Fortigate 50E で IPIP トンネルを張る
  • RTX 830 の LAN を Fortigate 50E の WAN に繋ぐ

の状態だった。

Day1

実現しようとした構成

  • RTX 830 で MAP-E トンネルを張る
  • Fortigate 50E で IPIP トンネルを張る
  • RTX 830 の LAN を Fortigate 50E の WAN に繋ぐ
  • Fortigate 50E のルーティング情報に、「デフォルトゲートウェイを MAP-E トンネルとする」「src がサーバのネットワークなら IPIP トンネルをゲートウェイとする」を追加する ←New✨

OpenWRT の Luci 上から静的経路設定をしても ip route コマンドから設定を確認出来ず、設定が悪いか OpenWRT が悪いかの2択に絞り込む。

iptables なら簡単に出来るのに…

Day2

Fortigate 50E の IPIP トンネルのインターフェースを削除して再度作成しようとしたら、IPIP トンネルがうまく張れない状態になる。 バックアップを取り忘れていた。

設定をこねくり回しても接続出来ないため、一旦諦める。

教訓 設定を変更するときは、バックアップはちゃんと取ろう!

Day3

設定をこねくり回す。

WAN インターフェースの ifaceid が設定出来ていないために、正しくパケットが返ってきていない状態だったことに気付く。 IPIP トンネルを確実に張ることが出来るようになる。

しかし、ルーティング情報が反映されないので色々調べていたら、ポリシーベースルーティングとやらにたどり着く。 ip rule というコマンドがあることを知った。

色々設定してみても、思ったとおりには動かなかった。

Day4

ソフトウェアの常識「1つのコンポーネントに複数の役割を持たせない」を用いることで、設定の破壊を防ごうと考える。 そこで考えた構成が以下の通り

  • RTX 830 で IPIP トンネルを張る ←New✨
  • 別のルータで MAP-E トンネルを張る ←New✨
  • Fortigate 50E の WAN にそれぞれの LAN をつないで、ルーティングを行う ←New✨

RTX 830 で IPIP トンネルを張ることには成功した。

急遽、稼働していない WX6000HP を拾いにいき、MAP-E をコイツに任せることにした。

起動して WAN を接続すると、「回線タイプを判別しています。しばらくお待ちください」のまま何分たっても返ってこない。微妙すぎる。 諦めて WAN の接続を外すことで回線タイプの判別をスキップ。管理画面に入ってから WAN を接続することでいい感じに操作が出来る状態になった。

IPIP トンネルと MAP-E トンネルが張れたので、Fortigate 50E の設定をする。 しかし、思ったとおりのルーティング情報が反映されない。

この Fortigate 50E が悪いのか?と考え始める。

Day5

この Fortigate 50E が悪い可能性を考え、別の Fortigate 50E を用意するために友人宅へ向かう。 2つの Fortigate 50E を入手したので、動作確認をする。

やはり設定が反映されないので、機体ではなく OpenWRT か設定が悪い状態へ絞り込めた。

「YAMAHA に乗っかることで全てが解決する!」と思い込んだ私は、WAN が2つある Fortigate 50E の代わりを探すことにした。

WAN が2つあってヤフオクに転がっていていい感じの値段で買えるのは、RTX 1210 か RTX 3500 のどちらかであった。 「うーーーんデカい方が強い!」ということで、RTX 3500 を落札。

届くのを待つことにした。

Day6

RTX3500 が届いた。

組もうとした構成は以下

  • RTX 830 で MAP-E トンネルを張る
  • Fortigate 50E で IPIP トンネルを張る
  • RTX 3500 の WAN とそれぞれの LAN をつなぎ、ルーティングを行う ←New✨

RTX 3500 を起動したが、ファンの音があまりにもうるさすぎる。 作業部屋に置けるものではないが、仕方ないので置いたまま生活することになりそうだ。

RTX 3500 の設定を詰められず、疲れて1日が終わる

Day7

RTX 3500 の設定を詰める。

デフォルトゲートウェイと filter の設定がうまく反映されない。

諦める。

Day8

一旦状況の整理をするため、ホワイトボードに実装したい構成を書き出した。

友人に見てもらったところ、「どうしてそんなにルータが必要なの…?」「ソフトウェアの常識はハードウェアに通じないよ」などと言われる。

「じゃあどうするねんな」と言ったところ、MAP-E トンネルを張るルータと IPIP トンネルを張るルータで別のネットワークを作成すれば良い とのこと。

「お互いの LAN に接続したら DHCP のアドレス配布が混線するじゃろ…」と思ったが、ポートごとにネットワークを区切れば良いことに気付く。

RTX 830 の LAN 分割機能を用いて lan1.5 をルータ用ネットワークの IP アドレスに設定し、Fortigate 50E の eth5 をルータ用ネットワークの IP アドレスに設定する。 お互いのルータに静的経路設定を行い、反映されていることを確認した。

成功である。 こんなに簡単な話だったのか…

感想

ネットワーク構成は脳内で完結させるのではなく、書き出して他の人にレビューしてもらうことが大切だと感じた。

RTX 3500 が余ったので、有効活用する方法について考えている。

ネットワークは難しい。

JetBrains IDEでREADME.mdを開くとクラッシュする

環境

Ubuntu 24.04

6.6.0-14-generic

IntelliJ IDEA Ultimate 2023.3.3

原因を調査する

ディスプレイサーバのプロトコルを変更する

  • Wayland: 発現した
  • X: 発現した

IntelliJ IDEA のバージョンを変更する

  • IntelliJ IDEA Ultimate 2024.1 EAP: 発現した
  • 2023.3.4 Release Candidate: 発現した
  • 2023.3.3: 発現した
  • 2023.2.5: 発現した
  • 2023.1.5: 発現した
  • 2022.3.3: 発現しなかった
  • 2022.2.5: 発現しなかった

別の JetBrains IDE を使用する

  • GoLand 2023.3.3: 発現した

プラグインのクラッシュを疑う

ターミナルから起動する

FATAL:credentials.cc(127)] Check failed: . : Permission denied (13)

と出てきたので Google で検索すると、 https://youtrack.jetbrains.com/issue/IDEA-319751 が見つかる

調査結果

対処方法

https://youtrack.jetbrains.com/issue/IDEA-319751/IDE-exits-immediately-after-opening-the-folder#focus=Comments-27-7315116.0-0 を参照する

Kaede Higuchi Live 2021 "AIM" に行った

この記事は、オタクのお気持ち表明のような物です。

要点まとめ

  • チケット消えて、開演5分前まで会場前で待ち
  • オール着席・コール禁止ライブは面白くない
  • ライブの内容は良かった

チケットが消えた件

今回のライブは、イープラスアプリ内でのスマチケ(スマートチケットかな?)というシステムでチケットが発券されました。

「機種変更などは済ませてからチケットをダウンロードしてください」的なことが注意書きにあったので、前日夜にチケットをダウンロードしました。多分1回きりしかダウンロード出来ないんだろうな~ということを勝手に想像しました。

チケットをダウンロードしようとイープラスアプリを開くと、更新を求められました。

更新をしてアプリを開いたら、謎のエラーダイアログが出て起動しません。何かがクラッシュしてるようです。

仕方がないので、アプリを再インストールしてチケットをダウンロードします。インストール直後にうまく動いたので、アップデートで何かが失敗したのかな?と思っていました。会場に着いてから、同じエラーダイアログが出て起動できなくなるとも知らずに。

チケットをダウンロードしたら、席番がわかるようになります。偶然、友人に Discord 上で席の番号を伝えていました。この行動が運命を左右します。

当日会場に着いて、イープラスアプリを開いてチケットの確認をしようとしたら、昨日の夜に見たエラーダイアログが出てきます。

「あ、起動しないしこれは終わった。しかし再インストールすれば開ける。」ということで、イープラスアプリの再インストールを行います。

アプリを再インストールしたにも関わらず、エラーダイアログが出続けます。「あれ?これ死んだのでは?」と思い、会場前でアナウンス等を行っているスタッフに話をしに行きました。

起きた問題は、

  • チケットを確認しようとしたら、アプリがクラッシュして開けない
  • 別のスマホで確認したら、チケットが一度きりしかダウンロード出来ない

の2点です。

これらを伝えたら、スタッフさんは中に確認へ行ってくれました。「別の者が来る」ということで、寒さの中待っていました。

会場整理をしているスタッフの方とは違う見た目の方が来ました。少し権限を持ってる人だったのでしょうか。

先程と同じ内容の話をしたら、「開演5分前に席が空いていることを確認出来次第ご案内します。」という回答が得られました。

開場16時半・開演17時半で、私が会場に到着したのは16時40分でした。

開演5分前まで待機するように頼まれたのは16時54分なので、30分入場できず会場前で待ちぼうけをする事になりました。

イープラスアプリのバグのせいでチケットが消え、寒い中30分無意味に会場前で時間を潰す人になってしまったのです。

開演50分前に会場に着いたのにも関わらず、開演5分前に入場した弊害

前説が聞けない

通常ライブの前説では、ライブ中の禁止行為等を説明することが多いです。禁止行為の説明等が聞けないので、何をしたら駄目なのかわかりません。

公式サイトには、ライブ中の禁止行為等の説明が書かれていませんでした。「新型コロナの拡大対策をします」しか書かれていませんでした。

会場内に入りライブが始まり、周囲のオタクの行動を眺めていたところ、2つのレギュレーションが見えてきました。

  • オール着席
  • コール禁止

コロナ対策ということでやっているのでしょうか。「楽しめないレギュレーションだな」と感じました。

レギュレーションに対して感じたことは、下でもう少し掘り下げます。

怒りの感情でライブが楽しめない

自分の失敗で入場出来なくなったわけではないので、怒りというものが湧いてきます。

怒りの矛先はイープラスです。イープラスアプリを開発した人間でしょうか。

「こんなバグがあるアプリをリリースして、お金やチケットを扱うなんて酷すぎる!」とキレ散らかしていました。

ライブ前は、"推しのライブなので楽しみたい" という気持ちを持って会場に向かいます。

入場しようとしたら入場が出来るかわからない状態になり、かなり寒い中で待たされたら、楽しみたい気持ちはかき消されてしまいます。

入場しても場の盛り上がりにはついて行けず、一人イライラしながらライブを見る事になり、純粋に楽しむことが出来ませんでした。

オール着席・コール禁止というレギュレーション

コロナ禍で感染を広げないためには仕方がないレギュレーションだと思います。ライブ開催が原因で感染が広がったら、演者や運営も大迷惑です。

感染を広げないよう対策するのは良いのですが、楽しいか楽しくないかという問題は全く別です。

楽しくなかったです。

会場にいるオタクの大半が棒振り地蔵になっていました。キモい。

ライブでは、動いて声を出すことで体のリソースをたくさん使います。これによってライブに参加している気持ちが高まります。一体感とかいう奴かもしれません。

オール着席でコール禁止の場合すべてのリソースが余った状態になり、ライブを聞いているだけになります。

リソースが余った結果、Twitter を一生見てました。

オール着席のデメリット

曲に合わせて動くことが困難

座っていると足先でリズムを取ることしかできません。

私はリズムに合わせて体を動かすタイプなので、足先でリズムを取るのは物足りない感じがしました。頭も振りづらかったです。

周囲のオタクの振動が伝わってくる

ライブが行われるホールは、基本1列席が繋がっています。

人間が動くと椅子が振動します。周囲のオタクの振動がめちゃめちゃ自分に伝わってきて気持ち悪かったです。

自分が動くことによって発生する振動が他人に伝わってると思うと、なんか気持ち悪いです。

コール禁止のデメリット

もどかしい

コール曲は楽しい感じの曲が多いです。アルバムを初めて聴いた時、「コールするとしたらここだな~」など色々考えていました。

声を出したいのに出せない状況は、非常にもどかしく感じました。

ライブ内容について

ここからは、ただ語彙力が死んだオタクの感想です。

めちゃめちゃ良かったです。

しっかりストーリーがあって、それに合わせた曲順で歌われていました。

野球部監督、カエデ、22歳の樋口楓 と3つの世界が、現実の樋口楓に反映されていくような感じ、良かったです。

ファンメイド曲や響鳴など、ランティスと関係ない曲もやってくれて、激アツでした。

響鳴は KANA-DERO を思い出しました。

いそっち…好き……… KANA-DERO パーカー着てたのヤバい…

最後に

ライブ前に色々問題が発生しましたが、樋口楓のライブはやはり良かったです。

個人的には KANA-DERO のほうが良かったと感じています。

コロナ騒動が収まったら、再演をやってほしいと強く願います。

この記事を読んだオタクは AIM を見てください。3月始めまでネットチケット購入すれば無料で無限回見れます。

secure.live.nicovideo.jp

終わり

【あつ森】南半球でお金を稼ぐ方法【タランチュラ超え】

初めに

3月に北半球ではタランチュラが出るので日本人の8割くらいがタランチュラでお金を稼ごうと方法を編み出していました。

南半球を選択した私はタランチュラが出ない代わりにサソリで代用しようと思いましたが、蚊とスズムシとコオロギが走っても逃げないので諦めました。

離島ツアーの回数を重ねていたら、タランチュラ島と同じくらい稼げる島を見つけたので紹介します。

f:id:chun37:20200327023835j:plain

環境

  • 南半球
  • 3月

離島の詳細

  • 木は針葉樹のみ
  • 池や川は無し
  • ヤシの木が4本(右2本・左2本)
  • ニジイロクワガタ、ヨナグニサンが木に止まっている
  • 地上にはオオセンチコガネのみ

手順

  1. 叶式の要領で湧きを固定させる 参考動画
    • 花を取る
    • 木を切る(ヤシの木は残す)
    • 見やすくするために雑草を刈る
  2. 取らない虫を走って蹴散らす
  3. ゴライアスオオツノハナムグリがヤシの木に止まっていたら捕らえる
  4. 気が済むまで2と3を繰り返す

利点

  • 失敗しても島の最初に戻されない
  • サソリやタランチュラを捕まえるは難しいが木に止まっている虫を捕るのは比較的簡単
  • 同時に2匹出ることも

f:id:chun37:20200327023927j:plain

詳細

ヨナグニサンは1月, 2月, 3月, 10月, 11月, 12月に発生

オオセンチコガネは1月, 2月, 3月に発生

ニジイロクワガタは1月, 2月, 3月, 12月に発生

ゴライアスオオツノハナムグリは1月, 2月, 3月, 12月に発生

ゴライアスオオツノハナムグリはヤシの木のみに発生するため針葉樹を切っても問題なくスポーンする。

最後に

時間変更はしていないため1月と2月と12月の検証はしていないのですが、南半球の3月のお金稼ぎ方法を見つけられたので北半球の人たちにも負けずにお金が稼げて良かった。