port_scanner.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import queue
  2. import socket
  3. import threading
  4. TIMEOUT = 1
  5. MAX_THREADS = 64
  6. class Scanner(object):
  7. def __init__(self, host):
  8. self.host = host
  9. # The job queue contains tuples of the form (protocol, port). We might
  10. # be able to remove this queue by using a lock in _get_next_port(), but
  11. # that might not be as optimized as accessing a Queue. Since we are too
  12. # lazy to write and run a benchmark, let's just do this.
  13. self.job_queue = queue.Queue()
  14. for job in self._get_next_port():
  15. self.job_queue.put(job)
  16. self.opened_ports_queue = queue.Queue()
  17. def _get_next_port(self):
  18. """Return the next (protocol, port) tuple to test."""
  19. # TODO: Improve this function :)
  20. # TODO: There are probably more "common" ports used for VPNs.
  21. self.common_ports = [80, 443, 8080]
  22. for port in self.common_ports:
  23. yield ('udp', port)
  24. for port in self.common_ports:
  25. yield ('tcp', port)
  26. # TODO: if necessary, check all possible ports
  27. def _compute_n_threads(self):
  28. """Compute the number of threads that can be used to scan ports.
  29. This is not as trivial as it seems: we need as much parallelism as
  30. possible, but we may not be able to open too many connections at once
  31. on some networks. Also, we do not want to overload the machine.
  32. """
  33. # TODO: Replace this stub by a real function.
  34. return 4
  35. def scan(self, n_threads=0):
  36. if n_threads <= 0 or n_threads > MAX_THREADS:
  37. n_threads = self._compute_n_threads()
  38. threads = []
  39. for i in range(n_threads):
  40. t = threading.Thread(target=self._scan)
  41. threads.append(t)
  42. t.start()
  43. self.job_queue.join()
  44. for t in threads:
  45. t.join()
  46. # TODO: We could probably start yielding the results while the workers
  47. # are scanning the network, but that would make the code a bit more
  48. # complex and would be more error-prone. Being a bit slow seems OK for
  49. # now :)
  50. while True:
  51. try:
  52. yield self.opened_ports_queue.get(block=False)
  53. except queue.Empty:
  54. break
  55. else:
  56. self.opened_ports_queue.task_done()
  57. def _scan(self):
  58. while True:
  59. try:
  60. protocol, port = self.job_queue.get(timeout=1)
  61. if protocol == 'udp':
  62. opened = self._scan_udp(port)
  63. elif protocol == 'tcp':
  64. opened = self._scan_tcp(port)
  65. else:
  66. raise ValueError('Unknown protocol %s' % protocol)
  67. if opened:
  68. self.opened_ports_queue.put((protocol, port))
  69. self.job_queue.task_done()
  70. except queue.Empty:
  71. break
  72. def _scan_tcp(self, port):
  73. """Return True if PORT is opened in TCP."""
  74. try:
  75. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  76. s.settimeout(TIMEOUT)
  77. s.connect((self.host, port))
  78. s.send(b'x')
  79. s.recvfrom(1)
  80. except (ConnectionRefusedError, socket.timeout):
  81. return False
  82. finally:
  83. s.close()
  84. return True
  85. def _scan_udp(self, port):
  86. """Return True if PORT is opened in UDP."""
  87. try:
  88. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  89. s.settimeout(TIMEOUT)
  90. # The message is a valid payload sent to OpenVPN in order to
  91. # trigger an answer from the server, and is computed like this:
  92. # msg = '38 36 d9 fc 5f e3 fe f7 09 00 00 00 00'
  93. # msg = bytes.fromhex(msg.replace(' ', ''))
  94. msg = b'86\xd9\xfc_\xe3\xfe\xf7\t\x00\x00\x00\x00'
  95. s.sendto(msg, (self.host, port))
  96. s.recvfrom(1)
  97. except socket.timeout:
  98. return False
  99. return True