Repository: twisted/twisted

Issue:
twisted.names.client.getHostByName fails if resolv.conf has IPv6 addresses for servers
|[<img alt="wsanchez's avatar" src="https://avatars.githubusercontent.com/u/50002?s=50" width="50" height="50">](https://github.com/wsanchez)| @wsanchez reported|
|-|-|
|Trac ID|trac#8877|
|Type|defect|
|Created|2016-10-28 22:20:25Z|
|Branch|https://github.com/twisted/twisted/tree/8877-getHostByName-ipv6|

This script:

```
from twisted.internet.task import react
from twisted.names.client import createResolver, getHostByName


def main(*argv):
    def gotAddress(address):
        print("Address is:", address)

    def gotError(f):
        print(f)

    d = getHostByName("www.mit.edu")
    d.addCallback(gotAddress)
    d.addErrback(gotError)

    return d


if __name__ == '__main__':
    react(main)
```

fails thusly:

```
[Failure instance: Traceback: <class 'twisted.internet.error.InvalidAddressError'>: ('2001:568:ff09:10a::54', 'IPv4 port write() called with IPv6 address')
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/client.py:398:_lookup
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/client.py:299:queryUDP
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/client.py:264:_query
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/dns.py:2926:query
--- <exception caught here> ---
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/dns.py:2804:_query
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/dns.py:2924:writeMessage
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/names/dns.py:2852:writeMessage
/Users/wsanchez/Developer/PIE/wsanchez/sample-klein-app/build/lib/python3.5/site-packages/twisted/internet/udp.py:295:write
]
```

when my `/etc/resolv.conf` file looks like:

```
domain telus
nameserver 2001:568:ff09:10a::54
nameserver 2001:568:ff09:10a::114
nameserver 192.168.1.254
nameserver 75.153.171.114
```


<details><summary>Searchable metadata</summary>

```
trac-id__8877 8877
type__defect defect
reporter__wsanchez wsanchez
priority__normal normal
milestone__None None
branch__8877_getHostByName_ipv6 8877-getHostByName-ipv6
branch_author__ 
status__closed closed
resolution__fixed fixed
component__names names
keywords__ipv6 ipv6
time__1477693225531475 1477693225531475
changetime__1484706281171035 1484706281171035
version__None None
owner__None None
cc__wsanchez
```
</details>



Discussion:

Resolving patch:
diff --git a/src/twisted/names/client.py b/src/twisted/names/client.py
--- a/src/twisted/names/client.py
+++ b/src/twisted/names/client.py
@@ -30,6 +30,7 @@
 from twisted.python import log, failure
 from twisted.names import (
     dns, common, resolve, cache, root, hosts as hostsModule)
+from twisted.internet.abstract import isIPv6Address
 
 
 
@@ -211,7 +212,7 @@ def pickServer(self):
             return self.dynServers[self.index - serverL]
 
 
-    def _connectedProtocol(self):
+    def _connectedProtocol(self, interface=''):
         """
         Return a new L{DNSDatagramProtocol} bound to a randomly selected port
         number.
@@ -219,7 +220,8 @@ def _connectedProtocol(self):
         proto = dns.DNSDatagramProtocol(self, reactor=self._reactor)
         while True:
             try:
-                self._reactor.listenUDP(dns.randomSource(), proto)
+                self._reactor.listenUDP(dns.randomSource(), proto,
+                                        interface=interface)
             except error.CannotListenError:
                 pass
             else:
@@ -260,7 +262,10 @@ def _query(self, *args):
         @return: A L{Deferred} which will be called back with the result of the
             query.
         """
-        protocol = self._connectedProtocol()
+        if isIPv6Address(args[0][0]):
+            protocol = self._connectedProtocol(interface='::')
+        else:
+            protocol = self._connectedProtocol()
         d = protocol.query(*args)
         def cbQueried(result):
             protocol.transport.stopListening()


Test patch:
diff --git a/src/twisted/names/test/test_client.py b/src/twisted/names/test/test_client.py
--- a/src/twisted/names/test/test_client.py
+++ b/src/twisted/names/test/test_client.py
@@ -24,7 +24,7 @@
 from twisted.names.common import ResolverBase
 
 from twisted.names.test.test_hosts import GoodTempPathMixin
-from twisted.names.test.test_util import MemoryReactor
+from twisted.names.test import test_util
 
 from twisted.test import proto_helpers
 
@@ -332,11 +332,11 @@ def test_datagramQueryServerOrder(self):
         """
         protocol = StubDNSDatagramProtocol()
 
-        servers = [object(), object()]
-        dynServers = [object(), object()]
+        servers = [("::1", 53), ("::2", 53)]
+        dynServers = [("::3", 53), ("::4", 53)]
         resolver = client.Resolver(servers=servers)
         resolver.dynServers = dynServers
-        resolver._connectedProtocol = lambda: protocol
+        resolver._connectedProtocol = lambda interface: protocol
 
         expectedResult = object()
         queryResult = resolver.queryUDP(None)
@@ -499,7 +499,7 @@ def test_resolverUsesOnlyParameterizedReactor(self):
         L{client.Resolver._connectedProtocol} should pass that reactor
         to L{twisted.names.dns.DNSDatagramProtocol}.
         """
-        reactor = MemoryReactor()
+        reactor = test_util.MemoryReactor()
         resolver = client.Resolver(resolv=self.mktemp(), reactor=reactor)
         proto = resolver._connectedProtocol()
         self.assertIs(proto._reactor, reactor)
@@ -528,6 +528,20 @@ def query(self, address, query, timeout=10, id=None):
         self.assertEqual(len(set(protocols)), 2)
 
 
+    def test_ipv6Resolver(self):
+        """
+        If the resolver is ipv6, open a ipv6 port.
+        """
+
+        fake = test_util.MemoryReactor()
+        resolver = client.Resolver(servers=[('::1', 53)],
+                                   reactor=fake)
+        resolver.query(dns.Query(b'foo.example.com'))
+        [(proto, transport)] = fake.udpPorts.items()
+        interface = transport.getHost().host
+        self.assertEqual("::", interface)
+
+
     def test_disallowedPort(self):
         """
         If a port number is initially selected which cannot be bound, the
@@ -536,7 +550,7 @@ def test_disallowedPort(self):
         ports = []
 
         class FakeReactor(object):
-            def listenUDP(self, port, *args):
+            def listenUDP(self, port, *args, **kwargs):
                 ports.append(port)
                 if len(ports) == 1:
                     raise CannotListenError(None, port, None)