diff options
| -rw-r--r-- | core/java/android/net/SntpClient.java | 95 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/net/SntpClientTest.java | 222 |
2 files changed, 300 insertions, 17 deletions
diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java index 767301152796..cf9243f7e7a3 100644 --- a/core/java/android/net/SntpClient.java +++ b/core/java/android/net/SntpClient.java @@ -22,6 +22,7 @@ import android.util.Log; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; +import java.util.Arrays; /** * {@hide} @@ -38,6 +39,7 @@ import java.net.InetAddress; public class SntpClient { private static final String TAG = "SntpClient"; + private static final boolean DBG = true; private static final int REFERENCE_TIME_OFFSET = 16; private static final int ORIGINATE_TIME_OFFSET = 24; @@ -47,8 +49,14 @@ public class SntpClient private static final int NTP_PORT = 123; private static final int NTP_MODE_CLIENT = 3; + private static final int NTP_MODE_SERVER = 4; + private static final int NTP_MODE_BROADCAST = 5; private static final int NTP_VERSION = 3; + private static final int NTP_LEAP_NOSYNC = 3; + private static final int NTP_STRATUM_DEATH = 0; + private static final int NTP_STRATUM_MAX = 15; + // Number of seconds between Jan 1, 1900 and Jan 1, 1970 // 70 years plus 17 leap days private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L; @@ -62,6 +70,12 @@ public class SntpClient // round trip time in milliseconds private long mRoundTripTime; + private static class InvalidServerReplyException extends Exception { + public InvalidServerReplyException(String message) { + super(message); + } + } + /** * Sends an SNTP request to the given host and processes the response. * @@ -70,13 +84,23 @@ public class SntpClient * @return true if the transaction was successful. */ public boolean requestTime(String host, int timeout) { + InetAddress address = null; + try { + address = InetAddress.getByName(host); + } catch (Exception e) { + if (DBG) Log.d(TAG, "request time failed: " + e); + return false; + } + return requestTime(address, NTP_PORT, timeout); + } + + public boolean requestTime(InetAddress address, int port, int timeout) { DatagramSocket socket = null; try { socket = new DatagramSocket(); socket.setSoTimeout(timeout); - InetAddress address = InetAddress.getByName(host); byte[] buffer = new byte[NTP_PACKET_SIZE]; - DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT); + DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, port); // set mode = 3 (client) and version = 3 // mode is in low 3 bits of first byte @@ -84,8 +108,8 @@ public class SntpClient buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3); // get current time and write it to the request packet - long requestTime = System.currentTimeMillis(); - long requestTicks = SystemClock.elapsedRealtime(); + final long requestTime = System.currentTimeMillis(); + final long requestTicks = SystemClock.elapsedRealtime(); writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime); socket.send(request); @@ -93,13 +117,21 @@ public class SntpClient // read the response DatagramPacket response = new DatagramPacket(buffer, buffer.length); socket.receive(response); - long responseTicks = SystemClock.elapsedRealtime(); - long responseTime = requestTime + (responseTicks - requestTicks); + final long responseTicks = SystemClock.elapsedRealtime(); + final long responseTime = requestTime + (responseTicks - requestTicks); // extract the results - long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET); - long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET); - long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET); + final byte leap = (byte) ((buffer[0] >> 6) & 0x3); + final byte mode = (byte) (buffer[0] & 0x7); + final int stratum = (int) (buffer[1] & 0xff); + final long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET); + final long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET); + final 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 @@ -110,8 +142,10 @@ public class SntpClient // = (transit + skew - transit + skew)/2 // = (2 * skew)/2 = skew long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2; - // if (false) Log.d(TAG, "round trip: " + roundTripTime + " ms"); - // if (false) Log.d(TAG, "clock offset: " + clockOffset + " ms"); + if (DBG) { + Log.d(TAG, "round trip: " + roundTripTime + "ms, " + + "clock offset: " + clockOffset + "ms"); + } // save our results - use the times on this side of the network latency // (response rather than request time) @@ -119,7 +153,7 @@ public class SntpClient mNtpTimeReference = responseTicks; mRoundTripTime = roundTripTime; } catch (Exception e) { - if (false) Log.d(TAG, "request time failed: " + e); + if (DBG) Log.d(TAG, "request time failed: " + e); return false; } finally { if (socket != null) { @@ -158,6 +192,23 @@ public class SntpClient return mRoundTripTime; } + private static void checkValidServerReply( + byte leap, byte mode, int stratum, long transmitTime) + throws InvalidServerReplyException { + 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. */ @@ -177,20 +228,30 @@ public class SntpClient } /** - * Reads the NTP time stamp at the given offset in the buffer and returns + * 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); - return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L); + // 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 + * 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); + return; + } + long seconds = time / 1000L; long milliseconds = time - seconds * 1000L; seconds += OFFSET_1900_TO_1970; diff --git a/core/tests/coretests/src/android/net/SntpClientTest.java b/core/tests/coretests/src/android/net/SntpClientTest.java new file mode 100644 index 000000000000..8b8cf671e723 --- /dev/null +++ b/core/tests/coretests/src/android/net/SntpClientTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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. + */ + +package android.net; + +import android.net.SntpClient; +import android.util.Log; +import libcore.util.HexEncoding; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.util.Arrays; +import junit.framework.TestCase; + + +public class SntpClientTest extends TestCase { + private static final String TAG = "SntpClientTest"; + + private static final int ORIGINATE_TIME_OFFSET = 24; + private static final int TRANSMIT_TIME_OFFSET = 40; + + private static final int NTP_MODE_SERVER = 4; + private static final int NTP_MODE_BROADCAST = 5; + + // From tcpdump (admittedly, an NTPv4 packet): + // + // Server, Leap indicator: (0), Stratum 2 (secondary reference), poll 6 (64s), precision -20 + // Root Delay: 0.005447, Root dispersion: 0.002716, Reference-ID: 221.253.71.41 + // Reference Timestamp: 3653932102.507969856 (2015/10/15 14:08:22) + // Originator Timestamp: 3653932113.576327741 (2015/10/15 14:08:33) + // Receive Timestamp: 3653932113.581012725 (2015/10/15 14:08:33) + // Transmit Timestamp: 3653932113.581012725 (2015/10/15 14:08:33) + // Originator - Receive Timestamp: +0.004684958 + // Originator - Transmit Timestamp: +0.004684958 + private static final String WORKING_VERSION4 = + "240206ec" + + "00000165" + + "000000b2" + + "ddfd4729" + + "d9ca9446820a5000" + + "d9ca9451938a3771" + + "d9ca945194bd3fff" + + "d9ca945194bd4001"; + + private final SntpTestServer mServer = new SntpTestServer(); + private final SntpClient mClient = new SntpClient(); + + public void testBasicWorkingSntpClientQuery() throws Exception { + mServer.setServerReply(HexEncoding.decode(WORKING_VERSION4.toCharArray(), false)); + assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500)); + assertEquals(1, mServer.numRequestsReceived()); + assertEquals(1, mServer.numRepliesSent()); + } + + public void testDnsResolutionFailure() throws Exception { + assertFalse(mClient.requestTime("ntp.server.doesnotexist.example", 5000)); + } + + public void testTimeoutFailure() throws Exception { + mServer.clearServerReply(); + assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500)); + assertEquals(1, mServer.numRequestsReceived()); + assertEquals(0, mServer.numRepliesSent()); + } + + public void testIgnoreLeapNoSync() throws Exception { + final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false); + reply[0] |= (byte) 0xc0; + mServer.setServerReply(reply); + assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500)); + assertEquals(1, mServer.numRequestsReceived()); + assertEquals(1, mServer.numRepliesSent()); + } + + public void testAcceptOnlyServerAndBroadcastModes() throws Exception { + final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false); + for (int i = 0; i <= 7; i++) { + final String logMsg = "mode: " + i; + reply[0] &= (byte) 0xf8; + reply[0] |= (byte) i; + mServer.setServerReply(reply); + final boolean rval = mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500); + switch (i) { + case NTP_MODE_SERVER: + case NTP_MODE_BROADCAST: + assertTrue(logMsg, rval); + break; + default: + assertFalse(logMsg, rval); + break; + } + assertEquals(logMsg, 1, mServer.numRequestsReceived()); + assertEquals(logMsg, 1, mServer.numRepliesSent()); + } + } + + public void testAcceptableStrataOnly() throws Exception { + final int STRATUM_MIN = 1; + final int STRATUM_MAX = 15; + + final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false); + for (int i = 0; i < 256; i++) { + final String logMsg = "stratum: " + i; + reply[1] = (byte) i; + mServer.setServerReply(reply); + final boolean rval = mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500); + if (STRATUM_MIN <= i && i <= STRATUM_MAX) { + assertTrue(logMsg, rval); + } else { + assertFalse(logMsg, rval); + } + assertEquals(logMsg, 1, mServer.numRequestsReceived()); + assertEquals(logMsg, 1, mServer.numRepliesSent()); + } + } + + public void testZeroTransmitTime() throws Exception { + final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false); + Arrays.fill(reply, TRANSMIT_TIME_OFFSET, TRANSMIT_TIME_OFFSET + 8, (byte) 0x00); + mServer.setServerReply(reply); + assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500)); + assertEquals(1, mServer.numRequestsReceived()); + assertEquals(1, mServer.numRepliesSent()); + } + + + private static class SntpTestServer { + private final Object mLock = new Object(); + private final DatagramSocket mSocket; + private final InetAddress mAddress; + private final int mPort; + private byte[] mReply; + private int mRcvd; + private int mSent; + private Thread mListeningThread; + + public SntpTestServer() { + mSocket = makeSocket(); + mAddress = mSocket.getLocalAddress(); + mPort = mSocket.getLocalPort(); + Log.d(TAG, "testing server listening on (" + mAddress + ", " + mPort + ")"); + + mListeningThread = new Thread() { + public void run() { + while (true) { + byte[] buffer = new byte[512]; + DatagramPacket ntpMsg = new DatagramPacket(buffer, buffer.length); + try { + mSocket.receive(ntpMsg); + } catch (IOException e) { + Log.e(TAG, "datagram receive error: " + e); + break; + } + synchronized (mLock) { + mRcvd++; + if (mReply == null) { continue; } + // Copy transmit timestamp into originate timestamp. + // TODO: bounds checking. + System.arraycopy(ntpMsg.getData(), TRANSMIT_TIME_OFFSET, + mReply, ORIGINATE_TIME_OFFSET, 8); + ntpMsg.setData(mReply); + ntpMsg.setLength(mReply.length); + try { + mSocket.send(ntpMsg); + } catch (IOException e) { + Log.e(TAG, "datagram send error: " + e); + break; + } + mSent++; + } + } + mSocket.close(); + } + }; + mListeningThread.start(); + } + + private DatagramSocket makeSocket() { + DatagramSocket socket; + try { + socket = new DatagramSocket(0, InetAddress.getLoopbackAddress()); + } catch (SocketException e) { + Log.e(TAG, "Failed to create test server socket: " + e); + return null; + } + return socket; + } + + public void clearServerReply() { + setServerReply(null); + } + + public void setServerReply(byte[] reply) { + synchronized (mLock) { + mReply = reply; + mRcvd = 0; + mSent = 0; + } + } + + public InetAddress getAddress() { return mAddress; } + public int getPort() { return mPort; } + public int numRequestsReceived() { synchronized (mLock) { return mRcvd; } } + public int numRepliesSent() { synchronized (mLock) { return mSent; } } + } +} |