blob: 87edb6e0142535720705e3e28ade71cd1e51fc81 [file] [log] [blame]
/*
* 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 static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
import libcore.util.HexEncoding;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
public class SntpClientTest {
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 SntpTestServer mServer;
private SntpClient mClient;
private Network mNetwork;
@Before
public void setUp() throws Exception {
// NETID_UNSET allows the test to run, with a loopback server, even w/o external networking
mNetwork = new Network(ConnectivityManager.NETID_UNSET);
mServer = new SntpTestServer();
mClient = new SntpClient();
}
@Test
public void testBasicWorkingSntpClientQuery() throws Exception {
mServer.setServerReply(HexEncoding.decode(WORKING_VERSION4.toCharArray(), false));
assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
assertEquals(1, mServer.numRequestsReceived());
assertEquals(1, mServer.numRepliesSent());
}
@Test
public void testDnsResolutionFailure() throws Exception {
assertFalse(mClient.requestTime("ntp.server.doesnotexist.example", 5000, mNetwork));
}
@Test
public void testTimeoutFailure() throws Exception {
mServer.clearServerReply();
assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
assertEquals(1, mServer.numRequestsReceived());
assertEquals(0, mServer.numRepliesSent());
}
@Test
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, mNetwork));
assertEquals(1, mServer.numRequestsReceived());
assertEquals(1, mServer.numRepliesSent());
}
@Test
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,
mNetwork);
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());
}
}
@Test
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,
mNetwork);
if (STRATUM_MIN <= i && i <= STRATUM_MAX) {
assertTrue(logMsg, rval);
} else {
assertFalse(logMsg, rval);
}
assertEquals(logMsg, 1, mServer.numRequestsReceived());
assertEquals(logMsg, 1, mServer.numRepliesSent());
}
}
@Test
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, mNetwork));
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; } }
}
}