Ticket #5086: twisted_udpv6.patch

File twisted_udpv6.patch, 21.6 KB (added by satis, 14 months ago)

patch based on ipv6-listenUDP-5086 branch and review comments

  • new file doc/core/howto/listings/udp/ipv6_listen.py

    diff --git doc/core/howto/listings/udp/ipv6_listen.py doc/core/howto/listings/udp/ipv6_listen.py
    new file mode 100644
    index 0000000..cac6b85
    - +  
     1from twisted.internet.protocol import DatagramProtocol 
     2from twisted.internet import reactor 
     3 
     4 
     5 
     6class Echo(DatagramProtocol): 
     7    def datagramReceived(self, data, addr): 
     8        print "received %r from %s" % (data, addr) 
     9        self.transport.write(data, addr) 
     10 
     11 
     12 
     13reactor.listenUDP(8006, Echo(), interface='::') 
     14reactor.run() 
  • doc/core/howto/udp.xhtml

    diff --git doc/core/howto/udp.xhtml doc/core/howto/udp.xhtml
    index 6fb5141..62c236c 100644
    reactor.run() 
    207207    base="twisted.internet.interfaces">IMulticastTransport</code> for more 
    208208    information.</p> 
    209209 
     210    <h2>IPv6</h2> 
     211    <p> 
     212      UDP sockets can also bind to IPv6 addresses to support sending and receiving 
     213      datagrams over IPv6. By passing an IPv6 address to <code class="API" 
     214      base="twisted.internet.interfaces.IReactorUDP">listenUDP</code>&apos;s 
     215      <code>interface</code> argument, the reactor will start an IPv6 socket that  
     216      can be used to send and receive UDP datagrams. 
     217    </p> 
     218 
     219    <a href="listings/udp/ipv6_listen.py" class="py-listing">ipv6_listen.py</a> 
     220 
    210221</body> 
    211222</html> 
  • twisted/internet/error.py

    diff --git twisted/internet/error.py twisted/internet/error.py
    index 4e9fb5f..3da045a 100644
    class AlreadyListened(Exception): 
    458458    listened on once. 
    459459    """ 
    460460 
     461class InvalidAddressError(Exception): 
     462    """ 
     463    An invalid address was specified (e.g. neither IPv4 or IPv6) 
     464 
     465    @ivar address: the address that was provided 
     466    @ivar message: Additional information provided by the calling context 
     467    """ 
     468    def __init__(self, address, message): 
     469        self.address = address 
     470        self.message = message 
     471 
     472    def __str__(self): 
     473        return "Invalid address %s: %s" %(self.address, self.message) 
    461474 
    462475__all__ = [ 
    463476    'BindError', 'CannotListenError', 'MulticastJoinError', 
    __all__ = [ 
    472485    'ProcessTerminated', 'ProcessExitedAlready', 'NotConnectingError', 
    473486    'NotListeningError', 'ReactorNotRunning', 'ReactorAlreadyRunning', 
    474487    'ReactorAlreadyInstalledError', 'ConnectingCancelledError', 
    475     'UnsupportedAddressFamily', 'UnsupportedSocketType'] 
     488    'UnsupportedAddressFamily', 'UnsupportedSocketType', 'InvalidAddressError'] 
  • twisted/internet/interfaces.py

    diff --git twisted/internet/interfaces.py twisted/internet/interfaces.py
    index 4021e57..da52c6d 100644
    class IUDPTransport(Interface): 
    22822282 
    22832283    def getHost(): 
    22842284        """ 
    2285         Returns L{IPv4Address}. 
     2285        Returns an L{IPv4Address} or L{IPv6Address}. 
    22862286        """ 
    22872287 
    22882288    def stopListening(): 
  • twisted/internet/iocpreactor/udp.py

    diff --git twisted/internet/iocpreactor/udp.py twisted/internet/iocpreactor/udp.py
    index 4dec51f..69fb6df 100644
    import socket, operator, struct, warnings, errno 
    1010from zope.interface import implements 
    1111 
    1212from twisted.internet import defer, address, error, interfaces 
    13 from twisted.internet.abstract import isIPAddress 
     13from twisted.internet.abstract import isIPAddress, isIPv6Address 
    1414from twisted.python import log, failure 
    1515 
    1616from twisted.internet.iocpreactor.const import ERROR_IO_PENDING 
    class Port(abstract.FileHandle): 
    4949        self.interface = interface 
    5050        self.setLogStr() 
    5151        self._connectedAddr = None 
     52        self._setAddressFamily() 
    5253 
    5354        abstract.FileHandle.__init__(self, reactor) 
    5455 
    class Port(abstract.FileHandle): 
    6061                struct.calcsize('i')) 
    6162 
    6263 
     64    def _setAddressFamily(self): 
     65        """ 
     66        Resolve address family for the socket. 
     67        """ 
     68        if isIPv6Address(self.interface): 
     69            self.addressFamily = socket.AF_INET6 
     70        elif isIPAddress(self.interface): 
     71            self.addressFamily = socket.AF_INET 
     72        elif self.interface: 
     73            raise error.InvalidAddressError(self.interface, 'not an IPv4 or IPv6 address.') 
     74 
     75 
    6376    def __repr__(self): 
    6477        if self._realPortNumber is not None: 
    6578            return ("<%s on %s>" % 
    class Port(abstract.FileHandle): 
    95108            skt = self.createSocket() 
    96109            skt.bind((self.interface, self.port)) 
    97110        except socket.error, le: 
    98             raise error.CannotListenError, (self.interface, self.port, le) 
     111            raise error.CannotListenError(self.interface, self.port, le) 
    99112 
    100113        # Make sure that if we listened on port 0, we update that to 
    101114        # reflect what the OS actually assigned us. 
    class Port(abstract.FileHandle): 
    166179                if no == errno.WSAEINTR: 
    167180                    return self.write(datagram) 
    168181                elif no == errno.WSAEMSGSIZE: 
    169                     raise error.MessageLengthError, "message too long" 
     182                    raise error.MessageLengthError("message too long") 
    170183                elif no in (errno.WSAECONNREFUSED, errno.WSAECONNRESET, 
    171184                            ERROR_CONNECTION_REFUSED, ERROR_PORT_UNREACHABLE): 
    172185                    self.protocol.connectionRefused() 
    class Port(abstract.FileHandle): 
    174187                    raise 
    175188        else: 
    176189            assert addr != None 
    177             if not addr[0].replace(".", "").isdigit(): 
    178                 warnings.warn("Please only pass IPs to write(), not hostnames", 
    179                               DeprecationWarning, stacklevel=2) 
     190            if not isIPAddress(addr[0]) and not isIPv6Address(addr[0]): 
     191                raise error.InvalidAddressError(addr[0], "write() only accepts IP addresses, not hostnames") 
     192            if isIPAddress(addr[0]) and self.addressFamily == socket.AF_INET6: 
     193                raise error.InvalidAddressError(addr[0], "IPv6 port write() called with IPv4 address") 
     194            if isIPv6Address(addr[0]) and self.addressFamily == socket.AF_INET: 
     195                raise error.InvalidAddressError(addr[0], "IPv4 port write() called with IPv6 address") 
    180196            try: 
    181197                return self.socket.sendto(datagram, addr) 
    182198            except socket.error, se: 
    class Port(abstract.FileHandle): 
    184200                if no == errno.WSAEINTR: 
    185201                    return self.write(datagram, addr) 
    186202                elif no == errno.WSAEMSGSIZE: 
    187                     raise error.MessageLengthError, "message too long" 
     203                    raise error.MessageLengthError("message too long") 
    188204                elif no in (errno.WSAECONNREFUSED, errno.WSAECONNRESET, 
    189205                            ERROR_CONNECTION_REFUSED, ERROR_PORT_UNREACHABLE): 
    190206                    # in non-connected UDP ECONNREFUSED is platform dependent, 
    class Port(abstract.FileHandle): 
    207223            raise RuntimeError( 
    208224                "already connected, reconnecting is not currently supported " 
    209225                "(talk to itamar if you want this)") 
    210         if not isIPAddress(host): 
    211             raise ValueError, "please pass only IP addresses, not domain names" 
     226        if not isIPAddress(host) and not isIPv6Address(host): 
     227            raise error.InvalidAddressError(host, 'not an IPv4 or IPv6 address.') 
    212228        self._connectedAddr = (host, port) 
    213229        self.socket.connect((host, port)) 
    214230 
    class Port(abstract.FileHandle): 
    268284 
    269285    def getHost(self): 
    270286        """ 
    271         Returns an IPv4Address. 
     287        Return the local address of the UDP connection 
    272288 
    273         This indicates the address from which I am connecting. 
     289        @returns: the local address of the UDP connection 
     290        @rtype: L{IPv4Address} or L{IPv6Address} 
    274291        """ 
    275         return address.IPv4Address('UDP', *self.socket.getsockname()) 
     292        addr = self.socket.getsockname() 
     293        if self.addressFamily == socket.AF_INET: 
     294            return address.IPv4Address('UDP', *addr) 
     295        elif self.addressFamily == socket.AF_INET6: 
     296            return address.IPv6Address('UDP', *(addr[:2])) 
    276297 
    277298 
    278299 
    class MulticastPort(MulticastMixin, Port): 
    378399            if hasattr(socket, "SO_REUSEPORT"): 
    379400                skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 
    380401        return skt 
    381  
    382  
  • twisted/internet/test/test_udp.py

    diff --git twisted/internet/test/test_udp.py twisted/internet/test/test_udp.py
    index 4452e2c..6a59760 100644
    from twisted.internet.test.reactormixins import ReactorBuilder 
    2121from twisted.internet.defer import Deferred, maybeDeferred 
    2222from twisted.internet.interfaces import ( 
    2323    ILoggingContext, IListeningPort, IReactorUDP, IReactorSocket) 
    24 from twisted.internet.address import IPv4Address 
     24from twisted.internet.address import IPv4Address, IPv6Address 
    2525from twisted.internet.protocol import DatagramProtocol 
    2626 
    2727from twisted.internet.test.connectionmixins import (LogObserverMixin, 
    2828                                                    findFreePort) 
     29from twisted.internet import defer, error 
     30from twisted.test.test_udp import Server, GoodClient 
    2931from twisted.trial.unittest import SkipTest 
    3032 
    3133 
    class UDPPortTestsMixin(object): 
    139141            port.getHost(), IPv4Address('UDP', host, portNumber)) 
    140142 
    141143 
     144    def test_getHostIPv6(self): 
     145        """ 
     146        L{IListeningPort.getHost} returns an L{IPv6Address} when listening on 
     147        an IPv6 interface. 
     148        """ 
     149        reactor = self.buildReactor() 
     150        port = self.getListeningPort( 
     151            reactor, DatagramProtocol(), interface='::1') 
     152        addr = port.getHost() 
     153        self.assertEqual(addr.host, "::1") 
     154        self.assertIsInstance(addr, IPv6Address) 
     155 
     156 
     157    def test_invalidInterface(self): 
     158        """ 
     159        A C{InvalidAddressError} is raised when trying to listen on an address that 
     160        isn't a valid IPv4 or IPv6 address. 
     161        """ 
     162        reactor = self.buildReactor() 
     163        self.assertRaises( 
     164            error.InvalidAddressError, reactor.listenUDP, DatagramProtocol(), 0, 
     165            interface='example.com') 
     166 
     167 
    142168    def test_logPrefix(self): 
    143169        """ 
    144170        Datagram transports implement L{ILoggingContext.logPrefix} to return a 
    class UDPPortTestsMixin(object): 
    192218        self.assertIn(repr(port.getHost().port), str(port)) 
    193219 
    194220 
     221    def test_writeToIPv6Interface(self): 
     222        """ 
     223        Writing to an IPv6 UDP socket on the loopback interface succeeds. 
     224        """ 
     225 
     226        reactor = self.buildReactor() 
     227        server = Server() 
     228        serverStarted = server.startedDeferred = defer.Deferred() 
     229        self.getListeningPort(reactor, server, interface="::1") 
     230 
     231        client = GoodClient() 
     232        clientStarted = client.startedDeferred = defer.Deferred() 
     233        self.getListeningPort(reactor, client, interface="::1") 
     234        cAddr = client.transport.getHost() 
     235 
     236        def cbClientStarted(ignored): 
     237            """ 
     238            Send a datagram from the client once it's started. 
     239 
     240            @param ignored: a list of C{[None, None]}, which is ignored 
     241            @returns: a deferred which fires when the server has received a 
     242                datagram. 
     243            """ 
     244            client.transport.write( 
     245                b"spam", ("::1", server.transport.getHost().port)) 
     246            serverReceived = server.packetReceived = defer.Deferred() 
     247            return serverReceived 
     248 
     249        def cbServerReceived(ignored): 
     250            """ 
     251            Stop the reactor after a datagram is received. 
     252 
     253            @param ignored: C{None}, which is ignored 
     254            @returns: C{None} 
     255            """ 
     256            reactor.stop() 
     257 
     258        d = defer.gatherResults([serverStarted, clientStarted]) 
     259        d.addCallback(cbClientStarted) 
     260        d.addCallback(cbServerReceived) 
     261        d.addErrback(err) 
     262        self.runReactor(reactor) 
     263 
     264        packet = server.packets[0] 
     265        self.assertEqual(packet[0], b'spam') 
     266        self.assertEqual(packet[1], (cAddr.host, cAddr.port)) 
     267 
     268 
     269    def test_connectedWriteToIPv6Interface(self): 
     270        """ 
     271        An IPv6 address can be passed as the C{interface} argument to  
     272        L{listenUDP}. The resulting Port accepts IPv6 datagrams. 
     273        """ 
     274 
     275        reactor = self.buildReactor() 
     276        server = Server() 
     277        serverStarted = server.startedDeferred = defer.Deferred() 
     278        self.getListeningPort(reactor, server, interface="::1") 
     279 
     280        client = GoodClient() 
     281        clientStarted = client.startedDeferred = defer.Deferred() 
     282        self.getListeningPort(reactor, client, interface="::1") 
     283        cAddr = client.transport.getHost() 
     284 
     285        def cbClientStarted(ignored): 
     286            """ 
     287            Send a datagram from the client once it's started. 
     288 
     289            @param ignored: a list of C{[None, None]}, which is ignored 
     290            @returns: a deferred which fires when the server has received a 
     291                datagram. 
     292            """ 
     293 
     294            client.transport.connect("::1", server.transport.getHost().port) 
     295            client.transport.write(b"spam") 
     296            serverReceived = server.packetReceived = defer.Deferred() 
     297            return serverReceived 
     298 
     299        def cbServerReceived(ignored): 
     300            """ 
     301            Stop the reactor after a datagram is received. 
     302 
     303            @param ignored: C{None}, which is ignored 
     304            @returns: C{None} 
     305            """ 
     306 
     307            reactor.stop() 
     308 
     309        d = defer.gatherResults([serverStarted, clientStarted]) 
     310        d.addCallback(cbClientStarted) 
     311        d.addCallback(cbServerReceived) 
     312        d.addErrback(err) 
     313        self.runReactor(reactor) 
     314 
     315        packet = server.packets[0] 
     316        self.assertEqual(packet[0], b'spam') 
     317        self.assertEqual(packet[1], (cAddr.host, cAddr.port)) 
     318 
     319 
     320    def test_writingToHostnameRaisesAddressError(self): 
     321        """ 
     322        Writing to a hostname instead of an IP address will raise an 
     323        C{InvalidAddressError}. 
     324        """ 
     325 
     326        reactor = self.buildReactor() 
     327        port = self.getListeningPort(reactor, DatagramProtocol()) 
     328        self.assertRaises(error.InvalidAddressError, port.write, 'spam', ('eggs.com', 1)) 
     329 
     330    def test_writingToIPv6OnIPv4RaisesAddressError(self): 
     331        """ 
     332        Writing to an IPv6 address on an IPv4 socket will raise an  
     333        C{InvalidAddressError}. 
     334        """ 
     335        reactor = self.buildReactor() 
     336        port = self.getListeningPort(reactor, DatagramProtocol()) 
     337        self.assertRaises(error.InvalidAddressError, port.write, 'spam', ('::1', 1)) 
     338 
     339    def test_writingToIPv4OnIPv6RaisesAddressError(self): 
     340        """ 
     341        Writing to an IPv6 address on an IPv4 socket will raise an  
     342        C{InvalidAddressError}. 
     343        """ 
     344        reactor = self.buildReactor() 
     345        port = self.getListeningPort(reactor, DatagramProtocol(), interface="::1") 
     346        self.assertRaises(error.InvalidAddressError, port.write, 'spam', ('127.0.0.1', 1)) 
     347 
     348    def test_connectingToHostnameRaisesAddressError(self): 
     349        """ 
     350        Connecting to a hostname instead of an IP address will raise an 
     351        C{InvalidAddressError}. 
     352        """ 
     353 
     354        reactor = self.buildReactor() 
     355        port = self.getListeningPort(reactor, DatagramProtocol()) 
     356        self.assertRaises(error.InvalidAddressError, port.connect, 'eggs.com', 1) 
     357 
     358 
    195359 
    196360class UDPServerTestsBuilder(ReactorBuilder, 
    197361                            UDPPortTestsMixin, DatagramTransportTestsMixin): 
  • twisted/internet/udp.py

    diff --git twisted/internet/udp.py twisted/internet/udp.py
    index 9dc55df..35eaa75 100644
    class Port(base.BasePort): 
    108108        self.interface = interface 
    109109        self.setLogStr() 
    110110        self._connectedAddr = None 
     111        self._setAddressFamily() 
    111112 
    112113 
    113114    @classmethod 
    class Port(base.BasePort): 
    230231                raise 
    231232            else: 
    232233                read += len(data) 
     234                if self.addressFamily == socket.AF_INET6: 
     235                    # Remove the flow and scope ID from the address tuple, 
     236                    # reducing it to a tuple of just (host, port). This should 
     237                    # be amended later to return an object that can unpack to 
     238                    # (host, port) but also includes the flow and scope ID. 
     239                    addr = addr[:2] 
    233240                try: 
    234241                    self.protocol.datagramReceived(data, addr) 
    235242                except: 
    class Port(base.BasePort): 
    245252 
    246253        @type addr: C{tuple} containing C{str} as first element and C{int} as 
    247254            second element, or C{None} 
    248         @param addr: A tuple of (I{stringified dotted-quad IP address}, 
     255        @param addr: A tuple of (I{stringified IPv4 or IPv6 address}, 
    249256            I{integer port number}); can be C{None} in connected mode. 
    250257        """ 
    251258        if self._connectedAddr: 
    class Port(base.BasePort): 
    264271                    raise 
    265272        else: 
    266273            assert addr != None 
    267             if not addr[0].replace(".", "").isdigit() and addr[0] != "<broadcast>": 
    268                 warnings.warn("Please only pass IPs to write(), not hostnames", 
    269                               DeprecationWarning, stacklevel=2) 
     274            if (not abstract.isIPAddress(addr[0]) 
     275                    and not abstract.isIPv6Address(addr[0]) 
     276                    and addr[0] != "<broadcast>"): 
     277                raise error.InvalidAddressError(addr[0], "write() only accepts IP addresses, not hostnames") 
     278            if (abstract.isIPAddress(addr[0]) or addr[0] == "<broadcast>") and self.addressFamily == socket.AF_INET6: 
     279                raise error.InvalidAddressError(addr[0], "IPv6 port write() called with IPv4 or broadcast address") 
     280            if abstract.isIPv6Address(addr[0]) and self.addressFamily == socket.AF_INET: 
     281                raise error.InvalidAddressError(addr[0], "IPv4 port write() called with IPv6 address") 
    270282            try: 
    271283                return self.socket.sendto(datagram, addr) 
    272284            except socket.error as se: 
    class Port(base.BasePort): 
    292304        """ 
    293305        if self._connectedAddr: 
    294306            raise RuntimeError("already connected, reconnecting is not currently supported") 
    295         if not abstract.isIPAddress(host): 
    296             raise ValueError("please pass only IP addresses, not domain names") 
     307        if not abstract.isIPAddress(host) and not abstract.isIPv6Address(host): 
     308            raise error.InvalidAddressError(host, 'not an IPv4 or IPv6 address.') 
    297309        self._connectedAddr = (host, port) 
    298310        self.socket.connect((host, port)) 
    299311 
    class Port(base.BasePort): 
    337349        logPrefix = self._getLogPrefix(self.protocol) 
    338350        self.logstr = "%s (UDP)" % logPrefix 
    339351 
     352    def _setAddressFamily(self): 
     353        """ 
     354        Resolve address family for the socket. 
     355        """ 
     356        if abstract.isIPv6Address(self.interface): 
     357            self.addressFamily = socket.AF_INET6 
     358        elif abstract.isIPAddress(self.interface): 
     359            self.addressFamily = socket.AF_INET 
     360        elif self.interface: 
     361            raise error.InvalidAddressError(self.interface, 'not an IPv4 or IPv6 address.') 
     362 
    340363 
    341364    def logPrefix(self): 
    342365        """ 
    class Port(base.BasePort): 
    347370 
    348371    def getHost(self): 
    349372        """ 
    350         Returns an IPv4Address. 
     373        Return the local address of the UDP connection 
    351374 
    352         This indicates the address from which I am connecting. 
     375        @returns: the local address of the UDP connection 
     376        @rtype: L{IPv4Address} or L{IPv6Address} 
    353377        """ 
    354         return address.IPv4Address('UDP', *self.socket.getsockname()) 
     378        addr = self.socket.getsockname() 
     379        if self.addressFamily == socket.AF_INET: 
     380            return address.IPv4Address('UDP', *addr) 
     381        elif self.addressFamily == socket.AF_INET6: 
     382            return address.IPv6Address('UDP', *(addr[:2])) 
    355383 
    356384 
    357385 
    class MulticastPort(MulticastMixin, Port): 
    417445    UDP Port that supports multicasting. 
    418446    """ 
    419447 
    420     def __init__(self, port, proto, interface='', maxPacketSize=8192, reactor=None, listenMultiple=False): 
     448    def __init__(self, port, proto, interface='', maxPacketSize=8192, 
     449                 reactor=None, listenMultiple=False): 
    421450        """ 
    422451        @see: L{twisted.internet.interfaces.IReactorMulticast.listenMulticast} 
    423452        """ 
  • twisted/test/test_udp.py

    diff --git twisted/test/test_udp.py twisted/test/test_udp.py
    index 21d145c..cfc8f5e 100644
    class UDPTestCase(unittest.TestCase): 
    267267        return d 
    268268 
    269269 
     270 
    270271    def test_connectionRefused(self): 
    271272        """ 
    272273        A L{ConnectionRefusedError} exception is raised when a connection 
    class UDPTestCase(unittest.TestCase): 
    312313 
    313314    def test_badConnect(self): 
    314315        """ 
    315         A call to the transport's connect method fails with a L{ValueError} 
     316        A call to the transport's connect method fails with a L{InvalidAddressError} 
    316317        when a non-IP address is passed as the host value. 
    317318 
    318319        A call to a transport's connect method fails with a L{RuntimeError} 
    class UDPTestCase(unittest.TestCase): 
    320321        """ 
    321322        client = GoodClient() 
    322323        port = reactor.listenUDP(0, client, interface="127.0.0.1") 
    323         self.assertRaises(ValueError, client.transport.connect, 
     324        self.assertRaises(error.InvalidAddressError, client.transport.connect, 
    324325                          "localhost", 80) 
    325326        client.transport.connect("127.0.0.1", 80) 
    326327        self.assertRaises(RuntimeError, client.transport.connect, 
  • new file twisted/topfiles/5086.feature

    diff --git twisted/topfiles/5086.feature twisted/topfiles/5086.feature
    new file mode 100644
    index 0000000..d0ea2f0
    - +  
     1IReactorUDP.listenUDP, IUDPTransport.write and IUDPTransport.connect now accept ipv6 address literals. 
     2 No newline at end of file