# Android Cast — On-Demand Remote Access (Reverse Tunnel) Proposals | Field | Value | |---|---| | Author | Anton Afanasyeu | | Revision | R1 | | Creation date | 2026-06-03 | | Last modification date | 2026-06-03 | | Co-authored | | | Severity | medium | | State | pending review | | Document type | DR | \newpage \newpage --- _Date: 2026-06-02 — **merged document**: initial proposal (rev. 1) + infra review addendum (rev. 2)_ --- --- --- --- --- --- --- --- --- ## Table of contents - [Document history](#document-history) - [Context and constraints](#context-and-constraints) - [Executive summary](#executive-summary) - [Initial proposal (rev. 1)](#initial-proposal-rev-1) - [Refined after infra review (rev. 2)](#refined-after-infra-review-rev-2) - [Stakeholder questions (rev. 2 — added to requirements)](#stakeholder-questions-rev-2-added-to-requirements) - [Problem statement](#problem-statement) - [Proposed control plane (heartbeat type: ra)](#proposed-control-plane-heartbeat-type-ra) - [Device → BE (poll, random interval 1–7 min when enabled)](#device-be-poll-random-interval-17-min-when-enabled) - [BE → device (only if whitelisted + operator requested session)](#be-device-only-if-whitelisted-operator-requested-session) - [Disable (user unchecks box or admin revokes)](#disable-user-unchecks-box-or-admin-revokes) - [Field reference](#field-reference) - [State machine (simplified)](#state-machine-simplified) - [Network topology — Gentoo FE → Alpine BE (rev. 2)](#network-topology-gentoo-fe-alpine-be-rev-2) - [FE ↔ BE tuning (by track)](#fe-be-tuning-by-track) - [Security model](#security-model) - [Key handling options (rev. 1)](#key-handling-options-rev-1) - [Embedding in the Android app (rev. 2)](#embedding-in-the-android-app-rev-2) - [Backend integration ease — Alpine BE (rev. 2)](#backend-integration-ease-alpine-be-rev-2) - [Licensing and open source (rev. 2)](#licensing-and-open-source-rev-2) - [What “Custom file API” means (rev. 2 — clarified)](#what-custom-file-api-means-rev-2-clarified) - [Tooling alternatives — full survey (rev. 1)](#tooling-alternatives-full-survey-rev-1) - [Tooling summary — infra-focused (rev. 2)](#tooling-summary-infra-focused-rev-2) - [Comparison matrix — initial scoring (rev. 1)](#comparison-matrix-initial-scoring-rev-1) - [Comparison matrix — FE proxy focus (rev. 2)](#comparison-matrix-fe-proxy-focus-rev-2) - [WireGuard vs SSH — decision lens (rev. 2)](#wireguard-vs-ssh-decision-lens-rev-2) - [Recommendations](#recommendations) - [Initial (rev. 1)](#initial-rev-1) - [Refined (rev. 2 — Gentoo FE → Alpine BE)](#refined-rev-2-gentoo-fe-alpine-be) - [RBAC roles (BE)](#rbac-roles-be) - [BE admin UI (crashes look-and-feel)](#be-admin-ui-crashes-look-and-feel) - [MariaDB schema (sketch — rev. 1)](#mariadb-schema-sketch-rev-1) - [Android app changes (sketch — rev. 1)](#android-app-changes-sketch-rev-1) - [Estimates (engineering)](#estimates-engineering) - [Open questions (merged)](#open-questions-merged) - [Rev. 3 — WG vs SSH Q&A + bastion deep-dive](#rev-3-wg-vs-ssh-qa-bastion-deep-dive) - [Q1 — Exact FE/BE rules: WireGuard (DNAT) vs SSH (no DNAT required)](#q1-exact-febe-rules-wireguard-dnat-vs-ssh-no-dnat-required) - [Q2 — Can we use ports other than :80? (heartbeats and everything else)](#q2-can-we-use-ports-other-than-80-heartbeats-and-everything-else) - [Q3 — Why is “Proxyable (FE→BE)” low for WG (2) and high for SSH (5)?](#q3-why-is-proxyable-febe-low-for-wg-2-and-high-for-ssh-5) - [Rev. 3 — Bastion Q&A](#rev-3-bastion-qa) - [Q4 — How easy to implement? Command checklist (FE + BE)](#q4-how-easy-to-implement-command-checklist-fe-be) - [Q5 — Bastion: file transfers only, or other operations too?](#q5-bastion-file-transfers-only-or-other-operations-too) - [Appendix 1 — WG test configuration (Linux laptop)](#appendix-1-wg-test-configuration-linux-laptop) - [Network topology (v1 WireGuard)](#network-topology-v1-wireguard) - [Prerequisites (laptop)](#prerequisites-laptop) - [A. HTTP E2E (no tunnel yet)](#a-http-e2e-no-tunnel-yet) - [B. UDP / WireGuard from laptop (after open session)](#b-udp-wireguard-from-laptop-after-open-session) - [C. Quick checks before blaming WG](#c-quick-checks-before-blaming-wg) - [Appendix 2 — WG production configuration](#appendix-2-wg-production-configuration) - [BE config.php (remote_access)](#be-configphp-remoteaccess) - [BE (Alpine) — WireGuard interface](#be-alpine-wireguard-interface) - [FE (Gentoo) — UDP DNAT (not nginx)](#fe-gentoo-udp-dnat-not-nginx) - [MariaDB / migrations](#mariadb-migrations) - [Production verify (on BE or laptop)](#production-verify-on-be-or-laptop) - [Android (field device)](#android-field-device) - [Source linkage](#source-linkage) **Documentation index:** [README.md](README.md) --- ## Document history | Rev | Focus | Summary | |---|---|---| | **1** | Initial research memo | On-demand reverse tunnel via heartbeat `type: ra`; compare WG, SSH, tinc, Tailscale, etc.; hybrid **WireGuard outbound + SSH/SFTP bastion** as first pick. | | **2** | Stakeholder Q&A | Gentoo FE → Alpine BE path, embed WG/SSH in app, licensing, Custom file API clarified; refined pick **reverse SSH/SFTP** (**Proxyable**) over WG (**Lightweight**) for this nginx topology. | | **3** | WG vs SSH + bastion deep-dive | Exact FE/BE commands (DNAT vs `stream`), port policy, **Proxyable** scoring explained, bastion scope (files vs full debug). | Both revisions are kept below. Where they differ, **§14 Recommendations** lists both and explains why rev. 2 refined the tunnel choice. **§Rev. 3** adds operator-ready command checklists for the final pick. --- ## Context and constraints - **Need:** occasional remote debug / manual settings on **one field device** (e.g. edit `codecs.json` in app home, inspect logs) without a **persistent** device↔BE tunnel. - **Stack:** minimalistic LAMP (nginx + php-fpm + MariaDB) on BE; Android app already posts to `/api/heartbeat.php`. - **Traffic path (rev. 2):** **Mobile app → Gentoo FE** (TLS, firewall, nginx reverse proxy) **→ Alpine BE** (XEN HVM, project services). Heartbeat already follows this; any data tunnel must be tunable on **both** FE and BE. - **Security:** no standing inbound connection from BE to device; user opt-in; admin whitelist; full audit trail; minimal secret exposure on wire. - **Ops:** sessions expire on inactivity; 24h DB cleanup; user can disable instantly from app settings. - **Convenience (rev. 2):** SCP-/SFTP-**like** file exchange is **not mandatory** but highly desirable for `codecs.json` and logs. --- ## Executive summary ### Initial proposal (rev. 1) - **Recommended:** on-demand reverse tunnel orchestrated by BE heartbeat (`type: ra`) + **WireGuard or SSH over a single ephemeral port** on BE, with **per-session keys** and **PHP admin UI** matching crashes console look-and-feel. - **Not recommended for v1:** always-on VPN mesh (tinc/Tailscale on every device), public inbound SSH to phones, or BE-initiated connections through carrier NAT. - **Pattern:** device polls BE at **random 1–7 min** when user enables “Remote access”; BE responds only if device is **whitelisted**; device then **dials out** and attaches tunnel; operator connects to BE **bastion/port**, not to the phone directly. - **Primary pick (rev. 1):** WireGuard outbound + SSH/SFTP on bastion for operator (combines mobile simplicity with familiar file edit workflow for `codecs.json`). - **Fallback (rev. 1):** if UDP blocked on target networks, use **chisel/frp over HTTPS** on same `ra` vhost with identical control plane. ### Refined after infra review (rev. 2) - **Control plane:** unchanged (heartbeat `type: ra`, checkbox, whitelist, jittered poll, device dials out). - **Tunnel choice (refined):** prefer **reverse SSH (+ SFTP)** over WireGuard for v1 **on this stack**. - **One word — SSH:** **Proxyable** — TCP (nginx **`stream`**) matches Gentoo FE → Alpine BE like existing HTTP proxy; SFTP gives SCP-like files. - **One word — WireGuard:** **Lightweight** — excellent embeddable crypto, but **UDP does not ride nginx HTTP reverse-proxy**; FE needs UDP DNAT/firewall rules. - **Custom file API:** optional third track — HTTPS upload/download via PHP (§10); good Phase-1 or narrow path without sshd. --- ## Stakeholder questions (rev. 2 — added to requirements) | # | Question | Short answer | |---|---|---| | Q1 | Is WireGuard “best”? | Best **crypto/simplicity**, not best **FE→BE proxy fit** for your stack. | | Q2 | Embed WG in app or separate app? | **Embed** via `wireguard-android` tunnel library + `VpnService`; no standalone WireGuard app. | | Q3 | Embed SSH or separate app? | **Embed** Dropbear/libssh2/MINA SSHD; no Termux. | | Q4 | BE integration: WG vs SSH? | **SSH easier** on Alpine (`openssh`, `authorized_keys`, `-R` port). WG needs `wg set` + **UDP** FE→BE. | | Q5 | Traffic Mobile → Gentoo FE → Alpine BE? | Heartbeat: `proxy_pass :80`. SSH: nginx **`stream`**. WG: **UDP DNAT** (not nginx `location /`). | | Q6 | Licensing / open source? | See §9; Tailscale server is main license caveat. | | Q7 | What is “Custom file API”? | HTTPS file transfer via PHP — not SCP, not a VPN; see §10. | | Q8 | SCP-like file exchange? | Native **SFTP (SSH)**; similar UX via Custom file API; WG needs SSH/SFTP on top for SCP-like UX. | --- ## Problem statement | Aspect | Detail | |---|---| | Trigger | Single device in the field needs interactive access (debug, replace config, pull logs). | | Anti-goals | No permanent VPN; no open listening port on mobile; no broad fleet exposure. | | User consent | Checkbox in app settings (“allow remote access at my own risk”). | | Admin gate | Device ID must be on BE **remote_access_devices** whitelist before any session is offered. | | Observability | Every enable/disable/connect/disconnect/timeout logged with reason and actor. | | Path (rev. 2) | Signalling via FE→BE :80; tunnel endpoint on FE forwarded to BE. | --- ## Proposed control plane (heartbeat `type: ra`) Reuse existing heartbeat endpoint; add branch for remote access. ### Device → BE (poll, random interval 1–7 min when enabled) ```json { "heartbeat": { "type": "ra", "status": "enable", "device_id": "", "random": "", "app_version": "0.1.0", "capabilities": ["ssh", "sftp", "files_app_home"] } } ``` _Also accepted (rev. 2): `"capabilities": ["ssh_reverse", "sftp", "files_app_home"]`._ ### BE → device (only if whitelisted + operator requested session) ```json { "heartbeat": { "type": "ra", "action": "connect", "session_id": "uuid", "endpoint": "ra.apps.f0xx.org:443", "tunnel": "wireguard|ssh|ssh_reverse", "credentials": { "mode": "ephemeral_pubkey|otp", "username": "ra-", "host_key_fingerprint": "…", "remote_forward": "127.0.0.1::127.0.0.1:8022" }, "expires_at": 1717340000 } } ``` _WireGuard branch (rev. 1): `"tunnel": "wireguard"` with peer keys in `credentials`. SSH branch (rev. 2): `"tunnel": "ssh_reverse"` as above._ ### Disable (user unchecks box or admin revokes) ```json { "heartbeat": { "type": "ra", "status": "disable", "device_id": "...", "random": "..." } } ``` ### Field reference | Field | Purpose | |---|---| | `random` | Client nonce for matching pending BE session; rotation on reconnect. | | `session_id` | DB primary key for audit + port/key binding. | | `status` | `enable` / `disable` / `poll` (optional alias for enable). | | `action` | BE command: `connect`, `wait`, `deny`, `rotate`. | ### State machine (simplified) ```text [user enables RA] → device polls → BE: wait (not whitelisted / no operator) → admin opens session for device_id → next poll → BE: connect + creds → device opens outbound tunnel → BE marks active → operator uses bastion [user disables] → disable heartbeat → tunnel tear-down → BE inactive + audit row [24h idle] → BE cron purges stale rows + closes ports ``` --- ## Network topology — Gentoo FE → Alpine BE (rev. 2) ```text [Android app] │ HTTPS POST /api/heartbeat.php (type: ra) ▼ [Gentoo FE — apps.f0xx.org] │ nginx: proxy_pass http://artc0.intra.raptor.org:80 (existing) ▼ [Alpine BE — artc0 / XEN HVM] │ php-fpm + MariaDB (control plane) │ sshd / ra-bastion (data plane — SSH track) │ wg0 + UDP listener (data plane — WG track, if used) [Operator laptop] │ SSH/SFTP to FE or BE bastion port (never to phone IP) ▼ session forwarded to device's reverse tunnel ``` ### FE ↔ BE tuning (by track) | Track | Public entry (FE) | FE config | BE service | Notes | |---|---|---|---|---| | **Heartbeat (all)** | `https://apps.f0xx.org/.../heartbeat.php` | `proxy_pass http://artc0:80` | php-fpm | Already works. | | **Reverse SSH** | `ra.apps.f0xx.org:443` TCP or `:2222` | `stream { proxy_pass artc0:22; }` | `sshd` + `Match User ra-*` | Rev. 2 pick (**Proxyable**). | | **WireGuard** | `ra.apps.f0xx.org:51820/udp` | **iptables/nft DNAT** → BE | `wg-quick` / dynamic peers | Rev. 1 pick candidate (**Lightweight**). | | **Custom file API** | `https://apps.f0xx.org/.../api/ra/*` | existing HTTP proxy | php-fpm only | No stream/UDP rules. | --- ## Security model | Layer | Mechanism | |---|---| | Authorization | RBAC roles (§12) + per-device whitelist + user opt-in on device. | | Authentication | Per-session ephemeral key pair or one-time port + password; no long-lived device secrets in APK. | | Transport | TLS to BE for heartbeat; tunnel encrypted (WireGuard / SSH). | | Exposure | BE listens on dedicated **ra** vhost/port; no inbound to mobile; optional IP allowlist for operators. | | Audit | `remote_access_events` table: who, device, action, reason, ip, timestamps. | | Cleanup | Inactivity timeout (e.g. 30–60 min); daily purge of records > 24h; credential invalidation on disable. | | Reconnect | Same `session_id` + rotated `random` within grace window; new keys if expired. | ### Key handling options (rev. 1) | Mode | Description | Risk | |---|---|---| | Ephemeral WG keypair | BE generates keys per session; device installs peer config once | Low; keys short-lived | | SSH host cert + user cert | Step-CA or internal CA signs 1h user cert | Medium ops; mature tooling | | Pre-allocated device key | One WG pubkey per device in DB | Higher leak impact; simpler reconnect | | OTP + reverse SSH | `autossh -R random_port` with password from BE | Simple; weaker than pubkey | **Key ceremony (rev. 1):** ephemeral WireGuard or SSH **user certificate** per session; optional device **identity key** (Ed25519) registered at whitelist time for authenticating poll requests only (not tunnel). **Additional (rev. 2):** - Terminate operator SSH on FE stream or require VPN to Gentoo before bastion. - Device **outbound-only** to `ra.apps.f0xx.org:443` (TCP). - Per-session **`ForceCommand internal-sftp`** if shell not required. --- ## Embedding in the Android app (rev. 2) | Solution | Separate app required? | How to embed | Android notes | |---|---|---|---| | **WireGuard** | **No** | [`wireguard-android`](https://git.zx2c4.com/wireguard-android) tunnel AAR / `GoBackend` | `VpnService`; VPN icon while active. | | **SSH reverse** | **No** | Dropbear + JNI, **libssh2**, **Apache MINA SSHD** | Foreground service; no VPN icon. | | **frp/chisel client** | **No** | Static binary or Go mobile bind | Extra artifact. | | **Custom file API** | **No** | OkHttp + JSON | Pure Java/Kotlin. | You do **not** need Termux or the standalone WireGuard app if libraries are embedded in **Android Cast**. --- ## Backend integration ease — Alpine BE (rev. 2) | Aspect | Reverse SSH | WireGuard | |---|---|---| | Package on Alpine | `openssh` | `wireguard-tools` + kernel mod | | Session open (PHP) | `authorized_keys` + `-R` port; optional `ForceCommand internal-sftp` | `wg set wg0 peer …` | | Session close | Remove key; kill session; free port | `wg set … remove` | | FE cooperation | nginx **`stream`** (TCP) | **UDP DNAT** | | Operator access | `sftp -P 443 ra-...@ra.apps.f0xx.org` | `wg-quick up` or SSH over WG | | Jump host `f0xx.org:222` | Natural extension | Separate UDP path | --- ## Licensing and open source (rev. 2) | Component | License | Ship in app / BE | Notes | |---|---|---|---| | **WireGuard** (kernel module) | **GPL-2.0** | BE kernel | Stock Alpine package. | | **wireguard-go** / Android tunnel | **MIT** / **Apache-2.0** | App embed | Userspace embed. | | **OpenSSH** | **BSD-style** | BE + client | Permissive. | | **Dropbear** | **Permissive (MIT-like)** | App embed | Small client. | | **libssh2** | **BSD-3-Clause** | App embed | | | **Apache MINA SSHD** | **Apache-2.0** | App (Java) | SSH/SFTP in Java. | | **chisel** | **MIT** | App/BE | | | **frp** | **Apache-2.0** | App/BE | | | **tinc** | **GPL-2.0+** | — | Copyleft; mesh. | | **Tailscale client** | **BSD-3-Clause** | — | Client OSS. | | **Headscale / Tailscale server** | **BSD** / **BSL** | — | Self-host Headscale if needed. | | **Custom file API** | **Project code** | BE PHP | No third-party tunnel license. | _GPL-2.0 Android app: use permissively licensed clients (MIT/BSD/Apache); review before linking GPL client libs into APK._ --- ## What “Custom file API” means (rev. 2 — clarified) **Not** a tunnel and **not** the SCP protocol. Application-layer remote file exchange on existing PHP: - Endpoints: `POST /api/ra/upload`, `GET /api/ra/download`, `GET /api/ra/list`. - Auth: short-lived token from heartbeat `connect` (same session as control plane). - Scope: **`getFilesDir()`** / `codecs.json` unless Storage permission granted (broader scope needs second consent). - Operator: crashes-themed **web UI** or `curl` — **SCP-like** push/pull without `scp` binary. - **Pros:** no FE stream/UDP; minimal attack surface (no shell); reuses LAMP. - **Cons:** no shell; implement listing/chunking for large logs. **Use when:** Phase-1 before sshd bastion, or permanent narrow path alongside SSH for power users. _Original rev. 1 one-liner: “Custom HTTPS file API — PHP endpoints; no shell; minimal attack surface; more app code for file ops.”_ --- ## Tooling alternatives — full survey (rev. 1) | Option | Maturity | Mobile fit | BE fit | Pros | Cons / risks | |---|---|---|---|---|---| | **WireGuard (outbound from device)** | High | Excellent (userspace WG) | Single UDP listener / per-session peers | Fast, modern crypto, small code | UDP blocked on some carriers; peer lifecycle automation | | **Reverse SSH (`autossh -R`)** | High | Good (embedded dropbear client) | sshd on bastion | Familiar, SFTP, shell | Key mgmt; battery if misconfigured | | **SSH via WebSocket (chisel, frp)** | Medium | Good | One HTTPS port | Traverses strict firewalls | Extra binary; audit frp history | | **OpenVPN / IKEv2 (on-demand)** | High | Heavy | openvpn-access | Full tunnel | Overkill; battery; not file-focused | | **tinc** | Medium | Poor (daemon, mesh) | mesh coordinator | P2P mesh | Persistent mesh ≠ on-demand; key distribution painful | | **Tailscale / Headscale** | High | App + VPN | control server | Easy UX | Persistent identity; fleet-wide trust | | **MeshCentral / RustDesk** | High | Agent app | relay server | Remote desktop | Different UX; screen control not primary need | | **ADB over TLS tunnel** | Medium | Dev-only | reverse port | Exact Android debug | Dev options; security sensitive | | **Custom HTTPS file API** | Low (build) | Native | PHP endpoints | No shell; minimal attack surface | No arbitrary shell; more app code | --- ## Tooling summary — infra-focused (rev. 2) | Option | Embed? | FE→BE fit | SCP-like | License | Verdict | |---|---|---|---|---|---| | **Reverse SSH + SFTP** | Yes | **Excellent** (`stream`) | **Native** | BSD | **v1 pick (rev. 2)** | | **WireGuard outbound** | Yes | Fair (UDP DNAT) | Via SFTP over WG | MIT/GPL | **v1 pick (rev. 1)** / v2 if UDP path ready | | **Custom file API** | Yes | **Excellent** (HTTP) | Similar UX | Yours | Phase-1 / narrow | | **frp/chisel** | Yes | Good | Possible | MIT/Apache | Fallback (both revs) | | **tinc / Tailscale** | VPN app | Poor on-demand | Varies | GPL/BSL | Not recommended | --- ## Comparison matrix — initial scoring (rev. 1) Scoring: 1 = low, 5 = high. Lower total was interpreted as stronger general fit in rev. 1 table orientation. | Criterion | WG outbound | Reverse SSH | frp/chisel | tinc | Tailscale | Custom file API | |---|:---:|:---:|:---:|:---:|:---:|:---:| | Security | 5 | 4 | 3 | 3 | 4 | 5 | | Simplicity (ops) | 4 | 4 | 3 | 2 | 3 | 4 | | On-demand fit | 5 | 5 | 5 | 1 | 2 | 5 | | File access (app dir) | 4 | 5 | 4 | 3 | 4 | 5 | | Shell/debug | 4 | 5 | 4 | 3 | 4 | 1 | | Fits LAMP BE | 4 | 5 | 4 | 3 | 3 | 5 | | Battery / data | 4 | 3 | 3 | 2 | 2 | 5 | | **Weighted total** | **31** | **30** | **26** | **17** | **22** | **30** | _Rev. 1 tie note: WireGuard, reverse SSH, and Custom file API scored highest. Rev. 1 suggested **WireGuard + SFTP/SSH bastion** if shell needed; **Custom file API** if only `codecs.json` + logs._ --- ## Comparison matrix — FE proxy focus (rev. 2) | Criterion | Rev SSH | WG outbound | Custom file API | frp/chisel | |---|:---:|:---:|:---:|:---:| | **Proxyable (FE→BE)** | **5** | 2 | **5** | 4 | | Security | 4 | 5 | 5 | 3 | | On-demand fit | 5 | 5 | 5 | 5 | | SCP-like files | **5** | 3* | 4 | 3 | | BE integration ease | **5** | 3 | **5** | 3 | | Embed without 3rd-party app | 5 | 5 | **5** | 4 | | **Total** | **29** | 23 | **29** | 22 | \*WG files row assumes SSH/SFTP layered on tunnel. --- ## WireGuard vs SSH — decision lens (rev. 2) | Criterion | WireGuard | Reverse SSH + SFTP | |---|---|---| | **One-word headline** | **Lightweight** | **Proxyable** | | Embed in app | Yes (`VpnService`) | Yes (libssh2/MINA/dropbear) | | FE nginx HTTP proxy | No (UDP) | Yes (**`stream` TCP**) | | Gentoo → Alpine path | Firewall UDP DNAT | Same as HTTP ops model | | SCP-like files | Needs SSH/SFTP on top | **Built-in (SFTP)** | | Carrier networks | UDP sometimes blocked | TCP 443 usually works | --- ## Recommendations ### Initial (rev. 1) 1. **Phase 0** — Design + threat model (1–2 days): consent copy, port allocation, key ceremony. 2. **Phase 1** — Control plane (1–2 weeks): heartbeat `ra`, whitelist, admin UI, audit, cron; dry-run `wait`/`connect`. 3. **Phase 2** — Tunnel (1–2 weeks): **WireGuard outbound** from app OR **reverse SSH to bastion**; operator connects via existing VPN/SSH to BE network. 4. **Phase 3** — Hardening: rate limits, mTLS on heartbeat, device identity signing, reconnect, pen-test. **Primary pick (rev. 1):** WireGuard outbound + SSH/SFTP on bastion for operator. **Fallback (rev. 1):** chisel/frp over HTTPS on same `ra` vhost. ### Refined (rev. 2 — Gentoo FE → Alpine BE) **Primary v1:** **reverse SSH (+ SFTP)** — path is Mobile → Gentoo FE (nginx) → Alpine BE; **Proxyable** TCP; SFTP for `codecs.json`. **Secondary:** WireGuard when dedicated **UDP forwarding** on FE is acceptable — **Lightweight** tunnel. **Optional parallel:** **Custom file API** for whitelist-only file push/pull without internet-facing sshd. **Phases (merged):** 1. Phase 0 — FE/BE port map + threat model (1–2 d). 2. Phase 1 — heartbeat `ra` + admin UI + optional Custom file API (1–2 w). 3. Phase 2 — reverse SSH/SFTP + Gentoo `stream` → Alpine sshd **or** WG track if chosen (1–2 w). 4. Phase 3 — MFA, reconnect, pen-test. --- ## RBAC roles (BE) | Role | Permissions | |---|---| | **remote_access_user** | Open/close own sessions for whitelisted devices; view active/inactive list (scoped); SFTP / file API. | | **admin** | Same as user + add/remove whitelist entries + assign `remote_access_user` role. | | **root** | Full role management + force-disconnect any session + view all audit logs. | Integrate with existing `users` / `Rbac`; new permissions: `remote_access_view`, `remote_access_operate`, `remote_access_admin`. --- ## BE admin UI (crashes look-and-feel) - New nav: **Remote access** under `/app/androidcast_project/crashes/?view=remote_access` (or `/remote/`). - Reuse `layout.php`, themes, locale picker, session auth. - Screens (rev. 1): 1. **Devices** — whitelist, last seen, RA opt-in status (from last heartbeat). 2. **Sessions** — active / inactive; device, operator, started, last activity, tunnel type, port, status. 3. **Audit log** — filter by device, user, action, date. 4. **Open session** — pick device → pending session → waits for next device poll. - Rev. 2 add-on: **file browser** tab when Custom file API enabled (upload/download `codecs.json`). - No new frontend framework; same CSS/JS as tickets/graphs. --- ## MariaDB schema (sketch — rev. 1) ```sql -- remote_access_devices (whitelist) -- remote_access_sessions (session_id, device_id, operator_user_id, status, -- tunnel, port, keys_ref, expires_at, last_activity) -- remote_access_events (audit) -- remote_access_credentials (optional encrypted blob, TTL) ``` Cron: `scripts/purge_remote_access.php` — inactive > 24h → `closed_timeout`; revoke keys; free ports. --- ## Android app changes (sketch — rev. 1) - Settings → Developer → **Allow remote access** (off by default, strong warning). - `RemoteAccessService` (foreground while tunnel active): jittered alarm 1–7 min → POST heartbeat. - On `action: connect`: start WireGuard/SSH client with supplied creds; expose app files dir only unless Storage permission granted (broader scope needs second consent). - On disable: stop service, wipe session keys, POST `status: disable`. - Persist nothing long-term except optional device identity pubkey for poll authentication. --- ## Estimates (engineering) | Track | Initial delivery | Hardening | Total | |---|---:|---:|---:| | Control plane only (heartbeat + DB + admin list) | 5–8 d | 3–5 d | 8–13 d | | Control plane + Custom file API | 6–9 d | 3–5 d | 9–14 d | | + WireGuard outbound tunnel (rev. 1 pick) | 8–12 d | 5–8 d | 13–20 d (+ FE UDP) | | + Reverse SSH/SFTP bastion (rev. 2 pick) | 7–11 d | 4–7 d | 11–18 d | | + FE nginx `stream` + firewall docs | +1–2 d | +1 d | (included in SSH track) | _Assumptions: one BE bastion; excludes Play Store policy review._ --- ## Open questions (merged) **Rev. 1:** - Maximum concurrent RA sessions per BE? - Store tunnel configs only in RAM vs encrypted MariaDB blob? - Require operator MFA (TOTP) before `Open session`? - Play Store disclosure for “remote access” foreground service? **Rev. 2:** - Public TCP port: dedicated `2222` vs share `443` with `ssl_preread`? - SFTP-only vs full shell for `remote_access_user`? - Store device identity Ed25519 pubkey at whitelist time? - Rate limit heartbeat `ra` polls? **Rev. 3:** - Confirm public RA port(s) with FE firewall owner before Phase 2? - SFTP-only (`ForceCommand internal-sftp`) vs full shell for v1? --- ## Rev. 3 — WG vs SSH Q&A + bastion deep-dive _Added 2026-06-02 — concrete FE/BE commands and decision support._ Constants used below (from [INFRA.md](INFRA.md)): | Symbol | Value | |---|---| | **FE** | Gentoo HVM, public `apps.f0xx.org` (TLS **443** terminates here) | | **BE** | Alpine `artc0.intra.raptor.org` / `10.7.16.128`, app vhost **`listen 80`** | | **FE→BE (existing HTTP)** | `proxy_pass http://artc0.intra.raptor.org:80` | | **Operator jump (existing)** | `f0xx.org:222` → Alpine `:22` | Replace `10.7.16.128` with the live BE IP if it changes. --- ### Q1 — Exact FE/BE rules: WireGuard (DNAT) vs SSH (no DNAT required) **Short answer:** WireGuard **must** use **UDP DNAT** (or equivalent firewall forward) on Gentoo FE because nginx **`proxy_pass` is HTTP/TCP-only** and cannot carry WG packets. SSH **does not need DNAT** on FE if you use nginx **`stream`** TCP forwarding (same nginx daemon, different block). You *can* use TCP DNAT for SSH as an alternative, but it is redundant when `stream` works. #### WireGuard track — FE (Gentoo): UDP DNAT + forward allow Public UDP **51820** on FE → BE **51820**. Example with **nftables** (adjust interface `eth0`, table/chain names to match your FE): ```bash # FE — one-time / boot-persistent (example) BE_IP=10.7.16.128 WG_PORT=51820 nft add rule inet nat prerouting iifname "eth0" udp dport $WG_PORT \ dnat ip to ${BE_IP}:${WG_PORT} nft add rule inet filter forward ip daddr ${BE_IP} udp dport ${WG_PORT} accept ``` **iptables equivalent:** ```bash BE_IP=10.7.16.128 WG_PORT=51820 iptables -t nat -A PREROUTING -i eth0 -p udp --dport $WG_PORT \ -j DNAT --to-destination ${BE_IP}:${WG_PORT} iptables -A FORWARD -p udp -d ${BE_IP} --dport $WG_PORT -j ACCEPT ``` **Persist:** save rules (`/etc/nftables.conf` or `iptables-save` → Gentoo `iptables` init). **Open UDP 51820** on FE edge firewall (in addition to 443/tcp). **Verify from outside:** ```bash # should show BE wg listening (once BE step done) nc -u -v apps.f0xx.org 51820 ``` #### WireGuard track — BE (Alpine): interface + listen ```bash # BE apk add wireguard-tools modprobe wireguard 2>/dev/null || echo "wireguard module required" cat >/etc/wireguard/wg0.conf <<'EOF' [Interface] Address = 10.66.66.1/24 ListenPort = 51820 PrivateKey = # PostUp/PostDown: optional iptables for operator subnet EOF wg-quick up wg0 rc-update add wg-quick.wg0 default # if using OpenRC ``` **Per RA session** (PHP or admin script adds ephemeral peer — example): ```bash wg set wg0 peer allowed-ips 10.66.66.2/32 # on session end: wg set wg0 peer remove ``` Device connects **outbound UDP** to `apps.f0xx.org:51820` (DNAT lands on BE). Operator joins same WG net (second peer) or uses SSH **over** the tunnel (see Q5). --- #### SSH track — FE (Gentoo): nginx **`stream`** (TCP proxy, not DNAT) Preferred: public TCP **443** on `ra.apps.f0xx.org` (or dedicated **2222**) → BE **22**. No kernel DNAT if nginx terminates the TCP hop. Add to FE nginx (often `/etc/nginx/nginx.conf`, **outside** `http {}`): ```nginx stream { upstream be_sshd { server 10.7.16.128:22; } server { listen 443; # or 2222 if 443 shared with TLS SNI split proxy_pass be_sshd; proxy_connect_timeout 30s; } } ``` If **443** is already used for HTTPS on the same IP, either: - use **`listen 2222`** for RA SSH, or - use **`ssl_preread`** on 443 to split HTTPS vs SSH by ClientHello (more complex). **Reload FE nginx:** ```bash nginx -t && nginx -s reload ``` **Verify:** ```bash ssh -p 443 -o StrictHostKeyChecking=accept-new ra-test@ra.apps.f0xx.org # or: nc -v ra.apps.f0xx.org 2222 ``` #### SSH track — optional FE DNAT (parallel to WG, for comparison only) If you refused nginx `stream`, SSH could mirror WG with **TCP DNAT**: ```bash # FE — iptables example (TCP 2222 → BE:22) iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2222 \ -j DNAT --to-destination 10.7.16.128:22 iptables -A FORWARD -p tcp -d 10.7.16.128 --dport 22 -j ACCEPT ``` This works but **duplicates** what `stream` already does and bypasses nginx logging/timeouts. **Recommendation:** use **`stream`**, not DNAT, for SSH. #### SSH track — BE (Alpine): sshd + reverse-forward slot ```bash # BE apk add openssh rc-update add sshd default rc-service sshd start # Dedicated match block — /etc/ssh/sshd_config.d/ra.conf cat >/etc/ssh/sshd_config.d/ra.conf <<'EOF' Match User ra-* AllowTcpForwarding yes GatewayPorts no X11Forwarding no PermitTTY yes # v1 file-only: ForceCommand internal-sftp EOF rc-service sshd restart ``` **Per session — device outbound reverse forward** (embedded client runs equivalent): ```bash # On device (conceptual — app embeds this): ssh -N -R 127.0.0.1:18022:127.0.0.1:8022 ra-@ra.apps.f0xx.org -p 443 ``` **BE — operator reaches device file area via forwarded port:** ```bash # On BE (after device connected): sftp -P 18022 ra-@127.0.0.1 # or scp -P 18022 ... ``` --- #### Side-by-side: what runs where | Step | WireGuard | Reverse SSH | |---|---|---| | **FE edge open** | UDP **51820** | TCP **443** or **2222** | | **FE mechanism** | **nft/iptables DNAT** | nginx **`stream`** (or optional TCP DNAT) | | **BE daemon** | `wg0` / `wg set` | `sshd` + ephemeral `Match User ra-*` | | **Device direction** | Outbound UDP to FE public IP | Outbound TCP to FE → proxied to BE sshd | | **Operator path** | WG peer IP or SSH over WG | SFTP/SSH to BE forwarded port (see Q5) | --- ### Q2 — Can we use ports other than `:80`? (heartbeats and everything else) **Two different “ports” — do not conflate them.** | Leg | Port today | Can change? | Notes | |---|---|---|---| | **Phone → FE** | **443** (HTTPS) | Yes, but **443 is correct** for TLS + carrier-friendly egress | App already targets `https://apps.f0xx.org/...`; users never hit `:80` directly. | | **FE → BE (heartbeat, crashes, graphs)** | **80** (HTTP) | **Yes** | Requires new FE `proxy_pass` upstream **and** BE `listen` block. Not forbidden — just **unused today**. | | **RA data plane (WG)** | **51820/udp** (proposed) | Must be **non-80** | UDP tunnel; unrelated to HTTP vhost. | | **RA data plane (SSH)** | **443/tcp** or **2222/tcp** | Must be **non-80** | Raw TCP/`stream`; not HTTP `location /`. | **Why heartbeats stay on the existing `:80` FE→BE path (recommended for v1):** 1. **Already deployed** — [nginx.fe-apps.f0xx.org.snippet](../examples/crash_reporter/backend/nginx.fe-apps.f0xx.org.snippet) and BE `listen 80` fragment are the repo source of truth; zero new FE/BE holes for control plane. 2. **Same security zone** as crash upload and graphs — one ops playbook. 3. **No benefit** to moving heartbeat to BE `:443` internally — the phone still speaks HTTPS to FE; internal hop is your choice. 4. **Port 8089** on BE is **Janus/other** — do **not** reuse for androidcast ([INFRA.md §2](INFRA.md)). **If you intentionally want heartbeat on BE `:443` internally:** ```bash # FE — change upstream in server block (example) # proxy_pass https://artc0.intra.raptor.org:443; # BE — ensure listen 443 ssl server includes /api/heartbeat.php location ``` That is a **separate migration**; not required for RA. **Summary:** Public **443** for all HTTPS (including heartbeat). Internal FE→BE **80** for LAMP today — **can** change, **should not** for v1 unless you have another reason. RA tunnels **must** use **dedicated non-80** ports (UDP 51820 or TCP 443/2222 stream). --- ### Q3 — Why is “Proxyable (FE→BE)” low for WG (2) and high for SSH (5)? The rev. 2 score measures one thing: **“Can we forward this service from Gentoo FE to Alpine BE the same way we already forward crashes/hub HTTP?”** | | **Reverse SSH** | **WireGuard** | |---|---|---| | **Protocol** | TCP | UDP | | **Existing FE tool** | nginx **`stream`** → `10.7.16.128:22` | **Not** nginx `location` / `proxy_pass` | | **FE change type** | Add `stream {}` block; reload nginx | **Firewall DNAT** + forward rules; separate from nginx HTTP config in repo | | **Ops familiarity** | Same team that edits `nginx.conf` | Often different (iptables/nft, UDP, kernel module) | | **Debugging** | `nginx -t`, error log, `curl`/ `nc` TCP | `nft list ruleset`, UDP blocked on some carriers, `wg show` on BE | | **Matches INFRA docs** | Extends nginx pattern | New parallel path not in current snippets | **Score meaning:** - **5 (SSH):** “Drop in a `stream` upstream to BE — same deployment rhythm as today’s reverse proxy.” - **2 (WG):** “Works, but **not** proxyable via HTTP nginx; needs **DNAT** (or a UDP proxy like `udp2raw`, which is extra complexity). Partial credit — not zero — because FE→BE forwarding is still one hop.” **Important nuance:** Low **Proxyable** does **not** mean WG is insecure or bad on the phone — it means **your FE→BE operational model** favours TCP/nginx. That is why rev. 2 refined the v1 pick to SSH despite WG being **Lightweight** on-device. **Custom file API also scores 5** because it reuses existing **`proxy_pass http://artc0:80`** with new PHP locations — no `stream`, no DNAT. --- ## Rev. 3 — Bastion Q&A ### Q4 — How easy to implement? Command checklist (FE + BE) Rough effort (after control plane PHP/DB exists): | Track | FE ops | BE ops | App embed | Typical first-time | |---|---|---|---|---| | **Reverse SSH + SFTP** | 1 nginx `stream` block + reload | `openssh`, `sshd_config.d/ra.conf`, session keys | libssh2/MINA client, `-R` forward | **~0.5–1 day** infra | | **WireGuard outbound** | UDP DNAT + firewall + persist | `wireguard-tools`, `wg0`, dynamic peers | `wireguard-android` + `VpnService` | **~1–2 days** infra | | **Custom file API only** | none (HTTP already proxied) | PHP endpoints only | OkHttp upload/download | **~0 FE days** | #### SSH bastion — minimal ordered checklist **FE (Gentoo):** ```bash # 1. Edit nginx — add stream {} → 10.7.16.128:22 (see Q1) nginx -t && nginx -s reload # 2. Firewall — allow inbound TCP on chosen RA port (443 or 2222) # (example — adjust for your firewall frontend) iptables -A INPUT -p tcp --dport 2222 -j ACCEPT ``` **BE (Alpine):** ```bash apk add openssh mkdir -p /etc/ssh/sshd_config.d # ra.conf Match User ra-* (see Q1) rc-service sshd restart # Per session (automate in PHP): # echo "" >> /home/ra-/.ssh/authorized_keys # adduser -D -s /bin/sh ra- ``` **App (Android):** foreground service → heartbeat poll → on `connect`, run embedded SSH client with `-R 127.0.0.1::127.0.0.1:8022` → local SFTP server or `internal-sftp` on device side (design choice). **Operator:** ```bash # Via existing jump, then BE localhost forward: ssh -J f0xx.org:222 sftp -P 18022 ra-@127.0.0.1 ``` #### WireGuard — minimal ordered checklist **FE (Gentoo):** ```bash BE_IP=10.7.16.128 WG_PORT=51820 # nft DNAT rules (Q1) + persist + open UDP WG_PORT on edge ``` **BE (Alpine):** ```bash apk add wireguard-tools # wg0.conf + wg-quick up wg0 # per session: wg set wg0 peer ... allowed-ips ... ``` **App (Android):** `VpnService` + userspace WG → outbound to `apps.f0xx.org:51820`. **Operator (files still need a channel):** ```bash # Option A — add operator as second WG peer (PHP generates config) # Option B — SSH/SFTP over WG inner IP (still need sshd on device or BE hop) wg-quick up operator-ra- sftp user@10.66.66.2 ``` WG **alone** does not give SFTP; you almost always add **SSH on top** or fall back to **Custom file API** — extra moving parts vs SSH-only track. --- ### Q5 — Bastion: file transfers only, or other operations too? | Capability | Custom file API | SSH/SFTP bastion | WireGuard alone | WG + SSH on tunnel | |---|---|---|---|---| | Edit `codecs.json` | Yes (scoped paths) | Yes (SFTP) | No | Yes | | Pull log files | Yes | Yes | No | Yes | | Interactive shell (`logcat`, `run-as`, debug cmds) | **No** | **Yes** (if not `ForceCommand internal-sftp`) | No | Yes | | Port forwarding (ADB-like / local tools) | No | **Yes** (`-L`/`-R`) | Yes (L3) | Yes | | Packet capture / low-level network | No | Limited | Yes (L3 VPN) | Yes | **What “bastion” means in this project:** 1. **Narrow bastion (file-only v1):** `ForceCommand internal-sftp`, chroot or path jail to app home — enough for **codecs.json + logs** with smallest attack surface. Matches your stated primary need. 2. **Full debug bastion:** interactive shell + reverse forwards — for **logcat**, running one-off commands, inspecting `/data/data/...` (where policy allows), or forwarding a port to a laptop tool. Same SSH track, one config flag difference. 3. **WireGuard “bastion”:** really a **VPN endpoint**, not a file bastion — gives IP reachability; you still mount **SFTP/SSH or file API** for codecs.json unless you build a custom agent. **Practical recommendation for your decision:** | Your priority | Best fit | |---|---| | **Only** replace `codecs.json` + download logs | **Custom file API** or **SFTP-only SSH** — bastion = file transfer only | | Occasional **shell debug** on one field device | **Reverse SSH bastion** with shell allowed (not SFTP-only) | | Lowest FE friction + familiar ops | **Reverse SSH** (`stream` to BE) — scores **Proxyable 5** | | Minimum bytes on wire / best battery on tunnel | **WireGuard** — but budget **+SSH or file API** for files | **Rev. 3 decision hint:** If bastion = **files only**, Custom file API + existing `:80` heartbeat is the shortest path (no FE `stream`, no UDP DNAT). If bastion = **files + shell debug**, **reverse SSH** wins on infra fit. Pure **WG** only wins if you explicitly want L3 VPN and accept FE UDP DNAT + a second protocol for files. --- ## Appendix 1 — WG test configuration (Linux laptop) _References: [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md), [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md)._ **DNS (prod):** `ra.apps.f0xx.org` is a **CNAME** of `apps.f0xx.org` / `f0xx.org` — same public edge IP for HTTPS heartbeats and UDP WireGuard. ### Network topology (v1 WireGuard) ```text ┌─────────────────┐ HTTPS :443 ┌──────────────────┐ HTTP :80 ┌─────────────────┐ │ Android device │ ───────────────────► │ Gentoo FE │ ────────────► │ Alpine BE │ │ or Linux laptop │ apps.f0xx.org │ apps.f0xx.org │ artc0:80 │ PHP heartbeat │ │ (device sim) │ /crashes/api/ │ TLS + nginx │ │ remote_access │ └────────┬────────┘ heartbeat.php └────────┬─────────┘ └────────┬────────┘ │ │ │ │ UDP :51820 │ UDP DNAT :51820 │ wg0 :51820 │ ra.apps.f0xx.org (CNAME) ▼ ▼ └──────────────────────────────► ┌──────────────────┐ ┌─────────────────┐ │ Ephemeral modem │ ── LAN ─────► │ 10.7.16.128 │ │ (RPi NAT/fw) │ │ ListenPort 51820│ └──────────────────┘ └─────────────────┘ Internet / global network ───────► modem ───────► FE ───────► BE ``` **Control plane** = HTTP only (no new nginx `location` for RA). **Data plane** = UDP to `ra.apps.f0xx.org:51820` → FE DNAT → BE `wg0`. ### Prerequisites (laptop) ```bash # Gentoo / Debian / Fedora — wireguard-tools + jq + curl sudo emerge -av net-vpn/wireguard-tools # or: apt install wireguard-tools jq curl ``` ### A. HTTP E2E (no tunnel yet) ```bash cd examples/crash_reporter/backend export CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes export ADMIN_USER=admin ADMIN_PASS='…' ./scripts/ra_e2e_cli.sh # expect: whitelist → open_session → connect (may wait ~45s for min_poll_interval_s) ``` ### B. UDP / WireGuard from laptop (after open session) **Option 1 — device sim with tunnel bring-up:** ```bash export CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes export RA_DEVICE_ID=ra-laptop-$(hostname -s) # Operator: whitelist device + open session in ?view=remote_access (or re-run E2E) ./scripts/ra_device_sim.sh poll --until connect --apply-wg ``` **Option 2 — manual:** ```bash source scripts/ra_lib.sh RESP="$(ra_ra_post enable "$RA_DEVICE_ID" "$(cat /tmp/androidcast-ra-${RA_DEVICE_ID}.random)" cli-sim)" ra_wg_quick_from_connect "$RESP" | sudo tee /tmp/ra-laptop.conf sudo wg-quick up /tmp/ra-laptop.conf sudo wg show ping -c3 10.66.66.1 # BE wg0 — OK if routed sudo wg-quick down /tmp/ra-laptop.conf ``` ### C. Quick checks before blaming WG | Check | Command | Expect | |-------|---------|--------| | CNAME | `dig +short ra.apps.f0xx.org` | same A/AAAA as `apps.f0xx.org` | | UDP port | `nc -vzu ra.apps.f0xx.org 51820` | open / reachable (not TCP) | | HTTP | `curl -sS -o /dev/null -w '%{http_code}\n' https://apps.f0xx.org/app/androidcast_project/crashes/` | 200 or 302 | | BE wg | on BE: `wg show wg0` | interface up, peers after connect | If HTTP E2E passes but `wg-quick up` fails → FE DNAT or firewall on modem/FE, not PHP. --- ## Appendix 2 — WG production configuration _References: [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) § Production deploy, [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md)._ ### BE `config.php` (`remote_access`) ```php 'remote_access' => [ 'wg_endpoint' => 'ra.apps.f0xx.org:51820', 'wg_server_public_key' => '', 'wg_interface' => 'wg0', 'provision_peers' => true, 'require_wg_tools' => true, 'session_ttl_s' => 3600, 'min_poll_interval_s' => 45, ], ``` ### BE (Alpine) — WireGuard interface ```bash apk add wireguard-tools wireguard-tools-wg # /etc/wireguard/wg0.conf — example skeleton: # [Interface] # Address = 10.66.66.1/24 # ListenPort = 51820 # PrivateKey = sudo wg-quick up wg0 sudo wg show wg0 public-key # → paste into config.php ``` PHP-FPM user (`nginx` on Alpine) must run `wg set` when `provision_peers=true`: ```bash sudo -u nginx wg show wg0 ``` Cron (hourly purge): `php81 scripts/purge_remote_access.php --hours=24` ### FE (Gentoo) — UDP DNAT (not nginx) HTTP stays as today: `https://apps.f0xx.org` → `http://artc0.intra.raptor.org:80`. WireGuard uses **UDP DNAT** on the FE edge (and/or modem) — **not** an nginx `location`: ```bash BE_IP=10.7.16.128 # Alpine BE on LAN WG_PORT=51820 # nftables example (adjust iface names): # table ip nat { # chain prerouting { # type nat hook prerouting priority dstnat; # udp dport $WG_PORT dnat to $BE_IP:$WG_PORT # } # } ``` Ensure **UDP $WG_PORT** is allowed on FE (and modem) toward the BE. `ra.apps.f0xx.org` CNAME ensures devices and laptops target the same edge as `apps.f0xx.org`. ### MariaDB / migrations Migration **007** (`remote_access_*` tables). See `sql/migrations/007_remote_access.sql`. ### Production verify (on BE or laptop) ```bash cd examples/crash_reporter/backend BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/verify_remote_access_prod.sh RA_E2E=1 BASE="$BASE" ./scripts/verify_remote_access_prod.sh ``` **Verified 2026-06-04:** HTTP `ra_e2e_cli.sh` on prod returned `connect` with live `peer_public_key` and endpoint `ra.apps.f0xx.org:51820`. UDP handshake is validated separately (Appendix 1 § B). ### Android (field device) Developer settings → Remote access → **WireGuard**; polls `…/api/heartbeat.php` (`type: ra`). See `REMOTE_ACCESS_IMPL.md` § App. --- ## Source linkage - Editable source: `docs/20260602_REVERSE_SSH_proposals_summary.md` - PDF: `docs/20260602_REVERSE_SSH_proposals_summary.pdf` - Regenerate: `bash scripts/build-reverse-ssh-proposals-pdf.sh` - All `docs/*.md` PDFs: `bash scripts/build-all-docs-pdf.sh`