---
title: Client PC Access — Reverse SSH Tunnel (how we reach the analyzer's Windows box)
slug: client-pc-tunnel
status: active
owner: hazem
updated: 2026-06-19
related:
  - middleware
---

## TL;DR
The lab analyzers and the middleware live on a **Windows PC at the client's site** (private LAN, no public IP). To reach that PC from our server I use a **reverse SSH tunnel**: the client's PC dials OUT to our server and publishes its own SSH (port 22) on **our server's `localhost:2222`**. So from our server, **`ssh -4 -p 2222 hp@127.0.0.1`** lands on the client PC. The tunnel **drops often**; the usual breakage is a **zombie listener still holding port 2222** on our server, which blocks the client from re-establishing. **Fix = free 2222 on OUR server (`kill -9` the stale sshd), then the client re-runs the tunnel from Windows.**

## The three connections (don't confuse them)
1. **Analyzer → middleware** (the lab's job): each analyzer connects over the client LAN (`192.168.0.x`) to the middleware's TCP listeners on the PC — Maglumi 15001, Maglumi-X3 15002, Udichem 15003, VITROS 15004, Dymind HL7 5600. This is the LIS data path. See [[middleware]].
2. **Middleware → cloud** (the product): the PC posts results to Moon ERP over **HTTPS** (`/lis/...`, `X-Authorization`). Cloud-canonical: config + code both originate from the cloud/repo (see middleware §🏛️). This path does NOT use the tunnel.
3. **Me → client PC** (admin/diagnostics only): the **reverse SSH tunnel** below. It is ONLY my access to inspect/deploy on the PC; it has nothing to do with how the lab runs day-to-day.

## How the tunnel is built
The **client PC initiates it** (outbound, so it works through their NAT/firewall). On Windows the PC runs roughly:
```
ssh -R 2222:localhost:22  <user>@151.80.18.217
```
- `-R 2222:localhost:22` = "publish MY port 22 on the SERVER's localhost:2222".
- Our server (`151.80.18.217`) `authorized_keys` for that key allows `port-forwarding`.
- Auth from the PC = key `C:\Users\HP\.ssh\id_rsa`.
- So afterwards, on **our server**, `localhost:2222` → the **client PC's sshd**.

I then hop in from our server:
```bash
ssh -4 -p 2222 -o BatchMode=yes -o StrictHostKeyChecking=no hp@127.0.0.1
```
- **`-4`** force IPv4 (the `[::1]:2222` path is flaky).
- **Run the Bash tool with `dangerouslyDisableSandbox: true`** — the sandbox blocks this outbound connect otherwise.
- User on the PC = **`hp`**.

## Why it drops + how I fix it (THE recurring issue)
**Symptom on my side:** `Connection timed out during banner exchange` / `Connection to 127.0.0.1 port 2222 timed out` even though `ss -tlnp | grep :2222` shows a listener. **Symptom on the client side (their ssh debug):** `Warning: remote port forwarding failed for listen port 2222`.

**Cause:** the client's `ssh -R` process died (network blip, PC sleep, screen lock…). When it dies the **server-side sshd that was holding 2222 often lingers as a zombie** — still BOUND to 2222 but with a dead forward. So: (a) my connects time out (nothing answers), and (b) the client's NEW tunnel can't bind 2222 ("port in use").

**Fix — free 2222 on OUR server (safe; it's our box, NOT the client machine):**
```bash
# find who holds 2222
ss -tlnp | grep ':2222'
# kill the stale sshd forwarder(s)
PIDS=$(ss -tlnp | grep ':2222' | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u)
for p in $PIDS; do ps -p "$p" -o comm= | grep -q sshd && kill -9 "$p"; done
# confirm free
ss -tlnp | grep ':2222' || echo "FREE — client can re-run the tunnel now"
```
Then tell the user: **re-run the tunnel command from Windows** (the failed attempt left no forward; closing+reopening their ssh is needed). Once they reconnect, `ss -tlnp | grep :2222` shows a fresh listener and my `ssh -p 2222` works again.

⚠️ **Boundary:** killing the sshd on **our server** is fine. **Killing/modifying anything on the CLIENT's live Windows machine needs the user's explicit go** (the harness classifier blocks unapproved writes/kills to it). Same for cloud-DB writes.

## Toolkit (commands that work over this tunnel)
- **Run a shell/PowerShell command on the PC:**
  ```bash
  ssh -4 -p 2222 -o BatchMode=yes hp@127.0.0.1 'powershell -NoProfile -Command "<cmd>"'
  ```
  For anything with quotes/special chars, avoid quoting hell with **`-EncodedCommand`** (base64 of the script as **UTF-16LE**):
  ```bash
  ENC=$(printf '%s' "$PS" | iconv -f UTF-8 -t UTF-16LE | base64 -w0)
  ssh ... hp@127.0.0.1 "powershell -NoProfile -EncodedCommand $ENC"
  ```
- **Copy a file to the PC:** `scp -P 2222 -o BatchMode=yes <local> hp@127.0.0.1:<dest>` (dest relative = `C:\Users\HP\`). **Use scp for anything >~1KB** — inline `python -c "exec(base64…)"` blows the Windows command-line length limit ("The command line is too long").
- **Run a small python snippet inline:** `python.exe -c "import base64;exec(base64.b64decode('<b64>').decode())"` (only for short scripts).
- **Always strip PS noise** from output: `... 2>&1 | grep -ivE "CLIXML|Objs|Preparing"`.

## Key paths / names on the PC
- Python: `C:\Users\HP\AppData\Local\Programs\Python\Python312\python.exe`
- Middleware: `C:\Users\HP\lis-middleware\` · log `lis-middleware.log` · admin UI `http://127.0.0.1:8765`
- Start it: user double-clicks **`run.bat`** (console). A python launched over SSH **detaches and dies** when the SSH session closes → can't start it persistently myself; the **user must run `run.bat`**.
- Our server (tunnel endpoint): `151.80.18.217`. SSH user on the PC: `hp`.

## Deploy loop over the tunnel (middleware code)
1. Edit source in the repo `moon-erp-be/lis-middleware/` → `py_compile` → commit (hazemdev) → CHANGELOG.
2. `scp -P 2222 lis_middleware/...` to `C:\Users\HP\lis-middleware\...`.
3. **User restarts `run.bat`** (python caches modules → restart loads new code; it also re-`sync_from_cloud()`s the device config). ⚠️ restart drops live analyzer connections → do it in a quiet window (e.g. lab closed).
4. Validate with the **device simulators** (inject a fake query/result on the PC's localhost listener) — see [[middleware]] §🧪.

## Health checks
- Tunnel up? On our server: `ss -tlnp | grep :2222` (listener present) + a test `ssh -4 -p 2222 ... hp@127.0.0.1 "echo OK"`.
- Middleware up + single instance? `ssh ... "Get-Process python,pythonw | Select Id,StartTime"` → exactly one. Ports each on ONE pid (`netstat -ano | findstr "15001 15003 15004 5600"`). (Two pids on a port = the duplicate-instance bug, see [[middleware]].)
