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

fix DoH error when using ip address as hostname #7073

Merged
merged 10 commits into from
Feb 16, 2022
Merged

fix DoH error when using ip address as hostname #7073

merged 10 commits into from
Feb 16, 2022

Conversation

TaoZang
Copy link
Contributor

@TaoZang TaoZang commented Feb 11, 2022

Fix the issue that when network trying to do a DNS lookup with an IP numeric hostname, the DnsOverHttps will throw an UnknownHostException instead of returning the InetAddress with this IP address.
For example, If user runs a http proxy, the DnsOverHttps module will try to lookup the local Proxy address such as 127.0.0.x.

@@ -67,7 +67,11 @@ class DnsOverHttps internal constructor(
}
}

return lookupHttps(hostname)
return try {
listOf(InetAddressUtil.forString(hostname))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a test?

We need to avoid an exception in most calls here. Maybe reverse this logic (try hostname first) and in the normal case call lookupHttps, and catch and exception, check if it's a ip address and then return forString.

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 think it would be better if we try converting by forString first because we can avoid a potential https connection if the hostname is an numerical IP address.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok, well avoid the exception then. 99+% of calls will be for hostnames, not IPs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If most of the calls will be hostname, maybe call the lookupHttps first would be better. I will update the code.

import java.net.UnknownHostException
import java.util.*

object InetAddressUtil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like a lot of supporting code. Are there existing methods that do this, or would a simple regex as a first parse be simpler (\d+.\d+...) or (\d+:...)?

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 implement the InetAddressUtil kotlin version refer to the com.google.common.net.InetAddresses class. I just concern about that a simple regex may cause some bugs due to the complex format of IPv4/IPv6. A common parse algorithm would be better.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If it's sourced from there, you can just commit it here with a new copyright. In other places we've derived new code from other projects but keep that copyright. Let me look into this tonight, there may be simpler ways to do this.

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 also updated the copyright just same as the raw implement file.

@yschimke
Copy link
Collaborator

I'll backport this to 4.9.x when we have a fix, but until that happens you might want to have your own Dns wrapper that does this logic yourself.

Thanks for the report and PR.

@TaoZang
Copy link
Contributor Author

TaoZang commented Feb 11, 2022

I'll backport this to 4.9.x when we have a fix, but until that happens you might want to have your own Dns wrapper that does this logic yourself.

Thanks for the report and PR.

Thank you for your review:)

@yschimke
Copy link
Collaborator

yschimke commented Feb 12, 2022

Looking at the logic of InetAddress.getAllByName, would the following be simpler?

  override fun lookup(hostname: String): List<InetAddress> {
    if (isIpAddress(hostname)) {
      return listOf(InetAddress.getByName(hostname))
    }
    ...
  private fun isIpAddress(hostname: String): Boolean {
    return hostname[0].digitToIntOrNull(16) != null
      || hostname[0] == ':' || hostname[0] == '['
  }

getAllByName shortcircuits and avoids Dns if the hostname is an ip address. Using similar logic in isIpAddress.

@yschimke
Copy link
Collaborator

Please add some tests to DnsOverHttpsTest.

@TaoZang
Copy link
Contributor Author

TaoZang commented Feb 12, 2022

Looking at the logic of InetAddress.getAllByName, would the following be simpler?

  override fun lookup(hostname: String): List<InetAddress> {
    if (isIpAddress(hostname)) {
      return listOf(InetAddress.getByName(hostname))
    }
    ...
  private fun isIpAddress(hostname: String): Boolean {
    return hostname[0].digitToIntOrNull(16) != null
      || hostname[0] == ':' || hostname[0] == '['
  }

getAllByName shortcircuits and avoids Dns if the hostname is an ip address. Using similar logic in isIpAddress.

It seems that the getAllByName function also needs some support functions such as textToNumericFormatV4/textToNumericFormatV6 from sun.net.util.IPAddressUtil class, I thought it might be btter to avoid import a class from sun.net package here.

@TaoZang
Copy link
Contributor Author

TaoZang commented Feb 12, 2022

Please add some tests to DnsOverHttpsTest.

I just added some tests in DnsOverHttpsText.

@yschimke
Copy link
Collaborator

I didn't mean import it, I just meant the method I showed or similar.

@TaoZang
Copy link
Contributor Author

TaoZang commented Feb 14, 2022

I just removed the complex support class.
Because the getAllName will shortcircuits and directly convert the hostname to InetAddress, so we only check the failed looking-up hostname when it is possible a valid IP address and convert it to InetAddress by getAllName.

Copy link
Member

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

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

I’m unconvinced that this is the right layer for this fix. I think it’d be better if we never call Dns.lookup() when we have an IP address.

}
}

private fun checkIsIpAddress(hostname: String): Boolean {
Copy link
Member

Choose a reason for hiding this comment

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

Should use our existing function, canParseAsIpAddress() from okhttp3.internal. We definitely don’t want to functions to convert IP addresses to strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I missed that there is a existing function.

return true
}

if (hostname[0].digitToIntOrNull(16) != null || hostname[0] == ':' || hostname[0] == '[') {
Copy link
Member

Choose a reason for hiding this comment

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

If the hostname is [, that’s malformed. We do not pass the square braces to Dns.

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 thought that the getAllByName will strip the square braces so I just pass the string to getAllByName directly.
Due to the canParseAsIpAddress will not handle [, I will strip the square braces and use the existing one to check if the hostname is a IP address.

} else {
hostname
}
if (address.canParseAsIpAddress()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice find, I forgot this was a thing. Can you invert this now?

Also if that function doesn't worry about [], we can probably skip that here also.

See

assertThat(parse("http://[::1]/").host()).isEqualTo("::1");

Which confirms the HttpUrl strips the [].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean that the HttpUrl will handle the square braces before calling dns lookup, so we don't need to check and strips the [] here and just remove the test case for hostname "[::1]"?

@swankjesse
Copy link
Member

At a risk of repeating myself, what happens if we never call any Dns implementation when the input is already an IP address?

@yschimke
Copy link
Collaborator

At a risk of repeating myself, what happens if we never call any Dns implementation when the input is already an IP address?

Yep - this makes sense. @TaoZang sorry to give you bad advice, and thanks for you patience. What do you think about make this change to avoid using Dns for IP Addresses.

@TaoZang
Copy link
Contributor Author

TaoZang commented Feb 15, 2022

Yep - this makes sense. @TaoZang sorry to give you bad advice, and thanks for you patience. What do you think about make this change to avoid using Dns for IP Addresses.

At a risk of repeating myself, what happens if we never call any Dns implementation when the input is already an IP address?

Yeah, maybe I just misunderstood your meaning before. Avoid to call DNS lookup in higher layer when the input is already an IP addres seems be make more sense. I will try to change the fix by this way.

val addresses = if (socketHost.canParseAsIpAddress()) {
listOf(InetAddress.getByName(socketHost))
} else {
eventListener.dnsStart(call, socketHost)
Copy link
Collaborator

@yschimke yschimke Feb 16, 2022

Choose a reason for hiding this comment

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

This seems like a nice benefit of this change. that DNS start event isn't emitted for an IP address.

@@ -53,6 +53,7 @@ class DnsOverHttps internal constructor(
@get:JvmName("resolvePrivateAddresses") val resolvePrivateAddresses: Boolean,
@get:JvmName("resolvePublicAddresses") val resolvePublicAddresses: Boolean
) : Dns {

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Can you revert this line.

Copy link
Collaborator

@yschimke yschimke left a comment

Choose a reason for hiding this comment

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

Please add a test that confirms this behaviour. Something in EventListenerTest like



  @Test public void successfulCallEventSequenceForIpAddress() throws IOException {
    server.enqueue(new MockResponse()
      .setBody("abc"));

    String ipAddress = InetAddress.getLocalHost().getHostAddress();

    Call call = client.newCall(new Request.Builder()
      .url(server.url("/").newBuilder().host(ipAddress).build())
      .build());
    Response response = call.execute();
    assertThat(response.code()).isEqualTo(200);
    assertThat(response.body().string()).isEqualTo("abc");
    response.body().close();

    assertThat(listener.recordedEventTypes()).containsExactly("CallStart",
      "ProxySelectStart", "ProxySelectEnd",
      "ConnectStart", "ConnectEnd", "ConnectionAcquired", "RequestHeadersStart",
      "RequestHeadersEnd", "ResponseHeadersStart", "ResponseHeadersEnd", "ResponseBodyStart",
      "ResponseBodyEnd", "ConnectionReleased", "CallEnd");
  }

@TaoZang
Copy link
Contributor Author

TaoZang commented Feb 16, 2022

Please add a test that confirms this behaviour. Something in EventListenerTest like



  @Test public void successfulCallEventSequenceForIpAddress() throws IOException {
    server.enqueue(new MockResponse()
      .setBody("abc"));

    String ipAddress = InetAddress.getLocalHost().getHostAddress();

    Call call = client.newCall(new Request.Builder()
      .url(server.url("/").newBuilder().host(ipAddress).build())
      .build());
    Response response = call.execute();
    assertThat(response.code()).isEqualTo(200);
    assertThat(response.body().string()).isEqualTo("abc");
    response.body().close();

    assertThat(listener.recordedEventTypes()).containsExactly("CallStart",
      "ProxySelectStart", "ProxySelectEnd",
      "ConnectStart", "ConnectEnd", "ConnectionAcquired", "RequestHeadersStart",
      "RequestHeadersEnd", "ResponseHeadersStart", "ResponseHeadersEnd", "ResponseBodyStart",
      "ResponseBodyEnd", "ConnectionReleased", "CallEnd");
  }

Sure, it seems a reasonable test case to cover this change.

Copy link
Member

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

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

Great PR, Great Test. Thank you!

@JoshuaJamesOng
Copy link

I'll backport this to 4.9.x when we have a fix, but until that happens you might want to have your own Dns wrapper that does this logic yourself.

Thanks for the report and PR.

@yschimke I see this change was accepted into the 5.x alphas. Are there any plans to backport this to 4.x, now?

yschimke pushed a commit to yschimke/okhttp that referenced this pull request Jan 11, 2023
@yschimke
Copy link
Collaborator

Trying on #7648

yschimke added a commit that referenced this pull request Jan 18, 2023
…7648)

* Cherrypick: fix DoH error when using ip address as hostname (#7073)

(cherry picked from commit 631a29e)

Co-authored-by: Tao.Zang <[email protected]>
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants