- Go 93.6%
- Nix 6.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The size-only readback still let the storage layer lose content: the bridge metadata kept reporting the uploaded size while the bytes were gone (ardbeg lost 3 more packs after the first fix). The readback now also downloads and decrypts the last 16 bytes of the stored ciphertext and compares them with the source file's tail — CTR keeps offsets aligned, so any truncation, empty body or storage-side corruption fails the file before the Drive row is registered. The app mock models per-upload network files (unique shard uuid through start/put/finish/info, per-uuid ciphertext and index) so concurrent uploads verify independently. |
||
| cmd/internxt-fuse-poc | ||
| docs | ||
| internal | ||
| nix | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
internxt-fuse
A minimal, headless daemon that mounts an Internxt Drive account as a FUSE filesystem on Linux, reproducing the official Internxt Linux Desktop client's API flow — no Electron, no React, no X11/Wayland, no Node.js at runtime.
Status: production-ready for read-only use. The internxt-fuse-poc
binary implements credential import/refresh, read-only FUSE mounting
(mount --read-only), encrypted offsite mirroring (backup push) and the
read/inspect commands (ls, cat with HTTP-range reads). It ships with a
NixOS module (nix/module.nix) and package (nix/package.nix) and runs in
production on a NixOS storage server (systemd service, StateDirectory
credential rotation, Restart=on-failure). Write support to the mounted
filesystem is the remaining major milestone (see "Roadmap").
This is not an official Internxt product. The rclone/CLI auth routes are
deliberately not used (they return 402 rclone access not allowed for this user tier on some accounts); this project uses the desktop flow exclusively
and never attempts to bypass server-side access control. If any endpoint
refuses access, that is reported as a limitation.
How it works
Internxt cloud
│ official Desktop API flow (Bearer token from a desktop login)
▼
internxt-fuse Drive metadata API https://gateway.internxt.com/drive
│ Network/storage API https://gateway.internxt.com/network
│ Go + FUSE
▼
/mnt/internxt
Files are AES-256-CTR encrypted client-side by every Internxt client. The
file key is derived deterministically from the account mnemonic, the bucket
id and a per-file index blob returned by the Network API; downloads are
plain HTTP Range requests against presigned shard URLs, so reading 1 MiB
from a large file does not download the whole file. Uploads use the same
desktop flow (network start → presigned PUT → finish → drive registration).
Build
go build -o internxt-fuse-poc ./cmd/internxt-fuse-poc
Requirements: Go 1.26+ (any recent Go works via GOTOOLCHAIN=auto), no
other build dependencies.
Usage
# 1. Log in: opens the official Internxt web login and captures the
# internxt:// callback automatically (Linux scheme handler) or via
# paste-in. Over SSH the URL + required steps are printed instead —
# the login works from any machine's browser:
internxt-fuse-poc login
# Headless alternative: capture the callback URI manually and import it
# from a file or stdin (never from shell history):
internxt-fuse-poc credentials import login-callback.txt
# 2. Refresh the session (rotates the bearer token, caches user metadata):
internxt-fuse-poc refresh
# 3. List the drive root:
internxt-fuse-poc ls /
# 4. Stream a decrypted file (whole or as a range):
internxt-fuse-poc cat /folder/file.txt
internxt-fuse-poc cat --offset 209715200 --bytes 1048576 /large.bin | sha256sum
# 5. Mount the drive:
# - Linux: kernel FUSE (fuse3/fusermount3)
# - macOS: macFUSE required (brew install --cask macfuse)
internxt-fuse-poc mount --read-only /mnt/internxt
./internxt-fuse-poc mount --dir-ttl 10s /mnt/test
# Flags: --read-only (recommended for servers), --allow-other, --dir-ttl,
# --credentials PATH. SIGHUP invalidates the listing cache; Ctrl+C unmounts.
# 6. Offsite backup: mirror a local folder (encrypted, incremental).
# "Changed" = size or mtime (±1 s) differs; replaced/removed files go to
# the trash (reversible). --delete trashes remote-only files AND folders:
# the remote tree becomes a true mirror of the source.
internxt-fuse-poc backup push ~/important /Backup-Server --dry-run # preview
internxt-fuse-poc backup push ~/important /Backup-Server # push
internxt-fuse-poc backup push --delete ~/important /Backup-Server # + trash remote-only
internxt-fuse-poc backup push --upload-concurrency 4 ~/important /Backup-Server # parallel uploads (default 3)
# 7. Non-secret state + counters:
internxt-fuse-poc status
Global flags: --credentials PATH, --drive-url, --bridge-url,
--log-level, --log-format (all optional; defaults are production).
Environment: INTERNXT_DESKTOP_HEADER_KEY, INTERNXT_FUSE_CREDENTIALS,
INTERNXT_DRIVE_URL, INTERNXT_BRIDGE_URL.
Exit codes: 0 ok, 1 generic, 2 auth failure, 3 storage/network
failure, 4 local configuration failure.
Credentials are stored as a 0600 JSON file in a 0700 directory
($XDG_STATE_HOME/internxt-fuse/credentials.json by default) and are
rewritten atomically on every token rotation.
Token rotation and multiple hosts
The refresh manager rotates the bearer token server-side: the previous
token is invalidated on every refresh (see internal/auth/manager.go). A
credentials file therefore represents exactly one live session — never
copy the same credentials.json to multiple machines, or each refresh will
invalidate the other host's stored token and brick it until it is re-seeded.
Run login/credentials import once per host to create a separate session
under the same account.
NixOS module
nix/module.nix exposes services.internxt-fuse:
services.internxt-fuse = {
enable = true;
user = "timvh"; # service user for the FUSE mount
group = "users";
mountPoint = "/mnt/internxt";
readOnly = true; # recommended for servers
dirTTL = "30s"; # directory-listing cache TTL
credentialsFile = null; # optional one-time seed, see docs/nixos.md
backup = {
enable = false; # systemd timer for `backup push`
sourceDir = "/data/important";
remoteFolder = "/Backup-Host";
delete = false; # trash remote-only files
startAt = "*-*-* 03:00:00"; # systemd calendar expression
};
};
The module provisions the setuid fusermount3 wrapper, symlinks
/bin/fusermount to it, declares the StateDirectory for the rotated
credentials copy and runs the daemon as the configured user. Two hard-won
rules for the unit (documented in detail in
docs/nixos.md):
- No mount-namespace sandboxing (
ProtectSystem,ProtectHome,PrivateTmp,ReadWritePaths, …) — systemd would run the unit in a private mount namespace and the FUSE mount would be invisible to the rest of the system. - PATH must contain the real wrapper directory (
/run/wrappersvia the NixOSpathoption, which appends/bin). Listing/run/wrappers/binproduces a doubled…/bin/binentry,exec.LookPath("fusermount3")then resolves to the non-setuid store binary and every mount fails withEPERM.
Verification status
- API endpoints, headers and crypto were reverse-engineered from the actual upstream sources and are documented with per-claim CONFIRMED/LIKELY/UNKNOWN/BLOCKED status in docs/upstream-analysis.md.
- Crypto is cross-checked against test vectors generated with the reference algorithm (see docs/upstream-analysis.md §7) and covered by unit tests, including seekable-range decryption.
go test ./...covers: credential import/store/permissions, API headers and retry behaviour (429/401/5xx), shard download/range stitching, multi-shard continuous-keystream handling, integrity-hash rejection, backup push mirroring (including dry-run) and an end-to-end import→refresh→ls→cat flow against a mock backend.- The daemon, the NixOS module and the encrypted mirror backup run in production on a NixOS storage server; mount visibility, multi-hour pushes and credential rotation are live-verified.
Known limitations
- Right after uploading a file through the web client, the drive listing
can briefly report
size: 0and/or an emptyfileId(async indexing on the server side). Runrefreshand retry; the values settle within seconds. A non-empty file whose listing still lacks afileIdmakescatfail loudly instead of producing empty output. - Same eventual consistency on folder creation:
POST /foldersreturns the new uuid in its response, but re-listing the parent immediately after can miss the new folder —backup pushuses the create-response uuid directly (live observation 2026-09-26). backup pushis a mirror: it keeps one live copy per remote folder and has no version history, no server-side copy and no retention pruning. Time-dated retention (GFS-style) requires pushing into time-suffixed folders and pruning them separately.
Roadmap
Upstream analysis→ docs/upstream-analysis.mdAPI PoC (refresh/user/list root)Content PoC against a real account (hash verification via golden files)Read-only FUSE mountHeadless browser/deeplink login (x-scheme-handler)systemd/NixOS service→ nix/package.nix, nix/module.nixEncrypted offsite mirror backup (backup push)- Write support (write spool + journal, milestone 2)
- Planned: multi-job backup timers (
backup.jobs), remote retention pruning (backup prune), per-host login sessions
Repository layout
cmd/internxt-fuse-poc/ CLI entrypoint
internal/api/ Drive metadata API client (headers, retry, DTOs)
internal/auth/ credential store, import parsing, refresh manager
internal/storage/ Network/storage API: file info, range downloads
internal/crypto/ file-key derivation + seekable AES-256-CTR
internal/config/ configuration + env overrides
internal/logging/ structured logging with secret redaction
internal/metrics/ counters
internal/app/ command implementations (import/refresh/ls/cat/
mount/backup)
nix/ Nix package + NixOS module
docs/ analysis + API/security/authentication/NixOS docs
upstream/ reference clones (gitignored, not committed)
Licensing
- This project's own code: to be decided by the repository owner; written from scratch except where noted below.
- Ported algorithm (internal/crypto): functional reimplementation of the
MIT-licensed
internxt/rclone-adapter(StarHack)buckets/encrypt.go, cross-checked against AGPLinternxt/inxt-jsand AGPLinternxt/drive-desktop-linuxas documentation of externally observable behaviour. No AGPL code was copied. - See docs/upstream-analysis.md §11 for the full license table and reuse policy.