Mount glftpd FTPS servers as native Windows drive letters. A single tray app for the full site-user workflow — browse in Explorer, auto-download from a wishlist, race releases between sites via FXP, chat on FiSH-encrypted IRC, stream media, watch the PreDB.
Built on .NET 10, WPF, WinFsp, FluentFTP, and GnuTLS. Windows 11, x64. Current version: 3.10.116.
For contributors: the full architecture reference lives in docs/. See docs/project-overview-pdr.md for the design rationale and docs/system-architecture.md for Mermaid diagrams and protocol walkthroughs.
glftpd sites have several quirks that off-the-shelf FTP clients handle badly. Each of these had to be solved for a reliable Windows 11 client:
- Behind a BNC,
PASVreturns backend addresses the client can't route to —CpsvDataHelperimplements CPSV with reverse-TLS data channels by hand - TLS 1.3 session tickets crash glftpd — forced TLS 1.2 +
GnuAdvanced.NoTickets - GnuTLS native disposal segfaults on corrupted sessions — session neutralized before disposal,
DisconnectWithQuit = false,StaleDataCheck = false - Stale BNC sessions eat slot counts — throttled
!usernameghost-kill automatic on pool failure - FXP racing needs SSCN, SFV-first priority, slot tracking, nuke detection, skiplist cascades, and four different CPSV/PASV modes — cbftp-style engine ships all of it
- IRC announce channels are FiSH-encrypted behind
SITE INVITE— Blowfish (ECB + CBC) + DH1080 + FTP-pool-borrowedSITE INVITEbefore auto-join - Native crashes bypass managed exception handlers — watchdog subprocess + Windows Restart Manager +
.running/.updatingmarkers recover and log the reason from the Event Log
- Multi-server — several glftpd sites mounted simultaneously, each on its own drive letter and WinFsp prefix (
\GlDrive\{serverId}) - Optional drive — connect a server without a drive letter and still get search, downloads, notifications, and IRC
- Bounded FTPS pool per server with ghost-kill (
!username), poisoned-connection tracking, and auto-reinitialize on exhaustion - TTL + LRU directory cache with stale-while-revalidate
- Whole-file read/write buffering with auto-spill to
DeleteOnClosetemp files above 50 MB - SOCKS5 proxy per server, credentials in Credential Manager
- Streaming FTP-to-disk with resume (REST for CPSV, positional
OpenReadfor standard) - Auto-retry with exponential backoff, scheduling windows (e.g. 22:00 → 06:00), CRC32 SFV verification before extraction
- Auto-extraction for RAR including multi-volume old-style (
.r00/.r01) and modern (.part01.rar) naming - Category download paths — route
TVtoD:\TV,MoviestoD:\Movies, etc. - Duplicate detection, NFO pre-check, disk space validation, drag-drop from Notifications or Search
/recentpolling with excluded-category filters and debounced toast notifications- Wishlist with TVMaze/TMDB/OMDB metadata and quality profiles (Any/SD/720p/1080p/2160p)
- Auto-download of matching releases across all connected servers via
SceneNameParser+WishlistMatcher - Wishlist import/export as JSON; completion toasts with optional sound
- TCP + TLS via
SslStream, TOFU cert validation (shared with FTP), keepalive + PING liveness, exponential-backoff reconnect that resets only after 60 s of stability (BNC rate-limit friendly) - FiSH Blowfish encryption in ECB and CBC modes with peer-mode auto-detect; keys DPAPI-encrypted per server
- DH1080 1080-bit key exchange with validated public keys; SHA-256 of the shared secret as the Blowfish key
- SITE INVITE integration — borrows an FTP pool connection to run
SITE INVITE <nick>, then auto-joins with retry on473 ERR_INVITEONLYCHAN - Announce detection — built-in verbose glftpd pattern + custom regex rules (200 ms timeout, 500-entry LRU dedup), auto-race to
SpreadManager - Pattern learning —
IrcPatternDetectoridentifies bots, infers announce regex, surfaces as clickable suggestions - Channel sidebar grouped by server, nick list with mode prefixes (
@/+/%/~/&), Tab completion, clickable release names, slash commands (/join /part /msg /me /topic /key /keyx …), raw IRC passthrough
- Four FXP modes picked automatically by
FxpModeDetector— PASV-PASV, CPSV-PASV, PASV-CPSV, and Relay (CPSV-CPSV piped through local memory with double-buffered 256 KB copy) - Per-server FXP connection pool separate from the main pool, auto-reinitialized before each race
- Global per-server transfer gates — every FXP transfer acquires a src + dst
ServerGatesemaphore (sorted-id order, deadlock-free) before opening connections. Caps total concurrent transfers per server across all jobs, preventing BNC login storms that previously crashed GnuTLS natively - BNC auto-tune — when a pool sees
530 restricted to N simultaneous logins, the gate and the pool's max size are both shrunk toN − 1(reserving one slot for ghost-kill keepalive). Shrink-only by design - Section availability pre-filter — auto-races dropped at planning time if no eligible destination has the announce section, eliminating thousands of "Need 2+ servers" noise failures
- PRET support —
PRET RETR/STORsent before PASV/CPSV; cached per-host so drftpd / ioFTPD configs get it and glftpd-only deployments skip the round-trip after the first 500 reply - Scoring 0 – 65535 — SFV = 65535, NFO-after-15 s = 65535, then file size + route speed (rolling 10-sample avg, persisted to
spread-speed-history.json) + site priority + ownership delta − failure penalty (rotates source on retry) - Per-pair retry cap 4, per-file global cap 7 — matches cbftp's
MAX_SINGLE_PAIR_FILE_TRANSFER_ATTEMPTS/MAX_TRANSFER_ATTEMPTS_BEFORE_SKIP - 60 s inactivity timeout with two-strike retry — first stall clears all retry state and tries again, second actually aborts
- Dupe-as-success —
553STOR replies matching cbftp's family ofuploaded by/dupescript/pre_check/X-DUPE/File exists/ile exists/already existpatterns count toward race completion (the file IS on the destination) - Skiplist cascade — per-site then global, glob or non-backtracking 100 ms regex, first match wins
- Pre-transfer guards —
TYPE Icanary,SSCN ON, deferredMKDuntil just beforeSTOR, completion sweep with 2× retry - Race history persisted (max 500) with full skiplist trace; auto-race on IRC announce or
/recentdetection
- Drag-drop RAR/ZIP/7z/TAR/GZ/BZ2/XZ/ISO/CAB with multi-volume RAR detection (old and modern naming, total set size)
- Watch folders with debounced
FileSystemWatcher, auto-extract, optional delete-after - Folder cleaner — scan a media root for leftover archives alongside extracted media, bulk reclaim
- Low-priority extraction thread; persistent settings
- Embedded VLC (LibVLCSharp) with direct play from mounted drives
- HTTP media server — local
HttpListenerstreams from FTP with Range/seek, caches extracted video in the library dir - RAR-wrapped video — sequential volume download with 5× retry, on-the-fly extraction, playback as soon as the first
.raris ready - Chromecast / DLNA casting via VLC renderer discoverer; TMDB trending/upcoming browser
- Torrent search + streaming (MonoTorrent) — parallel apibay/SolidTorrents/Torrents-CSV, DHT, HTTP streaming with seek
- Resume tracking per release name
- Cross-server parallel search, results tagged with server name
- Per-server modes — Auto, SITE SEARCH (server-side), Cached Index (background crawler), Live Crawl
- Borrows connections with 15 s timeout so search doesn't starve downloads
- Loopback-only HTTP surface on
127.0.0.1:8756for scripting the app without driving the tray UI - Bearer token minted on first enable, compared in fixed time; every request re-checks the caller is loopback
GET /route index,/status,/sections,/races,/races/{id},/history;POST /races,/races/{id}/stop- Enable via
controlApi.enabledinappsettings.json— requires a restart. For remote access, tunnel it (WireGuard / Tailscale / SSH) rather than binding the LAN
- Records per-stream JSONL telemetry (races, nukes, announces, section activity, downloads) under
ai-data/ - A daily run digests it and proposes config changes through an LLM; every mutation is gated by a frozen-path check, a confidence threshold, per-category budgets, and a validator that owns the actual write
- Dry-run mode by default, full before/after audit trail with undo
- Connection probing and invite management from the dashboard
- Dark / Light themes, runtime-swappable via
DynamicResource, system-theme detection - System tray with dynamic per-server menu and state-colored glyph
- 5-step first-run wizard, hot-reload on settings save (new servers mount without restart)
- Auto-update from GitHub Releases, SHA-256 mandatory, Authenticode issuer check when signed
- Watchdog subprocess + Windows Restart Manager for native crash recovery, reason pulled from Event Log
- Embedded WebView2 tabs (World Monitor / Discord / Streems) with serialized init and fallback UI
- Site import from FTPRush (XML/JSON) and FlashFXP (XML/DAT); remote glftpd installer via SSH.NET
See the Security table below and docs/changelog.md for recent releases.
| Notifications | Wishlist | Downloads |
|---|---|---|
![]() |
![]() |
![]() |
| Search | Upcoming |
|---|---|
![]() |
![]() |
| Servers | Performance | Downloads | Diagnostics |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Welcome | Connection | TLS |
|---|---|---|
![]() |
![]() |
![]() |
| Mount | Confirm |
|---|---|
![]() |
![]() |
Download GlDriveSetup-v{version}.exe from the Releases page and run it. The installer:
- Installs WinFsp silently if not already present (bundled in the installer)
- Copies GlDrive to
Program Files\GlDrive - Creates Start Menu and optional Desktop shortcuts
- Optionally registers auto-start via
HKCU\...\Run
No .NET runtime install needed — the app is self-contained on net10.0-windows/win-x64.
Upgrades preserve data. The uninstaller deliberately does not touch %AppData%\GlDrive\, so config, trusted certs, credentials, wishlists, FiSH keys, and race history all survive reinstall cycles.
Prerequisites: .NET 10 SDK, WinFsp pre-installed, Windows 11 x64.
git clone https://github.com/misterentity/GlDrive.git
cd GlDrive
dotnet build src/GlDrive/GlDrive.csproj
dotnet run --project src/GlDrive/GlDrive.csproj
dotnet test src/GlDrive.Tests/GlDrive.Tests.csprojGlDrive.sln includes the app and test projects, so dotnet build GlDrive.sln and
dotnet test GlDrive.sln also build and verify the application.
src/GlDrive.Tests covers the pure logic: crypto round-trips against known-answer vectors,
login-gate accounting, spread scoring, zipscript artifact filtering, failure classification,
log redaction, and control-API routing and bounds. The WinFsp, WPF and live-FTP layers still
need manual runtime verification — build and exercise the app.
Additional prerequisite: Inno Setup 6 with ISCC.exe discoverable.
# Publish + Inno Setup installer + update zip
powershell -File installer/build.ps1
# Full release: build + GitHub release with checksums (requires gh CLI)
powershell -File installer/release.ps1Version is read from <Version> in src/GlDrive/GlDrive.csproj. Full pipeline docs: docs/deployment-guide.md.
- Launch → first-run wizard: host, port, username, password, drive letter. Password goes to Credential Manager; config JSON gets non-secrets only
- TLS → auto-trust the first cert (TOFU); changes later pop a confirmation dialog
- Explorer → your drive letter appears as
glFTPd (G:) - Add more servers → Settings → Servers → Add. Configure per-server IRC, Spread, Pool, Cache, Notifications, Speed
- Wishlist → Dashboard → Wishlist. OMDB/TMDB/TVMaze search, quality profile, auto-download on match
- Spread → enable on ≥ 2 sites, set sections/priority/slots, then race from Dashboard → Spread (
Ctrl+R) or right-click Notifications/Search/PreDB - IRC → configure per server;
/keyx <nick>for FiSH key exchange
Notifications / Wishlist / Downloads / Search / Upcoming / PreDB / Player / Spread / Browse / IRC / World Monitor / Discord / Streems.
Highlights: Notifications and Search are drag sources for Downloads. PreDB live-feeds predb.net with auto-refresh and section filter. Spread surfaces active jobs, scoreboard, auto-race activity log, and race history with full skiplist evaluation traces. Browse is dual-pane with multi-select FXP. IRC groups channels by server with clickable release names in messages.
| Key | Context | Action |
|---|---|---|
Delete |
Downloads | Cancel selected |
R |
Downloads | Retry failed |
Enter |
Notifications / Search / PreDB | Download selected |
Ctrl+R |
Spread | Start new race |
Escape |
Spread | Stop selected race |
Tab |
IRC input | Cycle nick completion |
Space |
Player | Play / pause |
F / F11 |
Player | Fullscreen |
Esc |
Player | Exit fullscreen |
← / → |
Player | Seek ±10 s |
↑ / ↓ |
Player | Volume ±5 |
Everything per-user lives under %AppData%\GlDrive\. Full schema with defaults: docs/configuration-guide.md.
| Data | Location |
|---|---|
| App config | appsettings.json (camelCase JSON, non-secrets only) |
| Downloads (per server) | downloads-{serverId}.json |
| Race history | race-history.json (max 500) |
| Spread route speeds | spread-speed-history.json (rolling 10 samples per src→dst pair, survives restart) |
| Wishlist | wishlist.json (global) |
| Notifications | notifications.json (max 1000) |
| Trusted certs | trusted_certs.json (TOFU, user-only ACL) |
| FiSH keys (per server) | fish-keys-{serverId}.json (DPAPI-encrypted) |
| Extractor settings | extractor-settings.json |
| IRC PM history (per server) | pm-history-{serverId}.json (DPAPI-encrypted; channel chat stays plaintext in irc-logs/) |
| Agent telemetry & audit | ai-data/ (JSONL streams, config-change audit trail) |
| Update state | .update-declined, .update-deferred (both time-boxed; a granted UAC prompt clears the decline) |
| Logs | logs/gldrive-{date}.log (daily rolling, 10 MB cap) |
| Passwords / API keys / control-API token | Windows Credential Manager (the control-API token lives in appsettings.json, which is ACL-restricted) |
Program.cs (watchdog + update applier)
└─ App.xaml.cs
├─ SingleInstanceGuard, ConfigManager, SerilogSetup, CertificateManager (TOFU)
├─ WizardWindow (first run)
├─ ServerManager
│ ├─ per server: MountService
│ │ ├─ FtpClientFactory → FtpConnectionPool → FtpOperations → CpsvDataHelper
│ │ ├─ DirectoryCache → GlDriveFileSystem (WinFsp, prefix \GlDrive\{id})
│ │ ├─ ConnectionMonitor, NewReleaseMonitor
│ │ ├─ DownloadManager + DownloadStore + StreamingDownloader
│ │ └─ FtpSearchService, WishlistMatcher
│ ├─ per server: IrcService (IrcClient + FishCipher + Dh1080 + FishKeyStore
│ │ + IrcAnnounceListener + IrcPatternDetector)
│ └─ SpreadManager (per-server FXP pools, ServerGate (concurrency + BNC auto-tune),
│ SpreadJob, FxpTransfer, SpreadScorer, SpeedTracker (persistent),
│ SkiplistEvaluator, SectionBlacklistStore, RaceHistoryStore)
├─ WishlistStore, NotificationStore (global), UpdateChecker
├─ AgentRunner (daily) → LogDigester → AgentClient → Validators/* → ChangeApplier
├─ ControlApi (opt-in, loopback) → RouteTable → Endpoints/*
├─ PlexService
└─ TrayIcon + DashboardWindow + ExtractorWindow + ThemeManager
Subsystems are decoupled: each MountService builds its own full composition chain; SpreadManager keeps a separate FTP pool per server so races can't starve the filesystem/downloads pool. Mermaid diagrams and the full startup/FTP/CPSV/FXP/IRC/update flows live in docs/system-architecture.md.
Off-the-shelf FTP clients can't talk to glftpd behind a BNC because PASV returns backend addresses the client can't route to. Ftp/CpsvDataHelper.cs implements the workaround by hand:
- Send
CPSVon the control channel → backend(a,b,c,d,p1,p2)returned in PASV format - Raw TCP connect to that address (10 s timeout, no TLS yet)
- Send the data command (
LIST -a,RETR,STOR) on the control channel; expect150/125, do not wait for226 - TLS
AuthenticateAsServerAsyncon the raw socket with a cached self-signed RSA-2048 cert — glftpd callsSSL_connecton the data side, so we have to be the TLS server - Stream the data (LIST → parse Unix
ls -l; RETR → binary; STOR → binary) - Close the data socket, then read
226on the control channel
Resume for RETR sends explicit REST <offset> before the retry. Before every FXP transfer, FxpTransfer.SendTypeI() acts as a canary for BNC response-queue desync; EnableSscn() sends SSCN ON for encrypted FXP control data. Full protocol walkthrough: docs/system-architecture.md#cpsv-data-connections.
| Concern | Measure |
|---|---|
| Credentials | Windows Credential Manager (DPAPI); never in config files or logs |
| Log redaction | FTP PASS; IRC redaction keys on which parameter of a command is a credential — covers PASS, OPER, JOIN channel keys, services logins, and MODE/324 key echoes, inbound and outbound |
| Config telemetry | Credential-valued config changes record a SHA-256 digest, never the value |
| FiSH keys | Stored DPAPI-encrypted per server in fish-keys-{serverId}.json |
| Cert pinning | TOFU SHA-256 in trusted_certs.json, user-only ACL, rotation prompt required |
| FTP command injection | CpsvDataHelper.SanitizeFtpPath strips CR/LF/NUL before every wire path |
| GnuTLS native crashes | NeutralizeGnuTls before disposal, DisconnectWithQuit = false, StaleDataCheck = false |
| Poisoned connections | PooledConnection.Poisoned routes to Discard instead of Return |
| DH1080 | Peer public-key range check prevents trivial shared-secret recovery |
| Updates | SHA-256 mandatory against checksums.sha256; Authenticode issuer match when signed |
| Crash recovery | Watchdog subprocess + Windows Restart Manager; reason pulled from Event Log |
| Path traversal | Archive extraction validates entry paths (Zip Slip); updater validates install path contains GlDrive |
| Telemetry | None leaves the machine; update checks go directly to api.github.com |
| Inbound network | Off by default. The optional control API binds 127.0.0.1 only, never a wildcard, and re-checks the remote address is loopback on every request even with a valid token. The TLS-server role during CPSV is outbound-only to the glftpd backend |
| Control API auth | Bearer token, generated on first enable and compared in fixed time; the listener refuses to start if enabled without one |
| Control API input | Request bodies capped at 64 KB; section/release length-bounded before they reach the race engine or the log |
| Symptom | Fix |
|---|---|
| Drive letter doesn't appear | Check WinFsp is installed (sc query WinFsp); reinstall via the bundled MSI if missing |
| "BNC rate limit detected" toast | Rapid reconnects triggered a ~2-hour cooldown. Wait it out |
| Connection pool exhausted | Usually transient — the pool auto-reinitializes. Since v2.4 the BNC's login cap auto-shrinks the pool + gate from the first 530; if you still see exhaustion, check BNC !username ghost-kill isn't blocked |
| FXP transfers all fail with 530 | The first 530 should auto-tune the pool/gate down (look for Pool: max size shrunk in the log). If not, your SpreadPoolSize in appsettings.json may exceed BNC's hard cap; the auto-tune is shrink-only, so a restart resets to defaults |
| Update downloads but never applies | Missing or mismatched checksums.sha256. Rebuild via installer/release.ps1 |
| Watchdog restart loop | Delete %AppData%\GlDrive\.running and .updating manually |
| WebView2 tabs blank | Install the WebView2 Evergreen Runtime via the fallback-UI link |
| IRC can't join invite-only channel | Set inviteNick in the server's IRC config so SITE INVITE runs before the join |
| FXP race never completes | Check the race history tab's skiplist trace; verify upload/download slot counts on both sites |
Logs at %AppData%\GlDrive\logs\gldrive-{date}.log are authoritative. Bump log level to Debug in Settings → Diagnostics before reproducing.
Use Add or Remove Programs. The uninstaller intentionally leaves %AppData%\GlDrive\ in place so reinstall picks up where you left off. To fully remove: Remove-Item "$env:APPDATA\GlDrive" -Recurse -Force, then remove saved credentials from Control Panel → User Accounts → Credential Manager (search GlDrive:).
- docs/project-overview-pdr.md — what the project is, why it exists, success criteria
- docs/codebase-summary.md — subsystem inventory, folder layout, dependencies
- docs/system-architecture.md — Mermaid diagrams, CPSV, FXP, IRC, update flow
- docs/configuration-guide.md —
appsettings.jsonschema, data files, credential keys - docs/deployment-guide.md — build, installer, release, in-app update
- docs/design-guidelines.md — WPF themes, controls, commands
- docs/code-standards.md — conventions actually in use
- docs/changelog.md — recent versions
Provided as-is for personal use.













