Skip to content

SSH tunneling fails using paramiko+sshtunnel #491

@joefrancia

Description

@joefrancia

Describe the bug

mssql_python.connect() fails with a TCP timeout error when connecting through an SSH tunnel established via paramiko + sshtunnel. The identical connection works correctly when the SSH tunnel is established manually via OpenSSH in the terminal (ssh -L), and the identical paramiko+sshtunnel setup works correctly when using pymssql as the driver.

The SSH session dies approximately 15 seconds after authentication with SSH session not active before the channel can be opened. Logging suggests mssql_python.connect() is delaying ~15 seconds before attempting to open the tunnel channel, long enough for the server-side SSH session idle timeout to kill the session. pymssql does not exhibit this delay and connects successfully through the same tunnel code.

Exception message: RuntimeError: [Microsoft][ODBC Driver 18 for SQL Server]TCP Provider: Timeout error [258].
Stack trace:
  File "test_mssql.py", line 30, in <module>
    conn = mssql_python.connect(CONNECTION_STRING)
RuntimeError: [Microsoft][ODBC Driver 18 for SQL Server]TCP Provider: Timeout error [258].

Paramiko/sshtunnel logs at point of failure:

INFO  | Authentication (publickey) successful!
DEBUG | Received global request "hostkeys-00@openssh.com"
DEBUG | Rejecting "hostkeys-00@openssh.com" global request from server.
DEBUG | Debug msg: b'/home/ubuntu/.ssh/authorized_keys:1: key options: ...'
# ~15 seconds of silence here
ERROR | Could not establish connection from local ('127.0.0.1', 52316) to remote ('DB_HOST_INTERNAL_IP', 1433) side of the tunnel: open new channel ssh error: SSH session not active

For comparison, pymssql with identical tunnel code opens the channel immediately after auth with no delay:

INFO  | Authentication (publickey) successful!
DEBUG | [chan 0] Max packet in: 32768 bytes
DEBUG | [chan 0] Max packet out: 32768 bytes
DEBUG | Secsh channel 0 opened.
# connects successfully

To reproduce

import mssql_python
import paramiko
from sshtunnel import SSHTunnelForwarder

pkey = paramiko.Ed25519Key.from_private_key_file('/path/to/key.pem')

with SSHTunnelForwarder(
    ssh_address_or_host=("JUMPBOX_IP", 22),
    ssh_username="ubuntu",
    ssh_pkey=pkey,
    remote_bind_address=("DB_HOST_INTERNAL_IP", 1433),
    local_bind_address=("127.0.0.1", 0),
) as tunnel:
    CONNECTION_STRING = (
        f"Server=127.0.0.1,{tunnel.local_bind_port};"
        "UID=myuser;"
        "PWD=mypassword;"
        "Encrypt=no;"
        "TrustServerCertificate=yes;"
        "MultiSubnetFailover=no;"
    )
    conn = mssql_python.connect(CONNECTION_STRING)  # times out after ~15s
    res = conn.execute("SELECT 1")
    print(res.fetchall())

Swapping mssql_python.connect(CONNECTION_STRING) for pymssql.connect(host, user, password, db) with the same tunnel succeeds immediately.

The tunnel also works correctly if established manually first:

ssh -i /path/to/key.pem -L 1433:DB_HOST_INTERNAL_IP:1433 ubuntu@JUMPBOX_IP

...and then connecting with mssql_python directly to 127.0.0.1,1433 without sshtunnel.

Expected behavior

mssql_python.connect() should open the tunnel channel promptly after the SSH session is authenticated, consistent with the behavior of pymssql and OpenSSH tunnels. The ~15 second pre-connect delay (likely caused by ODBC Driver 18 performing DNS resolution, SQL Browser probing on UDP 1434, or TLS certificate validation against 127.0.0.1) should not occur when an explicit Server=127.0.0.1,<port> is provided in the connection string.

Further technical details

Python version: 3.12 (macOS)
SQL Server version: (to be filled in)
Operating system: macOS 26.3.1 (Apple Silicon)
sshtunnel version: 0.4.0
paramiko version: 3.5.1
mssql_python version: 1.4.0 (Microsoft ODBC Driver 18 for SQL Server)

Additional context

The 15-second delay matches a typical server-side ClientAliveInterval or LoginGraceTime SSH timeout, suggesting mssql_python/ODBC Driver 18 is performing significant pre-connect work before opening any socket, unlike pymssql which uses a direct pure-Python TDS implementation with no such delay. Adding MultiSubnetFailover=no to the connection string did not resolve the issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage neededFor new issues, not triaged yet.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions