susops 3.0.0rc3.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- susops/__init__.py +4 -0
- susops/client.py +230 -0
- susops/core/__init__.py +0 -0
- susops/core/browsers.py +330 -0
- susops/core/config.py +253 -0
- susops/core/log_style.py +92 -0
- susops/core/pac.py +185 -0
- susops/core/ports.py +57 -0
- susops/core/process.py +167 -0
- susops/core/rpc_protocol.py +186 -0
- susops/core/rpc_server.py +131 -0
- susops/core/services_daemon.py +312 -0
- susops/core/share.py +323 -0
- susops/core/socat.py +200 -0
- susops/core/ssh.py +330 -0
- susops/core/ssh_config.py +40 -0
- susops/core/status.py +245 -0
- susops/core/types.py +171 -0
- susops/facade.py +2237 -0
- susops/tray/__init__.py +20 -0
- susops/tray/base.py +650 -0
- susops/tray/linux.py +1623 -0
- susops/tray/mac.py +3105 -0
- susops/tui/__init__.py +0 -0
- susops/tui/__main__.py +44 -0
- susops/tui/app.py +191 -0
- susops/tui/cli.py +665 -0
- susops/tui/screens/__init__.py +114 -0
- susops/tui/screens/connections.py +871 -0
- susops/tui/screens/dashboard.py +935 -0
- susops/tui/screens/shares.py +357 -0
- susops/tui/widgets/__init__.py +0 -0
- susops/tui/widgets/connection_card.py +137 -0
- susops/version.py +12 -0
- susops-3.0.0rc3.dev1.dist-info/METADATA +977 -0
- susops-3.0.0rc3.dev1.dist-info/RECORD +40 -0
- susops-3.0.0rc3.dev1.dist-info/WHEEL +5 -0
- susops-3.0.0rc3.dev1.dist-info/entry_points.txt +7 -0
- susops-3.0.0rc3.dev1.dist-info/licenses/LICENSE +674 -0
- susops-3.0.0rc3.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: susops
|
|
3
|
+
Version: 3.0.0rc3.dev1
|
|
4
|
+
Summary: SusOps — SSH SOCKS5 proxy manager with PAC server, Textual TUI, and system tray apps
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pydantic==2.13.3
|
|
10
|
+
Requires-Dist: ruamel.yaml>=0.18
|
|
11
|
+
Requires-Dist: psutil>=5.9
|
|
12
|
+
Requires-Dist: aiohttp>=3.9
|
|
13
|
+
Requires-Dist: cryptography>=42
|
|
14
|
+
Provides-Extra: tui
|
|
15
|
+
Requires-Dist: textual>=0.80; extra == "tui"
|
|
16
|
+
Requires-Dist: textual-plotext>=0.2; extra == "tui"
|
|
17
|
+
Provides-Extra: tray-linux
|
|
18
|
+
Provides-Extra: udp
|
|
19
|
+
Provides-Extra: tray-mac
|
|
20
|
+
Requires-Dist: rumps>=0.4; extra == "tray-mac"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
24
|
+
Requires-Dist: textual-dev; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="assets/icon.png" alt="Menu" height="200" />
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
# SusOps - SSH Utilities & SOCKS5 Operations
|
|
32
|
+
|
|
33
|
+
SSH SOCKS5 proxy manager with PAC server, TCP/UDP port forwarding, Textual TUI, and system tray apps.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
SusOps manages SSH SOCKS5 proxy tunnels and serves a PAC (Proxy Auto-Config) file so browsers and other tools route traffic through your tunnels automatically. It replaces a 1600-line Bash CLI with a modern Python stack:
|
|
38
|
+
|
|
39
|
+
- **Textual TUI** — interactive split-pane dashboard, live bandwidth charts, CRUD editor, integrated log viewer
|
|
40
|
+
- **Non-interactive CLI** — scriptable `susops` command with semantic exit codes
|
|
41
|
+
- **Linux tray app** — GTK3 + AyatanaAppIndicator3
|
|
42
|
+
- **macOS tray app** — rumps + PyObjC
|
|
43
|
+
- **Shared Python core** — all business logic in `susops.core`, used by every frontend
|
|
44
|
+
- **TCP port forwarding** — SSH `-L`/`-R` slaves multiplexed over the existing ControlMaster
|
|
45
|
+
- **UDP port forwarding** — socat over SSH ControlMaster; no extra SSH ports needed
|
|
46
|
+
|
|
47
|
+
### Architecture
|
|
48
|
+
|
|
49
|
+
SusOps is split into a **services daemon** (`susops-services`) that owns the long-lived async functionality, and **thin frontends** (CLI, TUI, tray apps) that talk to it over a local JSON-over-HTTP RPC channel plus a Server-Sent Events stream. There is one daemon per workspace. If it isn't running when a frontend starts, the frontend auto-spawns it. When the last frontend disconnects and no SSH tunnels / shares / PAC are active, the daemon exits cleanly.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
susops/
|
|
53
|
+
src/susops/
|
|
54
|
+
core/ # Business logic (no UI)
|
|
55
|
+
services_daemon.py # Daemon main: PID claim, signal handlers, idle-shutdown
|
|
56
|
+
rpc_server.py # aiohttp /rpc — JSON-over-HTTP, allowlist-gated dispatch
|
|
57
|
+
rpc_protocol.py # InvocationRequest/Response, type-tagged JSON encoder
|
|
58
|
+
status.py # SSE /events server — state · share · forward · bandwidth
|
|
59
|
+
config.py # Pydantic v2 models + ruamel.yaml I/O (atomic save)
|
|
60
|
+
ssh.py # SSH ControlMaster + live -O forward / -O cancel
|
|
61
|
+
socat.py # UDP port forwarding via socat over SSH ControlMaster
|
|
62
|
+
pac.py # PAC generation + aiohttp HTTP server (shared async loop)
|
|
63
|
+
share.py # AES-256-CTR encrypted file sharing + client fetch
|
|
64
|
+
process.py # ProcessManager: PID files, start/stop/status, zombie detection
|
|
65
|
+
ports.py # Free port allocation, CIDR helpers
|
|
66
|
+
log_style.py # Shared log-line styler (tag/ok/warn/err colours)
|
|
67
|
+
types.py # Enums and result dataclasses
|
|
68
|
+
facade.py # SusOpsManager — in-process API used INSIDE the daemon
|
|
69
|
+
client.py # SusOpsClient — RPC proxy used by every frontend
|
|
70
|
+
tui/ # Textual TUI + argparse CLI
|
|
71
|
+
tray/ # GTK3 (Linux) and rumps (macOS) tray apps
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Daemon ↔ Frontend protocol
|
|
75
|
+
|
|
76
|
+
There are two channels between each frontend and the daemon, both bound to `127.0.0.1`:
|
|
77
|
+
|
|
78
|
+
| Channel | Endpoint | Direction | Use |
|
|
79
|
+
|---------|-----------------------|------------------|-----------------------------------------------------------------------------------------------------------|
|
|
80
|
+
| RPC | `POST /rpc` | frontend → daemon | One method invocation per request. Allowlisted methods from `_ALLOWED_METHODS` in `rpc_server.py` — anything else is rejected. Synchronous, request/response. |
|
|
81
|
+
| SSE | `GET /events` | daemon → frontend | Long-lived Server-Sent-Events stream. Pushes `state`, `share`, `forward`, and `bandwidth` events so frontends update instantly without polling. |
|
|
82
|
+
|
|
83
|
+
Ports are auto-allocated and written to `~/.susops/pids/susops-services.port` so frontends know where to dial. The PID file (`susops-services.pid`) is claimed atomically with `O_CREAT | O_EXCL` — two daemons racing each other can't both win, the loser exits with a clear "another daemon is running" message.
|
|
84
|
+
|
|
85
|
+
The frontend's `SusOpsClient` exposes the same API surface as `SusOpsManager` via `__getattr__` magic, so frontend code reads `mgr.start(tag="work")` the same whether it's running in-process (inside the daemon) or remoting over HTTP (everywhere else). Connection refused mid-call triggers exactly one retry through `ensure_daemon_running`, which transparently respawns the daemon if it died.
|
|
86
|
+
|
|
87
|
+
**Idle shutdown.** The daemon exits as soon as (a) the last SSE client disconnects AND (b) it has nothing tracked: no SSH masters, no shares, PAC down, reconnect monitor watching nothing. A 5 s safety-net check catches the "CLI fired one RPC and exited" case where no SSE connection ever opened. Startup gets a 3 s grace so a freshly-spawned daemon doesn't exit before its first SSE listener lands.
|
|
88
|
+
|
|
89
|
+
#### OpenAPI schema
|
|
90
|
+
|
|
91
|
+
The full RPC surface (every allowlisted method, its kwargs schema, its return type, plus the SSE event payloads) is documented as OpenAPI 3.1 at [`docs/openapi.yaml`](docs/openapi.yaml). The spec is auto-generated from `SusOpsManager` + `_ALLOWED_METHODS` via `tools/gen_openapi.py`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python tools/gen_openapi.py # regenerate docs/openapi.yaml
|
|
95
|
+
python tools/gen_openapi.py --check # CI: fail if stale
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A pytest case (`tests/test_openapi.py`) runs `--check` on every PR, so the committed spec can never drift from the source. Every tagged release also re-runs the generator and attaches the resulting `openapi.yaml` as a release asset, so the schema published alongside each version is always exact.
|
|
99
|
+
|
|
100
|
+
Plug the spec into Swagger UI / Redoc / Stoplight for browsable docs, or feed it into a client generator (`openapi-generator`, `oapi-codegen`, etc.) to build alternative frontends.
|
|
101
|
+
|
|
102
|
+
#### Access & authentication
|
|
103
|
+
|
|
104
|
+
The daemon is built for **single-user local-only access** — there is no authentication, no shared secret, and no per-method permission model. The protections in place are:
|
|
105
|
+
|
|
106
|
+
| Layer | Mechanism | What it prevents |
|
|
107
|
+
|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
108
|
+
| Bind address | Both `/rpc` and `/events` listen on `127.0.0.1` only | Remote hosts on the LAN/internet cannot reach the daemon. |
|
|
109
|
+
| Method allowlist | `_ALLOWED_METHODS` in `rpc_server.py` — any method not in the set returns `404 method not allowed` | Private helpers (`mgr._whatever`) can't be invoked. Defence against *accidental* exposure, not malicious callers. |
|
|
110
|
+
| Port discovery | Port + PID are written to `~/.susops/pids/susops-services.{port,pid}` | Filesystem perms (default `0644`) gate read access. A process running as a different local user that can't read your home directory can't find the port. |
|
|
111
|
+
|
|
112
|
+
**What this means in practice:**
|
|
113
|
+
|
|
114
|
+
- **Single-user laptop / desktop:** safe. Only your own processes can reach loopback and read the port file.
|
|
115
|
+
- **Multi-user host / shared dev box:** any other local user **whose UID can read `~/.susops/pids/susops-services.port`** can call any allowlisted method on your daemon. That means starting/stopping tunnels, reading config, listing shares, fetching files. The kernel doesn't help here — you'd need filesystem ACLs.
|
|
116
|
+
- **Untrusted local processes** (browser extensions with loopback access, sandboxed apps, etc.): same caveat. Loopback is treated as a security boundary, not a single trust zone.
|
|
117
|
+
|
|
118
|
+
**Hardening options** if you need stricter control:
|
|
119
|
+
|
|
120
|
+
- **Tighten the port file** — `chmod 600 ~/.susops/pids/susops-services.port` keeps it readable only by your UID. Not currently the default, but a one-line change to `services_daemon.py:_port_path` would make it permanent.
|
|
121
|
+
- **Unix domain socket instead of TCP** — bind aiohttp to `~/.susops/pids/susops-services.sock` with `0600` and kernel-enforced perms become the access control. Cleaner, no token rotation needed.
|
|
122
|
+
- **Per-daemon bearer token** — generate a random secret on startup, write it to `~/.susops/pids/susops-services.token` with `0600`, require it as an `Authorization: Bearer ...` header. `SusOpsClient` reads the file on each call.
|
|
123
|
+
|
|
124
|
+
None of these are implemented today; the daemon assumes a single-user trust zone. If your threat model includes co-resident users or hostile loopback-reachable software, the cheapest fix is the port-file `chmod 600`.
|
|
125
|
+
|
|
126
|
+
#### Component relations
|
|
127
|
+
|
|
128
|
+
```mermaid
|
|
129
|
+
flowchart TD
|
|
130
|
+
subgraph Frontends["Frontends"]
|
|
131
|
+
direction LR
|
|
132
|
+
MacOSApp["MacOS App"]
|
|
133
|
+
LinuxApp["Linux App"]
|
|
134
|
+
TUI["Textual TUI"]
|
|
135
|
+
CLI["CLI"]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Client["SusOpsClient — client.py\nensure_daemon_running\nRPC proxy + retry"]
|
|
139
|
+
|
|
140
|
+
MacOSApp --> Client
|
|
141
|
+
LinuxApp --> Client
|
|
142
|
+
TUI --> Client
|
|
143
|
+
CLI --> Client
|
|
144
|
+
|
|
145
|
+
Client -->|POST /rpc\nJSON-over-HTTP| RPC
|
|
146
|
+
Client -.->|GET /events\nServer-Sent Events stream — events pushed back| SSE
|
|
147
|
+
|
|
148
|
+
subgraph Daemon["susops-services daemon"]
|
|
149
|
+
RPC["rpc_server.py\n/rpc (POST)\nAllowlist-gated dispatch"]
|
|
150
|
+
Facade["facade.py — SusOpsManager\nconfig I/O · PID mgmt · bandwidth sampling\nis_idle() drives daemon shutdown"]
|
|
151
|
+
SSE["status.py — StatusServer\n/events (SSE)\nstate · share · forward · bandwidth"]
|
|
152
|
+
Reconnect["_ReconnectMonitor\nwatches ControlMaster sockets\nre-registers forwards on reconnect"]
|
|
153
|
+
BW["_BandwidthSampler\nreads /proc/<pid>/io every 2s"]
|
|
154
|
+
|
|
155
|
+
SSH["ssh.py\nControlMaster + -O forward"]
|
|
156
|
+
Socat["socat.py\nUDP forwards via socat"]
|
|
157
|
+
PAC["pac.py\nPAC server (aiohttp)"]
|
|
158
|
+
Share["share.py\nShareServer(s) (aiohttp)"]
|
|
159
|
+
|
|
160
|
+
Loop["Shared async event loop\n(daemon thread — pac · share · status)"]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
RPC --> Facade
|
|
164
|
+
Facade --> SSE
|
|
165
|
+
Facade --> Reconnect
|
|
166
|
+
Facade --> BW
|
|
167
|
+
Facade --> SSH
|
|
168
|
+
Facade --> Socat
|
|
169
|
+
Facade --> PAC
|
|
170
|
+
Facade --> Share
|
|
171
|
+
|
|
172
|
+
Share --> Loop
|
|
173
|
+
PAC --> Loop
|
|
174
|
+
Loop --> SSE
|
|
175
|
+
|
|
176
|
+
subgraph Processes["Managed processes"]
|
|
177
|
+
Master["susops-ssh-<tag>\nSSH ControlMaster\n(-M -N -D socks_port)"]
|
|
178
|
+
PacProc["susops-pac\nPAC HTTP server"]
|
|
179
|
+
UDPLocal["susops-udp-<tag>-<fw>-lsocat\nLocal UDP socat\n(EXEC → SSH → remote socat)"]
|
|
180
|
+
UDPRemote["susops-udp-<tag>-<fw>-ssh/rsocat/lsocat\nRemote UDP bridge\n(SSH -R + remote/local socat)"]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
SSH ---->|spawns| Master
|
|
184
|
+
PAC -->|spawns| PacProc
|
|
185
|
+
Socat -->|local direction| UDPLocal
|
|
186
|
+
Socat -->|remote direction| UDPRemote
|
|
187
|
+
Master -.->|ControlMaster socket| UDPLocal
|
|
188
|
+
Master -.->|ControlMaster socket| UDPRemote
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Requirements
|
|
194
|
+
|
|
195
|
+
| Component | Requirement |
|
|
196
|
+
|-------------------|-----------------------------------------------------------------------|
|
|
197
|
+
| Python | ≥ 3.11 |
|
|
198
|
+
| SSH tunnels | `ssh` (OpenSSH, for ControlMaster support) |
|
|
199
|
+
| PAC server | `aiohttp >= 3.9` (shared async loop) |
|
|
200
|
+
| TUI | `textual >= 8.2`, `textual-plotext >= 1.0` (optional extra) |
|
|
201
|
+
| File sharing | `cryptography >= 42`, `aiohttp >= 3.9` (optional extra) |
|
|
202
|
+
| UDP forwarding | `socat` (system package, see [UDP Port Forwarding](#udp-port-forwarding)) |
|
|
203
|
+
| Linux tray | `python-gobject`, `gtk3`, `libayatana-appindicator` (system packages) |
|
|
204
|
+
| macOS tray | `rumps >= 0.4` |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Installation
|
|
209
|
+
|
|
210
|
+
### pip
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# CLI only (no TUI, no tray)
|
|
214
|
+
pip install susops
|
|
215
|
+
|
|
216
|
+
# TUI
|
|
217
|
+
pip install "susops[tui]"
|
|
218
|
+
|
|
219
|
+
# TUI + encrypted file sharing
|
|
220
|
+
pip install "susops[tui,share]"
|
|
221
|
+
|
|
222
|
+
# Linux tray (system GTK3 packages must be installed separately)
|
|
223
|
+
pip install "susops[tui,share,tray-linux]"
|
|
224
|
+
|
|
225
|
+
# macOS tray
|
|
226
|
+
pip install "susops[tui,share,tray-mac]"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
UDP port forwarding has no Python dependencies, install `socat` via your system package manager:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# macOS
|
|
233
|
+
brew install socat
|
|
234
|
+
|
|
235
|
+
# Arch Linux
|
|
236
|
+
sudo pacman -S socat
|
|
237
|
+
|
|
238
|
+
# Ubuntu / Debian
|
|
239
|
+
sudo apt install socat
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Arch Linux (AUR)
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
yay -S susops
|
|
246
|
+
# Optional TUI: yay -S python-textual
|
|
247
|
+
# Optional file sharing: yay -S python-cryptography
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### macOS (Homebrew)
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
brew install mashb1t/susops/susops
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### From source
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
git clone https://github.com/mashb1t/susops
|
|
260
|
+
cd susops
|
|
261
|
+
pip install -e ".[tui,share,dev]"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Quick Start
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Add your first SSH connection
|
|
270
|
+
susops add-connection work user@bastion.example.com
|
|
271
|
+
|
|
272
|
+
# Add hosts that should route through the proxy
|
|
273
|
+
susops add *.internal.example.com
|
|
274
|
+
susops add 10.0.0.0/8
|
|
275
|
+
|
|
276
|
+
# Start tunnels + PAC server
|
|
277
|
+
susops start
|
|
278
|
+
|
|
279
|
+
# Check status
|
|
280
|
+
susops ps
|
|
281
|
+
|
|
282
|
+
# Point your browser at the PAC URL
|
|
283
|
+
susops ps # shows PAC port, e.g. http://localhost:51234/susops.pac
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Services Daemon
|
|
289
|
+
|
|
290
|
+
SusOps' background services (PAC HTTP server, status SSE endpoint, reconnect monitor, bandwidth sampler) run in a single long-lived process: `susops-services`. The CLI, TUI, and tray are thin clients that talk to it over **JSON-over-HTTP RPC** (`POST /rpc`) and consume live state updates from a **Server-Sent-Events stream** (`GET /events`). If the daemon isn't running when you invoke a frontend, it's auto-spawned; the PID + RPC port are recorded in `~/.susops/pids/susops-services.{pid,port}`.
|
|
291
|
+
|
|
292
|
+
By default the daemon **exits when idle**, last SSE client disconnected AND no SSH tunnels / shares / PAC running / connections being watched. Respawning takes well under a second, so frontends don't notice. If you want it always running anyway (faster cold-start, immediate reconnect of dropped connections without any frontend open), install one of the supervisor units below.
|
|
293
|
+
|
|
294
|
+
### macOS (launchd)
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
cp packaging/macos/org.susops.services.plist ~/Library/LaunchAgents/
|
|
298
|
+
launchctl load ~/Library/LaunchAgents/org.susops.services.plist
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Adjust the `ProgramArguments` path in the plist if `susops-services` isn't at `/usr/local/bin/susops-services` (run `which susops-services` to find yours).
|
|
302
|
+
|
|
303
|
+
### Linux (systemd-user)
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
mkdir -p ~/.config/systemd/user
|
|
307
|
+
cp packaging/linux/susops-services.service ~/.config/systemd/user/
|
|
308
|
+
systemctl --user daemon-reload
|
|
309
|
+
systemctl --user enable --now susops-services
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The unit uses `%h/.local/bin/susops-services`, adjust if your install path differs.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## TUI
|
|
317
|
+
|
|
318
|
+
Run `susops` (or `so`) with no arguments in a terminal to launch the interactive TUI:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
susops
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Screens
|
|
325
|
+
|
|
326
|
+
**Dashboard** (default) — split-pane view. Left sidebar shows all connections (status dot, SOCKS port, live throughput), PAC server status, and active file shares. Right panel is tabbed:
|
|
327
|
+
- **Stats** — CPU%, memory, active connections, PID for the selected connection
|
|
328
|
+
- **Bandwidth** — live RX and TX line charts (PlotextPlot, 60-sample rolling window, auto-scaled units)
|
|
329
|
+
- **Forwards** — DataTable of all port forwards (direction, local port, local bind, remote port, remote bind, label)
|
|
330
|
+
- **Logs** — RichLog of all tunnel output, auto-refreshed every 3 seconds
|
|
331
|
+
|
|
332
|
+
**Connection editor** — tabbed CRUD editor for Connections, PAC Hosts, Local Forwards, and Remote Forwards. Press `a` to add, `d` to delete, `t` to toggle enabled/disabled, `e` to run a connectivity test for the selected item, `s`/`x`/`r` to start/stop/restart. All add dialogs are modal overlays (dimmed background). A detail preview panel at the bottom shows expanded info for the selected row.
|
|
333
|
+
|
|
334
|
+
**Share screen** — split-pane: left list of shares with three-state indicators (green = running, dim = manually stopped, red = offline/connection down), right panel with file details, URL, password, access counts, and fetch commands. Press `a` to share a new file, `f` to fetch a remote share, `d` to stop a share, `s` to restart a stopped share, `x` to delete. Refreshes every 2 seconds via `set_interval` to reflect connection state changes.
|
|
335
|
+
|
|
336
|
+
**Config editor** — read-only YAML view of `~/.susops/config.yaml`. Press `e` to open in `$EDITOR`.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## CLI Reference
|
|
341
|
+
|
|
342
|
+
When a subcommand is given (or stdout is not a TTY), `susops` runs in non-interactive mode:
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
susops [-c TAG] COMMAND [args]
|
|
346
|
+
|
|
347
|
+
-c, --connection TAG Target a specific connection by tag
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Commands
|
|
351
|
+
|
|
352
|
+
#### Connection management
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
susops add-connection <tag> <user@host> [socks_port]
|
|
356
|
+
# Add a new SSH connection. Port 0 = auto-assign on start.
|
|
357
|
+
|
|
358
|
+
susops rm-connection <tag>
|
|
359
|
+
# Remove a connection (stops it first if running).
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Lifecycle
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
susops start [-c TAG] # Start tunnel(s) + PAC server (omit -c for all)
|
|
366
|
+
susops stop [-c TAG] [--keep-ports] [--force]
|
|
367
|
+
susops restart [-c TAG]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`-c TAG` targets a single connection; omit to operate on all connections.
|
|
371
|
+
When stopping a single connection its associated file shares are also stopped.
|
|
372
|
+
`--keep-ports` preserves assigned port numbers across restarts.
|
|
373
|
+
`--force` sends SIGKILL instead of SIGTERM.
|
|
374
|
+
|
|
375
|
+
#### Status
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
susops ps # Show running state; exit 0=all running, 2=partial, 3=stopped
|
|
379
|
+
susops ls # List full config (connections, PAC hosts, forwards)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### PAC hosts and port forwards
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# PAC host (routes matching traffic through SOCKS proxy)
|
|
386
|
+
susops add <host> # e.g. *.example.com, 10.0.0.0/8, host.example.com
|
|
387
|
+
susops rm <host>
|
|
388
|
+
|
|
389
|
+
# Local port forward (-L equivalent)
|
|
390
|
+
susops add -l <local_port> <remote_port> [label] [local_bind] [remote_bind]
|
|
391
|
+
susops rm -l <local_port>
|
|
392
|
+
|
|
393
|
+
# Remote port forward (-R equivalent)
|
|
394
|
+
susops add -r <remote_port> <local_port> [label] [remote_bind] [local_bind]
|
|
395
|
+
susops rm -r <remote_port>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Bind addresses default to `localhost`. Use `0.0.0.0` to listen on all interfaces or `172.17.0.1` for Docker bridge access.
|
|
399
|
+
|
|
400
|
+
#### Testing
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
susops test <hostname> # Test one host through SOCKS proxy
|
|
404
|
+
susops test --all # Test all PAC hosts; exit 0 if all pass
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### File sharing
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
# Share a file (AES-256-CTR encrypted over HTTP)
|
|
411
|
+
susops share <file> [password] [port]
|
|
412
|
+
# Prints URL + password + fetch command
|
|
413
|
+
|
|
414
|
+
# Fetch a shared file through an SSH tunnel
|
|
415
|
+
susops fetch <port> <password> [outfile]
|
|
416
|
+
# Auto-starts the connection if not running; stops it again after download
|
|
417
|
+
# Saves to ~/Downloads/<original_filename> if outfile omitted
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### Browser launch
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
susops chrome # Launch Chrome/Chromium with --proxy-pac-url
|
|
424
|
+
susops firefox # Launch Firefox with a temporary PAC profile
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### Proxy setup guide
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
susops [-c TAG] guide # Print copy-paste proxy config for common tools
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Shows `socks5h://127.0.0.1:<port>` snippets for shell, Homebrew, pip, npm/yarn/pnpm, git, curl, wget, apt, Docker, and proxychains4. Uses the live port if the tunnel is running, falls back to the saved config port, or shows a `<port>` placeholder with a warning if the port is unknown.
|
|
434
|
+
|
|
435
|
+
#### Reset
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
susops reset [--force] # Kill all processes, wipe ~/.susops workspace
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Exit codes
|
|
442
|
+
|
|
443
|
+
| Code | Meaning |
|
|
444
|
+
|------|---------|
|
|
445
|
+
| `0` | Success / all running |
|
|
446
|
+
| `1` | Error |
|
|
447
|
+
| `2` | Partial (some services stopped) |
|
|
448
|
+
| `3` | All stopped |
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## System Tray
|
|
453
|
+
|
|
454
|
+
### Linux
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
susops-tray
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Requires (system packages):
|
|
461
|
+
- Arch: `sudo pacman -S python-gobject gtk3 libayatana-appindicator`
|
|
462
|
+
- Ubuntu/Debian: `sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ayatana-appindicator3-0.1`
|
|
463
|
+
|
|
464
|
+
### macOS
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
susops-tray
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Requires `rumps`: `pip install "susops[tray-mac]"`
|
|
471
|
+
|
|
472
|
+
The tray icon reflects the current state (running/partial/stopped). State changes arrive over the daemon's SSE `/events` stream. If the stream drops, the listener reconnects within at most 5 seconds. Both tray implementations support the same feature set via native dialogs:
|
|
473
|
+
|
|
474
|
+
- **Manage** — toggle connection/PAC host/forward enabled state; start, stop, or restart a specific connection
|
|
475
|
+
- **Start / Stop / Restart All** — bulk lifecycle operations across all connections
|
|
476
|
+
- **Test** — test SSH reachability for a specific connection, curl a domain through its SOCKS proxy, or check port liveness for a specific forward; "Test All PAC Hosts" bulk-tests every configured domain
|
|
477
|
+
- **CRUD** — add/remove connections, PAC hosts, and port forwards (with bind address selection)
|
|
478
|
+
- **Settings** — configure PAC port, stop-on-quit, and ephemeral ports
|
|
479
|
+
- **Browser launch** — open Chrome or Firefox with the PAC URL pre-configured
|
|
480
|
+
- **Quit**
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Configuration
|
|
485
|
+
|
|
486
|
+
Config is stored at `~/.susops/config.yaml`. It is created automatically on first use.
|
|
487
|
+
|
|
488
|
+
```yaml
|
|
489
|
+
pac_server_port: 51234
|
|
490
|
+
connections:
|
|
491
|
+
- tag: work
|
|
492
|
+
ssh_host: user@bastion.example.com
|
|
493
|
+
socks_proxy_port: 51235
|
|
494
|
+
enabled: true # false = skip this connection on start-all
|
|
495
|
+
pac_hosts:
|
|
496
|
+
- "*.internal.example.com"
|
|
497
|
+
- "10.0.0.0/8"
|
|
498
|
+
pac_hosts_disabled: # hosts temporarily disabled without removal
|
|
499
|
+
- "*.staging.example.com"
|
|
500
|
+
forwards:
|
|
501
|
+
local:
|
|
502
|
+
- src_port: 5432
|
|
503
|
+
src_addr: localhost
|
|
504
|
+
dst_port: 5432
|
|
505
|
+
dst_addr: db.internal.example.com
|
|
506
|
+
tag: postgres
|
|
507
|
+
tcp: true
|
|
508
|
+
udp: false
|
|
509
|
+
enabled: true # false = forward skipped on connection start
|
|
510
|
+
- src_port: 5353
|
|
511
|
+
src_addr: localhost
|
|
512
|
+
dst_port: 53
|
|
513
|
+
dst_addr: dns.internal.example.com
|
|
514
|
+
tag: dns
|
|
515
|
+
tcp: false
|
|
516
|
+
udp: true
|
|
517
|
+
enabled: true
|
|
518
|
+
- src_port: 8080
|
|
519
|
+
src_addr: localhost
|
|
520
|
+
dst_port: 80
|
|
521
|
+
dst_addr: web.internal.example.com
|
|
522
|
+
tag: webui
|
|
523
|
+
tcp: true
|
|
524
|
+
udp: false
|
|
525
|
+
enabled: false
|
|
526
|
+
remote: []
|
|
527
|
+
susops_app:
|
|
528
|
+
stop_on_quit: true
|
|
529
|
+
ephemeral_ports: false
|
|
530
|
+
logo_style: COLORED_GLASSES
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Port forward bind addresses
|
|
534
|
+
|
|
535
|
+
| Field | Description |
|
|
536
|
+
|------------|----------------------------------------------------------------------------|
|
|
537
|
+
| `src_addr` | Local bind address for local forwards; remote bind for remote forwards |
|
|
538
|
+
| `dst_addr` | Remote destination host for local forwards; local bind for remote forwards |
|
|
539
|
+
|
|
540
|
+
Common values: `localhost` (default, loopback only), `0.0.0.0` (all interfaces), `172.17.0.1` (Docker bridge).
|
|
541
|
+
|
|
542
|
+
### Port forward protocol flags
|
|
543
|
+
|
|
544
|
+
Each forward has two boolean flags that control which transport protocols are enabled:
|
|
545
|
+
|
|
546
|
+
| Field | Default | Description |
|
|
547
|
+
|-------|---------|-----------------------------------------------------|
|
|
548
|
+
| `tcp` | `true` | Enable TCP forwarding via SSH `-L`/`-R` slave |
|
|
549
|
+
| `udp` | `false` | Enable UDP forwarding via socat (requires `socat`) |
|
|
550
|
+
|
|
551
|
+
Both can be `true` simultaneously — susops will start an SSH slave for TCP and socat process(es) for UDP independently. Requires at least one of `tcp` or `udp` to be `true`.
|
|
552
|
+
|
|
553
|
+
### PAC host syntax
|
|
554
|
+
|
|
555
|
+
| Pattern | Matches |
|
|
556
|
+
|--------------------|------------------------------|
|
|
557
|
+
| `*.example.com` | any subdomain of example.com |
|
|
558
|
+
| `10.0.0.0/8` | any IP in 10.0.0.0/8 CIDR |
|
|
559
|
+
| `host.example.com` | that exact hostname |
|
|
560
|
+
|
|
561
|
+
### Port assignment
|
|
562
|
+
|
|
563
|
+
Ports default to `0` (auto-assign). SusOps picks a random free port from the ephemeral range (49152–65535) at start time and saves it back to `config.yaml`. Pass a specific port to `add-connection` or set it in the config to pin it.
|
|
564
|
+
|
|
565
|
+
### Workspace
|
|
566
|
+
|
|
567
|
+
All runtime data lives in `~/.susops/`:
|
|
568
|
+
|
|
569
|
+
```
|
|
570
|
+
~/.susops/
|
|
571
|
+
config.yaml # persistent config
|
|
572
|
+
susops.pac # generated PAC file (regenerated on start/change)
|
|
573
|
+
pids/ # PID files + socket files for each managed process
|
|
574
|
+
susops-ssh-<tag>.pid # ControlMaster PID
|
|
575
|
+
susops-fwd-<tag>-<fw>.pid # forward slave PIDs
|
|
576
|
+
susops-pac.pid
|
|
577
|
+
susops-<tag>.sock # SSH ControlMaster Unix socket
|
|
578
|
+
logs/ # per-process log files
|
|
579
|
+
susops-ssh-<tag>.log
|
|
580
|
+
firefox_profile/ # temporary Firefox profile for PAC launch
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### SSH Server: Forward-Only Access
|
|
584
|
+
|
|
585
|
+
To restrict the SSH user on the server to only perform port forwarding (no shell, no file transfer):
|
|
586
|
+
|
|
587
|
+
**1. Create a dedicated system user:**
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
sudo useradd -r -m -s /usr/sbin/nologin susops-tunnel
|
|
591
|
+
sudo mkdir -p /home/susops-tunnel/.ssh
|
|
592
|
+
sudo chmod 700 /home/susops-tunnel/.ssh
|
|
593
|
+
sudo chown -R susops-tunnel:susops-tunnel /home/susops-tunnel/.ssh
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**2. Add your public key to `authorized_keys` with restrictions:**
|
|
597
|
+
|
|
598
|
+
```
|
|
599
|
+
# /home/susops-tunnel/.ssh/authorized_keys
|
|
600
|
+
restrict,port-forwarding,permitopen="any" ssh-ed25519 AAAA... your-key
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The `restrict` keyword disables shell, PTY, agent forwarding, and X11 forwarding. `port-forwarding` re-enables only TCP forwarding. `permitopen="any"` allows forwarding to any destination — restrict with `permitopen="host:port"` for tighter control.
|
|
604
|
+
|
|
605
|
+
**3. Configure `sshd_config`:**
|
|
606
|
+
|
|
607
|
+
```
|
|
608
|
+
# /etc/ssh/sshd_config.d/susops.conf
|
|
609
|
+
Match User susops-tunnel
|
|
610
|
+
AllowTcpForwarding yes
|
|
611
|
+
AllowStreamLocalForwarding no
|
|
612
|
+
GatewayPorts no
|
|
613
|
+
X11Forwarding no
|
|
614
|
+
PermitTTY no
|
|
615
|
+
ForceCommand /bin/false
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
**4. Reload sshd:**
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
sudo systemctl reload sshd
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
The connection stays alive for SOCKS proxying and port forwards but cannot execute commands or transfer files.
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## File Sharing
|
|
629
|
+
|
|
630
|
+
SusOps can share files over an encrypted HTTP server, useful for transferring files through an SSH tunnel. Multiple files can be shared simultaneously on different ports.
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
# On sender
|
|
634
|
+
susops share /path/to/secret.tar.gz
|
|
635
|
+
# Output:
|
|
636
|
+
# Sharing: /path/to/secret.tar.gz
|
|
637
|
+
# URL: http://localhost:52100
|
|
638
|
+
# Password: Xk7mN2qR...
|
|
639
|
+
# Port: 52100
|
|
640
|
+
# Fetch with: susops fetch 52100 Xk7mN2qR...
|
|
641
|
+
|
|
642
|
+
# On receiver (through the SOCKS tunnel or on the same LAN)
|
|
643
|
+
susops fetch 52100 Xk7mN2qR...
|
|
644
|
+
# Downloaded to: ~/Downloads/secret.tar.gz
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Protocol:** HTTP Basic auth (`:password`) + gzip compression + AES-256-CTR encryption (PBKDF2-HMAC-SHA256 key derivation, 600,000 iterations). The original filename is also encrypted and stored in `Content-Disposition`.
|
|
648
|
+
|
|
649
|
+
**Fetch behaviour:** `fetch` auto-starts the SSH ControlMaster if the connection is not running, using a minimal start (no configured forwards, PAC server, or file shares are touched). The connection is stopped again after the download completes. If the connection was already running it is left untouched.
|
|
650
|
+
|
|
651
|
+
**Share status** in the TUI uses three states:
|
|
652
|
+
- `●` green — server running and accessible
|
|
653
|
+
- `○` dim — manually stopped (will not auto-restart)
|
|
654
|
+
- `○` red — offline because its connection went down (auto-resumes when connection restarts)
|
|
655
|
+
|
|
656
|
+
Each share tracks successful and failed access counts in memory (shown in the detail panel; not persisted across restarts).
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## UDP Port Forwarding
|
|
661
|
+
|
|
662
|
+
SusOps can forward UDP traffic through an SSH tunnel using `socat`. This works without any additional ports on the SSH server beyond the existing ControlMaster connection.
|
|
663
|
+
|
|
664
|
+
### Requirements
|
|
665
|
+
|
|
666
|
+
`socat` must be installed on **both the local machine and the remote SSH host** for either direction:
|
|
667
|
+
|
|
668
|
+
- **Local UDP**: local socat listens and forks; remote socat relays each datagram to the destination (via SSH EXEC).
|
|
669
|
+
- **Remote UDP**: remote socat relays UDP → TCP (via SSH exec); local socat relays TCP → UDP.
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
brew install socat # macOS
|
|
673
|
+
sudo pacman -S socat # Arch Linux
|
|
674
|
+
sudo apt install socat # Ubuntu / Debian
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Enabling UDP on a forward
|
|
678
|
+
|
|
679
|
+
Set `udp: true` in the forward config, or check "UDP" in the TUI/tray add-forward dialog:
|
|
680
|
+
|
|
681
|
+
```yaml
|
|
682
|
+
forwards:
|
|
683
|
+
local:
|
|
684
|
+
- src_port: 5353
|
|
685
|
+
dst_port: 53
|
|
686
|
+
dst_addr: dns.internal.example.com
|
|
687
|
+
tag: dns
|
|
688
|
+
tcp: false
|
|
689
|
+
udp: true
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Architecture: local UDP forward
|
|
693
|
+
|
|
694
|
+
A single `socat` process listens locally on the UDP port. For each incoming datagram it forks a child that opens an SSH channel through the existing ControlMaster socket and runs `socat` on the remote host to relay the packet to the destination service. Responses travel back through the same channel.
|
|
695
|
+
|
|
696
|
+
```mermaid
|
|
697
|
+
flowchart LR
|
|
698
|
+
subgraph local["Local machine"]
|
|
699
|
+
Client["UDP client\ne.g. dig, game, VoIP"]
|
|
700
|
+
LSOcat["lsocat\nUDP4-RECVFROM:src_port,fork\nEXEC:'ssh -o ControlPath=sock...'"]
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
subgraph tunnel["SSH ControlMaster (one channel per datagram)"]
|
|
704
|
+
Channel["SSH channel"]
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
subgraph remote["Remote SSH host"]
|
|
708
|
+
RSOcat["remote socat\nsocat - UDP4-SENDTO:\ndst_addr:dst_port"]
|
|
709
|
+
Service["UDP service"]
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
Client -->|"UDP datagram"| LSOcat
|
|
713
|
+
LSOcat -->|"fork child"| Channel
|
|
714
|
+
Channel -->|"stdin/stdout"| RSOcat
|
|
715
|
+
RSOcat -->|"UDP"| Service
|
|
716
|
+
Service -. "response" .-> RSOcat
|
|
717
|
+
RSOcat -. "stdout" .-> Channel
|
|
718
|
+
Channel -. "response" .-> LSOcat
|
|
719
|
+
LSOcat -. "UDP response" .-> Client
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Process name: `susops-udp-<conn>-<fw>-lsocat`
|
|
723
|
+
|
|
724
|
+
**Latency note:** each datagram opens one SSH channel (~25–40 ms overhead on a typical WAN link). This is suitable for request/response protocols (DNS, database ping, STUN) but not for high-frequency streaming (real-time audio/video).
|
|
725
|
+
|
|
726
|
+
### Architecture: remote UDP forward
|
|
727
|
+
|
|
728
|
+
Three processes bridge the remote UDP listener to a local UDP service via an intermediate TCP port:
|
|
729
|
+
|
|
730
|
+
```mermaid
|
|
731
|
+
flowchart LR
|
|
732
|
+
subgraph remote["Remote SSH host"]
|
|
733
|
+
RClient["UDP client\non remote"]
|
|
734
|
+
RSOcat["rsocat\nUDP4-RECVFROM:src_port,fork\nTCP4:localhost:intermediate"]
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
subgraph local["Local machine"]
|
|
738
|
+
SSHSlave["SSH -R slave\n-R intermediate:localhost:intermediate"]
|
|
739
|
+
LSOcat["lsocat\nTCP4-LISTEN:intermediate,fork\nUDP4-SENDTO:dst_addr:dst_port"]
|
|
740
|
+
Service["UDP service\ndst_addr:dst_port"]
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
RClient -->|"UDP datagram"| RSOcat
|
|
744
|
+
RSOcat -->|"TCP to remote:intermediate\n(bound by SSH -R)"| SSHSlave
|
|
745
|
+
SSHSlave -->|"TCP to localhost:intermediate"| LSOcat
|
|
746
|
+
LSOcat -->|"UDP"| Service
|
|
747
|
+
Service -. "response" .-> LSOcat
|
|
748
|
+
LSOcat -. "TCP" .-> SSHSlave
|
|
749
|
+
SSHSlave -. "TCP" .-> RSOcat
|
|
750
|
+
RSOcat -. "UDP response" .-> RClient
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
Three managed processes per remote UDP forward:
|
|
754
|
+
|
|
755
|
+
| Name | Runs on | Role |
|
|
756
|
+
|------|---------|------|
|
|
757
|
+
| `susops-udp-<conn>-<fw>-ssh` | local | SSH `-R` slave — requests remote SSH server to bind `intermediate` port and tunnel it back locally |
|
|
758
|
+
| `susops-udp-<conn>-<fw>-rsocat` | remote | socat UDP → TCP (via SSH exec); connects to the `intermediate` port bound by the SSH -R slave |
|
|
759
|
+
| `susops-udp-<conn>-<fw>-lsocat` | local | socat TCP → UDP; listens on `intermediate`, forwards UDP to the destination service |
|
|
760
|
+
|
|
761
|
+
### Use cases
|
|
762
|
+
|
|
763
|
+
| Protocol | Direction | Example |
|
|
764
|
+
|----------|-----------|--------------------------------------------------------|
|
|
765
|
+
| DNS | local | Route `dig` queries to an internal resolver (bypasses split-DNS) |
|
|
766
|
+
| DNS | local | Use an external resolver (8.8.8.8) bypassing corporate DNS monitoring |
|
|
767
|
+
| SNMP | local | Query internal SNMP agents (UDP 161) through the tunnel |
|
|
768
|
+
| Syslog | remote | Receive syslog UDP 514 from remote host to a local collector |
|
|
769
|
+
| VoIP/SIP | local | Reach an internal SIP registrar over UDP 5060 |
|
|
770
|
+
| NTP | local | Sync to an internal NTP server (UDP 123) |
|
|
771
|
+
| Gaming | local | Tunnel UDP game traffic to an internal game server |
|
|
772
|
+
|
|
773
|
+
### SSH server restrictions and UDP
|
|
774
|
+
|
|
775
|
+
The EXEC-based local UDP approach runs a command on the remote SSH host for each datagram. This requires the SSH user to have command execution permission. A `ForceCommand /bin/false` or `restrict` without `command="..."` override will block UDP forwarding. If you need both security restrictions and UDP, use the remote UDP direction (which only requires TCP port forwarding on the server side) or relax `ForceCommand` for the specific socat command.
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
## Python API
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
from susops.facade import SusOpsManager
|
|
783
|
+
from susops.core.config import PortForward
|
|
784
|
+
|
|
785
|
+
mgr = SusOpsManager()
|
|
786
|
+
|
|
787
|
+
# Add a connection
|
|
788
|
+
mgr.add_connection("work", "user@bastion.example.com", socks_port=0)
|
|
789
|
+
|
|
790
|
+
# Add PAC hosts
|
|
791
|
+
mgr.add_pac_host("*.internal.example.com", conn_tag="work")
|
|
792
|
+
mgr.add_pac_host("10.0.0.0/8", conn_tag="work")
|
|
793
|
+
|
|
794
|
+
# Add a local port forward (with optional bind addresses)
|
|
795
|
+
mgr.add_local_forward("work", PortForward(
|
|
796
|
+
src_port=5432, src_addr="localhost",
|
|
797
|
+
dst_port=5432, dst_addr="db.internal.example.com",
|
|
798
|
+
tag="postgres",
|
|
799
|
+
))
|
|
800
|
+
|
|
801
|
+
# Add a remote port forward
|
|
802
|
+
mgr.add_remote_forward("work", PortForward(
|
|
803
|
+
src_port=8080, src_addr="localhost",
|
|
804
|
+
dst_port=8080, dst_addr="localhost",
|
|
805
|
+
tag="webserver",
|
|
806
|
+
))
|
|
807
|
+
|
|
808
|
+
# Start
|
|
809
|
+
result = mgr.start()
|
|
810
|
+
print(result.message) # "Started"
|
|
811
|
+
|
|
812
|
+
# Status
|
|
813
|
+
status = mgr.status()
|
|
814
|
+
print(status.state) # ProcessState.RUNNING
|
|
815
|
+
|
|
816
|
+
# React to state changes
|
|
817
|
+
mgr.on_state_change = lambda state: print(f"State changed: {state}")
|
|
818
|
+
mgr.on_log = lambda msg: print(f"[LOG] {msg}")
|
|
819
|
+
|
|
820
|
+
# Test connectivity
|
|
821
|
+
result = mgr.test("internal.example.com")
|
|
822
|
+
print(result.success, result.latency_ms)
|
|
823
|
+
|
|
824
|
+
# Test SSH reachability for a connection
|
|
825
|
+
ok, msg = mgr.test_connection("work")
|
|
826
|
+
print(ok, msg) # True, "SSH OK (42 ms)"
|
|
827
|
+
|
|
828
|
+
# Test a domain through a connection's SOCKS proxy
|
|
829
|
+
ok, msg = mgr.test_domain("internal.example.com", conn_tag="work")
|
|
830
|
+
print(ok, msg) # True, "HTTP 200 (91 ms)"
|
|
831
|
+
|
|
832
|
+
# Test liveness of a specific port forward
|
|
833
|
+
results = mgr.test_forward("work", src_port=5432, direction="local")
|
|
834
|
+
# {"tcp": (True, "port bound (PID 1234)"), "udp": (True, "socat running (PID 5678)")}
|
|
835
|
+
|
|
836
|
+
# Share a file (multiple concurrent shares supported)
|
|
837
|
+
from pathlib import Path
|
|
838
|
+
info = mgr.share(Path("/tmp/file.txt"))
|
|
839
|
+
print(info.url, info.password)
|
|
840
|
+
|
|
841
|
+
# Fetch a shared file
|
|
842
|
+
path = mgr.fetch(port=info.port, password=info.password, host="localhost")
|
|
843
|
+
print(path) # ~/Downloads/file.txt
|
|
844
|
+
|
|
845
|
+
# Stop
|
|
846
|
+
mgr.stop()
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Development
|
|
852
|
+
|
|
853
|
+
```bash
|
|
854
|
+
git clone https://github.com/mashb1t/susops
|
|
855
|
+
cd susops
|
|
856
|
+
python -m venv .venv && source .venv/bin/activate
|
|
857
|
+
pip install -e ".[tui,share,dev]"
|
|
858
|
+
|
|
859
|
+
# Run tests
|
|
860
|
+
pytest
|
|
861
|
+
|
|
862
|
+
# Run tests with coverage
|
|
863
|
+
pytest --cov=susops --cov-report=term-missing
|
|
864
|
+
|
|
865
|
+
# Run the TUI
|
|
866
|
+
susops
|
|
867
|
+
|
|
868
|
+
# Run the CLI
|
|
869
|
+
susops ps
|
|
870
|
+
susops ls
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### Project layout
|
|
874
|
+
|
|
875
|
+
```
|
|
876
|
+
susops/
|
|
877
|
+
pyproject.toml
|
|
878
|
+
src/susops/
|
|
879
|
+
core/
|
|
880
|
+
config.py # Pydantic v2 + ruamel.yaml
|
|
881
|
+
ssh.py # SSH ControlMaster/slave management + socket helpers
|
|
882
|
+
socat.py # UDP forwarding via socat EXEC + SSH ControlMaster
|
|
883
|
+
pac.py # PAC generation + aiohttp HTTP server
|
|
884
|
+
share.py # AES-256-CTR file sharing + shared async event loop
|
|
885
|
+
status.py # aiohttp SSE StatusServer (/events endpoint)
|
|
886
|
+
process.py # PID file-based process manager + zombie detection
|
|
887
|
+
ports.py # Port utilities (validate_port, is_port_free, CIDR)
|
|
888
|
+
ssh_config.py # ~/.ssh/config parser
|
|
889
|
+
types.py # Enums + result dataclasses (ShareInfo three-state)
|
|
890
|
+
facade.py # SusOpsManager public API
|
|
891
|
+
tui/
|
|
892
|
+
__main__.py # Dual-mode entrypoint
|
|
893
|
+
cli.py # argparse non-interactive CLI
|
|
894
|
+
app.py # Textual App (SusOpsTuiApp)
|
|
895
|
+
app.tcss # Global CSS theme
|
|
896
|
+
screens/
|
|
897
|
+
dashboard.py # Split-pane dashboard (sidebar + tabbed detail)
|
|
898
|
+
connections.py # CRUD editor with modal dialogs
|
|
899
|
+
share.py # File share + fetch screen
|
|
900
|
+
config_editor.py # Read-only YAML viewer
|
|
901
|
+
tray/
|
|
902
|
+
base.py # AbstractTrayApp
|
|
903
|
+
linux.py # GTK3 + AyatanaAppIndicator3
|
|
904
|
+
mac.py # rumps + PyObjC
|
|
905
|
+
tests/
|
|
906
|
+
test_config.py
|
|
907
|
+
test_process.py
|
|
908
|
+
test_pac.py
|
|
909
|
+
test_share.py
|
|
910
|
+
test_facade.py
|
|
911
|
+
packaging/
|
|
912
|
+
aur/PKGBUILD
|
|
913
|
+
homebrew/Formula/susops.rb
|
|
914
|
+
homebrew/Casks/susops.rb
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### Building Packages
|
|
918
|
+
|
|
919
|
+
#### PyPI / pip (all platforms)
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
pip install build
|
|
923
|
+
python -m build
|
|
924
|
+
# Produces dist/susops-<version>.tar.gz and dist/susops-<version>-py3-none-any.whl
|
|
925
|
+
pip install dist/susops-*.whl
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
#### Arch Linux (AUR)
|
|
929
|
+
|
|
930
|
+
The `packaging/aur/PKGBUILD` builds a wheel and installs it system-wide:
|
|
931
|
+
|
|
932
|
+
```bash
|
|
933
|
+
cd packaging/aur
|
|
934
|
+
makepkg -si
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Prerequisites:** `python-build`, `python-installer`, `python-wheel`, `python-setuptools`. The `ruamel.yaml` dependency is not in the official Arch repos — install `python-ruamel-yaml` from the AUR or use pip.
|
|
938
|
+
|
|
939
|
+
The PKGBUILD also installs `susops-tray.desktop` for the system tray launcher.
|
|
940
|
+
|
|
941
|
+
#### macOS (Homebrew)
|
|
942
|
+
|
|
943
|
+
```bash
|
|
944
|
+
brew tap mashb1t/susops
|
|
945
|
+
brew install susops
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
The formula at `packaging/homebrew/Formula/susops.rb` uses `virtualenv_install_with_resources` to create an isolated Python environment with all dependencies. The cask at `packaging/homebrew/Casks/susops.rb` is for a future `.dmg` distribution of `SusOps.app`.
|
|
949
|
+
|
|
950
|
+
**Note:** Resource sha256 checksums in the formula must be updated for each release. Generate them with:
|
|
951
|
+
|
|
952
|
+
```bash
|
|
953
|
+
shasum -a 256 <downloaded-tarball>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
#### Version bumping
|
|
957
|
+
|
|
958
|
+
Update the version in `src/susops/version.py`, then update `pkgver` in `packaging/aur/PKGBUILD` and the `url` version in `packaging/homebrew/Formula/susops.rb` to match.
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## Migration from susops.sh
|
|
963
|
+
|
|
964
|
+
If you used the previous Bash-based `susops.sh`:
|
|
965
|
+
|
|
966
|
+
1. Your `~/.susops/config.yaml` is **fully compatible** — no migration needed.
|
|
967
|
+
2. The `go-yq` / `yq` dependency is **removed** (replaced by Python + pydantic + ruamel.yaml).
|
|
968
|
+
3. Process detection no longer uses `pgrep -f` / `exec -a` hacks — PID files in `~/.susops/pids/` are used instead.
|
|
969
|
+
4. The PAC HTTP server is now a Python `http.server` daemon thread instead of a `nc` loop.
|
|
970
|
+
5. `autossh` is no longer used — replaced by plain `ssh` with ControlMaster mode for stable PID tracking and multiplexed forwards.
|
|
971
|
+
6. All CLI commands have the same names and behavior.
|
|
972
|
+
|
|
973
|
+
---
|
|
974
|
+
|
|
975
|
+
## License
|
|
976
|
+
|
|
977
|
+
[GNU General Public License v3.0](https://github.com/mashb1t/susops/blob/main/LICENSE)
|