8224159: JDWP IPv6 scope support

Reviewed-by: sspitsyn, cjplummer
This commit is contained in:
Alex Menkov 2019-10-31 14:23:06 -07:00
parent a3ee39cb81
commit 3f93ec68ee
4 changed files with 222 additions and 68 deletions

View File

@ -38,7 +38,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBDT_SOCKET, \
$(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)

View File

@ -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
/*
@ -266,17 +268,102 @@ static unsigned short getPort(struct sockaddr *sa)
: (((struct sockaddr_in6*)sa)->sin6_port));
}
/*
* 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 @@ parseAddress(const char *address, struct addrinfo **result) {
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 @@ static jdwpTransportError
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 @@ parseAllowedAddr(const char *buffer, struct in6_addr *result, int *isIPv4) {
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 @@ static jdwpTransportError connectToAddr(struct addrinfo *ai, jlong timeout, int
}
err = dbgsysConnect(socketFD, ai->ai_addr, (socklen_t)ai->ai_addrlen);
if (err == DBG_EINPROGRESS && timeout > 0) {
err = dbgsysFinishConnect(socketFD, (long)timeout);

View File

@ -32,6 +32,7 @@ import java.net.Inet6Address;
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.Map;
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 @@ import java.util.concurrent.TimeUnit;
*/
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 @@ public class JdwpAttachTest {
// 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 @@ public class JdwpAttachTest {
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 @@ public class JdwpAttachTest {
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 @@ public class JdwpAttachTest {
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)));
}
}

View File

@ -34,6 +34,7 @@ import java.net.Inet6Address;
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 @@ import java.util.Map;
*/
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 @@ public class JdwpListenTest {
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 @@ public class JdwpListenTest {
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 @@ public class JdwpListenTest {
}
}
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 @@ public class JdwpListenTest {
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) {