# RSSH routed egress (dev-only) — design review (DR)

<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R0 |
| Creation date | 2026-06-16 |
| Last modification date | 2026-06-16 |
| Co-authored | Cursor Agent (project assistant) |
| Severity | low |
| State | postponed |
| Document type | DR |
| Pre-requisite to | Finish WireGuard remote-access E2E ([REMOTE_ACCESS_VALIDATION.md](../REMOTE_ACCESS_VALIDATION.md), [VPN_DEMO_GAPS_20260613.md](../VPN_DEMO_GAPS_20260613.md)) |
<!-- doc-meta:end -->

\newpage
\newpage

---

**Document type:** DR (Design Review)  
**Source draft:** [docs/drafts/20260616_rssh_routed_egress.txt](../drafts/20260616_rssh_routed_egress.txt)  
**PDF:** [20260616_rssh_routed_egress.pdf](20260616_rssh_routed_egress.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh`  
**Status:** Draft for PO review — **no implementation** until WireGuard track is closed  
**Scope:** Developer settings only; optional future path; does **not** replace alpha RSSH (reverse tunnel, no VPN)

**Related:** [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md) · [20260602_REVERSE_SSH_proposals_summary.md](../20260602_REVERSE_SSH_proposals_summary.md) · [ROADMAP.md](../ROADMAP.md) (RSSH alpha)

**Documentation index:** [README.md](../README.md)

---

---

---

## Table of contents

<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Vocabulary and scope](#3-vocabulary-and-scope)
- [4. Current state](#4-current-state)
- [5. Goals and non-goals](#5-goals-and-non-goals)
- [6. Architecture options](#6-architecture-options)
- [7. DR recommendation](#7-dr-recommendation)
- [8. Reference design (if pursued later)](#8-reference-design-if-pursued-later)
- [9. Developer settings UX](#9-developer-settings-ux)
- [10. Backend and bastion requirements](#10-backend-and-bastion-requirements)
- [11. App components (sketch)](#11-app-components-sketch)
- [12. Third-party and licenses](#12-third-party-and-licenses)
- [13. Risks](#13-risks)
- [14. Effort estimate (order of magnitude)](#14-effort-estimate-order-of-magnitude)
- [15. Task dependency graph](#15-task-dependency-graph)
- [16. Open questions (PO)](#16-open-questions-po)
- [17. Changelog](#17-changelog)
<!-- /toc -->

---

## 1. Executive summary

**Question:** Can we add a developer-only, system-wide VPN-style path that routes part or all device traffic through the **RSSH** (reverse SSH) session?

**Short answer:** **Yes, technically** — but it requires a **new data plane** (`VpnService` + IP-to-SOCKS or TUN-over-SSH), not an extension of today’s RSSH `-R` forward. It **reintroduces VPN permission and the status-bar key**, and overlaps **WireGuard full-tunnel**, which already implements route/app scope in the app.

**DR decision (R0):**

| Item | Decision |
|------|----------|
| **Timing** | **Postponed** until WireGuard remote-access work is finished and validated E2E |
| **Alpha RSSH** | Unchanged — outbound reverse SSH for operator → device; **no** device routing |
| **Near-term routing** | Use **WireGuard** + `RemoteAccessVpnRouting` (hub-only or full-tunnel) |
| **If revived** | Dev-only sub-mode **“RSSH + route traffic (experimental)”** via **SSH dynamic forward (-D) + tun2socks + existing route/app prefs** |
| **SPEC** | Do **not** write implementation SPEC until PO confirms after WG closure |

---

## 2. Problem statement

Developers may want:

1. **Operator access** to the device (shell/SFTP) — **RSSH today**.
2. **Device traffic** to egress via the hub (e.g. Minsk NAT) or reach hub-private IPs — **WireGuard today** (split or full tunnel).
3. In some networks, **UDP WireGuard** may be blocked while **TCP 443 SSH** works — motivation to explore routing **over RSSH**.

Without a clear DR, it is easy to assume `vpn-route-scope` / `vpn-app-scope` apply to RSSH; they currently apply to **WireGuard only** ([REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md)).

---

## 3. Vocabulary and scope

| Term | Meaning here |
|------|----------------|
| **RSSH** | Device-initiated reverse SSH; `-R bastion:port → device:8022` for operator access |
| **Routed egress** | Device IP traffic (some or all) sent through bastion/hub NAT to the Internet or private nets |
| **VpnService** | Android API for app-provided VPN; required for system-wide or per-app routing **from our app** |
| **Route scope** | Hub-only (`172.200.2.1/32`) vs full-tunnel (`0.0.0.0/0`) — `RemoteAccessVpnRouting` |
| **App scope** | All apps vs this app only — `IncludedApplications` / `addAllowedApplication` |

**Not in scope for this DR:** Commercial VPN product (F4), always-on VPN in Android Settings, routing for non-developer builds.

---

## 4. Current state

```mermaid
flowchart LR
  subgraph phone [Device]
    RA[RemoteAccessService]
    WG[WireGuard :vpn]
    RSSH[ReverseSshTunnelBridge]
    LOCAL[RsshLocalSshServer :8022]
  end
  subgraph be [BE / bastion]
    WG0[wg0 UDP 45340]
    SSHD[sshd TCP 443]
  end
  RA -->|mode wireguard| WG
  WG -->|UDP| WG0
  RA -->|mode rssh| RSSH
  RSSH -->|TCP -R| SSHD
  SSHD -->|operator| LOCAL
```

| Mode | Transport | Device routing | VPN icon |
|------|-----------|----------------|----------|
| **WireGuard** | UDP → `wg0` | Yes (hub-only or full) | Yes |
| **RSSH** | TCP → sshd | **No** | **No** |

Existing prefs (`dev_remote_access_vpn_route_scope`, `dev_remote_access_vpn_app_scope`) are sent on heartbeat for WG; RSSH ignores them.

---

## 5. Goals and non-goals

### Goals (if implemented later)

| ID | Goal |
|----|------|
| G1 | Optional **dev-only** routing of device traffic via **TCP 443** SSH path |
| G2 | Reuse **route scope** and **app scope** semantics from `RemoteAccessVpnRouting` |
| G3 | Network self-test shows bastion, bind ports, egress expectation (like WG `vpn-ip`) |
| G4 | Clear separation from **alpha RSSH** (reverse access only) |

### Non-goals

| ID | Non-goal |
|----|----------|
| NG1 | Replace WireGuard for production/lab routing |
| NG2 | Alpha release blocker |
| NG3 | Avoid `VpnService.prepare()` — routed mode **will** need it |
| NG4 | SSH `PermitTunnel` / `ssh -w` as v1 (too heavy for Alpine bastion v1) |
| NG5 | Third-party “VPN provider” in Android Settings (no such API; we use `VpnService`) |

---

## 6. Architecture options

### 6.1 Option A — WireGuard full-tunnel (existing)

**Use when:** Hub egress or full WAN via Minsk NAT is needed **now**.

- Dev settings → WireGuard → route scope **All traffic** → reconnect session.
- BE `wg0` PostUp MASQUERADE already documented.

| Pros | Cons |
|------|------|
| Implemented | UDP DNAT + VPN consent |
| Kernel/userspace WG | Not RSSH transport |

**Verdict:** **Default for routing** until WG track is done.

### 6.2 Option B — RSSH + SSH `-D` (SOCKS) + `VpnService` + tun2socks

**Use when:** PO explicitly needs **TCP-only** routing and accepts VPN icon + extra stack.

```mermaid
flowchart TB
  subgraph device [Device]
    TUN[VpnService TUN]
    T2S[tun2socks userspace]
    SOCKS[127.0.0.1:SOCKS]
    JSch[JSch session]
  end
  subgraph bastion [Bastion / BE]
    SSD[sshd]
    NAT[NAT egress eth0]
  end
  TUN --> T2S --> SOCKS --> JSch --> SSD --> NAT
```

1. Extend `ReverseSshTunnelBridge`: keep `-R` for operator; add **`-D 127.0.0.1:<socks_port>`** on same session.
2. New **RsshRoutedVpnService** (or branch in `:vpn`) establishes TUN with routes from `RemoteAccessVpnRouting`.
3. **tun2socks** forwards IP packets → local SOCKS → SSH stream → bastion → Internet.

| Pros | Cons |
|------|------|
| TCP 443 friendly | VPN permission + icon |
| Reuses SSH session | tun2socks dep + licenses |
| Reuses route/app prefs | TCP-over-TCP, MTU, battery |
| | Bastion egress policy + capacity |
| | JSch dynamic forwarding QA |

**Verdict:** **Reference design if PO revives** routed RSSH.

### 6.3 Option C — SSH layer-3 `tun` (`ssh -w`)

Both ends need TUN, `PermitTunnel`, root/capabilities on bastion, custom Android client.

**Verdict:** **Reject** for dev-only v1 (operational cost).

### 6.4 Option D — Hybrid (proposal doc)

**WG for packets + RSSH (or SSH over WG) for operator files.**

**Verdict:** Valid **long-term**; does not require RSSH alone to carry IP routing.

### 6.5 Comparison matrix

| Criterion | A WG | B RSSH+SOCKS | C ssh -w | D hybrid |
|-----------|------|--------------|----------|----------|
| Ready today | Yes | No | No | Partial |
| TCP 443 only | No | Yes | Yes | Mixed |
| VPN icon | Yes | Yes | Yes | Yes |
| Alpha RSSH purity | N/A | Weakens if default | Weakens | OK |
| Impl effort | Done | High | Very high | Medium |

---

## 7. DR recommendation

1. **Close WireGuard E2E** (peer reconcile, self-test `vpn-ip`, hub→phone diagnostics, full-tunnel egress check) — **current priority**.
2. **Keep alpha RSSH** as reverse tunnel only ([ROADMAP.md](../ROADMAP.md)).
3. **Park Option B** in this DR; PO reviews §16 and decides **go / no-go** after WG.
4. If **go:** spawn SPEC `rssh_routed_egress` (dev-only feature flag), not a change to core RSSH connect payload for alpha.

---

## 8. Reference design (if pursued later)

### 8.1 Control plane

Unchanged: `heartbeat.php` `type: ra`, `tunnel: ssh_reverse`, credentials for bastion + `-R` port.

Optional dev flag in heartbeat meta: `rssh_route_experimental: true` (telemetry only; no BE requirement for v0).

### 8.2 Data plane

| Step | Component |
|------|-----------|
| 1 | `ReverseSshTunnelBridge` connects; `-R` as today |
| 2 | Same session: `session.setPortForwardingD("127.0.0.1", socksPort)` (JSch) |
| 3 | `RsshRoutedVpnEngine` starts TUN via `VpnService` |
| 4 | tun2socks reads TUN, connects to `127.0.0.1:socksPort` |
| 5 | Bastion sshd forwards; **iptables MASQUERADE** on bastion for egress |

### 8.3 Routing semantics

Reuse `RemoteAccessVpnRouting.resolveAllowedIps()`:

| Route scope | TUN routes |
|-------------|------------|
| **HUB_ONLY** | `172.200.2.1/32` (or configured hub CIDR) |
| **FULL_TUNNEL** | `0.0.0.0/0`, `::/0` |

Reuse `appendInterfaceExtras()` for **THIS_APP_ONLY**.

**Note:** Hub reachability over RSSH may need **explicit route to hub IP via TUN** or SOCKS; validate that hub-only does not assume WG interface addresses.

### 8.4 Failure modes

| Symptom | Likely cause |
|---------|----------------|
| SOCKS up, no Internet | Bastion NAT / forwarding disabled |
| Tunnel up, wrong egress IP | Route scope hub-only (expected) |
| Intermittent stalls | TCP-over-TCP congestion |
| VPN works, operator SFTP dead | `-R` forward dropped; session limits |

---

## 9. Developer settings UX

Proposed (not implemented):

```
Remote access mode: [ Disabled | WireGuard | RSSH ]

— When WireGuard —
  VPN route scope: Hub only | All traffic
  VPN app scope:   All apps | This app only

— When RSSH —
  [ ] Route device traffic via bastion (experimental)
      (shows route/app scope only when checked)
  Hint: Reverse tunnel for operator access is always on in RSSH mode.
        Routing requires VPN permission (same as WireGuard).
```

Network self-test (RSSH + experimental): `rssh-bastion`, `rssh-remote-bind`, `rssh-socks`, `wan-egress-via-vpn`, `rssh-connected`.

---

## 10. Backend and bastion requirements

| Requirement | Notes |
|-------------|--------|
| `AllowTcpForwarding yes` | Already for `-R` |
| Dynamic forwarding | Confirm sshd `GatewayPorts` / no restrictive `Match` block for `-D` |
| Egress NAT | iptables/nft MASQUERADE on bastion (similar to `wg0` PostUp) |
| Capacity | Routed dev traffic adds bandwidth; separate from §12.1 RSSH SSH counts in scaling DR |
| Audit | Log experimental flag; no change to alpha session schema required |

No new nginx HTTP paths; optional stream unchanged.

---

## 11. App components (sketch)

| Component | Process | Role |
|-----------|---------|------|
| `ReverseSshTunnelBridge` | main | SSH session; `-R` + `-D` |
| `RsshRoutedVpnService` | `:vpn` | TUN + lifecycle |
| `RsshTun2SocksEngine` | `:vpn` | Packet relay |
| `RemoteAccessVpnRouting` | shared | Route/app scope |
| `DevVpnStatusProbe` | main | Diagnostics |

Do **not** fold into `WireGuardVpnEngine`; separate engine behind common `VpnServiceClient` interface if refactored later.

---

## 12. Third-party and licenses

| Candidate | License | Action if adopted |
|-----------|---------|-------------------|
| **badvpn** / **tun2socks** | Various | Evaluate; add to `third-party/`, mobile + BE license files |
| **JSch** | BSD | Already in app; verify `-D` support |
| **Apache MINA SSHD** | Apache-2.0 | Local server only today |

Update [COMMERCIAL.md](../COMMERCIAL.md) and `app/src/main/assets/licenses/` before merge.

---

## 13. Risks

| Risk | Mitigation |
|------|------------|
| Duplicates WG | PO go/no-go; default to WG |
| Alpha scope creep | Dev-only flag; off in release if needed |
| VPN consent UX regression | Document in FR; separate from “no VPN” RSSH story |
| Bastion abuse (open egress) | Dev whitelist + session TTL + rate limits |
| Maintenance burden | Two routing stacks |

---

## 14. Effort estimate (order of magnitude)

| Phase | Work | Duration (indicative) |
|-------|------|------------------------|
| P0 | WG E2E closure | **Current sprint** (PO priority) |
| P1 | Spike: JSch `-D` + manual SOCKS browser test | 2–3 d |
| P2 | tun2socks + `VpnService` + route prefs | 5–8 d |
| P3 | Bastion NAT + self-test + docs | 2–3 d |
| P4 | Unit/integration tests, soak | 3–5 d |

**Total if pursued:** ~3–4 weeks **after** WG done — not parallelized with WG closure unless PO overrides.

---

## 15. Task dependency graph

Legend: **blocked by** upstream task.

```mermaid
flowchart TD
  WG1["1. WG peer reconcile on BE deploy"]
  WG2["2. WG E2E phone ↔ hub"]
  WG3["3. WG full-tunnel egress validation"]
  PO["4. PO review this DR"]
  GO{"5. Go routed RSSH?"}
  SPIKE["6. JSch -D spike"]
  SPEC["7. SPEC rssh_routed_egress"]
  IMPL["8. Implement dev experimental mode"]

  WG1 --> WG2 --> WG3 --> PO
  PO --> GO
  GO -->|no| PARK["Park — use WG only"]
  GO -->|yes| SPIKE --> SPEC --> IMPL
```

| Priority | Task | Owner | State |
|----------|------|-------|-------|
| **1** | Finish WG (peer sync, ping/handshake, self-test, full-tunnel) | Dev/PO | In progress |
| **2** | Review DR R0 | PO | Pending |
| **3** | RSSH routed egress | — | **Postponed** |

---

## 16. Open questions (PO)

| # | Question | Default if silent |
|---|----------|-------------------|
| Q1 | After WG E2E, is TCP-only routing still a requirement? | **No** — stay WG-only for routing |
| Q2 | Accept VPN icon for an experimental RSSH routing mode? | Required for Option B |
| Q3 | Should hub-only over RSSH target `172.200.2.1` without WG? | Clarify use case |
| Q4 | Bastion egress NAT for dev traffic — allowed on prod `be-vpn`? | Lab-only |
| Q5 | Single SSH session for `-R` + `-D` or two sessions? | Prefer **one** session |

---

## 17. Changelog

| Rev | Date | Change |
|-----|------|--------|
| R0 | 2026-06-16 | Initial DR from PO question; postponed pending WG closure |

---

## Related docs

- [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md) — WG vs RSSH modes
- [REMOTE_ACCESS_VALIDATION.md](../REMOTE_ACCESS_VALIDATION.md) — CLI/mobile WG checks
- [VPN_DEMO_GAPS_20260613.md](../VPN_DEMO_GAPS_20260613.md) — WG demo gaps
- [20260602_REVERSE_SSH_proposals_summary.md](../20260602_REVERSE_SSH_proposals_summary.md) — original transport comparison
- [specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md) — `be-vpn`, RSSH capacity
