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