Files
btest-rs/docs/architecture.md
Siavash Sameni da76c76c93
All checks were successful
CI / test (push) Successful in 2m27s
Update architecture docs: server-pro, Android, CPU platforms, byte budget
Complete rewrite reflecting current state: server-pro module structure,
BandwidthState fields, all 6 build targets, CPU sampling on 5 platforms,
web dashboard API endpoints, test counts, and key design decisions
including inline byte budget and TCP status message scanning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 08:51:07 +04:00

183 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# btest-rs Architecture
## Overview
btest-rs is a Rust reimplementation of the MikroTik Bandwidth Test protocol. It operates in two modes: **server** (accepts connections from MikroTik devices) and **client** (connects to MikroTik btest servers). An optional **server-pro** mode adds multi-user support, quotas, and a web dashboard.
## Module Structure
```
src/
├── main.rs # CLI entry point, argument parsing (clap)
├── lib.rs # Public API (re-exports all modules for tests/pro)
├── protocol.rs # Wire format: Command, StatusMessage, constants
├── auth.rs # MD5 challenge-response authentication
├── ecsrp5.rs # EC-SRP5 authentication (Curve25519 Weierstrass)
├── server.rs # Server mode: listener, TCP/UDP handlers, multi-conn
├── client.rs # Client mode: connector, TCP/UDP handlers, status parsing
├── bandwidth.rs # Rate limiting, formatting, shared BandwidthState, byte budget
├── cpu.rs # CPU sampler (macOS, Linux, Android, Windows, FreeBSD)
├── csv_output.rs # CSV result logging (append-mode, auto-header)
├── syslog_logger.rs # Remote syslog sender (RFC 3164 / BSD format)
├── bin/
│ ├── client_only.rs # Stripped client binary for embedded/OpenWrt
│ └── server_only.rs # Stripped server binary for embedded/OpenWrt
└── server_pro/ # Optional (--features pro)
├── main.rs # Pro CLI: user management, quota flags, web port
├── server_loop.rs # Accept loop with auth, quotas, multi-conn sessions
├── user_db.rs # SQLite: users, usage, ip_usage, sessions, intervals
├── quota.rs # QuotaManager: per-user + per-IP limits, remaining_budget()
├── enforcer.rs # QuotaEnforcer: periodic checks, max_duration, StopReason
├── ldap_auth.rs # LDAP auth scaffold (not yet wired)
└── web/
└── mod.rs # Axum web dashboard: Chart.js, quota bars, JSON export
```
## CLI Output Format
The client outputs one line per second per direction:
```
[ 5] TX 285.47 Mbps (35684352 bytes) cpu: 20%/62%
[ 5] RX 283.64 Mbps (35454988 bytes) cpu: 20%/62% lost: 12
```
Format: `[interval] direction speed (bytes) cpu: local%/remote% [lost: N]`
At test end, a summary line:
```
TEST_END peer=172.16.81.1 proto=TCP dir=both duration=60s tx_avg=284.94Mbps rx_avg=272.83Mbps tx_bytes=2137030656 rx_bytes=2046260728 lost=0
```
## Data Flow
### Server Mode (MikroTik connects to us)
```
MikroTik → TCP:2000 → HELLO → Command [16 bytes] → Auth → Data Transfer
```
1. Server sends HELLO `[01 00 00 00]`
2. Client sends 16-byte command (protocol, direction, tx_size, speeds, conn_count)
3. Auth: none (`01`), MD5 (`02`), or EC-SRP5 (`03`)
4. TCP: data flows on same connection, 12-byte status messages interleaved every 1s
5. UDP: server sends port number, data on UDP, status exchange stays on TCP
### Client Mode (we connect to MikroTik)
1. Connect to MikroTik:2000
2. Read HELLO, send command
3. Auto-detect auth type from response byte, authenticate
4. Start data transfer with status exchange
### Status Message Format (12 bytes)
```
[0x07][cpu:1][pad:2][seq:4 LE][bytes_received:4 LE]
```
- Byte 0: `0x07` (STATUS_MSG_TYPE)
- Byte 1: `0x80 | cpu_percentage` (MikroTik encoding)
- Bytes 4-7: sequence number (little-endian u32)
- Bytes 8-11: bytes received this interval (little-endian u32)
## Threading Model
All I/O is async via tokio. Per-client:
- **TX task**: sends data packets at target rate
- **RX task**: receives data, counts bytes, extracts status messages (TCP BOTH mode)
- **Status loop**: exchanges 12-byte status messages every 1s, prints bandwidth
- **Status reader** (TCP TX-only): reads server's status messages for remote CPU
Shared state via `Arc<BandwidthState>` with atomic counters — no mutexes.
### BandwidthState Fields
| Field | Type | Purpose |
|-------|------|---------|
| `tx_bytes` | AtomicU64 | Bytes sent this interval (reset by swap) |
| `rx_bytes` | AtomicU64 | Bytes received this interval |
| `tx_speed` | AtomicU32 | Target TX speed (dynamic, from server feedback) |
| `running` | AtomicBool | Test active flag |
| `remote_cpu` | AtomicU8 | Remote peer's CPU (from status messages) |
| `byte_budget` | AtomicU64 | Remaining quota bytes (u64::MAX = unlimited) |
| `total_tx_bytes` | AtomicU64 | Cumulative TX (never reset) |
| `total_rx_bytes` | AtomicU64 | Cumulative RX (never reset) |
## Server Pro Architecture
Optional feature (`--features pro`) providing a multi-user public btest server.
```
Accept → IP check → HELLO → Command → Auth (DB) → Quota check → Budget set → Test
QuotaEnforcer (parallel)
- checks every N seconds
- max_duration timeout
- sets running=false on exceed
```
**Byte budget**: Before the test starts, `remaining_budget()` computes the minimum remaining quota across all applicable limits. This is stored in `BandwidthState.byte_budget`. Every TX/RX loop checks `spend_budget()` per-packet — when budget hits 0, the test stops immediately. This prevents quota overshoot even on 10+ Gbps links.
**Multi-connection TCP**: MikroTik sends `tcp_conn_count` connections. The first authenticates and registers a session token. Subsequent connections match by token and join. When all connections arrive, the test starts with per-stream TX/RX tasks.
**Web dashboard** (axum):
- `GET /` — landing page with instructions
- `GET /dashboard/{ip}` — per-IP dashboard with Chart.js graph, session table, quota bars
- `GET /api/ip/{ip}/stats` — aggregate stats JSON
- `GET /api/ip/{ip}/sessions` — session list JSON
- `GET /api/ip/{ip}/quota` — quota usage JSON
- `GET /api/ip/{ip}/export` — full export with human-readable fields
- `GET /api/session/{id}/intervals` — per-second throughput data
## CPU Usage Monitoring
A background OS thread samples system CPU every 1 second:
| Platform | Method |
|----------|--------|
| macOS | `host_statistics(HOST_CPU_LOAD_INFO)` |
| Linux | `/proc/stat` aggregate CPU line |
| Android | `/proc/stat` (same as Linux) |
| Windows | `GetSystemTimes()` FFI |
| FreeBSD | `sysctl kern.cp_time` |
Stored in global `AtomicU8`, included in status messages as `0x80 | percentage`.
## Build Targets
| Target | Binary | Notes |
|--------|--------|-------|
| `x86_64-unknown-linux-musl` | btest | Static, zero deps |
| `aarch64-unknown-linux-musl` | btest | RPi 4/5, ARM servers |
| `armv7-unknown-linux-musleabihf` | btest | RPi 3, OpenWrt |
| `x86_64-pc-windows-gnu` | btest.exe | Cross-compiled |
| `aarch64-linux-android` | btest | Termux ARMv8 |
| `armv7-linux-androideabi` | btest | Termux ARMv7 |
| macOS (native) | btest | Apple Silicon + Intel |
| Docker (multi-arch) | image | amd64 + arm64 |
## Key Design Decisions
1. **Tokio async runtime** — all I/O is async, handles hundreds of concurrent connections
2. **Lock-free shared state** — AtomicU64 counters, `swap(0)` reads and resets per interval
3. **Direction bits from server perspective**`0x01`=server RX, `0x02`=server TX, `0x03`=both
4. **TCP socket half keepalive** — dropping `OwnedWriteHalf` sends FIN, so unused halves are kept alive
5. **Static musl binary** — ~2 MB, zero runtime dependencies
6. **EC-SRP5 with big integer arithmetic** — Curve25519 Weierstrass form via `num-bigint`
7. **Global singletons for syslog/CSV**`Mutex<Option<...>>` statics, initialized once at startup
8. **Shared BandwidthState for timeout survival** — state created in main(), survives tokio cancellation
9. **Inline byte budget** — per-packet quota check with fast path (u64::MAX = unlimited, returns immediately)
10. **TCP status message scanning** — RX loop detects 12-byte status messages in the data stream by scanning for `0x07` marker byte to extract remote CPU
## Tests
| Suite | Count | What |
|-------|-------|------|
| Unit tests (lib) | 12 | Bandwidth parsing, CPU sampling, auth hash vectors |
| Enforcer tests (pro) | 10 | Budget, quota, duration, flush |
| Integration tests | 8 | Server/client handshake, auth, TCP data |
| EC-SRP5 tests | 6 | Full auth flow, wrong password, UDP bidir |
| Full integration | 23 | All protocols × directions, IPv4/6, CSV, syslog, CPU |
| **Total** | **59** | |