Commit Graph

24 Commits

Author SHA1 Message Date
root 168596bcb5 sboxrun: domain+IP-CIDR rules + remove voice-quality test
Build / test (push) Failing after 33s
Build / build-windows (push) Has been skipped
Release / release (push) Failing after 3m22s
Three follow-up fixes after the WinDivert→sing-box pivot:

1. Discord updater now routes through upstream. Previously only the
   process-name rule matched, but sing-box's TUN-side process
   detection on Windows mis-attributes the in-process Rust updater's
   TLS connection to e.g. steam.exe — the connection went direct and
   hit RKN block. Adding domain_suffix + ip_cidr rules for Cloudflare
   (162.159/16, 104.16/13, 172.64/13) and Fastly (199.232/16,
   151.101/16) catches updates.discord.com regardless of which PID
   the kernel claims sent it. Verified via curl through mihomo:
   updates.discord.com responds 400 in 393ms (i.e. TLS handshake
   succeeds, only the path is wrong — proves the routing reaches it).

2. DiscordSystemHelper.exe added to TargetProcs alongside Update.exe
   (modern Discord builds use it for elevated updates).

3. UDP voice quality test removed from the checker. The STUN-via-
   relay burst measured private mihomo BND.ADDR (192.168.1.132)
   which is unroutable from external clients, so the test reported
   100% loss every time despite voice actually working through
   sing-box's TUN+SOCKS5. The remaining 6 checks (TCP/greet/auth/
   connect/UDP/api) cover what's actionable; voice quality is
   verified empirically by joining a Discord call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:21:50 +03:00
root 48097f8671 pivot: replace WinDivert engine with embedded sing-box + wintun
Build / test (push) Failing after 31s
Build / build-windows (push) Has been skipped
After 5+ hours of WinDivert NETWORK-layer NAT-rewrite debugging
(streamdump pattern, SOCKET-layer SYN preemption, lazy PID resolution,
UDP ASSOCIATE relay + manual reinject), Discord voice still wouldn't
connect. The fundamental issue is that WinDivert reinjected UDP
packets don't always reach connect()-bound application sockets — the
demux happens at a layer above the reinject point.

dvp/force-proxy avoids this entirely via DLL injection (above the
kernel demux). We avoid it the other way: embed sing-box, let it run
TUN inbound + per-process routing rule + SOCKS5 outbound. TUN packets
are read by sing-box from kernel as a normal flow; the application
socket sees a normal flow back. No reinject hairpin, no SYN race, no
spoofing concerns.

What this commit does:
  - Drops internal/divert, internal/engine, internal/redirect,
    internal/socks5, internal/procscan, plus cmd/drover/{proxy,
    debugflow}_*.go subcommands (all WinDivert-only).
  - Adds internal/sboxrun — embed sing-box.exe (1.12.25) + wintun.dll
    (0.14.1) via //go:embed, install to %PROGRAMDATA%\Drover\sboxrun\
    with SHA256 verify, generate JSON config from form, spawn as
    subprocess, manage lifecycle.
  - Wires sboxrun into internal/gui/app.go: StartEngine/StopEngine
    now call sboxrun.Engine instead of windivert engine.
  - Fixes Wails binding: StartEngine(cfg) now passes the form config
    (was zero-arg, hit ProxyHost-required validation silently).

Manual test: Discord chat + voice work end-to-end through mihomo
upstream. Yandex Music / svchost / etc continue direct via
process_name routing rule.

Binary grew from 12 MB → 49 MB (37 MB sing-box embedded), but ships
fully self-contained. AV-friendly: wintun is Microsoft-signed, no
DLL injection.

WinDivert work preserved on experimental/windivert branch in case we
ever want to come back to that path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:02:12 +03:00
root 4074e68715 experimental/windivert: P2.1+P2.2 with WinDivert NETWORK+SOCKET layers
WIP snapshot before pivot to sing-box+TUN. Reached:
- TCP redirect via streamdump pattern (swap+Outbound=0+reinject)
- SOCKET layer for SYN-stage flow detection (avoids FLOW Establish-too-late race)
- Lazy PID→name resolution (catches Update.exe inside procscan tick)
- UDP forward via SOCKS5 UDP ASSOCIATE relay + manual reinject
- Result: chat works, voice times out (Discord IP discovery / RTC handshake fails)

Reason for pivot: WinDivert NAT-reinject pattern has subtle layer-3
semantics issues that DLL-injection / TUN-based proxies sidestep
entirely. Going with embedded sing-box + wintun as the engine —
proven path for Discord voice through SOCKS5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:27:54 +03:00
root 8ceb7775d7 internal/gui: wire StartEngine/StopEngine to internal/engine
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
Replaces the stub flag-toggle with a real engine.Engine. GetStatus
now reports the engine's actual state machine value. Stats remain
randomised in P2.1 — real bytes-counters land in P2.4 with the tray
UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:05:51 +03:00
root bbe88b0f70 internal/engine: state machine + orchestrator (P2.1 scope)
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
Idle → Starting → Active → Failed lifecycle. bringUp resolves
upstream IP, installs the driver (idempotent), runs initial procscan,
opens redirector listener, builds filter + opens WinDivert handle,
then spawns the diverter reader and 2-second procscan ticker.

On every outbound TCP packet from a target PID: record (src_port →
real_target) mapping, rewrite dst to 127.0.0.1:listener_port,
re-inject. Loopback listener picks up the connection, looks up the
original target, and SOCKS5-tunnels.

P2.1 scope: no Reconnecting state, no panic recovery, no UDP
forwarding. Those land in P2.2/P2.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:04:09 +03:00
root dd402d4fc4 internal/redirect: TCP NAT-loopback redirector
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
Listener on 127.0.0.1 accepts NAT-rewritten Discord SYNs (rewrite
done by divert layer in Task 10), looks up the original destination
in a sync-protected map keyed by source port, opens a SOCKS5 CONNECT
to the upstream proxy targeting that destination, and pumps bytes
both directions until either side closes.

30-minute TTL sweeper handles T-6 in the edge case matrix (mapping
leak when a flow never properly closes).

Pump teardown: when one direction's io.Copy exits, the goroutine
CloseWrite's its write side AND sets a past read deadline on the
OTHER conn so the peer goroutine's blocked read unwinds promptly
even when the upstream half never sends EOF (test fake-SOCKS5 hits
this; the real upstream may too).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:00:21 +03:00
root 837208d9ed internal/procscan: Toolhelp32 PID enumerator
Build / test (push) Failing after 28s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:51:16 +03:00
root a45c1c0ab7 internal/socks5: production TCP CONNECT client
Build / test (push) Failing after 31s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:50:03 +03:00
root 1949abf011 internal/divert: WinDivert handle wrapper
Build / test (push) Failing after 29s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:47:58 +03:00
root 35da6be99e internal/divert: driver installer with SHA256 verification
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:45:27 +03:00
root feda075dc4 internal/divert: IPv4+TCP packet parse + RewriteDst + checksums
Build / test (push) Failing after 29s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:43:42 +03:00
root 223c7f5886 internal/divert: filter expression builder
Build / test (push) Failing after 32s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:41:53 +03:00
root 736c3ecfc7 internal/divert: embed WinDivert64.sys + WinDivert.dll v2.2.2 with SHA256 sentinels
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
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>
2026-05-01 19:40:11 +03:00
root 11c4eb7f4a internal/checker+gui: remove voice-srv test (Discord doesn't expose regional voice servers via public DNS)
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
Release / release (push) Failing after 3m20s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:02:11 +03:00
root 9ea777d7b7 internal/gui: surface warn status + new voice tests in Classic UI
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
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>
2026-05-01 18:42:24 +03:00
root 0a85979142 internal/checker: voice-quality + voice-srv tests for predictive voice diagnosis
Build / test (push) Has been cancelled
Build / build-windows (push) Has been cancelled
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>
2026-05-01 18:42:12 +03:00
root 1c1ab566d9 internal/gui: wire real checker.Run + CancelCheck binding + RawHex display
Build / test (push) Failing after 29s
Build / build-windows (push) Has been skipped
- 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>
2026-05-01 16:13:02 +03:00
root 4b985bb7f0 internal/checker: 7-step Run orchestrator + integration tests
Build / test (push) Failing after 29s
Build / build-windows (push) Has been skipped
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>
2026-05-01 16:08:36 +03:00
root acd5291604 internal/checker: error classification + RU hints + tests
Build / test (push) Failing after 38s
Build / build-windows (push) Has been skipped
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>
2026-05-01 15:58:56 +03:00
root 36e788402a internal/checker: STUN codec + tests
Build / test (push) Failing after 32s
Build / build-windows (push) Has been skipped
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>
2026-05-01 15:50:28 +03:00
root 52ce1e0aa7 internal/checker: SOCKS5 primitives + tests
Build / test (push) Failing after 31s
Build / build-windows (push) Has been skipped
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>
2026-05-01 15:46:33 +03:00
root b6619ef53b internal/gui: Wails app with Classic React variant + theme toggle
Build / test (push) Failing after 30s
Build / build-windows (push) Has been skipped
- 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>
2026-05-01 15:17:19 +03:00
root 1ad8de32f2 Implement internal/updater: selfupdate via Forgejo Releases API
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>
2026-05-01 00:20:24 +03:00
root 0acbc83e40 Add directory skeleton and CLI entry point with Cobra
- Create internal/{app,config,engine,divert,socks5,bypass,checker,service,tray,procscan,updater} with placeholder doc.go per package.
- Add .gitkeep stubs for internal/frontend, third_party/{windivert,icons}, installer/, .forgejo/workflows/, docs/.
- Implement cmd/drover/main.go: Cobra root with Version/Commit/BuildDate ldflags, --config global flag, and stub subcommands (check, update --check-only, service install/uninstall/start/stop).
- Add github.com/spf13/cobra v1.10.2 dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:10:30 +03:00