Repository: python-trio/trio

Issue:
Invalid argument exception during socket.accept on AF_INET6
minimal-ish testcase and exception: https://gist.github.com/8d647f4c8368bc318e73833474601526

If I remove the `setsockopt` call on line 192, the testcase works as expected.

Is this a bug, or am I not creating an IPv6 socket correctly?


Discussion:
I found this from the `ipv6(7)` manpage:

> The default value for [`IPV6_V6ONLY`] is defined by the contents of the file /proc/sys/net/ipv6/bindv6only.  The default value for that file is 0 (false).

I did a bit of testing, and found this behavior with respect to the sockets created by `accept`:

<table>
<tr>
  <th colspan=2 >connecting</th>
  <th colspan=2 >listening</th>
  <th colspan=2 >accept</th>
</tr>
<tr>
  <th>socket type</th>
  <th>address type</th>
  <th>socket type</th>
  <th>address type</th>
  <th>socket type</th>
  <th>v6only value</th>
</tr>
<tr>
  <td>AF_INET6</td>
  <td>ipv6</td>
  <td>AF_INET6</td>
  <td>ipv6</td>
  <td>AF_INET6</td>
  <td>1</td>
</tr>
<tr>
  <td>AF_INET6</td>
  <td>mapped-ipv4</td>
  <td>AF_INET6</td>
  <td>mapped-ipv4</td>
  <td>AF_INET6</td>
  <td>0</td>
</tr>
<tr>
  <td>AF_INET6</td>
  <td>mapped-ipv4</td>
  <td>AF_INET</td>
  <td>ipv4</td>
  <td>AF_INET6</td>
  <td>0</td>
</tr>
<tr>
  <td>AF_INET</td>
  <td>ipv4</td>
  <td>AF_INET6</td>
  <td>mapped-ipv4</td>
  <td>AF_INET</td>
  <td>X</td>
</tr>
</table>

It seems like because:

 * accept-v6onlyness seems correct to begin with
 * we should probably respect the system default
 * `IPV6_V6ONLY` doesn't seem to be supported on all INET6 sockets

 ... that we should remove [trio/socket.py#L191](https://github.com/python-trio/trio/blob/master/trio/socket.py#L191)
Right, there are two issues here: (1) what we should be doing with `IPV6_V6ONLY` by default (which overlaps with #72), and (2) fixing the exception.

Am I reading your table correctly as saying that the problem happens when you set up a listen socket with `IPV6_V6ONLY` set to False, then accept a v4 connection, and then trio tries to set `IPV6_V6ONLY` on the new labeled-as-ipv6-but-secretly-actually-a-v4-socket? That makes a lot of sense :-). So at the very least we should tolerate an error here.

As to the larger question of defaults, there are a few competing considerations:

The problem with accepting the system default is that it leads to non-portable programs (in particular in this case: test on linux, everything works fine because in fact everyone leaves `bindv6only` set to 0, and then you're broken on windows because they make `IPV6_V6ONLY` default to enabled. And in practice, setting V6ONLY=False doesn't actually make programs magically handle ipv4 and ipv6 equally, because e.g. people expect that their web server's connection logs will contain ipv4 addresses rather than ipv6-mapped-ipv4 addresses. So I figured better to have consistency and then let people pick if they want something different... libuv does something similar, though IIRC they have the opposite default from trio's current setting. I don't have a strong opinion on which default is better :-)

But then there's another issue: initially I hoped that `trio.socket` would be the standard user-level API for doing networking in trio, but in the course of implementing SSL support have come around to the conclusion that we need a higher-level layer that provides a dedicated "stream" abstraction. There's a huge work-in-progress branch in #107 with the initial part of this. As part of this, I've defined a `SocketStream` class that adapts a socket into the `Stream` interface; and probably I will also add high-level helpers for connecting to a host+port and for listening on a port, that are defined at this level. So... if `trio.socket` is becoming the "low-level raw" networking API, maybe it does make more sense to remove the default-setting code from here, and instead handle it in the higher-level layer. (So in particular, the "listen on this port" interface would automatically create both ipv4 and and ipv6 sockets, and transparently listen on both of them.)
> and then trio tries to set IPV6_V6ONLY on the new labeled-as-ipv6-but-secretly-actually-a-v4-socket?

This is also a problem, but what I was originally trying to describe, completely ignoring `SocketType.__init__` for a moment, and the ipv6-mapped-ipv4 stuff, is that if you do the equivalent of:

```python
with trio.socket.socket(trio.socket.AF_INET6) as socket:
    socket.bind((host, port))
    socket.listen()

    accept_socket = await socket.accept()
    assert accept_socket._sock.family == AF_INET6
    accept_socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, False)
```

...that the `accept_socket.setsockopt` call will throw `OSError: [Errno 22] Invalid argument`, and it doesn't matter what value you pass to `IPV6_V6ONLY`.

In the table, I'm just trying to show that it probably doesn't make sense to try to `IPV6_V6ONLY`-normalize the sockets returned by `accept()`.

Resolving patch:
diff --git a/trio/socket.py b/trio/socket.py
--- a/trio/socket.py
+++ b/trio/socket.py
@@ -188,7 +188,10 @@ def __init__(self, sock):
 
         # Defaults:
         if self._sock.family == AF_INET6:
-            self.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, False)
+            try:
+                self.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, False)
+            except OSError:
+                pass
 
         # See https://github.com/python-trio/trio/issues/39
         if _sys.platform == "win32":


Test patch:
diff --git a/trio/tests/test_socket.py b/trio/tests/test_socket.py
--- a/trio/tests/test_socket.py
+++ b/trio/tests/test_socket.py
@@ -316,12 +316,13 @@ async def test_SocketType_shutdown():
         assert await a.recv(3) == b"yyy"
 
 
-async def test_SocketType_simple_server():
+@pytest.mark.parametrize("address, socket_type", [('127.0.0.1', tsocket.AF_INET), ('::1', tsocket.AF_INET6)])
+async def test_SocketType_simple_server(address, socket_type):
     # listen, bind, accept, connect, getpeername, getsockname
-    with tsocket.socket() as listener, tsocket.socket() as client:
-        listener.bind(("127.0.0.1", 0))
+    with tsocket.socket(socket_type) as listener, tsocket.socket(socket_type) as client:
+        listener.bind((address, 0))
         listener.listen(20)
-        addr = listener.getsockname()
+        addr = listener.getsockname()[:2]
         async with _core.open_nursery() as nursery:
             nursery.spawn(client.connect, addr)
             accept_task = nursery.spawn(listener.accept)