feat: add netstable tester
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# NetStable <version>
|
||||
|
||||
## 下载
|
||||
|
||||
- `netstable-<version>-linux-amd64.tar.gz`: x86_64 Linux 服务器
|
||||
- `netstable-<version>-linux-arm64.tar.gz`: ARM64 Linux 服务器
|
||||
- `checksums.txt`: SHA256 校验和
|
||||
|
||||
## 快速启动
|
||||
|
||||
```bash
|
||||
tar -xzf netstable-<version>-linux-amd64.tar.gz
|
||||
chmod +x netstable
|
||||
./netstable -listen 0.0.0.0:18080 -data /var/lib/netstable/records.jsonl -bandwidth-limit-mbps 30
|
||||
```
|
||||
|
||||
CLI 客户端测试:
|
||||
|
||||
```bash
|
||||
./netstable client -server http://<server-ip>:18080 -duration 30
|
||||
```
|
||||
|
||||
## 校验
|
||||
|
||||
```bash
|
||||
shasum -a 256 -c checksums.txt
|
||||
```
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 单用户实时测速锁。
|
||||
- 理论最高带宽限制和页面展示。
|
||||
- 保存脱敏后的用户 IP、地区、运营商、测试摘要和完整样本曲线。
|
||||
- SSE 心跳辅助连接用于显示连接状态。
|
||||
- 内置 ip2region IPv4 数据库,优先离线解析地区和运营商。
|
||||
- 嵌入式 Web 页面,无需 Node、PHP 或数据库。
|
||||
@@ -0,0 +1,117 @@
|
||||
# Network Stability Tester Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a self-contained Go web app that tests browser-to-server upload/download speed and latency in real time, allows only one active tester, supports a startup Mbps limit, and stores all user records with client metadata and sample curves.
|
||||
|
||||
**Architecture:** A Go HTTP server serves an embedded static dashboard, JSON APIs, bandwidth-limited generated download traffic, upload sink traffic, and completion persistence. Rate limiting, IP metadata lookup, global active-test locking, and JSONL persistence are isolated in focused packages so behavior can be tested without a browser.
|
||||
|
||||
**Tech Stack:** Go standard library, static HTML/CSS/JavaScript browser fetch transfers, JSON Lines persistence, ipapi-compatible best-effort geolocation.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Project Skeleton And Probe Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `go.mod`
|
||||
- Create: `internal/probe/probe.go`
|
||||
- Create: `internal/probe/probe_test.go`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for target parsing and summary calculation**
|
||||
|
||||
Create `internal/probe/probe_test.go` with tests for `ParseTarget`, `Summarize`, packet loss, jitter, and stability score behavior.
|
||||
|
||||
- [ ] **Step 2: Run probe tests and verify they fail**
|
||||
|
||||
Run: `go test ./internal/probe`
|
||||
Expected: failure because `internal/probe` does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Implement minimal probe types and calculations**
|
||||
|
||||
Create `go.mod` and `internal/probe/probe.go` with target parsing, sample types, real TCP/HTTP probe runner, summary types, and summary calculations.
|
||||
|
||||
- [ ] **Step 4: Run probe tests and verify they pass**
|
||||
|
||||
Run: `go test ./internal/probe`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: JSONL Record Store
|
||||
|
||||
**Files:**
|
||||
- Create: `internal/store/store.go`
|
||||
- Create: `internal/store/store_test.go`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for append and list behavior**
|
||||
|
||||
Create store tests that append complete records to a temp JSONL file, reload them, verify newest-first ordering, preserve client metadata and sample curves, and ignore blank lines.
|
||||
|
||||
- [ ] **Step 2: Run store tests and verify they fail**
|
||||
|
||||
Run: `go test ./internal/store`
|
||||
Expected: failure because `internal/store` does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Implement JSONL store**
|
||||
|
||||
Create `internal/store/store.go` with an append-only file store guarded by a mutex.
|
||||
|
||||
- [ ] **Step 4: Run store tests and verify they pass**
|
||||
|
||||
Run: `go test ./internal/store`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Web API And SSE
|
||||
|
||||
**Files:**
|
||||
- Create: `internal/web/server.go`
|
||||
- Create: `internal/web/server_test.go`
|
||||
|
||||
- [ ] **Step 1: Write failing handler tests**
|
||||
|
||||
Create tests for `/api/records`, `/api/tests` validation, successful test creation, busy response when a test is active, lock release after completion, client metadata persistence, and SSE event streaming with an injected fake runner.
|
||||
|
||||
- [ ] **Step 2: Run web tests and verify they fail**
|
||||
|
||||
Run: `go test ./internal/web`
|
||||
Expected: failure because `internal/web` does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Implement web server**
|
||||
|
||||
Create routes, JSON helpers, single-active-session state, client IP extraction, resolver injection, and SSE streaming. Inject the runner function so tests do not depend on real network timing.
|
||||
|
||||
- [ ] **Step 4: Run web tests and verify they pass**
|
||||
|
||||
Run: `go test ./internal/web`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 4: Static Dashboard And Entrypoint
|
||||
|
||||
**Files:**
|
||||
- Create: `web/static/index.html`
|
||||
- Create: `web/static/styles.css`
|
||||
- Create: `web/static/app.js`
|
||||
- Create: `web/assets.go`
|
||||
- Create: `cmd/netstable/main.go`
|
||||
- Create: `README.md`
|
||||
- Create: `Makefile`
|
||||
- Create: `.gitignore`
|
||||
- Create: `deploy/netstable.service`
|
||||
|
||||
- [ ] **Step 1: Add browser dashboard**
|
||||
|
||||
Create a focused operational UI with target controls, live metrics, busy feedback, event log, live curve, historical time-series chart, and all user records.
|
||||
|
||||
- [ ] **Step 2: Add command-line entrypoint**
|
||||
|
||||
Create `cmd/netstable/main.go` with flags for listen address, data file, static asset directory, and IP metadata resolver settings.
|
||||
|
||||
- [ ] **Step 3: Add README deployment instructions**
|
||||
|
||||
Document local use, Linux build, upload, and systemd service setup.
|
||||
|
||||
- [ ] **Step 4: Run full verification**
|
||||
|
||||
Run: `go test ./...`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `go run ./cmd/netstable -listen 127.0.0.1:18080 -data /tmp/netstable-records.jsonl`
|
||||
Expected: server starts and responds at `http://127.0.0.1:18080/`.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Network Stability Tester Design
|
||||
|
||||
## Goal
|
||||
|
||||
Build a self-contained Go web service for testing upload, download, and latency between a browser user and the deployed server. A user opens the page, starts a live test, watches browser-measured speed and latency update in real time, sees the configured theoretical bandwidth limit, and reviews all historical test records from the same page. The same binary also provides a CLI client mode so other servers can run direct HTTP upload/download tests against the deployed node.
|
||||
|
||||
## Selected Approach
|
||||
|
||||
Use a single Go binary with no external runtime dependencies. The server exposes HTTP endpoints for browser download/upload traffic, serves an embedded static browser UI, embeds the ip2region IPv4 database for offline client region and ISP lookup, throttles generated test traffic when a startup Mbps limit is configured, and persists completed browser results as JSON Lines on disk.
|
||||
|
||||
This is preferred over PHP/LibreSpeed because it is easier to deploy on a weak network: upload one binary, start one service, and keep all behavior under our control.
|
||||
|
||||
## Scope
|
||||
|
||||
The first version enforces one active test at a time while still allowing other users to submit requests and receive a busy response. It measures:
|
||||
|
||||
- Browser download throughput from the deployed server.
|
||||
- Browser upload throughput to the deployed server.
|
||||
- Browser HTTP latency to the deployed server.
|
||||
- Failure rate approximation from failed browser transfer samples.
|
||||
- Jitter from the change between consecutive successful latency samples.
|
||||
- A stability score derived from success rate, latency, jitter, and failures.
|
||||
- Per-run event samples, summarized historical records, and saved user curves.
|
||||
- Anonymized client IP, best-effort region, city, country, ASN, and ISP/operator metadata.
|
||||
|
||||
## Architecture
|
||||
|
||||
The application is split into small Go packages:
|
||||
|
||||
- `internal/probe`: target parsing, sample types, summary types, and legacy probe helpers.
|
||||
- `internal/limit`: Mbps-to-byte-rate conversion and throttling delay calculations.
|
||||
- `internal/store`: append-only JSONL persistence and record loading for complete records.
|
||||
- `internal/web`: HTTP routes, global active-test lock, browser download/upload endpoints, completion persistence, static asset serving, API validation.
|
||||
- `internal/geo`: offline ip2region lookup first, then best-effort online IP metadata fallback through ipapi/ipinfo-compatible JSON.
|
||||
- `cmd/netstable`: command-line entrypoint for server startup and CLI client mode.
|
||||
- `internal/client`: CLI-compatible client runner that uses the same `/api/tests`, `/api/download`, `/api/upload`, and completion endpoints as the browser.
|
||||
- `web`: embedded static asset package.
|
||||
|
||||
The frontend lives in `web/static` and uses plain HTML, CSS, and JavaScript. It starts tests through `/api/tests`, continuously downloads bytes from `/api/download` for the configured phase duration, continuously uploads bytes to `/api/upload` for the same duration, submits results through `/api/tests/{id}/complete`, keeps an auxiliary SSE heartbeat open through `/api/health/events`, loads configuration from `/api/config`, loads records from `/api/records`, draws a live speed curve, and draws a historical time-series chart.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. The browser posts a test request with the current deployment host, per-phase duration, and timeout.
|
||||
2. The server checks the global active-test lock. If another test is active, it returns `409` with a wait message.
|
||||
3. The server captures client IP from `RemoteAddr` by default, or from proxy headers only when explicitly trusted, enriches it through the configured resolver, stores and returns an anonymized IP, creates a test session, and returns a session ID.
|
||||
4. The browser measures latency, continuously downloads generated bytes for the configured duration, then continuously uploads generated bytes for the same duration using that active session ID.
|
||||
5. The browser submits all samples to `/api/tests/{id}/complete`.
|
||||
6. The server writes a completed record containing client metadata, summary, and samples to the JSONL store.
|
||||
7. The server releases the active-test lock.
|
||||
8. The browser refreshes the record table and displays the latest summary.
|
||||
|
||||
The CLI client follows the same server API and lock behavior, but measures from the machine where the CLI is running instead of a browser tab.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Invalid targets, bad URLs, negative durations, and unsupported methods return JSON errors. Busy requests return `409` with a Chinese wait message. Browser transfer failures are saved as failed samples instead of crashing the page. IP metadata lookup failures fall back to the raw IP. Store write failures are reported through the completion endpoint and server logs.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests cover target parsing, summary calculation, IP resolver mapping, JSONL store behavior, active-test locking, download/upload endpoints, completion persistence, static file serving, and API validation. Handler tests use `httptest` with fake stores and runners. End-to-end verification starts the Go server locally and checks that the UI, API, browser transfer flow, busy response, and records respond.
|
||||
Reference in New Issue
Block a user