using System; namespace LiteNetLib.Utils { /// /// Represents RFC4330 SNTP packet used for communication to and from a network time server. /// /// /// /// Most applications should just use the property. /// /// /// The same data structure represents both request and reply packets. /// Request and reply differ in which properties are set and to what values. /// /// /// The only real property is . /// All other properties read from and write to the underlying byte array /// with the exception of , /// which is not part of the packet on network and it is instead set locally after receiving the packet. /// /// /// Copied from GuerrillaNtp project /// with permission from Robert Vazan (@robertvazan) under MIT license, see https://github.com/RevenantX/LiteNetLib/pull/236 /// /// public class NtpPacket { private static readonly DateTime Epoch = new DateTime(1900, 1, 1); /// /// Gets RFC4330-encoded SNTP packet. /// /// /// Byte array containing RFC4330-encoded SNTP packet. It is at least 48 bytes long. /// /// /// This is the only real property. All other properties except /// read from or write to this byte array. /// public byte[] Bytes { get; private set; } /// /// Gets the leap second indicator. /// /// /// Leap second warning, if any. Special value /// indicates unsynchronized server clock. /// Default is . /// /// /// Only servers fill in this property. Clients can consult this property for possible leap second warning. /// public NtpLeapIndicator LeapIndicator { get { return (NtpLeapIndicator)((Bytes[0] & 0xC0) >> 6); } } /// /// Gets or sets protocol version number. /// /// /// SNTP protocol version. Default is 4, which is the latest version at the time of this writing. /// /// /// In request packets, clients should leave this property at default value 4. /// Servers usually reply with the same protocol version. /// public int VersionNumber { get { return (Bytes[0] & 0x38) >> 3; } private set { Bytes[0] = (byte)((Bytes[0] & ~0x38) | value << 3); } } /// /// Gets or sets SNTP packet mode, i.e. whether this is client or server packet. /// /// /// SNTP packet mode. Default is in newly created packets. /// Server reply should have this property set to . /// public NtpMode Mode { get { return (NtpMode)(Bytes[0] & 0x07); } private set { Bytes[0] = (byte)((Bytes[0] & ~0x07) | (int)value); } } /// /// Gets server's distance from the reference clock. /// /// /// /// Distance from the reference clock. This property is set only in server reply packets. /// Servers connected directly to reference clock hardware set this property to 1. /// Statum number is incremented by 1 on every hop down the NTP server hierarchy. /// /// /// Special value 0 indicates that this packet is a Kiss-o'-Death message /// with kiss code stored in . /// /// public int Stratum { get { return Bytes[1]; } } /// /// Gets server's preferred polling interval. /// /// /// Polling interval in log2 seconds, e.g. 4 stands for 16s and 17 means 131,072s. /// public int Poll { get { return Bytes[2]; } } /// /// Gets the precision of server clock. /// /// /// Clock precision in log2 seconds, e.g. -20 for microsecond precision. /// public int Precision { get { return (sbyte)Bytes[3]; } } /// /// Gets the total round-trip delay from the server to the reference clock. /// /// /// Round-trip delay to the reference clock. Normally a positive value smaller than one second. /// public TimeSpan RootDelay { get { return GetTimeSpan32(4); } } /// /// Gets the estimated error in time reported by the server. /// /// /// Estimated error in time reported by the server. Normally a positive value smaller than one second. /// public TimeSpan RootDispersion { get { return GetTimeSpan32(8); } } /// /// Gets the ID of the time source used by the server or Kiss-o'-Death code sent by the server. /// /// /// /// ID of server's time source or Kiss-o'-Death code. /// Purpose of this property depends on value of property. /// /// /// Stratum 1 servers write here one of several special values that describe the kind of hardware clock they use. /// /// /// Stratum 2 and lower servers set this property to IPv4 address of their upstream server. /// If upstream server has IPv6 address, the address is hashed, because it doesn't fit in this property. /// /// /// When server sets to special value 0, /// this property contains so called kiss code that instructs the client to stop querying the server. /// /// public uint ReferenceId { get { return GetUInt32BE(12); } } /// /// Gets or sets the time when the server clock was last set or corrected. /// /// /// Time when the server clock was last set or corrected or null when not specified. /// /// /// This Property is usually set only by servers. It usually lags server's current time by several minutes, /// so don't use this property for time synchronization. /// public DateTime? ReferenceTimestamp { get { return GetDateTime64(16); } } /// /// Gets or sets the time when the client sent its request. /// /// /// This property is null in request packets. /// In reply packets, it is the time when the client sent its request. /// Servers copy this value from /// that they find in received request packet. /// /// /// public DateTime? OriginTimestamp { get { return GetDateTime64(24); } } /// /// Gets or sets the time when the request was received by the server. /// /// /// This property is null in request packets. /// In reply packets, it is the time when the server received client request. /// /// /// public DateTime? ReceiveTimestamp { get { return GetDateTime64(32); } } /// /// Gets or sets the time when the packet was sent. /// /// /// Time when the packet was sent. It should never be null. /// Default value is . /// /// /// This property must be set by both clients and servers. /// /// /// public DateTime? TransmitTimestamp { get { return GetDateTime64(40); } private set { SetDateTime64(40, value); } } /// /// Gets or sets the time of reception of response SNTP packet on the client. /// /// /// Time of reception of response SNTP packet on the client. It is null in request packets. /// /// /// This property is not part of the protocol and has to be set when reply packet is received. /// /// /// public DateTime? DestinationTimestamp { get; private set; } /// /// Gets the round-trip time to the server. /// /// /// Time the request spent traveling to the server plus the time the reply spent traveling back. /// This is calculated from timestamps in the packet as (t1 - t0) + (t3 - t2) /// where t0 is , /// t1 is , /// t2 is , /// and t3 is . /// This property throws an exception in request packets. /// public TimeSpan RoundTripTime { get { CheckTimestamps(); return (ReceiveTimestamp.Value - OriginTimestamp.Value) + (DestinationTimestamp.Value - TransmitTimestamp.Value); } } /// /// Gets the offset that should be added to local time to synchronize it with server time. /// /// /// Time difference between server and client. It should be added to local time to get server time. /// It is calculated from timestamps in the packet as 0.5 * ((t1 - t0) - (t3 - t2)) /// where t0 is , /// t1 is , /// t2 is , /// and t3 is . /// This property throws an exception in request packets. /// public TimeSpan CorrectionOffset { get { CheckTimestamps(); return TimeSpan.FromTicks(((ReceiveTimestamp.Value - OriginTimestamp.Value) - (DestinationTimestamp.Value - TransmitTimestamp.Value)).Ticks / 2); } } /// /// Initializes default request packet. /// /// /// Properties and /// are set appropriately for request packet. Property /// is set to . /// public NtpPacket() : this(new byte[48]) { Mode = NtpMode.Client; VersionNumber = 4; TransmitTimestamp = DateTime.UtcNow; } /// /// Initializes packet from received data. /// internal NtpPacket(byte[] bytes) { if (bytes.Length < 48) throw new ArgumentException("SNTP reply packet must be at least 48 bytes long.", "bytes"); Bytes = bytes; } /// /// Initializes packet from data received from a server. /// /// Data received from the server. /// Utc time of reception of response SNTP packet on the client. /// public static NtpPacket FromServerResponse(byte[] bytes, DateTime destinationTimestamp) { return new NtpPacket(bytes) { DestinationTimestamp = destinationTimestamp }; } internal void ValidateRequest() { if (Mode != NtpMode.Client) throw new InvalidOperationException("This is not a request SNTP packet."); if (VersionNumber == 0) throw new InvalidOperationException("Protocol version of the request is not specified."); if (TransmitTimestamp == null) throw new InvalidOperationException("TransmitTimestamp must be set in request packet."); } internal void ValidateReply() { if (Mode != NtpMode.Server) throw new InvalidOperationException("This is not a reply SNTP packet."); if (VersionNumber == 0) throw new InvalidOperationException("Protocol version of the reply is not specified."); if (Stratum == 0) throw new InvalidOperationException(string.Format("Received Kiss-o'-Death SNTP packet with code 0x{0:x}.", ReferenceId)); if (LeapIndicator == NtpLeapIndicator.AlarmCondition) throw new InvalidOperationException("SNTP server has unsynchronized clock."); CheckTimestamps(); } private void CheckTimestamps() { if (OriginTimestamp == null) throw new InvalidOperationException("Origin timestamp is missing."); if (ReceiveTimestamp == null) throw new InvalidOperationException("Receive timestamp is missing."); if (TransmitTimestamp == null) throw new InvalidOperationException("Transmit timestamp is missing."); if (DestinationTimestamp == null) throw new InvalidOperationException("Destination timestamp is missing."); } private DateTime? GetDateTime64(int offset) { var field = GetUInt64BE(offset); if (field == 0) return null; return new DateTime(Epoch.Ticks + Convert.ToInt64(field * (1.0 / (1L << 32) * 10000000.0))); } private void SetDateTime64(int offset, DateTime? value) { SetUInt64BE(offset, value == null ? 0 : Convert.ToUInt64((value.Value.Ticks - Epoch.Ticks) * (0.0000001 * (1L << 32)))); } private TimeSpan GetTimeSpan32(int offset) { return TimeSpan.FromSeconds(GetInt32BE(offset) / (double)(1 << 16)); } private ulong GetUInt64BE(int offset) { return SwapEndianness(BitConverter.ToUInt64(Bytes, offset)); } private void SetUInt64BE(int offset, ulong value) { FastBitConverter.GetBytes(Bytes, offset, SwapEndianness(value)); } private int GetInt32BE(int offset) { return (int)GetUInt32BE(offset); } private uint GetUInt32BE(int offset) { return SwapEndianness(BitConverter.ToUInt32(Bytes, offset)); } private static uint SwapEndianness(uint x) { return ((x & 0xff) << 24) | ((x & 0xff00) << 8) | ((x & 0xff0000) >> 8) | ((x & 0xff000000) >> 24); } private static ulong SwapEndianness(ulong x) { return ((ulong)SwapEndianness((uint)x) << 32) | SwapEndianness((uint)(x >> 32)); } } /// /// Represents leap second warning from the server that instructs the client to add or remove leap second. /// /// public enum NtpLeapIndicator { /// /// No leap second warning. No action required. /// NoWarning, /// /// Warns the client that the last minute of the current day has 61 seconds. /// LastMinuteHas61Seconds, /// /// Warns the client that the last minute of the current day has 59 seconds. /// LastMinuteHas59Seconds, /// /// Special value indicating that the server clock is unsynchronized and the returned time is unreliable. /// AlarmCondition } /// /// Describes SNTP packet mode, i.e. client or server. /// /// public enum NtpMode { /// /// Identifies client-to-server SNTP packet. /// Client = 3, /// /// Identifies server-to-client SNTP packet. /// Server = 4, } }