sshtunnel.py 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. *sshtunnel* - Initiate SSH tunnels via a remote gateway.
  5. ``sshtunnel`` works by opening a port forwarding SSH connection in the
  6. background, using threads.
  7. The connection(s) are closed when explicitly calling the
  8. :meth:`SSHTunnelForwarder.stop` method or using it as a context.
  9. """
  10. import os
  11. import sys
  12. import socket
  13. import getpass
  14. import logging
  15. import argparse
  16. import warnings
  17. import threading
  18. from select import select
  19. from binascii import hexlify
  20. import paramiko
  21. if sys.version_info[0] < 3: # pragma: no cover
  22. import Queue as queue
  23. import SocketServer as socketserver
  24. string_types = basestring, # noqa
  25. input_ = raw_input # noqa
  26. else: # pragma: no cover
  27. import queue
  28. import socketserver
  29. string_types = str
  30. input_ = input
  31. __version__ = '0.4.0'
  32. __author__ = 'pahaz'
  33. #: Timeout (seconds) for transport socket (``socket.settimeout``)
  34. SSH_TIMEOUT = 0.1 # ``None`` may cause a block of transport thread
  35. #: Timeout (seconds) for tunnel connection (open_channel timeout)
  36. TUNNEL_TIMEOUT = 10.0
  37. _DAEMON = True #: Use daemon threads in connections
  38. _CONNECTION_COUNTER = 1
  39. _LOCK = threading.Lock()
  40. _DEPRECATIONS = {
  41. 'ssh_address': 'ssh_address_or_host',
  42. 'ssh_host': 'ssh_address_or_host',
  43. 'ssh_private_key': 'ssh_pkey',
  44. 'raise_exception_if_any_forwarder_have_a_problem': 'mute_exceptions'
  45. }
  46. # logging
  47. DEFAULT_LOGLEVEL = logging.ERROR #: default level if no logger passed (ERROR)
  48. TRACE_LEVEL = 1
  49. logging.addLevelName(TRACE_LEVEL, 'TRACE')
  50. DEFAULT_SSH_DIRECTORY = '~/.ssh'
  51. _StreamServer = socketserver.UnixStreamServer if os.name == 'posix' \
  52. else socketserver.TCPServer
  53. #: Path of optional ssh configuration file
  54. DEFAULT_SSH_DIRECTORY = '~/.ssh'
  55. SSH_CONFIG_FILE = os.path.join(DEFAULT_SSH_DIRECTORY, 'config')
  56. ########################
  57. # #
  58. # Utils #
  59. # #
  60. ########################
  61. def check_host(host):
  62. assert isinstance(host, string_types), 'IP is not a string ({0})'.format(
  63. type(host).__name__
  64. )
  65. def check_port(port):
  66. assert isinstance(port, int), 'PORT is not a number'
  67. assert port >= 0, 'PORT < 0 ({0})'.format(port)
  68. def check_address(address):
  69. """
  70. Check if the format of the address is correct
  71. Arguments:
  72. address (tuple):
  73. (``str``, ``int``) representing an IP address and port,
  74. respectively
  75. .. note::
  76. alternatively a local ``address`` can be a ``str`` when working
  77. with UNIX domain sockets, if supported by the platform
  78. Raises:
  79. ValueError:
  80. raised when address has an incorrect format
  81. Example:
  82. >>> check_address(('127.0.0.1', 22))
  83. """
  84. if isinstance(address, tuple):
  85. check_host(address[0])
  86. check_port(address[1])
  87. elif isinstance(address, string_types):
  88. if os.name != 'posix':
  89. raise ValueError('Platform does not support UNIX domain sockets')
  90. if not (os.path.exists(address) or
  91. os.access(os.path.dirname(address), os.W_OK)):
  92. raise ValueError('ADDRESS not a valid socket domain socket ({0})'
  93. .format(address))
  94. else:
  95. raise ValueError('ADDRESS is not a tuple, string, or character buffer '
  96. '({0})'.format(type(address).__name__))
  97. def check_addresses(address_list, is_remote=False):
  98. """
  99. Check if the format of the addresses is correct
  100. Arguments:
  101. address_list (list[tuple]):
  102. Sequence of (``str``, ``int``) pairs, each representing an IP
  103. address and port respectively
  104. .. note::
  105. when supported by the platform, one or more of the elements in
  106. the list can be of type ``str``, representing a valid UNIX
  107. domain socket
  108. is_remote (boolean):
  109. Whether or not the address list
  110. Raises:
  111. AssertionError:
  112. raised when ``address_list`` contains an invalid element
  113. ValueError:
  114. raised when any address in the list has an incorrect format
  115. Example:
  116. >>> check_addresses([('127.0.0.1', 22), ('127.0.0.1', 2222)])
  117. """
  118. assert all(isinstance(x, (tuple, string_types)) for x in address_list)
  119. if (is_remote and any(isinstance(x, string_types) for x in address_list)):
  120. raise AssertionError('UNIX domain sockets not allowed for remote'
  121. 'addresses')
  122. for address in address_list:
  123. check_address(address)
  124. def create_logger(logger=None,
  125. loglevel=None,
  126. capture_warnings=True,
  127. add_paramiko_handler=True):
  128. """
  129. Attach or create a new logger and add a console handler if not present
  130. Arguments:
  131. logger (Optional[logging.Logger]):
  132. :class:`logging.Logger` instance; a new one is created if this
  133. argument is empty
  134. loglevel (Optional[str or int]):
  135. :class:`logging.Logger`'s level, either as a string (i.e.
  136. ``ERROR``) or in numeric format (10 == ``DEBUG``)
  137. .. note:: a value of 1 == ``TRACE`` enables Tracing mode
  138. capture_warnings (boolean):
  139. Enable/disable capturing the events logged by the warnings module
  140. into ``logger``'s handlers
  141. Default: True
  142. .. note:: ignored in python 2.6
  143. add_paramiko_handler (boolean):
  144. Whether or not add a console handler for ``paramiko.transport``'s
  145. logger if no handler present
  146. Default: True
  147. Return:
  148. :class:`logging.Logger`
  149. """
  150. logger = logger or logging.getLogger(
  151. 'sshtunnel.SSHTunnelForwarder'
  152. )
  153. if not any(isinstance(x, logging.Handler) for x in logger.handlers):
  154. logger.setLevel(loglevel or DEFAULT_LOGLEVEL)
  155. console_handler = logging.StreamHandler()
  156. _add_handler(logger,
  157. handler=console_handler,
  158. loglevel=loglevel or DEFAULT_LOGLEVEL)
  159. if loglevel: # override if loglevel was set
  160. logger.setLevel(loglevel)
  161. for handler in logger.handlers:
  162. handler.setLevel(loglevel)
  163. if add_paramiko_handler:
  164. _check_paramiko_handlers(logger=logger)
  165. if capture_warnings and sys.version_info >= (2, 7):
  166. logging.captureWarnings(True)
  167. pywarnings = logging.getLogger('py.warnings')
  168. pywarnings.handlers.extend(logger.handlers)
  169. return logger
  170. def _add_handler(logger, handler=None, loglevel=None):
  171. """
  172. Add a handler to an existing logging.Logger object
  173. """
  174. handler.setLevel(loglevel or DEFAULT_LOGLEVEL)
  175. if handler.level <= logging.DEBUG:
  176. _fmt = '%(asctime)s| %(levelname)-4.3s|%(threadName)10.9s/' \
  177. '%(lineno)04d@%(module)-10.9s| %(message)s'
  178. handler.setFormatter(logging.Formatter(_fmt))
  179. else:
  180. handler.setFormatter(logging.Formatter(
  181. '%(asctime)s| %(levelname)-8s| %(message)s'
  182. ))
  183. logger.addHandler(handler)
  184. def _check_paramiko_handlers(logger=None):
  185. """
  186. Add a console handler for paramiko.transport's logger if not present
  187. """
  188. paramiko_logger = logging.getLogger('paramiko.transport')
  189. if not paramiko_logger.handlers:
  190. if logger:
  191. paramiko_logger.handlers = logger.handlers
  192. else:
  193. console_handler = logging.StreamHandler()
  194. console_handler.setFormatter(
  195. logging.Formatter('%(asctime)s | %(levelname)-8s| PARAMIKO: '
  196. '%(lineno)03d@%(module)-10s| %(message)s')
  197. )
  198. paramiko_logger.addHandler(console_handler)
  199. def address_to_str(address):
  200. if isinstance(address, tuple):
  201. return '{0[0]}:{0[1]}'.format(address)
  202. return str(address)
  203. def get_connection_id():
  204. global _CONNECTION_COUNTER
  205. with _LOCK:
  206. uid = _CONNECTION_COUNTER
  207. _CONNECTION_COUNTER += 1
  208. return uid
  209. def _remove_none_values(dictionary):
  210. """ Remove dictionary keys whose value is None """
  211. return list(map(dictionary.pop,
  212. [i for i in dictionary if dictionary[i] is None]))
  213. ########################
  214. # #
  215. # Errors #
  216. # #
  217. ########################
  218. class BaseSSHTunnelForwarderError(Exception):
  219. """ Exception raised by :class:`SSHTunnelForwarder` errors """
  220. def __init__(self, *args, **kwargs):
  221. self.value = kwargs.pop('value', args[0] if args else '')
  222. def __str__(self):
  223. return self.value
  224. class HandlerSSHTunnelForwarderError(BaseSSHTunnelForwarderError):
  225. """ Exception for Tunnel forwarder errors """
  226. pass
  227. ########################
  228. # #
  229. # Handlers #
  230. # #
  231. ########################
  232. class _ForwardHandler(socketserver.BaseRequestHandler):
  233. """ Base handler for tunnel connections """
  234. remote_address = None
  235. ssh_transport = None
  236. logger = None
  237. info = None
  238. def _redirect(self, chan):
  239. while chan.active:
  240. rqst, _, _ = select([self.request, chan], [], [], 5)
  241. if self.request in rqst:
  242. data = self.request.recv(1024)
  243. if not data:
  244. self.logger.log(
  245. TRACE_LEVEL,
  246. '>>> OUT {0} recv empty data >>>'.format(self.info)
  247. )
  248. break
  249. self.logger.log(
  250. TRACE_LEVEL,
  251. '>>> OUT {0} send to {1}: {2} >>>'.format(
  252. self.info,
  253. self.remote_address,
  254. hexlify(data)
  255. )
  256. )
  257. chan.sendall(data)
  258. if chan in rqst: # else
  259. if not chan.recv_ready():
  260. self.logger.log(
  261. TRACE_LEVEL,
  262. '<<< IN {0} recv is not ready <<<'.format(self.info)
  263. )
  264. break
  265. data = chan.recv(1024)
  266. self.logger.log(
  267. TRACE_LEVEL,
  268. '<<< IN {0} recv: {1} <<<'.format(self.info, hexlify(data))
  269. )
  270. self.request.sendall(data)
  271. def handle(self):
  272. uid = get_connection_id()
  273. self.info = '#{0} <-- {1}'.format(uid, self.client_address or
  274. self.server.local_address)
  275. src_address = self.request.getpeername()
  276. if not isinstance(src_address, tuple):
  277. src_address = ('dummy', 12345)
  278. try:
  279. chan = self.ssh_transport.open_channel(
  280. kind='direct-tcpip',
  281. dest_addr=self.remote_address,
  282. src_addr=src_address,
  283. timeout=TUNNEL_TIMEOUT
  284. )
  285. except Exception as e: # pragma: no cover
  286. msg_tupe = 'ssh ' if isinstance(e, paramiko.SSHException) else ''
  287. exc_msg = 'open new channel {0}error: {1}'.format(msg_tupe, e)
  288. log_msg = '{0} {1}'.format(self.info, exc_msg)
  289. self.logger.log(TRACE_LEVEL, log_msg)
  290. raise HandlerSSHTunnelForwarderError(exc_msg)
  291. self.logger.log(TRACE_LEVEL, '{0} connected'.format(self.info))
  292. try:
  293. self._redirect(chan)
  294. except socket.error:
  295. # Sometimes a RST is sent and a socket error is raised, treat this
  296. # exception. It was seen that a 3way FIN is processed later on, so
  297. # no need to make an ordered close of the connection here or raise
  298. # the exception beyond this point...
  299. self.logger.log(TRACE_LEVEL, '{0} sending RST'.format(self.info))
  300. except Exception as e:
  301. self.logger.log(TRACE_LEVEL,
  302. '{0} error: {1}'.format(self.info, repr(e)))
  303. finally:
  304. chan.close()
  305. self.request.close()
  306. self.logger.log(TRACE_LEVEL,
  307. '{0} connection closed.'.format(self.info))
  308. class _ForwardServer(socketserver.TCPServer): # Not Threading
  309. """
  310. Non-threading version of the forward server
  311. """
  312. allow_reuse_address = True # faster rebinding
  313. def __init__(self, *args, **kwargs):
  314. self.logger = create_logger(kwargs.pop('logger', None))
  315. self.tunnel_ok = queue.Queue(1)
  316. socketserver.TCPServer.__init__(self, *args, **kwargs)
  317. def handle_error(self, request, client_address):
  318. (exc_class, exc, tb) = sys.exc_info()
  319. local_side = request.getsockname()
  320. remote_side = self.remote_address
  321. self.logger.error('Could not establish connection from local {0} '
  322. 'to remote {1} side of the tunnel: {2}'
  323. .format(local_side, remote_side, exc))
  324. try:
  325. self.tunnel_ok.put(False, block=False, timeout=0.1)
  326. except queue.Full:
  327. # wait untill tunnel_ok.get is called
  328. pass
  329. except exc:
  330. self.logger.error('unexpected internal error: {0}'.format(exc))
  331. @property
  332. def local_address(self):
  333. return self.server_address
  334. @property
  335. def local_host(self):
  336. return self.server_address[0]
  337. @property
  338. def local_port(self):
  339. return self.server_address[1]
  340. @property
  341. def remote_address(self):
  342. return self.RequestHandlerClass.remote_address
  343. @property
  344. def remote_host(self):
  345. return self.RequestHandlerClass.remote_address[0]
  346. @property
  347. def remote_port(self):
  348. return self.RequestHandlerClass.remote_address[1]
  349. class _ThreadingForwardServer(socketserver.ThreadingMixIn, _ForwardServer):
  350. """
  351. Allow concurrent connections to each tunnel
  352. """
  353. # If True, cleanly stop threads created by ThreadingMixIn when quitting
  354. # This value is overrides by SSHTunnelForwarder.daemon_forward_servers
  355. daemon_threads = _DAEMON
  356. class _StreamForwardServer(_StreamServer):
  357. """
  358. Serve over domain sockets (does not work on Windows)
  359. """
  360. def __init__(self, *args, **kwargs):
  361. self.logger = create_logger(kwargs.pop('logger', None))
  362. self.tunnel_ok = queue.Queue(1)
  363. _StreamServer.__init__(self, *args, **kwargs)
  364. @property
  365. def local_address(self):
  366. return self.server_address
  367. @property
  368. def local_host(self):
  369. return None
  370. @property
  371. def local_port(self):
  372. return None
  373. @property
  374. def remote_address(self):
  375. return self.RequestHandlerClass.remote_address
  376. @property
  377. def remote_host(self):
  378. return self.RequestHandlerClass.remote_address[0]
  379. @property
  380. def remote_port(self):
  381. return self.RequestHandlerClass.remote_address[1]
  382. class _ThreadingStreamForwardServer(socketserver.ThreadingMixIn,
  383. _StreamForwardServer):
  384. """
  385. Allow concurrent connections to each tunnel
  386. """
  387. # If True, cleanly stop threads created by ThreadingMixIn when quitting
  388. # This value is overrides by SSHTunnelForwarder.daemon_forward_servers
  389. daemon_threads = _DAEMON
  390. class SSHTunnelForwarder(object):
  391. """
  392. **SSH tunnel class**
  393. - Initialize a SSH tunnel to a remote host according to the input
  394. arguments
  395. - Optionally:
  396. + Read an SSH configuration file (typically ``~/.ssh/config``)
  397. + Load keys from a running SSH agent (i.e. Pageant, GNOME Keyring)
  398. Raises:
  399. :class:`.BaseSSHTunnelForwarderError`:
  400. raised by SSHTunnelForwarder class methods
  401. :class:`.HandlerSSHTunnelForwarderError`:
  402. raised by tunnel forwarder threads
  403. .. note::
  404. Attributes ``mute_exceptions`` and
  405. ``raise_exception_if_any_forwarder_have_a_problem``
  406. (deprecated) may be used to silence most exceptions raised
  407. from this class
  408. Keyword Arguments:
  409. ssh_address_or_host (tuple or str):
  410. IP or hostname of ``REMOTE GATEWAY``. It may be a two-element
  411. tuple (``str``, ``int``) representing IP and port respectively,
  412. or a ``str`` representing the IP address only
  413. .. versionadded:: 0.0.4
  414. ssh_config_file (str):
  415. SSH configuration file that will be read. If explicitly set to
  416. ``None``, parsing of this configuration is omitted
  417. Default: :const:`SSH_CONFIG_FILE`
  418. .. versionadded:: 0.0.4
  419. ssh_host_key (str):
  420. Representation of a line in an OpenSSH-style "known hosts"
  421. file.
  422. ``REMOTE GATEWAY``'s key fingerprint will be compared to this
  423. host key in order to prevent against SSH server spoofing.
  424. Important when using passwords in order not to accidentally
  425. do a login attempt to a wrong (perhaps an attacker's) machine
  426. ssh_username (str):
  427. Username to authenticate as in ``REMOTE SERVER``
  428. Default: current local user name
  429. ssh_password (str):
  430. Text representing the password used to connect to ``REMOTE
  431. SERVER`` or for unlocking a private key.
  432. .. note::
  433. Avoid coding secret password directly in the code, since this
  434. may be visible and make your service vulnerable to attacks
  435. ssh_port (int):
  436. Optional port number of the SSH service on ``REMOTE GATEWAY``,
  437. when `ssh_address_or_host`` is a ``str`` representing the
  438. IP part of ``REMOTE GATEWAY``'s address
  439. Default: 22
  440. ssh_pkey (str or paramiko.PKey):
  441. **Private** key file name (``str``) to obtain the public key
  442. from or a **public** key (:class:`paramiko.pkey.PKey`)
  443. ssh_private_key_password (str):
  444. Password for an encrypted ``ssh_pkey``
  445. .. note::
  446. Avoid coding secret password directly in the code, since this
  447. may be visible and make your service vulnerable to attacks
  448. ssh_proxy (socket-like object or tuple):
  449. Proxy where all SSH traffic will be passed through.
  450. It might be for example a :class:`paramiko.proxy.ProxyCommand`
  451. instance.
  452. See either the :class:`paramiko.transport.Transport`'s sock
  453. parameter documentation or ``ProxyCommand`` in ``ssh_config(5)``
  454. for more information.
  455. It is also possible to specify the proxy address as a tuple of
  456. type (``str``, ``int``) representing proxy's IP and port
  457. .. note::
  458. Ignored if ``ssh_proxy_enabled`` is False
  459. .. versionadded:: 0.0.5
  460. ssh_proxy_enabled (boolean):
  461. Enable/disable SSH proxy. If True and user's
  462. ``ssh_config_file`` contains a ``ProxyCommand`` directive
  463. that matches the specified ``ssh_address_or_host``,
  464. a :class:`paramiko.proxy.ProxyCommand` object will be created where
  465. all SSH traffic will be passed through
  466. Default: ``True``
  467. .. versionadded:: 0.0.4
  468. local_bind_address (tuple):
  469. Local tuple in the format (``str``, ``int``) representing the
  470. IP and port of the local side of the tunnel. Both elements in
  471. the tuple are optional so both ``('', 8000)`` and
  472. ``('10.0.0.1', )`` are valid values
  473. Default: ``('0.0.0.0', RANDOM_PORT)``
  474. .. versionchanged:: 0.0.8
  475. Added the ability to use a UNIX domain socket as local bind
  476. address
  477. local_bind_addresses (list[tuple]):
  478. In case more than one tunnel is established at once, a list
  479. of tuples (in the same format as ``local_bind_address``)
  480. can be specified, such as [(ip1, port_1), (ip_2, port2), ...]
  481. Default: ``[local_bind_address]``
  482. .. versionadded:: 0.0.4
  483. remote_bind_address (tuple):
  484. Remote tuple in the format (``str``, ``int``) representing the
  485. IP and port of the remote side of the tunnel.
  486. remote_bind_addresses (list[tuple]):
  487. In case more than one tunnel is established at once, a list
  488. of tuples (in the same format as ``remote_bind_address``)
  489. can be specified, such as [(ip1, port_1), (ip_2, port2), ...]
  490. Default: ``[remote_bind_address]``
  491. .. versionadded:: 0.0.4
  492. allow_agent (boolean):
  493. Enable/disable load of keys from an SSH agent
  494. Default: ``True``
  495. .. versionadded:: 0.0.8
  496. host_pkey_directories (list):
  497. Look for pkeys in folders on this list, for example ['~/.ssh'].
  498. Default: ``None`` (disabled)
  499. .. versionadded:: 0.1.4
  500. compression (boolean):
  501. Turn on/off transport compression. By default compression is
  502. disabled since it may negatively affect interactive sessions
  503. Default: ``False``
  504. .. versionadded:: 0.0.8
  505. logger (logging.Logger):
  506. logging instance for sshtunnel and paramiko
  507. Default: :class:`logging.Logger` instance with a single
  508. :class:`logging.StreamHandler` handler and
  509. :const:`DEFAULT_LOGLEVEL` level
  510. .. versionadded:: 0.0.3
  511. mute_exceptions (boolean):
  512. Allow silencing :class:`BaseSSHTunnelForwarderError` or
  513. :class:`HandlerSSHTunnelForwarderError` exceptions when enabled
  514. Default: ``False``
  515. .. versionadded:: 0.0.8
  516. set_keepalive (float):
  517. Interval in seconds defining the period in which, if no data
  518. was sent over the connection, a *'keepalive'* packet will be
  519. sent (and ignored by the remote host). This can be useful to
  520. keep connections alive over a NAT. You can set to 0.0 for
  521. disable keepalive.
  522. Default: 5.0 (no keepalive packets are sent)
  523. .. versionadded:: 0.0.7
  524. threaded (boolean):
  525. Allow concurrent connections over a single tunnel
  526. Default: ``True``
  527. .. versionadded:: 0.0.3
  528. ssh_address (str):
  529. Superseded by ``ssh_address_or_host``, tuple of type (str, int)
  530. representing the IP and port of ``REMOTE SERVER``
  531. .. deprecated:: 0.0.4
  532. ssh_host (str):
  533. Superseded by ``ssh_address_or_host``, tuple of type
  534. (str, int) representing the IP and port of ``REMOTE SERVER``
  535. .. deprecated:: 0.0.4
  536. ssh_private_key (str or paramiko.PKey):
  537. Superseded by ``ssh_pkey``, which can represent either a
  538. **private** key file name (``str``) or a **public** key
  539. (:class:`paramiko.pkey.PKey`)
  540. .. deprecated:: 0.0.8
  541. raise_exception_if_any_forwarder_have_a_problem (boolean):
  542. Allow silencing :class:`BaseSSHTunnelForwarderError` or
  543. :class:`HandlerSSHTunnelForwarderError` exceptions when set to
  544. False
  545. Default: ``True``
  546. .. versionadded:: 0.0.4
  547. .. deprecated:: 0.0.8 (use ``mute_exceptions`` instead)
  548. Attributes:
  549. tunnel_is_up (dict):
  550. Describe whether or not the other side of the tunnel was reported
  551. to be up (and we must close it) or not (skip shutting down that
  552. tunnel)
  553. .. note::
  554. This attribute should not be modified
  555. .. note::
  556. When :attr:`.skip_tunnel_checkup` is disabled or the local bind
  557. is a UNIX socket, the value will always be ``True``
  558. **Example**::
  559. {('127.0.0.1', 55550): True, # this tunnel is up
  560. ('127.0.0.1', 55551): False} # this one isn't
  561. where 55550 and 55551 are the local bind ports
  562. skip_tunnel_checkup (boolean):
  563. Disable tunnel checkup (default for backwards compatibility).
  564. .. versionadded:: 0.1.0
  565. """
  566. skip_tunnel_checkup = True
  567. # This option affects the `ForwardServer` and all his threads
  568. daemon_forward_servers = _DAEMON #: flag tunnel threads in daemon mode
  569. # This option affect only `Transport` thread
  570. daemon_transport = _DAEMON #: flag SSH transport thread in daemon mode
  571. def local_is_up(self, target):
  572. """
  573. Check if a tunnel is up (remote target's host is reachable on TCP
  574. target's port)
  575. Arguments:
  576. target (tuple):
  577. tuple of type (``str``, ``int``) indicating the listen IP
  578. address and port
  579. Return:
  580. boolean
  581. .. deprecated:: 0.1.0
  582. Replaced by :meth:`.check_tunnels()` and :attr:`.tunnel_is_up`
  583. """
  584. try:
  585. check_address(target)
  586. except ValueError:
  587. self.logger.warning('Target must be a tuple (IP, port), where IP '
  588. 'is a string (i.e. "192.168.0.1") and port is '
  589. 'an integer (i.e. 40000). Alternatively '
  590. 'target can be a valid UNIX domain socket.')
  591. return False
  592. self.check_tunnels()
  593. return self.tunnel_is_up.get(target, True)
  594. def check_tunnels(self):
  595. """
  596. Check that if all tunnels are established and populates
  597. :attr:`.tunnel_is_up`
  598. """
  599. skip_tunnel_checkup = self.skip_tunnel_checkup
  600. try:
  601. # force tunnel check at this point
  602. self.skip_tunnel_checkup = False
  603. for _srv in self._server_list:
  604. self._check_tunnel(_srv)
  605. finally:
  606. self.skip_tunnel_checkup = skip_tunnel_checkup # roll it back
  607. def _check_tunnel(self, _srv):
  608. """ Check if tunnel is already established """
  609. if self.skip_tunnel_checkup:
  610. self.tunnel_is_up[_srv.local_address] = True
  611. return
  612. self.logger.info('Checking tunnel to: {0}'.format(_srv.remote_address))
  613. if isinstance(_srv.local_address, string_types): # UNIX stream
  614. s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  615. else:
  616. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  617. s.settimeout(TUNNEL_TIMEOUT)
  618. try:
  619. # Windows raises WinError 10049 if trying to connect to 0.0.0.0
  620. connect_to = ('127.0.0.1', _srv.local_port) \
  621. if _srv.local_host == '0.0.0.0' else _srv.local_address
  622. s.connect(connect_to)
  623. self.tunnel_is_up[_srv.local_address] = _srv.tunnel_ok.get(
  624. timeout=TUNNEL_TIMEOUT * 1.1
  625. )
  626. self.logger.debug(
  627. 'Tunnel to {0} is DOWN'.format(_srv.remote_address)
  628. )
  629. except socket.error:
  630. self.logger.debug(
  631. 'Tunnel to {0} is DOWN'.format(_srv.remote_address)
  632. )
  633. self.tunnel_is_up[_srv.local_address] = False
  634. except queue.Empty:
  635. self.logger.debug(
  636. 'Tunnel to {0} is UP'.format(_srv.remote_address)
  637. )
  638. self.tunnel_is_up[_srv.local_address] = True
  639. finally:
  640. s.close()
  641. def _make_ssh_forward_handler_class(self, remote_address_):
  642. """
  643. Make SSH Handler class
  644. """
  645. class Handler(_ForwardHandler):
  646. remote_address = remote_address_
  647. ssh_transport = self._transport
  648. logger = self.logger
  649. return Handler
  650. def _make_ssh_forward_server_class(self, remote_address_):
  651. return _ThreadingForwardServer if self._threaded else _ForwardServer
  652. def _make_stream_ssh_forward_server_class(self, remote_address_):
  653. return _ThreadingStreamForwardServer if self._threaded \
  654. else _StreamForwardServer
  655. def _make_ssh_forward_server(self, remote_address, local_bind_address):
  656. """
  657. Make SSH forward proxy Server class
  658. """
  659. _Handler = self._make_ssh_forward_handler_class(remote_address)
  660. try:
  661. forward_maker_class = self._make_stream_ssh_forward_server_class \
  662. if isinstance(local_bind_address, string_types) \
  663. else self._make_ssh_forward_server_class
  664. _Server = forward_maker_class(remote_address)
  665. ssh_forward_server = _Server(
  666. local_bind_address,
  667. _Handler,
  668. logger=self.logger,
  669. )
  670. if ssh_forward_server:
  671. ssh_forward_server.daemon_threads = self.daemon_forward_servers
  672. self._server_list.append(ssh_forward_server)
  673. self.tunnel_is_up[ssh_forward_server.server_address] = False
  674. else:
  675. self._raise(
  676. BaseSSHTunnelForwarderError,
  677. 'Problem setting up ssh {0} <> {1} forwarder. You can '
  678. 'suppress this exception by using the `mute_exceptions`'
  679. 'argument'.format(address_to_str(local_bind_address),
  680. address_to_str(remote_address))
  681. )
  682. except IOError:
  683. self._raise(
  684. BaseSSHTunnelForwarderError,
  685. "Couldn't open tunnel {0} <> {1} might be in use or "
  686. "destination not reachable".format(
  687. address_to_str(local_bind_address),
  688. address_to_str(remote_address)
  689. )
  690. )
  691. def __init__(
  692. self,
  693. ssh_address_or_host=None,
  694. ssh_config_file=SSH_CONFIG_FILE,
  695. ssh_host_key=None,
  696. ssh_password=None,
  697. ssh_pkey=None,
  698. ssh_private_key_password=None,
  699. ssh_proxy=None,
  700. ssh_proxy_enabled=True,
  701. ssh_username=None,
  702. local_bind_address=None,
  703. local_bind_addresses=None,
  704. logger=None,
  705. mute_exceptions=False,
  706. remote_bind_address=None,
  707. remote_bind_addresses=None,
  708. set_keepalive=5.0,
  709. threaded=True, # old version False
  710. compression=None,
  711. allow_agent=True, # look for keys from an SSH agent
  712. host_pkey_directories=None, # look for keys in ~/.ssh
  713. *args,
  714. **kwargs # for backwards compatibility
  715. ):
  716. self.logger = logger or create_logger()
  717. # Ensure paramiko.transport has a console handler
  718. _check_paramiko_handlers(logger=logger)
  719. self.ssh_host_key = ssh_host_key
  720. self.set_keepalive = set_keepalive
  721. self._server_list = [] # reset server list
  722. self.tunnel_is_up = {} # handle tunnel status
  723. self._threaded = threaded
  724. self.is_alive = False
  725. # Check if deprecated arguments ssh_address or ssh_host were used
  726. for deprecated_argument in ['ssh_address', 'ssh_host']:
  727. ssh_address_or_host = self._process_deprecated(ssh_address_or_host,
  728. deprecated_argument,
  729. kwargs)
  730. # other deprecated arguments
  731. ssh_pkey = self._process_deprecated(ssh_pkey,
  732. 'ssh_private_key',
  733. kwargs)
  734. self._raise_fwd_exc = self._process_deprecated(
  735. None,
  736. 'raise_exception_if_any_forwarder_have_a_problem',
  737. kwargs) or not mute_exceptions
  738. if isinstance(ssh_address_or_host, tuple):
  739. check_address(ssh_address_or_host)
  740. (ssh_host, ssh_port) = ssh_address_or_host
  741. else:
  742. ssh_host = ssh_address_or_host
  743. ssh_port = kwargs.pop('ssh_port', None)
  744. if kwargs:
  745. raise ValueError('Unknown arguments: {0}'.format(kwargs))
  746. # remote binds
  747. self._remote_binds = self._get_binds(remote_bind_address,
  748. remote_bind_addresses,
  749. is_remote=True)
  750. # local binds
  751. self._local_binds = self._get_binds(local_bind_address,
  752. local_bind_addresses)
  753. self._local_binds = self._consolidate_binds(self._local_binds,
  754. self._remote_binds)
  755. (self.ssh_host,
  756. self.ssh_username,
  757. ssh_pkey, # still needs to go through _consolidate_auth
  758. self.ssh_port,
  759. self.ssh_proxy,
  760. self.compression) = self._read_ssh_config(
  761. ssh_host,
  762. ssh_config_file,
  763. ssh_username,
  764. ssh_pkey,
  765. ssh_port,
  766. ssh_proxy if ssh_proxy_enabled else None,
  767. compression,
  768. self.logger
  769. )
  770. (self.ssh_password, self.ssh_pkeys) = self._consolidate_auth(
  771. ssh_password=ssh_password,
  772. ssh_pkey=ssh_pkey,
  773. ssh_pkey_password=ssh_private_key_password,
  774. allow_agent=allow_agent,
  775. host_pkey_directories=host_pkey_directories,
  776. logger=self.logger
  777. )
  778. check_host(self.ssh_host)
  779. check_port(self.ssh_port)
  780. self.logger.info("Connecting to gateway: {0}:{1} as user '{2}'"
  781. .format(self.ssh_host,
  782. self.ssh_port,
  783. self.ssh_username))
  784. self.logger.debug('Concurrent connections allowed: {0}'
  785. .format(self._threaded))
  786. @staticmethod
  787. def _read_ssh_config(ssh_host,
  788. ssh_config_file,
  789. ssh_username=None,
  790. ssh_pkey=None,
  791. ssh_port=None,
  792. ssh_proxy=None,
  793. compression=None,
  794. logger=None):
  795. """
  796. Read ssh_config_file and tries to look for user (ssh_username),
  797. identityfile (ssh_pkey), port (ssh_port) and proxycommand
  798. (ssh_proxy) entries for ssh_host
  799. """
  800. ssh_config = paramiko.SSHConfig()
  801. if not ssh_config_file: # handle case where it's an empty string
  802. ssh_config_file = None
  803. # Try to read SSH_CONFIG_FILE
  804. try:
  805. # open the ssh config file
  806. with open(os.path.expanduser(ssh_config_file), 'r') as f:
  807. ssh_config.parse(f)
  808. # looks for information for the destination system
  809. hostname_info = ssh_config.lookup(ssh_host)
  810. # gather settings for user, port and identity file
  811. # last resort: use the 'login name' of the user
  812. ssh_username = (
  813. ssh_username or
  814. hostname_info.get('user')
  815. )
  816. ssh_pkey = (
  817. ssh_pkey or
  818. hostname_info.get('identityfile', [None])[0]
  819. )
  820. ssh_host = hostname_info.get('hostname')
  821. ssh_port = ssh_port or hostname_info.get('port')
  822. proxycommand = hostname_info.get('proxycommand')
  823. ssh_proxy = ssh_proxy or (paramiko.ProxyCommand(proxycommand) if
  824. proxycommand else None)
  825. if compression is None:
  826. compression = hostname_info.get('compression', '')
  827. compression = True if compression.upper() == 'YES' else False
  828. except IOError:
  829. if logger:
  830. logger.warning(
  831. 'Could not read SSH configuration file: {0}'
  832. .format(ssh_config_file)
  833. )
  834. except (AttributeError, TypeError): # ssh_config_file is None
  835. if logger:
  836. logger.info('Skipping loading of ssh configuration file')
  837. finally:
  838. return (ssh_host,
  839. ssh_username or getpass.getuser(),
  840. ssh_pkey,
  841. int(ssh_port) if ssh_port else 22, # fallback value
  842. ssh_proxy,
  843. compression)
  844. @staticmethod
  845. def get_agent_keys(logger=None):
  846. """ Load public keys from any available SSH agent
  847. Arguments:
  848. logger (Optional[logging.Logger])
  849. Return:
  850. list
  851. """
  852. paramiko_agent = paramiko.Agent()
  853. agent_keys = paramiko_agent.get_keys()
  854. if logger:
  855. logger.info('{0} keys loaded from agent'.format(len(agent_keys)))
  856. return list(agent_keys)
  857. @staticmethod
  858. def get_keys(logger=None, host_pkey_directories=None, allow_agent=False):
  859. """
  860. Load public keys from any available SSH agent or local
  861. .ssh directory.
  862. Arguments:
  863. logger (Optional[logging.Logger])
  864. host_pkey_directories (Optional[list[str]]):
  865. List of local directories where host SSH pkeys in the format
  866. "id_*" are searched. For example, ['~/.ssh']
  867. .. versionadded:: 0.1.0
  868. allow_agent (Optional[boolean]):
  869. Whether or not load keys from agent
  870. Default: False
  871. Return:
  872. list
  873. """
  874. keys = SSHTunnelForwarder.get_agent_keys(logger=logger) \
  875. if allow_agent else []
  876. if host_pkey_directories is None:
  877. host_pkey_directories = [DEFAULT_SSH_DIRECTORY]
  878. paramiko_key_types = {'rsa': paramiko.RSAKey,
  879. 'dsa': paramiko.DSSKey,
  880. 'ecdsa': paramiko.ECDSAKey}
  881. if hasattr(paramiko, 'Ed25519Key'):
  882. # NOQA: new in paramiko>=2.2: http://docs.paramiko.org/en/stable/api/keys.html#module-paramiko.ed25519key
  883. paramiko_key_types['ed25519'] = paramiko.Ed25519Key
  884. for directory in host_pkey_directories:
  885. for keytype in paramiko_key_types.keys():
  886. ssh_pkey_expanded = os.path.expanduser(
  887. os.path.join(directory, 'id_{}'.format(keytype))
  888. )
  889. try:
  890. if os.path.isfile(ssh_pkey_expanded):
  891. ssh_pkey = SSHTunnelForwarder.read_private_key_file(
  892. pkey_file=ssh_pkey_expanded,
  893. logger=logger,
  894. key_type=paramiko_key_types[keytype]
  895. )
  896. if ssh_pkey:
  897. keys.append(ssh_pkey)
  898. except OSError as exc:
  899. if logger:
  900. logger.warning('Private key file {0} check error: {1}'
  901. .format(ssh_pkey_expanded, exc))
  902. if logger:
  903. logger.info('{0} key(s) loaded'.format(len(keys)))
  904. return keys
  905. @staticmethod
  906. def _consolidate_binds(local_binds, remote_binds):
  907. """
  908. Fill local_binds with defaults when no value/s were specified,
  909. leaving paramiko to decide in which local port the tunnel will be open
  910. """
  911. count = len(remote_binds) - len(local_binds)
  912. if count < 0:
  913. raise ValueError('Too many local bind addresses '
  914. '(local_bind_addresses > remote_bind_addresses)')
  915. local_binds.extend([('0.0.0.0', 0) for x in range(count)])
  916. return local_binds
  917. @staticmethod
  918. def _consolidate_auth(ssh_password=None,
  919. ssh_pkey=None,
  920. ssh_pkey_password=None,
  921. allow_agent=True,
  922. host_pkey_directories=None,
  923. logger=None):
  924. """
  925. Get sure authentication information is in place.
  926. ``ssh_pkey`` may be of classes:
  927. - ``str`` - in this case it represents a private key file; public
  928. key will be obtained from it
  929. - ``paramiko.Pkey`` - it will be transparently added to loaded keys
  930. """
  931. ssh_loaded_pkeys = SSHTunnelForwarder.get_keys(
  932. logger=logger,
  933. host_pkey_directories=host_pkey_directories,
  934. allow_agent=allow_agent
  935. )
  936. if isinstance(ssh_pkey, string_types):
  937. ssh_pkey_expanded = os.path.expanduser(ssh_pkey)
  938. if os.path.exists(ssh_pkey_expanded):
  939. ssh_pkey = SSHTunnelForwarder.read_private_key_file(
  940. pkey_file=ssh_pkey_expanded,
  941. pkey_password=ssh_pkey_password or ssh_password,
  942. logger=logger
  943. )
  944. elif logger:
  945. logger.warning('Private key file not found: {0}'
  946. .format(ssh_pkey))
  947. if isinstance(ssh_pkey, paramiko.pkey.PKey):
  948. ssh_loaded_pkeys.insert(0, ssh_pkey)
  949. if not ssh_password and not ssh_loaded_pkeys:
  950. raise ValueError('No password or public key available!')
  951. return (ssh_password, ssh_loaded_pkeys)
  952. def _raise(self, exception=BaseSSHTunnelForwarderError, reason=None):
  953. if self._raise_fwd_exc:
  954. raise exception(reason)
  955. else:
  956. self.logger.error(repr(exception(reason)))
  957. def _get_transport(self):
  958. """ Return the SSH transport to the remote gateway """
  959. if self.ssh_proxy:
  960. if isinstance(self.ssh_proxy, paramiko.proxy.ProxyCommand):
  961. proxy_repr = repr(self.ssh_proxy.cmd[1])
  962. else:
  963. proxy_repr = repr(self.ssh_proxy)
  964. self.logger.debug('Connecting via proxy: {0}'.format(proxy_repr))
  965. _socket = self.ssh_proxy
  966. else:
  967. _socket = (self.ssh_host, self.ssh_port)
  968. if isinstance(_socket, socket.socket):
  969. _socket.settimeout(SSH_TIMEOUT)
  970. _socket.connect((self.ssh_host, self.ssh_port))
  971. transport = paramiko.Transport(_socket)
  972. sock = transport.sock
  973. if isinstance(sock, socket.socket):
  974. sock.settimeout(SSH_TIMEOUT)
  975. transport.set_keepalive(self.set_keepalive)
  976. transport.use_compression(compress=self.compression)
  977. transport.daemon = self.daemon_transport
  978. # try to solve https://github.com/paramiko/paramiko/issues/1181
  979. # transport.banner_timeout = 200
  980. if isinstance(sock, socket.socket):
  981. sock_timeout = sock.gettimeout()
  982. sock_info = repr((sock.family, sock.type, sock.proto))
  983. self.logger.debug('Transport socket info: {0}, timeout={1}'
  984. .format(sock_info, sock_timeout))
  985. return transport
  986. def _create_tunnels(self):
  987. """
  988. Create SSH tunnels on top of a transport to the remote gateway
  989. """
  990. if not self.is_active:
  991. try:
  992. self._connect_to_gateway()
  993. except socket.gaierror: # raised by paramiko.Transport
  994. msg = 'Could not resolve IP address for {0}, aborting!' \
  995. .format(self.ssh_host)
  996. self.logger.error(msg)
  997. return
  998. except (paramiko.SSHException, socket.error) as e:
  999. template = 'Could not connect to gateway {0}:{1} : {2}'
  1000. msg = template.format(self.ssh_host, self.ssh_port, e.args[0])
  1001. self.logger.error(msg)
  1002. return
  1003. for (rem, loc) in zip(self._remote_binds, self._local_binds):
  1004. try:
  1005. self._make_ssh_forward_server(rem, loc)
  1006. except BaseSSHTunnelForwarderError as e:
  1007. msg = 'Problem setting SSH Forwarder up: {0}'.format(e.value)
  1008. self.logger.error(msg)
  1009. @staticmethod
  1010. def _get_binds(bind_address, bind_addresses, is_remote=False):
  1011. addr_kind = 'remote' if is_remote else 'local'
  1012. if not bind_address and not bind_addresses:
  1013. if is_remote:
  1014. raise ValueError("No {0} bind addresses specified. Use "
  1015. "'{0}_bind_address' or '{0}_bind_addresses'"
  1016. " argument".format(addr_kind))
  1017. else:
  1018. return []
  1019. elif bind_address and bind_addresses:
  1020. raise ValueError("You can't use both '{0}_bind_address' and "
  1021. "'{0}_bind_addresses' arguments. Use one of "
  1022. "them.".format(addr_kind))
  1023. if bind_address:
  1024. bind_addresses = [bind_address]
  1025. if not is_remote:
  1026. # Add random port if missing in local bind
  1027. for (i, local_bind) in enumerate(bind_addresses):
  1028. if isinstance(local_bind, tuple) and len(local_bind) == 1:
  1029. bind_addresses[i] = (local_bind[0], 0)
  1030. check_addresses(bind_addresses, is_remote)
  1031. return bind_addresses
  1032. @staticmethod
  1033. def _process_deprecated(attrib, deprecated_attrib, kwargs):
  1034. """
  1035. Processes optional deprecate arguments
  1036. """
  1037. if deprecated_attrib not in _DEPRECATIONS:
  1038. raise ValueError('{0} not included in deprecations list'
  1039. .format(deprecated_attrib))
  1040. if deprecated_attrib in kwargs:
  1041. warnings.warn("'{0}' is DEPRECATED use '{1}' instead"
  1042. .format(deprecated_attrib,
  1043. _DEPRECATIONS[deprecated_attrib]),
  1044. DeprecationWarning)
  1045. if attrib:
  1046. raise ValueError("You can't use both '{0}' and '{1}'. "
  1047. "Please only use one of them"
  1048. .format(deprecated_attrib,
  1049. _DEPRECATIONS[deprecated_attrib]))
  1050. else:
  1051. return kwargs.pop(deprecated_attrib)
  1052. return attrib
  1053. @staticmethod
  1054. def read_private_key_file(pkey_file,
  1055. pkey_password=None,
  1056. key_type=None,
  1057. logger=None):
  1058. """
  1059. Get SSH Public key from a private key file, given an optional password
  1060. Arguments:
  1061. pkey_file (str):
  1062. File containing a private key (RSA, DSS or ECDSA)
  1063. Keyword Arguments:
  1064. pkey_password (Optional[str]):
  1065. Password to decrypt the private key
  1066. logger (Optional[logging.Logger])
  1067. Return:
  1068. paramiko.Pkey
  1069. """
  1070. ssh_pkey = None
  1071. key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey)
  1072. if hasattr(paramiko, 'Ed25519Key'):
  1073. # NOQA: new in paramiko>=2.2: http://docs.paramiko.org/en/stable/api/keys.html#module-paramiko.ed25519key
  1074. key_types += (paramiko.Ed25519Key, )
  1075. for pkey_class in (key_type,) if key_type else key_types:
  1076. try:
  1077. ssh_pkey = pkey_class.from_private_key_file(
  1078. pkey_file,
  1079. password=pkey_password
  1080. )
  1081. if logger:
  1082. logger.debug('Private key file ({0}, {1}) successfully '
  1083. 'loaded'.format(pkey_file, pkey_class))
  1084. break
  1085. except paramiko.PasswordRequiredException:
  1086. if logger:
  1087. logger.error('Password is required for key {0}'
  1088. .format(pkey_file))
  1089. break
  1090. except paramiko.SSHException:
  1091. if logger:
  1092. logger.debug('Private key file ({0}) could not be loaded '
  1093. 'as type {1} or bad password'
  1094. .format(pkey_file, pkey_class))
  1095. return ssh_pkey
  1096. def start(self):
  1097. """ Start the SSH tunnels """
  1098. if self.is_alive:
  1099. self.logger.warning('Already started!')
  1100. return
  1101. self._create_tunnels()
  1102. if not self.is_active:
  1103. self._raise(BaseSSHTunnelForwarderError,
  1104. reason='Could not establish session to SSH gateway')
  1105. for _srv in self._server_list:
  1106. thread = threading.Thread(
  1107. target=self._serve_forever_wrapper,
  1108. args=(_srv, ),
  1109. name='Srv-{0}'.format(address_to_str(_srv.local_port))
  1110. )
  1111. thread.daemon = self.daemon_forward_servers
  1112. thread.start()
  1113. self._check_tunnel(_srv)
  1114. self.is_alive = any(self.tunnel_is_up.values())
  1115. if not self.is_alive:
  1116. self._raise(HandlerSSHTunnelForwarderError,
  1117. 'An error occurred while opening tunnels.')
  1118. def stop(self, force=False):
  1119. """
  1120. Shut the tunnel down. By default we are always waiting until closing
  1121. all connections. You can use `force=True` to force close connections
  1122. Keyword Arguments:
  1123. force (bool):
  1124. Force close current connections
  1125. Default: False
  1126. .. versionadded:: 0.2.2
  1127. .. note:: This **had** to be handled with care before ``0.1.0``:
  1128. - if a port redirection is opened
  1129. - the destination is not reachable
  1130. - we attempt a connection to that tunnel (``SYN`` is sent and
  1131. acknowledged, then a ``FIN`` packet is sent and never
  1132. acknowledged... weird)
  1133. - we try to shutdown: it will not succeed until ``FIN_WAIT_2`` and
  1134. ``CLOSE_WAIT`` time out.
  1135. .. note::
  1136. Handle these scenarios with :attr:`.tunnel_is_up`: if False, server
  1137. ``shutdown()`` will be skipped on that tunnel
  1138. """
  1139. self.logger.info('Closing all open connections...')
  1140. opened_address_text = ', '.join(
  1141. (address_to_str(k.local_address) for k in self._server_list)
  1142. ) or 'None'
  1143. self.logger.debug('Listening tunnels: ' + opened_address_text)
  1144. self._stop_transport(force=force)
  1145. self._server_list = [] # reset server list
  1146. self.tunnel_is_up = {} # reset tunnel status
  1147. def close(self):
  1148. """ Stop the an active tunnel, alias to :meth:`.stop` """
  1149. self.stop()
  1150. def restart(self):
  1151. """ Restart connection to the gateway and tunnels """
  1152. self.stop()
  1153. self.start()
  1154. def _connect_to_gateway(self):
  1155. """
  1156. Open connection to SSH gateway
  1157. - First try with all keys loaded from an SSH agent (if allowed)
  1158. - Then with those passed directly or read from ~/.ssh/config
  1159. - As last resort, try with a provided password
  1160. """
  1161. for key in self.ssh_pkeys:
  1162. self.logger.debug('Trying to log in with key: {0}'
  1163. .format(hexlify(key.get_fingerprint())))
  1164. try:
  1165. self._transport = self._get_transport()
  1166. self._transport.connect(hostkey=self.ssh_host_key,
  1167. username=self.ssh_username,
  1168. pkey=key)
  1169. if self._transport.is_alive:
  1170. return
  1171. except paramiko.AuthenticationException:
  1172. self.logger.debug('Authentication error')
  1173. self._stop_transport()
  1174. if self.ssh_password: # avoid conflict using both pass and pkey
  1175. self.logger.debug('Trying to log in with password: {0}'
  1176. .format('*' * len(self.ssh_password)))
  1177. try:
  1178. self._transport = self._get_transport()
  1179. self._transport.connect(hostkey=self.ssh_host_key,
  1180. username=self.ssh_username,
  1181. password=self.ssh_password)
  1182. if self._transport.is_alive:
  1183. return
  1184. except paramiko.AuthenticationException:
  1185. self.logger.debug('Authentication error')
  1186. self._stop_transport()
  1187. self.logger.error('Could not open connection to gateway')
  1188. def _serve_forever_wrapper(self, _srv, poll_interval=0.1):
  1189. """
  1190. Wrapper for the server created for a SSH forward
  1191. """
  1192. self.logger.info('Opening tunnel: {0} <> {1}'.format(
  1193. address_to_str(_srv.local_address),
  1194. address_to_str(_srv.remote_address))
  1195. )
  1196. _srv.serve_forever(poll_interval) # blocks until finished
  1197. self.logger.info('Tunnel: {0} <> {1} released'.format(
  1198. address_to_str(_srv.local_address),
  1199. address_to_str(_srv.remote_address))
  1200. )
  1201. def _stop_transport(self, force=False):
  1202. """ Close the underlying transport when nothing more is needed """
  1203. try:
  1204. self._check_is_started()
  1205. except (BaseSSHTunnelForwarderError,
  1206. HandlerSSHTunnelForwarderError) as e:
  1207. self.logger.warning(e)
  1208. if force and self.is_active:
  1209. # don't wait connections
  1210. self.logger.info('Closing ssh transport')
  1211. self._transport.close()
  1212. self._transport.stop_thread()
  1213. for _srv in self._server_list:
  1214. status = 'up' if self.tunnel_is_up[_srv.local_address] else 'down'
  1215. self.logger.info('Shutting down tunnel: {0} <> {1} ({2})'.format(
  1216. address_to_str(_srv.local_address),
  1217. address_to_str(_srv.remote_address),
  1218. status
  1219. ))
  1220. _srv.shutdown()
  1221. _srv.server_close()
  1222. # clean up the UNIX domain socket if we're using one
  1223. if isinstance(_srv, _StreamForwardServer):
  1224. try:
  1225. os.unlink(_srv.local_address)
  1226. except Exception as e:
  1227. self.logger.error('Unable to unlink socket {0}: {1}'
  1228. .format(_srv.local_address, repr(e)))
  1229. self.is_alive = False
  1230. if self.is_active:
  1231. self.logger.info('Closing ssh transport')
  1232. self._transport.close()
  1233. self._transport.stop_thread()
  1234. self.logger.debug('Transport is closed')
  1235. @property
  1236. def local_bind_port(self):
  1237. # BACKWARDS COMPATIBILITY
  1238. self._check_is_started()
  1239. if len(self._server_list) != 1:
  1240. raise BaseSSHTunnelForwarderError(
  1241. 'Use .local_bind_ports property for more than one tunnel'
  1242. )
  1243. return self.local_bind_ports[0]
  1244. @property
  1245. def local_bind_host(self):
  1246. # BACKWARDS COMPATIBILITY
  1247. self._check_is_started()
  1248. if len(self._server_list) != 1:
  1249. raise BaseSSHTunnelForwarderError(
  1250. 'Use .local_bind_hosts property for more than one tunnel'
  1251. )
  1252. return self.local_bind_hosts[0]
  1253. @property
  1254. def local_bind_address(self):
  1255. # BACKWARDS COMPATIBILITY
  1256. self._check_is_started()
  1257. if len(self._server_list) != 1:
  1258. raise BaseSSHTunnelForwarderError(
  1259. 'Use .local_bind_addresses property for more than one tunnel'
  1260. )
  1261. return self.local_bind_addresses[0]
  1262. @property
  1263. def local_bind_ports(self):
  1264. """
  1265. Return a list containing the ports of local side of the TCP tunnels
  1266. """
  1267. self._check_is_started()
  1268. return [_server.local_port for _server in self._server_list if
  1269. _server.local_port is not None]
  1270. @property
  1271. def local_bind_hosts(self):
  1272. """
  1273. Return a list containing the IP addresses listening for the tunnels
  1274. """
  1275. self._check_is_started()
  1276. return [_server.local_host for _server in self._server_list if
  1277. _server.local_host is not None]
  1278. @property
  1279. def local_bind_addresses(self):
  1280. """
  1281. Return a list of (IP, port) pairs for the local side of the tunnels
  1282. """
  1283. self._check_is_started()
  1284. return [_server.local_address for _server in self._server_list]
  1285. @property
  1286. def tunnel_bindings(self):
  1287. """
  1288. Return a dictionary containing the active local<>remote tunnel_bindings
  1289. """
  1290. return dict((_server.remote_address, _server.local_address) for
  1291. _server in self._server_list if
  1292. self.tunnel_is_up[_server.local_address])
  1293. @property
  1294. def is_active(self):
  1295. """ Return True if the underlying SSH transport is up """
  1296. if (
  1297. '_transport' in self.__dict__ and
  1298. self._transport.is_active()
  1299. ):
  1300. return True
  1301. return False
  1302. def _check_is_started(self):
  1303. if not self.is_active: # underlying transport not alive
  1304. msg = 'Server is not started. Please .start() first!'
  1305. raise BaseSSHTunnelForwarderError(msg)
  1306. if not self.is_alive:
  1307. msg = 'Tunnels are not started. Please .start() first!'
  1308. raise HandlerSSHTunnelForwarderError(msg)
  1309. def __str__(self):
  1310. credentials = {
  1311. 'password': self.ssh_password,
  1312. 'pkeys': [(key.get_name(), hexlify(key.get_fingerprint()))
  1313. for key in self.ssh_pkeys]
  1314. if any(self.ssh_pkeys) else None
  1315. }
  1316. _remove_none_values(credentials)
  1317. template = os.linesep.join(['{0} object',
  1318. 'ssh gateway: {1}:{2}',
  1319. 'proxy: {3}',
  1320. 'username: {4}',
  1321. 'authentication: {5}',
  1322. 'hostkey: {6}',
  1323. 'status: {7}started',
  1324. 'keepalive messages: {8}',
  1325. 'tunnel connection check: {9}',
  1326. 'concurrent connections: {10}allowed',
  1327. 'compression: {11}requested',
  1328. 'logging level: {12}',
  1329. 'local binds: {13}',
  1330. 'remote binds: {14}'])
  1331. return (template.format(
  1332. self.__class__,
  1333. self.ssh_host,
  1334. self.ssh_port,
  1335. self.ssh_proxy.cmd[1] if self.ssh_proxy else 'no',
  1336. self.ssh_username,
  1337. credentials,
  1338. self.ssh_host_key if self.ssh_host_key else 'not checked',
  1339. '' if self.is_alive else 'not ',
  1340. 'disabled' if not self.set_keepalive else
  1341. 'every {0} sec'.format(self.set_keepalive),
  1342. 'disabled' if self.skip_tunnel_checkup else 'enabled',
  1343. '' if self._threaded else 'not ',
  1344. '' if self.compression else 'not ',
  1345. logging.getLevelName(self.logger.level),
  1346. self._local_binds,
  1347. self._remote_binds,
  1348. ))
  1349. def __repr__(self):
  1350. return self.__str__()
  1351. def __enter__(self):
  1352. try:
  1353. self.start()
  1354. return self
  1355. except KeyboardInterrupt:
  1356. self.__exit__()
  1357. def __exit__(self, *args):
  1358. self.stop(force=True)
  1359. def __del__(self):
  1360. if self.is_active or self.is_alive:
  1361. self.logger.warning(
  1362. "It looks like you didn't call the .stop() before "
  1363. "the SSHTunnelForwarder obj was collected by "
  1364. "the garbage collector! Running .stop(force=True)")
  1365. self.stop(force=True)
  1366. def open_tunnel(*args, **kwargs):
  1367. """
  1368. Open an SSH Tunnel, wrapper for :class:`SSHTunnelForwarder`
  1369. Arguments:
  1370. destination (Optional[tuple]):
  1371. SSH server's IP address and port in the format
  1372. (``ssh_address``, ``ssh_port``)
  1373. Keyword Arguments:
  1374. debug_level (Optional[int or str]):
  1375. log level for :class:`logging.Logger` instance, i.e. ``DEBUG``
  1376. skip_tunnel_checkup (boolean):
  1377. Enable/disable the local side check and populate
  1378. :attr:`~SSHTunnelForwarder.tunnel_is_up`
  1379. Default: True
  1380. .. versionadded:: 0.1.0
  1381. .. note::
  1382. A value of ``debug_level`` set to 1 == ``TRACE`` enables tracing mode
  1383. .. note::
  1384. See :class:`SSHTunnelForwarder` for keyword arguments
  1385. **Example**::
  1386. from sshtunnel import open_tunnel
  1387. with open_tunnel(SERVER,
  1388. ssh_username=SSH_USER,
  1389. ssh_port=22,
  1390. ssh_password=SSH_PASSWORD,
  1391. remote_bind_address=(REMOTE_HOST, REMOTE_PORT),
  1392. local_bind_address=('', LOCAL_PORT)) as server:
  1393. def do_something(port):
  1394. pass
  1395. print("LOCAL PORTS:", server.local_bind_port)
  1396. do_something(server.local_bind_port)
  1397. """
  1398. # Attach a console handler to the logger or create one if not passed
  1399. kwargs['logger'] = create_logger(logger=kwargs.get('logger', None),
  1400. loglevel=kwargs.pop('debug_level', None))
  1401. ssh_address_or_host = kwargs.pop('ssh_address_or_host', None)
  1402. # Check if deprecated arguments ssh_address or ssh_host were used
  1403. for deprecated_argument in ['ssh_address', 'ssh_host']:
  1404. ssh_address_or_host = SSHTunnelForwarder._process_deprecated(
  1405. ssh_address_or_host,
  1406. deprecated_argument,
  1407. kwargs
  1408. )
  1409. ssh_port = kwargs.pop('ssh_port', 22)
  1410. skip_tunnel_checkup = kwargs.pop('skip_tunnel_checkup', True)
  1411. block_on_close = kwargs.pop('block_on_close', None)
  1412. if block_on_close:
  1413. warnings.warn("'block_on_close' is DEPRECATED. You should use either"
  1414. " .stop() or .stop(force=True), depends on what you do"
  1415. " with the active connections. This option has no"
  1416. " affect since 0.3.0",
  1417. DeprecationWarning)
  1418. if not args:
  1419. if isinstance(ssh_address_or_host, tuple):
  1420. args = (ssh_address_or_host, )
  1421. else:
  1422. args = ((ssh_address_or_host, ssh_port), )
  1423. forwarder = SSHTunnelForwarder(*args, **kwargs)
  1424. forwarder.skip_tunnel_checkup = skip_tunnel_checkup
  1425. return forwarder
  1426. def _bindlist(input_str):
  1427. """ Define type of data expected for remote and local bind address lists
  1428. Returns a tuple (ip_address, port) whose elements are (str, int)
  1429. """
  1430. try:
  1431. ip_port = input_str.split(':')
  1432. if len(ip_port) == 1:
  1433. _ip = ip_port[0]
  1434. _port = None
  1435. else:
  1436. (_ip, _port) = ip_port
  1437. if not _ip and not _port:
  1438. raise AssertionError
  1439. elif not _port:
  1440. _port = '22' # default port if not given
  1441. return _ip, int(_port)
  1442. except ValueError:
  1443. raise argparse.ArgumentTypeError(
  1444. 'Address tuple must be of type IP_ADDRESS:PORT'
  1445. )
  1446. except AssertionError:
  1447. raise argparse.ArgumentTypeError("Both IP:PORT can't be missing!")
  1448. def _parse_arguments(args=None):
  1449. """
  1450. Parse arguments directly passed from CLI
  1451. """
  1452. parser = argparse.ArgumentParser(
  1453. description='Pure python ssh tunnel utils\n'
  1454. 'Version {0}'.format(__version__),
  1455. formatter_class=argparse.RawTextHelpFormatter
  1456. )
  1457. parser.add_argument(
  1458. 'ssh_address',
  1459. type=str,
  1460. help='SSH server IP address (GW for SSH tunnels)\n'
  1461. 'set with "-- ssh_address" if immediately after '
  1462. '-R or -L'
  1463. )
  1464. parser.add_argument(
  1465. '-U', '--username',
  1466. type=str,
  1467. dest='ssh_username',
  1468. help='SSH server account username'
  1469. )
  1470. parser.add_argument(
  1471. '-p', '--server_port',
  1472. type=int,
  1473. dest='ssh_port',
  1474. default=22,
  1475. help='SSH server TCP port (default: 22)'
  1476. )
  1477. parser.add_argument(
  1478. '-P', '--password',
  1479. type=str,
  1480. dest='ssh_password',
  1481. help='SSH server account password'
  1482. )
  1483. parser.add_argument(
  1484. '-R', '--remote_bind_address',
  1485. type=_bindlist,
  1486. nargs='+',
  1487. default=[],
  1488. metavar='IP:PORT',
  1489. required=True,
  1490. dest='remote_bind_addresses',
  1491. help='Remote bind address sequence: '
  1492. 'ip_1:port_1 ip_2:port_2 ... ip_n:port_n\n'
  1493. 'Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT\n'
  1494. 'If port is omitted, defaults to 22.\n'
  1495. 'Example: -R 10.10.10.10: 10.10.10.10:5900'
  1496. )
  1497. parser.add_argument(
  1498. '-L', '--local_bind_address',
  1499. type=_bindlist,
  1500. nargs='*',
  1501. dest='local_bind_addresses',
  1502. metavar='IP:PORT',
  1503. help='Local bind address sequence: '
  1504. 'ip_1:port_1 ip_2:port_2 ... ip_n:port_n\n'
  1505. 'Elements may also be valid UNIX socket domains: \n'
  1506. '/tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock\n'
  1507. 'Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, '
  1508. 'being the local IP address optional.\n'
  1509. 'By default it will listen in all interfaces '
  1510. '(0.0.0.0) and choose a random port.\n'
  1511. 'Example: -L :40000'
  1512. )
  1513. parser.add_argument(
  1514. '-k', '--ssh_host_key',
  1515. type=str,
  1516. help="Gateway's host key"
  1517. )
  1518. parser.add_argument(
  1519. '-K', '--private_key_file',
  1520. dest='ssh_private_key',
  1521. metavar='KEY_FILE',
  1522. type=str,
  1523. help='RSA/DSS/ECDSA private key file'
  1524. )
  1525. parser.add_argument(
  1526. '-S', '--private_key_password',
  1527. dest='ssh_private_key_password',
  1528. metavar='KEY_PASSWORD',
  1529. type=str,
  1530. help='RSA/DSS/ECDSA private key password'
  1531. )
  1532. parser.add_argument(
  1533. '-t', '--threaded',
  1534. action='store_true',
  1535. help='Allow concurrent connections to each tunnel'
  1536. )
  1537. parser.add_argument(
  1538. '-v', '--verbose',
  1539. action='count',
  1540. default=0,
  1541. help='Increase output verbosity (default: {0})'.format(
  1542. logging.getLevelName(DEFAULT_LOGLEVEL)
  1543. )
  1544. )
  1545. parser.add_argument(
  1546. '-V', '--version',
  1547. action='version',
  1548. version='%(prog)s {version}'.format(version=__version__),
  1549. help='Show version number and quit'
  1550. )
  1551. parser.add_argument(
  1552. '-x', '--proxy',
  1553. type=_bindlist,
  1554. dest='ssh_proxy',
  1555. metavar='IP:PORT',
  1556. help='IP and port of SSH proxy to destination'
  1557. )
  1558. parser.add_argument(
  1559. '-c', '--config',
  1560. type=str,
  1561. default=SSH_CONFIG_FILE,
  1562. dest='ssh_config_file',
  1563. help='SSH configuration file, defaults to {0}'.format(SSH_CONFIG_FILE)
  1564. )
  1565. parser.add_argument(
  1566. '-z', '--compress',
  1567. action='store_true',
  1568. dest='compression',
  1569. help='Request server for compression over SSH transport'
  1570. )
  1571. parser.add_argument(
  1572. '-n', '--noagent',
  1573. action='store_false',
  1574. dest='allow_agent',
  1575. help='Disable looking for keys from an SSH agent'
  1576. )
  1577. parser.add_argument(
  1578. '-d', '--host_pkey_directories',
  1579. nargs='*',
  1580. dest='host_pkey_directories',
  1581. metavar='FOLDER',
  1582. help='List of directories where SSH pkeys (in the format `id_*`) '
  1583. 'may be found'
  1584. )
  1585. return vars(parser.parse_args(args))
  1586. def _cli_main(args=None, **extras):
  1587. """ Pass input arguments to open_tunnel
  1588. Mandatory: ssh_address, -R (remote bind address list)
  1589. Optional:
  1590. -U (username) we may gather it from SSH_CONFIG_FILE or current username
  1591. -p (server_port), defaults to 22
  1592. -P (password)
  1593. -L (local_bind_address), default to 0.0.0.0:22
  1594. -k (ssh_host_key)
  1595. -K (private_key_file), may be gathered from SSH_CONFIG_FILE
  1596. -S (private_key_password)
  1597. -t (threaded), allow concurrent connections over tunnels
  1598. -v (verbose), up to 3 (-vvv) to raise loglevel from ERROR to DEBUG
  1599. -V (version)
  1600. -x (proxy), ProxyCommand's IP:PORT, may be gathered from config file
  1601. -c (ssh_config), ssh configuration file (defaults to SSH_CONFIG_FILE)
  1602. -z (compress)
  1603. -n (noagent), disable looking for keys from an Agent
  1604. -d (host_pkey_directories), look for keys on these folders
  1605. """
  1606. arguments = _parse_arguments(args)
  1607. # Remove all "None" input values
  1608. _remove_none_values(arguments)
  1609. verbosity = min(arguments.pop('verbose'), 4)
  1610. levels = [logging.ERROR,
  1611. logging.WARNING,
  1612. logging.INFO,
  1613. logging.DEBUG,
  1614. TRACE_LEVEL]
  1615. arguments.setdefault('debug_level', levels[verbosity])
  1616. # do this while supporting py27/py34 instead of merging dicts
  1617. for (extra, value) in extras.items():
  1618. arguments.setdefault(extra, value)
  1619. with open_tunnel(**arguments) as tunnel:
  1620. if tunnel.is_alive:
  1621. input_('''
  1622. Press <Ctrl-C> or <Enter> to stop!
  1623. ''')
  1624. if __name__ == '__main__': # pragma: no cover
  1625. _cli_main()