2010-12-10 40 views
4

Je travaille sur un script python pour interroger quelques bases de données distantes sur un tunnel ssh établi de temps en temps. Je connais assez bien la bibliothèque paramiko, donc c'était mon choix de route. Je préférerais garder ceci en python complet afin que je puisse utiliser paramiko pour gérer les problèmes clés, et utiliser python pour démarrer, contrôler et arrêter les tunnels SSH.Arrêt du tunnel SSH Paramiko Problème

Il y a eu quelques questions connexes à propos de ce sujet, mais la plupart semblaient incomplètes dans les réponses. Ma solution ci-dessous est un ensemble piraté des solutions que j'ai trouvées jusqu'ici.

Maintenant pour le problème: je suis capable de créer le premier tunnel assez facilement (dans un thread séparé) et de faire mes trucs DB/python, mais lorsque je tente de fermer le tunnel, localhost ne libère pas le port local Je me suis lié à. Ci-dessous, j'ai inclus ma source et les données Netstat pertinentes à chaque étape du processus.

#!/usr/bin/python 

import select 
import SocketServer 
import sys 
import paramiko 
from threading import Thread 
import time 



class ForwardServer(SocketServer.ThreadingTCPServer): 
    daemon_threads = True 
    allow_reuse_address = True 

class Handler (SocketServer.BaseRequestHandler): 
    def handle(self): 
     try: 
      chan = self.ssh_transport.open_channel('direct-tcpip', (self.chain_host, self.chain_port), self.request.getpeername()) 
     except Exception, e: 
      print('Incoming request to %s:%d failed: %s' % (self.chain_host, self.chain_port, repr(e))) 
      return 
     if chan is None: 
      print('Incoming request to %s:%d was rejected by the SSH server.' % (self.chain_host, self.chain_port)) 
      return 
     print('Connected! Tunnel open %r -> %r -> %r' % (self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port))) 
     while True: 
      r, w, x = select.select([self.request, chan], [], []) 
      if self.request in r: 
       data = self.request.recv(1024) 
       if len(data) == 0: 
        break 
       chan.send(data) 
      if chan in r: 
       data = chan.recv(1024) 
       if len(data) == 0: 
        break 
       self.request.send(data) 
     chan.close() 
     self.request.close() 
     print('Tunnel closed from %r' % (self.request.getpeername(),)) 

class DBTunnel(): 

    def __init__(self,ip): 
     self.c = paramiko.SSHClient() 
     self.c.load_system_host_keys() 
     self.c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 
     self.c.connect(ip, username='someuser') 
     self.trans = self.c.get_transport() 

    def startTunnel(self): 
     class SubHandler(Handler): 
      chain_host = '127.0.0.1' 
      chain_port = 5432 
      ssh_transport = self.c.get_transport() 
     def ThreadTunnel(): 
      global t 
      t = ForwardServer(('', 3333), SubHandler) 
      t.serve_forever() 
     Thread(target=ThreadTunnel).start() 

    def stopTunnel(self): 
     t.shutdown() 
     self.trans.close() 
     self.c.close() 

Bien que je terminerai en utilisant une méthode de type stopTunnel(), j'ai conscience que le code n'est pas tout à fait correct, mais plus encore une expérimentation d'essayer d'obtenir le tunnel à l'arrêt correctement et tester mes résultats .

Quand j'appelle créer l'objet DBTunnel et appeler startTunnel(), les rendements netstat les éléments suivants:

tcp4  0  0 *.3333     *.*     LISTEN 
tcp4  0  0 MYIP.36316  REMOTE_HOST.22    ESTABLISHED 
tcp4  0  0 127.0.0.1.5432   *.*     LISTEN 

Une fois que j'appelle stopTunnel(), ou même supprimer l'objet DBTunnel itself..I'm gauche avec cette connexion jusqu'à python sortie I tous ensemble, et ce que je suppose être le garbage collector prend soin:

tcp4  0  0 *.3333     *.*     LISTEN 

il serait bien de comprendre pourquoi cette prise ouverte est suspendue autour indépendante de l'objet DBConnect , et comment le fermer correctement de l'intérieur mon script. Si j'essaye de lier une connexion différente à une IP différente en utilisant le même port local avant de quitter complètement python (time_wait n'est pas le problème), alors j'obtiens l'adresse tristement célèbre err 48 utilisée. Merci d'avance :)

+1

Le code ci-dessus montre un bug dans l'origina l paramiko exemple de code forward, qui est que le self.request.getpeername() provoque une mauvaise exception de descripteur de fichier car il est appelé après la fermeture de la requête. Le correctif est ici: https://github.com/paramiko/paramiko/pull/36 –

Répondre

1

Il semble que la méthode d'arrêt du SocketServer ne ferme pas/ferme correctement le socket. Avec les changements ci-dessous dans mon code, je conserve l'accès à l'objet SocketServer et j'accède directement au socket pour le fermer. Notez que socket.close() fonctionne dans mon cas, mais d'autres pourraient être intéressés par socket.shutdown() suivi d'un socket.close() si d'autres ressources accèdent à ce socket.

[Ref: socket.shutdown vs Socket.close

def ThreadTunnel(): 
    self.t = ForwardServer(('127.0.0.1', 3333), SubHandler) 
    self.t.serve_forever() 
Thread(target=ThreadTunnel).start() 

def stopTunnel(self): 
    self.t.shutdown() 
    self.trans.close() 
    self.c.close() 
    self.t.socket.close() 
0

Vous pouvez ajouter une synchronisation entre le fil et donné naissance à l'appelant afin que vous ne pas essayer d'utiliser le tunnel avant qu'il ne soit prêt . Quelque chose comme:

from threading import Event 
    def startTunnel(self): 
     class SubHandler(Handler): 
      chain_host = '127.0.0.1' 
      chain_port = 5432 
      ssh_transport = self.c.get_transport() 
     mysignal = Event() 
     mysignal.clear() 
     def ThreadTunnel(): 
      global t 
      t = ForwardServer(('', 3333), SubHandler) 
      mysignal.set() 
      t.serve_forever() 
     Thread(target=ThreadTunnel).start() 
     mysignal.wait() 
1

Notez que vous n'avez pas le hack Subhandler comme indiqué dans le code de démonstration. Le commentaire est faux. Les gestionnaires ont accès aux données de leur serveur. À l'intérieur d'un gestionnaire, vous pouvez utiliser self.server.instance_data.

Si vous utilisez le code suivant, dans votre gestionnaire, vous devez utiliser

  • self.server.chain_host
  • self.server.chain_port
  • self.server.ssh_transport

class ForwardServer(SocketServer.ThreadingTCPServer): 
    daemon_threads = True 
    allow_reuse_address = True 

    def __init__(
      self, connection, handler, chain_host, chain_port, ssh_transport): 
     SocketServer.ThreadingTCPServer.__init__(self, connection, handler) 
     self.chain_host = chain_host 
     self.chain_port = chain_port 
     self.ssh_transport = ssh_transport 
... 

server = ForwardServer(('', local_port), Handler, 
         remote_host, remote_port, transport) 
server.serve_forever() 
+0

+1, bonne prise. Comme indiqué [dans les documents] (http://docs.python.org/2/library/socketserver.html#requesthandler-objects): "Plusieurs attributs d'instance sont disponibles ... [y compris] l'instance de serveur en tant que self.server , dans le cas où [RequestHandler] doit avoir accès à des informations par serveur. " – Air