# 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.