mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-14 08:55:19 +00:00
In the SQL with SSL tests, we need to find the interfaces that are up, are loopback devices, or have a loopback address. If we check if the device is up first, we can run into situations where the device is a virtual ethernet device that might have disappeared between us seeing the device, and checking if it is up. By first checking if the device is a loopback device or it has a loopback address, then we can avoid checking if the device is up except for loopback devices and therefore we can avoid the disappearing virtual ethernet device problem.
338 lines
11 KiB
Groovy
338 lines
11 KiB
Groovy
import org.elasticsearch.gradle.LoggedExec
|
|
import org.elasticsearch.gradle.info.BuildParams
|
|
|
|
// Tell the tests we're running with ssl enabled
|
|
integTest.runner {
|
|
systemProperty 'tests.ssl.enabled', 'true'
|
|
}
|
|
|
|
// needed to be consistent with ssl host checking
|
|
Object san = new SanEvaluator()
|
|
|
|
// location of generated keystores and certificates
|
|
File keystoreDir = new File(project.buildDir, 'keystore')
|
|
|
|
// Generate the node's keystore
|
|
File nodeKeystore = file("$keystoreDir/test-node.jks")
|
|
task createNodeKeyStore(type: LoggedExec) {
|
|
doFirst {
|
|
if (nodeKeystore.parentFile.exists() == false) {
|
|
nodeKeystore.parentFile.mkdirs()
|
|
}
|
|
if (nodeKeystore.exists()) {
|
|
delete nodeKeystore
|
|
}
|
|
}
|
|
executable = "${BuildParams.runtimeJavaHome}/bin/keytool"
|
|
standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8'))
|
|
args '-genkey',
|
|
'-alias', 'test-node',
|
|
'-keystore', nodeKeystore,
|
|
'-keyalg', 'RSA',
|
|
'-keysize', '2048',
|
|
'-validity', '712',
|
|
'-dname', 'CN=smoke-test-plugins-ssl',
|
|
'-keypass', 'keypass',
|
|
'-storepass', 'keypass',
|
|
'-ext', san
|
|
}
|
|
|
|
// Generate the client's keystore
|
|
File clientKeyStore = file("$keystoreDir/test-client.jks")
|
|
task createClientKeyStore(type: LoggedExec) {
|
|
doFirst {
|
|
if (clientKeyStore.parentFile.exists() == false) {
|
|
clientKeyStore.parentFile.mkdirs()
|
|
}
|
|
if (clientKeyStore.exists()) {
|
|
delete clientKeyStore
|
|
}
|
|
}
|
|
executable = "${BuildParams.runtimeJavaHome}/bin/keytool"
|
|
standardInput = new ByteArrayInputStream('FirstName LastName\nUnit\nOrganization\nCity\nState\nNL\nyes\n\n'.getBytes('UTF-8'))
|
|
args '-genkey',
|
|
'-alias', 'test-client',
|
|
'-keystore', clientKeyStore,
|
|
'-keyalg', 'RSA',
|
|
'-keysize', '2048',
|
|
'-validity', '712',
|
|
'-dname', 'CN=smoke-test-plugins-ssl',
|
|
'-keypass', 'keypass',
|
|
'-storepass', 'keypass',
|
|
'-ext', san
|
|
}
|
|
|
|
// Export the node's certificate
|
|
File nodeCertificate = file("$keystoreDir/test-node.cert")
|
|
task exportNodeCertificate(type: LoggedExec) {
|
|
dependsOn createNodeKeyStore
|
|
doFirst {
|
|
if (nodeCertificate.parentFile.exists() == false) {
|
|
nodeCertificate.parentFile.mkdirs()
|
|
}
|
|
if (nodeCertificate.exists()) {
|
|
delete nodeCertificate
|
|
}
|
|
}
|
|
executable = "${BuildParams.runtimeJavaHome}/bin/keytool"
|
|
args '-export',
|
|
'-alias', 'test-node',
|
|
'-keystore', nodeKeystore,
|
|
'-storepass', 'keypass',
|
|
'-file', nodeCertificate
|
|
}
|
|
|
|
// Import the node certificate in the client's keystore
|
|
task importNodeCertificateInClientKeyStore(type: LoggedExec) {
|
|
dependsOn createClientKeyStore, exportNodeCertificate
|
|
executable = "${BuildParams.runtimeJavaHome}/bin/keytool"
|
|
args '-import',
|
|
'-alias', 'test-node',
|
|
'-keystore', clientKeyStore,
|
|
'-storepass', 'keypass',
|
|
'-file', nodeCertificate,
|
|
'-noprompt'
|
|
}
|
|
|
|
// Export the client's certificate
|
|
File clientCertificate = file("$keystoreDir/test-client.cert")
|
|
task exportClientCertificate(type: LoggedExec) {
|
|
dependsOn createClientKeyStore
|
|
doFirst {
|
|
if (clientCertificate.parentFile.exists() == false) {
|
|
clientCertificate.parentFile.mkdirs()
|
|
}
|
|
if (clientCertificate.exists()) {
|
|
delete clientCertificate
|
|
}
|
|
}
|
|
executable = "${BuildParams.runtimeJavaHome}/bin/keytool"
|
|
args '-export',
|
|
'-alias', 'test-client',
|
|
'-keystore', clientKeyStore,
|
|
'-storepass', 'keypass',
|
|
'-file', clientCertificate
|
|
}
|
|
|
|
// Import the client certificate in the node's keystore
|
|
task importClientCertificateInNodeKeyStore(type: LoggedExec) {
|
|
dependsOn createNodeKeyStore, exportClientCertificate
|
|
executable = "${BuildParams.runtimeJavaHome}/bin/keytool"
|
|
args '-import',
|
|
'-alias', 'test-client',
|
|
'-keystore', nodeKeystore,
|
|
'-storepass', 'keypass',
|
|
'-file', clientCertificate,
|
|
'-noprompt'
|
|
}
|
|
|
|
forbiddenPatterns {
|
|
exclude '**/*.cert'
|
|
}
|
|
|
|
// Add keystores to test classpath: it expects it there
|
|
sourceSets.test.resources.srcDir(keystoreDir)
|
|
processTestResources.dependsOn(importNodeCertificateInClientKeyStore, importClientCertificateInNodeKeyStore)
|
|
|
|
integTest.runner {
|
|
dependsOn(importClientCertificateInNodeKeyStore)
|
|
onlyIf {
|
|
// Do not attempt to form a cluster in a FIPS JVM, as doing so with a JKS keystore will fail.
|
|
// TODO Revisit this when SQL CLI client can handle key/certificate instead of only Keystores.
|
|
// https://github.com/elastic/elasticsearch/issues/32306
|
|
BuildParams.inFipsJvm == false
|
|
}
|
|
}
|
|
|
|
testClusters.integTest {
|
|
// The setup that we actually want
|
|
setting 'xpack.license.self_generated.type', 'trial'
|
|
setting 'xpack.security.http.ssl.enabled', 'true'
|
|
setting 'xpack.security.transport.ssl.enabled', 'true'
|
|
|
|
// ceremony to set up ssl
|
|
setting 'xpack.security.transport.ssl.keystore.path', 'test-node.jks'
|
|
setting 'xpack.security.http.ssl.keystore.path', 'test-node.jks'
|
|
keystore 'xpack.security.transport.ssl.keystore.secure_password', 'keypass'
|
|
keystore 'xpack.security.http.ssl.keystore.secure_password', 'keypass'
|
|
|
|
|
|
// copy keystores into config/
|
|
extraConfigFile nodeKeystore.name, nodeKeystore
|
|
extraConfigFile clientKeyStore.name, clientKeyStore
|
|
}
|
|
|
|
|
|
/** A lazy evaluator to find the san to use for certificate generation. */
|
|
class SanEvaluator {
|
|
|
|
private static String san = null
|
|
|
|
String toString() {
|
|
synchronized (SanEvaluator.class) {
|
|
if (san == null) {
|
|
san = getSubjectAlternativeNameString()
|
|
}
|
|
}
|
|
return san
|
|
}
|
|
|
|
// Code stolen from NetworkUtils/InetAddresses/NetworkAddress to support SAN
|
|
/** Return all interfaces (and subinterfaces) on the system */
|
|
private static List<NetworkInterface> getInterfaces() throws SocketException {
|
|
List<NetworkInterface> all = new ArrayList<>();
|
|
addAllInterfaces(all, Collections.list(NetworkInterface.getNetworkInterfaces()));
|
|
Collections.sort(all, new Comparator<NetworkInterface>() {
|
|
@Override
|
|
public int compare(NetworkInterface left, NetworkInterface right) {
|
|
return Integer.compare(left.getIndex(), right.getIndex());
|
|
}
|
|
});
|
|
return all;
|
|
}
|
|
|
|
/** Helper for getInterfaces, recursively adds subinterfaces to {@code target} */
|
|
private static void addAllInterfaces(List<NetworkInterface> target, List<NetworkInterface> level) {
|
|
if (!level.isEmpty()) {
|
|
target.addAll(level);
|
|
for (NetworkInterface intf : level) {
|
|
addAllInterfaces(target, Collections.list(intf.getSubInterfaces()));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static String getSubjectAlternativeNameString() {
|
|
List<InetAddress> list = new ArrayList<>();
|
|
for (NetworkInterface intf : getInterfaces()) {
|
|
for (final InetAddress address : Collections.list(intf.getInetAddresses())) {
|
|
/*
|
|
* Some OS (e.g., BSD) assign a link-local address to the loopback interface. While technically not a loopback interface, some of
|
|
* these OS treat them as one (e.g., localhost on macOS), so we must too. Otherwise, things just won't work out of the box. So we
|
|
* include all addresses from loopback interfaces.
|
|
*
|
|
* By checking if the interface is a loopback interface or the address is a loopback address first, we avoid having to check if the
|
|
* interface is up unless necessary. This means we can avoid checking if the interface is up for virtual ethernet devices which have
|
|
* a tendency to disappear outside of our control (e.g., due to Docker).
|
|
*/
|
|
if ((intf.isLoopback() || address.isLoopbackAddress()) && isUp(intf, address)) {
|
|
list.add(address)
|
|
}
|
|
}
|
|
}
|
|
if (list.isEmpty()) {
|
|
throw new IllegalArgumentException("no up-and-running loopback addresses found, got " + getInterfaces());
|
|
}
|
|
|
|
StringBuilder builder = new StringBuilder("san=");
|
|
for (int i = 0; i < list.size(); i++) {
|
|
InetAddress address = list.get(i);
|
|
String hostAddress;
|
|
if (address instanceof Inet6Address) {
|
|
hostAddress = compressedIPV6Address((Inet6Address) address);
|
|
} else {
|
|
hostAddress = address.getHostAddress();
|
|
}
|
|
builder.append("ip:").append(hostAddress);
|
|
String hostname = address.getHostName();
|
|
if (hostname.equals(address.getHostAddress()) == false) {
|
|
builder.append(",dns:").append(hostname);
|
|
}
|
|
|
|
if (i != (list.size() - 1)) {
|
|
builder.append(",");
|
|
}
|
|
}
|
|
|
|
return builder.toString();
|
|
}
|
|
|
|
private static boolean isUp(final NetworkInterface intf, final InetAddress address) throws IOException {
|
|
try {
|
|
return intf.isUp();
|
|
} catch (final SocketException e) {
|
|
/*
|
|
* In Elasticsearch production code (NetworkUtils) we suppress this if the device is a virtual ethernet device. That should not happen
|
|
* here since the interface must be a loopback device or the address a loopback address to get here to begin with.
|
|
*/
|
|
assert intf.isLoopback() || address.isLoopbackAddress()
|
|
throw new IOException("failed to check if interface [" + intf.getName() + "] is up", e)
|
|
}
|
|
}
|
|
|
|
private static String compressedIPV6Address(Inet6Address inet6Address) {
|
|
byte[] bytes = inet6Address.getAddress();
|
|
int[] hextets = new int[8];
|
|
for (int i = 0; i < hextets.length; i++) {
|
|
hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255;
|
|
}
|
|
compressLongestRunOfZeroes(hextets);
|
|
return hextetsToIPv6String(hextets);
|
|
}
|
|
|
|
/**
|
|
* Identify and mark the longest run of zeroes in an IPv6 address.
|
|
*
|
|
* <p>Only runs of two or more hextets are considered. In case of a tie, the
|
|
* leftmost run wins. If a qualifying run is found, its hextets are replaced
|
|
* by the sentinel value -1.
|
|
*
|
|
* @param hextets {@code int[]} mutable array of eight 16-bit hextets
|
|
*/
|
|
private static void compressLongestRunOfZeroes(int[] hextets) {
|
|
int bestRunStart = -1;
|
|
int bestRunLength = -1;
|
|
int runStart = -1;
|
|
for (int i = 0; i < hextets.length + 1; i++) {
|
|
if (i < hextets.length && hextets[i] == 0) {
|
|
if (runStart < 0) {
|
|
runStart = i;
|
|
}
|
|
} else if (runStart >= 0) {
|
|
int runLength = i - runStart;
|
|
if (runLength > bestRunLength) {
|
|
bestRunStart = runStart;
|
|
bestRunLength = runLength;
|
|
}
|
|
runStart = -1;
|
|
}
|
|
}
|
|
if (bestRunLength >= 2) {
|
|
Arrays.fill(hextets, bestRunStart, bestRunStart + bestRunLength, -1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a list of hextets into a human-readable IPv6 address.
|
|
*
|
|
* <p>In order for "::" compression to work, the input should contain negative
|
|
* sentinel values in place of the elided zeroes.
|
|
*
|
|
* @param hextets {@code int[]} array of eight 16-bit hextets, or -1s
|
|
*/
|
|
private static String hextetsToIPv6String(int[] hextets) {
|
|
/*
|
|
* While scanning the array, handle these state transitions:
|
|
* start->num => "num" start->gap => "::"
|
|
* num->num => ":num" num->gap => "::"
|
|
* gap->num => "num" gap->gap => ""
|
|
*/
|
|
StringBuilder buf = new StringBuilder(39);
|
|
boolean lastWasNumber = false;
|
|
for (int i = 0; i < hextets.length; i++) {
|
|
boolean thisIsNumber = hextets[i] >= 0;
|
|
if (thisIsNumber) {
|
|
if (lastWasNumber) {
|
|
buf.append(':');
|
|
}
|
|
buf.append(Integer.toHexString(hextets[i]));
|
|
} else {
|
|
if (i == 0 || lastWasNumber) {
|
|
buf.append("::");
|
|
}
|
|
}
|
|
lastWasNumber = thisIsNumber;
|
|
}
|
|
return buf.toString();
|
|
}
|
|
}
|