手順3:HGWの 設置
HGW側インターフェースの 設定
HGW側インターフェースhgw
)のhgw
は
br0
ブリッジのメンバー - IPv6の
デフォルトゲートウェイ - DHCPv6サーバ
の
送信する
> rdisc6 -1 br0Soliciting ff02::2 (ff02::2) on br0...
Hop limit : 64 ( 0x40)Stateful address conf. : YesStateful other conf. : YesMobile home agent : NoRouter preference : mediumNeighbor discovery proxy : NoRouter lifetime : 1800 (0x00000708) secondsReachable time : 300000 (0x000493e0) millisecondsRetransmit time : 10000 (0x00002710) milliseconds Source link-layer address: XX:XX:XX:XX:XX:XX MTU : 1500 bytes (valid) from fe80::xxxx:xxxx:xxxx:xxxx
これを
[Match]Name=hgw
[Network]Bridge=br0LinkLocalAddressing=ipv6IPv6StableSecretAddress=xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxxIPv6Forwarding=yesIPv6SendRA=yes
# フレッツ網に似せたRAを射出する[IPv6SendRA]# M-flag、O-flagを有効にしておくManaged=yesOtherInformation=yesHopLimit=64RouterLifetimeSec=1800ReachableTimeSec=300RetransmitSec=10
ブリッジの 転送ルール設定
続いて、br0
)の
#!/usr/sbin/nft -fdestroy table bridge brouter
table bridge brouter { chain prerouting { type filter hook prerouting priority -300; policy accept; # IPv6パケットをルーティングに回す iifname hgw ether type ip6 counter meta broute set 1 accept; counter }
chain forward { type filter hook forward priority 0; policy drop; ct state { established, related } counter accept; # IPv4パケットのブリッジ通過を許可 iifname hgw oif wan0 ether type { ip, arp } counter accept; iif wan0 oifname hgw ether type { ip, arp } counter accept; counter }}
処理の
IPv4パケットの 流れ
hgw
から入った 上りパケットは、 ( hgw
がbr0
のメンバーである ため) bridgeレイヤへ 向かう - preroutingフックを
通過し、 forwardフックに 入る - forwardフックで
IPv4 ( ip
/arp
)パケットのみを 明示的に 許可している ため、 これも 通過し、 wan0
からフレッツ網に 出ていく
下りに
IPv6パケットの 流れ
hgw
から入った 上りパケットは、 同じく bridgeレイヤへ 向かう - preroutingフックで
meta broute
に1
がセットされる ため、 ルーティングに 回される - 通常の
nftables (IPレイヤ)の 処理に 入り、 hgw
から来た パケットと して 扱われる - IPレイヤの
forwardフックで 転送が 許可されている (直後で 設定する) ため、 br0
からフレッツ網に 出ていく
下りはbr0
宛てにhgw
に
ファイアウォールの 設定更新
HGWからの
変更点
#!/usr/bin/nft -f
destroy table inet filter
table inet filter { # (中略) chain input { type filter hook input priority filter; policy drop;
ct state invalid drop comment "early drop of invalid connections" ct state { established, related } accept comment "allow tracked connections" iif lo accept comment "allow from loopback" ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply } limit rate 5/second burst 10 packets accept ip6 nexthdr icmpv6 ip6 saddr fe80::/10 icmpv6 type { nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, 148, 149 } iifname "br0" accept ip6 nexthdr icmpv6 icmpv6 type { mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report, 148, 149, 151, 152, 153 } iifname "home" accept ip6 nexthdr icmpv6 icmpv6 type { mld-listener-query, mld-listener-report, mld-listener-done, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report, 148, 149, 151, 152, 153 } iifname { "home", "hgw" } accept ip protocol icmp accept comment "allow icmp" udp dport bootps ip saddr 0.0.0.0 ip daddr 255.255.255.255 iifname "home" accept comment "allow dhcpv4" udp dport dhcpv6-server ip6 saddr fe80::/10 ip6 daddr ff02::1:2 iifname "home" accept comment "allow dhcpv6" udp dport dhcpv6-server ip6 saddr fe80::/10 ip6 daddr ff02::1:2 iifname { "home", "hgw" } accept comment "allow dhcpv6" udp dport dhcpv6-client iifname "br0" accept ip saddr @LANv4 jump lan_input comment "allow private services" ip6 saddr @LANv6 iifname "home" jump lan_input comment "allow private services"
counter }
chain forward { type filter hook forward priority filter; policy drop;
iifname "home" ip protocol tcp tcp flags != syn / fin,syn,rst,ack ct state new counter reject with tcp reset meta protocol ip6 iifname "home" tcp flags != syn / fin,syn,rst,ack ct state new counter accept meta protocol ip6 iifname { "home", "hgw" } tcp flags != syn / fin,syn,rst,ack ct state new counter accept ct state invalid drop ct state { established, related } accept ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } accept ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply } accept
meta l4proto { tcp, udp } th dport { 135, 137-139, 445 } oifname { "br0", "mape0" } counter drop iifname "home" oifname { "br0", "mape0" } accept iifname "hgw" oifname "br0" accept
counter }}
include "/etc/nftables.d/*.nft"
DHCPv6サーバの 設定
DHCPv6サーバ
{ "Dhcp6": { "interfaces-config": { "interfaces": [ "hgw" ] }, "control-socket": { "socket-type": "unix", "socket-name": "kea6-ctrl-socket" }, "lease-database": { "type": "memfile", "lfc-interval": 3600 }, "expired-leases-processing": { "reclaim-timer-wait-time": 10, "flush-reclaimed-timer-wait-time": 25, "hold-reclaimed-time": 3600, "max-reclaim-leases": 100, "max-reclaim-time": 250, "unwarned-reclaim-cycles": 5 }, // フレッツ網を模倣 "renew-timer": 7200, "rebind-timer": 10800, "preferred-lifetime": 12600, "valid-lifetime": 14400, "hooks-libraries": [ // DHCPリースの変動時にスクリプトを実行するフック { "library": "/usr/lib/kea/hooks/libdhcp_run_script.so", "parameters": { "name": "/usr/share/kea/scripts/pd-route.sh", "sync": false } } ], "subnet6": [], "loggers": [ { "name": "kea-dhcp6", "output-options": [ { "output": "kea-dhcp6.log" } ], "severity": "INFO", "debuglevel": 0 } ] }}
払い出すIPv6プレフィックスや、
ただし、
#!/bin/shPROTO=static
remove_ipv6_routes() { if [ "$LEASE6_TYPE" = "IA_PD" ]; then ip -6 route delete "${LEASE6_ADDRESS}/${LEASE6_PREFIX_LEN}" proto "${PROTO}" fi}
on_commit() { if [ "$LEASES6_SIZE" -gt 0 ]; then if [ "$LEASES6_AT0_TYPE" = "IA_PD" ]; then ip -6 route replace "${LEASES6_AT0_ADDRESS}/${LEASES6_AT0_PREFIX_LEN}" via "${QUERY6_REMOTE_ADDR}" dev "${QUERY6_IFACE_NAME}" proto "${PROTO}" fi fi}
case "$1" in"lease6_release" | "lease6_expire") remove_ipv6_routes ;;"leases6_committed") on_commit ;;esac
systemctl enable --now kea-dhcp6
また、
{ "Control-agent": { "http-host": "127.0.0.1", "http-port": 8000, "control-sockets": { "dhcp4": { "socket-type": "unix", "socket-name": "kea4-ctrl-socket" }, "dhcp6": { "socket-type": "unix", "socket-name": "kea6-ctrl-socket" }, "d2": { "socket-type": "unix", "socket-name": "kea-ddns-ctrl-socket" } }, "hooks-libraries": [], "loggers": [ { "name": "kea-ctrl-agent", "output-options": [ { "output": "kea-ctrl-agent.log" } ], "severity": "INFO", "debuglevel": 0 } ] }}
systemctl enable --now kea-ctrl-agent
ひかり電話関連情報の 受信
第二部のように、
- Option 17, 22, 23, 24, 31を
要求し - Option 16に
適切な 値を 含める
ことで、
リンク先資料の
Option Request に
指定可能な オプション番号は、 ”17”,”22”,”23”,”24”,”31”です
そして
端末への
設定自動化の ための ネットワーク関連情報通知機能を 取得する 場合は、 端末からの 要求メッセージにて、 Option Requestオプション (6)に オプション17を 含め、 Vendor Classオプション(16)に 必要な 情報 (端末情報)を 設定し、 網に 送信します
何やら
ネットワーク関連情報を
設定する 端末の ハードウェアアドレス (MACアドレス)を 設定
と
これを
# ...[DHCPv6]DUIDType=link-layerIAID=0SendHostname=noUseCaptivePortal=noUseDNR=no# これがなければsystemd-networkdはOption 24を無視するUseDomains=yesRequestOptions=17 22 23 24 31# 一見文字列しかダメそうだが、実はhexも送れるVendorClass=\xde\xad\xbe\xef\x00\x05# ...
HGWのde:ad:be:ef:00:05
と
> ip -6 neigh show dev hgw | awk '{print $3}'(中略)de:ad:be:ef:00:05
などと
設定を
networkctl reloadbusctl -j call org.freedesktop.network1 /org/freedesktop/network1 org.freedesktop.network1.Manager Describe | jq '.data[0] | fromjson | .Interfaces[] | select(.DHCPv6Client) | .DHCPv6Client.VendorSpecificOptions'
ひかり電話関連情報が
[ { "EnterpriseId": 210, "SubOptionCode": 202, "SubOptionData": "XXXXXXXXXX" }, { "EnterpriseId": 210, "SubOptionCode": 204, "SubOptionData": "XXXXXXXXXX" }, { "EnterpriseId": 210, "SubOptionCode": 201, "SubOptionData": "deadbeef0005" }, { "EnterpriseId": 210, "SubOptionCode": 210, "SubOptionData": "XXXXXXXXXX" }]
それぞれの
ひかり電話関連情報の 配布
最後に、
設定は
スクリプト(長いので注意)
#!/usr/bin/env python
from asyncio import Event, create_task, runfrom ipaddress import IPv6Addressfrom json import loadsfrom time import sleepfrom typing import TypedDict
from requests import exceptions, postfrom sdbus import ( DbusInterfaceCommonAsync, dbus_method_async, dbus_signal_async, sd_bus_open_system, set_default_bus,)
SYSTEMD_IF = "org.freedesktop.systemd1"SYSTEMD_NODE_PATH = "/org/freedesktop/systemd1"SYSTEMD_MGR_IF = "org.freedesktop.systemd1.Manager"
NETWORKD_IF = "org.freedesktop.network1"NETWORKD_NODE_PATH = "/org/freedesktop/network1"NETWORKD_MGR_IF = "org.freedesktop.network1.Manager"NETWORKD_LINK_IF = "org.freedesktop.network1.Link"NETWORKD_DHCP6C_IF = "org.freedesktop.network1.DHCPv6Client"
DHCP6C_ETH = "br0"HGW_ETH = "hgw"
KEA_API = "http://127.0.0.1:8000"KEA_SERVICE = "kea-dhcp6.service"KEA_MAX_RETRIES = 10KEA_RETRY_DELAY = 5
# SIPサーバをD-Busで取得するにはsystemd v258が必要。それまではハードコードしておくSIP_SERVERS = ["YOUR_SIP_SERVER_ADDR"]
class Networkd(DbusInterfaceCommonAsync, interface_name=NETWORKD_MGR_IF): def __init__(self) -> None: super().__init__() self._proxify(NETWORKD_IF, NETWORKD_NODE_PATH)
@dbus_method_async("s", "io") async def get_link_by_name(self, name: str) -> tuple[int, str]: raise NotImplementedError
class NetworkdLink(DbusInterfaceCommonAsync, interface_name=NETWORKD_LINK_IF): def __init__(self, object_path: str) -> None: super().__init__() self._proxify(NETWORKD_IF, object_path)
@dbus_method_async(result_signature="s") async def describe(self) -> str: raise NotImplementedError
class Systemd(DbusInterfaceCommonAsync, interface_name=SYSTEMD_MGR_IF): def __init__(self, object_path: str) -> None: super().__init__() self._proxify(SYSTEMD_IF, SYSTEMD_NODE_PATH)
@dbus_signal_async(signal_signature="uoss") def job_removed(self) -> tuple[int, str, str, str]: raise NotImplementedError
class VendorSpecificOption(TypedDict): EnterpriseId: int SubOptionCode: int SubOptionData: str
class Dhcp6(TypedDict): prefix: IPv6Address dns: str ntp: str sip: str domains: str opts: list[VendorSpecificOption]
class Dhcp6State: def __init__(self): self.state: Dhcp6 | None = None
def get(self) -> Dhcp6 | None: return self.state
async def set(self, linkpath: str): dhcp6 = loads(await NetworkdLink(linkpath).describe()) dns, ntp, sip = [ ", ".join([IPv6Address(bytes(e["Address"])).compressed for e in dhcp6[k]]) for k in ["DNS", "NTP"] ] + SIP_SERVERS # dns, ntp, sip = [ # ", ".join([IPv6Address(bytes(e["Address"])).compressed for e in dhcp6[k]]) # for k in ["DNS", "NTP", "SIP"] # ] self.state = { "prefix": IPv6Address(bytes(dhcp6["DHCPv6Client"]["Prefixes"][0]["Prefix"])), "dns": dns, "ntp": ntp, "sip": sip, "domains": ", ".join([f"{e['Domain']}." for e in dhcp6["SearchDomains"]]), "opts": dhcp6["DHCPv6Client"]["VendorSpecificOptions"], }
return self.state
def update_kea_config(dhcp6: Dhcp6): def cmd(cmd: str, config: dict[str, str] | None): return post( KEA_API, json={"command": cmd, "service": ["dhcp6"], "arguments": {"Dhcp6": config}}, timeout=3, )
def get_config(): for i in range(KEA_MAX_RETRIES): try: resp = cmd("config-get", None) json = resp.json()[0] if resp.status_code == 200 and json["result"] == 0: return resp.json()[0]["arguments"]["Dhcp6"] else: raise exceptions.ConnectionError except exceptions.ConnectionError: print( f"Kea API not reachable, retrying in {KEA_RETRY_DELAY} seconds... ({i + 1}/{KEA_MAX_RETRIES})" ) sleep(KEA_RETRY_DELAY) print("Kea API is not reachable after maximum retries.") return None
config = get_config()
if config is None: return
config["subnet6"] = [ { "id": 1, "subnet": f"{dhcp6['prefix'] + (0xEF << 64)}/64", "interface": HGW_ETH, "pools": [{"pool": f"{dhcp6['prefix'] + (0xEF << 64)}/64"}], "pd-pools": [ { "prefix": f"{dhcp6['prefix'] + (0xF0 << 64)}", "prefix-len": 60, "delegated-len": 60, } ], } ]
config["option-def"] = [ { "name": f"ntt-{opt['SubOptionCode']}", "code": opt["SubOptionCode"], "type": "binary", "space": f"vendor-{opt['EnterpriseId']}", } for opt in dhcp6["opts"] ]
config["option-data"] = [ { "code": opt["SubOptionCode"], "always-send": True, "space": f"vendor-{opt['EnterpriseId']}", "csv-format": False, "data": opt["SubOptionData"], } for opt in dhcp6["opts"] ] + [ { "name": "vendor-opts", "data": f"{dhcp6['opts'][0]['EnterpriseId']}", }, {"name": "sip-server-addr", "data": dhcp6["sip"]}, {"name": "dns-servers", "data": dhcp6["dns"]}, {"name": "domain-search", "data": dhcp6["domains"]}, {"name": "sntp-servers", "data": dhcp6["ntp"]}, ]
test_config = cmd("config-test", config).json()[0]
print(f"Kea: {test_config['text']}")
if test_config["result"] != 0: return
set_config = cmd("config-set", config).json()[0]
if set_config["result"] != 0: print(f"Error: {set_config['text']}") return
print(f"Kea: {set_config['text']} Hash: {set_config['arguments']['hash']}")
async def observe_dhcp6_lease(dhcp6_state: Dhcp6State): async for linkpath, ( iface, props, _, ) in DbusInterfaceCommonAsync.properties_changed.catch_anywhere(NETWORKD_IF): if iface != NETWORKD_DHCP6C_IF or props["State"][1] != "bound": continue
print(f"DHCPv6 lease acquired on {linkpath}, updating Kea configuration...")
dhcp6 = await dhcp6_state.set(linkpath) update_kea_config(dhcp6)
async def observe_kea_restart(dhcp6_state: Dhcp6State): async for _, ( _, _, service, signal, ) in Systemd.job_removed.catch_anywhere(SYSTEMD_IF): if service != KEA_SERVICE or signal != "done": continue
print(f"{KEA_SERVICE} restarted, updating Kea configuration...")
dhcp6 = dhcp6_state.get()
if dhcp6 is None: print("No DHCPv6 state available, fetching from Networkd...") _, linkpath = await Networkd().get_link_by_name(DHCP6C_ETH) print(f"Link path: {linkpath}") dhcp6 = await dhcp6_state.set(linkpath)
update_kea_config(dhcp6)
async def main(): set_default_bus(sd_bus_open_system())
print("Start DHCPv6 proxying...")
dhcp6_state_manager = Dhcp6State()
task1 = create_task(observe_dhcp6_lease(dhcp6_state_manager)) task2 = create_task(observe_kea_restart(dhcp6_state_manager))
await task1 await task2
_ = await Event().wait()
if __name__ == "__main__": try: run(main()) except KeyboardInterrupt: print("\nExiting...") except Exception as e: print(f"Error: {e}")
スクリプト内にも
あとは
[Unit]Description=Networkd DHCPv6 lease info relay for KeaAfter=network.target sys-subsystem-net-devices-br0.deviceBindsTo=sys-subsystem-net-devices-br0.device
[Service]ExecStart=/usr/local/bin/networkd-kea6-relayRestart=alwaysRestartSec=5Environment=PYTHONUNBUFFERED=1
[Install]WantedBy=multi-user.target
systemctl enable --now networkd-kea6-relay
動作検証
IPv6 IPoE
VDSL回線
主要な
IPv4 over IPv6
速度・遅延にtc
を
HGW・ ひかり電話
HGWの
ハードウェアの 状態
ルータの
今後の 課題
- IPv6プレフィックス変更への
対応 - ごく
まれに 工事などで 変更される 場合が あるらしい - ルール計算を
自動化すれば 問題ない?
- ごく
- VDSLを
やめたい - 集合住宅を
脱出、 も しくは 爆破
- 集合住宅を
まとめ
みなさんも
-
DHCPv6では
デフォルトゲートウェイを 通知できないため ↩ -
これは
かつての ebtablesや br_netfilterを 代替するらしい ↩ -
あまり詳しくは
追えていないが、 そのような 振る 舞いを しているように 見える ↩ -
「IP通信網サービスの
インタフェース」 第三分冊 (第45版)、 2025年8月1日 最終閲覧 ↩ -
実際、
HGWが 送っている DHCPv6リクエストを キャプチャしてみると、 この 通りに なっている。 と いうより 本来は 順序が 逆で、 先に キャプチャを した ことで 技術資料の 意味が 明らかに なったのだが、 その点は 置いておく ↩ -
手元の
HGW (RX-600KI)の 場合、 「情報 > 現在の 状態」から 確認できた ↩ -
仕方が
ないのでコントリビュートした ↩ -
tshark -i br0 -f "udp src port 547" -Y "dhcpv6.msgtype == 7 and dhcpv6.sip_server_a" -T ek -e dhcpv6.sip_server_a
↩