1
0
mirror of git://f0xx.org/ac/ac-docs synced 2026-07-29 08:38:53 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-23 12:20:43 +02:00
commit 69a448f156
151 changed files with 33372 additions and 0 deletions

View File

@@ -0,0 +1,394 @@
# URL shortener service — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-11 |
| Last modification date | 2026-06-11 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | DR |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20100611_3_url_shortener.txt](../drafts/20100611_3_url_shortener.txt)
**Specification:** [docs/specs/20100611_3_url_shortener.md](../specs/20100611_3_url_shortener.md)
**Date:** 2026-06-11
**Status:** Reviewed — ready for post-alpha implementation planning
**Severity:** Medium
**Documentation index:** [README.md](../README.md)
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Resolved open questions](#2-resolved-open-questions)
- [3. Authentication and trust](#3-authentication-and-trust)
- [3.1 Bearer token](#31-bearer-token)
- [3.2 Signer](#32-signer)
- [3.3 Shared session (operators)](#33-shared-session-operators)
- [3.4 Trust flow (normative)](#34-trust-flow-normative)
- [4. Slug algorithm](#4-slug-algorithm)
- [4.1 Decision: SHA-256 truncated](#41-decision-sha-256-truncated)
- [4.2 Collision policy](#42-collision-policy)
- [4.3 Draft example (preserved for comparison)](#43-draft-example-preserved-for-comparison)
- [5. API and flow corrections](#5-api-and-flow-corrections)
- [6. Data store](#6-data-store)
- [7. Deployment and infra](#7-deployment-and-infra)
- [8. QR codes](#8-qr-codes)
- [9. Security model](#9-security-model)
- [10. Integration with androidcast](#10-integration-with-androidcast)
- [11. Estimates](#11-estimates)
- [12. Task dependency graph](#12-task-dependency-graph)
- [13. Suggestions (PO / dev discretion)](#13-suggestions-po-dev-discretion)
- [14. Remaining open items](#14-remaining-open-items)
- [15. Changelog](#15-changelog)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Executive summary
The draft defines a **standalone URL shortener** at `https://s.f0xx.org`, separate from the crash-reporter BE app, with bearer/signer trust, TTL/permalinks, QR support, and a JSON shorten API.
**DR outcome:**
| Area | Decision |
|------|----------|
| Priority | **Post-alpha**; medium severity; may move up if BE load stays low |
| Slug hash | **SHA-256 truncated (12 hex)** — reject draft interleave + Java `hashCode` |
| Auth | **Bearer tokens** for API; **signer** for permanent TTL; operator **session RBAC** only for token admin UI |
| Placement | `backend/url-shortener/` scaffold in repo; dedicated nginx vhost; **opt-in** deploy |
| Admin UI | Hub **Short links** card (not separate admin skin) — SPEC §20 |
| Infra | Git remote + FE TLS **live**; nginx snippets in repo |
| Stack | PHP + nginx + MariaDB per draft |
| OpenAPI | **Required** in v1 |
---
## 2. Resolved open questions
Questions from draft §“TBD and open questions list” and inline `TBD` markers.
| # | Draft question / gap | DR answer |
|---|----------------------|-----------|
| Q1 | Token signing, trust models, sessions, security | See [§3](#3-authentication-and-trust) and [§9](#9-security-model). Machine clients use **bearer** (+ optional **signer**). Humans use existing BE session only to **mint** bearers. Optional HMAC for GET left as [suggestion](#13-suggestions-po-dev-discretion). |
| Q2 | `... TBD — open part` | Filled in SPEC: audit log, rate limits, SSRF blocklist, normalization rules, collision policy, admin tables, smoke tests. |
| Q3 | Database `*... TBD` columns | Full schema in [SPEC §9](../specs/20100611_3_url_shortener.md#9-database-schema): `origin_hash`, `expires_at`, `access_count`, FK tables for bearer/signer. |
| Q4 | Java hash vs SHA-256 | **SHA-256** (see [§4](#4-slug-algorithm)). Java hash is 32-bit, non-cryptographic, JVM-stable but poor for URL slugs. |
| Q5 | Interleaved URL+TOKEN string | **Rejected** for production. Deterministic `SHA256(bearer_id ‖ normalized_url)` instead. Draft interleave kept as historical note only. |
| Q6 | Authentication: “app-wide shared session, project-wise token” | **Split roles:** `curl`/services → **project bearer token**; console operators → **shared `ac_crash_sess`** to create/revoke tokens (same RBAC as crashes/tickets). Shorten API does **not** accept PHP session cookies. |
| Q7 | Submodule remote | **Done**`git://f0xx.org/androidcast_project/url-shortener`; pinned at `backend/url-shortener/` submodule (`next`). |
| Q8 | OTA-style “invalid URL” on unrelated services | N/A here; shorten service validates URL scheme/host and returns `INVALID_URL`. |
| Q9 | Step 9.1.1 / 9.1.2 trusted sub-capabilities | `can_temporary` on bearer; `can_permanent` on bearer **and** valid signer required for `ttl=0`. |
| Q10 | Cached hit when TTL “exceeds given limits” | If stored link **expired** → error `TTL_EXPIRED`, not silent renew. If valid → return same slug with **remaining** `ttl` in response (draft step 8). |
| Q11 | Estimations (draft empty) | See [§11](#11-estimates). |
---
## 3. Authentication and trust
### 3.1 Bearer token
- Opaque string (≥32 chars random), stored as **SHA-256 hash** in `bearer_tokens`.
- **`trusted=1`** required to **create** new links (draft steps 67).
- **Demo token:** single `trusted=0` token that can only resolve existing slugs in docs/examples — cannot create (403).
### 3.2 Signer
- Separate long-lived secret for **elevated** operations.
- Required when `ttl=0` (permanent permalink).
- Validated against `signer_keys` where `can_permanent=1` and not revoked.
### 3.3 Shared session (operators)
- **Frozen:** hub **Short links** card (SPEC §20) — same nav shell as Remote access / Builder.
- Actions: create bearer, revoke bearer, list links for bearer, purge expired.
- Uses same RBAC roles as [REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md) pattern (`admin`, `operator`).
### 3.4 Trust flow (normative)
Matches draft low-level flow with SPEC clarifications:
```text
Request → bearer present? ─no→ 401 MISSING_BEARER
→ lookup (bearer, url)
→ hit & valid TTL → 200 cached (remaining ttl)
→ hit & expired → 409 TTL_EXPIRED
→ miss → bearer trusted? ─no→ 403 ACCESS_DENIED
→ ttl=0 → signer ok? ─no→ 403 SIGNER_REQUIRED
→ compute slug, insert, return 200
```
---
## 4. Slug algorithm
### 4.1 Decision: SHA-256 truncated
**Chosen:**
```text
slug = hex(SHA256(bearer_id + "\x1f" + normalized_url))[0:12]
```
| Criterion | Java `hashCode` + interleave (draft) | SHA-256 truncated (chosen) |
|-----------|--------------------------------------|----------------------------|
| Collision resistance | Poor (~32 bits) | ~48 bits with 12 hex chars |
| Determinism | JVM-specified but weak | Standard crypto |
| Reversibility | No (good) | No (good) |
| Debuggability | Opaque interleave | Simple test vectors |
### 4.2 Collision policy
- Unique index on `slug`.
- On duplicate insert: append `-1``-f` (4 chars max) **or** re-hash with `:\x1e{counter}` appended to material.
- Log collision to `audit_log` at `WARN` level.
### 4.3 Draft example (preserved for comparison)
Draft interleaved `https://t.co` + token → `htta5bpesf:4/3/3td.2c3oa` → Java hash or SHA slice `4623b436f`.
**Do not implement interleave** — kept only to show original intent (per-token namespacing). Namespacing is achieved via `bearer_id` in hash input instead.
---
## 5. API and flow corrections
| Draft item | Correction |
|------------|------------|
| `GET https://s.f0xx.org?url=...` | Use **`/api/v1/shorten`** path so root `/{slug}` stays redirect-only |
| Response `ttl` as string | Keep string in JSON for draft compatibility; document integer seconds |
| `signer` on GET | Optional query param; omit from success body (security) |
| Fail body echoes `bearer` | Truncate in response to first 6 + `…` in production logs; full echo in JSON only for client debugging (SPEC) |
**Routing suggestion:** nginx sends `/api/` → PHP front controller; slug paths match regex (quoted on BE: `location ~ "^/[a-f0-9]{6,16}$"`) → redirect handler.
---
## 6. Data store
| Option | Pros | Cons | DR pick |
|--------|------|------|---------|
| A. Same MariaDB instance, new schema `url_shortener` | Simple ops, one backup | Shared failure domain | **Recommended v1** |
| B. Separate MariaDB database | Blast radius isolation | More admin | If PO wants hard isolation |
| C. SQLite on BE | Zero MariaDB migration | Poor fit for BE pattern | **Reject** |
Retention cron: `scripts/purge_url_shortener.php` (mirror `purge_remote_access.php` style) — delete expired rows + audit >90d.
---
## 7. Deployment and infra
Per [INFRA.md](../INFRA.md) topology:
```text
Internet → FE (s.f0xx.org TLS) → proxy_pass BE :80 → url-shortener php-fpm
```
| Step | Owner | Notes |
|------|-------|-------|
| DNS `s.f0xx.org` → FE | PO / infra | **Done** (with TLS) |
| FE TLS cert | PO / infra | **Done**`/etc/letsencrypt/live/s.f0xx.org/` |
| FE nginx `server_name s.f0xx.org` | DEV | [nginx.fe-s.f0xx.org.snippet](../../backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet) |
| BE vhost fragment | DEV | [nginx.be-url-shortener.fragment](../../backend/url-shortener/deploy/nginx.be-url-shortener.fragment) |
| Git remote | PO | **Done**`git://f0xx.org/androidcast_project/url-shortener` |
| Licenses | DEV | Add QR library to BE license inventory when impl starts |
**Safe deploy:** nginx fragment only; no topology changes to router/FE↔BE path. Validate with `nginx -t` before reload.
**Not alpha-blocking:** omit from `validate_be_services.sh` until first deploy.
---
## 8. QR codes
| Decision | Detail |
|----------|--------|
| Must-have | Yes (draft) |
| Format | PNG via `/api/v1/qr/{slug}.png` |
| Library | `endroid/qr-code` (MIT) — **suggestion**; PO approves license addition |
| Inline in JSON | Optional `qr` URL field in shorten success — **suggestion** for v1.1 |
---
## 9. Security model
| Threat | Mitigation |
|--------|------------|
| Token leak | Revoke bearer; rotate signer; rate limit |
| SSRF via shorten | Block private/metadata IPs at validation |
| Slug enumeration | 48+ bits entropy; rate limit redirects (suggestion) |
| Open redirect abuse | Only whitelisted bearers create links |
| Session fixation | N/A — API is stateless |
**Trust model summary:**
```text
┌─────────────────┐
│ Operator (web) │
│ ac_crash_sess │
└────────┬────────┘
│ mint/revoke
┌─────────────────┐
signer ───────►│ bearer_tokens │◄────── curl / services
(permanent) └────────┬────────┘
│ trusted create
┌─────────────────┐
│ links │
└─────────────────┘
```
---
## 10. Integration with androidcast
| Consumer | Use case | When |
|----------|----------|------|
| BE remote-access UI | Short “open session” links for operators | Post-alpha |
| OTA / docs | Stable short links in printed QR | Post-alpha |
| Android app | None v1 — use HTTPS from BE | Deferred |
| Crash reporter | **No** embed — standalone service | Normative |
Submodule lives beside `examples/crash_reporter/backend/` conceptually but **not** inside it.
---
## 11. Estimates
Engineering person-days (single DEV, familiar with BE stack):
| Task | Days |
|------|------|
| Repo scaffold + OpenAPI + DB migrations | 1.5 |
| Shorten API + auth + TTL logic | 2 |
| Redirect handler + permalink cache | 1 |
| QR endpoint + license | 0.5 |
| Admin token UI (minimal) | 1.5 |
| nginx FE/BE fragments + deploy doc | 1 |
| PHPUnit + smoke script | 1 |
| **Total** | **~8.5 days** |
**Severity:** Medium — no alpha path dependency.
**Reorder triggers (PO):** high demand for shareable debug links; BE CPU <50%; remote-access operator UX needs short URLs.
---
## 12. Task dependency graph
Legend: **1** = highest priority within this feature.
```mermaid
flowchart TD
T1["1. Submodule scaffold + openapi.yaml"]
T2["2. MariaDB schema + migrations"]
T3["3. Bearer/signer auth layer"]
T4["4. Shorten API (POST/GET)"]
T5["5. Redirect /{slug} + TTL"]
T6["6. QR PNG endpoint"]
T7["7. Admin token UI (RBAC)"]
T8["8. nginx FE/BE + DNS s.f0xx.org"]
T9["9. Tests + smoke + licenses"]
T1 --> T2
T2 --> T3
T3 --> T4
T4 --> T5
T4 --> T6
T3 --> T7
T5 --> T8
T4 --> T9
T6 --> T9
T7 --> T9
T8 --> T9
```
| # | Task | Blocker | Assignee |
|---|------|---------|----------|
| 1 | Submodule + OpenAPI | — | DEV |
| 2 | Schema | 1 | DEV |
| 3 | Auth | 2 | DEV |
| 4 | Shorten API | 3 | DEV |
| 5 | Redirect | 4 | DEV |
| 6 | QR | 4 | DEV |
| 7 | Admin UI | 3 | DEV |
| 8 | Infra deploy | 5, PO DNS | DEV + PO |
| 9 | Tests/licenses | 47 | DEV |
---
## 13. Suggestions (PO / dev discretion)
Items intentionally **not** frozen in SPEC — recommended but optional:
1. **HMAC signed GET**`&sig=HMAC-SHA256(url|ttl, bearer_secret)` to prevent query tampering on shared links; skip for v1 if all clients use POST.
2. ~~**Console integration**~~**Done (spec):** hub **Short links** card (SPEC §20).
3. **Response field `qr`** — include ready-made QR URL in shorten JSON.
4. **Click analytics** — increment `access_count` only (already in schema); defer graphs to Grafana/nginx logs.
5. **Custom slug**`&alias=` for operators; increases collision/abuse risk — post-v1.
6. **Separate PHP-FPM pool** — if redirect traffic is high; share pool initially.
7. **FE rate limit**`limit_req` on `/api/v1/shorten` at nginx.
8. **Draft demo token** — ship `examples/url-shortener/demo_curl.sh` with documented read-only token for CI smoke.
9. **PDF export** — run `python3 docs/_pdf_build/md_to_pdf.py` on SPEC+DR when PO wants signed-off copies.
10. **Atomic commits on `next`** — per [bottomline_reminder.txt](../../bottomline_reminder.txt): one commit per task row above for easy revert.
---
## 14. Remaining open items
Requires PO / infra input (hard blockers for **prod** only):
| ID | Item | Owner | Status |
|----|------|-------|--------|
| O1 | Git remote `git://f0xx.org/androidcast_project/url-shortener` | PO | **Done** |
| O2 | DNS + TLS for `s.f0xx.org` on FE | PO / infra | **Done** |
| O3 | BE docroot `/var/www/localhost/htdocs/apps/s/` | DEV + PO | **Default in SPEC**; confirm on first sync |
| O4 | Approve QR library license addition | PO | Open (before QR impl) |
| O5 | First production bearer token issuance policy | PO | Open |
| O6 | PHP shorten/redirect handlers + hub Short links UI | DEV | Open (scaffold only) |
Soft blockers cleared for BE nginx + MariaDB local dev. Hard blocker for prod traffic: **O6**.
---
## 15. Changelog
| Date | Rev | Change |
|------|-----|--------|
| 2026-06-11 | 1 | Draft → SPEC + DR; resolved hash, auth, schema, API paths; task graph + estimates |
| 2026-06-11 | 2 | Scaffold `backend/url-shortener/`; openapi.yaml; nginx snippets; curl appendix; O1/O2 done per draft |
**Source linkage:** [docs/drafts/20100611_3_url_shortener.txt](../drafts/20100611_3_url_shortener.txt)

File diff suppressed because one or more lines are too long

676
DRs/20100612_1_scaling.md Normal file
View File

@@ -0,0 +1,676 @@
# Platform scaling and multi-environment architecture — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-12 |
| Last modification date | 2026-06-12 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | DR |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20100612_1_scaling.txt](../drafts/20100612_1_scaling.txt)
**Specification:** [docs/specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md)
**PDF:** [20100612_1_scaling.pdf](20100612_1_scaling.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh` (Mermaid → PNG diagrams; requires `npx`)
**Date:** 2026-06-12
**Status:** Reviewed — planning / brainstorming (no implementation sprint yet)
**Severity:** Strategic
**Documentation index:** [README.md](../README.md)
---
---
---
---
**Documentation index:** [README.md](../README.md)
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Architecture options](#3-architecture-options)
- [3.1 Option A — Dev cluster (draft default)](#31-option-a-dev-cluster-draft-default)
- [3.2 Option B — Tighter cost (minimal split)](#32-option-b-tighter-cost-minimal-split)
- [3.3 Option C — Cloud-first hybrid](#33-option-c-cloud-first-hybrid)
- [3.4 Comparison matrix](#34-comparison-matrix)
- [4. DR decision — recommended path](#4-dr-decision-recommended-path)
- [5. Current vs target diagrams](#5-current-vs-target-diagrams)
- [Today (single BE)](#today-single-be)
- [Target phase B (minimal split + HA web)](#target-phase-b-minimal-split-ha-web)
- [6. Service split rationale](#6-service-split-rationale)
- [7. Environment promotion model](#7-environment-promotion-model)
- [8. RSSH and connection capacity](#8-rssh-and-connection-capacity)
- [Assumptions](#assumptions)
- [Estimates](#estimates)
- [8b. Network bandwidth (DR)](#8b-network-bandwidth-dr)
- [Baseline risk (today)](#baseline-risk-today)
- [DR sizing decisions](#dr-sizing-decisions)
- [Planning numbers (single-site prod, until CDN)](#planning-numbers-single-site-prod-until-cdn)
- [8c. Global availability and GEO segments (DR)](#8c-global-availability-and-geo-segments-dr)
- [Policy model (DR decision)](#policy-model-dr-decision)
- [Phasing](#phasing)
- [Example use cases](#example-use-cases)
- [9. Future capabilities — priority weighting](#9-future-capabilities-priority-weighting)
- [10. Simulation lab design](#10-simulation-lab-design)
- [11. Cloud provider notes (GCP / Azure)](#11-cloud-provider-notes-gcp-azure)
- [12. RBAC evolution](#12-rbac-evolution)
- [13. Risks and mitigations](#13-risks-and-mitigations)
- [14. Estimates](#14-estimates)
- [15. Task dependency graph](#15-task-dependency-graph)
- [16. Resolved open questions](#16-resolved-open-questions)
- [17. Remaining open items (PO)](#17-remaining-open-items-po)
- [18. Suggestions (PO / dev discretion)](#18-suggestions-po-dev-discretion)
- [19. Changelog](#19-changelog)
- [Related docs](#related-docs)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Executive summary
The project outgrew **one Alpine BE VM** that hosts consoles, APIs, MariaDB, Gitea, OTA, WireGuard, RSSH, URL shortener, and Docker builds. The draft asks for **clusters**, **dev/staging/prod**, **zero-downtime deploys**, optional **cloud**, and headroom for **RT cast**, **VPN commercialization**, **payments**, and **ffmpeg** pipelines.
**DR outcome:**
| Area | Decision |
|------|----------|
| **Timing** | **Post-alpha** strategic track; does not block alpha soak |
| **Architecture** | **Option B → A** phased: minimal VM split first, then full dev cluster simulation |
| **Edge** | Keep **Gentoo FE** as TLS/DNAT anchor; add LB upstream to N BE web nodes |
| **Data** | **Dedicated MariaDB VM** before scaling anything else CPU-heavy |
| **Build** | **First split candidate** — Docker off shared PHP VM |
| **Cloud** | **Hybrid** — object store + staging dress rehearsal before prod migration |
| **RSSH capacity** | Plan **100 concurrent SSH** / **5k heartbeats** per bastion pair before commercial VPN |
| **Bandwidth** | Document **WAN / FE→BE / per-service** Mbps & GB-month budgets ([SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress)); CDN before horizontal web scale for OTA |
| **Global + GEO** | **Worldwide reach** default; **deny segments** per content (e.g. no serve in `ZM`) via layered enforcement ([SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) |
| **Future slots** | Weighted backlog in [§9](#9-future-capabilities-priority-weighting); SFU before ffmpeg farm; **F10 GEO** with F3/F5 |
---
## 2. Problem statement
| Pain | Evidence today |
|------|----------------|
| **Blast radius** | Submodule sync wiped gitignored configs; one BE restart affects all services |
| **Resource contention** | Docker build spikes CPU/RAM during PHP console use |
| **No promotion path** | Single prod-like env; staging OTA path still pending ([20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)) |
| **Scaling ceiling** | RSSH + WG + future SFU on same VM as MariaDB |
| **Bandwidth ceiling** | Single FE WAN + FE→BE path; OTA/APK egress competes with consoles ([SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress)) |
| **GEO gap** | No per-content region policy yet; required before global recordings/commerce ([SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) |
| **Operational risk** | Topology changes on FE/router are hard to roll back ([bottomline_reminder.txt](../../bottomline_reminder.txt)) |
**Success criteria:** PO can deploy unstable work to **dev**, beta-test on **staging**, and promote to **prod** with rollback per service — without rewriting mobile apps or public URLs each time.
---
## 3. Architecture options
### 3.1 Option A — Dev cluster (draft default)
Draft proposal: **N VMs per environment**, each role class (web+OTA, DB, object store, WG, url shortener+Gitea, build, +N for new features).
```mermaid
flowchart TB
subgraph Cluster["One environment cluster (e.g. dev)"]
W["Web + OTA VM(s)"]
D["Database VM(s)"]
O["Object store VM(s)"]
V["WireGuard VM(s)"]
U["URL shortener + Gitea VM"]
B["Build VM(s)"]
X["Future service VM(s)"]
end
W --> D
W --> O
U --> D
B --> O
V --> W
```
| Pros | Cons |
|------|------|
| Clear isolation; matches future SFU/ffmpeg | Highest VM count / cost |
| Easy to simulate on FE XEN | Ops overhead for small team |
| Aligns with cloud MIG later | Overkill before alpha exit |
### 3.2 Option B — Tighter cost (minimal split)
**Phase 1 prod shape** (recommended start):
| VM | Bundled services |
|----|------------------|
| **be-web** | nginx, PHP (hub, crashes, graphs, builder **UI**, OTA **metadata**) |
| **be-data** | MariaDB only |
| **be-ops** | Gitea, url-shortener |
| **be-build** | Docker builder |
| **be-vpn** | WireGuard + RSSH bastion |
```mermaid
flowchart LR
FE["Gentoo FE"] --> WEB["be-web"]
WEB --> DATA["be-data"]
WEB --> VPN["be-vpn"]
OPS["be-ops"] --> DATA
BUILD["be-build"] --> DATA
```
| Pros | Cons |
|------|------|
| ~5 VMs vs 7+; fits “money-wise” draft | Gitea + shortener still coupled |
| Unblocks build/DB contention immediately | Second web node needed later for true HA |
| Low risk to current URLs | WG still single point until pool |
### 3.3 Option C — Cloud-first hybrid
Move **staging** entirely to GCP/Azure; keep **prod** on FE→BE until hybrid LB proven.
| Pros | Cons |
|------|------|
| Managed LB, DB, object store day one | Egress cost; UDP/WG complexity |
| Dress rehearsal for full cloud | PO learning curve; vendor lock-in mild |
| ffmpeg/SFU elastic scale | **Violates** “safest topology” unless staged |
### 3.4 Comparison matrix
Scores 1 (poor) 5 (excellent) for **current team size** and **alpha proximity**:
| Criterion | A — Full cluster | B — Minimal split | C — Cloud-first |
|-----------|------------------|-------------------|-----------------|
| Cost now | 2 | **4** | 3 |
| Time to first value | 2 | **5** | 3 |
| Zero-downtime readiness | 4 | 3 | **5** |
| Topology safety | 4 | **5** | 2 |
| SFU/ffmpeg fit | **5** | 3 | **5** |
| Simulation fidelity | **5** | 4 | 2 |
| **Global CDN / GEO** | 3 | 3 | **5** |
| **Weighted total** | 25 | **27** | 25 |
**Weights used:** safety and cost now > cloud elasticity pre-alpha.
---
## 4. DR decision — recommended path
```text
Now ──► Option B (minimal split) on-prem HVMs
├── Simulate Option A as DEV cluster (37 Alpine HVMs)
├── Add be-web-2 + FE upstream when metrics say so
└── Option C for staging only → then hybrid prod
```
| Step | Action | Owner |
|------|--------|-------|
| 1 | Document service boundaries (this SPEC/DR) | Agent / PO review |
| 2 | Move Docker builder to **be-build-1** | Sysop |
| 3 | Move MariaDB to **be-data-1** (hardest cutover) | Sysop |
| 4 | Stand up **dev cluster** simulation on intra network | Sysop |
| 5 | Staging promotion runbook + DNS prefix | PO + DEV |
| 6 | Cloud object store for OTA/APK | Sysop |
| 7 | **CDN + bandwidth dashboards** (SPEC §12.3) | Sysop |
| 8 | SFU SPEC + media workers (separate track) | PO spec |
| 9 | **GEO policy schema + edge rules** (SPEC §14) | PO + DEV |
**Not chosen now:** full Option A in prod; full Option C prod cutover.
---
## 5. Current vs target diagrams
### Today (single BE)
```mermaid
flowchart TB
R["Router 134.17.26.161"] --> FE["FE Gentoo 10.7.0.10"]
FE --> BE["Single Alpine BE 10.7.16.128"]
BE --> S1["PHP consoles + APIs"]
BE --> S2["MariaDB"]
BE --> S3["Gitea + url-shortener"]
BE --> S4["Docker build"]
BE --> S5["WG + RSSH"]
FE -->|"UDP 45340 DNAT"| S5
```
### Target phase B (minimal split + HA web)
```mermaid
flowchart TB
R["Router"] --> FE["Gentoo FE"]
FE --> LB["nginx upstream / cloud LB"]
LB --> W1["be-web-1"]
LB --> W2["be-web-2"]
W1 --> DB["be-data MariaDB"]
W2 --> DB
OPS["be-ops Gitea+shortener"] --> DB
BUILD["be-build Docker"] --> DB
VPN1["be-vpn-1"] --> DB
VPN2["be-vpn-2 optional"]
FE -->|"UDP DNAT pool"| VPN1
FE --> VPN2
```
---
## 6. Service split rationale
Draft asked to balance **cost vs CPU/RAM needs**. DR sizing rules:
| Workload | CPU | RAM | Disk | Pin to |
|----------|-----|-----|------|--------|
| PHP consoles | Medium | 24 GiB | Small | be-web* |
| MariaDB | Medium | **8+ GiB** | **Fast SSD** | be-data |
| Gitea mirrors | Low | 2 GiB | Medium | be-ops |
| URL shortener | Low | 512 MiB | Tiny | be-ops |
| Docker build | **Burst high** | **16 GiB** | Large ephemeral | be-build |
| WireGuard | Low | 1 GiB | Tiny | be-vpn |
| RSSH sshd | Lowmed | 2 GiB | Tiny | be-vpn |
| ffmpeg (future) | **Sustained high** | 832 GiB | Temp SSD | be-media* |
| SFU (future) | **High** | 8+ GiB | Low | be-media* |
| Recordings (future) | Low | — | **Large** | object store |
**Anti-pattern rejected:** MariaDB + Docker build + ffmpeg on one VM (draft implied bundle risk).
---
## 7. Environment promotion model
| Gate | dev → staging | staging → prod |
|------|---------------|----------------|
| Automated tests | `./gradlew :app:testDebugUnitTest` + BE smoke | + soak checklist |
| Manual | PO feature acceptance | PO sign-off + tag |
| Data | Synthetic | Anonymized copy optional |
| Rollback | Re-deploy previous image | Git revert + DB restore runbook |
Align app releases with [GIT_FLOW.md](../GIT_FLOW.md); infra releases may cadence independently with **config version pins** per environment.
---
## 8. RSSH and connection capacity
Draft question: *how many simultaneous connections per instance?*
### Assumptions
- Heartbeat poll **17 min** ([REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md))
- Average SSH session **15 min** operator work
- Alpine **be-vpn-1**: 2 vCPU, 2 GiB RAM
### Estimates
| Resource | Limiting factor | Conservative | With tuning |
|----------|-----------------|--------------|-------------|
| **Concurrent SSH** | `sshd` `MaxSessions` / CPU | 50 | **100** |
| **Devices polling heartbeat** | PHP-FPM + MariaDB writes | 2 000 | **5 000** |
| **New SSH/min** | `MaxStartups` | 10 | 30 |
| **WG peers (lab)** | kernel + UDP | 200 | 500 / 2 nodes |
**Tuning knobs:** separate **heartbeat** PHP pool to **be-web** (off bastion); keep sshd on **be-vpn** only; connection pooling to MariaDB; read replicas for heartbeat read-heavy phases.
**DR recommendation:** Instrument **active SSH** and **heartbeat QPS** in graphs before selling commercial VPN (draft F4).
---
## 8b. Network bandwidth (DR)
Draft rev.2 adds **throughput** alongside connection counts.
### Baseline risk (today)
| Path | Risk |
|------|------|
| Single **WAN** uplink | OTA release or viral short link saturates all services |
| **FE→BE** proxy | All HTTPS doubles through FE NIC |
| No **CDN** | Every APK download hits origin BE |
| **Media future** | SFU + ffmpeg on same uplink = hard fail without PoP/CDN |
### DR sizing decisions
| Decision | Rationale |
|----------|-----------|
| **CDN before be-web-N** for OTA/static | Fixes egress Mbps cheaper than more PHP VMs |
| **Separate media NIC/subnet** when SFU lands | Isolate UDP/WebRTC from MariaDB LAN |
| **Egress budget alerts** at 80% ISP/cloud quota | PO notified before hard cap |
| **Simulate release day** on staging | e.g. 1000 concurrent OTA downloads |
### Planning numbers (single-site prod, until CDN)
| Traffic | Peak Mbps (conservative) | Mitigation |
|---------|--------------------------|------------|
| Steady HTTPS (API + consoles) | 520 | Normal |
| OTA drop (1000 devices × 40 MB APK) | **200400** burst 10 min | **CDN required** |
| Recording download (F3) | 50200 per hot file | Object store + CDN signed URLs |
| SFU fan-out 100 viewers | 50500+ | Regional PoP; not on FE WAN |
Full table: [SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress).
---
## 8c. Global availability and GEO segments (DR)
Draft rev.2: **global reach** + **segment isolation** (example: deny publish/serve in **Zambia `ZM`**).
### Policy model (DR decision)
| Rule | Choice |
|------|--------|
| **Default** | **Allow** worldwide for control plane and public APIs |
| **Content objects** | Attach optional `geo_policy` with `deny_publish` / `deny_serve` / `allow_only` |
| **Enforcement order** | CDN/WAF → BE API (trusted geo header) → object signed URL → app entitlement |
| **Operator UI** | Hub/console editor for geo segments when F3/F10 ship — not alpha |
| **Sanctions** | PO-maintained deny lists; not auto-synced from external API in v1 |
```mermaid
flowchart LR
REQ["Client request"] --> EDGE["CDN / FE edge"]
EDGE --> GEO{"Country in deny_serve?"}
GEO -->|yes| DENY["451 / blocked manifest"]
GEO -->|no| BE["BE + geo_policy DB"]
BE --> OK["200 / stream / redirect"]
```
### Phasing
| Phase | GEO capability |
|-------|----------------|
| **Now** | Implicit worldwide; no per-content rules |
| **CDN phase** | Edge geo block for **coarse** country bans |
| **F3 storage** | Per-recording `geo_policy` in MariaDB + object metadata |
| **F5 payments** | Entitlement includes `geo_allowed` |
| **Multi-PoP** | GeoDNS routes to nearest **allowed** PoP only |
### Example use cases
| Use case | Policy |
|----------|--------|
| OTA `stable` globally | `default: allow` |
| Beta APK EU-only | `allow_only: ["DE","FR",…]` |
| Recording licensed EU/US only | `deny_serve: ["ZM",…]` + `allow_only` optional |
| Short link to blocked catalog | BE returns 451 at redirect time |
**Open for PO:** whether **VPN exit** in denied country circumvents policy (DR: log + ToS; technical block on **VPN product** separate from HTTP geo).
---
## 9. Future capabilities — priority weighting
Weighted for **platform dependency** and **revenue enablement** (PO may override):
| Rank | Slot | Weight | Rationale |
|------|------|--------|-----------|
| **1** | F1 Multipoint cast (SFU/MCU) | Critical path | Unblocks WAN cast; already on [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md) |
| **2** | F2 ffmpeg transcode | High | Shared worker pool for F3, F9 |
| **3** | F3 Cast storage | High | Needs F1+F2 + object store |
| **4** | F5 Payments | Medium | Enables F3, F4, F7 |
| **5** | F4 Commercial VPN | Medium | RSSH/WG scale must be proven first |
| **6** | F9 3rd-party streaming | Medium | Uses F2 cast handlers |
| **7** | F6 Mobile add-ons | Lower | Can ship on-device first |
| **8** | F7 In-app transfers | Lower | Compliance heavy |
| **9** | F8 DNS / subdomains | Exploratory | Largest scope creep |
| **10** | F10 GEO enforcement | Medium | Required for F3/F5 global commerce; draft rev.2 |
```mermaid
flowchart TB
SFU["F1 SFU / multipoint"] --> STORE["F3 Cast storage"]
FF["F2 ffmpeg workers"] --> STORE
FF --> EXT["F9 3rd-party streams"]
SFU --> EXT
PAY["F5 Payments"] --> STORE
PAY --> VPN["F4 Commercial VPN"]
PAY --> XFER["F7 In-app transfers"]
PAY --> ADD["F6 Add-ons"]
DNS["F8 DNS / site builder"] --> PAY
GEO["F10 GEO policy"] --> STORE
GEO --> PAY
```
---
## 10. Simulation lab design
Sysop simulates **Option A** under dev without WAN changes:
| HVM | Alpine packages | Role |
|-----|-----------------|------|
| `dev-web-1` | nginx, php81-fpm | Web tier |
| `dev-data-1` | mariadb | DB |
| `dev-ops-1` | git, gitea (optional) | Gitea + shortener |
| `dev-build-1` | docker | Builder |
| `dev-vpn-1` | wireguard-tools, openssh | VPN lab |
**Network:** `10.7.0.0/8` only; FE routes `dev.apps.f0xx.org` to dev-web when ready.
**Deliverable suggestion:** `docs/SIMULATION_CLUSTER.md` + scripts in phase 1 — not blocking this DR.
---
## 11. Cloud provider notes (GCP / Azure)
| Concern | GCP | Azure |
|---------|-----|-------|
| **Managed LB** | ILB + NEG | App Gateway / Front Door |
| **Object store** | GCS | Blob |
| **Managed MariaDB** | Cloud SQL | Azure Database for MariaDB |
| **UDP / WG** | harder | harder — keep on-prem FE DNAT longer |
| **ffmpeg burst** | MIG (CPU) | VMSS |
| **Egress cost** | Watch OTA/APK | Same |
| **CDN + geo WAF** | Cloud CDN / Cloud Armor | Azure Front Door + rules |
| **Multi-region** | GCS dual-region; Cloud CDN PoPs | Blob + AFD PoPs |
**DR:** Use cloud for **staging cluster first** (Option C partial) — validates IaC without prod risk. **CDN geo rules** prototype on staging before prod F3.
---
## 12. RBAC evolution
| Stage | Who | Access |
|-------|-----|--------|
| **Alpha (now)** | Developers = operators | Max privileges; shared session |
| **Beta** | Split **developer** vs **support operator** | Tickets/RA without build admin |
| **Commercial** | Customer admin + platform admin | Tenant isolation; audit export |
New roles MUST NOT require mobile app changes until F5 entitlements API exists.
---
## 13. Risks and mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| MariaDB migration downtime | Prod outage | Replica + cutover window; practice on dev |
| Config loss on sync | 503 (seen url-shortener) | `ensure_*` scripts; secrets outside tree |
| FE upstream misconfig | Partial outage | `nginx -t`; weighted rollout |
| WG UDP cloud move | VPN breaks | Keep DNAT path until PO signs UDP plan |
| Over-scoping F5F9 | Never ship SFU | [§9](#9-future-capabilities-priority-weighting) order |
| WAN saturation on OTA | Global outage perception | CDN + [§8b](#8b-network-bandwidth-dr) budgets |
| Geo policy bypass via VPN | Compliance / licensing | Separate VPN egress policy; audit logs |
| Wrong deny list | Legitimate users blocked | Staging test + `X-Debug-Country`; PO approval workflow |
| Simulation sprawl | Sysop time | Cap dev HVMs at 7; destroy scripts |
---
## 14. Estimates
Person-days (**single sysop + DEV support**), planning accuracy ±40%:
| Phase | Work | Days |
|-------|------|------|
| **P0** | SPEC/DR + simulation design (this doc) | 2 |
| **P1** | Split Docker build VM | 2 |
| **P2** | MariaDB dedicated VM + migration | 3 |
| **P3** | be-ops split (Gitea + shortener) | 1 |
| **P4** | Dev cluster HVM simulation (5 VMs) | 4 |
| **P5** | Staging env + promotion runbook | 3 |
| **P6** | be-web-2 + FE upstream HA | 2 |
| **P7** | Cloud object store for OTA | 2 |
| **P7b** | CDN + bandwidth metrics on graphs | 2 |
| **P8** | GEO policy schema + staging CDN geo test | 3 |
| **P9** | SFU/media (separate SPEC) | TBD |
| **Total (through P7b)** | | **~21 days** |
**Severity:** Strategic — parallel to alpha; no alpha gate.
---
## 15. Task dependency graph
Legend: **1** = highest priority (PO: post-alpha infra). Assignee **Sysop** = PO today; **Agent** = documentation/automation; **DEV** = app/config changes.
```mermaid
flowchart TB
T1["1. SPEC+DR scaling docs<br/>Agent ✓"]
T2["2. Split be-build Docker<br/>Sysop"]
T3["3. Dedicated be-data MariaDB<br/>Sysop"]
T4["4. ensure_* config hardening all services<br/>Agent"]
T5["5. Dev cluster simulation HVMs<br/>Sysop"]
T6["6. Staging env + DNS prefix<br/>Sysop + PO"]
T7["7. Promotion runbook dev→stg→prod<br/>PO"]
T8["8. be-web-2 + FE upstream<br/>Sysop"]
T9["9. Cloud object store OTA<br/>Sysop"]
T9b["9b. CDN + bandwidth metrics<br/>Sysop"]
T10["10. GEO policy + edge rules<br/>PO+DEV"]
T11["11. SFU owner SPEC<br/>PO"]
T12["12. ffmpeg worker pool<br/>DEV post-SFU"]
T13["13. Payments / entitlements SPEC<br/>PO"]
T1 --> T2
T1 --> T4
T2 --> T3
T3 --> T5
T4 --> T6
T5 --> T6
T6 --> T7
T7 --> T8
T3 --> T8
T8 --> T9
T9 --> T9b
T9b --> T10
T10 --> T11
T11 --> T12
T12 --> T13
style T1 fill:#86efac,color:#14532d
style T11 fill:#e5e7eb,color:#374151
style T12 fill:#e5e7eb,color:#374151
style T13 fill:#e5e7eb,color:#374151
```
| ID | Task | Assignee | Blocks |
|----|------|----------|--------|
| 1 | Scaling SPEC/DR | Agent | — |
| 2 | Docker → be-build | Sysop | DB split |
| 3 | MariaDB → be-data | Sysop | HA web |
| 4 | Post-sync config scripts (all services) | Agent | staging |
| 5 | Dev HVM simulation | Sysop | staging realism |
| 6 | Staging cluster | Sysop | promotion |
| 7 | Promotion runbook | PO | prod multi-node |
| 8 | Second web node | Sysop | cloud OTA |
| 9 | Object store OTA | Sysop | CDN |
| 9b | CDN + bandwidth dashboards | Sysop | GEO test |
| 10 | GEO policy + edge | PO+DEV | F3 commerce |
| 11 | SFU SPEC | PO | media workers |
| 12 | ffmpeg pool | DEV | commercial storage |
| 13 | Payments SPEC | PO | F6F8 |
**Parallel now (non-blocking):** alpha soak, url-shortener ops, Gitea mirrors, ticket **SVC-OTA**.
---
## 16. Resolved open questions
| # | Draft question | DR answer |
|---|----------------|-----------|
| Q1 | How many VMs in a cluster? | **Option B:** 5 prod VMs to start; **dev simulation:** 37 ([SPEC §6.3](../specs/20100612_1_scaling.md#63-reference-cluster-layout-prod)) |
| Q2 | “Other” architecture approaches? | **Option C** hybrid for staging; not prod-first ([§3.3](#33-option-c-cloud-first-hybrid)) |
| Q3 | Cost vs CPU/RAM balance? | Split by [§6](#6-service-split-rationale) profile table |
| Q4 | Seamless scaling / LB? | FE upstream → N PHP nodes; cloud managed LB in phase 5 ([SPEC §9](../specs/20100612_1_scaling.md#9-zero-downtime-and-load-balancing)) |
| Q5 | Simultaneous connections / RSSH? | [§8](#8-rssh-and-connection-capacity) — 100 SSH, 5k heartbeats per bastion pair (tuned) |
| Q6 | Alpine simulation? | [§10](#10-simulation-lab-design) — intra `10.7.0.0/8` HVMs only |
| Q7 | Future MCU/SFU/ffmpeg/storage/VPN/payments? | Platform **slots** F1F9; priority [§9](#9-future-capabilities-priority-weighting) |
| Q8 | RBAC model change? | [§12](#12-rbac-evolution) three-stage |
| Q9 | Keep current topology safe? | **Yes** — [SPEC §18](../specs/20100612_1_scaling.md#18-safety-constraints-current-topology) |
| Q10 | Network bandwidth metrics? | [§8b](#8b-network-bandwidth-dr) + [SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress) — CDN before web horizontal scale |
| Q11 | Global availability + GEO deny (e.g. Zambia)? | [§8c](#8c-global-availability-and-geo-segments-dr) + [SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy) — default allow, per-content deny |
---
## 17. Remaining open items (PO)
| ID | Question | Needed for |
|----|----------|------------|
| O1 | Target **monthly infra budget** for phase B (5 VMs)? | Option A vs B timing |
| O2 | **Staging URL** pattern: `staging.apps.f0xx.org` vs path prefix? | DNS + TLS |
| O3 | **Cloud vendor** preference GCP vs Azure vs none for 12 months? | P7P9 |
| O4 | Accept **maintenance window** for MariaDB cutover (minutes of read-only)? | P2 schedule |
| O5 | **Commercial VPN** — same WG infra or separate product stack? | F4 design |
| O6 | **SFU** — Janus on `:8089` vs greenfield mediasoup? | F1 SPEC ([20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)) |
| O7 | **Payments** — Stripe-only vs multi-provider? | F5 SPEC |
| O8 | **ISP/cloud egress cap** (GB/month) on FE WAN? | [§8b](#8b-network-bandwidth-dr) budgets |
| O9 | **CDN provider** (Cloudflare vs cloud-native vs none 12 mo)? | OTA + GEO WAF |
| O10 | **GEO deny lists** — PO-owned static JSON vs admin UI first? | F10 rollout |
| O11 | Recordings **451** vs silent 404 for denied regions? | UX + compliance |
*When blocked on O1O7, append to `tmp/YYYYMMDD_implementation_summary.pdf` per bottomline_reminder.*
---
## 18. Suggestions (PO / dev discretion)
| # | Suggestion |
|---|------------|
| S1 | Add **infra smoke** to `validate_be_services.sh` per VM role after split |
| S2 | Version **nginx upstream** fragments in `examples/crash_reporter/backend/deploy/` |
| S3 | Use **Docker** for PHP-FPM pools only after MariaDB split — avoid double complexity |
| S4 | Keep **Gitea** on be-ops until mirror count > 10 repos |
| S5 | Document **connection pool** sizes when RSSH device count > 500 |
| S6 | Re-read this DR when **SFU SPEC** lands — revise [§9](#9-future-capabilities-priority-weighting) |
| S7 | Add **WAN Mbps** graph series on FE before next OTA wide release |
| S8 | Staging test matrix: `X-Debug-Country: ZM` for geo deny paths |
---
## 19. Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial DR from draft `20100612_1_scaling.txt`; options matrix; task graph |
| 2026-06-12 | Rev. 2 — bandwidth (§8b), global/GEO (§8c), F10, task graph + open items |
---
## Related docs
- [SPEC — Platform scaling](../specs/20100612_1_scaling.md)
- [INFRA.md](../INFRA.md)
- [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)
- [20260608_ALPHA_PRIORITIES.md](../20260608_ALPHA_PRIORITIES.md)

945
DRs/20100612_1_scaling.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,426 @@
# 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 todays 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 | 23 d |
| P2 | tun2socks + `VpnService` + route prefs | 58 d |
| P3 | Bastion NAT + self-test + docs | 23 d |
| P4 | Unit/integration tests, soak | 35 d |
**Total if pursued:** ~34 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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,375 @@
# Codec2 / ultra-low-bandwidth voice — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R0 |
| Creation date | 2026-06-17 |
| Last modification date | 2026-06-17 |
| Co-authored | Cursor Agent (project assistant) |
| Severity | low |
| State | approved for planning (implementation post-alpha) |
| Document type | DR |
| Pre-requisite to | [ROADMAP.md](../ROADMAP.md) §6.1 Opus/Speex JNI encode/decode; F1 SFU SPEC (post-alpha) |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20260617_codec2_ulbw_voice_integration.txt](../drafts/20260617_codec2_ulbw_voice_integration.txt)
**PDF:** [20260617_codec2_ulbw_voice.pdf](20260617_codec2_ulbw_voice.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh`
**Status:** PO-reviewed (inline draft comments incorporated) — **no native implementation** until Opus/Speex JNI sinks land
**Scope:** Voice-only / ULBW audio cast on P2P (LAN, BT, mesh); future MCU/SFU relay hooks
**Related:** [OPUS_SPEEX_VALIDATION.md](../OPUS_SPEEX_VALIDATION.md) · [ndk/README.md](../../ndk/README.md) · [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) § SFU · [specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md) F1
**Documentation index:** [README.md](../README.md)
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Vocabulary](#3-vocabulary)
- [4. Current state (mobile + BE)](#4-current-state-mobile--be)
- [5. Codec landscape and PO decisions](#5-codec-landscape-and-po-decisions)
- [6. Goals and non-goals](#6-goals-and-non-goals)
- [7. Mobile peer negotiation (today)](#7-mobile-peer-negotiation-today)
- [8. Proposed mobile integration](#8-proposed-mobile-integration)
- [9. Future backend: MCU / SFU](#9-future-backend-mcu--sfu)
- [10. Protocol extensions](#10-protocol-extensions)
- [11. Third-party and licenses](#11-third-party-and-licenses)
- [12. Testing and validation](#12-testing-and-validation)
- [13. Task dependency graph](#13-task-dependency-graph)
- [14. Risks](#14-risks)
- [15. Open questions (remaining)](#15-open-questions-remaining)
- [16. Changelog](#16-changelog)
<!-- /toc -->
---
## 1. Executive summary
**Question:** Should androidcast integrate **Codec2** for ultra-low-bandwidth (ULBW) **voice-only** casts when normal AAC/Opus paths are too heavy for the link?
**Short answer:** **Yes, as a post-alpha product slice** — not a lab experiment. End users on marginal links (BT P2P, slow mesh, very poor LTE) should get **intelligible audio** (“something rather than nothing”). **Codec2 @ 3200 bit/s** (and lower modes where needed) fits that goal better than Opus narrowband @ ~6 kbps + FEC in POs practical experience (lower sustained bandwidth, less audible noise on those links).
**DR decision (R0):**
| Item | Decision |
|------|----------|
| **Primary use case** | **Voice-only** ULBW cast; not the audio leg of full screen cast (8 kHz telephony; no lip-sync with HD video) |
| **Codec2 modes** | **3200 bit/s** default target; **700C** for extreme BT/mesh; user/dev sub-mode selection |
| **MELP / TWELP** | **Out of scope** (patent / slim OSS path) |
| **AAC** | Keep as platform MediaCodec path; no license conflict for screen cast |
| **Opus / Speex** | Finish **JNI encode/decode** first ([ROADMAP.md](../ROADMAP.md) — cross-compile done, sinks **TODO**); negotiation/scaffolding exists today but wire path is **stub → AAC** without `libopus.a` / encode JNI |
| **Codec2 timing** | After **6.1** JNI baseline; behind dev flag until E2E validated |
| **SFU / MCU** | Plan **transcode at ingress** (Codec2 → Opus) for mixed WebRTC rooms; separate F1 SPEC — signaling reuses extended audio caps |
| **MSG_AUDIO_SELECTED** | Add in handshake when Codec2 implementation starts (SETTINGS_VERSION bump) — aligns P2P multicast and SFU data channel |
---
## 2. Problem statement
Screen cast defaults to **AAC** (32128 kbps class). Negotiation already advertises **Opus** and **Speex**, but without native encode/decode the sender often still ships **AAC** while labeling Opus in diagnostics.
For **ULBW** links:
- Opus @ 6 kbps + FEC/NACK can exceed the sustained budget PO has seen on BT/mesh and adds noise versus Codec2-class vocoders.
- Users still want **audio cast** (commentary, coordination, voice relay) when video is impossible — a deliberate **voice-only** mode.
Future **multipoint** (F1 SFU/MCU) cannot rely on pairwise discovery bitmasks alone; room-level **negotiated_audio** must be defined before Codec2 legs join a relay.
---
## 3. Vocabulary
| Term | Meaning |
|------|---------|
| **ULBW** | Ultra-low bandwidth; sustained **&lt; 34 kbps** audio payload target on worst links |
| **Voice-only cast** | No video track; discovery may send `videoCodecs = 0` |
| **Radio mode** | Dev/user profile preferring Codec2 in AUTO when both peers advertise `AUDIO_CODEC2` |
| **P2P negotiation** | `CastProtoHeader.audioCodecs` bitmask + `AudioNegotiator` (today) |
| **Room negotiation** | Signaling API + server-published codec (SFU future) |
| **SFU** | Selective forwarding unit — relay without per-subscriber transcode |
| **MCU** | Mix decode → PCM → encode; needed for heterogeneous legs |
---
## 4. Current state (mobile + BE)
### 4.1 Audio codecs in app
| Layer | Opus / Speex today | Notes |
|-------|-------------------|-------|
| `CastCodecFlags` | `AUDIO_OPUS=4`, `AUDIO_SPEEX=8` | Masks advertised in discovery |
| `AudioNegotiator` | Works | AUTO scoring via `CodecPriorityCatalog` |
| `NativeCodecBridge` | **Probe only** (`nativeIsOpusAvailable`) | No encode/decode JNI in `opus_bridge.c` / `speex_bridge.c` |
| Wire path | **OPUS_STUB / SPEEX_STUB → AAC** | `PassthroughCodecPolicy`, `AudioCodecFactory` |
| Build | `scripts/build-native-codecs.sh` can link `.a` | [ROADMAP.md](../ROADMAP.md): JNI sinks **TODO** |
**Clarification for PO:** Prior agent work completed **negotiation, settings UI, priority catalog, and native autolink scaffolding** — not full Opus/Speex on-the-wire encoding. Task **6.1** remains open for JNI sinks + E2E per [OPUS_SPEEX_VALIDATION.md](../OPUS_SPEEX_VALIDATION.md).
### 4.2 Video vs audio negotiation asymmetry
```mermaid
sequenceDiagram
participant S as Sender
participant R as Receiver
Note over S,R: Discovery: audioCodecs bitmask in header
S->>R: MSG_HELLO, CAST_SETTINGS
S->>R: MSG_CODEC_CAPS (video MIME list)
R->>S: MSG_RECEIVER_CAST_SETTINGS
R->>S: MSG_CODEC_CAPS
R->>S: MSG_CODEC_SELECTED (video MIME only)
Note over S: AudioNegotiator.resolveForSender(peerMask)
Note over R: No MSG_AUDIO_SELECTED today
```
### 4.3 Backend (gray)
- `SfuRelayGate.PREVIEW_ENABLED = false`; `api/sfu_health.php` returns disabled.
- Janus on **:8089** is **other stack** — androidcast signaling stays on `apps.f0xx.org` PHP ([INFRA.md](../INFRA.md)).
---
## 5. Codec landscape and PO decisions
| Codec | Bitrate | Role in androidcast | DR decision |
|-------|---------|---------------------|-------------|
| **Codec2** | 7003200 bps | ULBW voice-only | **Integrate** (post-6.1) |
| **Opus SILK NB** | ~612 kbps | General narrowband when link allows | Finish JNI; keep in AUTO above Codec2 only when bandwidth sufficient |
| **Speex NB** | ~215 kbps | Legacy / fallback | Finish JNI |
| **AAC-LC** | 32+ kbps | Screen cast default | Unchanged |
| **MELP/TWELP** | ~2.4 kbps | — | **Rejected** |
**PO rationale (2026-06-17):** Opus @ 6k + FEC is **heavier and noisier** than Codec2 on target links; Codec2 **3200 bps** meets product goals; intent is **end-user value**, not codec shootout lab work.
**Maintainer risk:** Codec2 upstream is in active re-development; pin submodule tag (e.g. 1.2.0) and mirror on Gitea like `third-party/opus`.
---
## 6. Goals and non-goals
### Goals
- Voice-only cast mode with **Codec2 encode/decode** on Android (NDK).
- Extend **peer negotiation** (`AUDIO_CODEC2 = 16`, optional `MSG_AUDIO_SELECTED`).
- Document **SFU ingress transcode** and room caps for F1 SPEC.
- LGPL compliance + license files.
### Non-goals (R0)
- MELP/TWELP integration.
- Codec2 as default for **screen cast with video**.
- SFU passthrough-only design (closed Android mesh edge case only).
- Alpha ship — hidden until E2E + PO QA.
---
## 7. Mobile peer negotiation (today)
| Step | Mechanism | Codec2 impact |
|------|-----------|---------------|
| Advertise | `CastCodecFlags.localAudioMask()` in discovery | Add bit 16 when native probe passes |
| Resolve | `AudioNegotiator.negotiate(senderMask, receiverMask, pref, settings)` | Add `CODEC2` to enum + `CodecPriorityId` |
| Score | `codecs.json` + `CodecPriorityCatalog` | ULBW profile: CODEC2 lowest `bitrate_score` |
| Wire | Audio frames (today AAC ADTS) | New codec tag or `MSG_AUDIO_FRAME` subtype |
**Gap:** Receiver does not receive explicit audio selection — sender-resolved only. **Must fix** before multicast + SFU (§10).
---
## 8. Proposed mobile integration
### 8.1 Settings and enum
```text
CastSettings.AudioCodec.CODEC2
CastSettings.codec2Mode: 3200 | 1300 | 700C (default 3200 per PO)
CastSettings.VoiceOnlyMode: boolean
```
### 8.2 Native stack
1. `third-party/codec2` submodule + `scripts/init-third-party-submodules.sh`
2. `scripts/build-native-codecs.sh``libcodec2.a`, `ANDROIDCAST_HAVE_CODEC2`
3. JNI: `isCodec2Available()`, encode/decode **8 kHz mono** frames
4. Link via existing `libandroidcast_codecs.so`
### 8.3 Priority (ULBW profile)
Suggested AUTO order when `NetworkAdoption.ESTIMATED` or **radio mode** on:
**CODEC2 → OPUS → SPEEX → AAC → PCM**
### 8.4 Milestones
| # | Deliverable | Depends on |
|---|-------------|------------|
| M1 | Opus/Speex JNI encode/decode + E2E | 6.1 |
| M2 | Codec2 probe + round-trip unit test | M1 |
| M3 | Voice-only UI slice (dev gate) | M2 |
| M4 | `MSG_AUDIO_SELECTED` + SETTINGS_VERSION | M2 |
| M5 | SFU signaling fields in F1 SPEC | Owner SFU spec |
---
## 9. Future backend: MCU / SFU
```mermaid
flowchart TB
subgraph clients [Android clients]
A[Sender A Codec2]
B[Sender B Opus]
C[Receiver WebRTC]
end
subgraph be [BE post-alpha]
SIG[Signaling API PHP]
SFU[Janus / media worker]
end
A --> SIG
B --> SIG
SIG --> SFU
SFU -->|transcode| C
SFU -->|fan-out Opus| B
```
### 9.1 Negotiation shift (P2P → room)
| Layer | P2P today | SFU future |
|-------|-----------|------------|
| Caps | Header bitmask | `join` JSON: `audio_caps.bitmask`, `codec2_modes[]` |
| Pick | `AudioNegotiator` local | Server `negotiated_audio` |
| Auth | PIN / LAN | Session RBAC + room token |
### 9.2 Codec2 on media plane (DR preference)
| Mode | When | CPU on be-media-N |
|------|------|-------------------|
| **Transcode ingress** | Mixed room (Android + browser) | **Recommended** — decode Codec2 → Opus egress |
| Passthrough | All Codec2 clients | Low CPU; no WebRTC viewers |
| MCU mix | Conference voice | High CPU; radio bridge legs only |
**Signaling sketch** (F1 SPEC — not implemented):
- `POST /api/sfu/room``ingress_caps: [opus, codec2]`, `egress_caps: [opus]`
- `POST /api/sfu/room/{id}/join` — returns transport params + `negotiated_audio`
Mobile: `SfuSignalingClient` fills `CastSettings.resolvedAudioCodec` from server; **LAN fallback** if signaling down.
---
## 10. Protocol extensions
### 10.1 `CastCodecFlags`
```text
AUDIO_CODEC2 = 16 // next free bit after SPEEX=8
```
### 10.2 `MSG_AUDIO_SELECTED` (recommended at M4)
```text
payload: uint32 audio_flag (little-endian) + uint8 codec2_mode_id
```
Symmetric with `MSG_CODEC_SELECTED`; reusable on WebRTC data channel.
### 10.3 `CastSettings.SETTINGS_VERSION`
Bump when adding `sfu_room_id`, `relay_mode`, `server_negotiated_audio` (SFU phase).
---
## 11. Third-party and licenses
| Component | License | Action |
|-----------|---------|--------|
| codec2 (libcodec2) | LGPL-2.1 | Submodule + Gitea mirror; dynamic link in `.so` |
| Opus / Speex | BSD | Existing |
| ffmpeg (SFU transcode) | LGPL/GPL | BE media worker SPEC only |
Update mobile `LICENSES` / notices when submodule added ([bottomline_reminder.txt](../../bottomline_reminder.txt)).
---
## 12. Testing and validation
| Phase | Tests |
|-------|-------|
| Unit | `AudioNegotiatorTest` CODEC2 mask; `CastProtoHeader` round-trip |
| Native | JNI c2enc/c2dec parity arm64-v8a |
| E2E | Extend [OPUS_SPEEX_VALIDATION.md](../OPUS_SPEEX_VALIDATION.md) — voice-only, UDP/TCP, diagnostics kbps |
| SFU | Post-alpha: 2× Codec2 Android + 1× WebRTC viewer via transcode |
---
## 13. Task dependency graph
```mermaid
flowchart TB
A61["6.1 Opus/Speex JNI sinks<br/>Developer · P5"]
C2["Codec2 NDK + voice-only<br/>Agent · post-alpha"]
AUDIO_HS["MSG_AUDIO_SELECTED<br/>Agent"]
SFU["F1 SFU SPEC + signaling<br/>PO · post-alpha"]
TRANS["SFU Codec2 transcode worker<br/>BE media"]
A61 --> C2
C2 --> AUDIO_HS
AUDIO_HS --> SFU
SFU --> TRANS
style A61 fill:#b45309,color:#fff
style C2 fill:#7c3aed,color:#fff
style SFU fill:#e5e7eb,color:#374151
```
**Numbered priority (PO overrides AI):**
1. **6.1** — Opus/Speex JNI (alpha LAN quality path; unblocks honest ULBW comparison)
2. **Codec2 ULBW** — this DR (parallel after M1 green)
3. **F1 SFU SPEC** — room negotiation + transcode
4. Alpha blockers **5.1 / 5.2 / 4.rssh** remain above on [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)
---
## 14. Risks
| Risk | Mitigation |
|------|------------|
| Codec2 upstream churn | Pin tag; Gitea mirror |
| 8 kHz-only confuses users expecting music | Voice-only mode UX; no video leg |
| SFU CPU cost | Ingress transcode budget in scaling SPEC |
| Opus/Speex “done” confusion | ROADMAP + this DR state JNI sinks explicitly |
| BT latency 150300 ms | Document; jitter buffer 80120 ms |
---
## 15. Open questions (remaining)
| # | Question | Owner |
|---|----------|-------|
| 1 | SFU commercial story: always transcode vs optional passthrough mesh? | PO |
| 2 | Ship Codec2 in AUTO on ULBW profile or explicit “radio mode” only? | PO + UX |
| 3 | `700C` exposed to end users or dev-only? | PO |
**Resolved by PO (2026-06-17):** voice-only ULBW product intent; Codec2 3200 OK; skip MELP/TWELP; Opus @ 6k insufficient for target links; AAC platform use OK.
---
## 16. Changelog
| Rev | Date | Change |
|-----|------|--------|
| R0 | 2026-06-17 | Initial DR from draft; PO inline comments; Opus/Speex JNI status clarified |

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
# Live Cast Control Plane (P0-P2 kickoff)
## Scope implemented
- New BE API `POST/GET .../api/live_cast.php`:
- `create_intent`, `heartbeat` (60s KA), `join_event`
- `GET action=active|analytics|session|list`
- Public (no login): `session`, `join_event`, education `create_intent`/`heartbeat`
- New BE repository `LiveCastRepository` with auto-schema:
- `live_cast_sessions`, `live_cast_join_events`
- Expiry rule: sessions without heartbeat for 60s become `expired`.
- Short links + QR per session (join + direct) via internal short-links service.
- Analytics payload includes `live_cast` section; shell label **Analytics**.
- Web pages:
- `/live/join` — viewer entry, join_event tracking, player placeholder
- `/live/education` — 5-minute browser screen-share demo
- `?view=live_sessions` — session tree (read-only, auto-refresh)
- Cookie consent banner (`cookie_consent.js`) gates GA4 until user accepts.
- Mobile:
- `LiveCastControlPlane` — start/heartbeat/stop from sender/receiver services
- `LiveCastPeerTracker` — polls `active` sessions; green border on discovery list
## Non-goals (remaining)
- WebRTC/SFU media plane and real HTML5 playback.
- Push notification delivery (Web Push / FCM).
- Gravatar / refined avatar chrome on web lists.
- Short-link click counter migration in url-shortener submodule.
## Data model notes
`live_cast_sessions` stores owner/device/platform, state transitions, short URLs, codec/BW hints.
`live_cast_join_events` stores open/join/leave events with platform/channel/direct flags and basic anon dimensions.
## Follow-up phases
1. Integrate SFU/web viewer when media-plane branch is ready.
2. Browser/mobile notification fan-out on cast start.
3. Gravatar + green-border on web peer lists.
4. Shortener click stats joined into live session analytics.

View File

@@ -0,0 +1,178 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 8 0 R /F4 9 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 762.0394 116.381 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 751.0394 124.813 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 740.0394 108.381 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 729.0394 108.373 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
9 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
10 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R ] /Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
12 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Outlines 15 0 R /PageMode /UseNone /Pages 20 0 R /Type /Catalog
>>
endobj
14 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Live Cast Control Plane \(P0-P2 kickoff\)) /Trapped /False
>>
endobj
15 0 obj
<<
/Count 4 /First 16 0 R /Last 19 0 R /Type /Outlines
>>
endobj
16 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 17 0 R /Parent 15 0 R /Title (Scope implemented)
>>
endobj
17 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 18 0 R /Parent 15 0 R /Prev 16 0 R /Title (Non-goals \(remaining\))
>>
endobj
18 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 19 0 R /Parent 15 0 R /Prev 17 0 R /Title (Data model notes)
>>
endobj
19 0 obj
<<
/Dest [ 10 0 R /Fit ] /Parent 15 0 R /Prev 18 0 R /Title (Follow-up phases)
>>
endobj
20 0 obj
<<
/Count 3 /Kids [ 10 0 R 11 0 R 12 0 R ] /Type /Pages
>>
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 350
>>
stream
Gas309kseb&-h(iregH[(*#.+hJ6skH*ASb@?K(l<nD1gqYN?Oqk95ea?YoVH'0kG$pMp1#M^\c-4JHr/_H4TRc6%:mfd&n>":b;AJ%6OAL]3UZ:uf`isa".?A/Xl:03adIjq0?J%#_TS[?aOQWn-o)_`4O;6gqb''466Z,1a*3+C\3Nr+9`fljW)@:%7/E2KmC2k.NukCWf`/(,Gmf*0+i,i^PaB6$4KVmu;VgN[M"g1N9U^:]BH$WE:RIGa#Bq(U5hHf_Z8[mI8;U"@PW?GC&4FiuH(5<'HMa)JoW!u*;-n@4Hj?Im&rWQ8F1kni'A5TJl-Zm3U2:DYDs,FBteMGNHDF+j~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 491
>>
stream
Gatn"?#SI?'Sc)J/)JHk;CMj0Z$/3N\XKBio4=_R3]-Aa_)%n_qnl0.D/Co"W]Cb`S-.)a4s9?21p:a,HtdQ/,*5(]#V&r-jr;#mMa%S17i$uIQ/VtSer7e`aS3!0nOc=u_.6hl?%H]9l@L_X(AU[N_%9,cB8/B%KU<%g`]Q5=n;'!tA:9=oBRZ`P1pMOB]>QAu48AB"-WG/A1t-r^@Betl_:_#imIgm5;1B;YHgnMV?sLWr3SN>3^@Su0bnV.-ohcXq9etqMeiNSflo!l@mqMejpaS6>NjjcE_qr>ti5DGR1r1gM&80NR+KJG2lB:X_J"r%'P<K:9lf6I11a(a2[e;K[(F:^<hmi+-DH[_&AfB;V\BWrj\#eIr/VHU#D<c_.lhj]"0oSol4FZ(hnD)]l*@'"h,fISSKJ@%FZD0>g`jbh!7eW\^9G^+.`9[Ad_Fm=5<h=g-5D^hg)-+3t68:o.bt0MkmNDPi@.>&E$M(k~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1744
>>
stream
Gatm<D/\-!&H:Nn0d"Lpf\"gn;H9V?Qh7:NHYGgs01aLE*q6W3Y3WLSOS\!Uei&UL&DVet0]66_1G@'1ZWPSLQW:a)$,FLsb\IS;)obg\*5d?t%:"nMr-W#).im4Y%,IKWT'%!)rlCd]FIP0llXs?9N"`F4^"L',bO<pND@a#jFRB7!,4Hk;0gUq3GQQpo0`FFDqEhB&ecrSADR2f/l&N2bp%7pSFC=mni5G1Kpl#Tj\:X>,O&4OdYN`<<?0Cq=qm#tUC(,i*Qn7?2biea40U3SkPh(cYLN^sHJ9C8HTkP%GJM>0,jnQV?=<_Y`3c-L0OCbsu)]B)mdM_Zkr,Y%^H+)nJjPg4'X/E`!Smre0MhZ?pHPY[IMYHs&a'VNEGa\.grDKR.h=q_+eHT783]-K<Kcfo:JgD\\ElQ)ZjK&Zt#.6)tNMo$IM.$uMmIBY@g3bX(0*fMb*Jm$!VGcqhB$2On)p6q^Z0#r;U03$TSk$SR>]s*Y@D^`[M-q?j9P\f9e7hnZo!mAXaf*o(I4uQ[)Y:$0;GMegMAXZI"h+o8/94kU+/Udq9l;$TR:$[mlDN,e]5c;5iPBL*./5</gKdrHGi_lc_0mT8*+COL.!Q,,mNBJbjU%&C#F'ZJ$"&ci:12?bkh=?R!'oe^h.j.1CsE-!"54\8)YWcfr'8DjZ","d$K<6t[Bh]YV67$:[!1R6`\GEUa*>#FXr"4b]HT.aiLu\8L-E.,6e-dB"4oW?k*JfneEMJ6VJP"^3ktn6$5"joT#-5*OCp->_8YaTd[2V#$!dUkA/o(0_@L;"e8h3(CH:;;N8%(:-uoGTP7+5QT2Zk#=%+Z-#VhGnOS8uMcG6&5`p_(PhB@OeqA!.S_G#obrY0OPLaapPP%Qc!:dpS3(-^B'6_f,[jU[@_&6N3I4qiP-3U&B4BY!K'?=nS'H:CL_UGf12_mol'aE\2J:(.L39QSa=J<p'\+0QmH-g>^#obOu^mZFPneZ(8K&PU3R0b_&[q)Pi"F5:M'Q-0l=R7sP1"B!Gih*u]m[M%d>IF0&q7kWng41C%!=:7O!"\io5)9:&K]G`f&lZYdSG;.KN"^bm&/eAO[Xo_090;72qlL$^FlouqIQD-I)N%f&(-&`40^,AZ+SR]$^]CD_CJ,L)p%&khV\k%@.O@DKMc@:m>h709eq\8N8N,eaOTo?N6<!5PMI*1d>3CGH,P`&+'pTnmk$.kN9-7<XQg"rE2qN.sB,Ee-g;F2uiK_gHHb4I5S+;]])f1HufENR'N;7^+q9b>ZKL>HL87k.()/??WW6W]Dhh/^:tXe;]`',f5cf`Im(gqhnH"oT6+#07A#6BoJJ@c;Pnk5X:ZXf*,N0t2AV-[h3.]g+1u#\Jlr_+^lnS-J9><3%*`D&PD;SS.TgdqU4`YBMqt3?UfnoAU]W1pXP]Y2eq0&cA4Vbu;\!Io7!BE+R&1jp9MslMS&1cO;#R4_(f:^0qqdR4A%"1IW[!-;(O9:m@)Xj.<qo9&*bjf;-W2d4J6e,GnWZL=P2!h;3%@=9a-u8n^7.OMmQUU@la@Le-r,am2ujk>VA4#e8="(G=GFU1`p<nI*uc""pT6d7h[hiMkg2RHWqR++;YX:j(t;R//')p$q;d)khU_Fi<^F,O43`e9Y>W>^pG#5!G!(.9riVQ,%+*Wq)C)qP]]Qct$9TY3US$SjP:ra0N>Y1p7X_[IKA6W;d3QKMaZbCZl5DVGY^GL*b\OT/o:<M&gj45>cB;I/~>endstream
endobj
xref
0 24
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000488 00000 n
0000000635 00000 n
0000000782 00000 n
0000000929 00000 n
0000001044 00000 n
0000001149 00000 n
0000001355 00000 n
0000001597 00000 n
0000001803 00000 n
0000001890 00000 n
0000002206 00000 n
0000002280 00000 n
0000002379 00000 n
0000002497 00000 n
0000002608 00000 n
0000002706 00000 n
0000002781 00000 n
0000003222 00000 n
0000003804 00000 n
trailer
<<
/ID
[<c4fec4b99072874c8420e5c9ea80366d><c4fec4b99072874c8420e5c9ea80366d>]
% ReportLab generated PDF document -- digest (opensource)
/Info 14 0 R
/Root 13 0 R
/Size 24
>>
startxref
5640
%%EOF