1
0
mirror of git://f0xx.org/ac/ac-docs synced 2026-07-29 02:58:15 +03:00
Files
ac-docs/REMOTE_ACCESS_IMPL.md
Anton Afanasyeu 69a448f156 initial
2026-06-23 12:20:43 +02:00

249 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Remote access — implementation notes (v1 WireGuard)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-03 |
| Last modification date | 2026-06-03 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md).
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [App (developer settings)](#app-developer-settings)
- [Android VPN consent (WireGuard)](#android-vpn-consent-wireguard)
- [Production deploy (no new nginx HTTP paths)](#production-deploy-no-new-nginx-http-paths)
- [1. Database](#1-database)
- [2. config.php — required keys](#2-configphp-required-keys)
- [3. BE packages and permissions](#3-be-packages-and-permissions)
- [4. Cron (session cleanup)](#4-cron-session-cleanup)
- [5. Verify](#5-verify)
- [6. Browser](#6-browser)
- [WireGuard data plane (UDP, not nginx)](#wireguard-data-plane-udp-not-nginx)
- [VPN process (AIDL)](#vpn-process-aidl)
- [WireGuard third-party (Android)](#wireguard-third-party-android)
- [RSSH alpha (reverse SSH)](#rssh-alpha-reverse-ssh)
- [RA control HTTP (:ra_control)](#ra-control-http-racontrol)
- [Tests](#tests)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## App (developer settings)
- Dropdown: **Disabled** | **WireGuard** (lab) | **RSSH** (alpha — outbound reverse SSH, no VPN)
- **VPN routing (WireGuard only):** route scope **Hub only** (`172.200.2.1/32`, split tunnel) or **All traffic** (`0.0.0.0/0`); app scope **All apps** or **This app only** (`IncludedApplications` / `VpnService.addAllowedApplication`). RSSH ignores these. Prefs sent on heartbeat as `vpn_route_scope` / `vpn_app_scope`; changing them reconnects an active WG session.
- Pref key: `dev_remote_access_mode` (`AppPreferences`)
- **Disabled:** notifies BE (`status:disable`), tears down VPN, stops `RemoteAccessService`
- **WireGuard:** foreground service; jittered poll **17 min**`heartbeat.php` with `type: ra`
- Heartbeat URL derived from crash upload URL (`…/api/heartbeat.php`)
- Session credentials persisted until `expires_at`; expiry tears down tunnel locally
- Userspace WG via `third-party/wireguard-android` (`:tunnel` / `GoBackend`) in isolated process **`com.foxx.androidcast:vpn`**; main app controls via AIDL (`IVpnService` / `VpnServiceClient`); TUN fallback only if GoBackend fails
### Android VPN consent (WireGuard)
WireGuard on Android **must** use `VpnService` (see `DeveloperSettingsActivity``VpnService.prepare()`). This is **not avoidable** on stock devices:
| UX | WireGuard (v1, lab) | RSSH (**alpha essential**) |
|----|----------------|----------------|
| One-time system dialog | Yes — “Allow VPN?” | No VPN permission |
| Status-bar key icon | Yes while tunnel up | No |
| Full-device routing | TUN interface (split routes in config, still a VPN) | No L3 tunnel |
| Outbound from phone | UDP to BE `:45340` | HTTPS poll + outbound SSH to bastion |
There is no supported “hidden VPN” on non-root Android: any TUN creator is classified as a VPN app. **RSSH** (foreground service + existing HTTPS poll + outbound SSH reverse tunnel) is the **alpha deliverable** agreed by project owners — device initiates, operator reaches a forwarded port on the bastion, without VPN consent or status-bar key. See [Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive) and [ROADMAP.md § Remote access](ROADMAP.md#remote-access-alpha--rssh).
Uses the **existing crashes vhost** — no new nginx `location` blocks for HTTP.
| Piece | Path |
|-------|------|
| Migration | `sql/migrations/007_remote_access.sql` |
| Repository | `src/RemoteAccessRepository.php` |
| WG peers | `src/WireGuardPeerProvisioner.php` (`wg set wg0 peer …`) |
| Device API | `public/api/heartbeat.php` (`type: ra`) |
| Admin API | `public/api/remote_access.php` |
| UI | `?view=remote_access` (nav + hub card) |
| RBAC | `remote_access_view`, `remote_access_operate`, `remote_access_admin` |
| RBAC admin UI | `?view=rbac` — global roles (root), company roles, privilege sets |
| Cron | `scripts/purge_remote_access.php` (hourly recommended) |
**RBAC rules (enforced in API)**
| Permission | Scope |
|------------|--------|
| `remote_access_view` | List devices/sessions/events in allowed companies |
| `remote_access_operate` | Open session; close **own** sessions |
| `remote_access_admin` | Whitelist devices; close **any** session in company |
| Global admin / root | All companies; root edits global roles via `?view=rbac` |
Privilege sets (`remote_access_user`, `remote_access_admin`) on `company_memberships.permissions_json` grant extra actions to `member` rows — see crash console README § RBAC.
**Flow**
1. Device polls with `{type:ra, status:enable, capabilities:[wireguard], tunnel_mode:wireguard}`.
2. BE upserts `remote_access_devices`; returns `wait` until device is **whitelisted** and operator **opens session**.
3. Next eligible poll returns `action:connect` + ephemeral WG credentials; BE runs `wg set` when `provision_peers=true`.
4. Disable from app → `status:disable` → closes sessions, removes peers.
**URLs (production)**
- Dashboard: `https://apps.f0xx.org/app/androidcast_project/crashes/?view=remote_access`
- Heartbeat: `…/crashes/api/heartbeat.php`
- Admin API: `…/crashes/api/remote_access.php`
## Production deploy (no new nginx HTTP paths)
After syncing PHP/JS to the BE tree under
`…/androidcast_project/android_cast/examples/crash_reporter/backend/`:
### 1. Database
Migration **007** (already applied on prod per deploy notes). Tables auto-create on SQLite dev; MariaDB should use the migration file once as root.
### 2. `config.php` — required keys
```php
'remote_access' => [
'wg_endpoint' => 'ra.apps.f0xx.org:45340', // public UDP hostname:port (FE DNAT → BE)
'wg_server_public_key' => '…', // wg show wg0 public-key on BE — REQUIRED
'wg_interface' => 'wg0',
'provision_peers' => true, // wg set on connect/close
'require_wg_tools' => true, // reject connect if wg genkey missing
'session_ttl_s' => 3600,
'min_poll_interval_s' => 45, // poll rate limit per device
],
```
**Critical:** `wg_server_public_key` must match the live `wg0` interface. Empty value makes connect credentials unusable on real devices.
### 3. BE packages and permissions
```sh
apk add wireguard-tools wireguard-tools-wg
# wg0 configured separately (Address 172.200.2.1/16, ListenPort 45340, etc.)
# PHP-FPM user must run wg when provision_peers=true (same as CLI test):
sudo -u www-data wg show wg0
```
### 4. Cron (session cleanup)
```cron
0 * * * * cd /var/www/.../backend && php81 scripts/purge_remote_access.php --hours=24
# Orphan wg peers are pruned each run; skip with --no-prune-wg. Manual: php81 scripts/sync_wg_peers.php --dry-run
```
Use `php81` when Alpine default `php` is 8.5+ without the session module; FPM is `php-fpm81`.
### 5. Verify
```bash
cd examples/crash_reporter/backend
chmod +x scripts/ra_*.sh scripts/verify_remote_access_prod.sh scripts/test_remote_access_api.sh
./scripts/verify_remote_access_prod.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/test_remote_access_api.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/ra_e2e_cli.sh
```
**Linux CLI device simulator** (same heartbeat payloads as the app): [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md).
### 6. Browser
Hard-refresh after deploying `public/assets/js/remote_access.js`.
## WireGuard data plane (UDP, not nginx)
HTTP control plane stays on FE→BE `:80`. **WireGuard traffic is UDP** — configure at the **FE firewall/DNAT**, not in nginx:
```text
Internet UDP :45340 → FE DNAT → BE wg0 :45340
```
Hostname `ra.apps.f0xx.org` (or similar) should resolve to the FE public IP. Devices use `wg_endpoint` from config/connect payload.
## VPN process (AIDL)
| Component | Process | Role |
|-----------|---------|------|
| `VpnServiceClient` | main (`com.foxx.androidcast`) | Binds to `IVpnService`; async apply/tearDown + state callbacks |
| `AndroidCastVpnService` | `:vpn` | AIDL stub; worker thread for tunnel ops |
| `WireGuardVpnEngine` | `:vpn` | `GoBackend` + TUN fallback |
| `RemoteAccessVpnService` | `:vpn` | `VpnService` TUN when GoBackend fails |
| `GoBackend$VpnService` | `:vpn` | WireGuard tunnel library VPN entry (manifest merge) |
Developer settings still uses `VpnService.prepare(Activity)` for the one-time consent dialog (same app UID).
**Crash capture:** `CrashReporter.install()` registers uncaught handlers in `:vpn` (scenario `vpn_service`, process `com.foxx.androidcast:vpn`). Reports persist to shared app storage and upload via `:crashwatcher`. Lab: `scripts/simulate-vpn-crashes.sh` (debug APK + `MODE=device`, or `MODE=upload` for BE ingest validation).
## WireGuard third-party (Android)
```bash
bash scripts/init-third-party-submodules.sh # or ./rebuild.sh (includes init + :tunnel build)
./gradlew :tunnel:assembleDebug :app:assembleDebug
```
Gradle fails fast if the submodule is missing. VPN runs in `:vpn` (`AndroidCastVpnService` + AIDL); `WireGuardVpnEngine` uses `GoBackend` with TUN fallback in the same process.
## RSSH alpha (reverse SSH)
| Layer | Component | Notes |
|-------|-----------|--------|
| **App** | `ReverseSshTunnelBridge` | JSch outbound `-R 127.0.0.1:<port>:127.0.0.1:8022` |
| **App** | `RsshLocalSshServer` | Apache MINA SSHD on `127.0.0.1:8022` |
| **BE** | `RsshSessionProvisioner` / `RsshBastionProvisioner` | DB creds; optional Linux user via `rssh_bastion_user.sh` |
| **FE** | nginx `stream` | `nginx/rssh-bastion-stream.conf.example` |
| **Sim** | `examples/rssh/linux-sim/` | Laptop heartbeat + `ssh -R` |
Developer settings → **RSSH** (no VPN dialog). Operator command shown in admin active sessions.
## RA control HTTP (`:ra_control`)
Isolated process: template web UI + sandbox shell when RA session is up. Port/token in `adb.json` `ra_control` block.
## Tests
```bash
./gradlew :app:testDebugUnitTest --tests 'com.foxx.androidcast.remoteaccess.*'
bash examples/crash_reporter/backend/scripts/test_rssh_unit.sh
bash examples/crash_reporter/backend/scripts/test_rssh_api.sh
bash examples/crash_reporter/backend/scripts/test_remote_access_api.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes bash examples/crash_reporter/backend/scripts/test_remote_access_api.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes bash examples/crash_reporter/backend/scripts/ra_e2e_cli.sh
```
CLI validation guide: [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md).