Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion net/linuxptp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk

PKG_NAME:=linuxptp
PKG_VERSION:=4.4
PKG_RELEASE:=1
PKG_RELEASE:=2

PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tgz
PKG_SOURCE_URL:=https://downloads.nwtime.org/linuxptp/
Expand Down Expand Up @@ -38,15 +38,34 @@ define Package/linuxptp/description
computers.
endef

define Package/linuxptp/conffiles
/etc/config/linuxptp
endef

EXTRA_CFLAGS += -DHAVE_CLOCK_ADJTIME -DHAVE_POSIX_SPAWN -DHAVE_ONESTEP_SYNC

MAKE_VARS += \
EXTRA_CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS) $(EXTRA_CFLAGS)" \
KBUILD_OUTPUT="$(LINUX_DIR)" \
NO_AUTODETECT_SAD_MAC_LIB="y"

define Build/Compile/sysclock-discipline-check
$(TARGET_CC) $(TARGET_CFLAGS) -o $(PKG_BUILD_DIR)/sysclock-discipline-check $(CURDIR)/files/sysclock-discipline-check.c $(TARGET_LDFLAGS)
endef

define Build/Compile
$(call Build/Compile/Default)
$(call Build/Compile/sysclock-discipline-check)
endef

define Package/linuxptp/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DATA) $(CURDIR)/files/linuxptp.config $(1)/etc/config/linuxptp
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/phc2sys.init $(1)/etc/init.d/phc2sys
$(INSTALL_BIN) $(CURDIR)/files/ptp4l.init $(1)/etc/init.d/ptp4l
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/sysclock-discipline-check $(1)/usr/sbin/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/hwstamp_ctl $(1)/usr/sbin/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/phc2sys $(1)/usr/sbin/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/phc_ctl $(1)/usr/sbin/
Expand Down
45 changes: 45 additions & 0 deletions net/linuxptp/files/linuxptp.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
config ptp4l 'ptp4l' # configures the ptp4l daemon
#option role 'auto' # role to use: 'auto' (default) - BMCA algo determines role
# 'client' - will never attempt to be a server
# 'server' - will never attempt to be a client

#option loglevel '6' # Logging verbosity for ptp4l (0-7). 6=INFO (default), 7=DEBUG.

#option require_clock_sync '0' # 0 - skip waiting for kernel to report clock is sync'd (default unless server)
# 1 - require clock-in-sync check before starting (default if server)

#option clock_sync_check_interval '15' # Seconds between clock discipline checks (default: 15).
# Only used when require_clock_sync is 1.

#option clock_sync_max_tries '8' # Max attempts before giving up clock sync wait (default: 8).
# Total wait = clock_sync_check_interval x (tries - 1), ~105s at defaults.
# Set to 0 to wait indefinitely (blocks boot until clock disciplines).

# For global lines in ptp4l.conf - Each item is one line like "key value" for the [global] section
#list global_opts 'clockClass 6'

# For extra ptp4l command line options
#list extra_args ''

# At least one ptp_interface section is required for ptp4l to start.
# Repeat this block for each interface ptp4l should listen on.
#config ptp_interface
# option interface 'lan' # required - UCI's interface name, init-script looks up kernel ifname as needed
# #option transport 'UDPv4' # Network transport: UDPv4 (default), UDPv6, L2.
# #option delay_mechanism 'E2E' # PTP delay mechanism: E2E (End-to-End, default), P2P (Peer-to-Peer).
# #list interface_opts 'logSyncInterval -3'

config phc2sys 'phc2sys' # configures the phc2sys daemon, which syncs either system clock from PHC in NIC (as a client, or vice versa as a server)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit message says the config change makes it so "services do not auto-start on upgrade without explicit admin opt-in", but that only gates ptp4l. This config phc2sys section is still active and every option in it has a working fallback: phc_device defaults to /dev/ptp0 at phc2sys.init:22-23 and sync_direction defaults to phc_to_sys at phc2sys.init:25-29. default_postinst() at functions.sh:401-410 runs "$i" start for every init script the package ships, and the PKG_UPGRADE guard there covers only enable, not start — so on any box that has a PHC, both installing and upgrading linuxptp starts phc2sys.

The combination is worse than before this commit rather than better: ptp4l is now the daemon that refuses to start (no interface configured), so phc2sys ends up slewing CLOCK_REALTIME from a free-running, undisciplined NIC PHC, competing with whatever sysntpd/ntpd the box already runs. Previously at least ptp4l was disciplining that PHC.

Gating phc2sys the same way would make the commit message's claim true for both daemons — e.g. drop the /dev/ptp0 default so an unset phc_device is a hard "not configured", or add an explicit option enabled that both init scripts check.


Generated by Claude Code

#option phc_device '/dev/ptp0' # PTP Hardware Clock device - required, no default.
# Verify this matches the PHC for your PTP interface (e.g. /dev/ptp0).
# phc2sys will not start until this is set.
# specifies clock sync'ing direction # 'phc_to_sys': PHC (sync'd by ptp4l) disciplines system clock (default unless ptp4l role is 'server')
#option sync_direction 'phc_to_sys' # 'sys_to_phc': System clock disciplines PHC (default if ptp4l's role is 'server')

#option loglevel '5' # Logging verbosity for phc2sys, 5 by default, 6 if stats_interval is set

#option stats_interval '0' # Log status updates every N seconds (N defaults to 1 when -R param (by-default) is 1Hz).
# By default/if unset, log-level (above) will be set to 5 and these messages will not be seen

#list extra_args '' # For other phc2sys command line options.
# Example: list extra_args '-O 0'
73 changes: 73 additions & 0 deletions net/linuxptp/files/phc2sys.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/sh /etc/rc.common
# Init script for phc2sys from linuxptp

START=55
USE_PROCD=1
PROG=/usr/sbin/phc2sys

ID=phc2sys
ID_PTP4L=ptp4l

log() {
local loglevel=$1; shift; local msg="$*"
logger -t phc2sys-init -p daemon.$loglevel "$msg"
}

make_cmdline() {
local cfg=$1
local args dev dir loglevel ptp4l_role stats_set syncdir

echo $PROG

config_get dev $cfg "phc_device" ""
[ -z "$dev" ] && { log notice "phc_device not configured, phc2sys not starting. Set 'option phc_device' in /etc/config/linuxptp."; return 1; }
! [ -c "$dev" ] && { log crit "PHC device '$dev' not found!"; return 1; }

dir='phc_to_sys'
config_get ptp4l_role $ID_PTP4L "role" "auto"
[ "$ptp4l_role" = "server" ] && dir='sys_to_phc'

config_get syncdir $cfg "sync_direction" "$dir"
if [ "$syncdir" = "phc_to_sys" ]; then
echo "-s $dev -w"
elif [ "$syncdir" = "sys_to_phc" ]; then
echo "-s CLOCK_REALTIME -c $dev -w -O 0"
else
log err "Invalid sync_direction '$syncdir'! valid values: 'sys_to_phc', 'phc_to_sys'"; return 1
fi

echo "-m"
echo "-q"

config_get stats_set $cfg "stats_interval" "0"
[ "$stats_set" -ge 1 ] && echo "-u $stats_set"

config_get loglevel $cfg "loglevel" "5"
[ "$stats_set" -ge 1 ] && config_get loglevel $cfg "loglevel" "6"
echo "-l $loglevel"

config_list_foreach $cfg "extra_args" echo
}

service_triggers() {
procd_add_reload_trigger "linuxptp"
}

start_service() {
local cmdline

config_load linuxptp

cmdline=$(make_cmdline $ID) || return 1
log info "Starting phc2sys: $cmdline"

procd_open_instance phc2sys
procd_set_param command $cmdline
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}

stop_service() {
log info "phc2sys exiting (if running)."
}
189 changes: 189 additions & 0 deletions net/linuxptp/files/ptp4l.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/bin/sh /etc/rc.common
# Init script for ptp4l daemon, from linuxptp package

START=45
USE_PROCD=1

PROG=/usr/sbin/ptp4l
HELPER=/usr/sbin/sysclock-discipline-check
CONFIGFILE=/var/etc/ptp4l.conf

ID=ptp4l

log() {
local loglevel=$1; shift; local msg="$*"
logger -t ptp4l-init -p daemon.$loglevel "$msg"
}

is_clock_disciplined() {
local output rc

if ! [ -x "$HELPER" ]; then
echo "Helper $HELPER not found or not executable." >&2
echo "Error: Clock sync helper tool missing."
return 2
fi

output=$($HELPER)
rc=$?
echo $output

[ $rc -eq 2 ] && log err "ERROR: $HELPER call unsuccessful (rc=$rc): $output"
return $rc
}

wait_disciplined() {
local check_msg interval is_synced=0 max_tries rc role tries=0

config_get role $ID "role" ""
config_get interval $ID "clock_sync_check_interval" "15"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: clock_sync_check_interval is read here but is not documented anywhere in files/linuxptp.config, so there is no way for a user to discover it. Same for the ptp4l-section extra_args consumed at line 121 — extra_args is only documented under the phc2sys section (linuxptp.config:33). Worth adding commented-out entries for both in the config ptp4l block.


Generated by Claude Code

config_get max_tries $ID "clock_sync_max_tries" "8"

log info "Preparing to start with role:$role -> checking if system clock is disciplined..."
while [ $is_synced -eq 0 ]; do
tries=$((tries + 1))
check_msg=$(is_clock_disciplined); rc=$?

if [ $rc -eq 0 ]; then
is_synced=1
log info "sysclock-discipline-check: $check_msg. Continuing."
return 0
elif [ $rc -eq 1 ]; then
if [ "$max_tries" -gt 0 ] && [ "$tries" -ge "$max_tries" ]; then
log err "Clock sync wait timed out after $tries attempts. ptp4l will NOT start."
log err "Increase 'clock_sync_max_tries' or set 'require_clock_sync 0' to skip."
return 1
fi
Comment on lines +52 to +56

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bounding the wait does fix the rcS stall, but the new failure path is terminal. wait_disciplined returns 1, start_service returns at line 154 before procd_open_instance, so no instance is ever registered and procd has nothing to respawn or retry. The only thing that would start ptp4l afterwards is procd_add_reload_trigger "linuxptp" (line 141), i.e. a UCI config change.

So a role 'server' box whose clock disciplines late — an RTC-less device waiting on sysntpd to reach a peer over a slow or late-coming WAN — silently has no ptp4l until someone logs in and restarts it. That is exactly the device class the helper was added for, and the default budget is tighter than the ~2 min in the config comment: the timeout fires at the top of the 8th iteration, before its sleep, so it is 7 × clock_sync_check_interval = 105 s.

The idiomatic OpenWrt hook for "retry once the clock is good" is the ntp hotplug event: sysntpd passes HOTPLUG_SCRIPT=/usr/sbin/ntpd-hotplug (sysntpd:8) to busybox ntpd, which ubus-broadcasts ACTION=stratum to /etc/hotplug.d/ntp/. dnsmasq already uses it for the same "wait for valid time" problem — see dnsmasqsec.hotplug:3 and its install line in the dnsmasq Makefile:180. A three-line handler that runs /etc/init.d/ptp4l restart on stratum would make the timeout recoverable and let the default try count stay low. Raising the default clock_sync_max_tries is the cheap alternative, but it trades boot time straight back.

Minor, while in this block: with the documented clock_sync_max_tries '0' ("wait indefinitely") the progress line at line 57 renders as Waiting... (3/0).


Generated by Claude Code

if [ "$max_tries" -gt 0 ]; then
log info "sysclock-discipline-check: $check_msg. Waiting... ($tries/$max_tries)"
else
log info "sysclock-discipline-check: $check_msg. Waiting... (attempt $tries, unlimited)"
fi
else
log crit "Clock sync check FAILED: $check_msg."
log err "ptp4l will NOT start. Resolve issue, or set 'require_clock_sync' to 0"
return 1
fi
sleep $interval
done
Comment on lines +43 to +68

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop has no upper bound: as long as the helper keeps returning 1 (STA_UNSYNC still set) it sleeps and retries forever, and is_synced is only ever set on the path that already return 0s.

That was survivable while the procd instance was registered before the wait. Now that the procd_open_instance block has correctly moved below it, start_service does not return until the loop ends — and start_service runs synchronously from /etc/rc.d/S45ptp4l during rcS. So a box configured with role 'server' (where require_clock_sync defaults to 1 via line 136) that never gets a time source hangs the rest of the boot sequence: phc2sys at START=55 and every other init script ordered after 45 never runs. On a system with no RTC and no upstream NTP — precisely the scenario the helper was added for — that is an unbootable device rather than a delayed daemon.

Bounding the wait would keep the guard: a max attempt count or total deadline, after which it logs and return 1s so ptp4l simply does not start. Alternatively, keep the wait off the boot path entirely — background it and let a trigger register the instance once the clock disciplines.


Generated by Claude Code

}

handle_interface() {
local cfg=$1 delay_mechanism interface ifname transport

config_get interface $cfg "interface" ""
[ -z "$interface" ] && { log err "Required 'interface' option missing in 'ptp_interface' section"; exit 1; }

network_get_device ifname "$interface"
[ -z "$ifname" ] && { log err "Cannot resolve UCI interface '$interface' to kernel-compatible ifname."; exit 1; }

config_get transport $cfg "transport" "UDPv4"
config_get delay_mechanism $cfg "delay_mechanism" "E2E"

echo ""
echo "[$ifname]"
[ "$transport" != "UDPv4" ] && echo "network_transport $transport"
[ "$delay_mechanism" != "E2E" ] && echo "delay_mechanism $delay_mechanism"
config_list_foreach $cfg "interface_opts" echo
}

handle_global() {
local cfg=$1 role

config_get role $cfg "role" "auto"

echo "[global]"

case "$role" in
server) printf "serverOnly 1\n";;
client) printf "clientOnly 1\n";;
auto) printf "";;
*) log err "Invalid role: $role! valid values: 'auto', 'client', 'server'"; return 1;;
esac

config_list_foreach $cfg "global_opts" echo
}

make_configfile() {
local cfg=$1 iface_count rc

. /lib/functions/network.sh

mkdir -p "${CONFIGFILE%/*}"

(
handle_global $cfg || exit 1
config_foreach handle_interface ptp_interface
) > "$CONFIGFILE"
Comment on lines +110 to +117

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/var/etc does not exist on a stock OpenWrt boot — base-files boot() at init.d/boot:26-33 creates only /var/lock, /var/log, /var/run, /var/state and /var/tmp. The redirection on line 104 therefore fails with "cannot create" and ptp4l never gets a config file. Every in-tree package that writes there creates the directory itself (net/samba4, net/ocserv, net/igmpproxy, net/tinyproxy, …).

Suggested change
. /lib/functions/network.sh
(
handle_global $cfg || exit 1
config_foreach handle_interface ptp_interface
) > "$CONFIGFILE"
. /lib/functions/network.sh
mkdir -p "${CONFIGFILE%/*}"
(
handle_global $cfg || exit 1
config_foreach handle_interface ptp_interface
) > "$CONFIGFILE"

Generated by Claude Code

rc=$?

[ $rc -ne 0 ] && return 1

# Require at least one [interface] section beyond [global]
iface_count=$(grep -c '^\[' "$CONFIGFILE" 2>/dev/null)
if [ "${iface_count:-0}" -lt 2 ]; then
log notice "No PTP interfaces configured. Add a 'config ptp_interface' section to /etc/config/linuxptp."
return 1
fi
}

make_cmdline() {
local cfg=$1 loglevel

config_get loglevel $cfg "loglevel" "6"

echo $PROG
echo "-l $loglevel"
echo "-f $CONFIGFILE"
echo "-m"
echo "-q"

config_list_foreach $cfg "extra_args" echo
}

service_triggers() {
procd_add_reload_trigger "linuxptp"
}

start_service() {
local clocksync_wanted cmdline rc role should_have_disciplined_clock=0

config_load linuxptp

# Fast pre-check: fail immediately if no ptp_interface section carries a
# non-empty 'interface' option, before spending minutes in the clock-sync wait below.
if [ -z "$(uci -q get linuxptp.@ptp_interface[0].interface)" ]; then
log notice "No ptp_interface section with an 'interface' option configured in /etc/config/linuxptp. ptp4l will not start."
return 1
fi

config_get role $ID "role" "auto"
[ "$role" = "server" ] && should_have_disciplined_clock=1
config_get_bool clocksync_wanted $ID "require_clock_sync" "$should_have_disciplined_clock"

if [ "$clocksync_wanted" = 1 ]; then
wait_disciplined || return 1
fi

make_configfile $ID; rc=$?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new "no PTP interfaces configured" check is the cheapest validation in start_service, but it runs last — after a wait that can now burn up to 105 s of the boot sequence.

Concretely: an admin sets option role 'server' (so require_clock_sync defaults to 1 via line 151) but has not yet uncommented a config ptp_interface section — the state the shipped config now ships in. Boot then spends the full clock_sync_max_tries budget inside rcS polling the helper, and only afterwards discovers at line 120 that there was never a config worth starting. Everything ordered after START=45, phc2sys included, waits for that.

Running make_configfile first and the clock wait second would fail that case in milliseconds. The one thing to weigh before reordering is handle_interface's network_get_device call — at START=45 the wait currently doubles as incidental settle time for the UCI interface, so moving it earlier makes an unresolvable interface slightly more likely. That is really the interface-trigger question from the earlier round rather than an argument for keeping the order, but worth deciding deliberately rather than by side effect.


Generated by Claude Code

if [ $rc -ne 0 ]; then
log err "Failed to generate $CONFIGFILE. Aborting."
rm -f "$CONFIGFILE"
return 1
fi

cmdline=$(make_cmdline $ID)
log info "Starting ptp4l: $cmdline"

procd_open_instance "ptp4l"
procd_set_param command $cmdline
procd_set_param file $CONFIGFILE
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}

stop_service() {
rm -f "$CONFIGFILE"
log info "ptp4l exiting (if running)"
}
30 changes: 30 additions & 0 deletions net/linuxptp/files/sysclock-discipline-check.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Helper utility to query the kernel for the synchronization status of the
* system clock (CLOCK_REALTIME) via adjtimex(). Used by the ptp4l init script
* to delay startup in server mode until the clock is known to be accurate.
*
* Exit codes: 0 = synchronized, 1 = unsynchronized, 2 = error
*/

#include <stdio.h>
#include <string.h>
#include <sys/timex.h>

int main(void) {
struct timex txc;

memset(&txc, 0, sizeof(txc));

if (adjtimex(&txc) < 0) {
perror("adjtimex failed");
return 2;
}

if (txc.status & STA_UNSYNC) {
fprintf(stdout, "Clock status: 0x%04X (STA_UNSYNC is SET - Unsynchronized)\n", (unsigned int)txc.status);
return 1;
} else {
fprintf(stdout, "Clock status: 0x%04X (STA_UNSYNC is CLEAR - Synchronized)\n", (unsigned int)txc.status);
return 0;
}
}