Skip to content

feat: add workstation-wide active bastion routing - #129

Merged
kayodebristol merged 3 commits into
mainfrom
feat/jump-host-tunnel
Aug 23, 2026
Merged

feat: add workstation-wide active bastion routing#129
kayodebristol merged 3 commits into
mainfrom
feat/jump-host-tunnel

Conversation

@kayodebristol

Copy link
Copy Markdown
Contributor

Summary

  • add a user-local authenticated SSH bastion service with automatic TCP routing for toolkit callers
  • route core connections, scanner SSH discovery, and Ansible modules without inventory-specific proxy fields
  • retain legacy per-device jump-host compatibility and document active-bastion usage

Validation

  • python -m ruff check ...
  • python -m pytest -o addopts='' targeted suite (341 passed)

Adds JumpHostParams + ConnectionParams.jump_host so DeviceConnection can
tunnel through an SSH jump box (Paramiko direct-tcpip channel handed to
Netmiko via sock=) purely as network transport - no netops-toolkit code
runs on the jump box. Enables running the toolkit natively on Windows
(newer Python, no admin) while still reaching switches only reachable
from an existing, unmaintained Linux bastion.

- netops/core/connection.py: JumpHostParams dataclass, DeviceConnection
  wires sock= through _open_jump_channel() when jump_host is set, closes
  the paramiko client on disconnect(); resolve_jump_host_params() helper
  resolves inventory fields + vault credentials.
- netops/core/inventory.py: Device gains jump_host/jump_port/jump_username/
  jump_key_file fields, resolved via the existing defaults-merge pattern.
- netops/check/health.py: wires resolve_jump_host_params() into the
  inventory-driven ConnectionParams build (reference call-site; other
  check/collect modules can adopt the same one-line pattern).
- tests/test_jump_host.py: 15 new tests covering resolve_jump_host_params,
  tunneled vs non-tunneled DeviceConnection.connect()/disconnect() (mocked
  at the paramiko/netmiko seam, no live devices), inventory YAML parsing
  of jump-host fields, and the telnet+jump_host rejection path.
- docs/guides/jump-host-tunnel.md: new guide with Windows setup, inventory
  config, vault credential wiring, and the creativity-skill design
  rationale (4 mechanisms considered, why in-process Paramiko channel won).
- README.md: feature table + inventory format section updated.
@kayodebristol
kayodebristol marked this pull request as ready for review August 23, 2026 23:21
Copilot AI balanced review requested due to automatic review settings August 23, 2026 23:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds workstation-wide SSH bastion routing while preserving per-device jump-host support.

Changes:

  • Introduces an authenticated local SOCKS5 bastion service.
  • Routes device, discovery, and Ansible connections through bastions.
  • Adds inventory fields, documentation, and tests.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
netops/core/bastion.py Implements active bastion service and CLI.
netops/core/connection.py Adds active and legacy bastion routing.
netops/core/inventory.py Adds per-device jump-host fields.
netops/inventory/scan.py Routes discovery through active bastions.
netops/collect/config.py Applies inventory jump-host settings.
netops/collect/backup.py Applies jump-host settings to backups.
netops/check/vlan.py Adds bastion support to VLAN checks.
netops/check/paloalto.py Adds bastion support to Palo Alto checks.
netops/check/juniper.py Adds bastion support to Juniper checks.
netops/check/health.py Adds bastion support to health checks.
netops/check/cisco.py Adds bastion support to Cisco checks.
netops/check/bgp.py Adds bastion support to BGP checks.
netops/check/arista.py Adds bastion support to Arista checks.
netops/ansible/modules/netops_facts.py Routes fact collection through active bastions.
netops/ansible/modules/netops_command.py Routes commands through active bastions.
netops/__main__.py Registers the bastion CLI command.
tests/test_jump_host.py Tests legacy jump-host behavior.
tests/test_active_bastion.py Tests active routing and SOCKS protocol helpers.
README.md Advertises and demonstrates bastion support.
docs/guides/jump-host-tunnel.md Documents per-device tunneling.
docs/guides/active-bastion.md Documents workstation-wide routing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread netops/core/bastion.py Outdated
Comment on lines +88 to +92
try:
response = _control_request(state, "status")
except OSError:
return None
return state if response.get("connected") is True else None
Comment thread netops/core/connection.py
Comment on lines +161 to +165
active_socket = open_active_bastion_socket(
self.params.host, self.params.effective_port, self.params.timeout
)
if active_socket is not None:
device_params["sock"] = active_socket
Comment thread netops/core/bastion.py Outdated
Comment on lines +442 to +443
"--token",
state.token,
Comment thread netops/core/connection.py
Comment on lines +206 to +207
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
Comment thread netops/core/connection.py Outdated
Comment on lines +254 to +263
if self._connection:
self._connection.disconnect()
logger.info(f"Disconnected from {self.params.host}")
if self._jump_client is not None:
self._jump_client.close() # type: ignore[attr-defined]
self._jump_client = None
logger.info(f"Closed jump-host tunnel for {self.params.host}")
if self._active_bastion_socket is not None:
self._active_bastion_socket.close() # type: ignore[attr-defined]
self._active_bastion_socket = None
Comment thread netops/core/bastion.py
Comment on lines +300 to +304
channel = service.transport.open_channel(host, port, timeout=30)
self.request.sendall(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
self._bridge(channel)
except Exception as exc:
logger.debug("active bastion SOCKS request failed: %s", exc)
Comment thread netops/core/connection.py
Comment on lines +361 to +365
"""Translate a :class:`netops.core.inventory.Device`'s bastion fields.

Keeping this mapping at the connection boundary means every inventory-driven
command uses exactly the same tunnel semantics rather than independently
rebuilding a partial set of jump-host parameters.
Comment thread docs/guides/jump-host-tunnel.md Outdated
Comment on lines +188 to +190
Three tunnel mechanisms were considered for the Windows→bastion→switch
path; a fourth (single shared/multiplexed bastion connection reused across
devices) was noted but not implemented in this pass:
Comment thread netops/core/bastion.py
temporary.replace(path)


def connect_active_bastion(
Comment thread netops/core/bastion.py Outdated
def _write_state(state: ActiveBastion, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(asdict(state), indent=2), encoding="utf-8")
… security

Co-authored-by: kayodebristol <3579196+kayodebristol@users.noreply.github.com>
@kayodebristol
kayodebristol merged commit 2455124 into main Aug 23, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants