testing.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tools for automated testing of L{twisted.pair}-based applications.
  5. """
  6. import struct
  7. import socket
  8. from errno import (
  9. EPERM, EAGAIN, EWOULDBLOCK, ENOSYS, EBADF, EINVAL, EINTR, ENOBUFS)
  10. from collections import deque
  11. from functools import wraps
  12. from zope.interface import implementer
  13. from twisted.internet.protocol import DatagramProtocol
  14. from twisted.pair.ethernet import EthernetProtocol
  15. from twisted.pair.rawudp import RawUDPProtocol
  16. from twisted.pair.ip import IPProtocol
  17. from twisted.pair.tuntap import (
  18. _IFNAMSIZ, _TUNSETIFF, _IInputOutputSystem, TunnelFlags)
  19. from twisted.python.compat import nativeString
  20. # The number of bytes in the "protocol information" header that may be present
  21. # on datagrams read from a tunnel device. This is two bytes of flags followed
  22. # by two bytes of protocol identification. All this code does with this
  23. # information is use it to discard the header.
  24. _PI_SIZE = 4
  25. def _H(n):
  26. """
  27. Pack an integer into a network-order two-byte string.
  28. @param n: The integer to pack. Only values that fit into 16 bits are
  29. supported.
  30. @return: The packed representation of the integer.
  31. @rtype: L{bytes}
  32. """
  33. return struct.pack('>H', n)
  34. _IPv4 = 0x0800
  35. def _ethernet(src, dst, protocol, payload):
  36. """
  37. Construct an ethernet frame.
  38. @param src: The source ethernet address, encoded.
  39. @type src: L{bytes}
  40. @param dst: The destination ethernet address, encoded.
  41. @type dst: L{bytes}
  42. @param protocol: The protocol number of the payload of this datagram.
  43. @type protocol: L{int}
  44. @param payload: The content of the ethernet frame (such as an IP datagram).
  45. @type payload: L{bytes}
  46. @return: The full ethernet frame.
  47. @rtype: L{bytes}
  48. """
  49. return dst + src + _H(protocol) + payload
  50. def _ip(src, dst, payload):
  51. """
  52. Construct an IP datagram with the given source, destination, and
  53. application payload.
  54. @param src: The source IPv4 address as a dotted-quad string.
  55. @type src: L{bytes}
  56. @param dst: The destination IPv4 address as a dotted-quad string.
  57. @type dst: L{bytes}
  58. @param payload: The content of the IP datagram (such as a UDP datagram).
  59. @type payload: L{bytes}
  60. @return: An IP datagram header and payload.
  61. @rtype: L{bytes}
  62. """
  63. ipHeader = (
  64. # Version and header length, 4 bits each
  65. b'\x45'
  66. # Differentiated services field
  67. b'\x00'
  68. # Total length
  69. + _H(20 + len(payload))
  70. + b'\x00\x01\x00\x00\x40\x11'
  71. # Checksum
  72. + _H(0)
  73. # Source address
  74. + socket.inet_pton(socket.AF_INET, nativeString(src))
  75. # Destination address
  76. + socket.inet_pton(socket.AF_INET, nativeString(dst)))
  77. # Total all of the 16-bit integers in the header
  78. checksumStep1 = sum(struct.unpack('!10H', ipHeader))
  79. # Pull off the carry
  80. carry = checksumStep1 >> 16
  81. # And add it to what was left over
  82. checksumStep2 = (checksumStep1 & 0xFFFF) + carry
  83. # Compute the one's complement sum
  84. checksumStep3 = checksumStep2 ^ 0xFFFF
  85. # Reconstruct the IP header including the correct checksum so the platform
  86. # IP stack, if there is one involved in this test, doesn't drop it on the
  87. # floor as garbage.
  88. ipHeader = (
  89. ipHeader[:10] +
  90. struct.pack('!H', checksumStep3) +
  91. ipHeader[12:])
  92. return ipHeader + payload
  93. def _udp(src, dst, payload):
  94. """
  95. Construct a UDP datagram with the given source, destination, and
  96. application payload.
  97. @param src: The source port number.
  98. @type src: L{int}
  99. @param dst: The destination port number.
  100. @type dst: L{int}
  101. @param payload: The content of the UDP datagram.
  102. @type payload: L{bytes}
  103. @return: A UDP datagram header and payload.
  104. @rtype: L{bytes}
  105. """
  106. udpHeader = (
  107. # Source port
  108. _H(src)
  109. # Destination port
  110. + _H(dst)
  111. # Length
  112. + _H(len(payload) + 8)
  113. # Checksum
  114. + _H(0))
  115. return udpHeader + payload
  116. class Tunnel(object):
  117. """
  118. An in-memory implementation of a tun or tap device.
  119. @cvar _DEVICE_NAME: A string representing the conventional filesystem entry
  120. for the tunnel factory character special device.
  121. @type _DEVICE_NAME: C{bytes}
  122. """
  123. _DEVICE_NAME = b"/dev/net/tun"
  124. # Between POSIX and Python, there are 4 combinations. Here are two, at
  125. # least.
  126. EAGAIN_STYLE = IOError(EAGAIN, "Resource temporarily unavailable")
  127. EWOULDBLOCK_STYLE = OSError(EWOULDBLOCK, "Operation would block")
  128. # Oh yea, and then there's the case where maybe we would've read, but
  129. # someone sent us a signal instead.
  130. EINTR_STYLE = IOError(EINTR, "Interrupted function call")
  131. nonBlockingExceptionStyle = EAGAIN_STYLE
  132. SEND_BUFFER_SIZE = 1024
  133. def __init__(self, system, openFlags, fileMode):
  134. """
  135. @param system: An L{_IInputOutputSystem} provider to use to perform I/O.
  136. @param openFlags: Any flags to apply when opening the tunnel device.
  137. See C{os.O_*}.
  138. @type openFlags: L{int}
  139. @param fileMode: ignored
  140. """
  141. self.system = system
  142. # Drop fileMode on the floor - evidence and logic suggest it is
  143. # irrelevant with respect to /dev/net/tun
  144. self.openFlags = openFlags
  145. self.tunnelMode = None
  146. self.requestedName = None
  147. self.name = None
  148. self.readBuffer = deque()
  149. self.writeBuffer = deque()
  150. self.pendingSignals = deque()
  151. @property
  152. def blocking(self):
  153. """
  154. If the file descriptor for this tunnel is open in blocking mode,
  155. C{True}. C{False} otherwise.
  156. """
  157. return not (self.openFlags & self.system.O_NONBLOCK)
  158. @property
  159. def closeOnExec(self):
  160. """
  161. If the file descriptor for this tunnel is marked as close-on-exec,
  162. C{True}. C{False} otherwise.
  163. """
  164. return bool(self.openFlags & self.system.O_CLOEXEC)
  165. def addToReadBuffer(self, datagram):
  166. """
  167. Deliver a datagram to this tunnel's read buffer. This makes it
  168. available to be read later using the C{read} method.
  169. @param datagram: The IPv4 datagram to deliver. If the mode of this
  170. tunnel is TAP then ethernet framing will be added automatically.
  171. @type datagram: L{bytes}
  172. """
  173. # TAP devices also include ethernet framing.
  174. if self.tunnelMode & TunnelFlags.IFF_TAP.value:
  175. datagram = _ethernet(
  176. src=b'\x00' * 6, dst=b'\xff' * 6, protocol=_IPv4,
  177. payload=datagram)
  178. self.readBuffer.append(datagram)
  179. def read(self, limit):
  180. """
  181. Read a datagram out of this tunnel.
  182. @param limit: The maximum number of bytes from the datagram to return.
  183. If the next datagram is larger than this, extra bytes are dropped
  184. and lost forever.
  185. @type limit: L{int}
  186. @raise OSError: Any of the usual I/O problems can result in this
  187. exception being raised with some particular error number set.
  188. @raise IOError: Any of the usual I/O problems can result in this
  189. exception being raised with some particular error number set.
  190. @return: The datagram which was read from the tunnel. If the tunnel
  191. mode does not include L{TunnelFlags.IFF_NO_PI} then the datagram is
  192. prefixed with a 4 byte PI header.
  193. @rtype: L{bytes}
  194. """
  195. if self.readBuffer:
  196. if self.tunnelMode & TunnelFlags.IFF_NO_PI.value:
  197. header = b""
  198. else:
  199. # Synthesize a PI header to include in the result. Nothing in
  200. # twisted.pair uses the PI information yet so we can synthesize
  201. # something incredibly boring (ie 32 bits of 0).
  202. header = b"\x00" * _PI_SIZE
  203. limit -= 4
  204. return header + self.readBuffer.popleft()[:limit]
  205. elif self.blocking:
  206. raise NotImplementedError()
  207. else:
  208. raise self.nonBlockingExceptionStyle
  209. def write(self, datagram):
  210. """
  211. Write a datagram into this tunnel.
  212. @param datagram: The datagram to write.
  213. @type datagram: L{bytes}
  214. @raise IOError: Any of the usual I/O problems can result in this
  215. exception being raised with some particular error number set.
  216. @return: The number of bytes of the datagram which were written.
  217. @rtype: L{int}
  218. """
  219. if self.pendingSignals:
  220. self.pendingSignals.popleft()
  221. raise IOError(EINTR, "Interrupted system call")
  222. if len(datagram) > self.SEND_BUFFER_SIZE:
  223. raise IOError(ENOBUFS, "No buffer space available")
  224. self.writeBuffer.append(datagram)
  225. return len(datagram)
  226. def _privileged(original):
  227. """
  228. Wrap a L{MemoryIOSystem} method with permission-checking logic. The
  229. returned function will check C{self.permissions} and raise L{IOError} with
  230. L{errno.EPERM} if the function name is not listed as an available
  231. permission.
  232. @param original: The L{MemoryIOSystem} instance to wrap.
  233. @return: A wrapper around C{original} that applies permission checks.
  234. """
  235. @wraps(original)
  236. def permissionChecker(self, *args, **kwargs):
  237. if original.__name__ not in self.permissions:
  238. raise IOError(EPERM, "Operation not permitted")
  239. return original(self, *args, **kwargs)
  240. return permissionChecker
  241. @implementer(_IInputOutputSystem)
  242. class MemoryIOSystem(object):
  243. """
  244. An in-memory implementation of basic I/O primitives, useful in the context
  245. of unit testing as a drop-in replacement for parts of the C{os} module.
  246. @ivar _devices:
  247. @ivar _openFiles:
  248. @ivar permissions:
  249. @ivar _counter:
  250. """
  251. _counter = 8192
  252. O_RDWR = 1 << 0
  253. O_NONBLOCK = 1 << 1
  254. O_CLOEXEC = 1 << 2
  255. def __init__(self):
  256. self._devices = {}
  257. self._openFiles = {}
  258. self.permissions = set(['open', 'ioctl'])
  259. def getTunnel(self, port):
  260. """
  261. Get the L{Tunnel} object associated with the given L{TuntapPort}.
  262. @param port: A L{TuntapPort} previously initialized using this
  263. L{MemoryIOSystem}.
  264. @return: The tunnel object created by a prior use of C{open} on this
  265. object on the tunnel special device file.
  266. @rtype: L{Tunnel}
  267. """
  268. return self._openFiles[port.fileno()]
  269. def registerSpecialDevice(self, name, cls):
  270. """
  271. Specify a class which will be used to handle I/O to a device of a
  272. particular name.
  273. @param name: The filesystem path name of the device.
  274. @type name: L{bytes}
  275. @param cls: A class (like L{Tunnel}) to instantiated whenever this
  276. device is opened.
  277. """
  278. self._devices[name] = cls
  279. @_privileged
  280. def open(self, name, flags, mode=None):
  281. """
  282. A replacement for C{os.open}. This initializes state in this
  283. L{MemoryIOSystem} which will be reflected in the behavior of the other
  284. file descriptor-related methods (eg L{MemoryIOSystem.read},
  285. L{MemoryIOSystem.write}, etc).
  286. @param name: A string giving the name of the file to open.
  287. @type name: C{bytes}
  288. @param flags: The flags with which to open the file.
  289. @type flags: C{int}
  290. @param mode: The mode with which to open the file.
  291. @type mode: C{int}
  292. @raise OSError: With C{ENOSYS} if the file is not a recognized special
  293. device file.
  294. @return: A file descriptor associated with the newly opened file
  295. description.
  296. @rtype: L{int}
  297. """
  298. if name in self._devices:
  299. fd = self._counter
  300. self._counter += 1
  301. self._openFiles[fd] = self._devices[name](self, flags, mode)
  302. return fd
  303. raise OSError(ENOSYS, "Function not implemented")
  304. def read(self, fd, limit):
  305. """
  306. Try to read some bytes out of one of the in-memory buffers which may
  307. previously have been populated by C{write}.
  308. @see: L{os.read}
  309. """
  310. try:
  311. return self._openFiles[fd].read(limit)
  312. except KeyError:
  313. raise OSError(EBADF, "Bad file descriptor")
  314. def write(self, fd, data):
  315. """
  316. Try to add some bytes to one of the in-memory buffers to be accessed by
  317. a later C{read} call.
  318. @see: L{os.write}
  319. """
  320. try:
  321. return self._openFiles[fd].write(data)
  322. except KeyError:
  323. raise OSError(EBADF, "Bad file descriptor")
  324. def close(self, fd):
  325. """
  326. Discard the in-memory buffer and other in-memory state for the given
  327. file descriptor.
  328. @see: L{os.close}
  329. """
  330. try:
  331. del self._openFiles[fd]
  332. except KeyError:
  333. raise OSError(EBADF, "Bad file descriptor")
  334. @_privileged
  335. def ioctl(self, fd, request, args):
  336. """
  337. Perform some configuration change to the in-memory state for the given
  338. file descriptor.
  339. @see: L{fcntl.ioctl}
  340. """
  341. try:
  342. tunnel = self._openFiles[fd]
  343. except KeyError:
  344. raise IOError(EBADF, "Bad file descriptor")
  345. if request != _TUNSETIFF:
  346. raise IOError(EINVAL, "Request or args is not valid.")
  347. name, mode = struct.unpack('%dsH' % (_IFNAMSIZ,), args)
  348. tunnel.tunnelMode = mode
  349. tunnel.requestedName = name
  350. tunnel.name = name[:_IFNAMSIZ - 3] + b"123"
  351. return struct.pack('%dsH' % (_IFNAMSIZ,), tunnel.name, mode)
  352. def sendUDP(self, datagram, address):
  353. """
  354. Write an ethernet frame containing an ip datagram containing a udp
  355. datagram containing the given payload, addressed to the given address,
  356. to a tunnel device previously opened on this I/O system.
  357. @param datagram: A UDP datagram payload to send.
  358. @type datagram: L{bytes}
  359. @param address: The destination to which to send the datagram.
  360. @type address: L{tuple} of (L{bytes}, L{int})
  361. @return: A two-tuple giving the address from which gives the address
  362. from which the datagram was sent.
  363. @rtype: L{tuple} of (L{bytes}, L{int})
  364. """
  365. # Just make up some random thing
  366. srcIP = '10.1.2.3'
  367. srcPort = 21345
  368. serialized = _ip(
  369. src=srcIP, dst=address[0], payload=_udp(
  370. src=srcPort, dst=address[1], payload=datagram))
  371. openFiles = list(self._openFiles.values())
  372. openFiles[0].addToReadBuffer(serialized)
  373. return (srcIP, srcPort)
  374. def receiveUDP(self, fileno, host, port):
  375. """
  376. Get a socket-like object which can be used to receive a datagram sent
  377. from the given address.
  378. @param fileno: A file descriptor representing a tunnel device which the
  379. datagram will be received via.
  380. @type fileno: L{int}
  381. @param host: The IPv4 address to which the datagram was sent.
  382. @type host: L{bytes}
  383. @param port: The UDP port number to which the datagram was sent.
  384. received.
  385. @type port: L{int}
  386. @return: A L{socket.socket}-like object which can be used to receive
  387. the specified datagram.
  388. """
  389. return _FakePort(self, fileno)
  390. class _FakePort(object):
  391. """
  392. A socket-like object which can be used to read UDP datagrams from
  393. tunnel-like file descriptors managed by a L{MemoryIOSystem}.
  394. """
  395. def __init__(self, system, fileno):
  396. self._system = system
  397. self._fileno = fileno
  398. def recv(self, nbytes):
  399. """
  400. Receive a datagram sent to this port using the L{MemoryIOSystem} which
  401. created this object.
  402. This behaves like L{socket.socket.recv} but the data being I{sent} and
  403. I{received} only passes through various memory buffers managed by this
  404. object and L{MemoryIOSystem}.
  405. @see: L{socket.socket.recv}
  406. """
  407. data = self._system._openFiles[self._fileno].writeBuffer.popleft()
  408. datagrams = []
  409. receiver = DatagramProtocol()
  410. def capture(datagram, address):
  411. datagrams.append(datagram)
  412. receiver.datagramReceived = capture
  413. udp = RawUDPProtocol()
  414. udp.addProto(12345, receiver)
  415. ip = IPProtocol()
  416. ip.addProto(17, udp)
  417. mode = self._system._openFiles[self._fileno].tunnelMode
  418. if (mode & TunnelFlags.IFF_TAP.value):
  419. ether = EthernetProtocol()
  420. ether.addProto(0x800, ip)
  421. datagramReceived = ether.datagramReceived
  422. else:
  423. datagramReceived = lambda data: ip.datagramReceived(
  424. data, None, None, None, None)
  425. dataHasPI = not (mode & TunnelFlags.IFF_NO_PI.value)
  426. if dataHasPI:
  427. # datagramReceived can't handle the PI, get rid of it.
  428. data = data[_PI_SIZE:]
  429. datagramReceived(data)
  430. return datagrams[0][:nbytes]