Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pd] add pd daemon service to support the automation of dhcpcd PD management #2360

Merged
merged 12 commits into from
Nov 7, 2024
Merged
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
yangsong-cnyn marked this conversation as resolved.
Show resolved Hide resolved
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
sherysheng marked this conversation as resolved.
Show resolved Hide resolved
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()
sherysheng marked this conversation as resolved.
Show resolved Hide resolved
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

Loading