Add core modules (SSH args parser, cache, resolver, NetBox client) with tests
Release / release (push) Failing after 51s

This commit is contained in:
Sebastian Unterschütz
2026-05-23 12:38:41 +02:00
commit 8ef4bbec16
24 changed files with 2524 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
# netssh
A transparent SSH wrapper that resolves hostnames via [NetBox](https://netbox.dev/) before connecting.
Instead of looking up an IP manually, you just type the hostname as it appears in NetBox:
```sh
netssh my-router-01
netssh -p 2222 admin@app-server-03 uptime
```
`netssh` looks up the host in NetBox, resolves the right IP using a configurable strategy chain, and replaces the process with the native `ssh` binary — so all your existing SSH configs, keys, and agent forwarding work without any changes.
## Features
- **Transparent proxy** — replaces itself with `ssh` via `syscall.Exec`, preserving all SSH flags and options
- **Flexible IP resolution** — configurable chain of strategies: management subnet, primary IP, or named interface
- **Interactive TUI** — fuzzy search with live NetBox queries and 300 ms debouncing (start with `netssh`, no arguments)
- **Persistent cache** — successful lookups are cached to `~/.cache/netssh/hosts.json` for instant shell completion
- **Shell completion** — tab-complete hostnames from the cache in zsh, bash, and fish
- **Default SSH user** — set a fallback username once in config instead of typing it every time
## Installation
### One-liner (Linux & macOS)
```sh
curl -fsSL https://git.zb-server.de/Sebi/ssh-netbox-wrapper/raw/branch/main/install.sh | bash
```
The script detects your OS and architecture, downloads the matching binary from the [latest release](https://git.zb-server.de/Sebi/ssh-netbox-wrapper/releases/latest), verifies the SHA-256 checksum, and installs to `/usr/local/bin/netssh` (using `sudo` only if necessary).
To install to a custom directory:
```sh
INSTALL_DIR=~/.local/bin curl -fsSL https://git.zb-server.de/Sebi/ssh-netbox-wrapper/raw/branch/main/install.sh | bash
```
### Build from source
```sh
git clone ssh://git@git.zb-server.de:30022/Sebi/ssh-netbox-wrapper.git
cd ssh-netbox-wrapper
go build -o netssh ./cmd/netssh
```
## Configuration
Create `~/.config/netssh.yaml`:
```yaml
netbox:
url: https://netbox.example.com
token: your-api-token-here
resolver:
# Strategies are tried in order; the first to return an IP wins.
strategies:
- management_subnet
- primary_ip
# Used by the management_subnet strategy.
management_subnets:
- 10.0.0.0/8
- 172.16.0.0/12
# Used by the interface_name strategy.
interface_name: mgmt0
cache:
ttl: 3600 # seconds; 0 = always query NetBox on connect (cache still used for completion)
# path: ~/.cache/netssh/hosts.json # default
ssh:
default_user: admin # used when no user is specified on the command line
```
Any value can be overridden with environment variables (`NETSSH_NETBOX_URL`, `NETSSH_NETBOX_TOKEN`, etc.) or will be read from the config file.
## Usage
### SSH wrapper mode
Pass any SSH flags and a NetBox hostname:
```sh
netssh my-router-01
netssh -p 2222 admin@app-server-03 uptime
netssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no db-primary
```
The process is replaced by `ssh` with the resolved IP — your `~/.ssh/config`, agent, and keys all work as normal.
### Default username
Set `ssh.default_user` in the config to avoid typing a username every time:
```sh
netssh my-router # → ssh -l admin 10.0.0.1
```
The default is only applied when no user is specified on the command line. An explicit user always takes precedence:
```sh
netssh root@my-router # user@ prefix wins → ssh root@10.0.0.1
netssh -l ops my-router # -l flag wins → ssh -l ops 10.0.0.1
```
### Interactive TUI
Run without arguments to open the interactive search:
```sh
netssh
```
| Key | Action |
|-----|--------|
| type | filter hosts (300 ms debounce → NetBox query) |
| `Tab` | autocomplete top result into the search field |
| `↑` / `↓` | navigate results |
| `Enter` | connect to selected host |
| `Esc` / `Ctrl+C` | quit |
### Cache management
```sh
netssh cache list # show all cached entries
netssh cache refresh # re-fetch all hosts from NetBox
netssh cache clear # wipe the cache
```
### Search (for scripting)
```sh
netssh search app- # prints matching hostnames, one per line
```
## IP Resolution Strategies
Strategies are tried in the configured order; the first to succeed wins.
| Name | Description |
|------|-------------|
| `primary_ip` | Returns the `primary_ip4` (or `primary_ip6`) set in NetBox. No extra API call. |
| `management_subnet` | Fetches all IPs for the host and returns the first one matching a configured CIDR. |
| `interface_name` | Fetches IPs attached to a specific named interface (e.g. `mgmt0`). |
## Shell Completion
### zsh
```sh
netssh completion zsh > "${fpath[1]}/_netssh"
```
Or add to `.zshrc`:
```zsh
source <(netssh completion zsh)
```
### bash
```sh
netssh completion bash > /etc/bash_completion.d/netssh
```
### fish
```sh
netssh completion fish > ~/.config/fish/completions/netssh.fish
```
Completions are served from the local cache — no network request on every `<Tab>`.
## Development
```sh
go test ./... # run all tests
go build ./... # build all packages
```
The test suite covers the cache, NetBox client (via `httptest`), IP resolver chain, and SSH argument parser.
## How it works
1. `netssh` checks whether the first argument is a known subcommand (`search`, `cache`, `completion`). If not, it enters SSH wrapper mode.
2. It parses the SSH arguments to extract the destination hostname, handling all flags that consume an extra argument (`-p`, `-i`, `-J`, …).
3. It checks the local cache. If the entry exists and is within the TTL, it connects immediately.
4. Otherwise it queries NetBox (`/api/dcim/devices/` and `/api/virtualization/virtual-machines/` in parallel), runs the result through the resolver chain, and caches the IP.
5. It calls `syscall.Exec` to replace itself with `ssh`, substituting the hostname with the resolved IP.