Skip to content

Instantly share code, notes, and snippets.

@BravoTango86
Created September 20, 2016 22:04
Show Gist options
  • Save BravoTango86/2e221d6cac22f7e432c187c941b01648 to your computer and use it in GitHub Desktop.
Save BravoTango86/2e221d6cac22f7e432c187c941b01648 to your computer and use it in GitHub Desktop.
C# SNTP Client based on android.net.SntpClient
/*
* 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