diff --git a/wxdata/include/scwx/types/ntp_types.hpp b/wxdata/include/scwx/types/ntp_types.hpp index 39c1f12f..cfb8f764 100644 --- a/wxdata/include/scwx/types/ntp_types.hpp +++ b/wxdata/include/scwx/types/ntp_types.hpp @@ -48,8 +48,7 @@ struct NtpPacket std::uint32_t rxTm_s; // Received time-stamp seconds. std::uint32_t rxTm_f; // Received time-stamp fraction of a second. - std::uint32_t txTm_s; // The most important field the client cares about. - // Transmit time-stamp seconds. + std::uint32_t txTm_s; // Transmit time-stamp seconds. std::uint32_t txTm_f; // Transmit time-stamp fraction of a second. static NtpPacket Parse(const std::span data); diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index 7315eeec..a20540c4 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace scwx::network { @@ -15,15 +16,86 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr std::size_t kReceiveBufferSize_ {48u}; +class NtpTimestamp +{ +public: + // NTP epoch: January 1, 1900 + // Unix epoch: January 1, 1970 + // Difference = 70 years = 2,208,988,800 seconds + static constexpr std::uint32_t kNtpToUnixOffset_ = 2208988800UL; + + // NTP fractional part represents 1/2^32 of a second + static constexpr std::uint64_t kFractionalMultiplier_ = 0x100000000ULL; + + static constexpr std::uint64_t _1e9 = 1000000000ULL; + + std::uint32_t seconds_ {0}; + std::uint32_t fraction_ {0}; + + explicit NtpTimestamp() = default; + explicit NtpTimestamp(std::uint32_t seconds, std::uint32_t fraction) : + seconds_ {seconds}, fraction_ {fraction} + { + } + ~NtpTimestamp() = default; + + NtpTimestamp(const NtpTimestamp&) = default; + NtpTimestamp& operator=(const NtpTimestamp&) = default; + NtpTimestamp(NtpTimestamp&&) = default; + NtpTimestamp& operator=(NtpTimestamp&&) = default; + + template + std::chrono::time_point ToTimePoint() const + { + // Convert NTP seconds to Unix seconds + // Don't cast to a larger type to account for rollover, and this should + // work until 2106 + const std::uint32_t unixSeconds = seconds_ - kNtpToUnixOffset_; + + // Convert NTP fraction to nanoseconds + const auto nanoseconds = + static_cast(fraction_) * _1e9 / kFractionalMultiplier_; + + return std::chrono::time_point( + std::chrono::duration_cast( + std::chrono::seconds {unixSeconds} + + std::chrono::nanoseconds {nanoseconds})); + } + + template + static NtpTimestamp FromTimePoint(std::chrono::time_point timePoint) + { + // Convert to duration since Unix epoch + const auto unixDuration = timePoint.time_since_epoch(); + + // Extract seconds and nanoseconds + const auto unixSeconds = + std::chrono::duration_cast(unixDuration); + const auto nanoseconds = + std::chrono::duration_cast(unixDuration - + unixSeconds); + + // Convert Unix seconds to NTP seconds + const auto ntpSeconds = + static_cast(unixSeconds.count() + kNtpToUnixOffset_); + + // Convert nanoseconds to NTP fractional seconds + const auto ntpFraction = static_cast( + nanoseconds.count() * kFractionalMultiplier_ / _1e9); + + return NtpTimestamp(ntpSeconds, ntpFraction); + } +}; + class NtpClient::Impl { public: explicit Impl(); ~Impl(); - Impl(const Impl&) = delete; - Impl& operator=(const Impl&) = delete; - Impl(const Impl&&) = delete; - Impl& operator=(const Impl&&) = delete; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(Impl&&) = delete; void Open(std::string_view host, std::string_view service); void Poll(); @@ -31,14 +103,22 @@ public: boost::asio::thread_pool threadPool_ {2u}; + bool enabled_; + types::ntp::NtpPacket transmitPacket_ {}; boost::asio::ip::udp::socket socket_; std::optional serverEndpoint_ {}; std::array receiveBuffer_ {}; - std::vector serverList_ { - "time.nist.gov", "ntp.pool.org", "time.windows.com"}; + std::chrono::system_clock::duration timeOffset_ {}; + + std::vector serverList_ {"time.nist.gov", + "time.cloudflare.com", + "ntp.pool.org", + "time.aws.com", + "time.windows.com", + "time.apple.com"}; }; NtpClient::NtpClient() : p(std::make_unique()) {} @@ -59,6 +139,18 @@ void NtpClient::Poll() NtpClient::Impl::Impl() : socket_ {threadPool_} { + using namespace std::chrono_literals; + + const auto now = + std::chrono::floor(std::chrono::system_clock::now()); + + // The NTP timestamp will overflow in 2036. Overflow is handled in such a way + // that should work until 2106. Additional handling for subsequent eras is + // required. + static constexpr auto kMaxYear_ = 2106y; + + enabled_ = now < kMaxYear_ / 1 / 1; + transmitPacket_.fields.vn = 3; // Version transmitPacket_.fields.mode = 3; // Client (3) } @@ -95,10 +187,15 @@ void NtpClient::Impl::Poll() try { + const auto originTimestamp = + NtpTimestamp::FromTimePoint(std::chrono::system_clock::now()); + transmitPacket_.txTm_s = ntohl(originTimestamp.seconds_); + transmitPacket_.txTm_f = ntohl(originTimestamp.fraction_); + std::size_t transmitPacketSize = sizeof(transmitPacket_); // Send NTP request socket_.send_to(boost::asio::buffer(&transmitPacket_, transmitPacketSize), - *serverEndpoint_); + *serverEndpoint_); // Receive NTP response auto future = @@ -131,8 +228,55 @@ void NtpClient::Impl::ReceivePacket(std::size_t length) { if (length >= sizeof(types::ntp::NtpPacket)) { - auto packet = types::ntp::NtpPacket::Parse(receiveBuffer_); - (void) packet; + const auto destinationTime = std::chrono::system_clock::now(); + + const auto packet = types::ntp::NtpPacket::Parse(receiveBuffer_); + + if (packet.stratum == 0) + { + const std::uint32_t refId = ntohl(packet.refId); + const std::string kod = + std::string(reinterpret_cast(&refId), 4); + + logger_->warn("KoD packet received: {}", kod); + + if (kod == "DENY" || kod == "RSTR") + { + // TODO + // The client MUST demobilize any associations to that server and + // stop sending packets to that server + } + else if (kod == "RATE") + { + // TODO + // The client MUST immediately reduce its polling interval to that + // server and continue to reduce it each time it receives a RATE + // kiss code + } + } + else + { + const auto originTimestamp = + NtpTimestamp(packet.origTm_s, packet.origTm_f); + const auto receiveTimestamp = + NtpTimestamp(packet.rxTm_s, packet.rxTm_f); + const auto transmitTimestamp = + NtpTimestamp(packet.txTm_s, packet.txTm_f); + + const auto originTime = originTimestamp.ToTimePoint(); + const auto receiveTime = receiveTimestamp.ToTimePoint(); + const auto transmitTime = transmitTimestamp.ToTimePoint(); + + const auto& t0 = originTime; + const auto& t1 = receiveTime; + const auto& t2 = transmitTime; + const auto& t3 = destinationTime; + + // Update time offset + timeOffset_ = ((t1 - t0) + (t2 - t3)) / 2; + + logger_->debug("Time offset updated: {:%jd %T}", timeOffset_); + } } else { diff --git a/wxdata/source/scwx/types/ntp_types.cpp b/wxdata/source/scwx/types/ntp_types.cpp index cca0c422..9ff39095 100644 --- a/wxdata/source/scwx/types/ntp_types.cpp +++ b/wxdata/source/scwx/types/ntp_types.cpp @@ -20,15 +20,6 @@ NtpPacket NtpPacket::Parse(const std::span data) packet = *reinterpret_cast(data.data()); - // Detect Kiss-o'-Death (KoD) packet - if (packet.stratum == 0) - { - // TODO - std::string kissCode = - std::string(reinterpret_cast(&packet.refId), 4); - (void) kissCode; - } - packet.rootDelay = ntohl(packet.rootDelay); packet.rootDispersion = ntohl(packet.rootDispersion); packet.refId = ntohl(packet.refId);