Skip to content

Commit fd540d4

Browse files
Respect TTL of a DNS record for proxy config (#5960)
#### Motivation: From now on, armeria client ignores DNS TTL. Thus the client will keep sending the request to the old proxy. #### Modifications: - Make `InetSocketAddress` mutable to support a feature which update DNS. - Add fields to `xxxProxyConfig` (lastUpdatedTime, Schedulers) - Add a method for DNS update scheduling. - Add create method which supports `refreshInterval` JVM doesn't consider DNS TTL as default. Therefore, client has a intent to DNS update, IMHO, Client should configure JVM options. ``` # java.security networkaddress.cache.ttl=... networkaddress.cache.negative.ttl=... # Command line -Dsun.net.inetaddr.ttl=... ``` #### Result: - Fixes #5843
1 parent b1a09d4 commit fd540d4

File tree

12 files changed

+339
-44
lines changed

12 files changed

+339
-44
lines changed

core/src/main/java/com/linecorp/armeria/client/HttpClientDelegate.java

+76-24
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.linecorp.armeria.client;
1717

18+
import static com.linecorp.armeria.internal.common.util.IpAddrUtil.isCreatedWithIpAddressOnly;
1819
import static java.util.Objects.requireNonNull;
1920

2021
import java.net.InetAddress;
@@ -88,24 +89,36 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex
8889
}
8990

9091
final SessionProtocol protocol = ctx.sessionProtocol();
91-
final ProxyConfig proxyConfig;
92+
93+
final Endpoint endpointWithPort = endpoint.withDefaultPort(ctx.sessionProtocol());
94+
final EventLoop eventLoop = ctx.eventLoop().withoutContext();
95+
// TODO(ikhoon) Use ctx.exchangeType() to create an optimized HttpResponse for non-streaming response.
96+
final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop);
97+
updateCancellationTask(ctx, req, res);
98+
9299
try {
93-
proxyConfig = getProxyConfig(protocol, endpoint);
100+
resolveProxyConfig(protocol, endpoint, ctx, (proxyConfig, thrown) -> {
101+
if (thrown != null) {
102+
earlyFailedResponse(thrown, ctx, res);
103+
} else {
104+
assert proxyConfig != null;
105+
execute0(ctx, endpointWithPort, req, res, proxyConfig);
106+
}
107+
});
94108
} catch (Throwable t) {
95109
return earlyFailedResponse(t, ctx);
96110
}
111+
return res;
112+
}
97113

114+
private void execute0(ClientRequestContext ctx, Endpoint endpointWithPort, HttpRequest req,
115+
DecodedHttpResponse res, ProxyConfig proxyConfig) {
98116
final Throwable cancellationCause = ctx.cancellationCause();
99117
if (cancellationCause != null) {
100-
return earlyFailedResponse(cancellationCause, ctx);
118+
earlyFailedResponse(cancellationCause, ctx, res);
119+
return;
101120
}
102121

103-
final Endpoint endpointWithPort = endpoint.withDefaultPort(ctx.sessionProtocol());
104-
final EventLoop eventLoop = ctx.eventLoop().withoutContext();
105-
// TODO(ikhoon) Use ctx.exchangeType() to create an optimized HttpResponse for non-streaming response.
106-
final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop);
107-
updateCancellationTask(ctx, req, res);
108-
109122
final ClientConnectionTimingsBuilder timingsBuilder = ClientConnectionTimings.builder();
110123

111124
if (endpointWithPort.hasIpAddr() ||
@@ -125,8 +138,6 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex
125138
}
126139
});
127140
}
128-
129-
return res;
130141
}
131142

132143
private static void updateCancellationTask(ClientRequestContext ctx, HttpRequest req,
@@ -215,24 +226,52 @@ private void acquireConnectionAndExecute0(ClientRequestContext ctx, Endpoint end
215226
}
216227
}
217228

218-
private ProxyConfig getProxyConfig(SessionProtocol protocol, Endpoint endpoint) {
219-
final ProxyConfig proxyConfig = factory.proxyConfigSelector().select(protocol, endpoint);
220-
requireNonNull(proxyConfig, "proxyConfig");
229+
private void resolveProxyConfig(SessionProtocol protocol, Endpoint endpoint, ClientRequestContext ctx,
230+
BiConsumer<@Nullable ProxyConfig, @Nullable Throwable> onComplete) {
231+
final ProxyConfig unresolvedProxyConfig = factory.proxyConfigSelector().select(protocol, endpoint);
232+
requireNonNull(unresolvedProxyConfig, "unresolvedProxyConfig");
233+
final ProxyConfig proxyConfig = maybeSetHAProxySourceAddress(unresolvedProxyConfig);
221234

222-
// special behavior for haproxy when sourceAddress is null
223-
if (proxyConfig.proxyType() == ProxyType.HAPROXY &&
224-
((HAProxyConfig) proxyConfig).sourceAddress() == null) {
225-
final InetSocketAddress proxyAddress = proxyConfig.proxyAddress();
235+
final InetSocketAddress proxyAddress = proxyConfig.proxyAddress();
236+
final boolean needsDnsResolution = proxyAddress != null && !isCreatedWithIpAddressOnly(proxyAddress);
237+
if (needsDnsResolution) {
226238
assert proxyAddress != null;
239+
final Future<InetSocketAddress> resolveFuture = addressResolverGroup
240+
.getResolver(ctx.eventLoop().withoutContext())
241+
.resolve(createUnresolvedAddressForRefreshing(proxyAddress));
227242

228-
// use proxy information in context if available
229-
final ServiceRequestContext serviceCtx = ServiceRequestContext.currentOrNull();
230-
if (serviceCtx != null) {
231-
final ProxiedAddresses proxiedAddresses = serviceCtx.proxiedAddresses();
232-
return ProxyConfig.haproxy(proxyAddress, proxiedAddresses.sourceAddress());
233-
}
243+
resolveFuture.addListener(future -> {
244+
if (future.isSuccess()) {
245+
final InetSocketAddress resolvedAddress = (InetSocketAddress) future.getNow();
246+
final ProxyConfig newProxyConfig = proxyConfig.withProxyAddress(resolvedAddress);
247+
onComplete.accept(newProxyConfig, null);
248+
} else {
249+
final Throwable cause = future.cause();
250+
onComplete.accept(null, cause);
251+
}
252+
});
253+
} else {
254+
onComplete.accept(proxyConfig, null);
234255
}
256+
}
235257

258+
private static ProxyConfig maybeSetHAProxySourceAddress(ProxyConfig proxyConfig) {
259+
if (proxyConfig.proxyType() != ProxyType.HAPROXY) {
260+
return proxyConfig;
261+
}
262+
if (((HAProxyConfig) proxyConfig).sourceAddress() != null) {
263+
return proxyConfig;
264+
}
265+
266+
final ServiceRequestContext sctx = ServiceRequestContext.currentOrNull();
267+
final ProxiedAddresses serviceProxiedAddresses = sctx == null ? null : sctx.proxiedAddresses();
268+
if (serviceProxiedAddresses != null) {
269+
// A special behavior for haproxy when sourceAddress is null.
270+
// Use proxy information in the service context if available.
271+
final InetSocketAddress proxyAddress = proxyConfig.proxyAddress();
272+
assert proxyAddress != null;
273+
return ProxyConfig.haproxy(proxyAddress, serviceProxiedAddresses.sourceAddress());
274+
}
236275
return proxyConfig;
237276
}
238277

@@ -253,11 +292,24 @@ private static HttpResponse earlyFailedResponse(Throwable t, ClientRequestContex
253292
return HttpResponse.ofFailure(cause);
254293
}
255294

295+
private static HttpResponse earlyFailedResponse(Throwable t,
296+
ClientRequestContext ctx,
297+
DecodedHttpResponse response) {
298+
final UnprocessedRequestException cause = UnprocessedRequestException.of(t);
299+
ctx.cancel(cause);
300+
response.close(cause);
301+
return response;
302+
}
303+
256304
private static void doExecute(PooledChannel pooledChannel, ClientRequestContext ctx,
257305
HttpRequest req, DecodedHttpResponse res) {
258306
final Channel channel = pooledChannel.get();
259307
final HttpSession session = HttpSession.get(channel);
260308
res.init(session.inboundTrafficController());
261309
session.invoke(pooledChannel, ctx, req, res);
262310
}
311+
312+
private static InetSocketAddress createUnresolvedAddressForRefreshing(InetSocketAddress previousAddress) {
313+
return InetSocketAddress.createUnresolved(previousAddress.getHostString(), previousAddress.getPort());
314+
}
263315
}

core/src/main/java/com/linecorp/armeria/client/proxy/ConnectProxyConfig.java

+6
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ public ProxyType proxyType() {
9090
return ProxyType.CONNECT;
9191
}
9292

93+
@Override
94+
public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) {
95+
return new ConnectProxyConfig(newProxyAddress, this.username,
96+
this.password, this.headers, this.useTls);
97+
}
98+
9399
@Override
94100
public boolean equals(@Nullable Object o) {
95101
if (this == o) {

core/src/main/java/com/linecorp/armeria/client/proxy/DirectProxyConfig.java

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ public InetSocketAddress proxyAddress() {
4040
return null;
4141
}
4242

43+
@Override
44+
public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) {
45+
throw new UnsupportedOperationException(
46+
"A proxy address can't be set to DirectProxyConfig.");
47+
}
48+
4349
@Override
4450
public String toString() {
4551
return "DirectProxyConfig{proxyType=DIRECT}";

core/src/main/java/com/linecorp/armeria/client/proxy/HAProxyConfig.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.linecorp.armeria.client.proxy;
1818

1919
import static com.google.common.base.Preconditions.checkArgument;
20+
import static java.util.Objects.requireNonNull;
2021

2122
import java.net.InetSocketAddress;
2223
import java.util.Objects;
@@ -42,8 +43,11 @@ public final class HAProxyConfig extends ProxyConfig {
4243
}
4344

4445
HAProxyConfig(InetSocketAddress proxyAddress, InetSocketAddress sourceAddress) {
45-
checkArgument(sourceAddress.getAddress().getClass() == proxyAddress.getAddress().getClass(),
46-
"sourceAddress and proxyAddress should be the same type");
46+
// If proxyAddress is unresolved, getAddress() will return null.
47+
if (proxyAddress.getAddress() != null) {
48+
checkArgument(sourceAddress.getAddress().getClass() == proxyAddress.getAddress().getClass(),
49+
"sourceAddress and proxyAddress should be the same type");
50+
}
4751
this.proxyAddress = proxyAddress;
4852
this.sourceAddress = sourceAddress;
4953
}
@@ -67,6 +71,13 @@ public InetSocketAddress sourceAddress() {
6771
return sourceAddress;
6872
}
6973

74+
@Override
75+
public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) {
76+
requireNonNull(newProxyAddress, "newProxyAddress");
77+
return this.sourceAddress == null ? new HAProxyConfig(newProxyAddress)
78+
: new HAProxyConfig(newProxyAddress, this.sourceAddress);
79+
}
80+
7081
@Override
7182
public boolean equals(@Nullable Object o) {
7283
if (this == o) {

core/src/main/java/com/linecorp/armeria/client/proxy/ProxyConfig.java

+6-10
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public abstract class ProxyConfig {
4040
*/
4141
public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress) {
4242
requireNonNull(proxyAddress, "proxyAddress");
43-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
4443
return new Socks4ProxyConfig(proxyAddress, null);
4544
}
4645

@@ -52,7 +51,6 @@ public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress) {
5251
*/
5352
public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress, String username) {
5453
requireNonNull(proxyAddress, "proxyAddress");
55-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
5654
return new Socks4ProxyConfig(proxyAddress, requireNonNull(username, "username"));
5755
}
5856

@@ -63,7 +61,6 @@ public static Socks4ProxyConfig socks4(InetSocketAddress proxyAddress, String us
6361
*/
6462
public static Socks5ProxyConfig socks5(InetSocketAddress proxyAddress) {
6563
requireNonNull(proxyAddress, "proxyAddress");
66-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
6764
return new Socks5ProxyConfig(proxyAddress, null, null);
6865
}
6966

@@ -77,7 +74,6 @@ public static Socks5ProxyConfig socks5(InetSocketAddress proxyAddress) {
7774
public static Socks5ProxyConfig socks5(
7875
InetSocketAddress proxyAddress, String username, String password) {
7976
requireNonNull(proxyAddress, "proxyAddress");
80-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
8177
return new Socks5ProxyConfig(proxyAddress, requireNonNull(username, "username"),
8278
requireNonNull(password, "password"));
8379
}
@@ -89,7 +85,6 @@ public static Socks5ProxyConfig socks5(
8985
*/
9086
public static ConnectProxyConfig connect(InetSocketAddress proxyAddress) {
9187
requireNonNull(proxyAddress, "proxyAddress");
92-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
9388
return new ConnectProxyConfig(proxyAddress, null, null, HttpHeaders.of(), false);
9489
}
9590

@@ -101,7 +96,6 @@ public static ConnectProxyConfig connect(InetSocketAddress proxyAddress) {
10196
*/
10297
public static ConnectProxyConfig connect(InetSocketAddress proxyAddress, boolean useTls) {
10398
requireNonNull(proxyAddress, "proxyAddress");
104-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
10599
return new ConnectProxyConfig(proxyAddress, null, null, HttpHeaders.of(), useTls);
106100
}
107101

@@ -129,7 +123,6 @@ public static ConnectProxyConfig connect(
129123
public static ConnectProxyConfig connect(
130124
InetSocketAddress proxyAddress, HttpHeaders headers, boolean useTls) {
131125
requireNonNull(proxyAddress, "proxyAddress");
132-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
133126
return new ConnectProxyConfig(proxyAddress, null, null, headers, useTls);
134127
}
135128

@@ -146,7 +139,6 @@ public static ConnectProxyConfig connect(
146139
public static ConnectProxyConfig connect(InetSocketAddress proxyAddress, String username, String password,
147140
HttpHeaders headers, boolean useTls) {
148141
requireNonNull(proxyAddress, "proxyAddress");
149-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
150142
requireNonNull(username, "username");
151143
requireNonNull(password, "password");
152144
requireNonNull(headers, "headers");
@@ -162,7 +154,6 @@ public static ConnectProxyConfig connect(InetSocketAddress proxyAddress, String
162154
public static HAProxyConfig haproxy(
163155
InetSocketAddress proxyAddress, InetSocketAddress sourceAddress) {
164156
requireNonNull(proxyAddress, "proxyAddress");
165-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
166157
requireNonNull(sourceAddress, "sourceAddress");
167158
checkArgument(!sourceAddress.isUnresolved(), "sourceAddress must be resolved");
168159
return new HAProxyConfig(proxyAddress, sourceAddress);
@@ -176,7 +167,6 @@ public static HAProxyConfig haproxy(
176167
*/
177168
public static ProxyConfig haproxy(InetSocketAddress proxyAddress) {
178169
requireNonNull(proxyAddress, "proxyAddress");
179-
checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");
180170
return new HAProxyConfig(proxyAddress);
181171
}
182172

@@ -201,6 +191,12 @@ public static ProxyConfig direct() {
201191
@Nullable
202192
public abstract InetSocketAddress proxyAddress();
203193

194+
/**
195+
* Returns a new proxy address instance that respects DNS TTL.
196+
* @param newProxyAddress the inet socket address
197+
*/
198+
public abstract ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress);
199+
204200
@Nullable
205201
static String maskPassword(@Nullable String username, @Nullable String password) {
206202
return username != null ? "****" : null;

core/src/main/java/com/linecorp/armeria/client/proxy/Socks4ProxyConfig.java

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public ProxyType proxyType() {
5656
return ProxyType.SOCKS4;
5757
}
5858

59+
@Override
60+
public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) {
61+
return new Socks4ProxyConfig(newProxyAddress, this.username);
62+
}
63+
5964
@Override
6065
public boolean equals(@Nullable Object o) {
6166
if (this == o) {

core/src/main/java/com/linecorp/armeria/client/proxy/Socks5ProxyConfig.java

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public ProxyType proxyType() {
6969
return ProxyType.SOCKS5;
7070
}
7171

72+
@Override
73+
public ProxyConfig withProxyAddress(InetSocketAddress newProxyAddress) {
74+
return new Socks5ProxyConfig(newProxyAddress, this.username, this.password);
75+
}
76+
7277
@Override
7378
public boolean equals(@Nullable Object o) {
7479
if (this == o) {

core/src/main/java/com/linecorp/armeria/internal/common/util/IpAddrUtil.java

+13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package com.linecorp.armeria.internal.common.util;
1717

18+
import java.net.InetAddress;
19+
import java.net.InetSocketAddress;
20+
1821
import com.linecorp.armeria.common.annotation.Nullable;
1922

2023
import io.netty.util.NetUtil;
@@ -36,5 +39,15 @@ public static String normalize(@Nullable String ipAddr) {
3639
return NetUtil.bytesToIpAddress(array);
3740
}
3841

42+
public static boolean isCreatedWithIpAddressOnly(InetSocketAddress socketAddress) {
43+
if (socketAddress.isUnresolved()) {
44+
return false;
45+
}
46+
47+
final InetAddress inetAddress = socketAddress.getAddress();
48+
// If hostname and host address are the same, it was created with an IP address
49+
return socketAddress.getHostString().equals(inetAddress.getHostAddress());
50+
}
51+
3952
private IpAddrUtil() {}
4053
}

0 commit comments

Comments
 (0)