Headless Internxt Drive client (desktop API flow) for NixOS - FUSE daemon PoC
  • Go 93.6%
  • Nix 6.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tim van Herpen 0f856d9d47 gateway: upgrade upload readback to content verification
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.
2026-10-07 11:45:37 +02:00
cmd/internxt-fuse-poc module+cli: webdav gateway service option with env token 2026-09-30 16:24:03 +02:00
docs docs: reflect production status and live-verified NixOS rules 2026-09-27 19:43:17 +02:00
internal gateway: upgrade upload readback to content verification 2026-10-07 11:45:37 +02:00
nix package: vendorHash for golang.org/x/net/webdav dependency 2026-09-30 17:12:41 +02:00
.gitignore Nix integration: package + NixOS module for the read-only mount 2026-09-26 17:38:50 +02:00
flake.lock Nix integration: package + NixOS module for the read-only mount 2026-09-26 17:38:50 +02:00
flake.nix Nix integration: package + NixOS module for the read-only mount 2026-09-26 17:38:50 +02:00
go.mod webdav: local WebDAV gateway for standard tooling (restic over rclone) 2026-09-30 08:36:36 +02:00
go.sum webdav: local WebDAV gateway for standard tooling (restic over rclone) 2026-09-30 08:36:36 +02:00
README.md backup: mirror integrity fixes, parallel streaming uploads 2026-09-28 22:17:47 +02:00

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):

  1. 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.
  2. PATH must contain the real wrapper directory (/run/wrappers via the NixOS path option, which appends /bin). Listing /run/wrappers/bin produces a doubled …/bin/bin entry, exec.LookPath("fusermount3") then resolves to the non-setuid store binary and every mount fails with EPERM.

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: 0 and/or an empty fileId (async indexing on the server side). Run refresh and retry; the values settle within seconds. A non-empty file whose listing still lacks a fileId makes cat fail loudly instead of producing empty output.
  • Same eventual consistency on folder creation: POST /folders returns the new uuid in its response, but re-listing the parent immediately after can miss the new folder — backup push uses the create-response uuid directly (live observation 2026-09-26).
  • backup push is 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

  1. Upstream analysis → docs/upstream-analysis.md
  2. API PoC (refresh/user/list root)
  3. Content PoC against a real account (hash verification via golden files)
  4. Read-only FUSE mount
  5. Headless browser/deeplink login (x-scheme-handler)
  6. systemd/NixOS service → nix/package.nix, nix/module.nix
  7. Encrypted offsite mirror backup (backup push)
  8. Write support (write spool + journal, milestone 2)
  9. 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 AGPL internxt/inxt-js and AGPL internxt/drive-desktop-linux as documentation of externally observable behaviour. No AGPL code was copied.
  • See docs/upstream-analysis.md §11 for the full license table and reuse policy.