前回 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 は移行後に追加した。詳しくは末尾の後日談で。)
移行戦略 — ブルー/グリーンの物理版
- 別 SSD に NixOS を新規インストール (Debian 側のディスクは抜いて温存)
/etc/nixos/configuration.nix+/etc/nixos/modules/*.nixで構成を書き上げるnixos-rebuild test→switchで完成形を作ってから、ONU と CRS305 のケーブルを Debian → NixOS 機に物理で差し替え- 切り戻し = ケーブルを 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)
- Debian ルータの全サービス停止
- ONU → NixOS
enp1s0f0差し替え - CRS305 trunk → NixOS
enp1s0f1差し替え - 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 両方向化など) はそれぞれ別記事で書く予定です。

