A minimal, cross-platform (Android / Windows / macOS) app for sending files directly between devices on the same WiFi network — no internet, no cloud server, no account.
- Discovery: UDP broadcast on port
58211(every ~3s), no mDNS/Bonjour dependency. - Transfer: a custom length-prefixed TCP protocol (no HTTP framework) — one control connection for the accept/reject handshake, then up to N parallel data connections (default 4, "quick mode") so multiple files move at once instead of one at a time.
- Security model: every incoming transfer needs a manual Accept/Reject on the receiving device, like AirDrop. There's no transport encryption in v1 (plain TCP) — fine for trusted home/office WiFi; treat it like AirDrop, not like something for open/public networks.
- Stack: Tauri 2 + Rust backend, plain HTML/CSS/vanilla JS frontend (no
framework, no bundler — Tauri's built-in dev/build pipeline serves
src/directly).
See docs/ for the architecture, high-level, and low-level
design documents — architecture.md,
high-level-design.md,
low-level-design.md.
- Xcode Command Line Tools:
xcode-select --install - Rust via rustup (not just the Homebrew
rustformula — rustup is what manages the Android cross-targets below):curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
If you also have Rust installed via Homebrew: check
which rustcafterwards. If it prints/usr/local/bin/rustcor/opt/homebrew/bin/rustcinstead of$HOME/.cargo/bin/rustc, Homebrew's (older) rustc is shadowing rustup's — see Troubleshooting for the permanent fix. This causescargo build/Android build failures likerustc X.Y.Z is not supported by the following packagessince some dependencies need a newer compiler than Homebrew's formula ships. - Node.js 18+ (any recent LTS). Via nvm:
nvm install 18 nvm use 18
- Rust via rustup (MSVC toolchain — the installer will prompt to install the Visual Studio Build Tools / C++ workload if missing).
- Node.js 18+.
- WebView2 runtime (preinstalled on current Windows 10/11).
macOS cannot cross-compile a Windows build — there's no supported Tauri path for that. Build on an actual Windows machine, or use the GitHub Actions workflow in this repo (see §4 CI).
- Rust Android targets:
rustup target add aarch64-linux-android armv7-linux-androideabi \ i686-linux-android x86_64-linux-android
- Android Studio (or just the SDK command-line tools), then via its SDK
Manager (or
sdkmanageron the CLI) install:- NDK (side by side) — this project was built and tested against
27.1.12297006. Tauri's docs deliberately don't pin a version; if a newer NDK is offered, it will very likely still work. - Android SDK Platform 34 and Build-Tools 34.0.0 (or newer).
- Command-line Tools (latest) — needed for
sdkmanageritself.
- NDK (side by side) — this project was built and tested against
- Environment variables (add to your shell profile):
On Linux,
export ANDROID_HOME="$HOME/Library/Android/sdk" # macOS export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.1.12297006" export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" # macOS export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
ANDROID_HOMEis typically~/Android/SdkandJAVA_HOMEpoints at Android Studio's bundledjbrunder its install directory (or any JDK 17/21). npx tauri android inithas already been run for this project (its output,src-tauri/gen/android/, is committed) — you don't need to redo it, justnpm installand build (see below).
npm install
npm run tauri devThis opens a native window with hot-reload on frontend changes (Rust changes trigger a recompile + relaunch). Works the same on macOS, Windows, and Linux.
Two instances need separate identities (device name/id) but don't need
manually-offset ports — the discovery socket uses SO_REUSEPORT so multiple
local instances share port 58211 fine, and the TCP control/data ports are
always OS-assigned. Just build once and run the binary twice with a
different profile env var:
cargo build --manifest-path src-tauri/Cargo.toml
./src-tauri/target/debug/gklsan_wifi_transfer &
GKLSAN_WIFI_TRANSFER_PROFILE=b ./src-tauri/target/debug/gklsan_wifi_transfer &Each profile gets its own identity file (identity.json /
identity-b.json under the OS app-data directory), so the two windows show
up as distinct peers to each other within one beacon interval (≤ ~5s).
A single machine cannot cross-build other OSes' installers — Tauri only bundles for the host platform. So "build everything" comes in two flavors:
npm run dist # every installer format THIS host OS can make, into dist/
npm run dist:ci # macOS + Windows + Android via CI, downloaded into dist-ci/npm run dist(scripts/build-local.sh) runstauri buildfor the host and collects the result intodist/:.dmg/.appon macOS,.msi/.exeon Windows,.deb/.rpm/.AppImageon Linux. It also builds the signed Android APK whenANDROID_HOMEis set, and skips Android otherwise.npm run dist:ci(scripts/build-all-os.sh) is the only way to get every OS's installer from one command: it triggers the GitHub Actions matrix (each OS on its own runner), waits for it, and downloads all artifacts intodist-ci/. Needs theghCLI authenticated and push access, and it builds the pushed ref — commit & push first, since CI never sees local uncommitted changes.
The per-platform sections below are the manual equivalents if you'd rather run one target at a time.
npm run tauri build -- --target universal-apple-darwinOutput: src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg
and .../macos/*.app.
npm run tauri buildOutput: src-tauri\target\release\bundle\msi\*.msi and
...\nsis\*.exe.
Don't build --apk --debug with no target filter — that produces a
"universal" APK bundling all 4 CPU architectures with unstripped debug
native libraries and no code shrinking, which comes out to ~475MB.
Always restrict to your phone's architecture (almost every phone made since
~2017 is aarch64; check yours under Settings → About phone if unsure).
Recommended — small AND directly installable in one command:
npm run android:release # aarch64 by default, ~6.9MB
npm run android:release -- armv7 # or: aarch64 | armv7 | i686 | x86_64This runs scripts/android-release-signed.sh:
builds a release APK (LTO + strip + R8 shrinking) for the target
architecture, then zipaligns and signs it with your local
~/.android/debug.keystore — so the result is small and installable
without any extra steps. Output:
src-tauri/gen/android/app/build/outputs/apk/universal/release/gklsan_wifi_transfer-release-signed.apk.
Install via adb install <path> or copy it to the phone and tap it (allow
"install from unknown sources" the first time). Only files with
-signed in the name are ever installable —
app-universal-release-unsigned.apk (the plain Gradle output, also present
alongside it) will fail with "app not installed as package appears to be
invalid" if you tap it directly, since it has no signature at all.
Signing with the debug keystore is fine for sideloading to your own device, but not valid for Play Store distribution — that needs a real release keystore, not set up in this project yet.
Quick debug build (no signing step needed, Gradle auto-signs debug builds, but bigger since nothing is optimized/shrunk):
npx tauri android build --apk --debug -t aarch64 # ~129MBRun it live for development:
npx tauri android devThis deploys to a connected device or running emulator and streams logs.
Emulator networking limitation: the Android emulator's default networking on macOS/Linux is NAT'd into an isolated subnet — it will not see real LAN UDP broadcasts from other devices (including your host machine). Use the emulator only to confirm the APK installs/launches and the UI renders. For actual discovery/transfer testing, install on a physical Android phone on the same WiFi network as your other test device.
- Put all devices on the same WiFi network (same subnet — most home networks are fine; corporate/guest networks sometimes isolate clients from each other, which will break discovery).
- Launch Gklsan Wifi Transfer on each device. They should appear in each other's "Nearby devices" list within a few seconds.
- Click Send files on one device, pick files, and the receiving device gets an Accept/Reject prompt — nothing transfers until it's accepted.
- Settings (gear icon) lets you rename the device, change the save folder (desktop only — Android always saves to the app's own storage, no SAF folder picker yet), and toggle Quick mode / max-parallel-connections (default 4) for how many files transfer simultaneously.
If a firewall is active (Windows Defender Firewall, macOS Application Firewall), you may be prompted to allow Gklsan Wifi Transfer to accept incoming network connections the first time it runs — allow it, otherwise peers won't be able to open the control/data TCP connections to you.
.github/workflows/build.yml builds all three platforms on every push to
main and on v* tags:
- build-macos → universal
.dmg/.app - build-windows →
.msi/.exe - build-android → debug-signed
.apk
Pushing a tag like v0.1.0 additionally runs the release job, which
attaches all three artifacts to a GitHub Release — the easiest way to get a
Windows or Android build without owning that hardware:
git tag v0.1.0
git push origin v0.1.0For manual/non-tag runs, artifacts are still uploaded per-job and
downloadable from the Actions run page (Actions → the workflow run →
Artifacts section) for 90 days by default. npm run dist:ci automates that
whole loop — dispatch the workflow, wait, and pull every artifact into
dist-ci/ locally (see §3 One command).
- Android/cargo build fails with
rustc X.Y.Z is not supported by the following packages(e.g.darling,plist,timerequiring a newer rustc): you have Rust installed both via rustup and via Homebrew, and Homebrew's olderrustcis winning thePATHrace. This happens because~/.zshenvsourcing. "$HOME/.cargo/env"early prepends~/.cargo/bin, but Homebrew'sbrew shellenv(typically in~/.zprofile, which runs after.zshenvfor login shells) prepends/usr/local/binor/opt/homebrew/binagain afterwards — and since~/.zshrcsourcingcargo/enva second time is a no-op (it only prepends if~/.cargo/binisn't already inPATH), Homebrew ends up first. Confirm withwhich rustc— it should print something under$HOME/.cargo/bin, not/usr/local/binor/opt/homebrew/bin. Permanent fix: add this as the very last line of~/.zshrc:Open a new terminal (orexport PATH="$HOME/.cargo/bin:$PATH"
source ~/.zshrc) and re-checkwhich rustc. This project's~/.zshrcalready has this fix applied as of the initial setup — if you're on a different machine or it regressed, this is the fix to reapply. Doesn't require uninstalling Homebrew'srustformula, just makes rustup's shim win. npm installwarns about plaintext HTTP to the npm registry: that's a pre-existingregistry=http://...entry in your global~/.npmrc, unrelated to this project. Consider switching it tohttps://at some point.- Two local instances don't see each other: check nothing else on the
machine is already bound to UDP 58211 without
SO_REUSEPORTsupport (unlikely, but a stale process from a previous run can hold the port —lsof -i :58211to check), and confirm both processes actually started (ps aux | grep gklsan_wifi_transfer). - Android build fails on NDK/SDK version mismatches: re-run
sdkmanager --list_installedand make sure thendk;<version>folder matches whatANDROID_NDK_HOMEpoints at.