381 lines
15 KiB
Java
381 lines
15 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2008 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.annotation.Nullable;
|
||
|
import android.compat.annotation.UnsupportedAppUsage;
|
||
|
import android.net.sntp.Duration64;
|
||
|
import android.net.sntp.Timestamp64;
|
||
|
import android.os.SystemClock;
|
||
|
import android.util.Log;
|
||
|
import android.util.Slog;
|
||
|
|
||
|
import com.android.internal.annotations.VisibleForTesting;
|
||
|
import com.android.internal.util.TrafficStatsConstants;
|
||
|
|
||
|
import java.net.DatagramPacket;
|
||
|
import java.net.DatagramSocket;
|
||
|
import java.net.InetAddress;
|
||
|
import java.net.InetSocketAddress;
|
||
|
import java.net.UnknownHostException;
|
||
|
import java.security.NoSuchAlgorithmException;
|
||
|
import java.security.SecureRandom;
|
||
|
import java.time.Duration;
|
||
|
import java.time.Instant;
|
||
|
import java.util.Objects;
|
||
|
import java.util.Random;
|
||
|
import java.util.function.Supplier;
|
||
|
|
||
|
/**
|
||
|
* Simple, single-use SNTP client class for retrieving network time.
|
||
|
*
|
||
|
* Sample usage:
|
||
|
* <pre>SntpClient client = new SntpClient();
|
||
|
* if (client.requestTime("time.foo.com")) {
|
||
|
* long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
|
||
|
* }
|
||
|
* </pre>
|
||
|
*
|
||
|
* <p>This class is not thread-safe.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
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;
|
||
|
private static final int RECEIVE_TIME_OFFSET = 32;
|
||
|
private static final int TRANSMIT_TIME_OFFSET = 40;
|
||
|
private static final int NTP_PACKET_SIZE = 48;
|
||
|
|
||
|
public static final int STANDARD_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;
|
||
|
|
||
|
// The source of the current system clock time, replaceable for testing.
|
||
|
private final Supplier<Instant> mSystemTimeSupplier;
|
||
|
|
||
|
private final Random mRandom;
|
||
|
|
||
|
// The last offset calculated from an NTP server response
|
||
|
private long mClockOffset;
|
||
|
|
||
|
// The last system time computed from an NTP server response
|
||
|
private long mNtpTime;
|
||
|
|
||
|
// The value of SystemClock.elapsedRealtime() corresponding to mNtpTime / mClockOffset
|
||
|
private long mNtpTimeReference;
|
||
|
|
||
|
// The round trip (network) time in milliseconds
|
||
|
private long mRoundTripTime;
|
||
|
|
||
|
// Details of the NTP server used to obtain the time last.
|
||
|
@Nullable private InetSocketAddress mServerSocketAddress;
|
||
|
|
||
|
private static class InvalidServerReplyException extends Exception {
|
||
|
public InvalidServerReplyException(String message) {
|
||
|
super(message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@UnsupportedAppUsage
|
||
|
public SntpClient() {
|
||
|
this(Instant::now, defaultRandom());
|
||
|
}
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public SntpClient(Supplier<Instant> systemTimeSupplier, Random random) {
|
||
|
mSystemTimeSupplier = Objects.requireNonNull(systemTimeSupplier);
|
||
|
mRandom = Objects.requireNonNull(random);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sends an SNTP request to the given host and processes the response.
|
||
|
*
|
||
|
* @param host host name of the server.
|
||
|
* @param port port of the server.
|
||
|
* @param timeout network timeout in milliseconds. the timeout doesn't include the DNS lookup
|
||
|
* time, and it applies to each individual query to the resolved addresses of
|
||
|
* the NTP server.
|
||
|
* @param network network over which to send the request.
|
||
|
* @return true if the transaction was successful.
|
||
|
*/
|
||
|
public boolean requestTime(String host, int port, int timeout, Network network) {
|
||
|
final Network networkForResolv = network.getPrivateDnsBypassingCopy();
|
||
|
try {
|
||
|
final InetAddress[] addresses = networkForResolv.getAllByName(host);
|
||
|
for (int i = 0; i < addresses.length; i++) {
|
||
|
if (requestTime(addresses[i], port, timeout, networkForResolv)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
} catch (UnknownHostException e) {
|
||
|
Log.w(TAG, "Unknown host: " + host);
|
||
|
EventLogTags.writeNtpFailure(host, e.toString());
|
||
|
}
|
||
|
|
||
|
if (DBG) Log.d(TAG, "request time failed");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
public boolean requestTime(InetAddress address, int port, int timeout, Network network) {
|
||
|
DatagramSocket socket = null;
|
||
|
final int oldTag = TrafficStats.getAndSetThreadStatsTag(
|
||
|
TrafficStatsConstants.TAG_SYSTEM_NTP);
|
||
|
try {
|
||
|
socket = new DatagramSocket();
|
||
|
network.bindSocket(socket);
|
||
|
socket.setSoTimeout(timeout);
|
||
|
byte[] buffer = new byte[NTP_PACKET_SIZE];
|
||
|
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
|
||
|
// 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
|
||
|
final Instant requestTime = mSystemTimeSupplier.get();
|
||
|
final Timestamp64 requestTimestamp = Timestamp64.fromInstant(requestTime);
|
||
|
|
||
|
final Timestamp64 randomizedRequestTimestamp =
|
||
|
requestTimestamp.randomizeSubMillis(mRandom);
|
||
|
final long requestTicks = SystemClock.elapsedRealtime();
|
||
|
writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, randomizedRequestTimestamp);
|
||
|
|
||
|
socket.send(request);
|
||
|
|
||
|
// read the response
|
||
|
DatagramPacket response = new DatagramPacket(buffer, buffer.length);
|
||
|
socket.receive(response);
|
||
|
final long responseTicks = SystemClock.elapsedRealtime();
|
||
|
final Instant responseTime = requestTime.plusMillis(responseTicks - requestTicks);
|
||
|
final Timestamp64 responseTimestamp = Timestamp64.fromInstant(responseTime);
|
||
|
|
||
|
// extract the results
|
||
|
final byte leap = (byte) ((buffer[0] >> 6) & 0x3);
|
||
|
final byte mode = (byte) (buffer[0] & 0x7);
|
||
|
final int stratum = (int) (buffer[1] & 0xff);
|
||
|
final Timestamp64 referenceTimestamp = readTimeStamp(buffer, REFERENCE_TIME_OFFSET);
|
||
|
final Timestamp64 originateTimestamp = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
|
||
|
final Timestamp64 receiveTimestamp = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
|
||
|
final Timestamp64 transmitTimestamp = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
|
||
|
|
||
|
/* Do validation according to RFC */
|
||
|
checkValidServerReply(leap, mode, stratum, transmitTimestamp, referenceTimestamp,
|
||
|
randomizedRequestTimestamp, originateTimestamp);
|
||
|
|
||
|
long totalTransactionDurationMillis = responseTicks - requestTicks;
|
||
|
long serverDurationMillis =
|
||
|
Duration64.between(receiveTimestamp, transmitTimestamp).toDuration().toMillis();
|
||
|
long roundTripTimeMillis = totalTransactionDurationMillis - serverDurationMillis;
|
||
|
|
||
|
Duration clockOffsetDuration = calculateClockOffset(requestTimestamp,
|
||
|
receiveTimestamp, transmitTimestamp, responseTimestamp);
|
||
|
long clockOffsetMillis = clockOffsetDuration.toMillis();
|
||
|
|
||
|
EventLogTags.writeNtpSuccess(
|
||
|
address.toString(), roundTripTimeMillis, clockOffsetMillis);
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "round trip: " + roundTripTimeMillis + "ms, "
|
||
|
+ "clock offset: " + clockOffsetMillis + "ms");
|
||
|
}
|
||
|
|
||
|
// save our results - use the times on this side of the network latency
|
||
|
// (response rather than request time)
|
||
|
mClockOffset = clockOffsetMillis;
|
||
|
mNtpTime = responseTime.plus(clockOffsetDuration).toEpochMilli();
|
||
|
mNtpTimeReference = responseTicks;
|
||
|
mRoundTripTime = roundTripTimeMillis;
|
||
|
mServerSocketAddress = new InetSocketAddress(address, port);
|
||
|
} catch (Exception e) {
|
||
|
EventLogTags.writeNtpFailure(address.toString(), e.toString());
|
||
|
if (DBG) Log.d(TAG, "request time failed: " + e);
|
||
|
return false;
|
||
|
} finally {
|
||
|
if (socket != null) {
|
||
|
socket.close();
|
||
|
}
|
||
|
TrafficStats.setThreadStatsTag(oldTag);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** Performs the NTP clock offset calculation. */
|
||
|
@VisibleForTesting
|
||
|
public static Duration calculateClockOffset(Timestamp64 clientRequestTimestamp,
|
||
|
Timestamp64 serverReceiveTimestamp, Timestamp64 serverTransmitTimestamp,
|
||
|
Timestamp64 clientResponseTimestamp) {
|
||
|
// According to RFC4330:
|
||
|
// t is the system clock offset (the adjustment we are trying to find)
|
||
|
// t = ((T2 - T1) + (T3 - T4)) / 2
|
||
|
//
|
||
|
// Which is:
|
||
|
// t = (([server]receiveTimestamp - [client]requestTimestamp)
|
||
|
// + ([server]transmitTimestamp - [client]responseTimestamp)) / 2
|
||
|
//
|
||
|
// See the NTP spec and tests: the numeric types used are deliberate:
|
||
|
// + Duration64.between() uses 64-bit arithmetic (32-bit for the seconds).
|
||
|
// + plus() / dividedBy() use Duration, which isn't the double precision floating point
|
||
|
// used in NTPv4, but is good enough.
|
||
|
return Duration64.between(clientRequestTimestamp, serverReceiveTimestamp)
|
||
|
.plus(Duration64.between(clientResponseTimestamp, serverTransmitTimestamp))
|
||
|
.dividedBy(2);
|
||
|
}
|
||
|
|
||
|
@Deprecated
|
||
|
@UnsupportedAppUsage
|
||
|
public boolean requestTime(String host, int timeout) {
|
||
|
Log.w(TAG, "Shame on you for calling the hidden API requestTime()!");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the offset calculated to apply to the client clock to arrive at {@link #getNtpTime()}
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public long getClockOffset() {
|
||
|
return mClockOffset;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the time computed from the NTP transaction.
|
||
|
*
|
||
|
* @return time value computed from NTP server response.
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
public long getNtpTime() {
|
||
|
return mNtpTime;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the reference clock value (value of SystemClock.elapsedRealtime())
|
||
|
* corresponding to the NTP time.
|
||
|
*
|
||
|
* @return reference clock corresponding to the NTP time.
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
public long getNtpTimeReference() {
|
||
|
return mNtpTimeReference;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the round trip time of the NTP transaction
|
||
|
*
|
||
|
* @return round trip time in milliseconds.
|
||
|
*/
|
||
|
@UnsupportedAppUsage
|
||
|
public long getRoundTripTime() {
|
||
|
return mRoundTripTime;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the address of the NTP server used in the NTP transaction
|
||
|
*/
|
||
|
@Nullable
|
||
|
public InetSocketAddress getServerSocketAddress() {
|
||
|
return mServerSocketAddress;
|
||
|
}
|
||
|
|
||
|
private static void checkValidServerReply(
|
||
|
byte leap, byte mode, int stratum, Timestamp64 transmitTimestamp,
|
||
|
Timestamp64 referenceTimestamp, Timestamp64 randomizedRequestTimestamp,
|
||
|
Timestamp64 originateTimestamp) 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 (!randomizedRequestTimestamp.equals(originateTimestamp)) {
|
||
|
throw new InvalidServerReplyException(
|
||
|
"originateTimestamp != randomizedRequestTimestamp");
|
||
|
}
|
||
|
if (transmitTimestamp.equals(Timestamp64.ZERO)) {
|
||
|
throw new InvalidServerReplyException("zero transmitTimestamp");
|
||
|
}
|
||
|
if (referenceTimestamp.equals(Timestamp64.ZERO)) {
|
||
|
throw new InvalidServerReplyException("zero referenceTimestamp");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reads an unsigned 32 bit big endian number from the given offset in the buffer.
|
||
|
*/
|
||
|
private long readUnsigned32(byte[] buffer, int offset) {
|
||
|
int i0 = buffer[offset++] & 0xFF;
|
||
|
int i1 = buffer[offset++] & 0xFF;
|
||
|
int i2 = buffer[offset++] & 0xFF;
|
||
|
int i3 = buffer[offset] & 0xFF;
|
||
|
|
||
|
int bits = (i0 << 24) | (i1 << 16) | (i2 << 8) | i3;
|
||
|
return bits & 0xFFFF_FFFFL;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reads the NTP time stamp from the given offset in the buffer.
|
||
|
*/
|
||
|
private Timestamp64 readTimeStamp(byte[] buffer, int offset) {
|
||
|
long seconds = readUnsigned32(buffer, offset);
|
||
|
int fractionBits = (int) readUnsigned32(buffer, offset + 4);
|
||
|
return Timestamp64.fromComponents(seconds, fractionBits);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Writes the NTP time stamp at the given offset in the buffer.
|
||
|
*/
|
||
|
private void writeTimeStamp(byte[] buffer, int offset, Timestamp64 timestamp) {
|
||
|
long seconds = timestamp.getEraSeconds();
|
||
|
// 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);
|
||
|
|
||
|
int fractionBits = timestamp.getFractionBits();
|
||
|
// write fraction in big endian format
|
||
|
buffer[offset++] = (byte) (fractionBits >>> 24);
|
||
|
buffer[offset++] = (byte) (fractionBits >>> 16);
|
||
|
buffer[offset++] = (byte) (fractionBits >>> 8);
|
||
|
buffer[offset] = (byte) (fractionBits);
|
||
|
}
|
||
|
|
||
|
private static Random defaultRandom() {
|
||
|
Random random;
|
||
|
try {
|
||
|
random = SecureRandom.getInstanceStrong();
|
||
|
} catch (NoSuchAlgorithmException e) {
|
||
|
// This should never happen.
|
||
|
Slog.wtf(TAG, "Unable to access SecureRandom", e);
|
||
|
random = new Random(System.currentTimeMillis());
|
||
|
}
|
||
|
return random;
|
||
|
}
|
||
|
}
|