🔗

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

Linuxでルータを作る(基本機能構築編)


記事の構成について

第二部では、Linuxマシンをフレッツ網に接続し、基本的なルータ機能を構築する。


手順2:基本的なルータ機能の構築

第一部で、すでにネットワーク構成の概要述べた。そのため第二部では設定例を主に示し、軽く注釈を加えるにどめる。

インターフェースの準備

最初に、必要な仮想インターフェースをすべて作成しておく。

/etc/systemd/network/10-home.netdev
[NetDev]
Name=home
Kind=vlan
[VLAN]
Id=10
/etc/systemd/network/15-hgw.netdev
[NetDev]
Name=hgw
Kind=vlan
[VLAN]
Id=100
/etc/systemd/network/01-lan0.network
[Match]
Name=lan0
[Network]
VLAN=home
VLAN=hgw
LinkLocalAddressing=no
LLMNR=no

フレッツ網への接続

IPv6 IPoE

DHCPv6クライアントを動かし、/56のプレフィックスを受け取る。また、受け取ったプレフィックスの一部(/64)から、通信用の一時アドレス(RFC 4941)と、IPv4 over IPv6用のCEアドレスも割り当てる。

/etc/systemd/network/05-wan0.network
[Match]
Name=wan0
[Link]
RequiredForOnline=yes
[Network]
DHCP=ipv6
IPv6Forwarding=yes
IPv6AcceptRA=yes
# 一時アドレスを割り当てる
IPv6PrivacyExtensions=yes
DHCPPrefixDelegation=yes
Tunnel=ip6tnl0
[DHCPv6]
DUIDType=link-layer
IAID=0
# DHCPv6リクエストに余分な情報を含めないようにする
SendHostname=no
UseCaptivePortal=no
UseDNR=no
[DHCPPrefixDelegation]
UplinkInterface=:self
SubnetId=0
Announce=no
# CEアドレスの後半64bit(= インターフェースID)を指定
Token=::ce

CEアドレスはNetwork.Address=でも指定できるが、DHCPPrefixDelegation.Token=ほうが楽だ(プレフィックス部分を書かずに済むため)。

ひかり電話関連情報の取得については次回触れる。

IPv4 over IPv6

トンネルデバイスip6tnl0)を作成し、その上にトンネルを張る。

/etc/systemd/network/05-ip6tnl0.netdev
[NetDev]
Name=ip6tnl0
Kind=ip6tnl
[Tunnel]
Mode=ipip6
# CEアドレス
Local=dhcp_pd
# BRアドレス
Remote=2001:db8::1
DiscoverPathMTU=yes
EncapsulationLimit=none
/etc/systemd/network/05-ip6tnl0.network
[Match]
Name=ip6tnl0
[Link]
RequiredForOnline=yes
[Network]
BindCarrier=wan0
IPv4Forwarding=yes
LinkLocalAddressing=no
LLMNR=no
# DefaultRouteOnDevice=yes
[Route]
Gateway=0.0.0.0
[QDisc]
Parent=clsact
Handle=ffff

あとでtcフィルタを付けるため、この時点でベースとなるqdiscも設定している1

[Route] セクションの代わりに、Network.DefaultRouteOnDevice=yes指定することもできる。細かい設定Route.InitialCongestionWindowなど)が不要な場合、そちらのほうが楽だろう。

MAP-E関連の設定

前回紹介した記事倣い、tc使ったNATなどを設定する。iptablesの代わりにnftablesを使い、TCP MSSの自動調整(MSS Clamping)を加えている点以外は、リンク先とほぼ同じだ2

まずMSS Clamping・fwmarkの付与・ソースNATをするnftablesのルールを書く。

/etc/nftables.d/05-ip6tnl0.nft
#!/usr/bin/nft -f
destroy table ip mape
table ip mape {
chain mape-clamp {
type filter hook forward priority mangle - 5; policy accept;
# TCP MSSの調整
iifname "ip6tnl0" counter tcp flags syn tcp option maxseg size set rt mtu
oifname "ip6tnl0" counter tcp flags syn tcp option maxseg size set rt mtu
}
chain mape-mark {
type filter hook postrouting priority filter - 5; policy accept;
# fwmarkの付与
oifname "ip6tnl0" meta l4proto tcp mark set mark & 0xffffff00 | 0x54 counter
oifname "ip6tnl0" meta l4proto udp mark set mark & 0xffffff00 | 0x55 counter
oifname "ip6tnl0" ip protocol icmp icmp type { echo-request } mark set mark & 0xffffff00 | 0x59 counter
}
chain mape-snat {
type nat hook postrouting priority srcnat - 5; policy accept;
# ソースNAT
oifname "ip6tnl0" meta l4proto { tcp, udp, icmp } counter snat to 192.0.2.1:32784-33023
}
}

起動時に実行すべき3tcコマンドも、スクリプトにまとめておく。

/usr/local/sbin/tc-ip6tnl0.sh
#!/usr/bin/env bash
DEVICE="ip6tnl0"
PSID=ab
# outbound
# TCP, UDP
tc filter add dev $DEVICE egress handle 0x55/0xff fw action csum ip4h udp continue
tc filter add dev $DEVICE egress handle 0x54/0xff fw action csum ip4h tcp continue
tc filter add dev $DEVICE egress handle 0x54/0xfe fw action pedit pedit munge ip sport set "0x0${PSID}0" retain 0x0ff0 continue
tc filter add dev $DEVICE egress u32 match mark 0x54 0x000000fe match ip sport 0x0010 0x0010 action pedit pedit munge ip sport set 0x1000 retain 0x1000 continue
tc filter add dev $DEVICE egress u32 match mark 0x54 0x000000fe match ip sport 0x0020 0x0020 action pedit pedit munge ip sport set 0x2000 retain 0x2000 continue
tc filter add dev $DEVICE egress u32 match mark 0x54 0x000000fe match ip sport 0x0040 0x0040 action pedit pedit munge ip sport set 0x4000 retain 0x4000 continue
tc filter add dev $DEVICE egress u32 match mark 0x54 0x000000fe match ip sport 0x0000 0x0080 action pedit pedit munge ip sport set 0x0000 retain 0x8000 continue
# ICMP echo request
tc filter add dev $DEVICE egress u32 match mark 0x59 0x000000ff action pedit pedit munge offset 24 u16 set "0x0${PSID}0" retain 0x0ff0 pipe action csum ip4h icmp continue
tc filter add dev $DEVICE egress u32 match mark 0x59 0x000000ff match u16 0x0010 0x0010 at 24 action pedit pedit munge offset 24 u16 set 0x1000 retain 0x1000 continue
tc filter add dev $DEVICE egress u32 match mark 0x59 0x000000ff match u16 0x0020 0x0020 at 24 action pedit pedit munge offset 24 u16 set 0x2000 retain 0x2000 continue
tc filter add dev $DEVICE egress u32 match mark 0x59 0x000000ff match u16 0x0040 0x0040 at 24 action pedit pedit munge offset 24 u16 set 0x4000 retain 0x4000 continue
tc filter add dev $DEVICE egress u32 match mark 0x59 0x000000ff match u16 0x0000 0x0080 at 24 action pedit pedit munge offset 24 u16 set 0x0000 retain 0x8000 continue
# inbound
# TCP, UDP
tc filter add dev $DEVICE ingress handle 0x65/0xff fw action csum ip4h udp continue
tc filter add dev $DEVICE ingress handle 0x64/0xff fw action csum ip4h tcp continue
tc filter add dev $DEVICE ingress handle 0x64/0xfe fw action pedit pedit munge ip dport set 0x8000 retain 0xf000 continue
tc filter add dev $DEVICE ingress u32 match mark 0x64 0x000000fe match ip dport 0x8000 0x8000 action pedit pedit munge ip dport set 0x0080 retain 0x0080 continue
tc filter add dev $DEVICE ingress u32 match mark 0x64 0x000000fe match ip dport 0x4000 0x4000 action pedit pedit munge ip dport set 0x0040 retain 0x0040 continue
tc filter add dev $DEVICE ingress u32 match mark 0x64 0x000000fe match ip dport 0x2000 0x2000 action pedit pedit munge ip dport set 0x0020 retain 0x0020 continue
tc filter add dev $DEVICE ingress u32 match mark 0x64 0x000000fe match ip dport 0x1000 0x1000 action pedit pedit munge ip dport set 0x0010 retain 0x0010 continue
tc filter add dev $DEVICE ingress u32 match mark 0x64 0x000000fe action pedit pedit munge ip dport set 0 retain 0x0ff0 continue
tc filter add dev $DEVICE ingress u32 match ip protocol 17 0xff match u16 0 1fff at 6 match ip dport "0x0${PSID}0" 0x0ff0 action skbedit mark 0x65/0xff continue
tc filter add dev $DEVICE ingress u32 match ip protocol 6 0xff match u16 0 1fff at 6 match ip dport "0x0${PSID}0" 0x0ff0 action skbedit mark 0x64/0xff continue
# ICMP echo reply (0)
tc filter add dev $DEVICE ingress handle 0x69/0xff fw action pedit pedit munge offset 24 u16 set 0x8000 retain 0xf000 pipe action csum ip4h icmp continue
tc filter add dev $DEVICE ingress u32 match mark 0x69 0x000000ff match u16 0x8000 0x8000 at 24 action pedit pedit munge offset 24 u16 set 0x0080 retain 0x0080 continue
tc filter add dev $DEVICE ingress u32 match mark 0x69 0x000000ff match u16 0x4000 0x4000 at 24 action pedit pedit munge offset 24 u16 set 0x0040 retain 0x0040 continue
tc filter add dev $DEVICE ingress u32 match mark 0x69 0x000000ff match u16 0x2000 0x2000 at 24 action pedit pedit munge offset 24 u16 set 0x0020 retain 0x0020 continue
tc filter add dev $DEVICE ingress u32 match mark 0x69 0x000000ff match u16 0x1000 0x1000 at 24 action pedit pedit munge offset 24 u16 set 0x0010 retain 0x0010 continue
tc filter add dev $DEVICE ingress u32 match mark 0x69 0x000000ff action pedit pedit munge offset 24 u16 set 0 retain 0x0ff0 continue
tc filter add dev $DEVICE ingress u32 match ip protocol 1 0xff match ip icmp_type 0 0xff match ip ihl 0x5 0xf match u16 0 1fff at 6 match u16 "0x0${PSID}0" 0x0ff0 at 24 action skbedit mark 0x69/0xff continue
# ICMP destination unreachable (3): port unreachable (UDP)
tc filter add dev $DEVICE ingress handle 0x79/0xff fw action pedit pedit munge offset 48 u16 set 0x8000 retain 0xf000 pipe action csum ip4h and icmp continue
tc filter add dev $DEVICE ingress u32 match mark 0x79 0x000000ff match ip dport 0x8000 0x8000 at 48 action pedit pedit munge offset 48 u16 set 0x0080 retain 0x0080 continue
tc filter add dev $DEVICE ingress u32 match mark 0x79 0x000000ff match ip dport 0x4000 0x4000 at 48 action pedit pedit munge offset 48 u16 set 0x0040 retain 0x0040 continue
tc filter add dev $DEVICE ingress u32 match mark 0x79 0x000000ff match ip dport 0x2000 0x2000 at 48 action pedit pedit munge offset 48 u16 set 0x0020 retain 0x0020 continue
tc filter add dev $DEVICE ingress u32 match mark 0x79 0x000000ff match ip dport 0x1000 0x1000 at 48 action pedit pedit munge offset 48 u16 set 0x0010 retain 0x0010 continue
tc filter add dev $DEVICE ingress handle 0x79/0xff fw action pedit pedit munge offset 48 u16 set 0 retain 0x0ff0 continue
tc filter add dev $DEVICE ingress u32 match ip protocol 1 0xff match ip icmp_type 3 0xff match ip ihl 0x5 0xf match ip ihl 0x5 0xf at 28 match u16 0 1fff at 6 match ip sport "0x0${PSID}0" 0x0ff0 at 48 action skbedit mark 0x79/0xff continue
# ICMP time-exceeded (11)
tc filter add dev $DEVICE ingress handle 0x89/0xff fw action pedit pedit munge offset 48 u16 set 0x8000 retain 0xf000 pipe action csum ip4h and icmp continue
tc filter add dev $DEVICE ingress u32 match mark 0x89 0x000000ff match ip dport 0x8000 0x8000 at 48 action pedit pedit munge offset 48 u16 set 0x0080 retain 0x0080 continue
tc filter add dev $DEVICE ingress u32 match mark 0x89 0x000000ff match ip dport 0x4000 0x4000 at 48 action pedit pedit munge offset 48 u16 set 0x0040 retain 0x0040 continue
tc filter add dev $DEVICE ingress u32 match mark 0x89 0x000000ff match ip dport 0x2000 0x2000 at 48 action pedit pedit munge offset 48 u16 set 0x0020 retain 0x0020 continue
tc filter add dev $DEVICE ingress u32 match mark 0x89 0x000000ff match ip dport 0x1000 0x1000 at 48 action pedit pedit munge offset 48 u16 set 0x0010 retain 0x0010 continue
tc filter add dev $DEVICE ingress handle 0x89/0xff fw action pedit pedit munge offset 48 u16 set 0 retain 0x0ff0 continue
tc filter add dev $DEVICE ingress u32 match ip protocol 1 0xff match ip icmp_type 11 0xff match ip ihl 0x5 0xf match ip ihl 0x5 0xf at 28 match u16 0 1fff at 6 match ip sport "0x0${PSID}0" 0x0ff0 at 48 action skbedit mark 0x89/0xff continue

このスクリプトを、ip6tnl0デバイスの存在が確認された時点で適用するよう、systemdサービスを設定する。

/etc/systemd/system/tc-ip6tnl0.service
[Unit]
Description=Apply custom tc rules
Wants=network-online.target
# 下のように書くと、このサービスはip6tnl0デバイスの設定完了後に実行される
After=network-online.target sys-subsystem-net-devices-ip6tnl0.device
BindsTo=sys-subsystem-net-devices-ip6tnl0.device
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/tc-ip6tnl0.sh
ExecStop=/usr/bin/tc filter del dev ip6tnl0 egress && /usr/bin/tc filter del dev ip6tnl0 ingress
[Install]
WantedBy=multi-user.target
Terminal window
systemctl enable tc-ip6tnl0
reboot

天地の運行が正常で、あなたが祝福されていれば、再起動後にIPv6 / IPv4の疎通が取れるはずだ。

Terminal window
> ping -c3 2606:4700:4700::1001
PING 2606:4700:4700::1001 (2606:4700:4700::1001) 56 data bytes
64 bytes from 2606:4700:4700::1001: icmp_seq=1 ttl=54 time=7.45 ms
64 bytes from 2606:4700:4700::1001: icmp_seq=2 ttl=54 time=7.19 ms
64 bytes from 2606:4700:4700::1001: icmp_seq=3 ttl=54 time=7.12 ms
--- 2606:4700:4700::1001 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 7.123/7.254/7.451/0.141 ms
> ping -c3 1.0.0.1
PING 1.0.0.1 (1.0.0.1) 56(84) bytes of data.
64 bytes from 1.0.0.1: icmp_seq=1 ttl=58 time=7.36 ms
64 bytes from 1.0.0.1: icmp_seq=2 ttl=58 time=7.17 ms
64 bytes from 1.0.0.1: icmp_seq=3 ttl=58 time=7.84 ms
--- 1.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 7.172/7.458/7.839/0.280 ms

宅内機器側インターフェースの設定

続いて宅内機器側のインターフェースhome)を設定する。IPv6はSLAAC + RDNSS、IPv4はDHCPv4でそれぞれ配布している。

/etc/systemd/network/10-home.network
[Match]
Name=home
[Network]
IPv6Forwarding=yes
IPv6AcceptRA=no
IPv6SendRA=yes
DHCPPrefixDelegation=yes
Address=10.0.0.1/24
DHCPServer=yes
MulticastDNS=yes
[DHCPPrefixDelegation]
# このセグメントに/64のサブネット(3fff:0:0:ab01::/64)を割り当てる
SubnetId=1
# ただしインターフェース自体にはアドレスを割り振らない
Assign=no
# 割り当てたサブネットとDNSサーバをRAで配布
[IPv6SendRA]
DNS=_link_local
RouterLifetimeSec=9000
DNSLifetimeSec=14400
[DHCPServer]
DNS=_server_address
PoolOffset=32
PoolSize=208
DefaultLeaseTimeSec=24h
MaxLeaseTimeSec=48h
# IPv4の固定もお手のもの
[DHCPServerStaticLease]
MACAddress=de:ad:be:ef:00:04
Address=10.0.0.2

先ほどWAN側wan0)にIPv6グローバルアドレスを付けたため、こちらのインターフェースには付けていない。

DNS=_link_localDNS=_server_address書いているのは、ルータ自身をDNSサーバとして広告するためだ。ルータ上でDNSキャッシュサーバ動かしているため、このようにした。

Terminal window
networkctl reload

設定を再度読み込み、クライアント端末をスイッチの適切なポートに繋げば、ルータとして動作していることがわかるはずだ。

お、この時点で接続した端末はインターネットに露出するので注意すること。

ファイアウォール設定

インターネット側から丸見えでは怖いので、基本的なファイアウォールを設定しておく。以下の設定はとりあえずのものだが、最低限の機能は果たしてくれるだろう。

/etc/nftables.conf
#!/usr/bin/nft -f
destroy table inet filter
table inet filter {
set LANv4 {
type ipv4_addr; flags interval;
elements = { 10.0.0.0/24 }
}
chain input {
type filter hook input priority filter; policy drop;
ct state invalid drop; ct state { established, related } accept;
iif lo accept
icmpv6 type {
destination-unreachable,
packet-too-big,
time-exceeded,
parameter-problem,
nd-neighbor-solicit,
nd-neighbor-advert
} limit rate 5/second burst 10 packets accept
icmpv6 type { nd-router-advert } iif "wan0" counter accept
icmpv6 type { echo-request, nd-router-solicit } iifname "home" counter accept
icmp type { destination-unreachable, time-exceeded, parameter-problem } limit rate 5/second burst 10 packets accept
icmp type { echo-request } iifname { "home" } accept
udp dport dhcpv6-client iif "wan0" ip6 saddr fe80::/64 counter accept
udp dport bootps iifname "home" accept
ip saddr @LANv4 iifname { "home" } jump lan-services
ip6 saddr fe80::/64 iifname { "home" } jump lan-services
counter
}
chain lan-services {
meta l4proto { tcp, udp } th dport { ssh, domain } accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state invalid drop; ct state { established, related } accept;
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem } counter accept
icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
meta l4proto { tcp, udp } th dport { 135, 137-139, 445 } oifname { "wan0", "ip6tnl0" } counter drop comment "NetBIOS"
iifname "home" oif "wan0" accept comment "v6"
iifname "home" oifname "ip6tnl0" accept comment "v4"
counter
}
}
include "/etc/nftables.d/*.nft"
Terminal window
systemctl enable --now nftables

以上で、基本的なルータとしての機能を実現できた。最終回・第三部では、HGWをルータの下流に設置し、今回の記事の最終目標を達成する。


  1. ちなみにclsactいうのは、それ自体は特に何もしない(フィルタなどをアタッチするためだけ)のqdiscで、ingressにもegressにも使えるらしい

  2. とはいえ少しだけ改変している。主な差異は以下の通り:1. tcのqdiscとしてclsact使っている / 2. qdiscはsystemd-networkdですでに追加済み / 3. fwmarkの下位2桁以外を無視している / 4. traceroute必要な、ICMP Time Exceeded(11)を処理している

  3. tcコマンドの効果は永続しないため、再起動ごとに実行する必要がある