Filters by exe basename, case-insensitive. DiffPIDs reports add/remove
sets so the engine can decide whether to rebuild the WinDivert filter.
Pure syscalls, no third-party dependencies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Separate from internal/checker/socks5.go (different requirements: no
hex dumps, no diagnostic-friendly errors, faster path). Single Dial
entry point that handles greet + optional auth + CONNECT and returns
a ready-to-use net.Conn. UDP support deferred to P2.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin Go layer over imgk/divert-go. Exposes Open/Close/Recv/Send and
maps the most relevant Windows errors to sentinels (ErrAccessDenied,
ErrDriverFailedPriorUnload, ErrInvalidHandle, ErrShutdown) so the
engine's recovery classifier can reason about them without importing
golang.org/x/sys/windows.
Verified imgk/divert-go@v0.1.0 API matches plan; only deviation is
Recv/Send returning uint (cast to int at our boundary).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts embedded WinDivert binaries to %PROGRAMDATA%\Drover\windivert\
on first run; subsequent runs detect matching SHAs and no-op. SHA
mismatch after write produces an AV-friendly error message pointing
the user at adding the directory to exclusions.
ARM64 detected at runtime via runtime.GOARCH and refused gracefully.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-Go RFC 791/793 checksum implementation. Mutates buffer in
place — no allocations on the hot path. Used by the redirect layer
to NAT-rewrite Discord packets to 127.0.0.1:listener_port before
reinjecting via WinDivertSend.
UDP support deferred to P2.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-Go assembly of the WinDivert filter clause. Empty PID list →
"false" (captures nothing — used during Discord-not-running window).
Non-IPv4 upstream → 0.0.0.0 fallback (caller should validate; the
builder degrades gracefully rather than panicking).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds github.com/imgk/divert-go v0.1.0 dependency. Embedded driver
binaries land at runtime in %PROGRAMDATA%\Drover\windivert\ via the
installer (next task).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend now renders three-tier status (passed/warn/failed) instead of
two. Warn rows get a yellow exclamation-mark dot, expand by default to
show the hint, and contribute to a new "(with warnings)" suffix on the
"All checks passed" header.
Test catalog gains "voice-quality" and "voice-srv" rows replacing the
single "stun" row, in the same position (after udp, before api). RU
descriptions explain what each test actually probes.
useDrover's lastSummary now reports {total, failed, warnings} so the
Classic header can pick the right tier color.
App.go counts StatusWarn as passed in the final {passed, failed} summary
emitted via "check:done" — warn is a soft pass per spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single-packet `stun` test with two predictive voice tests:
- voice-quality: 30-packet STUN burst through the SOCKS5 UDP relay.
Computes loss%, jitter (RFC-3550-ish mean abs of inter-arrival
delta), p50/p95 RTT. Three-tier gating: pass (loss≤5%, jitter≤30,
p50≤250), warn (loss≤15%, jitter≤60, p50≤400 — voice glitches but
works), fail (anything worse, including 100% loss).
- voice-srv: parallel-DNS the 16-region <region>.discord.media
hostnames, then SOCKS5 CONNECT to :443 on each through the proxy.
Catches the very common Russian-DPI failure mode where the proxy
passes generic Discord.com TCP but blocks the .discord.media voice
CIDRs — a regression all 5 prior SOCKS5 sanity checks miss.
New StatusWarn = "warn" — soft pass with Hint kept visible. Counted as
passed in summary but flagged in UI.
Config gains VoiceBurstCount (default 30), VoiceBurstInterval (default
20ms), VoiceServerHostnames (default = built-in 16-region list).
Tests cover happy path, warn-tier (10% drop), fail-tier (100% drop),
voice-srv blocked, plus standalone unit tests on
runVoiceQualityBurst and runVoiceServerProbe with a fake UDP relay
and fake SOCKS5 server. Race + cover stays at 82.4%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RunCheck now drives internal/checker.Run instead of the fake 7-step
sleep loop. Streams checker.Result events as "check:result" with
Duration converted to milliseconds; emits "check:done" summary on
channel close. Re-running while a check is in flight cancels the
previous run and waits for its goroutine to drain so two emitter
goroutines never race on event order.
- New CancelCheck method (no-op when nothing is running) is bound
through wailsjs/go/gui/App.js and surfaced in useDrover as
cancelCheck() with a "check cancelled by user" log line.
- Classic.jsx: while phase==='checking', the Check button collapses to
a disabled "Checking…" pill side-by-side with a Cancel button (uses
Stop's secondary visual weight, t.danger on hover). The expanded
failure row now renders r.rawHex (truncated to 64 chars) on its own
mono line and the copy button includes raw=<hex> when present.
- CheckResult struct gains RawHex (json:"rawHex") and Attempt fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public Run(ctx, cfg) <-chan Result streams diagnostic events for the seven
tests (tcp, greet, auth?, connect, udp, stun, api) wired through the
SOCKS5 primitives, STUN codec, retry classification and RU hints.
- Per-test attempt loop with running/passed/failed events, transient-only
retries (per-attempt timeout treated as transient, parent ctx cancel as
permanent), context-aware backoff sleep.
- Connection lifecycle: tcpConn shared across greet/auth/connect (closed
and redialed on retry); separate udpConn2 control channel for UDP
ASSOCIATE kept alive for the duration of the stun test.
- STUN-via-SOCKS5: builds 10-byte SOCKS5 UDP header + STUN binding
request, decodes reply with ATYP-aware header strip (1/3/4).
- runAPI plugs SOCKS5 dial into http.Transport.DialContext; passes on
HTTP 200 OR 401.
- Skip semantics: dependency-failed tests emit single skipped result;
cancellation latches and propagates as cancelled-failed (current) +
cancelled-skipped (remaining).
- Defaults applied to a copy of cfg; UseAuth=false suppresses any "auth"
result entirely.
Tests: 10 TestRun_* covering happy/auth-rejected/all-rejected/
connect-refused/udp-unsupported/timeout-then-ok/cancelled-mid-flight/
defaults plus extractRawHex unit. Fake SOCKS5 proxy + UDP relay echoing
synthetic STUN binding success responses; httptest stub for API splice.
Combined coverage 84.3% (>=80% target). go test -race clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds:
- retry.go: classifyError() splits errors into Permanent vs Transient
(used to gate auto-retry); isContextErr() detects ctx cancellation
through wrapping (OpError, errors.Join).
- hints.go: hintFor(testID, err) returns short Russian explanation per
failure step, with dedicated branches for SOCKS5 sentinels, every
documented REP code (0x01..0x08), STUN sentinels, timeouts, and a
friendly-name fallback.
Coverage: retry.go 100%, hints.go 100%; package total 94.2%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hand-rolled RFC 5389 binding-request encoder + binding-success-response
parser. Just enough to extract XOR-MAPPED-ADDRESS from a server's reply
after socks5UDPAssociate returns a relay endpoint. Avoids pulling in
pion/stun for ~80 LOC of encoding/binary work.
Provides NewTransactionID, EncodeBindingRequest, ParseBindingResponse and
six sentinel errors (ErrSTUN*) so HintFor (T11) can match specific
failure modes. Full TLV attribute walking with bounds checks; supports
both IPv4 and IPv6 XOR-MAPPED-ADDRESS values.
Tests cover encoder layout, IPv4/IPv6 happy paths, attribute walking
past unknown attributes, all error paths, sentinel uniqueness, and a
real loopback round-trip via net.ListenPacket. 90.0% combined coverage
(socks5+stun); stun.go funcs all >= 87%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
socks5Greeting/Auth/Connect/UDPAssociate per docs/superpowers/specs/
2026-05-01-checker-design.md. RFC 1928 + RFC 1929 wire bytes, raw
reply bytes returned on every error path for RawHex display, ctx
deadline applied via SetDeadline, ctx.Err() joined into error chain
on cancellation. Sentinel errors and ErrSocks5Reply{Code} for code
matching via errors.Is.
Tests: 22 subtests with fake net.Listen server, table-driven per
primitive (happy paths, REP codes, short reads, bad version,
oversize input rejection without I/O, ctx-cancel mid-read).
go test -race -cover passes at 89.0%, go vet clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app.go: App struct with stub bindings (RunCheck/StartEngine/
StopEngine/GetStatus/Version) — emits check:result, check:done,
engine:status, stats:update events. Real backend lands in Phase 1.
- run.go: wails.Run() with frameless 480x640 fixed window, Classic
dark bg matching theme.
- embed.go: //go:embed all:frontend/dist for the Vite build output.
- frontend/: Vite + React project derived from `wails init -t react`.
Removed default template assets and wired Classic variant from
docs/design/v2/.
- components/Classic.jsx: variant 1 with custom title bar
(drag region, sun/moon theme toggle, min/close hooked to
Wails WindowMinimise/Quit).
- components/shared.jsx: useDrover hook adapted to call Wails
bindings and listen on backend events instead of mock SCENARIOS.
Added IconSun + IconMoon for the title-bar toggle.
- App.jsx: owns mode state, wraps setMode in
document.startViewTransition so the title-bar toggle gives a
circle-reveal sweep from the cursor.
- style.css: clean reset (overflow hidden, no scrollbars, brand
background) — replaces the wails-react-template defaults.
- wailsjs/go/gui/App.js: hand-written bindings since our App
struct lives in package gui rather than the standard top-level
main; `wails generate module` would have written package main
bindings here.
- build/: standard wails artifacts (icon, manifest); will be
consumed by `wails build` once we wire it through CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small, well-tested package that:
- Queries /api/v1/repos/root/drover-go/releases/latest (404 = no updates,
not an error).
- Compares the published tag against the running Version using
golang.org/x/mod/semver, so v0.1.0-rc.2 < v0.1.0. "dev" or any
semver-invalid current version is treated as "always update".
- Downloads the windows-amd64 asset + SHA256SUMS.txt, verifies the
sha256 of the binary against its line in the sums file (tolerates
the asterisk binary-mode prefix), and atomically swaps the running
exe via github.com/minio/selfupdate.
- Uses a 15s connect timeout with no overall request deadline, so
large asset downloads aren't truncated.
- Reports progress via an optional callback.
Public surface: Source interface + ForgejoSource implementation,
CheckForUpdate, ApplyUpdate, SetVersion. No GUI/cobra/Wails imports
in the package, so the same code is reusable from the CLI, the
Windows service, and the future tray UI.
Wires the package into "drover update" / "drover update --check-only"
in cmd/drover/main.go. --check-only exits 0 whether or not an update
is available; only network/sha/apply errors are non-zero.
Tests cover CheckForUpdate (table-driven incl. semver pre-release
ordering, dev fallthrough, source errors), parseSHA256Sums (text and
binary modes, CRLF, malformed lines, missing entries),
ForgejoSource.Latest (httptest with canned JSON, 404, 500, missing
asset, missing SHA256SUMS), and downloadAndVerify (success, sha
mismatch, HTTP 404, context cancellation). All run with -race.
Smoke-tested manually: built drover.exe and "drover update --check-only"
against git.okcu.io prints "No updates available" and exits 0 (no
releases yet).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>