#include #include #include #include #include #include #include #include namespace scwx::network { static const std::string logPrefix_ = "scwx::network::ntp_client"; 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(Impl&&) = delete; Impl& operator=(Impl&&) = delete; void Open(std::string_view host, std::string_view service); void Poll(); void ReceivePacket(std::size_t length); 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::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()) {} NtpClient::~NtpClient() = default; NtpClient::NtpClient(NtpClient&&) noexcept = default; NtpClient& NtpClient::operator=(NtpClient&&) noexcept = default; void NtpClient::Open(std::string_view host, std::string_view service) { p->Open(host, service); } void NtpClient::Poll() { p->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) } NtpClient::Impl::~Impl() { threadPool_.join(); } void NtpClient::Impl::Open(std::string_view host, std::string_view service) { boost::asio::ip::udp::resolver resolver(threadPool_); boost::system::error_code ec; auto results = resolver.resolve(host, service, ec); if (ec.value() == boost::system::errc::success && !results.empty()) { logger_->info("Using NTP server: {}", host); serverEndpoint_ = *results.begin(); socket_.open(serverEndpoint_->protocol()); } else { serverEndpoint_ = std::nullopt; logger_->warn("Could not resolve host {}: {}", host, ec.message()); } } void NtpClient::Impl::Poll() { using namespace std::chrono_literals; static constexpr auto kTimeout_ = 15s; 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_); // Receive NTP response auto future = socket_.async_receive_from(boost::asio::buffer(receiveBuffer_), *serverEndpoint_, boost::asio::use_future); std::size_t bytesReceived = 0; switch (future.wait_for(kTimeout_)) { case std::future_status::ready: bytesReceived = future.get(); ReceivePacket(bytesReceived); break; case std::future_status::timeout: case std::future_status::deferred: logger_->warn("Timeout waiting for NTP response"); socket_.cancel(); break; } } catch (const std::exception& ex) { logger_->error("Error polling: {}", ex.what()); } } void NtpClient::Impl::ReceivePacket(std::size_t length) { if (length >= sizeof(types::ntp::NtpPacket)) { 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 { logger_->warn("Received too few bytes: {}", length); } } } // namespace scwx::network