Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect TTL of a DNS record for proxy config #5960

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
417bf3b
Client respects DNS TTL.
chickenchickenlove Oct 27, 2024
3681f0f
Merge branch 'main' into 241027-proxy-config
chickenchickenlove Oct 27, 2024
f57259c
Revert "Client respects DNS TTL."
chickenchickenlove Oct 30, 2024
f40cbe4
Use RefreshingAddressResolver to refresh proxy address.
chickenchickenlove Oct 30, 2024
a26e3f5
Merge branch '241027-proxy-config' of github.com-ojt90902:chickenchic…
chickenchickenlove Oct 30, 2024
649469e
Handle null of proxyconfig.
chickenchickenlove Oct 30, 2024
ab43277
Add a copmment.
chickenchickenlove Oct 30, 2024
a6e725c
Add withNewProxyAddress() to create after refresh DNS.
chickenchickenlove Nov 2, 2024
e0819c4
Resolve Proxy DNS to respect DNS TTL asynchronously.
chickenchickenlove Nov 3, 2024
0b3cc2b
Remove useless method from ProxyConfig.
chickenchickenlove Nov 3, 2024
54ec6f3
Fixes lint error.
chickenchickenlove Nov 3, 2024
a67ee3f
Revert previous commit.
chickenchickenlove Nov 3, 2024
241b4ff
Remove useless log codes.
chickenchickenlove Nov 4, 2024
fce4fd2
Remove unused import.
chickenchickenlove Nov 4, 2024
7d85a69
Fixes lint error.
chickenchickenlove Nov 4, 2024
67f4997
Add integration test for DNS refreshing.
chickenchickenlove Nov 4, 2024
e775351
Remove deprecated test case.
chickenchickenlove Nov 4, 2024
18b24ea
Apply reviews.
chickenchickenlove Nov 11, 2024
979e6fd
Add a private method to refresh proxy DNS.
chickenchickenlove Nov 17, 2024
3dbc847
Skip refreshing ProxyConfig DNS when it is configured with a direct IP.
chickenchickenlove Nov 20, 2024
fcfed45
clean up
ikhoon Nov 21, 2024
09efa86
add more tests
ikhoon Nov 21, 2024
25ff1af
Fixes broken test codes and add new test cases.
chickenchickenlove Nov 21, 2024
43212b6
Remove useless logging.
chickenchickenlove Nov 22, 2024
195033f
Add null condition.
chickenchickenlove Nov 22, 2024
d1c9596
Update core/src/main/java/com/linecorp/armeria/client/HttpClientDeleg…
chickenchickenlove Nov 28, 2024
d5c8326
Update core/src/main/java/com/linecorp/armeria/client/proxy/HAProxyCo…
chickenchickenlove Nov 28, 2024
1240a24
Remove unused import
chickenchickenlove Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
import java.net.InetSocketAddress;
import java.util.function.BiConsumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.linecorp.armeria.client.HttpChannelPool.PoolKey;
import com.linecorp.armeria.client.endpoint.EmptyEndpointGroupException;
import com.linecorp.armeria.client.proxy.HAProxyConfig;
Expand Down Expand Up @@ -50,6 +53,7 @@

final class HttpClientDelegate implements HttpClient {

private static final Logger logger = LoggerFactory.getLogger(HttpClientDelegate.class);
private final HttpClientFactory factory;
private final AddressResolverGroup<InetSocketAddress> addressResolverGroup;

Expand Down Expand Up @@ -88,24 +92,37 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex
}

final SessionProtocol protocol = ctx.sessionProtocol();
final ProxyConfig proxyConfig;

final Endpoint endpointWithPort = endpoint.withDefaultPort(ctx.sessionProtocol());
final EventLoop eventLoop = ctx.eventLoop().withoutContext();
// TODO(ikhoon) Use ctx.exchangeType() to create an optimized HttpResponse for non-streaming response.
final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop);
updateCancellationTask(ctx, req, res);

try {
proxyConfig = getProxyConfig(protocol, endpoint);
resolveProxyConfig(protocol, endpoint, ctx, (proxyConfig, thrown) -> {
if (thrown != null) {
earlyFailed(thrown, ctx);
res.close(thrown);
} else {
execute0(ctx, endpointWithPort, req, res, proxyConfig);
chickenchickenlove marked this conversation as resolved.
Show resolved Hide resolved
}
});
} catch (Throwable t) {
return earlyFailedResponse(t, ctx);
}
return res;
}

private void execute0(ClientRequestContext ctx, Endpoint endpointWithPort, HttpRequest req,
DecodedHttpResponse res, ProxyConfig proxyConfig) {
final Throwable cancellationCause = ctx.cancellationCause();
if (cancellationCause != null) {
return earlyFailedResponse(cancellationCause, ctx);
final Throwable t = earlyFailed(cancellationCause, ctx);
res.close(t);
return;
}

final Endpoint endpointWithPort = endpoint.withDefaultPort(ctx.sessionProtocol());
final EventLoop eventLoop = ctx.eventLoop().withoutContext();
// TODO(ikhoon) Use ctx.exchangeType() to create an optimized HttpResponse for non-streaming response.
final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop);
updateCancellationTask(ctx, req, res);

final ClientConnectionTimingsBuilder timingsBuilder = ClientConnectionTimings.builder();

if (endpointWithPort.hasIpAddr() ||
Expand All @@ -125,8 +142,6 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex
}
});
}

return res;
}

private static void updateCancellationTask(ClientRequestContext ctx, HttpRequest req,
Expand Down Expand Up @@ -215,24 +230,58 @@ private void acquireConnectionAndExecute0(ClientRequestContext ctx, Endpoint end
}
}

private ProxyConfig getProxyConfig(SessionProtocol protocol, Endpoint endpoint) {
private void resolveProxyConfig(SessionProtocol protocol, Endpoint endpoint, ClientRequestContext ctx,
BiConsumer<@Nullable ProxyConfig, @Nullable Throwable> onComplete) {
final ProxyConfig proxyConfig = factory.proxyConfigSelector().select(protocol, endpoint);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be efficient if we could skip the resolution process if proxyConfig.proxyAddress() was created with IP addresses.

requireNonNull(proxyConfig, "proxyConfig");

final ServiceRequestContext serviceCtx = ServiceRequestContext.currentOrNull();
final ProxiedAddresses capturedProxiedAddresses = serviceCtx == null ? null
: serviceCtx.proxiedAddresses();
ikhoon marked this conversation as resolved.
Show resolved Hide resolved

// DirectProxyConfig does not have proxyAddress as its field.
if (proxyConfig.proxyAddress() != null) {
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
final Future<InetSocketAddress> resolveFuture = addressResolverGroup
.getResolver(ctx.eventLoop().withoutContext())
.resolve(proxyConfig.proxyAddress());

resolveFuture.addListener(future -> {
if (future.isSuccess()) {
final InetSocketAddress resolvedAddress = (InetSocketAddress) future.getNow();
if (resolvedAddress != null && !resolvedAddress.isUnresolved()) {
final ProxyConfig newProxyConfig = proxyConfig.withNewProxyAddress(resolvedAddress);
onComplete.accept(maybeHAProxy(newProxyConfig, capturedProxiedAddresses), null);
} else {
logger.warn("Resolved address is invalid or unresolved: {}. " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this case - are you concerned that the resolution succeeds but the InetAddress does not exist?

Would it make sense to just align error handling with dns resolution for the normal code path (not using proxy)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comments!
After take a look RefreshingAddressResolver, RefreshingAddressResolver returns InetSocketAddress when succeed.
As you mentioned before, it is unnecessary codes.
Therefore, I delete this codes.

"Using the previous address instead.", resolvedAddress);
onComplete.accept(maybeHAProxy(proxyConfig, capturedProxiedAddresses), null);
}
} else {
final Throwable cause = future.cause();
logger.warn("Failed to refresh {}'s ip address. " +
"Using the previous address instead. reason: {}",
proxyConfig.proxyAddress(),
cause != null ? cause.getMessage() : "Unknown Error");
onComplete.accept(maybeHAProxy(proxyConfig, capturedProxiedAddresses), null);
chickenchickenlove marked this conversation as resolved.
Show resolved Hide resolved
}
});
} else {
onComplete.accept(maybeHAProxy(proxyConfig, capturedProxiedAddresses), null);
}
}

private ProxyConfig maybeHAProxy(ProxyConfig proxyConfig, @Nullable ProxiedAddresses proxiedAddresses) {
// special behavior for haproxy when sourceAddress is null
if (proxyConfig.proxyType() == ProxyType.HAPROXY &&
((HAProxyConfig) proxyConfig).sourceAddress() == null) {
final InetSocketAddress proxyAddress = proxyConfig.proxyAddress();
assert proxyAddress != null;

// use proxy information in context if available
final ServiceRequestContext serviceCtx = ServiceRequestContext.currentOrNull();
if (serviceCtx != null) {
final ProxiedAddresses proxiedAddresses = serviceCtx.proxiedAddresses();
if (proxiedAddresses != null) {
return ProxyConfig.haproxy(proxyAddress, proxiedAddresses.sourceAddress());
}
}

return proxyConfig;
}

Expand All @@ -247,9 +296,14 @@ private static void logSession(ClientRequestContext ctx, @Nullable PooledChannel
}
}

private static HttpResponse earlyFailedResponse(Throwable t, ClientRequestContext ctx) {
private static Throwable earlyFailed(Throwable t, ClientRequestContext ctx) {
chickenchickenlove marked this conversation as resolved.
Show resolved Hide resolved
final UnprocessedRequestException cause = UnprocessedRequestException.of(t);
ctx.cancel(cause);
return cause;
}

private static HttpResponse earlyFailedResponse(Throwable t, ClientRequestContext ctx) {
final Throwable cause = earlyFailed(t, ctx);
return HttpResponse.ofFailure(cause);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public ProxyType proxyType() {
return ProxyType.CONNECT;
}

@Override
public ProxyConfig withNewProxyAddress(InetSocketAddress newProxyAddress) {
return new ConnectProxyConfig(newProxyAddress, this.username,
this.password, this.headers, this.useTls);
}

@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public InetSocketAddress proxyAddress() {
return null;
}

@Override
public ProxyConfig withNewProxyAddress(InetSocketAddress newProxyAddress) {
return new DirectProxyConfig();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; it may make more sense to throw an exception instead

}

@Override
public String toString() {
return "DirectProxyConfig{proxyType=DIRECT}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ public InetSocketAddress sourceAddress() {
return sourceAddress;
}

@Override
public ProxyConfig withNewProxyAddress(InetSocketAddress newProxyAddress) {
return this.sourceAddress == null ? new HAProxyConfig(proxyAddress)
: new HAProxyConfig(proxyAddress, this.sourceAddress);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input is ignored.

Suggested change
return this.sourceAddress == null ? new HAProxyConfig(proxyAddress)
: new HAProxyConfig(proxyAddress, this.sourceAddress);
requireNonNull(newProxyAddress, "newProxyAddress");
return this.sourceAddress == null ? new HAProxyConfig(newProxyAddress)
: new HAProxyConfig(newProxyAddress, this.sourceAddress);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment wasn't addressed. Was it intended? Let me know if I'm missing something or misunderstood.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really sorry about this. 🙇‍♂️
I missed it.
Now, I fixed it!

}

@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ public static ProxyConfig direct() {
@Nullable
public abstract InetSocketAddress proxyAddress();

/**
* Returns a new proxy address instance that respects DNS TTL.
* @param newProxyAddress the inet socket address
*/
public abstract ProxyConfig withNewProxyAddress(InetSocketAddress newProxyAddress);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; I believe that currently only resolved InetSocketAddress can be used to create a ProxyConfig.
Can you add some integration tests to confirm the current code works correctly?

Copy link
Contributor Author

@chickenchickenlove chickenchickenlove Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrhee17 Thanks for your comments. 🙇‍♂️
I added two integration test codes for this feature.
Due to restrictions on mocking, narrow Access specifier, I couldn't use a spy instance to validate it or inject DNSResolver as well.
That's why I made a new class called DNSResolverFacadeUtils class 😅 .

As @minwoox mentioned, we don't need to check if InetSocketAddress is resolved, since it will be resolved when HttpClientDelegate calls execute(). After removing checkArgument(...), I was finally able to write some integration test cases... 😅

When you have time, please take another look. 🙇‍♂️

chickenchickenlove marked this conversation as resolved.
Show resolved Hide resolved

@Nullable
static String maskPassword(@Nullable String username, @Nullable String password) {
return username != null ? "****" : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public ProxyType proxyType() {
return ProxyType.SOCKS4;
}

@Override
public ProxyConfig withNewProxyAddress(InetSocketAddress newProxyAddress) {
return new Socks4ProxyConfig(newProxyAddress, this.username);
}

@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public ProxyType proxyType() {
return ProxyType.SOCKS5;
}

@Override
public ProxyConfig withNewProxyAddress(InetSocketAddress newProxyAddress) {
return new Socks5ProxyConfig(newProxyAddress, this.username, this.password);
}

@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
Expand Down
Loading