%PDF- %PDF-
Direktori : /lib/python3/dist-packages/twisted/pair/test/ |
Current File : //lib/python3/dist-packages/twisted/pair/test/test_tuntap.py |
# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Tests for L{twisted.pair.tuntap}. """ import os import socket import struct from collections import deque from errno import EAGAIN, EBADF, EINVAL, ENODEV, ENOENT, EPERM, EWOULDBLOCK from itertools import cycle from random import randrange from signal import SIGINT from typing import Optional from twisted.python.reflect import ObjectNotFound, namedAny platformSkip: Optional[str] try: namedAny("fcntl.ioctl") except (ObjectNotFound, AttributeError): platformSkip = "Platform is missing fcntl/ioctl support" else: platformSkip = None from zope.interface import Interface, implementer from zope.interface.verify import verifyObject from twisted.internet.error import CannotListenError from twisted.internet.interfaces import IAddress, IListeningPort, IReactorFDSet from twisted.internet.protocol import ( AbstractDatagramProtocol, DatagramProtocol, Factory, ) from twisted.internet.task import Clock from twisted.pair.ethernet import EthernetProtocol from twisted.pair.ip import IPProtocol from twisted.pair.raw import IRawPacketProtocol from twisted.pair.rawudp import RawUDPProtocol from twisted.python.compat import iterbytes from twisted.python.log import addObserver, removeObserver, textFromEventDict from twisted.python.reflect import fullyQualifiedName from twisted.trial.unittest import SkipTest, SynchronousTestCase # Let the module-scope testing subclass of this still be defined (and then not # used) in case we can't import from twisted.pair.testing due to platform # limitations. _RealSystem = object # Same rationale as for _RealSystem. _IInputOutputSystem = Interface if not platformSkip: from twisted.pair.testing import ( _H, _PI_SIZE, MemoryIOSystem, Tunnel, _ethernet, _ip, _IPv4, _udp, ) from twisted.pair.tuntap import ( _IFNAMSIZ, _TUNSETIFF, TunnelAddress, TunnelFlags, TuntapPort, _IInputOutputSystem, _RealSystem, ) else: skip = platformSkip @implementer(IReactorFDSet) class ReactorFDSet: """ An implementation of L{IReactorFDSet} which only keeps track of which descriptors have been registered for reading and writing. This implementation isn't actually capable of determining readability or writeability and generates no events for the descriptors registered with it. @ivar _readers: A L{set} of L{IReadDescriptor} providers which the reactor is supposedly monitoring for read events. @ivar _writers: A L{set} of L{IWriteDescriptor} providers which the reactor is supposedly monitoring for write events. """ def __init__(self): self._readers = set() self._writers = set() self.addReader = self._readers.add self.addWriter = self._writers.add def removeReader(self, reader): self._readers.discard(reader) def removeWriter(self, writer): self._writers.discard(writer) def getReaders(self): return iter(self._readers) def getWriters(self): return iter(self._writers) def removeAll(self): try: return list(self._readers | self._writers) finally: self._readers = set() self._writers = set() verifyObject(IReactorFDSet, ReactorFDSet()) class FSSetClock(Clock, ReactorFDSet): """ An L{FSSetClock} is a L{IReactorFDSet} and an L{IReactorClock}. """ def __init__(self): Clock.__init__(self) ReactorFDSet.__init__(self) class TunHelper: """ A helper for tests of tun-related functionality (ip-level tunnels). """ @property def TUNNEL_TYPE(self): # Hide this in a property because TunnelFlags is not always imported. return TunnelFlags.IFF_TUN | TunnelFlags.IFF_NO_PI def __init__(self, tunnelRemote, tunnelLocal): """ @param tunnelRemote: The source address for UDP datagrams originated from this helper. This is an IPv4 dotted-quad string. @type tunnelRemote: L{bytes} @param tunnelLocal: The destination address for UDP datagrams originated from this helper. This is an IPv4 dotted-quad string. @type tunnelLocal: L{bytes} """ self.tunnelRemote = tunnelRemote self.tunnelLocal = tunnelLocal def encapsulate(self, source, destination, payload): """ Construct an ip datagram containing a udp datagram containing the given application-level payload. @param source: The source port for the UDP datagram being encapsulated. @type source: L{int} @param destination: The destination port for the UDP datagram being encapsulated. @type destination: L{int} @param payload: The application data to include in the udp datagram. @type payload: L{bytes} @return: An ethernet frame. @rtype: L{bytes} """ return _ip( src=self.tunnelRemote, dst=self.tunnelLocal, payload=_udp(src=source, dst=destination, payload=payload), ) def parser(self): """ Get a function for parsing a datagram read from a I{tun} device. @return: A function which accepts a datagram exactly as might be read from a I{tun} device. The datagram is expected to ultimately carry a UDP datagram. When called, it returns a L{list} of L{tuple}s. Each tuple has the UDP application data as the first element and the sender address as the second element. """ datagrams = [] receiver = DatagramProtocol() def capture(*args): datagrams.append(args) receiver.datagramReceived = capture udp = RawUDPProtocol() udp.addProto(12345, receiver) ip = IPProtocol() ip.addProto(17, udp) def parse(data): # TUN devices omit the ethernet framing so we can start parsing # right at the IP layer. ip.datagramReceived(data, False, None, None, None) return datagrams return parse class TapHelper: """ A helper for tests of tap-related functionality (ethernet-level tunnels). """ @property def TUNNEL_TYPE(self): flag = TunnelFlags.IFF_TAP if not self.pi: flag |= TunnelFlags.IFF_NO_PI return flag def __init__(self, tunnelRemote, tunnelLocal, pi): """ @param tunnelRemote: The source address for UDP datagrams originated from this helper. This is an IPv4 dotted-quad string. @type tunnelRemote: L{bytes} @param tunnelLocal: The destination address for UDP datagrams originated from this helper. This is an IPv4 dotted-quad string. @type tunnelLocal: L{bytes} @param pi: A flag indicating whether this helper will generate and consume a protocol information (PI) header. @type pi: L{bool} """ self.tunnelRemote = tunnelRemote self.tunnelLocal = tunnelLocal self.pi = pi def encapsulate(self, source, destination, payload): """ Construct an ethernet frame containing an ip datagram containing a udp datagram containing the given application-level payload. @param source: The source port for the UDP datagram being encapsulated. @type source: L{int} @param destination: The destination port for the UDP datagram being encapsulated. @type destination: L{int} @param payload: The application data to include in the udp datagram. @type payload: L{bytes} @return: An ethernet frame. @rtype: L{bytes} """ tun = TunHelper(self.tunnelRemote, self.tunnelLocal) ip = tun.encapsulate(source, destination, payload) frame = _ethernet( src=b"\x00\x00\x00\x00\x00\x00", dst=b"\xff\xff\xff\xff\xff\xff", protocol=_IPv4, payload=ip, ) if self.pi: # Going to send a datagram using IPv4 addressing protocol = _IPv4 # There are no flags though flags = 0 frame = _H(flags) + _H(protocol) + frame return frame def parser(self): """ Get a function for parsing a datagram read from a I{tap} device. @return: A function which accepts a datagram exactly as might be read from a I{tap} device. The datagram is expected to ultimately carry a UDP datagram. When called, it returns a L{list} of L{tuple}s. Each tuple has the UDP application data as the first element and the sender address as the second element. """ datagrams = [] receiver = DatagramProtocol() def capture(*args): datagrams.append(args) receiver.datagramReceived = capture udp = RawUDPProtocol() udp.addProto(12345, receiver) ip = IPProtocol() ip.addProto(17, udp) ether = EthernetProtocol() ether.addProto(0x800, ip) def parser(datagram): # TAP devices might include a PI header. Strip that off if we # expect it to be there. if self.pi: datagram = datagram[_PI_SIZE:] # TAP devices include ethernet framing so start parsing at the # ethernet layer. ether.datagramReceived(datagram) return datagrams return parser class TunnelTests(SynchronousTestCase): """ L{Tunnel} is mostly tested by other test cases but some tests don't fit there. Those tests are here. """ def test_blockingRead(self): """ Blocking reads are not implemented by L{Tunnel.read}. Attempting one results in L{NotImplementedError} being raised. """ tunnel = Tunnel(MemoryIOSystem(), os.O_RDONLY, None) self.assertRaises(NotImplementedError, tunnel.read, 1024) class TunnelDeviceTestsMixin: """ A mixin defining tests that apply to L{_IInputOutputSystem} implementations. """ def setUp(self): """ Create the L{_IInputOutputSystem} provider under test and open a tunnel using it. """ self.system = self.createSystem() self.fileno = self.system.open(b"/dev/net/tun", os.O_RDWR | os.O_NONBLOCK) self.addCleanup(self.system.close, self.fileno) mode = self.helper.TUNNEL_TYPE config = struct.pack("%dsH" % (_IFNAMSIZ,), self._TUNNEL_DEVICE, mode.value) self.system.ioctl(self.fileno, _TUNSETIFF, config) def test_interface(self): """ The object under test provides L{_IInputOutputSystem}. """ self.assertTrue(verifyObject(_IInputOutputSystem, self.system)) def _invalidFileDescriptor(self): """ Get an invalid file descriptor. @return: An integer which is not a valid file descriptor at the time of this call. After any future system call which allocates a new file descriptor, there is no guarantee the returned file descriptor will still be invalid. """ fd = self.system.open(b"/dev/net/tun", os.O_RDWR) self.system.close(fd) return fd def test_readEBADF(self): """ The device's C{read} implementation raises L{OSError} with an errno of C{EBADF} when called on a file descriptor which is not valid (ie, which has no associated file description). """ fd = self._invalidFileDescriptor() exc = self.assertRaises(OSError, self.system.read, fd, 1024) self.assertEqual(EBADF, exc.errno) def test_writeEBADF(self): """ The device's C{write} implementation raises L{OSError} with an errno of C{EBADF} when called on a file descriptor which is not valid (ie, which has no associated file description). """ fd = self._invalidFileDescriptor() exc = self.assertRaises(OSError, self.system.write, fd, b"bytes") self.assertEqual(EBADF, exc.errno) def test_closeEBADF(self): """ The device's C{close} implementation raises L{OSError} with an errno of C{EBADF} when called on a file descriptor which is not valid (ie, which has no associated file description). """ fd = self._invalidFileDescriptor() exc = self.assertRaises(OSError, self.system.close, fd) self.assertEqual(EBADF, exc.errno) def test_ioctlEBADF(self): """ The device's C{ioctl} implementation raises L{OSError} with an errno of C{EBADF} when called on a file descriptor which is not valid (ie, which has no associated file description). """ fd = self._invalidFileDescriptor() exc = self.assertRaises(IOError, self.system.ioctl, fd, _TUNSETIFF, b"tap0") self.assertEqual(EBADF, exc.errno) def test_ioctlEINVAL(self): """ The device's C{ioctl} implementation raises L{IOError} with an errno of C{EINVAL} when called with a request (second argument) which is not a supported operation. """ # Try to invent an unsupported request. Hopefully this isn't a real # request on any system. request = 0xDEADBEEF exc = self.assertRaises( IOError, self.system.ioctl, self.fileno, request, b"garbage" ) self.assertEqual(EINVAL, exc.errno) def test_receive(self): """ If a UDP datagram is sent to an address reachable by the tunnel device then it can be read out of the tunnel device. """ parse = self.helper.parser() found = False # Try sending the datagram a lot of times. There are no delivery # guarantees for UDP - not even over localhost. for i in range(100): key = randrange(2 ** 64) message = b"hello world:%d" % (key,) source = self.system.sendUDP(message, (self._TUNNEL_REMOTE, 12345)) # Likewise try receiving each of those datagrams a lot of times. # Timing might cause us to miss it the first few dozen times # through the loop. for j in range(100): try: packet = self.system.read(self.fileno, 1024) except OSError as e: if e.errno in (EAGAIN, EWOULDBLOCK): break raise else: datagrams = parse(packet) if (message, source) in datagrams: found = True break del datagrams[:] if found: break if not found: self.fail("Never saw probe UDP packet on tunnel") def test_send(self): """ If a UDP datagram is written the tunnel device then it is received by the network to which it is addressed. """ # Construct a unique application payload so the receiving side can # unambiguously identify the datagram we sent. key = randrange(2 ** 64) message = b"hello world:%d" % (key,) # To avoid really inconvenient test failures where the test just hangs # forever, set up a timeout for blocking socket operations. This # shouldn't ever be triggered when the test is passing. It only serves # to make sure the test runs eventually completes if something is # broken in a way that prevents real traffic from flowing. The value # chosen is totally arbitrary (but it might coincidentally exactly # match trial's builtin timeout for asynchronous tests). self.addCleanup(socket.setdefaulttimeout, socket.getdefaulttimeout()) socket.setdefaulttimeout(120) # Start listening for the test datagram first. The resulting port # object can be used to receive datagrams sent to _TUNNEL_LOCAL:12345 - # in other words, an application using the tunnel device will be able # to cause datagrams to arrive at this port as though they actually # traversed a network to arrive at this host. port = self.system.receiveUDP(self.fileno, self._TUNNEL_LOCAL, 12345) # Construct a packet with the appropriate wrappers and headings so that # it will arrive at the port created above. packet = self.helper.encapsulate(50000, 12345, message) # Write the packet to the tunnel device. self.system.write(self.fileno, packet) # Try to receive that datagram and verify it has the correct payload. packet = port.recv(1024) self.assertEqual(message, packet) class FakeDeviceTestsMixin: """ Define a mixin for use with test cases that require an L{_IInputOutputSystem} provider. This mixin hands out L{MemoryIOSystem} instances as the provider of that interface. """ _TUNNEL_DEVICE = b"tap-twistedtest" _TUNNEL_LOCAL = b"172.16.2.1" _TUNNEL_REMOTE = b"172.16.2.2" def createSystem(self): """ Create and return a brand new L{MemoryIOSystem}. The L{MemoryIOSystem} knows how to open new tunnel devices. @return: The newly created I/O system object. @rtype: L{MemoryIOSystem} """ system = MemoryIOSystem() system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel) return system class FakeTapDeviceTests( FakeDeviceTestsMixin, TunnelDeviceTestsMixin, SynchronousTestCase ): """ Run various tap-type tunnel unit tests against an in-memory I/O system. """ setattr( FakeTapDeviceTests, "helper", TapHelper( FakeTapDeviceTests._TUNNEL_REMOTE, FakeTapDeviceTests._TUNNEL_LOCAL, pi=False ), ) class FakeTapDeviceWithPITests( FakeDeviceTestsMixin, TunnelDeviceTestsMixin, SynchronousTestCase ): """ Run various tap-type tunnel unit tests against an in-memory I/O system with the PI header enabled. """ setattr( FakeTapDeviceWithPITests, "helper", TapHelper( FakeTapDeviceTests._TUNNEL_REMOTE, FakeTapDeviceTests._TUNNEL_LOCAL, pi=True ), ) class FakeTunDeviceTests( FakeDeviceTestsMixin, TunnelDeviceTestsMixin, SynchronousTestCase ): """ Run various tun-type tunnel unit tests against an in-memory I/O system. """ setattr( FakeTunDeviceTests, "helper", TunHelper(FakeTunDeviceTests._TUNNEL_REMOTE, FakeTunDeviceTests._TUNNEL_LOCAL), ) @implementer(_IInputOutputSystem) class TestRealSystem(_RealSystem): """ Add extra skipping logic so tests that try to create real tunnel devices on platforms where those are not supported automatically get skipped. """ def open(self, filename, *args, **kwargs): """ Attempt an open, but if the file is /dev/net/tun and it does not exist, translate the error into L{SkipTest} so that tests that require platform support for tuntap devices are skipped instead of failed. """ try: return super().open(filename, *args, **kwargs) except OSError as e: # The device file may simply be missing. The device file may also # exist but be unsupported by the kernel. if e.errno in (ENOENT, ENODEV) and filename == b"/dev/net/tun": raise SkipTest("Platform lacks /dev/net/tun") raise def ioctl(self, *args, **kwargs): """ Attempt an ioctl, but translate permission denied errors into L{SkipTest} so that tests that require elevated system privileges and do not have them are skipped instead of failed. """ try: return super().ioctl(*args, **kwargs) except OSError as e: if EPERM == e.errno: raise SkipTest("Permission to configure device denied") raise def sendUDP(self, datagram, address): """ Use the platform network stack to send a datagram to the given address. @param datagram: A UDP datagram payload to send. @type datagram: L{bytes} @param address: The destination to which to send the datagram. @type address: L{tuple} of (L{bytes}, L{int}) @return: The address from which the UDP datagram was sent. @rtype: L{tuple} of (L{bytes}, L{int}) """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(("172.16.0.1", 0)) s.sendto(datagram, address) return s.getsockname() def receiveUDP(self, fileno, host, port): """ Use the platform network stack to receive a datagram sent to the given address. @param fileno: The file descriptor of the tunnel used to send the datagram. This is ignored because a real socket is used to receive the datagram. @type fileno: L{int} @param host: The IPv4 address at which the datagram will be received. @type host: L{bytes} @param port: The UDP port number at which the datagram will be received. @type port: L{int} @return: A L{socket.socket} which can be used to receive the specified datagram. """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind((host, port)) return s class RealDeviceTestsMixin: """ Define a mixin for use with test cases that require an L{_IInputOutputSystem} provider. This mixin hands out L{TestRealSystem} instances as the provider of that interface. """ skip = platformSkip def createSystem(self): """ Create a real I/O system that can be used to open real tunnel device provided by the underlying system and previously configured. @return: The newly created I/O system object. @rtype: L{TestRealSystem} """ return TestRealSystem() class RealDeviceWithProtocolInformationTests( RealDeviceTestsMixin, TunnelDeviceTestsMixin, SynchronousTestCase ): """ Run various tap-type tunnel unit tests, with "protocol information" (PI) turned on, against a real I/O system. """ _TUNNEL_DEVICE = b"tap-twtest-pi" _TUNNEL_LOCAL = b"172.16.1.1" _TUNNEL_REMOTE = b"172.16.1.2" # The PI flag is not an inherent part of the tunnel. It must be specified # by each user of the tunnel. Thus, we must also have an indication of # whether we want PI so the tests can properly initialize the tunnel # device. helper = TapHelper(_TUNNEL_REMOTE, _TUNNEL_LOCAL, pi=True) class RealDeviceWithoutProtocolInformationTests( RealDeviceTestsMixin, TunnelDeviceTestsMixin, SynchronousTestCase ): """ Run various tap-type tunnel unit tests, with "protocol information" (PI) turned off, against a real I/O system. """ _TUNNEL_DEVICE = b"tap-twtest" _TUNNEL_LOCAL = b"172.16.0.1" _TUNNEL_REMOTE = b"172.16.0.2" helper = TapHelper(_TUNNEL_REMOTE, _TUNNEL_LOCAL, pi=False) class TuntapPortTests(SynchronousTestCase): """ Tests for L{TuntapPort} behavior that is independent of the tunnel type. """ def test_interface(self): """ A L{TuntapPort} instance provides L{IListeningPort}. """ port = TuntapPort(b"device", EthernetProtocol()) self.assertTrue(verifyObject(IListeningPort, port)) def test_realSystem(self): """ When not initialized with an I/O system, L{TuntapPort} uses a L{_RealSystem}. """ port = TuntapPort(b"device", EthernetProtocol()) self.assertIsInstance(port._system, _RealSystem) class TunnelTestsMixin: """ A mixin defining tests for L{TuntapPort}. These tests run against L{MemoryIOSystem} (proven equivalent to the real thing by the tests above) to avoid performing any real I/O. """ def setUp(self): """ Create an in-memory I/O system and set up a L{TuntapPort} against it. """ self.name = b"tun0" self.system = MemoryIOSystem() self.system.registerSpecialDevice(Tunnel._DEVICE_NAME, Tunnel) self.protocol = self.factory.buildProtocol( TunnelAddress(self.helper.TUNNEL_TYPE, self.name) ) self.reactor = FSSetClock() self.port = TuntapPort( self.name, self.protocol, reactor=self.reactor, system=self.system ) def _tunnelTypeOnly(self, flags): """ Mask off any flags except for L{TunnelType.IFF_TUN} and L{TunnelType.IFF_TAP}. @param flags: Flags from L{TunnelType} to mask. @type flags: L{FlagConstant} @return: The flags given by C{flags} except the two type flags. @rtype: L{FlagConstant} """ return flags & (TunnelFlags.IFF_TUN | TunnelFlags.IFF_TAP) def test_startListeningOpensDevice(self): """ L{TuntapPort.startListening} opens the tunnel factory character special device C{"/dev/net/tun"} and configures it as a I{tun} tunnel. """ system = self.system self.port.startListening() tunnel = self.system.getTunnel(self.port) expected = ( system.O_RDWR | system.O_CLOEXEC | system.O_NONBLOCK, b"tun0" + b"\x00" * (_IFNAMSIZ - len(b"tun0")), self.port.interface, False, True, ) actual = ( tunnel.openFlags, tunnel.requestedName, tunnel.name, tunnel.blocking, tunnel.closeOnExec, ) self.assertEqual(expected, actual) def test_startListeningSetsConnected(self): """ L{TuntapPort.startListening} sets C{connected} on the port object to C{True}. """ self.port.startListening() self.assertTrue(self.port.connected) def test_startListeningConnectsProtocol(self): """ L{TuntapPort.startListening} calls C{makeConnection} on the protocol the port was initialized with, passing the port as an argument. """ self.port.startListening() self.assertIs(self.port, self.protocol.transport) def test_startListeningStartsReading(self): """ L{TuntapPort.startListening} passes the port instance to the reactor's C{addReader} method to begin watching the port's file descriptor for data to read. """ self.port.startListening() self.assertIn(self.port, self.reactor.getReaders()) def test_startListeningHandlesOpenFailure(self): """ L{TuntapPort.startListening} raises L{CannotListenError} if opening the tunnel factory character special device fails. """ self.system.permissions.remove("open") self.assertRaises(CannotListenError, self.port.startListening) def test_startListeningHandlesConfigureFailure(self): """ L{TuntapPort.startListening} raises L{CannotListenError} if the C{ioctl} call to configure the tunnel device fails. """ self.system.permissions.remove("ioctl") self.assertRaises(CannotListenError, self.port.startListening) def _stopPort(self, port): """ Verify that the C{stopListening} method of an L{IListeningPort} removes that port from the reactor's "readers" set and also that the L{Deferred} returned by that method fires with L{None}. @param port: The port object to stop. @type port: L{IListeningPort} provider """ stopped = port.stopListening() self.assertNotIn(port, self.reactor.getReaders()) # An unfortunate implementation detail self.reactor.advance(0) self.assertIsNone(self.successResultOf(stopped)) def test_stopListeningStopsReading(self): """ L{TuntapPort.stopListening} returns a L{Deferred} which fires after the port has been removed from the reactor's reader list by passing it to the reactor's C{removeReader} method. """ self.port.startListening() fileno = self.port.fileno() self._stopPort(self.port) self.assertNotIn(fileno, self.system._openFiles) def test_stopListeningUnsetsConnected(self): """ After the L{Deferred} returned by L{TuntapPort.stopListening} fires, the C{connected} attribute of the port object is set to C{False}. """ self.port.startListening() self._stopPort(self.port) self.assertFalse(self.port.connected) def test_stopListeningStopsProtocol(self): """ L{TuntapPort.stopListening} calls C{doStop} on the protocol the port was initialized with. """ self.port.startListening() self._stopPort(self.port) self.assertIsNone(self.protocol.transport) def test_stopListeningWhenStopped(self): """ L{TuntapPort.stopListening} returns a L{Deferred} which succeeds immediately if it is called when the port is not listening. """ stopped = self.port.stopListening() self.assertIsNone(self.successResultOf(stopped)) def test_multipleStopListening(self): """ It is safe and a no-op to call L{TuntapPort.stopListening} more than once with no intervening L{TuntapPort.startListening} call. """ self.port.startListening() self.port.stopListening() second = self.port.stopListening() self.reactor.advance(0) self.assertIsNone(self.successResultOf(second)) def test_loseConnection(self): """ L{TuntapPort.loseConnection} stops the port and is deprecated. """ self.port.startListening() self.port.loseConnection() # An unfortunate implementation detail self.reactor.advance(0) self.assertFalse(self.port.connected) warnings = self.flushWarnings([self.test_loseConnection]) self.assertEqual(DeprecationWarning, warnings[0]["category"]) self.assertEqual( "twisted.pair.tuntap.TuntapPort.loseConnection was deprecated " "in Twisted 14.0.0; please use twisted.pair.tuntap.TuntapPort." "stopListening instead", warnings[0]["message"], ) self.assertEqual(1, len(warnings)) def _stopsReadingTest(self, style): """ Test that L{TuntapPort.doRead} has no side-effects under a certain exception condition. @param style: An exception instance to arrange for the (python wrapper around the) underlying platform I{read} call to fail with. @raise C{self.failureException}: If there are any observable side-effects. """ self.port.startListening() tunnel = self.system.getTunnel(self.port) tunnel.nonBlockingExceptionStyle = style self.port.doRead() self.assertEqual([], self.protocol.received) def test_eagainStopsReading(self): """ Once L{TuntapPort.doRead} encounters an I{EAGAIN} errno from a C{read} call, it returns. """ self._stopsReadingTest(Tunnel.EAGAIN_STYLE) def test_ewouldblockStopsReading(self): """ Once L{TuntapPort.doRead} encounters an I{EWOULDBLOCK} errno from a C{read} call, it returns. """ self._stopsReadingTest(Tunnel.EWOULDBLOCK_STYLE) def test_eintrblockStopsReading(self): """ Once L{TuntapPort.doRead} encounters an I{EINTR} errno from a C{read} call, it returns. """ self._stopsReadingTest(Tunnel.EINTR_STYLE) def test_unhandledReadError(self): """ If L{Tuntap.doRead} encounters any exception other than one explicitly handled by the code, the exception propagates to the caller. """ class UnexpectedException(Exception): pass self.assertRaises( UnexpectedException, self._stopsReadingTest, UnexpectedException() ) def test_unhandledEnvironmentReadError(self): """ Just like C{test_unhandledReadError}, but for the case where the exception that is not explicitly handled happens to be of type C{EnvironmentError} (C{OSError} or C{IOError}). """ self.assertRaises( IOError, self._stopsReadingTest, IOError(EPERM, "Operation not permitted") ) def test_doReadSmallDatagram(self): """ L{TuntapPort.doRead} reads a datagram of fewer than C{TuntapPort.maxPacketSize} from the port's file descriptor and passes it to its protocol's C{datagramReceived} method. """ datagram = b"x" * (self.port.maxPacketSize - 1) self.port.startListening() tunnel = self.system.getTunnel(self.port) tunnel.readBuffer.append(datagram) self.port.doRead() self.assertEqual([datagram], self.protocol.received) def test_doReadLargeDatagram(self): """ L{TuntapPort.doRead} reads the first part of a datagram of more than C{TuntapPort.maxPacketSize} from the port's file descriptor and passes the truncated data to its protocol's C{datagramReceived} method. """ datagram = b"x" * self.port.maxPacketSize self.port.startListening() tunnel = self.system.getTunnel(self.port) tunnel.readBuffer.append(datagram + b"y") self.port.doRead() self.assertEqual([datagram], self.protocol.received) def test_doReadSeveralDatagrams(self): """ L{TuntapPort.doRead} reads several datagrams, of up to C{TuntapPort.maxThroughput} bytes total, before returning. """ values = cycle(iterbytes(b"abcdefghijklmnopqrstuvwxyz")) total = 0 datagrams = [] while total < self.port.maxThroughput: datagrams.append(next(values) * self.port.maxPacketSize) total += self.port.maxPacketSize self.port.startListening() tunnel = self.system.getTunnel(self.port) tunnel.readBuffer.extend(datagrams) tunnel.readBuffer.append(b"excessive datagram, not to be read") self.port.doRead() self.assertEqual(datagrams, self.protocol.received) def _datagramReceivedException(self): """ Deliver some data to a L{TuntapPort} hooked up to an application protocol that raises an exception from its C{datagramReceived} method. @return: Whatever L{AttributeError} exceptions are logged. """ self.port.startListening() self.system.getTunnel(self.port).readBuffer.append(b"ping") # Break the application logic self.protocol.received = None self.port.doRead() return self.flushLoggedErrors(AttributeError) def test_datagramReceivedException(self): """ If the protocol's C{datagramReceived} method raises an exception, the exception is logged. """ errors = self._datagramReceivedException() self.assertEqual(1, len(errors)) def test_datagramReceivedExceptionIdentifiesProtocol(self): """ The exception raised by C{datagramReceived} is logged with a message identifying the offending protocol. """ messages = [] addObserver(messages.append) self.addCleanup(removeObserver, messages.append) self._datagramReceivedException() error = next(m for m in messages if m["isError"]) message = textFromEventDict(error) self.assertEqual( "Unhandled exception from %s.datagramReceived" % (fullyQualifiedName(self.protocol.__class__),), message.splitlines()[0], ) def test_write(self): """ L{TuntapPort.write} sends a datagram into the tunnel. """ datagram = b"a b c d e f g" self.port.startListening() self.port.write(datagram) self.assertEqual( self.system.getTunnel(self.port).writeBuffer, deque([datagram]) ) def test_interruptedWrite(self): """ If the platform write call is interrupted (causing the Python wrapper to raise C{IOError} with errno set to C{EINTR}), the write is re-tried. """ self.port.startListening() tunnel = self.system.getTunnel(self.port) tunnel.pendingSignals.append(SIGINT) self.port.write(b"hello, world") self.assertEqual(deque([b"hello, world"]), tunnel.writeBuffer) def test_unhandledWriteError(self): """ Any exception raised by the underlying write call, except for EINTR, is propagated to the caller. """ self.port.startListening() tunnel = self.system.getTunnel(self.port) self.assertRaises( IOError, self.port.write, b"x" * tunnel.SEND_BUFFER_SIZE + b"y" ) def test_writeSequence(self): """ L{TuntapPort.writeSequence} sends a datagram into the tunnel by concatenating the byte strings in the list passed to it. """ datagram = [b"a", b"b", b"c", b"d"] self.port.startListening() self.port.writeSequence(datagram) self.assertEqual( self.system.getTunnel(self.port).writeBuffer, deque([b"".join(datagram)]) ) def test_getHost(self): """ L{TuntapPort.getHost} returns a L{TunnelAddress} including the tunnel's type and name. """ self.port.startListening() address = self.port.getHost() self.assertEqual( TunnelAddress( self._tunnelTypeOnly(self.helper.TUNNEL_TYPE), self.system.getTunnel(self.port).name, ), address, ) def test_listeningString(self): """ The string representation of a L{TuntapPort} instance includes the tunnel type and interface and the protocol associated with the port. """ self.port.startListening() self.assertRegex(str(self.port), fullyQualifiedName(self.protocol.__class__)) expected = " listening on {}/{}>".format( self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name, self.system.getTunnel(self.port).name, ) self.assertTrue(str(self.port).find(expected) != -1) def test_unlisteningString(self): """ The string representation of a L{TuntapPort} instance includes the tunnel type and interface and the protocol associated with the port. """ self.assertRegex(str(self.port), fullyQualifiedName(self.protocol.__class__)) expected = " not listening on {}/{}>".format( self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name, self.name, ) self.assertTrue(str(self.port).find(expected) != -1) def test_logPrefix(self): """ L{TuntapPort.logPrefix} returns a string identifying the application protocol and the type of tunnel. """ self.assertEqual( "%s (%s)" % ( self.protocol.__class__.__name__, self._tunnelTypeOnly(self.helper.TUNNEL_TYPE).name, ), self.port.logPrefix(), ) class TunnelAddressTests(SynchronousTestCase): """ Tests for L{TunnelAddress}. """ def test_interfaces(self): """ A L{TunnelAddress} instances provides L{IAddress}. """ self.assertTrue( verifyObject(IAddress, TunnelAddress(TunnelFlags.IFF_TAP, "tap0")) ) def test_indexing(self): """ A L{TunnelAddress} instance can be indexed to retrieve either the byte string C{"TUNTAP"} or the name of the tunnel interface, while triggering a deprecation warning. """ address = TunnelAddress(TunnelFlags.IFF_TAP, "tap0") self.assertEqual("TUNTAP", address[0]) self.assertEqual("tap0", address[1]) warnings = self.flushWarnings([self.test_indexing]) message = ( "TunnelAddress.__getitem__ is deprecated since Twisted 14.0.0 " "Use attributes instead." ) self.assertEqual(DeprecationWarning, warnings[0]["category"]) self.assertEqual(message, warnings[0]["message"]) self.assertEqual(DeprecationWarning, warnings[1]["category"]) self.assertEqual(message, warnings[1]["message"]) self.assertEqual(2, len(warnings)) def test_repr(self): """ The string representation of a L{TunnelAddress} instance includes the class name and the values of the C{type} and C{name} attributes. """ self.assertRegex( repr(TunnelAddress(TunnelFlags.IFF_TUN, name=b"device")), "TunnelAddress type=IFF_TUN name=b'device'>", ) class TunnelAddressEqualityTests(SynchronousTestCase): """ Tests for the implementation of equality (C{==} and C{!=}) for L{TunnelAddress}. """ def setUp(self): self.first = TunnelAddress(TunnelFlags.IFF_TUN, b"device") # Construct a different object representing IFF_TUN to make this a little # trickier. Two FlagConstants from the same container and with the same # value do not compare equal to each other. # # The implementation will have to compare their values directly until # https://twistedmatrix.com/trac/ticket/6878 is resolved. self.second = TunnelAddress( TunnelFlags.IFF_TUN | TunnelFlags.IFF_TUN, b"device" ) self.variedType = TunnelAddress(TunnelFlags.IFF_TAP, b"tap1") self.variedName = TunnelAddress(TunnelFlags.IFF_TUN, b"tun1") def test_selfComparesEqual(self): """ A L{TunnelAddress} compares equal to itself. """ self.assertTrue(self.first == self.first) def test_selfNotComparesNotEqual(self): """ A L{TunnelAddress} doesn't compare not equal to itself. """ self.assertFalse(self.first != self.first) def test_sameAttributesComparesEqual(self): """ Two L{TunnelAddress} instances with the same value for the C{type} and C{name} attributes compare equal to each other. """ self.assertTrue(self.first == self.second) def test_sameAttributesNotComparesNotEqual(self): """ Two L{TunnelAddress} instances with the same value for the C{type} and C{name} attributes don't compare not equal to each other. """ self.assertFalse(self.first != self.second) def test_differentTypeComparesNotEqual(self): """ Two L{TunnelAddress} instances that differ only by the value of their type don't compare equal to each other. """ self.assertFalse(self.first == self.variedType) def test_differentTypeNotComparesEqual(self): """ Two L{TunnelAddress} instances that differ only by the value of their type compare not equal to each other. """ self.assertTrue(self.first != self.variedType) def test_differentNameComparesNotEqual(self): """ Two L{TunnelAddress} instances that differ only by the value of their name don't compare equal to each other. """ self.assertFalse(self.first == self.variedName) def test_differentNameNotComparesEqual(self): """ Two L{TunnelAddress} instances that differ only by the value of their name compare not equal to each other. """ self.assertTrue(self.first != self.variedName) def test_differentClassNotComparesEqual(self): """ A L{TunnelAddress} doesn't compare equal to an instance of another class. """ self.assertFalse(self.first == self) def test_differentClassComparesNotEqual(self): """ A L{TunnelAddress} compares not equal to an instance of another class. """ self.assertTrue(self.first != self) @implementer(IRawPacketProtocol) class IPRecordingProtocol(AbstractDatagramProtocol): """ A protocol which merely records the datagrams delivered to it. """ def startProtocol(self): self.received = [] def datagramReceived( self, datagram, partial=False, dest=None, source=None, protocol=None ): self.received.append(datagram) def addProto(self, num, proto): # IRawPacketProtocol.addProto pass class TunTests(TunnelTestsMixin, SynchronousTestCase): """ Tests for L{TuntapPort} when used to open a Linux I{tun} tunnel. """ factory = Factory() # Type is wrong. See: https://twistedmatrix.com/trac/ticket/10008#ticket factory.protocol = IPRecordingProtocol # type: ignore[assignment] helper = TunHelper(None, None) class EthernetRecordingProtocol(EthernetProtocol): """ A protocol which merely records the datagrams delivered to it. """ def startProtocol(self): self.received = [] def datagramReceived(self, datagram, partial=False): self.received.append(datagram) class TapTests(TunnelTestsMixin, SynchronousTestCase): """ Tests for L{TuntapPort} when used to open a Linux I{tap} tunnel. """ factory = Factory() # Type is wrong. See: https://twistedmatrix.com/trac/ticket/10008#ticket factory.protocol = EthernetRecordingProtocol # type: ignore[assignment] helper = TapHelper(None, None, pi=False) class IOSystemTestsMixin: """ Tests that apply to any L{_IInputOutputSystem} implementation. """ def test_noSuchDevice(self): """ L{_IInputOutputSystem.open} raises L{OSError} when called with a non-existent device path. """ system = self.createSystem() self.assertRaises( OSError, system.open, b"/dev/there-is-no-such-device-ever", os.O_RDWR ) class MemoryIOSystemTests( IOSystemTestsMixin, SynchronousTestCase, FakeDeviceTestsMixin ): """ General L{_IInputOutputSystem} tests applied to L{MemoryIOSystem}. """ class RealIOSystemTests(IOSystemTestsMixin, SynchronousTestCase, RealDeviceTestsMixin): """ General L{_IInputOutputSystem} tests applied to L{_RealSystem}. """