square / okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM.
https://square.github.io/okhttp/
Apache License 2.0
45.82k stars 9.16k forks source link

UnknownHostException with IPv6 (known bug in system libraries?) #6903

Closed Stefan-Fritzsche closed 2 years ago

Stefan-Fritzsche commented 2 years ago

Disclaimer: This is actually not a bug in OkHttp, but a bug in the system libraries on smartphones/mobile devices. A workaround is possible in OkHttp. I considered this closer to a bug report than to a feature request.

Problem:

I run a private host with a DNS configured to return an IPv6-address (AAAA record) only, so no IPv4-address (A record). I had to do so, because my internet provider migrated me from an IPv4 connection to a "IPv4/IPv6 dual stack lite" (DS lite) connection. So although my host has a (provider-internal) IPv4 address, this address is not reachable form the internet. Only the IPv6 address is reachable.

It turned out, that some applications on smartphones fail to connect to that host. While all the browsers I tested connected properly, other applications like DAVx5 (formerly Davdroid) or Nextcloud app for Android showed a "UnknownHostException".

I did trace back the UnknownHostException in both apps to a call to InetAddress.getAllByName() in the Dns interface of OkHttp. I checked the DNS configuration via nslookup on a Laptop and with network tools for Android. Both show the correct IPv6 address.

The cause of the UnknownHostException from the call to InetAddress.getAllByName() seems to be a bug in the Android system libraries for DNS resolution. See oh-ipv6-where-art-thou:

Networking APIs, particularly in the area of host name lookups, are notoriously finicky and display subtly different behaviour on different platforms. We did find a few bugs caused by these incompatibilities:

  • getaddrinfo() on Android does not return IPv6 results when the ‘hints’ parameter is NULL, as required by POSIX. A hints parameter with the field ai_flags set to PF_UNSPEC is required to get both IPv4 and IPv6 results.

I can reproduce this bug with a simplistic Android app, which tries to resolve the host in two different ways:

  1. using the system library via InetAddress.getAllByName()
  2. using the DNSJava library via Address.getAllByName()

The system call throws a UnknownHostException while the DNSJava library returns the correct IPv6 address (private information replayced by '*'):

System - InetAddress.getAllByName(host)
java.net.UnknownHostException: Unable to resolve host "**********************": No address associated with hostname

DNSJava - Address.getAllByName(host):
count: 1
**********************/****:****:****:****:****:****:****:****

When switching frequently between WiFi and Mobile Internet, I sometimes also get a UnknownHostException from DNSJava library. I suspect that the DNSJava library may cache the DNS server to ask, and then fails after switching. But this is just a suspicion.

Workaround

In the end I did adapt the OkHttp library such that the default DNS lookup performs up to 3 steps:

  1. try the system library
  2. in case of UnknownHostException: try the DNSJava library using default lookup
  3. in case of UnknownHostException: try the DNSJava library explicitly using DNS server "8.8.8.8" and explicitly asking for A and AAAA records, without further filtering/sorting - see new lookupFrom():
  fun lookupFrom(hostname: String, server: String): List<InetAddress> {
    var simpleResolver = SimpleResolver(server);
    var addresses = listOf<InetAddress>()

    var Alookup = Lookup(hostname)
    Alookup.setResolver(simpleResolver)
    var Arecords = Alookup.run()
    if (Arecords != null)
      for (record in Arecords)
        if (record.getType() == Type.A)
          addresses += ((record as ARecord).getAddress())

    var AAAAlookup = Lookup(hostname, Type.AAAA)
    AAAAlookup.setResolver(simpleResolver)
    var AAAArecords = AAAAlookup.run()
    if (AAAArecords != null)
      for (record in AAAArecords)
        if (record.getType() == Type.AAAA)
          addresses += ((record as AAAARecord).getAddress())

    if (addresses.size < 1)
      throw UnknownHostException("Host '$hostname' unknown to '$server'")
    return addresses
  }

  override fun lookup(hostname: String): List<InetAddress> {
    try {
      try {
        return InetAddress.getAllByName(hostname).toList()  // 1. try: original to system library
      } catch (e: UnknownHostException) {
        try {
          return Address.getAllByName(hostname).toList()  // 2. try: DNSJava default lookup
        } catch (e: UnknownHostException) {
          return lookupFrom(hostname, "8.8.8.8")  // fallback: tailored lookup via DNSJava
        }
      }
    } catch (e: NullPointerException) {
      throw UnknownHostException("Broken system behaviour for dns lookup of $hostname").apply {
        initCause(e)
      }
    }
  }

Disclaimer: This is just a proof of concept. I am not a Kotlin developer. And I am not a DNS specialist.

I did build DAVx5 and Nextcloud app using a patched OkHttp library as shown above. Since a couple of days I saw no further connection problems.

I was able to reproduce this DNS resolution bug on Huawei P20 and Sony Xperia X Compact. I use dynv6.net as dynamic DNS provider, which allows to configure IPv6-address (AAAA record) without insisting on configuring also an IPv4-address (A record).

Is it possible for others to reproduce this bug? (requiring specific IPv6 DNS configuration) Is it possible for others to verify the work-around?

In the end, would you consider to include a work-around in the OkHttp library, so that all the depending Apps benefit from that automatically?

yschimke commented 2 years ago

You shouldn't need to patch OkHttp. You should be able to implement your own Dns provider that uses your logic. But the dependency on DNSJava is a non starter for us. If the bug is as prevalent as you suggest, it should really be patched in the Android OS.

For reference we have other Dns implementation like DnsOverHttps https://github.com/square/okhttp/blob/cc3ce11fc455b2bf6790f66b6c5b417302c75f57/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt

peerless2012 commented 2 years ago

I have the same error on Android emulator.

yschimke commented 2 years ago

Closing, as it's not clear we can feasibly implement this and it's much easier to write this yourself. It also feels like a dangerous game to put in any defaults that work around the system behaviour, better to get attention to fixing the root cause instead of additional work.

Stefan-Fritzsche commented 2 years ago

I see your point. The work-around solves the issue for me in an easy way (just change OkHttp once and then re-build apps with changed library). But for OkHttp I think it's a wise decision of yours, not to include something like that.

It's a pity, that my mobile devices will likely never see an update with this being fixed.

Thank you for your quick replies.