家のインターネットの "玄関口" にあたる ルータ を自作した話を書きます。 一般の家庭で言うところの「プロバイダから渡される機械」「家電量販店で買う 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 点。
- テーブルを機能ごとに分けて書ける (機能別にレビュー可能)
inetファミリで v4/v6 を一本化できる- ベンチで「ルールが増えても 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 が見えます。ルータの仕事は:
wide-dhcpv6-clientで DHCPv6-PD /56 をもらう (ISP が「/56 のブロックをあげるから自由に分けて使って」と言ってくれる)/etc/wide-dhcpv6/assign-ipv6-prefix.shで /56 を VLAN ごとに /64 へ機械的に切り分ける- 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 netdata1 発で終わる軽さがちょうどいい。 - 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.nix1 本 (実際にはモジュール 9 分割) でルータ全体を宣言的に定義- 本記事で見た パス・ファイル・サービス定義 が、NixOS では どの Nix 式に変換されるか を対応表付きで詳しく書きます
nixos-rebuild switch --rollbackで物理ルータがロールバック可能になる、という精神安定上ものすごく大きい話
続編に続きます。