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 25 commits into
base: main
Choose a base branch
from

Conversation

chickenchickenlove
Copy link
Contributor

@chickenchickenlove chickenchickenlove commented Oct 27, 2024

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:

@minwoox
Copy link
Member

minwoox commented Oct 30, 2024

Hi, @chickenchickenlove! Thanks for submitting this PR.

There is a limitation in the current patch:

  • It still does not respect TTL.
  • Users would need to carefully consider and set TTL values appropriate to each use case.

To address this, we need to ensure TTL is respected and refresh the address once TTL expires.
Luckily, we already have the implementation for this: the RefreshingAddressResolver. The resolver is created internally in a ClientFactory so we all have to do is make use of it. We can integrate it here as follows:

final ProxyConfig proxyConfig = factory.proxyConfigSelector().select(protocol, endpoint);
final Future<InetSocketAddress> resolveFuture = addressResolverGroup.getResolver(
        ctx.eventLoop().withoutContext()).resolve(proxyConfig.proxyAddress());
if (resolveFuture.isSuccess()) {
    ...
} else {
    resolveFuture.addListener(...);
}

Because we resolve the address internally, we can now accept unresolved addresses as well:

checkArgument(!proxyAddress.isUnresolved(), "proxyAddress must be resolved");

Please, let me know if you need any further information. 😉

@minwoox minwoox added the defect label Oct 30, 2024
@@ -215,10 +219,34 @@ private void acquireConnectionAndExecute0(ClientRequestContext ctx, Endpoint end
}
}

private ProxyConfig getProxyConfig(SessionProtocol protocol, Endpoint endpoint) {
private ProxyConfig getProxyConfig(SessionProtocol protocol, Endpoint endpoint, ClientRequestContext ctx) {
Copy link
Member

Choose a reason for hiding this comment

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

We have to recreate ProxyConfig with the new proxy address because the address is resolved:

if (proxyConfig.proxyAddress() != null) {
    final Future<InetSocketAddress> resolveFuture = addressResolverGroup.getResolver(
            ctx.eventLoop().withoutContext()).resolve(proxyConfig.proxyAddress());

    resolveFuture.addListener(future -> {
        if (future.isSuccess()) {
            final ProxyConfig newProxyConfig = proxyConfig.withNewProxyAddress(
                    (InetSocketAddress) future.get());
            ...
        }
    }
}

Because getting the ProxyConfig is now asynchronous, we have to change the signature of this method.
We have two options:

  • Returning CompletableFuture<ProxyConfig> instead of ProxyConfig
    // in HttpClientDelegate.execute()
    try {
        final CompletableFuture<ProxyConfig> future = getProxyConfig(protocol, endpoint, ctx);
        future.handle(....)
    } catch (Throwable t) {
        return earlyFailedResponse(t, ctx);
    }
  • Using a callback that is executed after resolution like we do for resolving address
    private void resolveProxyConfig(..., 
        BiConsumer<@Nullable ProxyConig, @Nullable Throwable> onComplete) {
        final ProxyConfig proxyConfig = factory.proxyConfigSelector().select(protocol, endpoint);
          requireNonNull(proxyConfig, "proxyConfig");
    
          ...
    }

Please let me know if you need further information. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@minwoox , Thanks for your comments 🙇‍♂️
I made a new commit with following second option!

Because ProxyConfig will be resolved in other thread, so the method maybeHAProxy() will get capturedProxyAddress instead of ServiceRequestContext.
Also, res instance will be returned right away even if future don't completed.
So I used the method earlyFailed(...) instead of earlyfailedResponse(...).

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

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Looks good overall, left some minor comments 🙇

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.

@@ -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

* 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. 🙇‍♂️

@jrhee17 jrhee17 added this to the 1.32.0 milestone Nov 6, 2024
Comment on lines 72 to 73
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!

@@ -44,19 +44,6 @@ void testEqualityAndHashCode(ProxyConfig config) {
assertThat(equalProxyConfigs.get(0).hashCode()).isEqualTo(config.hashCode());
}

@Test
void testUnresolvedProxyAddress() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we revive this test and fix it to check if ProxyConfig is successfully created with unresolved addresses?

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 🙇‍♂️
Good point, I didn't expect it needed to be kept at all. 😓
I revived this test and fixed it!

@chickenchickenlove
Copy link
Contributor Author

@ikhoon nim, thanks for your comments! 🙇‍♂️
I made a new commit to apply your comments.
When you have time, please take another look!

@@ -215,24 +224,48 @@ 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.

Comment on lines 85 to 109
return Objects.hash(proxyAddress, sourceAddress);
return hash(proxyAddress, sourceAddress);
Copy link
Contributor

Choose a reason for hiding this comment

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

Revert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have been reverted!


final InetAddress inetAddress = socketAddress.getAddress();
// If hostname and host address are the same, it was created with an IP address
return socketAddress.getHostString().equals(inetAddress.getHostAddress());
Copy link
Contributor Author

@chickenchickenlove chickenchickenlove Nov 21, 2024

Choose a reason for hiding this comment

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

@ikhoon nim
I removed this code because of test fail.

        final InetAddress inetAddress = socketAddress.getAddress();
        if (inetAddress.isAnyLocalAddress()) {
            return false; // Wildcard address
        }

If IP is wildcard address, addressResolver start to resolve wildcard address like 0.0.0.0, ::.
In that case, The addressResolver try to resolve 0.0.0.0 and throws UnknownHostException.
The error message is Caused by: java.net.UnknownHostException: Failed to resolve '0.0.0.0.akadns.net.' [A(1)] after 2 queries.

Please refer to HAProxyClientIntegrationTest#testConnectionFailure(). It's failed test reason described above.

I think that we don't need to resolve either 0.0.0.0 or ::.
Also, I think 0.0.0.0 and :: are fixed IP address.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agreed. Thanks for fixing that.

@chickenchickenlove
Copy link
Contributor Author

chickenchickenlove commented Nov 21, 2024

@ikhoon nim, thanks for your clean up commit.
It's more readable 👍 👍 👍 .
By the way, I found unexpected failed two test cases.

The one is fixed.
The other one is fixed as well. However, to fix it, I make a change IpAddrUtil.isCreatedWithIpAddressOnly().
Because the AddressResolver attempts to resolve considering wildcard domains and even akamai CDN addresses.
I have updated the commit with my thoughts on this matter, so I would appreciate your feedback.

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

Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

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

checkArgument(sourceAddress.getAddress().getClass() == proxyAddress.getAddress().getClass(),
"sourceAddress and proxyAddress should be the same type");
} else {
logger.warn("Either the source or proxy address could not be resolved. " +
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think of removing this warning? This message doesn't seem useful, and I'm unsure if warn is a proper level.

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 agree. I removed it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Respect TTL of a DNS record for proxy config.
4 participants