Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ name = "mhrv-rs-ui"
path = "src/bin/ui.rs"
required-features = ["ui"]

[[bin]]
name = "mhrv-drive-node"
path = "src/bin/drive_node.rs"

[features]
default = []
ui = ["dep:eframe"]
Expand All @@ -46,6 +50,13 @@ httparse = "1"
rand = "0.8"
h2 = "0.4"
http = "1"
# hyper drives the Drive REST client over a single multiplexed HTTP/2
# connection. Without this, every Drive API call paid a 300-500 ms TLS
# handshake; HTTP/2 multiplexing folds them all onto one keep-alive
# stream and gets us roughly an order of magnitude on perceived latency.
hyper = { version = "1", features = ["client", "http2"] }
hyper-util = { version = "0.1", features = ["client", "client-legacy", "tokio", "http2"] }
http-body-util = "0.1"
flate2 = "1"
directories = "5"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
Expand Down
52 changes: 52 additions & 0 deletions Dockerfile.drive-node
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Dockerfile for `mhrv-drive-node` — the server side of google_drive mode.
#
# Build:
# docker build -t mhrv-drive-node -f Dockerfile.drive-node .
#
# First run (interactive — completes Google OAuth):
# docker run -it --rm \
# -v /opt/mhrv-drive:/data \
# mhrv-drive-node
#
# Subsequent runs (detached, restart on host reboot):
# docker run -d --name mhrv-drive-node --restart unless-stopped \
# -v /opt/mhrv-drive:/data \
# mhrv-drive-node
#
# /data is expected to contain `credentials.json` and `config.drive.json`
# before first run. The binary writes `credentials.json.token` (the cached
# refresh token, chmod 0600) into the same dir on successful OAuth.

# ---- builder ------------------------------------------------------------
# Pin the Rust toolchain so a future `rust:1` retag (or a transitive dep
# bumping its MSRV) can't silently break this image. Need >= 1.85 for
# the edition2024 stabilization that time-macros and a few other
# transitive deps in our lockfile now require; bump deliberately.
FROM rust:1.85-slim-bookworm AS builder
WORKDIR /src

# `ring` (TLS backend) needs a C compiler + assembler. Everything else is
# pure Rust thanks to rustls.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY Cargo.toml Cargo.lock ./
COPY src ./src

# Skip the desktop / Android binaries — we only need the drive node.
RUN cargo build --release --bin mhrv-drive-node

# ---- runtime ------------------------------------------------------------
FROM debian:bookworm-slim

# CA bundle for HTTPS to www.googleapis.com.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /src/target/release/mhrv-drive-node /usr/local/bin/

WORKDIR /data
ENTRYPOINT ["/usr/local/bin/mhrv-drive-node"]
CMD ["-c", "/data/config.drive.json"]
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,54 @@ More deployments = more total concurrency = lower per-session latency. Each batc
}
```

## Google Drive queue mode

Experimental `google_drive` mode ports the FlowDriver idea: Google Drive is used as a shared file queue instead of Apps Script. The client listens as a SOCKS5 proxy, writes multiplexed request envelopes into a Drive folder, and `mhrv-drive-node` polls the same folder, opens the real TCP connections, and writes response envelopes back.

Use [`config.drive.example.json`](config.drive.example.json) on both machines. Create a Google OAuth desktop credential JSON with the Drive API enabled, set `drive_credentials_path`, then start the node first:

```bash
mhrv-drive-node -c config.drive.json
mhrv-rs -c config.drive.json
```

On first run each side prints a Google OAuth URL and saves `<credentials>.token` (the client tries to chmod 0600 on Unix — the file holds a long-lived OAuth refresh token, treat it like a credential). If `drive_folder_id` is empty, the program finds or creates `drive_folder_name`; for different machines/accounts, copy the resulting folder ID into both configs. This mode is SOCKS5-only and does not support UDP.

Tune `drive_idle_timeout_secs` (default 300) upward if you tunnel long-poll HTTP, idle WebSockets, or anything else that can go quiet for minutes — sessions silent past this window are force-closed.

> **Security note:** `mhrv-drive-node` is effectively an open TCP relay for whoever has read/write access to the shared Drive folder — anything that can drop a `req-…mux-…bin` file in there can open arbitrary `host:port` connections through the node. Keep the folder narrowly scoped (one OAuth account, no link sharing) and don't run the node on a machine you don't control.

### Onboarding a non-technical user (Android)

Once one device has finished OAuth, you can hand the configured state to another via QR or text — no Cloud Console steps required on the receiving end. In the Drive section: **Share Drive setup** → **Show QR + payload** → copy / send the `mhrv-rs-setup://import/...` link via WhatsApp / Telegram / SMS. The recipient pastes the link, scans the QR, picks the QR image from their gallery, or just taps the link if their messenger linkifies it. The bundle includes the OAuth refresh token, so they don't run their own consent flow — they share the sharer's Google identity for `drive.file` scope.

> **Read this before you share.** The setup blob bundles the OAuth `client_secret` AND a long-lived refresh token. Anything that can read the QR / link — a chat backup, a screenshot synced to cloud, a compromised device — gets the same `drive.file` access this app has, indefinitely. There is no per-recipient revoke: the only way to invalidate a leaked share is to rotate (or delete) the OAuth client in Google Cloud Console, which also kicks every device you've already onboarded with that client. Treat the share like a long-lived password: keep the recipient list small, prefer scanning camera-to-camera over messengers, and rotate the OAuth client on a schedule if the same identity is shared widely.
>
> If you want per-device revocation without a Cloud Console round-trip, do the OAuth flow separately on each device instead of using setup-share — refresh tokens minted from independent consent flows can be revoked one at a time from your Google Account's "Third-party apps with account access" page.

Caveat: the **sharer** still needs an unfiltered path to `accounts.google.com` for the initial OAuth dance, since the consent page opens in their system browser. If your network blocks Google Accounts, do the initial OAuth on a different network (mobile data, friend's Wi-Fi) and then share the resulting setup. Recipients aren't bound by this — they get the refresh token via the QR.

When the consent page warns _"Google hasn't verified this app"_, that's expected for personal Cloud projects in **Testing** publication status. Click **Advanced → Go to mhrv-drive (unsafe)** → grant the `drive.file` scope. Same flow as deploying an Apps Script for the existing modes.

### Quota and reachability

Google Drive's free-tier per-user quota is **1,000 requests per 100 seconds**. Default `drive_poll_ms = 100` plus `drive_flush_ms = 100` is comfortably below that even under heavy traffic, but if you turn polling down further or run a single OAuth identity across many devices you can blow it. The Rust side logs a `WARN` at 80% and `ERROR`s past 100% — watch for `Drive API rate climbing` in the logs. Bump `drive_poll_ms` / `drive_flush_ms` if you see them.

Before deploying, sanity-check that your network can actually reach Drive's edge IPs. The most informative test (from the host that will run `mhrv-drive-node` or the client):

```bash
curl --resolve www.googleapis.com:443:216.239.38.120 \
-I https://www.googleapis.com/drive/v3/files
```

A 401 response (no auth) is success — it means TCP reached Google and the TLS handshake completed. A connect timeout, RST, or TLS error means the same DPI / RST-injection path that affects the Apps Script outbound also hits Drive's API endpoint, and this mode won't work better than the existing Apps Script ones on that network.

### Garbage collection

Both sides reap their own files via `cleanup_loop` (every 5 s, deletes own files older than `OLD_FILE_TTL = 60 s` using Drive's `createdTime` so cross-machine clock skew can't false-positive). The poll path also auto-deletes peer files older than `STARTUP_STALE_TTL = 5 min` that look like leftovers from a previous run, plus reaps orphan response files for our own client ID at the same TTL — covers the edge case where `mhrv-drive-node` dies mid-batch and can't run its own cleanup.

If you ever notice `MHRV-Drive` accumulating files past these windows, check the Live logs / Docker logs on both sides for poll errors that prevent the cleanup loop from firing.

## Running on OpenWRT (or any musl distro)

The `*-linux-musl-*` archives ship a fully static CLI that runs on OpenWRT, Alpine, and any libc-less Linux userland. Put the binary on the router and start it as a service:
Expand Down
18 changes: 16 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,28 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link: tapping mhrv://... in any app opens MainActivity
and auto-imports the encoded config. -->
<!-- Deep link: tapping mhrv-rs://... in any app opens
MainActivity and auto-imports the encoded config. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mhrv-rs" />
</intent-filter>
<!-- Drive setup deep link: tapping
mhrv-rs-setup://import/<base64> in WhatsApp / Telegram /
SMS opens the app and offers to import the bundled
credentials + refresh token. Distinct scheme + fixed
host="import" so a foreign URL like
`mhrv-rs-setup://attacker.example/...` doesn't trigger
the import flow; the trust prompt is the second line
of defence. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mhrv-rs-setup" android:host="import" />
</intent-filter>
</activity>

<!-- FileProvider for sharing QR code images via the share sheet. -->
Expand Down
Loading
Loading