From fbbfd8acf07374e4e4e58fc483b032c44327c004 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Wed, 21 Dec 2016 17:48:47 +0000 Subject: [PATCH] NTP: - Verify source address and port - Add to command line - main() test improvements - Add KoD support (ticket #1896) - Add initial IPv6 support (ticket #1897) - Make some methods private - Add year 2036 warning --- .../java/src/net/i2p/router/CommandLine.java | 1 + .../src/net/i2p/router/time/NtpClient.java | 103 +++++++++++++++--- .../src/net/i2p/router/time/NtpMessage.java | 27 +++-- .../i2p/router/time/RouterTimestamper.java | 3 +- 4 files changed, 111 insertions(+), 23 deletions(-) diff --git a/router/java/src/net/i2p/router/CommandLine.java b/router/java/src/net/i2p/router/CommandLine.java index c3af8d7381..60781d1548 100644 --- a/router/java/src/net/i2p/router/CommandLine.java +++ b/router/java/src/net/i2p/router/CommandLine.java @@ -21,6 +21,7 @@ public class CommandLine extends net.i2p.util.CommandLine { "net.i2p.router.RouterVersion", "net.i2p.router.peermanager.ProfileOrganizer", "net.i2p.router.tasks.CryptoChecker", + "net.i2p.router.time.NtpClient", "net.i2p.router.transport.GeoIPv6", "net.i2p.router.transport.udp.MTU", "net.i2p.router.transport.UPnP" diff --git a/router/java/src/net/i2p/router/time/NtpClient.java b/router/java/src/net/i2p/router/time/NtpClient.java index e7f9b2a635..6b649ec7ba 100644 --- a/router/java/src/net/i2p/router/time/NtpClient.java +++ b/router/java/src/net/i2p/router/time/NtpClient.java @@ -33,8 +33,13 @@ 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; @@ -50,18 +55,24 @@ import net.i2p.util.Log; * 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 */ -class NtpClient { +public class NtpClient { /** difference between the unix epoch and jan 1 1900 (NTP uses that) */ - public final static double SECONDS_1900_TO_EPOCH = 2208988800.0; + 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 @@ -95,7 +106,7 @@ class NtpClient { * @throws IllegalArgumentException if none of the servers are reachable * @since 0.7.12 */ - public static long[] currentTimeAndStratum(String serverNames[], int perServerTimeout, Log log) { + 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); @@ -103,7 +114,7 @@ class NtpClient { names.add(serverNames[i]); Collections.shuffle(names); for (int i = 0; i < names.size(); i++) { - long[] rv = currentTimeAndStratum(names.get(i), perServerTimeout, log); + long[] rv = currentTimeAndStratum(names.get(i), perServerTimeout, preferIPv6, log); if (rv != null && rv[0] > 0) return rv; } @@ -131,16 +142,39 @@ class NtpClient { * @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, Log log) { + private static long[] currentTimeAndStratum(String serverName, int timeout, boolean preferIPv6, Log log) { DatagramSocket socket = null; try { // Send request - socket = new DatagramSocket(); - InetAddress address = InetAddress.getByName(serverName); + 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, @@ -151,7 +185,7 @@ class NtpClient { // save for check System.arraycopy(packet.getData(), OFF_TXTIME, txtime, 0, 8); if (log != null && log.shouldDebug()) - log.debug("Sent:\n" + HexDump.dump(buf)); + log.debug("Sent to " + serverName + " (" + who + ")\n" + HexDump.dump(buf)); // Get response packet = new DatagramPacket(buf, buf.length); @@ -170,18 +204,29 @@ class NtpClient { // 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: " + packet.getAddress().getHostAddress() + + 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 - if ((msg.stratum < 1) || (msg.stratum > 15)) { + // KoD (stratum 0) processing is below, after origin time check + if (msg.stratum > 15) { if (log != null && log.shouldWarn()) - log.warn("Response from NTP server of unacceptable stratum " + msg.stratum + ", failing."); + 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) + @@ -189,6 +234,17 @@ class NtpClient { 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; @@ -213,15 +269,32 @@ class NtpClient { } } + /** + * Usage: NtpClient [-6] [servers...] + * default pool.ntp.org + */ public static void main(String[] args) throws IOException { - // Process command-line args - if(args.length <= 0) { + 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); - long[] rv = currentTimeAndStratum(args, DEFAULT_TIMEOUT, log); - System.out.println("Current time: " + new java.util.Date(rv[0]) + " (stratum " + rv[1] + ')'); + 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()); + } } /**** diff --git a/router/java/src/net/i2p/router/time/NtpMessage.java b/router/java/src/net/i2p/router/time/NtpMessage.java index 4f471cce95..bd5e55459e 100644 --- a/router/java/src/net/i2p/router/time/NtpMessage.java +++ b/router/java/src/net/i2p/router/time/NtpMessage.java @@ -72,6 +72,8 @@ import net.i2p.util.RandomSource; * inspired by http://www.pps.jussieu.fr/~jch/enseignement/reseaux/ * NTPMessage.java which is copyright (c) 2003 by Juliusz Chroboczek * + * TODO NOT 2036-compliant, see RFC 4330 + * * @author Adam Buckley * @since 0.9.1 moved from net.i2p.time */ @@ -206,7 +208,7 @@ class NtpMessage { * GPS Global Positioning Service * GOES Geostationary Orbit Environment Satellite */ - public byte[] referenceIdentifier = {0, 0, 0, 0}; + public final byte[] referenceIdentifier = {0, 0, 0, 0}; /** @@ -347,7 +349,7 @@ class NtpMessage { "Precision: " + precision + " (" + precisionStr + " seconds)\n" + "Root delay: " + new DecimalFormat("0.00").format(rootDelay*1000) + " ms\n" + "Root dispersion: " + new DecimalFormat("0.00").format(rootDispersion*1000) + " ms\n" + - "Reference identifier: " + referenceIdentifierToString(referenceIdentifier, stratum, version) + "\n" + + "Reference identifier: " + referenceIdentifierToString() + "\n" + "Reference timestamp: " + timestampToString(referenceTimestamp) + "\n" + "Originate timestamp: " + timestampToString(originateTimestamp) + "\n" + "Receive timestamp: " + timestampToString(receiveTimestamp) + "\n" + @@ -360,7 +362,7 @@ class NtpMessage { * Converts an unsigned byte to a short. By default, Java assumes that * a byte is signed. */ - public static short unsignedByteToShort(byte b) { + private static short unsignedByteToShort(byte b) { if((b & 0x80)==0x80) return (short) (128 + (b & 0x7f)); else @@ -378,7 +380,7 @@ class NtpMessage { * @param pointer the offset * @return the time since 1900 (NOT Java time) */ - public static double decodeTimestamp(byte[] array, int pointer) { + private static double decodeTimestamp(byte[] array, int pointer) { double r = 0.0; for(int i=0; i<8; i++) { @@ -431,7 +433,7 @@ class NtpMessage { * Returns a timestamp (number of seconds since 00:00 1-Jan-1900) as a * formatted date/time string. */ - public static String timestampToString(double timestamp) { + private static String timestampToString(double timestamp) { if(timestamp==0) return "0"; // timestamp is relative to 1900, utc is used by Java and is relative @@ -451,13 +453,20 @@ class NtpMessage { return date + fractionSting; } - + /** + * @since 0.9.29 + * @return non-null, "" if unset + */ + public String referenceIdentifierToString() { + return referenceIdentifierToString(referenceIdentifier, stratum, version); + } /** * Returns a string representation of a reference identifier according * to the rules set out in RFC 2030. + * @return non-null, "" if unset */ - public static String referenceIdentifierToString(byte[] ref, short stratum, byte version) { + private static String referenceIdentifierToString(byte[] ref, short stratum, byte version) { // From the RFC 2030: // In the case of NTP Version 3 or Version 4 stratum-0 (unspecified) // or stratum-1 (primary) servers, this is a four-character ASCII @@ -484,6 +493,10 @@ class NtpMessage { // In NTP Version 4 secondary servers, this is the low order 32 bits // of the latest transmit timestamp of the reference source. else if(version==4) { + // Unimplemented RFC 4330: + // For IPv6 and OSI secondary servers, the value is the first 32 bits of + // the MD5 hash of the IPv6 or NSAP address of the synchronization + // source. return "" + ((unsignedByteToShort(ref[0]) / 256.0) + (unsignedByteToShort(ref[1]) / 65536.0) + (unsignedByteToShort(ref[2]) / 16777216.0) + diff --git a/router/java/src/net/i2p/router/time/RouterTimestamper.java b/router/java/src/net/i2p/router/time/RouterTimestamper.java index 83b22e56ae..9750754116 100644 --- a/router/java/src/net/i2p/router/time/RouterTimestamper.java +++ b/router/java/src/net/i2p/router/time/RouterTimestamper.java @@ -273,7 +273,8 @@ public class RouterTimestamper extends Timestamper { // // this delays startup when net is disconnected or the timeserver list is bad, don't make it too long // try { Thread.sleep(2*1000); } catch (InterruptedException ie) {} //} - long[] timeAndStratum = NtpClient.currentTimeAndStratum(serverList, perServerTimeout, _log); + // IPv6 arg TODO + long[] timeAndStratum = NtpClient.currentTimeAndStratum(serverList, perServerTimeout, false, _log); now = timeAndStratum[0]; stratum = (int) timeAndStratum[1]; long delta = now - _context.clock().now(); -- GitLab