Files
internet_use_control/TRANSPORT_DESIGN.md

158 lines
5.4 KiB
Markdown

# Internet Control — transport design (MQTT vs polling)
**Date:** 2026-10-10
**Question from Sam:** can the internet controls use a broadcast mechanism
(Mosquitto / MQTT) as the source of truth? Devices check in, apply state, and
block on device. Then Harry's phone can be off while his desktop stays on.
---
## Short answer
**Yes — and it is better than polling. But do not make the broker the database.**
Use both, each for its own job:
| Layer | Job |
|---|---|
| **MQTT (Mosquitto)** | Push state, presence, and telemetry. Instant. |
| **HTTP API** | Setup and queries: schedules, blocklists, the portal, the Hub. |
| **The service (Go) + SQLite** | The **source of truth**. MQTT is transport, not storage. |
Mosquitto is already installed. Home Assistant already speaks MQTT. This is proven
ground.
---
## Why MQTT beats the 30-second poll
| Property | Poll every 30 s | MQTT |
|---|---|---|
| Enforcement delay | up to 30 s | instant |
| Presence (online/offline) | must be inferred | free, via Last Will and Testament |
| Reconnect state | next poll | **retained** message delivers it at once |
| Per-device state | possible | natural (one topic per device) |
| Device discovery | service must know addresses | device announces itself |
---
## Topic design
| Topic | Direction | Retained | Payload |
|---|---|---|---|
| `restrict/<device>/config` | service → device | **yes** | desired state |
| `restrict/<device>/status` | device → service | yes | online, app, usage |
| `restrict/<device>/telemetry` | device → service | no | periodic usage |
| `restrict/<device>/cmd` | service → device | no | one-shot command |
| `restrict/<device>/ack` | device → service | no | command result |
### Example — the "phone off, desktop on" case
```
restrict/harry_phone/config {"locked": true, "until": "2026-10-10T21:00Z", "allow": ["spotify"]}
restrict/harry_desktop/config {"locked": false}
restrict/finn_phone/config {"locked": true, "until": "2026-10-10T21:00Z"}
```
Each device subscribes only to its own config. Retained means a phone that
reconnects is locked again **immediately**, with no waiting.
---
## Security (do not skip this)
- TLS, plus a per-device username/password or a client certificate.
- **Topic ACLs.** A device may read only **its own** config and write only **its
own** status. Without this, a child can publish a fake "unlocked".
- Rotate device credentials when a device is removed.
---
## Keep the HTTP API
- Admin portal: schedules, blocklists, usage history.
- The Hub: `/ask` (*"how long have the kids been on screens?"*), `/capabilities`.
- Approval callbacks.
Setup and questions are request/response. That is what HTTP is for. Do not force
them through MQTT.
---
## Enforcement: device only, kids only (corrected)
Rules apply to the **kids' devices only**. Nothing house-wide. No router change.
No Pi-hole. So filtering happens **on the device**:
| Target | How (device-local) |
|---|---|
| An app (YouTube, a game) | Block the app. Android Device Owner `setPackagesSuspended`. Windows process kill. |
| A website by name | A **device-local** filter. Android: a local `VpnService`. Windows: hosts file or a firewall rule. |
| Everything except Spotify | Block all, allow a short list — on the device. |
MQTT carries the commands. The device applies them to itself.
---
## Anti-bypass (honest)
The client runs on the device, so a determined child can interfere. Reasonable
steps, not a fortress:
- **Android Device Owner** — the app cannot be uninstalled or force-stopped.
- **Windows service** with auto-restart and a watchdog.
- A child can buy another SIM. That is accepted. Aim for **reasonable friction**,
not a lockdown.
---
## Fail safe (when a device cannot reach the broker)
The device follows instructions. If it cannot hear the broker, what should it do?
- **Fail-open:** assume "not locked". A child turns off Wi-Fi and plays games.
- **Fail-closed:** assume "locked". A server reboot locks everyone out.
- **Keep last state:** keep following the last instruction. An outage changes
nothing.
**Recommended: keep last state.** Use a **retained** MQTT message, so a
reconnecting device gets the current instruction at once. Add an **admin
override** for real exceptions (travel, holidays).
The open question: after a **very long** outage, does the device still keep the
last state, or does it lock?
---
## The broker
Use the **existing Mosquitto on `.13`**. Do not add a second broker. Isolation is
done with **per-device credentials and topic ACLs**. A second broker is optional
hardening only.
---
## Who publishes config
See `SYSTEM_MAP.md`. Short answer: **the Control Service, and only the Control
Service.** Everything else writes to it over HTTP.
---
## Recommendation
1. SQLite in the service stays the **source of truth**.
2. The service publishes desired state to MQTT (**retained**).
3. Devices subscribe, apply **instantly**, and block locally.
4. Telemetry returns over MQTT; the service writes it to SQLite.
5. HTTP stays for setup and Hub queries.
6. **Do not** make the broker the database.
7. Use a **per-device** topic so state is per device, not per family.
## Open questions
1. Fail safe: when the broker is unreachable, lock or keep the last state?
2. One broker only, or one broker per VLAN? (Mosquitto is single today.)
3. Should device state also appear in Home Assistant (MQTT discovery)?
4. Who publishes config — only the Go service, or also the admin app directly?