diff --git a/GuerrillaNtp_CF/GuerrillaNtp_CF.csproj b/GuerrillaNtp_CF/GuerrillaNtp_CF.csproj new file mode 100644 index 0000000..e64ada0 --- /dev/null +++ b/GuerrillaNtp_CF/GuerrillaNtp_CF.csproj @@ -0,0 +1,75 @@ + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {6A920E41-4597-4312-A5D0-43CC0DE70EBC} + Library + Properties + GuerrillaNtp + GuerrillaNtp_CF + {4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + PocketPC + b2c48bd2-963d-4549-9169-1fa021dce484 + 5.2 + GuerrillaNtp_CF + v3.5 + Windows Mobile 6 Professional SDK + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE;$(PlatformFamilyName) + true + true + prompt + 512 + 4 + Off + + + pdbonly + true + bin\Release\ + TRACE;$(PlatformFamilyName) + true + true + prompt + 512 + 4 + Off + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GuerrillaNtp_CF/NtpClient.cs b/GuerrillaNtp_CF/NtpClient.cs new file mode 100644 index 0000000..fd91a50 --- /dev/null +++ b/GuerrillaNtp_CF/NtpClient.cs @@ -0,0 +1,144 @@ +// Part of GuerrillaNtp: https://guerrillantp.machinezoo.com +using System; +using System.Net; +using System.Net.Sockets; + +namespace GuerrillaNtp +{ + /// + /// Represents UDP socket used to communicate with RFC4330-compliant SNTP/NTP server. + /// + /// + /// + /// See project homepage for guidance on how to use GuerrillaNtp. + /// Most applications should just call + /// after instantiating this class. Method + /// can be used to obtain additional details stored in reply . + /// + /// + /// This class holds unmanaged resources (the socket) and callers are responsible + /// for calling when they are done, + /// perhaps by instantiating this class in using block. + /// + /// + /// It is application responsibility to be a good netizen, + /// which most importantly means using reasonable polling intervals + /// and exponential backoff when querying public NTP server. + /// + /// + public class NtpClient + { + readonly Socket socket; + + /// + /// Creates new from server endpoint. + /// + /// Endpoint of the remote SNTP server. + /// + /// + public NtpClient(IPEndPoint endpoint) + { + socket = new Socket(endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + try + { + socket.Connect(endpoint); + } + catch + { + throw; + } + } + + /// + /// Creates new from server's IP address and optional port. + /// + /// IP address of remote SNTP server, standard NTP port is used. + /// + /// + public NtpClient(IPAddress address) : this(new IPEndPoint(address, 123)) { } + + /// + /// Creates new from server's IP address and optional port. + /// + /// IP address of remote SNTP server + /// Port of remote SNTP server. Default is 123 (standard NTP port). + /// + /// + public NtpClient(IPAddress address, int port) : this(new IPEndPoint(address, port)) { } + + /// + /// Queries the SNTP server and returns correction offset. + /// + /// + /// Use this method if you just want correction offset from the server. + /// Call to obtain + /// with additional information besides . + /// + /// + /// Offset that should be added to local time to match server time. + /// + /// Thrown when the server responds with invalid reply packet. + /// + /// Thrown when no reply is received before is reached + /// or when there is an error communicating with the server. + /// + /// + /// + public TimeSpan GetCorrectionOffset() { return Query().CorrectionOffset; } + + /// + /// Queries the SNTP server with configurable request. + /// + /// SNTP request packet to use when querying the network time server. + /// SNTP reply packet returned by the server. + /// + /// constructor + /// creates valid request packet, which you can further customize. + /// If you don't need any customization of the request packet, call instead. + /// Returned contains correction offset in + /// property. + /// + /// + /// Thrown when the request packet is invalid or when the server responds with invalid reply packet. + /// + /// + /// Thrown when no reply is received before is reached + /// or when there is an error communicating with the server. + /// + /// + /// + /// + public NtpPacket Query(NtpPacket request) + { + request.ValidateRequest(); + socket.Send(request.Bytes); + var response = new byte[160]; + int received = socket.Receive(response); + var truncated = new byte[received]; + Array.Copy(response, truncated, received); + NtpPacket reply = new NtpPacket(truncated) { DestinationTimestamp = DateTime.UtcNow }; + reply.ValidateReply(request); + return reply; + } + + /// + /// Queries the SNTP server with default options. + /// + /// + /// Use this method to obtain additional details from the returned + /// besides . + /// If you just need the correction offset, call instead. + /// You can customize request packed by calling . + /// + /// SNTP reply packet returned by the server. + /// Thrown when the server responds with invalid reply packet. + /// + /// Thrown when no reply is received before is reached + /// or when there is an error communicating with the server. + /// + /// + /// + /// + public NtpPacket Query() { return Query(new NtpPacket()); } + } +} diff --git a/GuerrillaNtp_CF/NtpException.cs b/GuerrillaNtp_CF/NtpException.cs new file mode 100644 index 0000000..3bb6970 --- /dev/null +++ b/GuerrillaNtp_CF/NtpException.cs @@ -0,0 +1,26 @@ +// Part of GuerrillaNtp: https://guerrillantp.machinezoo.com +using System; + +namespace GuerrillaNtp +{ + /// + /// Represents errors that occur in SNTP packets or during SNTP operation. + /// + public class NtpException : Exception + { + /// + /// Gets the SNTP packet that caused this exception, if any. + /// + /// + /// SNTP packet that caused this exception, usually reply packet, + /// or null if the error is not specific to any packet. + /// + public NtpPacket Packet { get; private set; } + + internal NtpException(NtpPacket packet, String message) + : base(message) + { + Packet = packet; + } + } +} diff --git a/GuerrillaNtp_CF/NtpLeapIndicator.cs b/GuerrillaNtp_CF/NtpLeapIndicator.cs new file mode 100644 index 0000000..8f0335e --- /dev/null +++ b/GuerrillaNtp_CF/NtpLeapIndicator.cs @@ -0,0 +1,30 @@ +// Part of GuerrillaNtp: https://guerrillantp.machinezoo.com +namespace GuerrillaNtp +{ + /// + /// 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 + } +} \ No newline at end of file diff --git a/GuerrillaNtp_CF/NtpMode.cs b/GuerrillaNtp_CF/NtpMode.cs new file mode 100644 index 0000000..0f0b116 --- /dev/null +++ b/GuerrillaNtp_CF/NtpMode.cs @@ -0,0 +1,20 @@ +// Part of GuerrillaNtp: https://guerrillantp.machinezoo.com +namespace GuerrillaNtp +{ + /// + /// 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, + } +} \ No newline at end of file diff --git a/GuerrillaNtp_CF/NtpPacket.cs b/GuerrillaNtp_CF/NtpPacket.cs new file mode 100644 index 0000000..aa1aac8 --- /dev/null +++ b/GuerrillaNtp_CF/NtpPacket.cs @@ -0,0 +1,357 @@ +// Part of GuerrillaNtp: https://guerrillantp.machinezoo.com +using System; + +namespace GuerrillaNtp +{ + /// + /// Represents RFC4330 SNTP packet used for communication to and from a network time server. + /// + /// + /// + /// See project homepage for guidance on how to use GuerrillaNtp. + /// Most applications should just use the property + /// or even better call . + /// + /// + /// 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. + /// + /// + /// + /// + /// + public class NtpPacket + { + 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; } + 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); } + 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 log₂ 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 log₂ 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); } set { SetDateTime64(16, value); } } + + /// + /// 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); } set { SetDateTime64(24, value); } } + + /// + /// 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); } set { SetDateTime64(32, value); } } + + /// + /// 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); } 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. + /// It is set by when reply packet is received. + /// + /// + /// + public DateTime? DestinationTimestamp { get; set; } + + /// + /// Gets the round-trip time to the server. + /// + /// + /// Time the request spent travelling to the server plus the time the reply spent travelling 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. + /// + /// Thrown when one of the required timestamps is not present. + /// + /// + /// + /// + /// + 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. + /// + /// Thrown when one of the required timestamps is not present. + /// + /// + /// + /// + /// + /// + public TimeSpan CorrectionOffset + { + get + { + CheckTimestamps(); + return TimeSpan.FromTicks(((ReceiveTimestamp.Value - OriginTimestamp.Value) - (DestinationTimestamp.Value - TransmitTimestamp.Value)).Ticks / 2); + } + } + + /// + /// Initializes default request packet. + /// + /// + /// Created request packet can be passed to . + /// 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; + } + + internal NtpPacket(byte[] bytes) + { + if (bytes.Length < 48) + throw new NtpException(null, "SNTP reply packet must be at least 48 bytes long."); + Bytes = bytes; + } + + internal void ValidateRequest() + { + if (Mode != NtpMode.Client) + throw new NtpException(this, "This is not a request SNTP packet."); + if (VersionNumber == 0) + throw new NtpException(this, "Protocol version of the request is not specified."); + if (TransmitTimestamp == null) + throw new NtpException(this, "TransmitTimestamp must be set in request packet."); + } + + internal void ValidateReply(NtpPacket request) + { + if (Mode != NtpMode.Server) + throw new NtpException(this, "This is not a reply SNTP packet."); + if (VersionNumber == 0) + throw new NtpException(this, "Protocol version of the reply is not specified."); + if (Stratum == 0) + throw new NtpException(this, String.Format("Received Kiss-o'-Death SNTP packet with code 0x{0:x}.", ReferenceId)); + if (LeapIndicator == NtpLeapIndicator.AlarmCondition) + throw new NtpException(this, "SNTP server has unsynchronized clock."); + CheckTimestamps(); + if (OriginTimestamp != request.TransmitTimestamp) + throw new NtpException(this, "Origin timestamp in reply doesn't match transmit timestamp in request."); + } + + void CheckTimestamps() + { + if (OriginTimestamp == null) + throw new NtpException(this, "Origin timestamp is missing."); + if (ReceiveTimestamp == null) + throw new NtpException(this, "Receive timestamp is missing."); + if (TransmitTimestamp == null) + throw new NtpException(this, "Transmit timestamp is missing."); + if (DestinationTimestamp == null) + throw new NtpException(this, "Destination timestamp is missing."); + } + + 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))); + } + void SetDateTime64(int offset, DateTime? value) { SetUInt64BE(offset, value == null ? 0 : Convert.ToUInt64((value.Value.Ticks - epoch.Ticks) * (0.0000001 * (1L << 32)))); } + TimeSpan GetTimeSpan32(int offset) { return TimeSpan.FromSeconds(GetInt32BE(offset) / (double)(1 << 16)); } + ulong GetUInt64BE(int offset) { return SwapEndianness(BitConverter.ToUInt64(Bytes, offset)); } + void SetUInt64BE(int offset, ulong value) { Array.Copy(BitConverter.GetBytes(SwapEndianness(value)), 0, Bytes, offset, 8); } + int GetInt32BE(int offset) { return (int)GetUInt32BE(offset); } + uint GetUInt32BE(int offset) { return SwapEndianness(BitConverter.ToUInt32(Bytes, offset)); } + static uint SwapEndianness(uint x) { return ((x & 0xff) << 24) | ((x & 0xff00) << 8) | ((x & 0xff0000) >> 8) | ((x & 0xff000000) >> 24); } + static ulong SwapEndianness(ulong x) { return ((ulong)SwapEndianness((uint)x) << 32) | SwapEndianness((uint)(x >> 32)); } + } +} diff --git a/GuerrillaNtp_CF/Properties/AssemblyInfo.cs b/GuerrillaNtp_CF/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..079fde3 --- /dev/null +++ b/GuerrillaNtp_CF/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GuerrillaNtp")] +[assembly: AssemblyDescription("App-embedded NTP client for time-sensitive apps")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Robert Važan")] +[assembly: AssemblyProduct("GuerrillaNtp")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("Copyright © 2014-2020 Robert Važan")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e59126d9-8544-41e5-8205-e385dc561133")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.4.1.0")] +