diff --git a/vertx-core/src/main/asciidoc/tcp.adoc b/vertx-core/src/main/asciidoc/tcp.adoc index d8e8590663c..05c1768a4cb 100644 --- a/vertx-core/src/main/asciidoc/tcp.adoc +++ b/vertx-core/src/main/asciidoc/tcp.adoc @@ -273,6 +273,14 @@ specifying the port and host of the server, returning a future completed with th {@link examples.TcpExamples#connectingToAServer} ---- +{@link io.vertx.core.net.ConnectOptions} gives more control over the connection, e.g. the local address the +connection is bound to before connecting, when the operating system default is not desired: + +[source,$lang] +---- +{@link examples.TcpExamples#connectingToAServerFromALocalAddress} +---- + === Making connections to Unix domain sockets When running on JDK 16+, or using a <<_native_transports,native transport>>, a client can connect to Unix domain sockets: diff --git a/vertx-core/src/main/generated/io/vertx/core/http/HttpConnectOptionsConverter.java b/vertx-core/src/main/generated/io/vertx/core/http/HttpConnectOptionsConverter.java index c70f536c71d..1d8eeaef421 100644 --- a/vertx-core/src/main/generated/io/vertx/core/http/HttpConnectOptionsConverter.java +++ b/vertx-core/src/main/generated/io/vertx/core/http/HttpConnectOptionsConverter.java @@ -47,6 +47,11 @@ static void fromJson(Iterable> json, HttpCon obj.setConnectTimeout(((Number)member.getValue()).longValue()); } break; + case "localAddress": + if (member.getValue() instanceof JsonObject) { + obj.setLocalAddress(io.vertx.core.net.SocketAddress.fromJson((JsonObject)member.getValue())); + } + break; } } } @@ -75,5 +80,8 @@ static void toJson(HttpConnectOptions obj, java.util.Map json) { json.put("sslOptions", obj.getSslOptions().toJson()); } json.put("connectTimeout", obj.getConnectTimeout()); + if (obj.getLocalAddress() != null) { + json.put("localAddress", obj.getLocalAddress().toJson()); + } } } diff --git a/vertx-core/src/main/java/examples/TcpExamples.java b/vertx-core/src/main/java/examples/TcpExamples.java index c98e1bfa291..7b2e94267c2 100755 --- a/vertx-core/src/main/java/examples/TcpExamples.java +++ b/vertx-core/src/main/java/examples/TcpExamples.java @@ -247,6 +247,25 @@ public void connectingToAServer(Vertx vertx) { }); } + public void connectingToAServerFromALocalAddress(Vertx vertx) { + + NetClient client = vertx.createNetClient(); + client + .connect(new ConnectOptions() + .setHost("localhost") + .setPort(4321) + // Bind the connection to this network interface, with an ephemeral port + .setLocalAddress(SocketAddress.inetSocketAddress(0, "192.168.0.10"))) + .onComplete(res -> { + if (res.succeeded()) { + NetSocket socket = res.result(); + System.out.println("Connected from " + socket.localAddress()); + } else { + System.out.println("Failed to connect: " + res.cause().getMessage()); + } + }); + } + public void configurationOfTcpClientReconnect(Vertx vertx) { TcpClientConfig options = new TcpClientConfig(). diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpConnectOptions.java b/vertx-core/src/main/java/io/vertx/core/http/HttpConnectOptions.java index d914d7aa5a0..bbe4884ba74 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpConnectOptions.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpConnectOptions.java @@ -67,14 +67,20 @@ public class HttpConnectOptions { */ public static final long DEFAULT_CONNECT_TIMEOUT = -1L; + /** + * The default local address = {@code null} (client default) + */ + public static final SocketAddress DEFAULT_LOCAL_ADDRESS = null; + private HttpVersion protocolVersion; private ProxyOptions proxyOptions; private Address server; private String host; private Integer port; private Boolean ssl; - private ClientSSLOptions sslOptions;; + private ClientSSLOptions sslOptions; private long connectTimeout; + private SocketAddress localAddress; /** * Default constructor @@ -98,6 +104,7 @@ public HttpConnectOptions(HttpConnectOptions other) { setSsl(other.ssl); sslOptions = other.sslOptions != null ? new ClientSSLOptions(other.sslOptions) : null; setConnectTimeout(other.connectTimeout); + setLocalAddress(other.localAddress); } /** @@ -123,6 +130,7 @@ protected void init() { ssl = DEFAULT_SSL; sslOptions = null; connectTimeout = DEFAULT_CONNECT_TIMEOUT; + localAddress = DEFAULT_LOCAL_ADDRESS; } /** @@ -296,6 +304,33 @@ private URL parseUrl(String surl) { } } + /** + * Get the local address to bind the connection to, when none is provided the client default local address + * is used and when the client does not define one, the operating system chooses the local address. + * + * @return the local address + */ + public SocketAddress getLocalAddress() { + return localAddress; + } + + /** + * Set the local address to bind the connection to. + * + *

When set, the connection is bound to this address before connecting to the server, an ephemeral + * port is used when the port is {@code 0}. This overrides the default local address of the client, if any. + * + * @param localAddress the local address, must be an inet socket address + * @return a reference to this, so the API can be used fluently + */ + public HttpConnectOptions setLocalAddress(SocketAddress localAddress) { + if (localAddress != null && localAddress.isDomainSocket()) { + throw new IllegalArgumentException("Cannot set a domain socket local address"); + } + this.localAddress = localAddress; + return this; + } + public JsonObject toJson() { JsonObject json = new JsonObject(); HttpConnectOptionsConverter.toJson(this, json); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java index d875394badb..6472fdc7f9f 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java @@ -324,6 +324,8 @@ public HttpClientInternal build() { .protocol("http") .sslOptions(sslOptions) .sslEngineOptions(sslEngineOptions) + // Legacy client options may define a default local address + .localAddress(clientOptions != null ? NetClientBuilder.localAddress(clientOptions) : null) .build(); LogConfig logConfig = co.getTcpConfig().getLogConfig(); ObservabilityConfig observabilityConfig = co.getObservabilityConfig(); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java index 9ad932d25c4..4c2102e340a 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java @@ -394,7 +394,7 @@ private Future connect(HttpClientTransp Boolean ssl = connect.isSsl(); boolean useSSL = ssl != null ? ssl : defaultSsl; checkClosed(); - HttpConnectParams params = new HttpConnectParams(protocols, sslOptions, proxyOptions, useSSL); + HttpConnectParams params = new HttpConnectParams(protocols, sslOptions, proxyOptions, useSSL, false, connect.getLocalAddress()); return transport.connect(vertx.getOrCreateContext(), server, authority, params, clientMetrics) .map(conn -> new UnpooledHttpClientConnection(conn).init()); } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpConnectParams.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpConnectParams.java index 7f6bde92c4c..ae82e4fc8ec 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpConnectParams.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpConnectParams.java @@ -3,6 +3,7 @@ import io.vertx.core.http.HttpVersion; import io.vertx.core.net.ClientSSLOptions; import io.vertx.core.net.ProxyOptions; +import io.vertx.core.net.SocketAddress; import java.util.List; @@ -20,11 +21,21 @@ public HttpConnectParams(List protocols, ProxyOptions proxyOptions, boolean ssl, boolean forwardProxy) { + this(protocols, sslOptions, proxyOptions, ssl, forwardProxy, null); + } + + public HttpConnectParams(List protocols, + ClientSSLOptions sslOptions, + ProxyOptions proxyOptions, + boolean ssl, + boolean forwardProxy, + SocketAddress localAddress) { this.protocols = protocols; this.sslOptions = sslOptions; this.proxyOptions = proxyOptions; this.ssl = ssl; this.forwardProxy = forwardProxy; + this.localAddress = localAddress; } public final List protocols; @@ -35,4 +46,9 @@ public HttpConnectParams(List protocols, public final boolean forwardProxy; + /** + * The local address to bind the connection to, {@code null} to use the client default local address. + */ + public final SocketAddress localAddress; + } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java b/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java index e88dc532aad..7301414237b 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java @@ -163,6 +163,7 @@ private void connect(ContextInternal context, HttpConnectParams params, HostAndP ConnectOptions connectOptions = new ConnectOptions(); connectOptions.setRemoteAddress(server); + connectOptions.setLocalAddress(params.localAddress); if (proxyHttps) { // Forward mode through an HTTPS proxy: the socket's TLS terminates at the proxy, so the peer / // SNI is the proxy (the server), not the origin authority. diff --git a/vertx-core/src/main/java/io/vertx/core/impl/VertxImpl.java b/vertx-core/src/main/java/io/vertx/core/impl/VertxImpl.java index 09e72d7a0df..3607767f71f 100644 --- a/vertx-core/src/main/java/io/vertx/core/impl/VertxImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/impl/VertxImpl.java @@ -432,6 +432,7 @@ private WebSocketClientImpl createWebSocketClientImpl(WebSocketClientOptions opt .protocol("http") .sslOptions(options.getSslOptions()) .sslEngineOptions(options.getSslEngineOptions()) + .localAddress(NetClientBuilder.localAddress(options)) .build(); TcpHttpClientTransport channelConnector = TcpHttpClientTransport.create(tcpClient, config, false, httpMetrics); return new WebSocketClientImpl(this, o, options, channelConnector, httpMetrics); diff --git a/vertx-core/src/main/java/io/vertx/core/net/ConnectOptions.java b/vertx-core/src/main/java/io/vertx/core/net/ConnectOptions.java index 675663d3ceb..7b92199eadb 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/ConnectOptions.java +++ b/vertx-core/src/main/java/io/vertx/core/net/ConnectOptions.java @@ -29,6 +29,7 @@ public class ConnectOptions { private Integer port; private String sniServerName; private SocketAddress remoteAddress; + private SocketAddress localAddress; private ProxyOptions proxyOptions; private boolean ssl; private ClientSSLOptions sslOptions; @@ -42,6 +43,7 @@ public ConnectOptions() { port = null; sniServerName = null; remoteAddress = null; + localAddress = null; proxyOptions = null; ssl = DEFAULT_SSL; sslOptions = null; @@ -58,6 +60,7 @@ public ConnectOptions(ConnectOptions other) { port = other.getPort(); sniServerName = other.getSniServerName(); remoteAddress = other.getRemoteAddress(); + localAddress = other.getLocalAddress(); proxyOptions = other.getProxyOptions() != null ? new ProxyOptions(other.getProxyOptions()) : null; ssl = other.isSsl(); sslOptions = other.getSslOptions() != null ? new ClientSSLOptions(other.getSslOptions()) : null; @@ -126,6 +129,33 @@ public ConnectOptions setRemoteAddress(SocketAddress remoteAddress) { return this; } + /** + * Get the local address to bind the client connection to, when none is provided the client default local address + * is used and when the client does not define one, the operating system chooses the local address. + * + * @return the local address + */ + public SocketAddress getLocalAddress() { + return localAddress; + } + + /** + * Set the local address to bind the client connection to. + * + *

When set, the connection is bound to this address before connecting to the remote address, an ephemeral + * port is used when the port is {@code 0}. This overrides the default local address of the client, if any. + * + * @param localAddress the local address, must be an inet socket address + * @return a reference to this, so the API can be used fluently + */ + public ConnectOptions setLocalAddress(SocketAddress localAddress) { + if (localAddress != null && localAddress.isDomainSocket()) { + throw new IllegalArgumentException("Cannot set a domain socket local address"); + } + this.localAddress = localAddress; + return this; + } + /** * @return the SNI (server name indication) server name */ diff --git a/vertx-core/src/main/java/io/vertx/core/net/TcpClientConfig.java b/vertx-core/src/main/java/io/vertx/core/net/TcpClientConfig.java index 5ae99ff348e..a48830f72d4 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/TcpClientConfig.java +++ b/vertx-core/src/main/java/io/vertx/core/net/TcpClientConfig.java @@ -29,7 +29,6 @@ public class TcpClientConfig extends TcpEndpointConfig { private Duration connectTimeout; private ProxyOptions proxyOptions; private List nonProxyHosts; - private SocketAddress localAddress; private int reconnectAttempts; private Duration reconnectInterval; @@ -38,7 +37,6 @@ public TcpClientConfig() { this.connectTimeout = Duration.ofMillis(ClientOptionsBase.DEFAULT_CONNECT_TIMEOUT); this.proxyOptions = null; this.nonProxyHosts = null; - this.localAddress = null; this.reconnectAttempts = NetClientOptions.DEFAULT_RECONNECT_ATTEMPTS; this.reconnectInterval = Duration.ofMillis(NetClientOptions.DEFAULT_RECONNECT_INTERVAL); } @@ -48,17 +46,12 @@ public TcpClientConfig(TcpClientConfig other) { this.connectTimeout = other.connectTimeout; this.proxyOptions = other.proxyOptions != null ? new ProxyOptions(other.proxyOptions) : null; this.nonProxyHosts = other.nonProxyHosts != null ? new ArrayList<>(other.nonProxyHosts) : null; - this.localAddress = other.localAddress; this.reconnectAttempts = other.reconnectAttempts; this.reconnectInterval = other.reconnectInterval; } public TcpClientConfig(NetClientOptions options) { this((ClientOptionsBase)options); - String localAddress = options.getLocalAddress(); - if (localAddress != null) { - setLocalAddress(SocketAddress.inetSocketAddress(0, localAddress)); - } setReconnectAttempts(options.getReconnectAttempts()); setReconnectInterval(Duration.ofMillis(options.getReconnectInterval())); } @@ -178,28 +171,6 @@ public TcpClientConfig addNonProxyHost(String host) { return this; } - /** - * @return the local address to bind for network connections. - */ - public SocketAddress getLocalAddress() { - return localAddress; - } - - /** - * Set the local address to bind for network connections. When the local address is null, - * it will pick any local address and a random port, the default local address is null. - * - * @param localAddress the local address - * @return a reference to this, so the API can be used fluently - */ - public TcpClientConfig setLocalAddress(SocketAddress localAddress) { - if (localAddress != null && localAddress.isDomainSocket()) { - throw new IllegalArgumentException("Cannot set a domain socket local address"); - } - this.localAddress = localAddress; - return this; - } - /** * @return the value of reconnect attempts */ diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientBuilder.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientBuilder.java index c9cb56510a2..a500da7a456 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientBuilder.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientBuilder.java @@ -11,9 +11,11 @@ package io.vertx.core.net.impl.tcp; import io.vertx.core.internal.VertxInternal; +import io.vertx.core.net.ClientOptionsBase; import io.vertx.core.net.ClientSSLOptions; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.SSLEngineOptions; +import io.vertx.core.net.SocketAddress; import io.vertx.core.net.TcpClientConfig; /** @@ -27,6 +29,7 @@ public class NetClientBuilder { private ClientSSLOptions sslOptions; private SSLEngineOptions sslEngineOptions; private boolean registerWriteHandler; + private SocketAddress localAddress; public NetClientBuilder(VertxInternal vertx, TcpClientConfig config) { this.vertx = vertx; @@ -42,6 +45,23 @@ public NetClientBuilder(VertxInternal vertx, NetClientOptions options) { this.sslOptions = options.getSslOptions(); this.sslEngineOptions = options.getSslEngineOptions(); this.protocol = null; + this.localAddress = localAddress(options); + } + + /** + * @return the client default local address of {@code options}, {@code null} when none is configured + */ + public static SocketAddress localAddress(ClientOptionsBase options) { + String localAddress = options.getLocalAddress(); + return localAddress != null ? SocketAddress.inetSocketAddress(0, localAddress) : null; + } + + /** + * Set the client default local address, used when connect options do not define one. + */ + public NetClientBuilder localAddress(SocketAddress localAddress) { + this.localAddress = localAddress; + return this; } public NetClientBuilder sslOptions(ClientSSLOptions sslOptions) { @@ -60,6 +80,6 @@ public NetClientBuilder protocol(String protocol) { } public NetClientImpl build() { - return new NetClientImpl(vertx, config, protocol, sslOptions, sslEngineOptions, registerWriteHandler); + return new NetClientImpl(vertx, config, protocol, sslOptions, sslEngineOptions, registerWriteHandler, localAddress); } } diff --git a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientImpl.java b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientImpl.java index c8c91015353..6844df3b19f 100644 --- a/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/net/impl/tcp/NetClientImpl.java @@ -62,6 +62,7 @@ public class NetClientImpl implements NetClientInternal { private final VertxInternal vertx; private final TcpClientConfig config; + private final SocketAddress localAddress; private final TcpConfig transportOptions; private final String protocol; private final boolean registerWriteHandler; @@ -77,8 +78,19 @@ public NetClientImpl(VertxInternal vertx, ClientSSLOptions sslOptions, SSLEngineOptions sslEngineOptions, boolean registerWriteHandler) { + this(vertx, config, protocol, sslOptions, sslEngineOptions, registerWriteHandler, null); + } + + public NetClientImpl(VertxInternal vertx, + TcpClientConfig config, + String protocol, + ClientSSLOptions sslOptions, + SSLEngineOptions sslEngineOptions, + boolean registerWriteHandler, + SocketAddress localAddress) { this.vertx = vertx; + this.localAddress = localAddress; this.channelGroup = new ConnectionGroup(vertx.acceptorEventLoopGroup().next()) { @Override protected void handleClose(Completable completion) { @@ -314,7 +326,11 @@ private void connectInternal(ConnectOptions connectOptions, // Transport specific TCP configuration vertx.transport().configure(config.getTransportConfig(), domainSocket, bootstrap); - SocketAddress localAddress = config.getLocalAddress(); + // The connect options local address overrides the client default local address + SocketAddress localAddress = connectOptions.getLocalAddress(); + if (localAddress == null) { + localAddress = this.localAddress; + } if (localAddress != null) { bootstrap.localAddress(localAddress.host(), localAddress.port()); } diff --git a/vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java b/vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java index 487f91df395..4fcfd16f936 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java @@ -2075,6 +2075,63 @@ public void testContexts() throws Exception { thread.join(20_000); } + private static int freeLocalPort() throws Exception { + try (java.net.ServerSocket ss = new java.net.ServerSocket(0)) { + return ss.getLocalPort(); + } + } + + /** + * @return a loopback address distinct from the default one, or {@code null} when the platform does not provide one + */ + private static String alternativeLoopbackAddress() { + try (java.net.ServerSocket ss = new java.net.ServerSocket(0, 1, java.net.InetAddress.getByName("127.0.0.2"))) { + return "127.0.0.2"; + } catch (Exception e) { + return null; + } + } + + @Test + public void testClientOptionsLocalAddress() throws Exception { + // Legacy HttpClientOptions#setLocalAddress must be honoured by the HTTP client, use a loopback address + // distinct from the default one so that the assertion is meaningful + String expectedAddress = alternativeLoopbackAddress(); + Assume.assumeNotNull(expectedAddress); + AtomicReference remote = new AtomicReference<>(); + client = vertx.createHttpClient(new HttpClientOptions().setLocalAddress(expectedAddress)); + server.requestHandler(req -> { + remote.set(req.remoteAddress()); + req.response().end(); + }); + startServer(testAddress); + io.vertx.core.http.HttpClientConnection conn = client.connect(new HttpConnectOptions().setHost(config.host()).setPort(config.port())).await(); + conn.request().compose(req -> req.send().map(HttpClientResponse::statusCode)).await(); + assertEquals(expectedAddress, remote.get().host()); + assertEquals(expectedAddress, conn.localAddress().host()); + } + + @Test + public void testConnectOptionsLocalAddress() throws Exception { + String expectedAddress = TestUtils.loopbackAddress(); + int expectedPort = freeLocalPort(); + AtomicReference remote = new AtomicReference<>(); + client = vertx.createHttpClient(); + server.requestHandler(req -> { + remote.set(req.remoteAddress()); + req.response().end(); + }); + startServer(testAddress); + io.vertx.core.http.HttpClientConnection conn = client.connect(new HttpConnectOptions() + .setHost(config.host()) + .setPort(config.port()) + .setLocalAddress(SocketAddress.inetSocketAddress(expectedPort, expectedAddress))).await(); + conn.request().compose(req -> req.send().map(HttpClientResponse::statusCode)).await(); + assertEquals(expectedAddress, remote.get().host()); + assertEquals(expectedPort, remote.get().port()); + assertEquals(expectedPort, conn.localAddress().port()); + } + @Test public void testRequestHandlerNotCalledInvalidRequest(Checkpoint checkpoint) throws Exception { server.requestHandler(req -> { diff --git a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java index c00503bef0e..d006a33a264 100755 --- a/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/NetTest.java @@ -3418,6 +3418,48 @@ public void testClientLocalAddress(Checkpoint checkpoint) { client.connect(1234, "localhost").await(); } + private static int freeLocalPort() throws Exception { + try (java.net.ServerSocket ss = new java.net.ServerSocket(0)) { + return ss.getLocalPort(); + } + } + + @Test + public void testConnectOptionsLocalAddress(Checkpoint checkpoint) throws Exception { + String expectedAddress = TestUtils.loopbackAddress(); + int expectedPort = freeLocalPort(); + server.connectHandler(sock -> { + assertEquals(expectedAddress, sock.remoteAddress().host()); + assertEquals(expectedPort, sock.remoteAddress().port()); + checkpoint.succeed(); + }); + client = vertx.createNetClient(); + server.listen(1234, "localhost").await(); + NetSocket so = client.connect(new ConnectOptions() + .setHost("localhost") + .setPort(1234) + .setLocalAddress(SocketAddress.inetSocketAddress(expectedPort, expectedAddress))).await(); + assertEquals(expectedPort, so.localAddress().port()); + } + + @Test + public void testConnectOptionsLocalAddressOverridesClientLocalAddress(Checkpoint checkpoint) throws Exception { + String expectedAddress = TestUtils.loopbackAddress(); + int expectedPort = freeLocalPort(); + server.connectHandler(sock -> { + assertEquals(expectedAddress, sock.remoteAddress().host()); + assertEquals(expectedPort, sock.remoteAddress().port()); + checkpoint.succeed(); + }); + // The client default local address binds to an ephemeral port, the connect options must take precedence + client = vertx.createNetClient(new NetClientOptions().setLocalAddress(expectedAddress)); + server.listen(1234, "localhost").await(); + client.connect(new ConnectOptions() + .setHost("localhost") + .setPort(1234) + .setLocalAddress(SocketAddress.inetSocketAddress(expectedPort, expectedAddress))).await(); + } + @Test public void testWorkerClient(Checkpoint checkpoint) throws Exception { String expected = TestUtils.randomAlphaString(2000);