Skip to content

Commit

Permalink
systemd/timeinit: add HTTPS time synchronisation service
Browse files Browse the repository at this point in the history
Add a new timesync-https systemd service to synchronise the system
time at boot using an HTTPS header. The service uses curl to request
an HTTPS header from either $API_ENDPOINT/connectivity-check (default)
or the URL defined by the os.network.connectivity.uri field in
config.json. The URL used *must* return HTTP code 204 (No Content)
in response to a request so that we can determine that we have full
network connectivity and are not operating behind a captive portal.

The date field returned by a valid header is used to set the system
time providing that it is in advance of the current system time.

The service will exit when a valid response has been received. Poll
attempts will be made at an increasing interval starting at 2s and
doubling up to a maximum of 64s. Polling will continue at the maximum
interval until a valid response has been received.

The service provides time synchronisation for devices where NTP is
blocked. For devices where NTP is available it should ensure that any
system 'time jump' is only a few seconds when NTP synchronisation is
eventually achieved. It also allows other services to start with a
reasonably accurate time without having to wait for the NTP
synchronisation process to complete.

Services that are ordered after the new time-sync-https-wait target
can be sure that full network connectivity has been achieved and that
time has been synchronised with an accuracy of a few seconds.

Change-type: minor
Connects-to: #1337 #1776 #2044 #2139
Signed-off-by: Mark Corbin <[email protected]>
  • Loading branch information
markcorbinuk committed Oct 26, 2021
1 parent 62fc607 commit aeca7ea
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[Unit]
Wants=balena-net-config.service bind-var-lib-NetworkManager.service chronyd.service
After=balena-net-config.service bind-var-lib-NetworkManager.service chronyd.service
Wants=resin-net-config.service bind-var-lib-NetworkManager.service
After=resin-net-config.service bind-var-lib-NetworkManager.service

[Service]
ExecStartPre=/bin/systemd-tmpfiles --remove /etc/tmpfiles.d/nm-tmpfiles.conf
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[Unit]
Description=OpenVPN
Requires=prepare-openvpn.service bind-etc-openvpn.service
After=syslog.target network.target prepare-openvpn.service bind-etc-openvpn.service time-sync.target
Requires=prepare-openvpn.service bind-etc-openvpn.service time-sync-https-wait.target
After=syslog.target network.target prepare-openvpn.service bind-etc-openvpn.service time-sync-https-wait.target
ConditionFileNotEmpty=/etc/openvpn/openvpn.conf

[Service]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ After=\
os-config-devicekey.service \
bind-etc-systemd-system-resin.target.wants.service \
bind-etc-balena-supervisor.service \
chronyd.service \
migrate-supervisor-state.service
Wants=balena.service
ConditionPathExists=/etc/balena-supervisor/supervisor.conf
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[Unit]
Before=time-sync.target
Wants=time-sync.target var-volatile-lib.service
After=var-volatile-lib.service
Wants=time-sync-https-wait.target var-volatile-lib.service
After=time-sync-https-wait.target var-volatile-lib.service

[Service]
Type=simple
Expand Down
10 changes: 9 additions & 1 deletion meta-balena-common/recipes-core/systemd/timeinit.bb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2018-2020 Balena Ltd.
# Copyright 2018-2021 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,9 @@ SRC_URI = " \
file://fake-hwclock-update.timer \
file://timeinit-rtc.service \
file://timeinit-rtc.sh \
file://timesync-https.service \
file://timesync-https.sh \
file://time-sync-https-wait.target \
file://time-set.target \
file://time-sync.conf \
"
Expand All @@ -40,6 +43,8 @@ SYSTEMD_SERVICE_${PN} = " \
fake-hwclock-update.service \
fake-hwclock-update.timer \
timeinit-rtc.service \
timesync-https.service \
time-sync-https-wait.target \
time-set.target \
"

Expand All @@ -51,12 +56,15 @@ do_install() {
install -d ${D}${sysconfdir}/systemd/system/time-sync.target.d/
install -m 0775 ${WORKDIR}/timeinit-buildtime.sh ${D}${bindir}
install -m 0775 ${WORKDIR}/timeinit-rtc.sh ${D}${bindir}
install -m 0775 ${WORKDIR}/timesync-https.sh ${D}${bindir}
install -m 0775 ${WORKDIR}/fake-hwclock ${D}${base_sbindir}
install -m 0644 ${WORKDIR}/timeinit-buildtime.service ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/fake-hwclock.service ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/fake-hwclock-update.service ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/fake-hwclock-update.timer ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/timeinit-rtc.service ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/timesync-https.service ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/time-sync-https-wait.target ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/time-set.target ${D}${systemd_unitdir}/system
install -m 0644 ${WORKDIR}/time-sync.conf ${D}${sysconfdir}/systemd/system/time-sync.target.d/
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020 Balena Ltd.
# Copyright 2020-2021 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@

[Unit]
Description=Periodically update the saved fake-hwclock.
After=time-sync.target
After=time-sync-https-wait.target

[Timer]
OnCalendar=hourly
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2021 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[Unit]
Description=Wait for time synchronisation via HTTPS header
RefuseManualStart=yes
After=network.target time-set.target
Wants=time-set.target
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2021 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[Unit]
Description=Set system clock from a secure website
DefaultDependencies=no
Wants=network.target time-sync.target
After=network.target time-sync.target
Before=time-sync-https-wait.target chronyd.service

[Service]
Type=oneshot
ExecStart=/usr/bin/timesync-https.sh
RemainAfterExit=yes

[Install]
WantedBy=time-sync-https-wait.target
74 changes: 74 additions & 0 deletions meta-balena-common/recipes-core/systemd/timeinit/timesync-https.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/sh
#
# Copyright 2021 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. /usr/libexec/os-helpers-logging
. /usr/libexec/os-helpers-time

. /usr/sbin/balena-config-vars

# Expected HTTP response code. Used to determine that we are not
# behind a captive portal.
EXPECTED_SERVER_CODE=204
# Initial delay in seconds between poll attempts.
INITIAL_HTTPS_POLL_DELAY=2
# Maximum delay in seconds between poll attempts.
MAX_HTTPS_POLL_DELAY=64
# Timeout for curl command in seconds.
# Note that curl does not apply this timeout to DNS lookups.
CURL_TIMEOUT=5
# Don't bother updating or reporting errors for small differences.
TIME_DIFF_THRESHOLD=2

# Poll HTTPS server for time string.
info "Starting HTTPS time synchronisation."

HTTPS_POLL_DELAY=$INITIAL_HTTPS_POLL_DELAY

# In theory the maximum duration of each poll delay is given by:
# (HTTPS_POLL_DELAY + CURL_TIMEOUT) seconds.
# Note that this period can be extended as curl DNS lookup timeouts do
# not obey the -m (--max-time) parameter.

while [ true ]; do
SYS_TIME=$(get_system_time_as_timestamp)
readarray -t https_header <<<$(curl -m5 -k -I -s $OS_NET_CONN_URI | sed 's/\r$//' | awk '/HTTP/{printf $2"\n"} /[Dd]ate/{print $2, $3, $4, $5, $6, $7"\n"}')
SERVER_CODE=${https_header[0]}
SERVER_TIME_STRING=${https_header[1]}
if [ "$SERVER_CODE" = "$EXPECTED_SERVER_CODE" ]; then
if [ ! -z "$SERVER_TIME_STRING" ]; then
SERVER_TIME=$(get_server_time_as_timestamp "$SERVER_TIME_STRING")
TIME_DIFF=$(get_abs_time_diff_from_timestamps "$SYS_TIME" "$SERVER_TIME")
if [ "$TIME_DIFF" -gt "$TIME_DIFF_THRESHOLD" ]; then
$(set_system_time_from_timestamp "$SERVER_TIME")
if [ "$SYS_TIME" -gt "$SERVER_TIME" ]; then
warn "HTTPS header time is in the past."
warn "Check time sources if this issue persists."
fi
info "Time synchronised via HTTPS."
info "Old time: $(get_display_time_from_timestamp "$SYS_TIME")"
info "New time: $(get_display_time_from_timestamp "$SERVER_TIME")"
else
info "System time is already synchronised."
exit 0
fi
fi
fi
sleep $HTTPS_POLL_DELAY

if [ "$HTTPS_POLL_DELAY" -lt "$MAX_HTTPS_POLL_DELAY" ]; then
HTTPS_POLL_DELAY=$(($HTTPS_POLL_DELAY * 2))
fi
done
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh

# Copyright 2020 Balena Ltd.
# Copyright 2020-2021 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -31,6 +31,14 @@ get_hwclock_time_as_timestamp() {
echo "${timestamp}"
}

# Get an HTTP server header time string as a timestamp.
# Arguments:
# 1 - server time string (Day, DD Mon YYYY hh:mm:ss TZ)
get_server_time_as_timestamp() {
local server_time_str=$1
echo "$(date -u "+%4Y%2m%2d%2H%2M%2S" -d "${server_time_str}")"
}

# Get a 'date' compatible string from a timestamp for display.
# YYYYMMDDhhmmss -> YYYYMMDD hh:mm:ss
# Arguments:
Expand Down

0 comments on commit aeca7ea

Please sign in to comment.