How to use Python SSHTunnle to forward multiple ports

Question:

I need to forward to multiple ports which are sits behind a server

server1(22) -> Server2(mysql, 3360) = local 3360
            -> Server3(http, 8080)  = local 8080
            -> Server4(oracle,1234) = local 1234

I can only access Server2,3, and 4 via server1.

I am using Python ssltunnel package https://pypi.org/project/sshtunnel/

In example1&2, I can only specify one remote&local bind address.
Not sure how to connect multiple servers(2,3,4)

Example1

from sshtunnel import SSHTunnelForwarder

server = SSHTunnelForwarder(
    'pahaz.urfuclub.ru',
    ssh_username="pahaz",
    ssh_password="secret",
    remote_bind_address=('127.0.0.1', 8080)
)

server.start()

print(server.local_bind_port)  # show assigned local port
# work with `SECRET SERVICE` through `server.local_bind_port`.

server.stop()

Example 2

import paramiko
import sshtunnel

with sshtunnel.open_tunnel(
    (REMOTE_SERVER_IP, 443),
    ssh_username="",
    ssh_pkey="/var/ssh/rsa_key",
    ssh_private_key_password="secret",
    remote_bind_address=(PRIVATE_SERVER_IP, 22),
    local_bind_address=('0.0.0.0', 10022)
) as tunnel:
    client = paramiko.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect('127.0.0.1', 10022)
    # do some operations with client session
    client.close()

print('FINISH!')

I could use any other Python package that can do the job.

Asked By: learner2017

||

Answers:

Both examples can be modified slightly to work the way you want.

There is the singular versions of bindings (local_bind_address & remote_bind_address) and the plural versions of bindings (local_bind_addresses & remote_bind_addresses.

The singular verisons expects a tuple containing variables for the connections, while the plural versions expects a list of one or more tuple(s).

Here is a modified version of your example 2:

import paramiko
import sshtunnel

tunnels = [("172.16.0.1", 80),
           ("172.16.0.2", 22)]

localPorts = [("127.0.0.1", 1180),
              ("127.0.0.1", 10022)]

with sshtunnel.open_tunnel(
    (REMOTE_SERVER_IP, 22),
    ssh_username="",
    ssh_pkey="/var/ssh/rsa_key",
    ssh_private_key_password="secret",
    remote_bind_addresses=tunnels,
    local_bind_addresses=localPorts
) as tunnel:
    client = paramiko.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect('127.0.0.1', 10022)
    # do some operations with client session
    client.close()

If the lengths of the lists are the same length, then the IP-addresses / ports will correspond with each other.

In my example above, the following is happening:

Connection: 172.16.0.1 Port: 80, Is tunneled via: 127.0.0.1 Port: 1180

Connection: 172.16.0.2 Port: 22, Is tunneled via: 127.0.0.1 Port:
10022

Answered By: Hampus Larsson

I am adding this solution for multiple hops on multiple ports:

I have this setup:
enter image description here
the goal is to access the database by calling my machine on port 33306. This isn’t possible because only gateway2 is allowed to speak to the database. We cant access gateway2 because only gateway1 is allowed to speak to it.

the following is the corresponding ssh .config file:

Host gateway1
    HostName gtw1_IP_address
    User gtw1_user
    IdentityFile "path_to_gtw1_ssh_key"
    IdentitiesOnly True

Host gateway2
    User gtw2_user
    Hostname gtw2_IP_address
    IdentityFile "path_to_gtw2_ssh_key"
    IdentitiesOnly True
    # mysql
    LocalForward 127.0.0.1:33306 127.0.0.1:3306
    ProxyCommand ssh -W %h:%p switch-cede

this is how I reproduce it in python:

from sqlalchemy import create_engine
import config
import pandas as pd
import sshtunnel
from paramiko import SSHClient

with sshtunnel.open_tunnel(
    ssh_username='gtw1_user',
    ssh_address_or_host=('gtw1_IP_address', 22),
    remote_bind_addresses=[('gtw2_IP_address', 22), ('gtw2_IP_address', 33306)],
    local_bind_addresses=[('0.0.0.0', 22), ('0.0.0.0', 33306)], #this line is optional
    ssh_pkey=path_to_gtw1_ssh_key,
) as tunnel1: # tunnel1 is the tunnel between myMachine and gateway1 I believe
    
    print(tunnel1.local_bind_ports)
    with sshtunnel.open_tunnel(
        ssh_address_or_host=('localhost', tunnel1.local_bind_ports[0]),
        remote_bind_addresses=[('127.0.0.1', 22),('127.0.0.1', 3306)],
        local_bind_addresses=[('0.0.0.0', 22), ('127.0.0.1', 33306)],
        ssh_username='gtw2_user',
        ssh_pkey=path_to_gtw2_ssh_key,
    ) as tunnel2: # tunnel2 is the tunnel between gtw1 and gtw2 I believe
       
        print(tunnel2.local_bind_ports)
        db = create_engine(
            f"mysql+pymysql://{config.USER}:{config.PASSWORD}@{config.HOST}:{config.PORT}/{config.DATABASE}")
        print(db)
        query = "SELECT * FROM randomTable LIMIT 10;"
        df = pd.read_sql(query, db)
        print(df)
# note that config is a file holding the credentials to connect to the database
Answered By: Yannick Pezeu
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.