8224159: JDWP IPv6 scope support
authoramenkov
Thu, 31 Oct 2019 14:23:06 -0700
changeset 58876 1a8d65e71a66
parent 58875 8c0e8cff877f
child 58877 aec7bf35d6f5
8224159: JDWP IPv6 scope support Reviewed-by: sspitsyn, cjplummer
make/lib/Lib-jdk.jdwp.agent.gmk
src/jdk.jdwp.agent/share/native/libdt_socket/socketTransport.c
test/jdk/com/sun/jdi/JdwpAttachTest.java
test/jdk/com/sun/jdi/JdwpListenTest.java
--- a/make/lib/Lib-jdk.jdwp.agent.gmk	Tue Oct 29 15:08:19 2019 +0100
+++ b/make/lib/Lib-jdk.jdwp.agent.gmk	Thu Oct 31 14:23:06 2019 -0700
@@ -38,7 +38,7 @@
         $(call SET_SHARED_LIBRARY_ORIGIN), \
     LIBS_linux := -lpthread, \
     LIBS_solaris := -lnsl -lsocket, \
-    LIBS_windows := $(JDKLIB_LIBS) ws2_32.lib, \
+    LIBS_windows := $(JDKLIB_LIBS) ws2_32.lib iphlpapi.lib, \
 ))
 
 $(BUILD_LIBDT_SOCKET): $(call FindLib, java.base, java)
--- a/src/jdk.jdwp.agent/share/native/libdt_socket/socketTransport.c	Tue Oct 29 15:08:19 2019 +0100
+++ b/src/jdk.jdwp.agent/share/native/libdt_socket/socketTransport.c	Thu Oct 31 14:23:06 2019 -0700
@@ -35,9 +35,11 @@
 #ifdef _WIN32
  #include <winsock2.h>
  #include <ws2tcpip.h>
+ #include <iphlpapi.h>
 #else
  #include <arpa/inet.h>
  #include <sys/socket.h>
+ #include <net/if.h>
 #endif
 
 /*
@@ -267,16 +269,101 @@
 }
 
 /*
+ * Parses scope id.
+ * Scope id is ulong on Windows, uint32 on unix, so returns long which can be cast to uint32.
+ * On error sets last error and returns -1.
+ */
+static long parseScopeId(const char *str) {
+    // try to handle scope as interface name
+    unsigned long scopeId = if_nametoindex(str);
+    if (scopeId == 0) {
+        // try to parse integer value
+        char *end;
+        scopeId = strtoul(str, &end, 10);
+        if (*end != '\0') {
+            setLastError(JDWPTRANSPORT_ERROR_ILLEGAL_ARGUMENT, "failed to parse scope");
+            return -1;
+        }
+    }
+    // ensure parsed value is in uint32 range
+    if (scopeId > 0xFFFFFFFF) {
+        setLastError(JDWPTRANSPORT_ERROR_ILLEGAL_ARGUMENT, "scope is out of range");
+        return -1;
+    }
+    return (long)scopeId;
+}
+
+/*
+ * Wrapper for dbgsysGetAddrInfo (getaddrinfo).
+ * Handles enclosing square brackets and scopes.
+ */
+static jdwpTransportError
+getAddrInfo(const char *hostname, size_t hostnameLen,
+            const char *service,
+            const struct addrinfo *hints,
+            struct addrinfo **result)
+{
+    int err = 0;
+    char *buffer = NULL;
+    long scopeId = 0;
+
+    if (hostname != NULL) {
+        char *scope = NULL;
+        // skip surrounding
+        if (hostnameLen > 2 && hostname[0] == '[' && hostname[hostnameLen - 1] == ']') {
+            hostname++;
+            hostnameLen -= 2;
+        }
+        buffer = (*callback->alloc)((int)hostnameLen + 1);
+        if (buffer == NULL) {
+            RETURN_ERROR(JDWPTRANSPORT_ERROR_OUT_OF_MEMORY, "out of memory");
+        }
+        memcpy(buffer, hostname, hostnameLen);
+        buffer[hostnameLen] = '\0';
+
+        scope = strchr(buffer, '%');
+        if (scope != NULL) {
+            // drop scope from the address
+            *scope = '\0';
+            // and parse the value
+            scopeId = parseScopeId(scope + 1);
+            if (scopeId < 0) {
+                (*callback->free)(buffer);
+                return JDWPTRANSPORT_ERROR_ILLEGAL_ARGUMENT;
+            }
+        }
+    }
+
+    err = dbgsysGetAddrInfo(buffer, service, hints, result);
+
+    if (buffer != NULL) {
+        (*callback->free)(buffer);
+    }
+    if (err != 0) {
+        setLastError(err, "getaddrinfo: failed to parse address");
+        return JDWPTRANSPORT_ERROR_IO_ERROR;
+    }
+
+    if (scopeId > 0) {
+        if ((*result)->ai_family != AF_INET6) {
+            RETURN_ERROR(JDWPTRANSPORT_ERROR_ILLEGAL_ARGUMENT, "IPv4 address cannot contain scope");
+        }
+
+        ((struct sockaddr_in6 *)((*result)->ai_addr))->sin6_scope_id = (uint32_t)scopeId;
+    }
+
+    return JDWPTRANSPORT_ERROR_NONE;
+}
+
+/*
  * Result must be released with dbgsysFreeAddrInfo.
  */
 static jdwpTransportError
 parseAddress(const char *address, struct addrinfo **result) {
     const char *colon;
-    size_t hostLen;
-    char *host = NULL;
+    size_t hostnameLen;
     const char *port;
     struct addrinfo hints;
-    int res;
 
     *result = NULL;
 
@@ -295,39 +382,21 @@
     hints.ai_protocol = IPPROTO_TCP;
     hints.ai_flags = AI_NUMERICSERV;    // port must be a number
 
-    hostLen = (colon == NULL ? 0 : colon - address);
-    if (hostLen == 0) {
+    hostnameLen = (colon == NULL ? 0 : colon - address);
+    if (hostnameLen == 0) {
         /* no hostname - use localhost address (pass NULL to getaddrinfo) */
-    } else  if (*address == '*' && hostLen == 1) {
+        address = NULL;
+    } else  if (*address == '*' && hostnameLen == 1) {
         /* *:port - listen on all interfaces
          * use IPv6 socket (to accept IPv6 and mapped IPv4),
          * pass hostname == NULL to getaddrinfo.
          */
         hints.ai_family = allowOnlyIPv4 ? AF_INET : AF_INET6;
         hints.ai_flags |= AI_PASSIVE | (allowOnlyIPv4 ? 0 : AI_V4MAPPED | AI_ALL);
-    } else {
-        if (address[0] == '[' && colon[-1] == ']') {
-            address++;
-            hostLen -= 2;
-        }
-        host = (*callback->alloc)((int)hostLen + 1);
-        if (host == NULL) {
-            RETURN_ERROR(JDWPTRANSPORT_ERROR_OUT_OF_MEMORY, "out of memory");
-        }
-        strncpy(host, address, hostLen);
-        host[hostLen] = '\0';
+        address = NULL;
     }
 
-    res = dbgsysGetAddrInfo(host, port, &hints, result);
-    if (host != NULL) {
-        (*callback->free)(host);
-    }
-    if (res != 0) {
-        setLastError(res, "getaddrinfo: unknown host");
-        return JDWPTRANSPORT_ERROR_IO_ERROR;
-    }
-
-    return JDWPTRANSPORT_ERROR_NONE;
+    return getAddrInfo(address, hostnameLen, port, &hints, result);
 }
 
 /*
@@ -352,7 +421,7 @@
 parseAllowedAddr(const char *buffer, struct in6_addr *result, int *isIPv4) {
     struct addrinfo hints;
     struct addrinfo *addrInfo = NULL;
-    int err;
+    jdwpTransportError err;
 
     /*
      * To parse both IPv4 and IPv6 need to specify AF_UNSPEC family
@@ -364,11 +433,10 @@
     hints.ai_protocol = IPPROTO_TCP;
     hints.ai_flags = AI_NUMERICHOST;        // only numeric addresses, no resolution
 
-    err = dbgsysGetAddrInfo(buffer, NULL, &hints, &addrInfo);
+    err = getAddrInfo(buffer, strlen(buffer), NULL, &hints, &addrInfo);
 
-    if (err != 0) {
-        setLastError(err, "getaddrinfo: failed to parse address");
-        return JDWPTRANSPORT_ERROR_IO_ERROR;
+    if (err != JDWPTRANSPORT_ERROR_NONE) {
+        return err;
     }
 
     if (addrInfo->ai_family == AF_INET6) {
@@ -844,6 +912,7 @@
     }
 
     err = dbgsysConnect(socketFD, ai->ai_addr, (socklen_t)ai->ai_addrlen);
+
     if (err == DBG_EINPROGRESS && timeout > 0) {
         err = dbgsysFinishConnect(socketFD, (long)timeout);
 
--- a/test/jdk/com/sun/jdi/JdwpAttachTest.java	Tue Oct 29 15:08:19 2019 +0100
+++ b/test/jdk/com/sun/jdi/JdwpAttachTest.java	Thu Oct 31 14:23:06 2019 -0700
@@ -32,6 +32,7 @@
 import java.net.InetAddress;
 import java.net.NetworkInterface;
 import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.Iterator;
@@ -41,7 +42,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 
 /*
  * @test
@@ -54,24 +54,36 @@
  */
 public class JdwpAttachTest {
 
+    private static final boolean IsWindows = System.getProperty("os.name").toLowerCase().contains("windows");
+
+    // Set to true to perform testing of attach from wrong address (expected to fail).
+    // It's off by default as it caused significant test time increase\
+    // (tests <number_of_addresses> * <number_of_addresses> cases, each case fails by timeout).
+    private static boolean testFailedAttach = false;
+
     public static void main(String[] args) throws Exception {
         List<InetAddress> addresses = getAddresses();
 
         boolean ipv4EnclosedTested = false;
         boolean ipv6EnclosedTested = false;
         for (InetAddress addr: addresses) {
-            // also test that addresses enclosed in square brackets are supported
-            attachTest(addr.getHostAddress(), addr.getHostAddress());
+            if (testFailedAttach) {
+                for (InetAddress connectAddr : addresses) {
+                    attachTest(addr.getHostAddress(), connectAddr.getHostAddress(), addr.equals(connectAddr));
+                }
+            } else {
+                attachTest(addr.getHostAddress(), addr.getHostAddress(), true);
+            }
             // listening on "*" should accept connections from all addresses
-            attachTest("*", addr.getHostAddress());
+            attachTest("*", addr.getHostAddress(), true);
 
-            // test that addresses enclosed in square brackets are supported.
+            // also test that addresses enclosed in square brackets are supported.
             if (addr instanceof Inet4Address && !ipv4EnclosedTested) {
-                attachTest("[" + addr.getHostAddress() + "]", "[" + addr.getHostAddress() + "]");
+                attachTest("[" + addr.getHostAddress() + "]", "[" + addr.getHostAddress() + "]", true);
                 ipv4EnclosedTested = true;
             }
             if (addr instanceof Inet6Address && !ipv6EnclosedTested) {
-                attachTest("[" + addr.getHostAddress() + "]", "[" + addr.getHostAddress() + "]");
+                attachTest("[" + addr.getHostAddress() + "]", "[" + addr.getHostAddress() + "]", true);
                 ipv6EnclosedTested = true;
             }
         }
@@ -80,13 +92,14 @@
         // we should be able to attach to both IPv4 and IPv6 addresses (127.0.0.1 & ::1)
         InetAddress localAddresses[] = InetAddress.getAllByName("localhost");
         for (int i = 0; i < localAddresses.length; i++) {
-            attachTest(localAddresses[i].getHostAddress(), "");
+            attachTest(localAddresses[i].getHostAddress(), "", true);
         }
     }
 
-    private static void attachTest(String listenAddress, String connectAddresses)
+    private static void attachTest(String listenAddress, String connectAddress, boolean expectedResult)
             throws Exception {
-        log("Starting listening at " + listenAddress);
+        log("\nTest: listen on '" + listenAddress + "', attach to '" + connectAddress + "'");
+        log("  Starting listening at " + listenAddress);
         ListeningConnector connector = getListenConnector();
         Map<String, Connector.Argument> args = connector.defaultArguments();
         setConnectorArg(args, "localAddress", listenAddress);
@@ -100,31 +113,48 @@
             throw new RuntimeException("values from connector.startListening (" + actualPort
                     + " is not equal to values from arguments (" + port + ")");
         }
-        log("Listening port: " + port);
+        log("  Listening port: " + port);
 
-        log("Attaching from " + connectAddresses);
+        log("  Attaching from " + connectAddress);
         try {
             ExecutorService executor = Executors.newSingleThreadExecutor();
             executor.submit((Callable<Exception>)() -> {
                 VirtualMachine vm = connector.accept(args);
-                log("ACCEPTED.");
                 vm.dispose();
                 return null;
             });
             executor.shutdown();
 
-            LingeredApp debuggee = LingeredApp.startApp(
-                    Arrays.asList("-agentlib:jdwp=transport=dt_socket"
-                                +",address=" + connectAddresses + ":" + port
-                                + ",server=n,suspend=n"));
-            debuggee.stopApp();
-
-            executor.awaitTermination(20, TimeUnit.SECONDS);
+            try {
+                LingeredApp debuggee = LingeredApp.startApp(
+                        Arrays.asList("-agentlib:jdwp=transport=dt_socket"
+                                + ",address=" + connectAddress + ":" + port
+                                + ",server=n,suspend=n"
+                                // if failure is expected set small timeout (default is 20 sec)
+                                + (!expectedResult ? ",timeout=1000" : "")));
+                debuggee.stopApp();
+                if (expectedResult) {
+                    log("OK: attached as expected");
+                } else {
+                    throw new RuntimeException("ERROR: LingeredApp.startApp was able to attach");
+                }
+            } catch (Exception ex) {
+                if (expectedResult) {
+                    throw new RuntimeException("ERROR: LingeredApp.startApp was able to attach");
+                } else {
+                    log("OK: failed to attach as expected");
+                }
+            }
         } finally {
             connector.stopListening(args);
         }
     }
 
+    private static void addAddr(List<InetAddress> list, InetAddress addr) {
+        log(" - (" + addr.getClass().getSimpleName() + ") " + addr.getHostAddress());
+        list.add(addr);
+    }
+
     private static List<InetAddress> getAddresses() {
         List<InetAddress> result = new LinkedList<>();
         try {
@@ -136,17 +166,37 @@
                         Enumeration<InetAddress> addresses = iface.getInetAddresses();
                         while (addresses.hasMoreElements()) {
                             InetAddress addr = addresses.nextElement();
-                            // Java reports link local addresses with named scope,
-                            // but Windows sockets routines support only numeric scope id.
-                            // skip such addresses.
+                            // Java reports link local addresses with symbolic scope,
+                            // but on Windows java.net.NetworkInterface generates its own scope names
+                            // which are incompatible with native Windows routines.
+                            // So on Windows test only addresses with numeric scope.
+                            // On other platforms test both symbolic and numeric scopes.
                             if (addr instanceof Inet6Address) {
                                 Inet6Address addr6 = (Inet6Address)addr;
-                                if (addr6.getScopedInterface() != null) {
-                                    continue;
+                                NetworkInterface scopeIface = addr6.getScopedInterface();
+                                if (scopeIface != null && scopeIface.getName() != null) {
+                                    // On some test machines VPN creates link local addresses
+                                    // which we cannot connect to.
+                                    // Skip them.
+                                    if (scopeIface.isPointToPoint()) {
+                                        continue;
+                                    }
+
+                                    try {
+                                        // the same address with numeric scope
+                                        addAddr(result, Inet6Address.getByAddress(null, addr6.getAddress(), addr6.getScopeId()));
+                                    } catch (UnknownHostException e) {
+                                        // cannot happen!
+                                        throw new RuntimeException("Unexpected", e);
+                                    }
+
+                                    if (IsWindows) {
+                                        // don't add addresses with symbolic scope
+                                        continue;
+                                    }
                                 }
                             }
-                            log(" - (" + addr.getClass().getSimpleName() + ") " + addr.getHostAddress());
-                            result.add(addr);
+                            addAddr(result, addr);
                         }
                     }
                 } catch (SocketException e) {
@@ -184,8 +234,11 @@
         arg.setValue(value);
     }
 
+    private static long startTime = System.currentTimeMillis();
+
     private static void log(Object o) {
-        System.out.println(String.valueOf(o));
+        long time = System.currentTimeMillis() - startTime;
+        System.out.println(String.format("[%7.3f] %s", (time / 1000f), String.valueOf(o)));
     }
 
 }
--- a/test/jdk/com/sun/jdi/JdwpListenTest.java	Tue Oct 29 15:08:19 2019 +0100
+++ b/test/jdk/com/sun/jdi/JdwpListenTest.java	Thu Oct 31 14:23:06 2019 -0700
@@ -34,6 +34,7 @@
 import java.net.InetAddress;
 import java.net.NetworkInterface;
 import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -51,6 +52,8 @@
  */
 public class JdwpListenTest {
 
+    private static final boolean IsWindows = System.getProperty("os.name").toLowerCase().contains("windows");
+
     public static void main(String[] args) throws Exception {
         List<InetAddress> addresses = getAddresses();
 
@@ -60,6 +63,8 @@
             for (InetAddress attach: addresses) {
                 // can connect only from the same address
                 // IPv6 cannot connect to IPv4 (::1 to 127.0.0.1) and vice versa.
+                // Note: for IPv6 addresses equals() does not compare scopes
+                // (so addresses with symbolic and numeric scopes are equals).
                 listenTest(listen.getHostAddress(), attach.getHostAddress(), attach.equals(listen));
             }
             // test that addresses enclosed in square brackets are supported.
@@ -80,6 +85,8 @@
 
     private static void listenTest(String listenAddress, String connectAddress, boolean expectedResult)
             throws IOException {
+        log("\nTest: listen at " + listenAddress + ", attaching from " + connectAddress
+                + ", expected: " + (expectedResult ? "SUCCESS" : "FAILURE"));
         log("Starting listening debuggee at " + listenAddress);
         try (Debuggee debuggee = Debuggee.launcher("HelloWorld").setAddress(listenAddress + ":0").launch()) {
             log("Debuggee is listening on " + listenAddress + ":" + debuggee.getAddress());
@@ -98,6 +105,11 @@
         }
     }
 
+    private static void addAddr(List<InetAddress> list, InetAddress addr) {
+        log(" - (" + addr.getClass().getSimpleName() + ") " + addr.getHostAddress());
+        list.add(addr);
+    }
+
     private static List<InetAddress> getAddresses() {
         List<InetAddress> result = new LinkedList<>();
         try {
@@ -109,17 +121,37 @@
                         Enumeration<InetAddress> addresses = iface.getInetAddresses();
                         while (addresses.hasMoreElements()) {
                             InetAddress addr = addresses.nextElement();
-                            // Java reports link local addresses with named scope,
-                            // but Windows sockets routines support only numeric scope id.
-                            // skip such addresses.
+                            // Java reports link local addresses with symbolic scope,
+                            // but on Windows java.net.NetworkInterface generates its own scope names
+                            // which are incompatible with native Windows routines.
+                            // So on Windows test only addresses with numeric scope.
+                            // On other platforms test both symbolic and numeric scopes.
                             if (addr instanceof Inet6Address) {
                                 Inet6Address addr6 = (Inet6Address)addr;
-                                if (addr6.getScopedInterface() != null) {
-                                    continue;
+                                NetworkInterface scopeIface = addr6.getScopedInterface();
+                                if (scopeIface != null && scopeIface.getName() != null) {
+                                    // On some test machines VPN creates link local addresses
+                                    // which we cannot connect to.
+                                    // Skip them.
+                                    if (scopeIface.isPointToPoint()) {
+                                        continue;
+                                    }
+
+                                    try {
+                                        // the same address with numeric scope
+                                        addAddr(result, Inet6Address.getByAddress(null, addr6.getAddress(), addr6.getScopeId()));
+                                    } catch (UnknownHostException e) {
+                                        // cannot happen!
+                                        throw new RuntimeException("Unexpected", e);
+                                    }
+
+                                    if (IsWindows) {
+                                        // don't add addresses with symbolic scope
+                                        continue;
+                                    }
                                 }
                             }
-                            log(" - (" + addr.getClass().getSimpleName() + ") " + addr.getHostAddress());
-                            result.add(addr);
+                            addAddr(result, addr);
                         }
                     }
                 } catch (SocketException e) {