diff --git a/apps/edr-freight-api/src/modules/gps-tracking/README.md b/apps/edr-freight-api/src/modules/gps-tracking/README.md new file mode 100644 index 000000000..668b581b8 --- /dev/null +++ b/apps/edr-freight-api/src/modules/gps-tracking/README.md @@ -0,0 +1,150 @@ +# GPS Tracking (GT06) — Operations & Device Configuration + +GT06 trackers speak a **raw TCP binary protocol**, not HTTP/HTTPS. This shapes +everything about how the service is deployed and how devices are pointed at it. + +--- + +## 1. Why GPS needs its own dedicated TCP port + +- **Not HTTP.** GT06 devices send binary frames + (`0x78 0x78 | len | protocol | payload | serial | CRC16 | 0x0D 0x0A`). + An HTTP server receiving these answers `400 Bad Request` and closes. +- **Dedicated port required.** A listening socket is keyed on `(IP, port)`; two + listeners on the same pair collide (`EADDRINUSE`). The REST API already owns + its port, so GPS traffic needs a separate one. +- **No hostname routing.** GT06 frames carry no `Host` header and no TLS SNI, so + L7 proxies (Nginx `http`, AWS ALB, Cloudflare proxy) cannot route them by + domain. Routing must happen at **Layer 4 (TCP)** by port. +- **DNS carries no port.** An A record maps a name to an IP only. The tracker + config must state the port explicitly (e.g. `gps.example.com:5023`). + +### Operational requirements + +| Item | Value | +| --- | --- | +| Protocol | Raw TCP (not HTTP, not TLS) | +| Default port | `5023` (configurable via `GT06_TCP_PORT`) | +| Listener bind | `0.0.0.0` inside the `freight-gps` container | +| Edge terminator | **L4** — AWS NLB or Nginx `stream {}`. **Not** ALB / Cloudflare proxy. | + +--- + +## 2. Port configuration + +`5023` is only this project's default — **not** a GT06 protocol requirement. The +listener binds whatever `GT06_TCP_PORT` says, as long as trackers are configured +with the same number. + +Host and container ports are decoupled in `docker-compose.yaml`: + +```yaml +freight-gps: + ports: + - "${GT06_TCP_PORT:-5023}:5023" # host is configurable; container fixed + environment: + GT06_TCP_PORT: "5023" # pinned inside the container +``` + +- The **container** always listens on `5023`. +- The **host/public** port is configurable (443, 5023, 9000, …) via the root + `.env`'s `GT06_TCP_PORT`. +- This split is required because the image runs as a **non-root** user + (`nestjs`, uid 1001), which cannot bind ports `<1024`. Docker (root) binds the + host port and forwards to `5023` inside. +- Running **outside Docker** (`pnpm dev:gps`, systemd), `GT06_TCP_PORT` is the + actual bind port, so `<1024` needs root or `CAP_NET_BIND_SERVICE`. +- **443 is allowed but risky:** GT06 stays raw TCP, not TLS. Middleboxes that + expect a TLS handshake on 443 may drop the connection. + +--- + +## 3. Deployment topology + +The GT06 listener runs as its own process (`dist/main.gps.js`, module +`GpsIngestModule`) — DB + GPS only, no HTTP server. It shares the `edr_freight` +DB with the API; the DB is the seam (ingester writes `gps_devices` / +`gps_positions`, API reads them). + +``` +freight-api HTTP :3001 GT06_TCP_PORT=0 (listener off, applies migrations) +freight-gps TCP :5023 DB_MIGRATIONS_RUN=false (owns the tracker socket) +``` + +`DB_MIGRATIONS_RUN=false` keeps the second process from racing migrations. + +Horizontal scale: each tracker holds one long-lived TCP connection with +per-socket session state, so N `freight-gps` replicas can run behind an L4 LB — +each device sticks to one replica. `ensureDevice` is safe under concurrency +(unique IMEI). + +--- + +## 4. Device configuration (GT06 side) + +Config is done by **SMS to the tracker's SIM**. Commands below are the canonical +Concox/GT06 set — **verify against your unit's sheet**, syntax varies by firmware. +Default command password is usually `123456`. + +Prep: data-enabled SIM, SMS on, **SIM PIN off**, know your carrier APN. + +``` +STATUS# # 1. sanity check — returns GSM/GPS/batt/GPRS +APN,# # 2. carrier data APN (add ,user,pass if needed) +SERVER,1,gps.example.com,5023,0# # 3. point at server (1=domain). Port MUST match GT06_TCP_PORT +GPRSON,1# # 4. enable data +GPSON,1# # enable GPS +TIMER,10# # 5. upload interval, seconds (some use UPLOAD,10#) +RESET# # 6. reboot so it reconnects (many cache DNS until reboot) +``` + +Raw-IP variant of step 3: `SERVER,0,203.0.113.50,5023,0#` +Custom host port (e.g. 443): `SERVER,1,gps.example.com,443,0#` + +### Verify from the server + +```bash +docker compose logs -f freight-gps | grep -Ei "login|Auto-registering|ingester up" +nc -vz gps.example.com 5023 +curl -H "Authorization: Bearer " https://api.example.com/api/gps/positions/latest +``` + +First login packet **auto-registers** the IMEI (no manual step). `online:true` +only when `lastSeenAt` < 5 min (computed at read time). + +### Link a tracker to a vehicle (optional) + +Auto-register leaves `vehicleId` null. Attach it (needs `tracking.manage`): + +``` +PATCH /api/gps/devices/:id { "vehicleId": "", "name": "Truck 03-ET" } +``` + +### Failure map + +| Symptom | Cause | +| --- | --- | +| No SMS reply | SIM PIN on / no signal / wrong number | +| Replies but never connects | APN wrong, or `SERVER` port ≠ `GT06_TCP_PORT` | +| Connects then drops | server not ACKing, or middlebox on 443 expecting TLS | +| Registered but `online:false` | packets blocked by firewall — open inbound TCP | +| Wrong location / `positioned:false` | no GPS fix yet — open sky, cold start ~1–2 min | + +--- + +## 5. Security + +- GT06 authenticates with **IMEI only**, which is **spoofable**. Anyone who can + reach the port can inject fake positions. +- **Do not** expose the port to `0.0.0.0/0`. Restrict at the firewall / security + group to the SIM provider's **APN / IP range**. +- Trackers must use the same host+port as the server: + `SERVER,1,gps.example.com,,0#`. + +--- + +## 6. Edge (L4) termination + +See [`infrastructure/nginx/gps-stream.conf`](../../../../../infrastructure/nginx/gps-stream.conf) +for an Nginx `stream {}` example, and the AWS NLB notes in the same file. +Reminder: **L4 only** — an HTTP proxy cannot route GT06.