Metadata-Version: 2.4
Name: reachssh
Version: 0.1.0
Summary: SSH to network gear you can't reach directly — legacy crypto + jump-host support over Paramiko.
Author: Scott Peterman
License: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/scottpeterman/reachssh
Project-URL: Repository, https://github.com/scottpeterman/reachssh
Keywords: ssh,paramiko,network,automation,bastion,jump-host,legacy,cisco
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: System Administrators
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: paramiko==4.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# reachssh

SSH to network gear you can't reach directly.

A thin [Paramiko](https://www.paramiko.org/) wrapper built for the awkward
realities of network-device SSH — the two problems plain Paramiko makes you
solve yourself, solved once:

1. **Legacy crypto.** Old IOS/NX-OS/Junos/ASA gear negotiates ancient KEX,
   ciphers, and host-key types (DH group14/group1-SHA1, 3DES-CBC, ssh-rsa
   with SHA-1) that modern OpenSSH and recent Paramiko disable by default.
   reachssh registers every algorithm handler the installed Paramiko ships
   and runs a two-pass connect (force ssh-rsa/SHA-1 first, fall back to
   rsa-sha2 for hardened devices) so both crusty and modern gear just work.

2. **Jump hosts / bastions.** Paramiko reads `ProxyJump` from *nowhere* — not
   from `~/.ssh/config`, not from anywhere. reachssh opens a `direct-tcpip`
   channel through the bastion and tunnels the device connection over it,
   with the channel re-opened per connect attempt so the legacy/​modern retry
   still works through the jump.

Everything else it does is the network-CLI plumbing you'd otherwise rewrite
per tool: interactive `invoke_shell` sessions, prompt detection, vendor-
agnostic pagination disable, and deterministic command-output stripping so
config snapshots diff cleanly run-to-run.

reachssh depends only on Paramiko.

## Install

```bash
pip install reachssh
```

## Basic use

```python
from reachssh import SSHClient, SSHClientConfig

cfg = SSHClientConfig(
    host="172.16.1.2",
    username="cisco",
    password="cisco",
    legacy_mode=True,      # register + prefer legacy algorithms
)

with SSHClient(cfg) as c:
    c.find_prompt()
    c.disable_pagination()
    print(c.execute_command("show ip interface brief"))
```

## Through a jump host

A single bastion hop — build a `JumpSpec` and hand it to the config:

```python
from reachssh import SSHClient, SSHClientConfig, JumpSpec, JumpHop

jump = JumpSpec(hops=[JumpHop(
    name="thinkstation",
    host="10.0.0.108",
    username="speterman",
    key_content=open("/home/me/.ssh/id_rsa").read(),   # or password=...
)])

cfg = SSHClientConfig(
    host="172.16.1.2", username="cisco", password="cisco",
    legacy_mode=True, jump=jump,
)

with SSHClient(cfg) as c:
    c.find_prompt()
    print(c.execute_command("show version"))
```

## Rule-based jump selection (many devices, different bastions)

Route-map / ACL ordering — rules are evaluated top-to-bottom, **first match
wins**, so a special-case device rule sits *above* the broad site rule and
wins by position:

```python
from reachssh import build_proxy_resolver

proxy_config = {
    "jump_hosts": {
        "iad-bastion": {"host": "10.0.0.108", "port": 22, "credential": "lab-jump"},
    },
    "proxy_rules": [
        {"match": {"devices": ["bastion-*"]}, "jump": "direct"},   # the bastion itself
        {"match": {"site": "iad-lab"}, "jump": "iad-bastion"},
    ],
}

# reachssh never touches secrets at rest. YOU supply a resolver mapping a
# credential *name* to a tuple: (user, password) or (user, password, key_pem).
creds = {"lab-jump": ("speterman", None, open("/home/me/.ssh/id_rsa").read())}
resolver = build_proxy_resolver(proxy_config, credential_resolver=creds.get)

spec = resolver.resolve({"name": "wan-core-1", "site_slug": "iad-lab"})
# -> JumpSpec through iad-bastion; pass as SSHClientConfig(jump=spec)
```

The `credential_resolver` is the only place bastion secrets enter the library.
Pass a vault lookup, a dict `.get`, a lambda reading env vars — whatever you
already use.

## Migrating a tool off its own SSH client

If you have a tool carrying its own copy of this client (this code originated
inside such tools), replace the vendored `ssh/client.py` import with:

```python
from reachssh import SSHClient, SSHClientConfig
```

Tools that never had jump support gain it for free — populate
`SSHClientConfig.jump` and connections route through the bastion with no other
change.

## Paramiko version note

reachssh registers whatever algorithm handlers the installed Paramiko ships
and silently skips ones that aren't present — it never crashes on a missing
handler. Consequence: on **Paramiko 5.x**, the `kex_group1` module was removed,
so `diffie-hellman-group1-sha1` cannot be offered. `group14-sha1`, `3des-cbc`,
and `ssh-rsa` (SHA-1) are still available and cover the large majority of
legacy gear. If you must talk to a device that **only** speaks
`diffie-hellman-group1-sha1`, pin **Paramiko ≤ 4.x**, where that handler still
ships.

## Emulation (optional)

For testing without live gear, `enable_emulation("ip_lookup.json")` transparently
redirects connections to a local mock-device server. Inert unless enabled.

## License

MIT
