n番煎じなネタですが、ラズパイルーターを構築しました

前提条件

  • ハードウェア: Raspberry Pi 4 Model B Rev 1.4
    • メモリ2GBモデル
  • OS: Raspberry Pi OS Lite 64bit
    • Debian 12 (bookworm) がベースのやつ
  • 作るものは「WireGuard付き無線LANルーター」
    • 無線部分は5GHzのみ
  • networkdで出来ることは極力networkdでやる
    • 有線/VPN部分はnetworkdで完結した
    • 無線部分はさすがにhostapdが必要だった
  • 有線/無線共にUSB NICは足さない
    • 有線オンボードNICはL2SWに繋ぐ
    • L2SW側はトランクポートに設定し、各種クライアントはNativeVLANで繋ぐ。WAN側はVLAN10を使う

OS準備

SDへの書き込みはRaspberry Pi Imagerを使い、カスタマイズを入れる

  • 一般
    • ユーザー名とパスワードを指定する
    • ロケールをAsia/Tokyoにしてキーボードレイアウトもjpにする
  • サービス
    • sshdの有効化とauthorized_keysの指定
  • オプション
    • テレメトリーを切る

ネットワーク系 SSHするまで

# NetworkManagerからnetworkdに切り替える。適用は豪快にreboot
sudo systemctl disable NetworkManager.service
sudo systemctl enable systemd-networkd.service
sudo reboot

# 推測できるインターフェース名(Predictable Network Interface Names)を有効化する
# この2つのファイルは/dev/nullへのシンボリックリンクになっており、消すことで
# /usr/lib/systemd/network/99-default.linkなどのマスクが解除される
sudo rm /etc/systemd/network/99-default.link
sudo rm /etc/systemd/network/73-usb-net-by-mac.link
# これも適用方法が分からないのでrebootする。再起動後はeth0がend0になる
# eno0になると思っていたが違った
sudo reboot

# 有線LANの最低限の設定を入れる。vimは入っていないのでnanoを使う
sudo nano /etc/systemd/network/80-end0.network
[Match]
Name=end0

[Network]
Address=192.168.11.1/24
DHCPServer=true
# 適用はnetworkctlで出来るらしい。ダメそうならreboot
sudo networkctl reload
  • ここでSSHできるはず。PCを直結してSSHしてみる

有線LAN IPv4関連の設定

  • ドキュメントはここ。デフォルトで入っているsystemdのバージョンは252だった
# 上で作ったファイルをもう一度編集する
sudo nano /etc/systemd/network/80-end0.network
[Match]
Name=end0

[Network]
# メモをつける。networkctl status end0 で表示されなかったので、どこに出てくるのか分からない
Description=Onboard NIC
# 割り当てるIPアドレス
Address=192.168.11.1/24
# IPv4のDHCPサーバを起動する
# 0.0.0.0:67 でlistenするので、WAN側のnftablesはしっかり書かないと怖い
DHCPServer=yes

# IPv4/IPv6でパケットのフォワードを有効化する
# sysctlで net.ipv4.ip_forward と net.ipv6.conf.all.forwarding を書き込んでくれる
# IPForward=ipv4 と書けばIPv4だけ書き込むはず
IPForward=yes

# いわゆるNAPTを有効化する。sudo nft list rulesetすると色々ルールが増えている
# nftなので、sudo systemctl restart nftables.serviceすると消えてしまう
# 不便なので使うのをやめた
# IPMasquerade=ipv4

# WAN接続用のVLANを紐づける。対応する.netdevをまだ作ってないので今は何も起きない
VLAN=vlan10
# DHCPv6-PDで受け取ったアドレスをRAで配布する。まだ上流の設定が無いので何も起きない
DHCPPrefixDelegation=yes
IPv6SendRA=yes

[DHCPServer]
# DHCPで配るIPアドレスは最初の100個を飛ばしてそこから50個を使う(多分。境界値がちょっと怪しい)
PoolOffset=100
PoolSize=50
# DHCPで配るDNSサーバのアドレス。適当にGoogleDNS
DNS=8.8.8.8
DNS=8.8.4.4

# DHCPv6-PDの設定。まだ上流の設定が無いので何も起きない
[DHCPPrefixDelegation]
UplinkInterface=:auto
SubnetId=11
# ここでもう1回適用
sudo networkctl reload

WAN側VLANインターフェース作成

  • ドキュメントはここ
  • 契約してる回線の仕様は以下
    • IPv4は素直にDHCPで降ってくる
    • IPv6はDHCPv6-PDで/56のプレフィックスがもらえる
  • 今回は試していないが、networkdにNDProxyの機能も内蔵されているらしい
sudo nano /etc/systemd/network/25-vlan10.netdev
[NetDev]
Name=vlan10
Kind=vlan

[VLAN]
Id=10
# 適用。networkctlのドキュメントにある通り、reloadが効くのはnetdev新規作成時のみ。修正する時はよく分からないので再起動する
# Note that even if an existing .netdev is modified or removed, systemd-networkd does not update or remove the netdev.
sudo networkctl reload
# vlan10@end0 が増えているはず
ip link
# VLANインターフェース向けにIPアドレス設定などを入れる
sudo nano /etc/systemd/network/81-vlan10.network
[Match]
Name=vlan10

[Network]
# DHCPクライアントを有効化する。yesだとIPv4/v6両方で有効化されるので、必要に応じてipv4に変える
DHCP=yes
# DHCPv6-PDの移譲を受ける
DHCPPrefixDelegation=yes
# IPv6のアドレスはDHCPで受け取るがRAでも受け取る。ルーティング情報などが降ってくる
IPv6AcceptRA=yes

[DHCPv6]
# サンプルにこう書いてあったので書く。必要性はよくわかっていない
# Allows DHCPv6 client to start without router advertisements's "managed" or "other configuration" flag.
WithoutRA=solicit
# フレッツ環境だとこれも必要らしい?DUIDはなんでも良い気がするので適当に入れる
DUIDType=link-layer

[DHCPPrefixDelegation]
# DHCPv6-PDの上流インターフェースは自分だと明示する
UplinkInterface=:self
# 受け取ったプレフィックスにくっつけるサブネットID
# https://www.nic.ad.jp/ja/newsletter/No32/090.html
SubnetId=0
# 受け取ったプレフィックスを広報するか。これを許可するのはLAN側のみ
Announce=no

nftables設定

  • 中から外への通信を全許可し、その戻りも全許可
  • 外から中への通信は基本的に拒否
sudo nano /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

# LAN側とWAN側のインターフェース名を定義しておく
define LAN_IF_NAME = { "end0", "wld0", "wg0" }
define WAN_IF_NAME = { "vlan10" }

# IPv4/v6両対応のテーブル
table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;

    # established/relatedは許可
    ct state established,related accept
    # invalidは拒否
    ct state invalid drop
    # loopback許可
    iif lo accept

    # 外からのICMPv6許可。厳密にやるならRFC4890に沿う
    iifname $WAN_IF_NAME ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, mld-listener-query } accept
    # 中からのICMPは全許可
    iifname $LAN_IF_NAME ip6 nexthdr icmpv6 accept
    iifname $LAN_IF_NAME ip protocol icmp accept
    # 外からのDHCPv6許可
    iifname $WAN_IF_NAME ip6 saddr fe80::/10 udp dport 546 accept
    # 中からのSSH/DHCP許可
    iifname $LAN_IF_NAME tcp dport 22 accept
    iifname $LAN_IF_NAME udp dport 67 accept
    # wireguardは外内どこからでも許可
    udp dport 51820 accept
  }
  chain forward {
    type filter hook forward priority filter; policy drop;
    # 中から外へ全許可し、戻りを許可
    iifname $WAN_IF_NAME ct state established,related accept
    iifname $LAN_IF_NAME accept
  }
  chain output {
    type filter hook output priority filter; policy accept;
  }
}

# IPv4のみのテーブル
table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat;
    # いわゆるNAPT
    oifname $WAN_IF_NAME masquerade
  }
}
# nftables起動。忘れず自動起動も設定する
sudo systemctl start nftables.service
sudo systemctl enable nftables.service
  • ここでついに結線する
    • L2SW側でトランクポートを作り、allowed vlanにインターネットセグメント、native vlanにクライアント用セグメントをアサインしておく
  • ip addrnetworkctl status vlan10 でIPアドレスが降ってきたことを見る
    • DHCPv6のリース状況はnetworkctl status vlan10の下の方のログにしか出ない

DNS設定とapt更新

  • Debian12ではデフォルトでsystemd-resolvedがインストールされなくなった
  • なくても困らないので、直でresolv.confをいじる
sudo nano /etc/resolv.conf
# 適当にGoogleDNSを指定
nameserver 8.8.8.8
nameserver 8.8.4.4
# いつもの更新をかける
sudo apt update
sudo apt full-upgrade
sudo reboot

WireGuard設定

  • クライアント側の公開鍵を登録しないといけないので、サーバ単体では設定が完了しない
    • SSHのauthorized_keysみたいなもの
# wgコマンドのインストール
sudo apt install wireguard-tools

# 秘密鍵を作り保存する。権限的にnetworkdで読めるようにする
wg genkey | sudo tee /etc/wireguard/private.key
sudo chmod 640 /etc/wireguard/private.key
sudo chown root:systemd-network /etc/wireguard/private.key
# 公開鍵を作る。秘密鍵が変わらなければ公開鍵も変わらないので、特に保存する必要はない
# 今後iPhoneなどクライアント側の接続設定を作る時に使う
sudo cat /etc/wireguard/private.key | wg pubkey

# netdev作成
sudo nano /etc/systemd/network/25-wg0.netdev
[NetDev]
Name=wg0
Kind=wireguard

[WireGuard]
PrivateKeyFile = /etc/wireguard/private.key
ListenPort = 51820

[WireGuardPeer]
PublicKey = <ここにクライアント側の公開鍵>
# クライアント側のIPアドレス。このIPアドレスがサーバ側のルーティングに足されるイメージ
AllowedIPs = 192.168.42.5/32
PersistentKeepalive = 25
# network作成
sudo nano /etc/systemd/network/81-wg0.network
[Match]
Name=wg0

[Network]
Address=192.168.42.1/24
# 適用
sudo networkctl reload
# wg0の状態を見る
networkctl list
sudo wg show

内蔵無線LANの名前固定

  • Predictable Nameがなぜか内蔵無線LANでは効かず、wlan0になるので設定を入れる
    • 必須ではないが、無線用にUSB NICを足したりする時に効いてくる
# Pathを確認
networkctl status wlan0 | grep Path
# 名前固定用の設定を入れる
sudo nano /etc/systemd/network/10-onboard-wlan.link
[Match]
# 確認したPath
Path=platform-fe300000.mmcnr

[Link]
# end0に寄せてwld0に固定する
Name=wld0
# 適用方法が分からなかったのでreboot
sudo reboot
# wlan0 が wld0 に変わったことを見る
networkctl list

無線LANの設定

# 無線の規制範囲を日本にする。hostapdでも指定しているので、要らないかもしれない
echo 'options cfg80211 ieee80211_regdom=JP' | sudo tee /etc/modprobe.d/ieee80211_regdom.conf
# 適用方法が分からなかったのでreboot
sudo reboot
# 規制範囲などを眺めてみる
iw reg get
iw list

# hostapdインストール。AP作成だけはnetworkdでは出来なかった
sudo apt install hostapd

# hostapdのconfig作成
sudo nano /etc/hostapd/wld0.conf
# APにするインターフェース名
interface=wld0
# ログを標準出力に出す。systemd経由で起動するのでjournaldで読める
# -1は全てのモジュールという意味で、モジュールごとに有効・無効を切り替えることもできる
logger_stdout=-1
# 出すログはinfo以上
logger_stdout_level=2
# 起動したhostapdを操作するためのファイル(socket?)を置くディレクトリ
# hostapd_cliで使う
ctrl_interface=/var/run/hostapd

# SSID名
ssid=test-ap-A
# 準拠する規制範囲
country_code=JP
# ビーコンに国情報を含めるか。DFSで使う
ieee80211d=1
# DFSを有効化するか。5GHz帯の上の方のチャンネルで使うやつ
ieee80211h=1

# 5GHz帯を使う
hw_mode=a
# チャンネルは自動調整が良い感じに動くことを祈る
channel=acs_survey
# まれにhostapdが起動しなくなることがあったので、固定した方が安心。固定する場合は普通に数値を指定する
#channel=52

# WEPを明示的に切る。ビット指定なので分かりにくいが、3にするとWEPも有効になる
auth_algs=1

# 802.11n(Wi-Fi4)を有効化する
ieee80211n=1
# 有効化するHTの機能。深く考えずiw list で表示されたHT機能を指定する
ht_capab=[HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]

# 802.11ac(Wi-Fi5)を有効化する
ieee80211ac=1
# 有効化するVHTの機能。これも深く考えずiw list で表示されたVHT機能を指定する
vht_capab=[SHORT-GI-80][SU-BEAMFORMEE]
# 80 MHz幅を使う
vht_oper_chwidth=1

# WPA2を有効化する
# これもビット表現なので、3にするとWPAとWPA2が有効になる
wpa=2
# パスワード
wpa_passphrase=password01
# 事前共有鍵方式
wpa_key_mgmt=WPA-PSK
# 暗号アルゴリズム
wpa_pairwise=CCMP
# hostapd起動。@の後ろはconfig名に合わせる(interface名ではない)
# 起動後は適当なスマホ等でSSIDが飛んでいることを見る
sudo systemctl start hostapd@wld0.service
# 忘れず有効化
sudo systemctl enable hostapd@wld0.service

# APにIPアドレスを付与する
sudo nano /etc/systemd/network/81-wld0.network
# 有線部分とほぼ同じ設定を入れる
[Match]
Name=wld0
WLANInterfaceType=ap

[Network]
Address=192.168.31.1/24
DHCPServer=yes
DHCPPrefixDelegation=yes
IPv6SendRA=yes

[DHCPServer]
PoolOffset=100
PoolSize=50
DNS=8.8.8.8
DNS=8.8.4.4

[DHCPPrefixDelegation]
UplinkInterface=:auto
SubnetId=31
# 適用
sudo networkctl reload
# wld0が管理状態になっているのを見る
networkctl list

ログ書き込み抑制と不要なもの削除

# ログの保存先を消す。消すとtmpfsの/run/log/journal/に書かれるようになる
# この挙動は/etc/systemd/journald.confのStorage=autoのあたりを参照のこと
sudo rm -rf /var/log/journal

# mDNSとbluetoothは使わないので消す
sudo apt purge avahi-daemon pi-bluetooth
sudo apt autopurge
# ついでにソフトウェア的にも切っておく
rfkill block bluetooth

おわりに

思った以上に時間が溶けた