package net.i2p.router.time; /* * Copyright (c) 2004, Adam Buckley * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of Adam Buckley nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ import java.io.IOException; import java.io.InterruptedIOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.Inet6Address; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import net.i2p.data.DataHelper; import net.i2p.util.HexDump; import net.i2p.util.Log; /** * NtpClient - an NTP client for Java. This program connects to an NTP server * and prints the response to the console. * * The local clock offset calculation is implemented according to the SNTP * algorithm specified in RFC 2030. * * Note that on windows platforms, the curent time-of-day timestamp is limited * to an resolution of 10ms and adversely affects the accuracy of the results. * * Public only for main(), not a public API, not for external use. * * TODO NOT 2036-compliant, see RFC 4330 * * @author Adam Buckley * (minor refactoring by jrandom) * @since 0.9.1 moved from net.i2p.time */ public class NtpClient { /** difference between the unix epoch and jan 1 1900 (NTP uses that) */ final static double SECONDS_1900_TO_EPOCH = 2208988800.0; private final static int NTP_PORT = 123; private static final int DEFAULT_TIMEOUT = 10*1000; private static final int OFF_ORIGTIME = 24; private static final int OFF_TXTIME = 40; private static final int MIN_PKT_LEN = 48; // IP:reason for servers that sent us a kiss of death private static final Map<String, String> kisses = new ConcurrentHashMap<String, String>(2); /** * Query the ntp servers, returning the current time from first one we find * * @return milliseconds since january 1, 1970 (UTC) * @throws IllegalArgumentException if none of the servers are reachable */ /**** public static long currentTime(String serverNames[]) { if (serverNames == null) throw new IllegalArgumentException("No NTP servers specified"); ArrayList<String> names = new ArrayList<String>(serverNames.length); for (int i = 0; i < serverNames.length; i++) names.add(serverNames[i]); Collections.shuffle(names); for (int i = 0; i < names.size(); i++) { long now = currentTime(names.get(i)); if (now > 0) return now; } throw new IllegalArgumentException("No reachable NTP servers specified"); } ****/ /** * Query the ntp servers, returning the current time from first one we find * Hack to return time and stratum * * @param log may be null * @return time in rv[0] and stratum in rv[1] * @throws IllegalArgumentException if none of the servers are reachable * @since 0.7.12 */ static long[] currentTimeAndStratum(String serverNames[], int perServerTimeout, boolean preferIPv6, Log log) { if (serverNames == null) throw new IllegalArgumentException("No NTP servers specified"); ArrayList<String> names = new ArrayList<String>(serverNames.length); for (int i = 0; i < serverNames.length; i++) names.add(serverNames[i]); Collections.shuffle(names); for (int i = 0; i < names.size(); i++) { long[] rv = currentTimeAndStratum(names.get(i), perServerTimeout, preferIPv6, log); if (rv != null && rv[0] > 0) return rv; } throw new IllegalArgumentException("No reachable NTP servers specified"); } /** * Query the given NTP server, returning the current internet time * * @return milliseconds since january 1, 1970 (UTC), or -1 on error */ /**** public static long currentTime(String serverName) { long[] la = currentTimeAndStratum(serverName, DEFAULT_TIMEOUT); if (la != null) return la[0]; return -1; } ****/ /** * Hack to return time and stratum * * @param log may be null * @return time in rv[0] and stratum in rv[1], or null for error * @since 0.7.12 */ private static long[] currentTimeAndStratum(String serverName, int timeout, boolean preferIPv6, Log log) { DatagramSocket socket = null; try { // Send request InetAddress address; if (preferIPv6) { InetAddress[] addrs = InetAddress.getAllByName(serverName); if (addrs == null || addrs.length == 0) throw new UnknownHostException(); address = null; for (int i = 0; i < addrs.length; i++) { if (addrs[i] instanceof Inet6Address) { address = addrs[i]; break; } if (address == null) address = addrs[0]; } } else { address = InetAddress.getByName(serverName); } String who = address.getHostAddress(); String why = kisses.get(who); if (why != null) { if (log != null) log.warn("Not querying, previous KoD from NTP server " + serverName + " (" + who + ") " + why); return null; } byte[] buf = new NtpMessage().toByteArray(); DatagramPacket packet = new DatagramPacket(buf, buf.length, address, NTP_PORT); byte[] txtime = new byte[8]; socket = new DatagramSocket(); // Set the transmit timestamp *just* before sending the packet // ToDo: Does this actually improve performance or not? NtpMessage.encodeTimestamp(packet.getData(), OFF_TXTIME, (System.currentTimeMillis()/1000.0) + SECONDS_1900_TO_EPOCH); socket.send(packet); // save for check System.arraycopy(packet.getData(), OFF_TXTIME, txtime, 0, 8); if (log != null && log.shouldDebug()) log.debug("Sent to " + serverName + " (" + who + ")\n" + HexDump.dump(buf)); // Get response packet = new DatagramPacket(buf, buf.length); socket.setSoTimeout(timeout); socket.receive(packet); // Immediately record the incoming timestamp double destinationTimestamp = (System.currentTimeMillis()/1000.0) + SECONDS_1900_TO_EPOCH; if (packet.getLength() < MIN_PKT_LEN) { if (log != null && log.shouldWarn()) log.warn("Short packet length " + packet.getLength()); return null; } // Process response NtpMessage msg = new NtpMessage(packet.getData()); String from = packet.getAddress().getHostAddress(); int port = packet.getPort(); if (log != null && log.shouldDebug()) log.debug("Received from: " + from + " port " + port + '\n' + msg + '\n' + HexDump.dump(packet.getData())); // spoof check if (port != NTP_PORT || !who.equals(from)) { if (log != null && log.shouldWarn()) log.warn("Sent to " + who + " port " + NTP_PORT+ " but received from " + packet.getSocketAddress()); return null; } // Stratum must be between 1 (atomic) and 15 (maximum defined value) // Anything else is right out, treat such responses like errors // KoD (stratum 0) processing is below, after origin time check if (msg.stratum > 15) { if (log != null && log.shouldWarn()) log.warn("NTP server " + serverName + " bad stratum " + msg.stratum); return null; } // spoof check if (!DataHelper.eq(txtime, 0, packet.getData(), OFF_ORIGTIME, 8)) { if (log != null && log.shouldWarn()) log.warn("Origin time mismatch sent:\n" + HexDump.dump(txtime) + "rcvd:\n" + HexDump.dump(packet.getData(), OFF_ORIGTIME, 8)); return null; } // KoD check (AFTER spoof checks) if (msg.stratum == 0) { why = msg.referenceIdentifierToString(); // Remember the specific IP, not the server name, although RFC 4330 // probably wants us to block the name kisses.put(who, why); if (log != null) log.logAlways(Log.WARN, "KoD from NTP server " + serverName + " (" + who + ") " + why); return null; } double localClockOffset = ((msg.receiveTimestamp - msg.originateTimestamp) + (msg.transmitTimestamp - destinationTimestamp)) / 2; long[] rv = new long[2]; rv[0] = (long)(System.currentTimeMillis() + localClockOffset*1000); rv[1] = msg.stratum; if (log != null && log.shouldInfo()) { double roundTripDelay = (destinationTimestamp-msg.originateTimestamp) - (msg.receiveTimestamp-msg.transmitTimestamp); log.info("host: " + packet.getAddress().getHostAddress() + " rtt: " + roundTripDelay + " offset: " + localClockOffset + " seconds"); } return rv; } catch (IOException ioe) { if (log != null && log.shouldWarn()) log.warn("NTP failure from " + serverName, ioe); return null; } finally { if (socket != null) socket.close(); } } /** * Usage: NtpClient [-6] [servers...] * default pool.ntp.org */ public static void main(String[] args) throws IOException { boolean ipv6 = false; if (args.length > 0 && args[0].equals("-6")) { ipv6 = true; if (args.length == 1) args = new String[0]; else args = Arrays.copyOfRange(args, 1, args.length); } if (args.length <= 0) { args = new String[] { "pool.ntp.org" }; } System.out.println("Querying " + Arrays.toString(args)); Log log = new Log(NtpClient.class); try { long[] rv = currentTimeAndStratum(args, DEFAULT_TIMEOUT, ipv6, log); System.out.println("Current time: " + new java.util.Date(rv[0]) + " (stratum " + rv[1] + ") offset " + (rv[0] - System.currentTimeMillis()) + "ms"); } catch (IllegalArgumentException iae) { System.out.println("Failed: " + iae.getMessage()); } } /**** private static void printUsage() { System.out.println( "NtpClient - an NTP client for Java.\n" + "\n" + "This program connects to an NTP server and prints the current time to the console.\n" + "\n" + "\n" + "Usage: java NtpClient server[ server]*\n" + "\n" + "\n" + "This program is copyright (c) Adam Buckley 2004 and distributed under the terms\n" + "of the GNU General Public License. This program is distributed in the hope\n" + "that it will be useful, but WITHOUT ANY WARRANTY; without even the implied\n" + "warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + "General Public License available at http://www.gnu.org/licenses/gpl.html for\n" + "more details."); } ****/ }