Created
September 20, 2016 22:04
-
-
Save BravoTango86/2e221d6cac22f7e432c187c941b01648 to your computer and use it in GitHub Desktop.
C# SNTP Client based on android.net.SntpClient
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Derived from https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/net/SntpClient.java | |
* | |
* Copyright (C) 2016 BravoTango86 | |
* | |
* 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. | |
*/ | |
using System; | |
using System.Net; | |
using System.Net.Sockets; | |
using System.Threading.Tasks; | |
public class SntpClient { | |
public string DefaultHostName { get; set; } = "time1.google.com"; | |
public int DefaultPort { get; set; } = 123; | |
public int DefaultTimeout { get; set; } = 1000; | |
public bool Debug { get; set; } = true; | |
private const int REFERENCE_TIME_OFFSET = 16; | |
private const int ORIGINATE_TIME_OFFSET = 24; | |
private const int RECEIVE_TIME_OFFSET = 32; | |
private const int TRANSMIT_TIME_OFFSET = 40; | |
private const int NTP_PACKET_SIZE = 48; | |
private const int NTP_PORT = 123; | |
private const int NTP_MODE_CLIENT = 3; | |
private const int NTP_MODE_SERVER = 4; | |
private const int NTP_MODE_BROADCAST = 5; | |
private const int NTP_VERSION = 3; | |
private const int NTP_LEAP_NOSYNC = 3; | |
private const int NTP_STRATUM_DEATH = 0; | |
private const int NTP_STRATUM_MAX = 15; | |
// Number of seconds between Jan 1, 1900 and Jan 1, 1970 | |
// 70 years plus 17 leap days | |
private const long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L; | |
// system time computed from NTP server response | |
public long NTPTime { get; private set; } | |
// value of SystemClock.elapsedRealtime() corresponding to mNtpTime | |
public long NTPTimeReference { get; private set; } | |
public long NTPOffset { get; private set; } | |
// round trip time in milliseconds | |
public long RoundTripTime { get; private set; } | |
private class InvalidServerReplyException : Exception { | |
public InvalidServerReplyException(string message) : base(message) { | |
} | |
} | |
public async Task<bool> RequestTime(IPEndPoint endPoint, int? timeout = null) => await DoRequestTime(endPoint: endPoint, timeout: timeout); | |
public async Task<bool> RequestTime(string hostName, int? port = 123, int? timeout = null) => await DoRequestTime(hostName: hostName, port: port, timeout: timeout); | |
public async Task<bool> RequestTime(int? timeout = null) => await RequestTime(DefaultHostName, timeout); | |
private async Task<bool> DoRequestTime(IPEndPoint endPoint = null, string hostName = null, int? port = null, int? timeout = null) { | |
UdpClient client = null; | |
try { | |
if (endPoint == null && string.IsNullOrEmpty(hostName)) throw new ArgumentException("No destination specified"); | |
using (client = new UdpClient()) { | |
byte[] buffer = new byte[NTP_PACKET_SIZE]; | |
// set mode = 3 (client) and version = 3 | |
// mode is in low 3 bits of first byte | |
// version is in bits 3-5 of first byte | |
buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3); | |
// get current time and write it to the request packet | |
long requestTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); | |
long requestTicks = Environment.TickCount; | |
writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime); | |
if (endPoint != null) await client.SendAsync(buffer, buffer.Length, endPoint); | |
else await client.SendAsync(buffer, buffer.Length, hostName, port ?? 123); | |
// No point in using this, won't timeout | |
// client.Client.ReceiveTimeout = timeout ?? DefaultTimeout; | |
// buffer = (await client.ReceiveAsync()).Buffer; | |
// Messy, not sure how well this works but waiting perpetually for data is hardly efficient... | |
Task<UdpReceiveResult> Receiver = client.ReceiveAsync(); | |
if (Task.WaitAny(Task.Delay(timeout ?? DefaultTimeout), Receiver) == 0) { | |
if (Debug) Console.WriteLine("Timed out on receive"); | |
return false; | |
} else buffer = Receiver.Result.Buffer; | |
long responseTicks = Environment.TickCount; | |
long responseTime = requestTime + (responseTicks - requestTicks); | |
// extract the results | |
byte leap = (byte)((buffer[0] >> 6) & 0x3); | |
byte mode = (byte)(buffer[0] & 0x7); | |
int stratum = (int)(buffer[1] & 0xff); | |
long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET); | |
long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET); | |
long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET); | |
/* do sanity check according to RFC */ | |
// TODO: validate originateTime == requestTime. | |
checkValidServerReply(leap, mode, stratum, transmitTime); | |
long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime); | |
// receiveTime = originateTime + transit + skew | |
// responseTime = transmitTime + transit - skew | |
// clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2 | |
// = ((originateTime + transit + skew - originateTime) + | |
// (transmitTime - (transmitTime + transit - skew)))/2 | |
// = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2 | |
// = (transit + skew - transit + skew)/2 | |
// = (2 * skew)/2 = skew | |
long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2; | |
if (Debug) Console.WriteLine("round trip: {0}ms clock offset: {1}ms", roundTripTime, clockOffset); | |
// save our results - use the times on this side of the network latency | |
// (response rather than request time) | |
NTPTime = responseTime + clockOffset; | |
NTPTimeReference = responseTicks; | |
NTPOffset = clockOffset; | |
RoundTripTime = roundTripTime; | |
} | |
} catch (Exception e) { | |
if (Debug) Console.WriteLine("request time failed: {0}", e); | |
return false; | |
} | |
return true; | |
} | |
/// <summary> | |
/// Provides the current <see cref="NTPTime"/> as a DateTimeOffset relative to local time or UTC | |
/// </summary> | |
/// <param name="local">Uses local TimeZoneInfo if true else defaults to UTC</param> | |
public DateTimeOffset GetDateTimeOffset(bool local = false) => | |
TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(NTPTime), local ? TimeZoneInfo.Local : TimeZoneInfo.Utc); | |
/// <summary> | |
/// Provides the current NTPOffset as a TimeSpan | |
/// </summary> | |
public TimeSpan GetOffset() => TimeSpan.FromMilliseconds(NTPOffset); | |
private static void checkValidServerReply(byte leap, byte mode, int stratum, long transmitTime) { | |
if (leap == NTP_LEAP_NOSYNC) { | |
throw new InvalidServerReplyException("unsynchronized server"); | |
} | |
if ((mode != NTP_MODE_SERVER) && (mode != NTP_MODE_BROADCAST)) { | |
throw new InvalidServerReplyException("untrusted mode: " + mode); | |
} | |
if ((stratum == NTP_STRATUM_DEATH) || (stratum > NTP_STRATUM_MAX)) { | |
throw new InvalidServerReplyException("untrusted stratum: " + stratum); | |
} | |
if (transmitTime == 0) { | |
throw new InvalidServerReplyException("zero transmitTime"); | |
} | |
} | |
/** | |
* Reads an unsigned 32 bit big endian number from the given offset in the buffer. | |
*/ | |
private long read32(byte[] buffer, int offset) { | |
byte b0 = buffer[offset]; | |
byte b1 = buffer[offset + 1]; | |
byte b2 = buffer[offset + 2]; | |
byte b3 = buffer[offset + 3]; | |
// convert signed bytes to unsigned values | |
int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0); | |
int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1); | |
int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2); | |
int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3); | |
return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3; | |
} | |
/** | |
* Reads the NTP time stamp at the given offset in the buffer and returns | |
* it as a system time (milliseconds since January 1, 1970). | |
*/ | |
private long readTimeStamp(byte[] buffer, int offset) { | |
long seconds = read32(buffer, offset); | |
long fraction = read32(buffer, offset + 4); | |
// Special case: zero means zero. | |
if (seconds == 0 && fraction == 0) { | |
return 0; | |
} | |
return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L); | |
} | |
/** | |
* Writes system time (milliseconds since January 1, 1970) as an NTP time stamp | |
* at the given offset in the buffer. | |
*/ | |
private void writeTimeStamp(byte[] buffer, int offset, long time) { | |
// Special case: zero means zero. | |
if (time == 0) { | |
//Arrays.fill(buffer, offset, offset + 8, (byte)0x00); | |
Buffer.BlockCopy(new byte[8], 0, buffer, offset, 8); | |
return; | |
} | |
long seconds = time / 1000L; | |
long milliseconds = time - seconds * 1000L; | |
seconds += OFFSET_1900_TO_1970; | |
// write seconds in big endian format | |
buffer[offset++] = (byte)(seconds >> 24); | |
buffer[offset++] = (byte)(seconds >> 16); | |
buffer[offset++] = (byte)(seconds >> 8); | |
buffer[offset++] = (byte)(seconds >> 0); | |
long fraction = milliseconds * 0x100000000L / 1000L; | |
// write fraction in big endian format | |
buffer[offset++] = (byte)(fraction >> 24); | |
buffer[offset++] = (byte)(fraction >> 16); | |
buffer[offset++] = (byte)(fraction >> 8); | |
// low order bits should be random data | |
buffer[offset++] = (byte)(new Random().Next(255)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment