From 70eb582b3c7da3ee35593ac4ef4f972ee4ae5438 Mon Sep 17 00:00:00 2001 From: Mark Corbin Date: Fri, 5 Nov 2021 10:51:20 +0000 Subject: [PATCH] systemd/timeinit: add HTTPS time synchronisation service 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 current system time. The date/time derived from the header is assumed to be a reasonable source of 'truth' such that it can be used to adjust the system time both backwards and forwards. This will compensate for any erroneous timestamps saved via fake-hwclock or any invalid data read from an RTC. 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. This service will provide initial time synchronisation for devices where NTP ports have been blocked. For devices where NTP access 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 --- .../balena-files/NetworkManager.conf.systemd | 4 +- .../openvpn/files/openvpn.service | 4 +- .../balena-supervisor.service | 1 - .../chrony/files/chronyd.conf.systemd | 5 +- .../recipes-core/systemd/timeinit.bb | 10 ++- .../timeinit/fake-hwclock-update.timer | 4 +- .../timeinit/time-sync-https-wait.target | 19 +++++ .../systemd/timeinit/timesync-https.service | 28 +++++++ .../systemd/timeinit/timesync-https.sh | 75 +++++++++++++++++++ .../os-helpers/os-helpers/os-helpers-time | 10 ++- 10 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 meta-balena-common/recipes-core/systemd/timeinit/time-sync-https-wait.target create mode 100644 meta-balena-common/recipes-core/systemd/timeinit/timesync-https.service create mode 100644 meta-balena-common/recipes-core/systemd/timeinit/timesync-https.sh diff --git a/meta-balena-common/recipes-connectivity/networkmanager/balena-files/NetworkManager.conf.systemd b/meta-balena-common/recipes-connectivity/networkmanager/balena-files/NetworkManager.conf.systemd index 6a26660d5e..8362b3806e 100644 --- a/meta-balena-common/recipes-connectivity/networkmanager/balena-files/NetworkManager.conf.systemd +++ b/meta-balena-common/recipes-connectivity/networkmanager/balena-files/NetworkManager.conf.systemd @@ -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=balena-net-config.service bind-var-lib-NetworkManager.service +After=balena-net-config.service bind-var-lib-NetworkManager.service [Service] ExecStartPre=/bin/systemd-tmpfiles --remove /etc/tmpfiles.d/nm-tmpfiles.conf diff --git a/meta-balena-common/recipes-connectivity/openvpn/files/openvpn.service b/meta-balena-common/recipes-connectivity/openvpn/files/openvpn.service index 36f8f1d712..40b9e89b46 100644 --- a/meta-balena-common/recipes-connectivity/openvpn/files/openvpn.service +++ b/meta-balena-common/recipes-connectivity/openvpn/files/openvpn.service @@ -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] diff --git a/meta-balena-common/recipes-containers/balena-supervisor/balena-supervisor/balena-supervisor.service b/meta-balena-common/recipes-containers/balena-supervisor/balena-supervisor/balena-supervisor.service index 07ffda56df..5bed28a023 100644 --- a/meta-balena-common/recipes-containers/balena-supervisor/balena-supervisor/balena-supervisor.service +++ b/meta-balena-common/recipes-containers/balena-supervisor/balena-supervisor/balena-supervisor.service @@ -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 diff --git a/meta-balena-common/recipes-core/chrony/files/chronyd.conf.systemd b/meta-balena-common/recipes-core/chrony/files/chronyd.conf.systemd index 63465abe90..7330666200 100644 --- a/meta-balena-common/recipes-core/chrony/files/chronyd.conf.systemd +++ b/meta-balena-common/recipes-core/chrony/files/chronyd.conf.systemd @@ -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 diff --git a/meta-balena-common/recipes-core/systemd/timeinit.bb b/meta-balena-common/recipes-core/systemd/timeinit.bb index f143a6503f..b93fb0085c 100644 --- a/meta-balena-common/recipes-core/systemd/timeinit.bb +++ b/meta-balena-common/recipes-core/systemd/timeinit.bb @@ -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. @@ -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 \ " @@ -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 \ " @@ -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/ } diff --git a/meta-balena-common/recipes-core/systemd/timeinit/fake-hwclock-update.timer b/meta-balena-common/recipes-core/systemd/timeinit/fake-hwclock-update.timer index 36cfd5a2eb..edd191b39e 100644 --- a/meta-balena-common/recipes-core/systemd/timeinit/fake-hwclock-update.timer +++ b/meta-balena-common/recipes-core/systemd/timeinit/fake-hwclock-update.timer @@ -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. @@ -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 diff --git a/meta-balena-common/recipes-core/systemd/timeinit/time-sync-https-wait.target b/meta-balena-common/recipes-core/systemd/timeinit/time-sync-https-wait.target new file mode 100644 index 0000000000..a3ec21e93b --- /dev/null +++ b/meta-balena-common/recipes-core/systemd/timeinit/time-sync-https-wait.target @@ -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 diff --git a/meta-balena-common/recipes-core/systemd/timeinit/timesync-https.service b/meta-balena-common/recipes-core/systemd/timeinit/timesync-https.service new file mode 100644 index 0000000000..ef30793d00 --- /dev/null +++ b/meta-balena-common/recipes-core/systemd/timeinit/timesync-https.service @@ -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 diff --git a/meta-balena-common/recipes-core/systemd/timeinit/timesync-https.sh b/meta-balena-common/recipes-core/systemd/timeinit/timesync-https.sh new file mode 100644 index 0000000000..0e0a963cae --- /dev/null +++ b/meta-balena-common/recipes-core/systemd/timeinit/timesync-https.sh @@ -0,0 +1,75 @@ +#!/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")" + exit 0 + 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 diff --git a/meta-balena-common/recipes-support/os-helpers/os-helpers/os-helpers-time b/meta-balena-common/recipes-support/os-helpers/os-helpers/os-helpers-time index b51593ba1f..114776a684 100644 --- a/meta-balena-common/recipes-support/os-helpers/os-helpers/os-helpers-time +++ b/meta-balena-common/recipes-support/os-helpers/os-helpers/os-helpers-time @@ -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. @@ -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: