Skip to content

Commit

Permalink
[pd] add pd daemon service to support the automation of dhcpcd PD man…
Browse files Browse the repository at this point in the history
…agement (openthread#2360)

The changes include:
- PD daemon (dhcp6_pd_daemon.py) to monitor and react to PD state
  changes between running and stopped/disabled.
- Creation of systemd service file (dhcp6_pd_daemon.service) for
  managing the daemon.
- Update of _dhcpv6_pd_ref to support pd daemon installation and
  uninstallation

This scripts essentially automate the process of setting up a machine
as a dhcpcd pd reference device, handling prefix delegation.
  • Loading branch information
sherysheng authored Nov 7, 2024
1 parent 15409fd commit 6934ff0
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 20 deletions.
104 changes: 90 additions & 14 deletions script/_dhcpv6_pd_ref
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,115 @@
# This script manipulates DHCPv6-PD-REF configuration.
#

customise_dhcpcd_conf()
# TODO: set the upstream interface according to the environment variables of `script/setup`.
UPSTREAM_INTERFACE="eth0"

DHCPCD_ENTER_HOOK="/etc/dhcpcd.enter-hook"
DHCPCD_EXIT_HOOK="/etc/dhcpcd.exit-hook"

PD_DAEMON_DIR="/opt/pd-daemon"
PD_DAEMON_PATH="${PD_DAEMON_DIR}/dhcp6_pd_daemon.py"
PD_DAEMON_SERVICE_NAME="dhcp6_pd_daemon.service"
PD_DAEMON_SERVICE_PATH="/etc/systemd/system/${PD_DAEMON_SERVICE_NAME}"

DHCP_CONFIG_PATH="/etc/dhcpcd.conf"
DHCP_CONFIG_ORIG_PATH="/etc/dhcpcd.conf.orig"
DHCP_CONFIG_PD_PATH="/etc/dhcpcd.conf.pd"
DHCP_CONFIG_NO_PD_PATH="/etc/dhcpcd.conf.no-pd"

# Create dhcpcd configuration file with ipv6 prefix request.
create_dhcpcd_conf_pd()
{
# This has to be run after script/_border_routing, and this will
# invalidate all changes to dhcpcd.conf made by script/_border_routing.
sudo tee /etc/dhcpcd.conf >/dev/null <<EOF
sudo tee ${DHCP_CONFIG_PD_PATH} >/dev/null <<EOF
noipv6rs # disable router solicitation
interface eth0
interface ${UPSTREAM_INTERFACE}
iaid 1
ia_pd 2/::/64 -
release
# Disable Router Solicitations (RS) again, specifically for ${UPSTREAM_INTERFACE}.
# This ensures that accept_ra is prevented from being set to 0, allowing
# the interface to accepting Router Advertisements and configuring IPv6
# based on them. The exact reason for requiring 'noipv6rs' twice
# is not fully understood but has been observed to be necessary through
# experimentation.
noipv6rs
EOF
}

# Create dhcpcd configuration file with no prefix request.
create_dhcpcd_conf_no_pd()
{
sudo tee ${DHCP_CONFIG_NO_PD_PATH} >/dev/null <<EOF
noipv6rs # disable router solicitation
EOF
}

create_dhcp6_pd_daemon_service()
{
sudo tee ${PD_DAEMON_SERVICE_PATH} <<EOF
[Unit]
Description=Daemon to manage dhcpcd based on otbr-agent's PD state change
After=multi-user.service
ConditionPathExists=${PD_DAEMON_PATH}
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 ${PD_DAEMON_PATH}
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
}

dhcpv6_pd_ref_uninstall()
{
with DHCPV6_PD_REF || return 0

if [[ -f "/etc/dhcpcd.conf.orig" ]]; then
sudo mv /etc/dhcpcd.conf.orig /etc/dhcpcd.conf
if have systemctl; then
sudo systemctl disable ${PD_DAEMON_SERVICE_NAME} || true
sudo systemctl stop ${PD_DAEMON_SERVICE_NAME} || true
sudo rm -f ${PD_DAEMON_SERVICE_PATH} || true
fi
sudo systemctl restart dhcpcd
sudo rm -f /etc/dhcpcd.enter-hook /etc/dhcpcd.exit-hook

if [[ -f ${DHCP_CONFIG_ORIG_PATH} ]]; then
sudo mv ${DHCP_CONFIG_ORIG_PATH} ${DHCP_CONFIG_PATH}
fi

sudo rm -f ${DHCPCD_ENTER_HOOK} ${DHCPCD_EXIT_HOOK}
sudo rm -f ${PD_DAEMON_PATH}

if have systemctl; then
sudo systemctl daemon-reload

if systemctl is-active dhcpcd; then
sudo systemctl restart dhcpcd || true
fi
fi
}

dhcpv6_pd_ref_install()
{
with DHCPV6_PD_REF || return 0

if [[ -f "/etc/dhcpcd.conf" ]]; then
sudo mv /etc/dhcpcd.conf /etc/dhcpcd.conf.orig
if [[ -f ${DHCP_CONFIG_PATH} ]]; then
sudo mv ${DHCP_CONFIG_PATH} ${DHCP_CONFIG_ORIG_PATH}
fi
customise_dhcpcd_conf

# Add dhcpcd.hooks
sudo install -m 755 "$(dirname "$0")"/reference-device/dhcpcd.enter-hook /etc/dhcpcd.enter-hook
sudo install -m 755 "$(dirname "$0")"/reference-device/dhcpcd.exit-hook /etc/dhcpcd.exit-hook
sudo install -m 755 "$(dirname "$0")"/reference-device/dhcpcd.enter-hook ${DHCPCD_ENTER_HOOK}
sudo install -m 755 "$(dirname "$0")"/reference-device/dhcpcd.exit-hook ${DHCPCD_EXIT_HOOK}
sudo mkdir -p ${PD_DAEMON_DIR}
sudo install -m 755 "$(dirname "$0")"/reference-device/dhcp6_pd_daemon.py ${PD_DAEMON_PATH}

create_dhcpcd_conf_pd
create_dhcpcd_conf_no_pd
create_dhcp6_pd_daemon_service

if have systemctl; then
sudo systemctl daemon-reload
sudo systemctl enable ${PD_DAEMON_SERVICE_NAME}
sudo systemctl start ${PD_DAEMON_SERVICE_NAME}
fi
}
143 changes: 143 additions & 0 deletions script/reference-device/dhcp6_pd_daemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024, The OpenThread Authors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

import logging
import dbus
import gi.repository.GLib as GLib
import subprocess
import threading
import os

from dbus.mainloop.glib import DBusGMainLoop

DBusGMainLoop(set_as_default=True)

logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')

bus = dbus.SystemBus()
intended_dhcp6pd_state = None

DHCP_CONFIG_PATH = "/etc/dhcpcd.conf"
DHCP_CONFIG_PD_PATH = "/etc/dhcpcd.conf.pd"
DHCP_CONFIG_NO_PD_PATH = "/etc/dhcpcd.conf.no-pd"


def restart_dhcpcd_service(config_path):
if not os.path.isfile(config_path):
logging.error(f"{config_path} not found. Cannot apply configuration.")
return
try:
subprocess.run(["sudo", "cp", config_path, DHCP_CONFIG_PATH],
check=True)
subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True)
subprocess.run(["sudo", "service", "dhcpcd", "restart"], check=True)
logging.info(
f"Successfully restarted dhcpcd service with {config_path}.")
except subprocess.CalledProcessError as e:
logging.error(f"Error restarting dhcpcd service: {e}")


def restart_dhcpcd_with_pd_config():
global intended_dhcp6pd_state
restart_dhcpcd_service(DHCP_CONFIG_PD_PATH)
intended_dhcp6pd_state = None


def restart_dhcpcd_with_no_pd_config():
restart_dhcpcd_service(DHCP_CONFIG_NO_PD_PATH)


def properties_changed_handler(interface_name, changed_properties,
invalidated_properties):
global intended_dhcp6pd_state
if "Dhcp6PdState" not in changed_properties:
return
new_state = changed_properties["Dhcp6PdState"]
logging.info(f"Dhcp6PdState changed to: {new_state}")
if new_state == "running" and intended_dhcp6pd_state != "running":
intended_dhcp6pd_state = "running"
thread = threading.Thread(target=restart_dhcpcd_with_pd_config)
thread.start()
elif new_state in ("stopped", "idle",
"disabled") and intended_dhcp6pd_state is None:
restart_dhcpcd_with_no_pd_config()


def connect_to_signal():
try:
dbus_obj = bus.get_object('io.openthread.BorderRouter.wpan0',
'/io/openthread/BorderRouter/wpan0')
properties_dbus_iface = dbus.Interface(
dbus_obj, 'org.freedesktop.DBus.Properties')
dbus_obj.connect_to_signal(
"PropertiesChanged",
properties_changed_handler,
dbus_interface=properties_dbus_iface.dbus_interface)
logging.info("Connected to D-Bus signal.")
return dbus_obj
except dbus.DBusException as e:
logging.error(f"Error connecting to D-Bus: {e}")
return None


def check_and_reconnect(dbus_obj):
if dbus_obj is None:
connect_to_signal()


def main():
# Ensure dhcpcd is running in its last known state. This addresses a potential race condition
# during system startup due to the loop dependency in dhcpcd-radvd-network.target.
#
# - network.target activation relies on the completion of dhcpcd start
# - during bootup, dhcpcd tries to start radvd with PD enabled before network.target is
# active, which leads to a timeout failure
# - so we will prevent radvd from starting before target.network is active
#
# By restarting dhcpcd here, we ensure it runs after network.target is active, allowing
# radvd to start correctly and dhcpcd to configure the interface.
try:
subprocess.run(["sudo", "systemctl", "reload-or-restart", "dhcpcd"],
check=True)
logging.info("Successfully restarting dhcpcd service.")
except subprocess.CalledProcessError as e:
logging.error(f"Error restarting dhcpcd service: {e}")

loop = GLib.MainLoop()

thread_dbus_obj = connect_to_signal()

GLib.timeout_add_seconds(5, check_and_reconnect, thread_dbus_obj)

loop.run()


if __name__ == '__main__':
main()
13 changes: 10 additions & 3 deletions script/reference-device/dhcpcd.enter-hook
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,16 @@ if [ ${interface} = ${UPSTREAM_INTERFACE} ]; then

case $reason in
DELEGATED6 | REBIND6 | RENEW6 | BOUND6 )
logger "$LOG_TAG prefix is $new_dhcp6_ia_pd1_prefix1 length is $new_dhcp6_ia_pd1_prefix1_length pltime is $new_dhcp6_ia_pd1_prefix1_pltime vltime is $new_dhcp6_ia_pd1_prefix1_vltime"
config_ra $new_dhcp6_ia_pd1_prefix1 $new_dhcp6_ia_pd1_prefix1_length $new_dhcp6_ia_pd1_prefix1_pltime $new_dhcp6_ia_pd1_prefix1_vltime
sudo systemctl reload radvd || logger "$LOG_TAG Failed to reload radvd"
if [ -z "$new_dhcp6_ia_pd1_prefix1" ] || [ -z "$new_dhcp6_ia_pd1_prefix1_length" ] || \
[ -z "$new_dhcp6_ia_pd1_prefix1_pltime" ] || [ -z "$new_dhcp6_ia_pd1_prefix1_vltime" ]; then
logger "$LOG_TAG WARNING: Missing DHCPv6 prefix information. Skipping radvd configuration."
else
logger "$LOG_TAG prefix is $new_dhcp6_ia_pd1_prefix1 length is $new_dhcp6_ia_pd1_prefix1_length pltime is $new_dhcp6_ia_pd1_prefix1_pltime vltime is $new_dhcp6_ia_pd1_prefix1_vltime"
config_ra $new_dhcp6_ia_pd1_prefix1 $new_dhcp6_ia_pd1_prefix1_length $new_dhcp6_ia_pd1_prefix1_pltime $new_dhcp6_ia_pd1_prefix1_vltime
if systemctl is-active network.target; then
sudo systemctl reload-or-restart radvd || logger "$LOG_TAG Failed to reload radvd"
fi
fi
;;
esac
fi
11 changes: 8 additions & 3 deletions script/reference-device/dhcpcd.exit-hook
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,14 @@ if [ ${interface} = ${UPSTREAM_INTERFACE} ]; then

case $reason in
EXPIRE6 | STOP6)
config_ra $old_dhcp6_ia_pd1_prefix1 $old_dhcp6_ia_pd1_prefix1_length 0 0
sudo systemctl reload radvd
if [ -z "$old_dhcp6_ia_pd1_prefix1" ] || [ -z "$old_dhcp6_ia_pd1_prefix1_length" ]; then
logger "$LOG_TAG WARNING: Missing DHCPv6 prefix information. Skipping radvd configuration."
else
config_ra $old_dhcp6_ia_pd1_prefix1 $old_dhcp6_ia_pd1_prefix1_length 0 0
if systemctl is-active network.target; then
sudo systemctl reload-or-restart radvd
fi
fi
;;
esac
fi

0 comments on commit 6934ff0

Please sign in to comment.