8369950: TLS connection to IPv6 address fails with BCJSSE due to IllegalArgumentException

Co-authored-by: Mikhail Yankelevich <myankelevich@openjdk.org>
Reviewed-by: djelinski, vyazici, dfuchs, myankelevich
This commit is contained in:
Sergey Chernyshev 2025-12-08 09:06:21 +00:00 committed by Volkan Yazici
parent 5f083abafc
commit 7da91533aa
2 changed files with 388 additions and 2 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -44,6 +44,7 @@ import java.util.Objects;
import java.util.StringTokenizer;
import javax.net.ssl.*;
import sun.net.util.IPAddressUtil;
import sun.net.www.http.HttpClient;
import sun.net.www.protocol.http.AuthCacheImpl;
import sun.net.www.protocol.http.HttpURLConnection;
@ -471,7 +472,13 @@ final class HttpsClient extends HttpClient
SSLParameters parameters = s.getSSLParameters();
parameters.setEndpointIdentificationAlgorithm("HTTPS");
// host has been set previously for SSLSocketImpl
if (!(s instanceof SSLSocketImpl)) {
if (!(s instanceof SSLSocketImpl) &&
!IPAddressUtil.isIPv4LiteralAddress(host) &&
!(host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']' &&
IPAddressUtil.isIPv6LiteralAddress(host.substring(1, host.length() - 1))
)) {
// Fully qualified DNS hostname of the server, as per section 3, RFC 6066
// Literal IPv4 and IPv6 addresses are not permitted in "HostName".
parameters.setServerNames(List.of(new SNIHostName(host)));
}
s.setSSLParameters(parameters);

View File

@ -0,0 +1,379 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8369950
* @summary Test that the HttpsURLConnection does not set IP address literals for
* SNI hostname during TLS handshake
* @library /test/lib
* @modules java.base/sun.net.util
* @comment Insert -Djavax.net.debug=all into the following lines to enable SSL debugging
* @run main/othervm SubjectAltNameIP 127.0.0.1
* @run main/othervm SubjectAltNameIP [::1]
*/
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import jdk.test.lib.Asserts;
import jdk.test.lib.net.IPSupport;
import jdk.test.lib.net.SimpleSSLContext;
import jtreg.SkippedException;
import sun.net.util.IPAddressUtil;
public class SubjectAltNameIP {
// Is the server ready to serve?
private final CountDownLatch serverReady = new CountDownLatch(1);
// Use any free port by default.
volatile int serverPort = 0;
// Stores an exception thrown by server in a separate thread.
volatile Exception serverException = null;
// SSLSocket object created by HttpsClient internally.
SSLSocket clientSSLSocket = null;
// The hostname the server socket is bound to.
String hostName;
static final byte[] requestEnd = new byte[] {'\r', '\n', '\r', '\n' };
// Read until the end of the request.
void readOneRequest(InputStream is) throws IOException {
int requestEndCount = 0, r;
while ((r = is.read()) != -1) {
if (r == requestEnd[requestEndCount]) {
requestEndCount++;
if (requestEndCount == 4) {
break;
}
} else {
requestEndCount = 0;
}
}
}
/*
* Define the server side of the test.
*
* If the server prematurely exits, serverReady will be set to true
* to avoid infinite hangs.
*/
void doServerSide() throws Exception {
SSLServerSocketFactory sslssf =
new SimpleSSLContext().get().getServerSocketFactory();
SSLServerSocket sslServerSocket =
(SSLServerSocket) sslssf.createServerSocket(
serverPort, 0,
InetAddress.getByName(hostName));
sslServerSocket.setEnabledProtocols(new String[]{"TLSv1.3"});
serverPort = sslServerSocket.getLocalPort();
/*
* Signal the client, the server is ready to accept connection.
*/
serverReady.countDown();
SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
OutputStream sslOS = sslSocket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(sslOS));
bw.write("HTTP/1.1 200 OK\r\n\r\n");
bw.flush();
readOneRequest(sslSocket.getInputStream());
sslSocket.close();
}
/*
* Define the client side of the test.
*
* If the server prematurely exits, serverReady will be set to true
* to avoid infinite hangs.
*/
void doClientSide() throws Exception {
/*
* Wait for server to get started.
*/
serverReady.await();
if (serverException != null) {
throw new RuntimeException("Server failed to start.", serverException);
}
SSLSocketFactory sf = new SimpleSSLContext().get().getSocketFactory();
URI uri = new URI("https://" + hostName + ":" + serverPort + "/index.html");
HttpsURLConnection conn = (HttpsURLConnection)uri.toURL().openConnection();
/*
* Simulate an external JSSE implementation and store the client SSLSocket
* used internally.
*/
conn.setSSLSocketFactory(wrapSocketFactory(sf,
sslSocket -> {
Asserts.assertEquals(null, clientSSLSocket, "clientSSLSocket is");
clientSSLSocket = sslSocket;
}));
conn.getInputStream();
var sniSN = clientSSLSocket.getSSLParameters().getServerNames();
if (sniSN != null && !sniSN.isEmpty()) {
throw new RuntimeException("SNI server name '" +
sniSN.getFirst() + "' must not be set.");
}
if (conn.getResponseCode() == -1) {
throw new RuntimeException("getResponseCode() returns -1");
}
}
public static void main(String[] args) throws Exception {
if (IPAddressUtil.isIPv6LiteralAddress(args[0]) && !IPSupport.hasIPv6()) {
throw new SkippedException("Skipping test - IPv6 is not supported");
}
/*
* Start the tests.
*/
new SubjectAltNameIP(args[0]);
}
Thread serverThread = null;
/*
* Primary constructor, used to drive remainder of the test.
*
* Fork off the other side, then do your work.
*/
SubjectAltNameIP(String host) throws Exception {
hostName = host;
startServer();
doClientSide();
/*
* Wait for other side to close down.
*/
serverThread.join();
if (serverException != null)
throw serverException;
}
void startServer() {
serverThread = new Thread(() -> {
try {
doServerSide();
} catch (Exception e) {
/*
* Our server thread just died.
*
* Store the exception and release the client.
*/
serverException = e;
serverReady.countDown();
}
});
serverThread.start();
}
/*
* Wraps SSLSocketImpl to simulate a different JSSE implementation
*/
private static SSLSocketFactory wrapSocketFactory(final SSLSocketFactory wrap, final Consumer<SSLSocket> store) {
return new SSLSocketFactory() {
@Override
public String[] getDefaultCipherSuites() {
return wrap.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return wrap.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose)
throws IOException {
final SSLSocket so =
(SSLSocket) wrap.createSocket(s, host, port, autoClose);
// store the underlying SSLSocket for later use
store.accept(so);
return new SSLSocket() {
@Override
public void connect(SocketAddress endpoint,
int timeout) throws IOException {
so.connect(endpoint, timeout);
}
@Override
public String[] getSupportedCipherSuites() {
return so.getSupportedCipherSuites();
}
@Override
public String[] getEnabledCipherSuites() {
return so.getEnabledCipherSuites();
}
@Override
public void setEnabledCipherSuites(String[] suites) {
so.setEnabledCipherSuites(suites);
}
@Override
public String[] getSupportedProtocols() {
return so.getSupportedProtocols();
}
@Override
public String[] getEnabledProtocols() {
return so.getEnabledProtocols();
}
@Override
public void setEnabledProtocols(String[] protocols) {
so.setEnabledProtocols(protocols);
}
@Override
public SSLSession getSession() {
return so.getSession();
}
@Override
public SSLSession getHandshakeSession() {
return so.getHandshakeSession();
}
@Override
public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
so.addHandshakeCompletedListener(listener);
}
@Override
public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
so.removeHandshakeCompletedListener(listener);
}
@Override
public void startHandshake() throws IOException {
so.startHandshake();
}
@Override
public void setUseClientMode(boolean mode) {
so.setUseClientMode(mode);
}
@Override
public boolean getUseClientMode() {
return so.getUseClientMode();
}
@Override
public void setNeedClientAuth(boolean need) {
}
@Override
public boolean getNeedClientAuth() {
return false;
}
@Override
public void setWantClientAuth(boolean want) {
}
@Override
public boolean getWantClientAuth() {
return false;
}
@Override
public void setEnableSessionCreation(boolean flag) {
so.setEnableSessionCreation(flag);
}
@Override
public boolean getEnableSessionCreation() {
return so.getEnableSessionCreation();
}
@Override
public void close() throws IOException {
so.close();
}
@Override
public boolean isClosed() {
return so.isClosed();
}
@Override
public void shutdownInput() throws IOException {
so.shutdownInput();
}
@Override
public boolean isInputShutdown() {
return so.isInputShutdown();
}
@Override
public void shutdownOutput() throws IOException {
so.shutdownOutput();
}
@Override
public boolean isOutputShutdown() {
return so.isOutputShutdown();
}
@Override
public InputStream getInputStream() throws IOException {
return so.getInputStream();
}
@Override
public OutputStream getOutputStream() throws IOException {
return so.getOutputStream();
}
@Override
public SSLParameters getSSLParameters() {
return so.getSSLParameters();
}
@Override
public void setSSLParameters(SSLParameters params) {
so.setSSLParameters(params);
}
};
}
@Override
public Socket createSocket(String h, int p) {
return null;
}
@Override
public Socket createSocket(String h, int p, InetAddress ipa, int lp) {
return null;
}
@Override
public Socket createSocket(InetAddress h, int p) {
return null;
}
@Override
public Socket createSocket(InetAddress a, int p, InetAddress l, int lp) {
return null;
}
};
}
}