Skip to content

Commit 9e8aa85

Browse files
vydfuch
authored andcommitted
8346017: Socket.connect specified to throw UHE for unresolved address is problematic for SOCKS V5 proxy
Reviewed-by: dfuchs, alanb
1 parent 5b703c7 commit 9e8aa85

File tree

3 files changed

+234
-19
lines changed

3 files changed

+234
-19
lines changed

src/java.base/share/classes/java/net/Socket.java

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -569,9 +569,8 @@ void setConnected() {
569569
/**
570570
* Connects this socket to the server.
571571
*
572-
* <p> If the endpoint is an unresolved {@link InetSocketAddress}, or the
573-
* connection cannot be established, then the socket is closed, and an
574-
* {@link IOException} is thrown.
572+
* <p> If the connection cannot be established, then the socket is closed,
573+
* and an {@link IOException} is thrown.
575574
*
576575
* <p> This method is {@linkplain Thread#interrupt() interruptible} in the
577576
* following circumstances:
@@ -591,8 +590,8 @@ void setConnected() {
591590
* @param endpoint the {@code SocketAddress}
592591
* @throws IOException if an error occurs during the connection, the socket
593592
* is already connected or the socket is closed
594-
* @throws UnknownHostException if the endpoint is an unresolved
595-
* {@link InetSocketAddress}
593+
* @throws UnknownHostException if the connection could not be established
594+
* because the endpoint is an unresolved {@link InetSocketAddress}
596595
* @throws java.nio.channels.IllegalBlockingModeException
597596
* if this socket has an associated channel,
598597
* and the channel is in non-blocking mode
@@ -609,9 +608,8 @@ public void connect(SocketAddress endpoint) throws IOException {
609608
* A timeout of zero is interpreted as an infinite timeout. The connection
610609
* will then block until established or an error occurs.
611610
*
612-
* <p> If the endpoint is an unresolved {@link InetSocketAddress}, the
613-
* connection cannot be established, or the timeout expires before the
614-
* connection is established, then the socket is closed, and an
611+
* <p> If the connection cannot be established, or the timeout expires
612+
* before the connection is established, then the socket is closed, and an
615613
* {@link IOException} is thrown.
616614
*
617615
* <p> This method is {@linkplain Thread#interrupt() interruptible} in the
@@ -634,8 +632,8 @@ public void connect(SocketAddress endpoint) throws IOException {
634632
* @throws IOException if an error occurs during the connection, the socket
635633
* is already connected or the socket is closed
636634
* @throws SocketTimeoutException if timeout expires before connecting
637-
* @throws UnknownHostException if the endpoint is an unresolved
638-
* {@link InetSocketAddress}
635+
* @throws UnknownHostException if the connection could not be established
636+
* because the endpoint is an unresolved {@link InetSocketAddress}
639637
* @throws java.nio.channels.IllegalBlockingModeException
640638
* if this socket has an associated channel,
641639
* and the channel is in non-blocking mode
@@ -660,12 +658,6 @@ public void connect(SocketAddress endpoint, int timeout) throws IOException {
660658
if (!(endpoint instanceof InetSocketAddress epoint))
661659
throw new IllegalArgumentException("Unsupported address type");
662660

663-
if (epoint.isUnresolved()) {
664-
var uhe = new UnknownHostException(epoint.getHostName());
665-
closeSuppressingExceptions(uhe);
666-
throw uhe;
667-
}
668-
669661
InetAddress addr = epoint.getAddress();
670662
checkAddress(addr, "connect");
671663

test/jdk/java/net/Socket/ConnectFailTest.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
import java.io.IOException;
3030
import java.net.InetAddress;
3131
import java.net.InetSocketAddress;
32+
import java.net.Proxy;
3233
import java.net.ServerSocket;
3334
import java.net.Socket;
3435
import java.net.SocketException;
3536
import java.net.UnknownHostException;
3637
import java.nio.channels.SocketChannel;
3738
import java.util.List;
3839

40+
import static java.net.InetAddress.getLoopbackAddress;
3941
import static org.junit.jupiter.api.Assertions.assertEquals;
4042
import static org.junit.jupiter.api.Assertions.assertFalse;
4143
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -50,6 +52,8 @@
5052
*/
5153
class ConnectFailTest {
5254

55+
// Implementation Note: Explicitly binding on the loopback address to avoid potential unstabilities.
56+
5357
private static final int DEAD_SERVER_PORT = 0xDEAD;
5458

5559
private static final InetSocketAddress REFUSING_SOCKET_ADDRESS = Utils.refusingEndpoint();
@@ -83,7 +87,7 @@ void testUnboundSocket(Socket socket) throws IOException {
8387
@MethodSource("sockets")
8488
void testBoundSocket(Socket socket) throws IOException {
8589
try (socket) {
86-
socket.bind(new InetSocketAddress(0));
90+
socket.bind(new InetSocketAddress(getLoopbackAddress(), 0));
8791
assertTrue(socket.isBound());
8892
assertFalse(socket.isConnected());
8993
assertThrows(IOException.class, () -> socket.connect(REFUSING_SOCKET_ADDRESS));
@@ -132,7 +136,7 @@ void testUnboundSocketWithUnresolvedAddress(Socket socket) throws IOException {
132136
@MethodSource("sockets")
133137
void testBoundSocketWithUnresolvedAddress(Socket socket) throws IOException {
134138
try (socket) {
135-
socket.bind(new InetSocketAddress(0));
139+
socket.bind(new InetSocketAddress(getLoopbackAddress(), 0));
136140
assertTrue(socket.isBound());
137141
assertFalse(socket.isConnected());
138142
assertThrows(UnknownHostException.class, () -> socket.connect(UNRESOLVED_ADDRESS));
@@ -161,7 +165,8 @@ static List<Socket> sockets() throws Exception {
161165
Socket socket = new Socket();
162166
@SuppressWarnings("resource")
163167
Socket channelSocket = SocketChannel.open().socket();
164-
return List.of(socket, channelSocket);
168+
Socket noProxySocket = new Socket(Proxy.NO_PROXY);
169+
return List.of(socket, channelSocket, noProxySocket);
165170
}
166171

167172
private static ServerSocket createEphemeralServerSocket() throws IOException {
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import jdk.test.lib.Utils;
25+
import org.junit.jupiter.api.AfterAll;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.Test;
28+
29+
import java.io.IOException;
30+
import java.net.Authenticator;
31+
import java.net.InetSocketAddress;
32+
import java.net.PasswordAuthentication;
33+
import java.net.Proxy;
34+
import java.net.ServerSocket;
35+
import java.net.Socket;
36+
import java.net.SocketException;
37+
38+
import static java.net.InetAddress.getLoopbackAddress;
39+
import static org.junit.jupiter.api.Assertions.assertEquals;
40+
import static org.junit.jupiter.api.Assertions.assertFalse;
41+
import static org.junit.jupiter.api.Assertions.assertThrows;
42+
import static org.junit.jupiter.api.Assertions.assertTrue;
43+
44+
/*
45+
* @test
46+
* @bug 8346017
47+
* @summary Verifies the `connect()` behaviour of a SOCKS proxy socket. In particular, that passing a resolvable
48+
* unresolved address doesn't throw an exception.
49+
* @library /test/lib /java/net/Socks
50+
* @build SocksServer
51+
* @run junit ConnectSocksProxyTest
52+
*/
53+
class ConnectSocksProxyTest {
54+
55+
// Implementation Note: Explicitly binding on the loopback address to avoid potential unstabilities.
56+
57+
private static final int DEAD_SERVER_PORT = 0xDEAD;
58+
59+
private static final InetSocketAddress REFUSING_SOCKET_ADDRESS = Utils.refusingEndpoint();
60+
61+
private static final InetSocketAddress UNRESOLVED_ADDRESS =
62+
InetSocketAddress.createUnresolved("no.such.host", DEAD_SERVER_PORT);
63+
64+
private static final String PROXY_AUTH_USERNAME = "foo";
65+
66+
private static final String PROXY_AUTH_PASSWORD = "bar";
67+
68+
private static SocksServer PROXY_SERVER;
69+
70+
private static Proxy PROXY;
71+
72+
@BeforeAll
73+
static void initAuthenticator() {
74+
Authenticator.setDefault(new Authenticator() {
75+
@Override
76+
protected PasswordAuthentication getPasswordAuthentication() {
77+
return new PasswordAuthentication(PROXY_AUTH_USERNAME, PROXY_AUTH_PASSWORD.toCharArray());
78+
}
79+
});
80+
}
81+
82+
@BeforeAll
83+
static void initProxyServer() throws IOException {
84+
PROXY_SERVER = new SocksServer(getLoopbackAddress(), 0, false);
85+
PROXY_SERVER.addUser(PROXY_AUTH_USERNAME, PROXY_AUTH_PASSWORD);
86+
PROXY_SERVER.start();
87+
InetSocketAddress proxyAddress = new InetSocketAddress(getLoopbackAddress(), PROXY_SERVER.getPort());
88+
PROXY = new Proxy(Proxy.Type.SOCKS, proxyAddress);
89+
}
90+
91+
@AfterAll
92+
static void stopProxyServer() {
93+
PROXY_SERVER.close();
94+
}
95+
96+
@Test
97+
void testUnresolvedAddress() {
98+
assertTrue(UNRESOLVED_ADDRESS.isUnresolved());
99+
}
100+
101+
/**
102+
* Verifies that an unbound socket is closed when {@code connect()} fails.
103+
*/
104+
@Test
105+
void testUnboundSocket() throws IOException {
106+
try (Socket socket = createProxiedSocket()) {
107+
assertFalse(socket.isBound());
108+
assertFalse(socket.isConnected());
109+
assertThrows(IOException.class, () -> socket.connect(REFUSING_SOCKET_ADDRESS));
110+
assertTrue(socket.isClosed());
111+
}
112+
}
113+
114+
/**
115+
* Verifies that a bound socket is closed when {@code connect()} fails.
116+
*/
117+
@Test
118+
void testBoundSocket() throws IOException {
119+
try (Socket socket = createProxiedSocket()) {
120+
socket.bind(new InetSocketAddress(getLoopbackAddress(), 0));
121+
assertTrue(socket.isBound());
122+
assertFalse(socket.isConnected());
123+
assertThrows(IOException.class, () -> socket.connect(REFUSING_SOCKET_ADDRESS));
124+
assertTrue(socket.isClosed());
125+
}
126+
}
127+
128+
/**
129+
* Verifies that a connected socket is not closed when {@code connect()} fails.
130+
*/
131+
@Test
132+
void testConnectedSocket() throws Throwable {
133+
try (Socket socket = createProxiedSocket();
134+
ServerSocket serverSocket = createEphemeralServerSocket()) {
135+
socket.connect(serverSocket.getLocalSocketAddress());
136+
try (Socket _ = serverSocket.accept()) {
137+
assertTrue(socket.isBound());
138+
assertTrue(socket.isConnected());
139+
SocketException exception = assertThrows(
140+
SocketException.class,
141+
() -> socket.connect(REFUSING_SOCKET_ADDRESS));
142+
assertEquals("Already connected", exception.getMessage());
143+
assertFalse(socket.isClosed());
144+
}
145+
}
146+
}
147+
148+
/**
149+
* Delegates to {@link #testUnconnectedSocketWithUnresolvedAddress(boolean, Socket)} using an unbound socket.
150+
*/
151+
@Test
152+
void testUnboundSocketWithUnresolvedAddress() throws IOException {
153+
try (Socket socket = createProxiedSocket()) {
154+
assertFalse(socket.isBound());
155+
assertFalse(socket.isConnected());
156+
testUnconnectedSocketWithUnresolvedAddress(false, socket);
157+
}
158+
}
159+
160+
/**
161+
* Delegates to {@link #testUnconnectedSocketWithUnresolvedAddress(boolean, Socket)} using a bound socket.
162+
*/
163+
@Test
164+
void testBoundSocketWithUnresolvedAddress() throws IOException {
165+
try (Socket socket = createProxiedSocket()) {
166+
socket.bind(new InetSocketAddress(getLoopbackAddress(), 0));
167+
testUnconnectedSocketWithUnresolvedAddress(true, socket);
168+
}
169+
}
170+
171+
/**
172+
* Verifies the behaviour of an unconnected socket when {@code connect()} is invoked using an unresolved address.
173+
*/
174+
private static void testUnconnectedSocketWithUnresolvedAddress(boolean bound, Socket socket) throws IOException {
175+
assertEquals(bound, socket.isBound());
176+
assertFalse(socket.isConnected());
177+
try (ServerSocket serverSocket = createEphemeralServerSocket()) {
178+
InetSocketAddress unresolvedAddress = InetSocketAddress.createUnresolved(
179+
getLoopbackAddress().getHostAddress(),
180+
serverSocket.getLocalPort());
181+
socket.connect(unresolvedAddress);
182+
try (Socket _ = serverSocket.accept()) {
183+
assertTrue(socket.isBound());
184+
assertTrue(socket.isConnected());
185+
assertFalse(socket.isClosed());
186+
}
187+
}
188+
}
189+
190+
/**
191+
* Verifies that a connected socket is not closed when {@code connect()} is invoked using an unresolved address.
192+
*/
193+
@Test
194+
void testConnectedSocketWithUnresolvedAddress() throws Throwable {
195+
try (Socket socket = createProxiedSocket();
196+
ServerSocket serverSocket = createEphemeralServerSocket()) {
197+
socket.connect(serverSocket.getLocalSocketAddress());
198+
try (Socket _ = serverSocket.accept()) {
199+
assertTrue(socket.isBound());
200+
assertTrue(socket.isConnected());
201+
SocketException exception = assertThrows(
202+
SocketException.class,
203+
() -> socket.connect(UNRESOLVED_ADDRESS));
204+
assertEquals("Already connected", exception.getMessage());
205+
assertFalse(socket.isClosed());
206+
}
207+
}
208+
}
209+
210+
private static Socket createProxiedSocket() {
211+
return new Socket(PROXY);
212+
}
213+
214+
private static ServerSocket createEphemeralServerSocket() throws IOException {
215+
return new ServerSocket(0, 0, getLoopbackAddress());
216+
}
217+
218+
}

0 commit comments

Comments
 (0)