Repository: aio-libs/aiohttp

Issue:
Tuple index out of range with 3.7.1 and no IPv6
🐞 **Describe the bug**
After upgrading to aiohttp==3.7.1, dns resolution fails with an exception.


💡 **To Reproduce**
<!-- How to reproduce the behavior?

For example:
1. Have certain environment
2. Run given code snippet in a certain way
3. See some behavior described

Add these steps below this comment: -->

1. I believe this is due to not have IPV6 enabled in my kernel, which is custom compiled with.
   `CONFIG_IPV6=n`
2. Run:
   ```python
   import asyncio
   import aiohttp


   async def main():
       async with aiohttp.ClientSession() as session:
           async with session.get('http://python.org') as response:
               print(response.status)


   asyncio.run(main())
   ```
3. Actual behavior
   ```python-traceback
   $ python bug.py
   Traceback (most recent call last):
     File "bug.py", line 11, in <module>
       asyncio.run(main())
     File "/usr/lib/python3.7/asyncio/runners.py", line 43, in run
       return loop.run_until_complete(main)
     File "/usr/lib/python3.7/asyncio/base_events.py", line 587, in run_until_complete
       return future.result()
     File "bug.py", line 7, in main
       async with session.get('http://python.org') as response:
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/client.py", line 1124, in __aenter__
       self._resp = await self._coro
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/client.py", line 528, in _request
       req, traces=traces, timeout=real_timeout
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 541, in connect
       proto = await self._create_connection(req, traces, timeout)
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 898, in _create_connection
       _, proto = await self._create_direct_connection(req, traces, timeout)
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 1005, in _create_direct_connection
       hosts = await asyncio.shield(host_resolved)
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 871, in _resolve_host
       addrs = await self._resolver.resolve(host, port, family=self._family)
     File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/resolver.py", line 37, in resolve
       if family == socket.AF_INET6 and address[3]:  # type: ignore
   IndexError: tuple index out of range
   ```


💡 **Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
Should print `200`.


📋 **Logs/tracebacks**
<!-- If applicable, add logs/tracebacks to help explain your problem. -->
```python-traceback (paste your traceback in the next line)

```

📋 **Your version of the Python**
<!-- Attach your version of the Python. -->
```console
$ python --version
Python 3.7.9
```

📋 **Your version of the aiohttp/yarl/multidict distributions**
<!-- Attach your version of the distributions in the code blocks below. -->
```console
$ python -m pip show aiohttp
Name: aiohttp
Version: 3.7.1
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Author: Nikolay Kim
Author-email: fafhrd91@gmail.com
License: Apache 2
Location: /home/dbn/usr/py37/lib/python3.7/site-packages
Requires: multidict, async-timeout, typing-extensions, yarl, chardet, attrs
Required-by: replay-proxy
```
```console
$ python -m pip show multidict
Name: multidict
Version: 4.7.6
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /home/dbn/usr/py37/lib/python3.7/site-packages
Requires:
Required-by: yarl, aiohttp
```
```console
$ python -m pip show yarl
Name: yarl
Version: 1.5.1
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl/
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /home/dbn/usr/py37/lib/python3.7/site-packages
Requires: typing-extensions, multidict, idna
Required-by: aiohttp, replay-proxy
```

📋 **Additional context**
<!-- Add any other context about the problem here, in the next line. -->

<!-- Describe the environment you have that lead to your issue.
     This includes aiohttp version, OS, proxy server and other bits that
     are related to your case.

     IMPORTANT: aiohttp is both server framework and client library.
     For getting rid of confusing please put 'server', 'client' or 'both'
     word here.
     -->



Discussion:
I'm curious what parameters are passed into `getaddrinfo()` in your case and what list is returned?
Could you print them in a comment?

My initial thought is: on systems with IPv6 not installed the check `if family == socket.AF_INET6 and address[3]:` should fail on `family == socket.AF_INET6` test; `adress[3]` test is not executed in this case.
I added some debug statements to resolver.py:
```diff
  async def resolve(
      self, hostname: str, port: int = 0, family: int = socket.AF_INET
  ) -> List[Dict[str, Any]]:
+     print('getaddrinfo(%s, %s, type=%s, family=%s)' % (hostname, port, type, family))
      infos = await self._loop.getaddrinfo(
          hostname, port, type=socket.SOCK_STREAM, family=family
      )
+     print('infos:', infos)
```

```
getaddrinfo(python.org, 80, type=<class 'type'>, family=0)
infos: [(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('45.55.99.72', 80))]
getaddrinfo(python.org, 443, type=<class 'type'>, family=0)
infos: [(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('45.55.99.72', 443))]
getaddrinfo(www.python.org, 443, type=<class 'type'>, family=0)
infos: [(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('151.101.188.223', 443)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', (10, b'\x01\xbb\x00\x00\x00\x00*\x04NB\x00-\x00\x00'))]
Traceback (most recent call last):
  File "bug-5156.py", line 11, in <module>
    asyncio.run(main())
  File "/usr/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/usr/lib/python3.7/asyncio/base_events.py", line 587, in run_until_complete
    return future.result()
  File "bug-5156.py", line 7, in main
    async with session.get('http://python.org') as response:
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/client.py", line 1124, in __aenter__
    self._resp = await self._coro
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/client.py", line 528, in _request
    req, traces=traces, timeout=real_timeout
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 541, in connect
    proto = await self._create_connection(req, traces, timeout)
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 898, in _create_connection
    _, proto = await self._create_direct_connection(req, traces, timeout)
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 1005, in _create_direct_connection
    hosts = await asyncio.shield(host_resolved)
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/connector.py", line 871, in _resolve_host
    addrs = await self._resolver.resolve(host, port, family=self._family)
  File "/home/dbn/usr/py37/lib/python3.7/site-packages/aiohttp/resolver.py", line 39, in resolve
    if family == socket.AF_INET6 and address[3]:  # type: ignore
IndexError: tuple index out of range
```

For reference, here is my current DNS resolution for `www.python.org`
```
$ host www.python.org
www.python.org is an alias for dualstack.python.map.fastly.net.
dualstack.python.map.fastly.net has address 151.101.188.223
dualstack.python.map.fastly.net has IPv6 address 2a04:4e42:2d::223
```
Interesting...
In `(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', (10, b'\x01\xbb\x00\x00\x00\x00*\x04NB\x00-\x00\x00')` the `(10, b'\x01\xbb\x00\x00\x00\x00*\x04NB\x00-\x00\x00')` doesn't look like IPv6 at all!
Is it correct?
I mean if I do `host, port = (10, b'\x01\xbb\x00\x00\x00\x00*\x04NB\x00-\x00\x00')` -- both host and port are broken anyway.
I found this which explains the cause: https://bugs.python.org/issue16208

FWIW, I was able to process the results of getaddrinfo in C.  Here is some embarrassingly bad C code:
```c
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <arpa/inet.h>
#include <stdlib.h>


#define BUF_SIZE 500

void
print_addr(struct addrinfo *result) {
	char *addr = NULL;
	switch(result->ai_family) {
		case AF_INET: {
			struct sockaddr_in *addr_in = (struct sockaddr_in *)result->ai_addr;
			addr = malloc(INET_ADDRSTRLEN);
			inet_ntop(AF_INET, &(addr_in->sin_addr), addr, INET_ADDRSTRLEN);
			break;
		}
		case AF_INET6: {
			struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)result->ai_addr;
			addr = malloc(INET6_ADDRSTRLEN);
			inet_ntop(AF_INET6, &(addr_in6->sin6_addr), addr, INET6_ADDRSTRLEN);
			break;
		}
		default:
			break;
	}
	printf("IP address: %s\n", addr);
	free(addr);
}

int
main(int argc, char *argv[])
{
	struct addrinfo hints;
	struct addrinfo *result, *rp;
	int sfd, s, j;
	size_t len;
	ssize_t nread;
	char buf[BUF_SIZE];

	if (argc < 3) {
		fprintf(stderr, "Usage: %s host port msg...\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	memset(&hints, 0, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
	hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
	hints.ai_flags = 0;
	hints.ai_protocol = 0;          /* Any protocol */

	s = getaddrinfo(argv[1], argv[2], &hints, &result);
	if (s != 0) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
		exit(EXIT_FAILURE);
	}

	while (result != 0) {
		print_addr(result);
		result = result->ai_next;
	}
}
```

```
$ gcc bug.c
$ ./a.out www.python.org 443
IP address: 151.101.188.223
IP address: 2a04:4e42:2d::223
```

I tried to do something similar in Python, I'm not sure if I'm doing it right though:
```python
import socket

for ai in socket.getaddrinfo('www.python.org', 443):
    print(ai)
    if ai[0] == socket.AF_INET6:
        af, socktype, proto, canonname, sa = ai
        print("AF_INET6", socket.inet_ntop(socket.AF_INET6, sa[1]))
```

```
$ python getaddrinfo.py 
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('151.101.188.223', 443))
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('1imagine51.101.188.223', 443))
(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_RAW: 3>, 0, '', ('151.101.188.223', 443))
(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', (10, b'\x01\xbb\x00\x00\x00\x00*\x04NB\x00-\x00\x00'))
Traceback (most recent call last):
  File "getaddrinfo.py", line 7, in <module>
    print("AF_INET6", socket.inet_ntop(socket.AF_INET6, sa[1]))
ValueError: unknown address family 10
```

I'm running a Gentoo system with the `-ipv6` use flag set, but I enabled it for the Python build as it seems problematic.

Perhaps a fix in aiohttp would be to check at AF_INET6 addresses are a 4 tuple and ignore if they are not.

Also, I think this is the same issue as #4557
> I'm running a Gentoo system with the `-ipv6` use flag set, but I enabled it for the Python build as it seems problematic.

So it's enabled in CPython but disabled in the kernel? What about sysctl? Could you post your `sudo sysctl -a | grep ipv6` output? Does it have something like `net.ipv6.conf.lo.disable_ipv6` set? How about other interfaces? Or is this setting just missing?
> Also, I think this is the same issue as #4557

So it should probably be mitigated with "has ipv6" check like https://github.com/tornadoweb/tornado/pull/670/files#diff-e9701aba4f8165d329018bef200b0d98dd70c24856c159b5c29cce89ddd69e8bR56
> Perhaps a fix in aiohttp would be to check at AF_INET6 addresses are a 4 tuple and ignore if they are not.

From a practical point of view, I'm ok with this solution **if** it raises a warning before the ignoring.
From my understanding, we see a perfect example of a gray corner case that we can pass around easily.
A detection for address tuple size smells bad but looks working.
> So it's enabled in CPython but disabled in the kernel? What about sysctl?

Sorry, my comment was a little confusing, when I reported this issue it was disabled in Python and in the Kernel.  My workaround is to enable it in Python and leave it disabled in the Kernel.

> Could you post your sudo sysctl -a | grep ipv6 output?

It looks like there are no results containing ipv6.

I like the `socket.has_ipv6` solution, maybe we should test for this when creating the TCPConnector and set the family to AF_UNSPEC or AF_INET.  Essentially the linked tornado code.
So I looked at those bugs on CPython tracker and I'm puzzled why they are "wontfix". Moreover, *I think* that CPython may be incorrectly processing NUL-terminated strings or something like that.

I've tried decoding that bytesting manually on a Gentoo laptop with IPv6 on and still got an error (a different error but still):
```python
>>> socket.inet_ntop(socket.AF_INET6, b'\x01\xbb\x00\x00\x00\x00*\x04NB\x00-\x00\x00')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid length of packed IP address string
```
Something about it looked weird and I tried to do the packing of a known address `2a04:4e42:41::223`:
```python
>>> socket.getaddrinfo('www.python.org', 443, type=socket.SOCK_STREAM, family=socket.AF_INET6)
[(<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('2a04:4e42:41::223', 443, 0, 0))]
>>> socket.inet_pton(socket.AF_INET6, '2a04:4e42:2d::223')
b'*\x04NB\x00-\x00\x00\x00\x00\x00\x00\x00\x00\x02#'
```
Woah! That looks different! Let's try unpacking that correct value, just to be sure:
```python
>>> socket.inet_ntop(socket.AF_INET6, b'*\x04NB\x00-\x00\x00\x00\x00\x00\x00\x00\x00\x02#')
'2a04:4e42:2d::223'
```
Yaaay! We've got the correct value!

Based on this, I'm pretty much convinced that the bug in CPython originates in `socket.getaddrinfo()` returning a malformed packed IPv6 address.

P.S. It's interesting that both representations have identical `*\x04NB\x00-\x00\x00` substring ("\x01\xbb\x00\x00\x00\x00 ***\x04NB\x00-\x00\x00**" and " ***\x04NB\x00-\x00\x00**\x00\x00\x00\x00\x00\x00\x02#") so I'm convinced it has something to do with processing the memory offsets on the C level.
So I looked up the [`getaddrinfo(3)` manpage](https://man7.org/linux/man-pages/man3/getaddrinfo.3.html) and it seems like the fix could be as easy as adding a `socket.AI_ADDRCONFIG` as a flag for the `getaddrinfo()` call:
> **If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4
> addresses are returned in the list pointed to by res only if the
> local system has at least one IPv4 address configured, and IPv6
> addresses are returned only if the local system has at least one IPv6
> address configured.**  The **loopback address is not considered** for this
> case as valid as a configured address.  This flag **is useful on, for
> example, IPv4-only systems**, to ensure that **getaddrinfo() does not
> return IPv6 socket addresses** that would always fail in connect(2) or
> bind(2).

@danielnelson could you please try this in your Python REPL:
```python
>>> socket.getaddrinfo('www.python.org', 443, type=socket.SOCK_STREAM, family=socket.AF_INET6, flags=socket.AI_ADDRCONFIG)
```
and also (just in case):
```python
>>> socket.getaddrinfo('www.python.org', 443, type=socket.SOCK_STREAM, flags=socket.AI_ADDRCONFIG)
```
Other observations:
1) The broken bytesting is 14 bytes long while the correct one is 16 bytes
2) I haven't noticed anything immediately obvious in https://github.com/python/cpython/blob/master/Modules/socketmodule.c so maybe this is how the underlying C-libs behave...

Resolving patch:
diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py
--- a/aiohttp/resolver.py
+++ b/aiohttp/resolver.py
@@ -28,7 +28,11 @@ async def resolve(
         self, hostname: str, port: int = 0, family: int = socket.AF_INET
     ) -> List[Dict[str, Any]]:
         infos = await self._loop.getaddrinfo(
-            hostname, port, type=socket.SOCK_STREAM, family=family
+            hostname,
+            port,
+            type=socket.SOCK_STREAM,
+            family=family,
+            flags=socket.AI_ADDRCONFIG,
         )
 
         hosts = []


Test patch: