Install
Download the installer for your platform from the
Braid releases. Every package
installs two programs: braid, the desktop app, and dl, the same engine on the
command line.
The current release is Braid 0.1.0 beta 3.
| Platform | File | How |
|---|---|---|
| macOS 11+, Apple silicon and Intel | Braid-0.1.0-macos.dmg | Drag to Applications |
| Windows 10+ x86_64 | braid-0.1.0-x86_64.msi | Double-click |
| Windows on ARM | braid-0.1.0-aarch64.msi | Double-click |
| Debian, Ubuntu x86_64 | braid_0.1.0-1_amd64.deb | sudo apt install ./braid_*.deb |
| Debian, Ubuntu ARM64 | braid_0.1.0-1_arm64.deb | sudo apt install ./braid_*.deb |
| Fedora, RHEL x86_64 | braid-0.1.0-1.x86_64.rpm | sudo dnf install ./braid-*.rpm |
| Fedora, RHEL ARM64 | braid-0.1.0-1.aarch64.rpm | sudo dnf install ./braid-*.rpm |
Nothing is code signed yet. On macOS, the first launch needs a right-click on the app, then Open, then confirm once. Windows SmartScreen warns for the same reason. If that is not acceptable, build from source.
On macOS, Braid registers as the handler for magnet: links and .torrent files,
so clicking one in a browser opens it in Braid.
Your first download
- Press + in the toolbar.
- Paste a URL, a magnet link, or the path to a
.torrentfile. - For HTTP, Braid asks the server first: the size, whether ranges really work, and whether there is a validator to resume against.
- Pick where to save it, how many connections to open (8 by default), and which interfaces to use. All means every interface you have ticked in settings.
- If the site publishes a checksum, switch on Verify checksum and paste it.
- Press Add.
The bar under the transfer fills in the colour of each interface that carried a chunk, and the sidebar shows each interface's live speed. Select a transfer and open the inspector (the panel button in the toolbar) for Info, Pieces, Connections and File.
Quitting is safe at any point. Each transfer comes back in the state you left it, and an unfinished one resumes from its journal.
Use every network
Braid only gains speed when your paths are really different roads to the internet.
| Setup | What carries the file | Needs the Android app |
|---|---|---|
| Computer alone | Every interface you select | No |
| Plain USB tethering | The phone shows up as one more network card | No |
| Phone on your network | Your connection plus the phone's mobile data | Yes |
| Phone on a cable | Your connection plus its mobile data plus its Wi-Fi | Yes |
| Several phones | Every network of every phone, each weighted by its speed | Yes |
Open Settings → Network to choose which interfaces Braid may use. Each row shows its address, the Binding mechanism actually in force, and an optional per-interface Limit. Unticked interfaces are never used, and a VPN tunnel shows as excluded.
The Binding column is worth a glance. IP_BOUND_IF on macOS, SO_BINDTODEVICE on Linux or
IP_UNICAST_IF on Windows means the connection is truly pinned to that interface. A fall back to a
plain source-address bind still works, but the traffic is not guaranteed to leave by
that interface, so any aggregation you see may be extra connections over one route.
Resolve hostnames on under DNS chooses which interface looks up the server's name. Fastest interface is the default.
A transfer's paths are fixed when it starts. Plug in the cable or pair the phone first, then start the download.
Add a phone
Braid for Android lends a phone's networks to Braid on your computer. You need Braid for Android on the phone, and Android 8.0 or newer.
Install the app
Download braid-android-0.1.1.apk (Braid for Android 0.1.1, 511 KiB) from the
Braid for Android releases
and open it on the phone. The first time, Android asks you to allow installs from
your browser or file manager. The same APK runs on every processor
(arm64-v8a, armeabi-v7a, x86_64, x86), because it has no native code.
Pair by code
- On the phone, open Braid, tap Start sharing, and switch on Mobile data.
- In Braid on your computer, press Add phone at the foot of the interfaces list, then Show code. The code is good for two minutes.
- On the phone, tap Add to a computer by code, point the camera at the screen, check the computer's name, and tap Add.
The phone appears in Braid's sidebar with its own speed. No Google Play services on the phone? Point the phone's own camera app at the code instead.
Pair without a camera
- In the same sheet on the computer, press Scan. Braid looks for phones on your network and down any cable, and the phone asks you to Allow the computer.
- Or type the phone's address, shown at the bottom of the app, into Or add one by
address. It looks like
192.168.1.40:8710or[2409:…]:8710.
Choose what the phone lends
Each network on the phone has its own switch. Mobile data is the useful one when the phone is on the same Wi-Fi as your computer:
A phone on your Wi-Fi is usually not a second path. It reaches the internet through the same router you do. Braid compares the public address each path leaves from and marks the duplicate Not used, rather than pretending.
A USB cable is the fastest way to connect: the phone's hop to your computer then does not share airtime with your computer's own Wi-Fi, and both the phone's mobile data and its Wi-Fi can be real paths.
Set a limit under Mobile data caps what the phone will spend in a session. When it is reached, sharing on that network stops. Sharing keeps working with the screen off, as a foreground service with a notification that shows what has been used.
Forget a computer
Tap Forget beside a computer on the phone. Its key is deleted and it is cut off at once. Pairing keys are stored on the phone only as SHA-256 hashes.
Verify the APK
Every release is signed with the same InfoDive Labs certificate. An APK that does not match it did not come from us.
apksigner verify --print-certs braid-android-0.1.1.apkThe certificate's SHA-256 fingerprint is:
04:C0:0D:53:6A:D1:DB:AD:25:90:C8:43:3C:41:6B:EA:6A:66:4C:9E:2B:4C:B6:E5:49:C2:4F:80:B5:08:77:E2
Limits and schedules
Settings → Bandwidth sets a download and an upload limit, each Unlimited or Custom. Switch on Limit bandwidth on a schedule and paint the hours you want capped on the week grid. Outside the painted windows Braid runs unlimited, and a change reaches transfers that are already running.
Ignore limits for transfers I start manually lets a download you start by hand run at full speed while queued and scheduled ones stay capped.
A per-interface limit in Settings → Network is a ceiling on that link, shared by every connection over it. Use it for a metered tether.
Integrity and durability
Every chunk is hashed with BLAKE3 as it lands. A chunk that fails its hash is fetched again on its own, so a single bad chunk costs its size, not the whole download.
Under Settings → Integrity:
- Verify every completed transfer checks the whole file once it finishes.
- Checksum algorithm is BLAKE3, SHA-256 or MD5 for an expected digest.
- Re-fetch only the damaged part keeps the rest of a file whose check fails.
Durability modes
Braid writes chunks into one preallocated .part file beside the destination, with a
.dlmeta journal next to it. The rule that never changes:
Chunk data is made durable before the journal claims the chunk.
The modes only change how often that pair happens:
| Mode | Flushes | Journal |
|---|---|---|
| Safe | every chunk | waits for the drive |
| Balanced (default) | every 5 s or 64 MiB | waits for the drive |
| Fast | every 30 s or 512 MiB | left to the OS |
A looser mode never risks a wrong file, only more re-downloading.
What Braid refuses
These produce an error rather than a file:
- a server that advertises
Rangeand then ignores it - a
200where a206was asked for, or a changedContent-Rangetotal - a ranged response with a non-identity
Content-Encoding - an
ETagthat changes mid-download - a login or error page served with a plausible
Content-Length
Expiring links
Pre-signed links from S3, cloud storage and CDNs are re-resolved before they lapse. Sixteen connections hitting an expired link produce one refresh, not sixteen. A link signed for one source address is resolved once per interface, because each path leaves from a different address.
On the command line, --refresh-command names a program that prints a fresh URL,
either bare or as {"url": ..., "headers": {...}}. It is off unless you ask for it,
and it runs directly, never through a shell:
dl add "$URL" --refresh-command "yt-dlp -g https://example.com/watch?v=..."Torrents
Paste a magnet link or a .torrent file into Add. A torrent expands in the list to
show its files, each with its own progress, and reports peers while it runs.
Torrents obey the same limits and schedule as HTTP transfers.
Keep seeding after a torrent completes is under Settings. Off, a torrent stops when the last piece arrives.
Seeding is publication. Joining a swarm announces your address to every peer and tracker in it, and binding a transfer to one interface is not a privacy control.
Braid is a client, not an index. There is no search and no bundled tracker list. A torrent currently uses one interface at a time.
The dl command
dl is the same engine without the window. It prints a single-line progress bar on
a terminal, and Ctrl-C pauses rather than kills: run the same command again and it
resumes from the journal.
dl interfaces # every interface and the binding in force
dl probe <url> # size, ranges, validator, without downloading
dl add <url> [options] # downloadCommon recipes
# Spread over two interfaces and verify against a published SHA-256
dl add https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso \
--interfaces en0,en5 --verify sha256:9d3f...
# Every interface that has a gateway, 16 connections
dl add "$URL" --all-interfaces -n 16
# Cap the metered tether, and lift a daytime cap overnight
dl add "$URL" --all-interfaces --limit-interface en5=5M \
--limit 20M --at 23:00-07:00=off
# Two mirrors of the same file, each a lane of its own
dl add https://mirror-a.example/file.iso --mirror https://mirror-b.example/file.isodl add options
| Option | What it does |
|---|---|
-o, --output <path> | Where to write. For a torrent, the folder its files land in. |
-n, --connections <n> | Parallel connections. Default 8. |
--interfaces <names> | Spread across these interfaces, e.g. en0,en5. A misspelled name is an error. |
--all-interfaces | Spread across every interface that has a gateway. |
--limit <rate> | Cap this download, e.g. 500K, 2M. |
--limit-interface <name=rate> | Cap one interface. Repeatable. |
--at <HH:MM-HH:MM=rate> | Apply a limit only in a window; =off means unlimited. Repeatable. |
--verify <digest> | sha256:<hex>, blake3:<hex>, md5:<hex>, or a bare BLAKE3 digest. |
--durability <mode> | safe, balanced (default) or fast. |
--chunk-size <size> | Chunk size. Chosen from file size and connections if omitted. |
--verify-existing | On resume, re-read existing chunks and re-fetch any that fail. |
--mirror <url> | Another URL for the same file. Must agree on length and validator. |
--refresh-command <cmd> | A program that prints a fresh URL when the link expires. |
--head <size> | Fetch only the first N bytes, for testing large files. |
--no-resume | One stream, no journal, nothing to resume. |
--force | Overwrite the destination if it exists. |
A bare 64-character digest is read as BLAKE3, because that is what dl prints. Paste a
SHA-256 from a release page with its sha256: prefix.
Troubleshooting
The phone's Wi-Fi says Not used. Working as intended: the phone is on the same network as your computer, so its Wi-Fi is not a second path. Use its mobile data, or connect it by cable.
The phone does not appear after pairing. A phone joins the next transfer, not one already running. Start a new download. If it still does not appear, check that sharing is on and at least one network is switched on in the app.
Scanning finds nothing. Some networks drop the multicast discovery uses. Use Show code instead: it needs no discovery at all.
A path shows a speed but the total does not rise. Open Settings → Network and check the Binding column. A source-address fall back means the connection may not be leaving by that interface.
macOS says the app cannot be opened. It is not code signed yet. Right-click it, choose Open, and confirm.
The download failed with a range or encoding error. The server did something that would have produced a wrong file. Try again later or use a mirror; it is not something a retry inside Braid can fix safely.
Build Braid from source
git clone https://github.com/InfoDiveLabs/braid && cd braid
cargo build --release # both binaries
cargo test --workspace # 507 tests
cargo run -p xtask -- package # the host platform's installerRust 1.92 or newer. Linux also needs libxkbcommon-dev and libfontconfig-dev.
Packaging builds only the host platform: .dmg on macOS (stock hdiutil and
iconutil), .deb and .rpm on Linux (cargo-deb, cargo-generate-rpm), .msi on
Windows (cargo-wix).
How the code is laid out
| Crate | Role |
|---|---|
dl-core | The engine: scheduling, chunking, crash-safe storage, link refresh. No UI, no sockets. |
dl-net | The only crate that knows about sockets and NICs: enumeration, per-interface binding, HTTP, phone pairing. |
dl-torrent | BitTorrent over librqbit. |
dl-testkit | A misbehaving origin server, fault injection and the adversarial scenario catalogue. |
dl-cli | The dl binary. |
dl-gui | The braid binary, a Slint UI. |
xtask | Screenshots, size checks, packaging. |
dl-core reaches the network only through ByteSource and LaneSet, and the disk only
through Storage, so the engine runs against a lying server and a failing drive with
no sockets involved. dl-gui never depends on dl-net directly.
Rules worth knowing before a change
- Journal ordering.
Journal::flushis the only place the durable-before-claimed order is expressed. Read it before touchingdl-core/src/store. Accept-Encoding: identityon every ranged request. Ranges address encoded bytes.Interface.nameis the identity. It keys the socket option, saved selections and limits. The display name is presentation only.- One writer. Windows
seek_writemoves the file cursor, so chunks go through a single writer task. - One UI, two token files.
ui/platform/{macos,generic}.slinthold per-platform values. A token added to one must be added to the other, or that target stops compiling.
Try the misbehaving server
cargo run -p dl-cli --features devtools -- dev scenarios
cargo run -p dl-cli --features devtools -- dev serve <scenario>Each scenario serves a file the way a broken server would, and prints the URL and the
expected BLAKE3 so you can point dl add at it.
Check other platforms from a Mac
Linux, full test suite in Docker:
docker run --rm -v "$PWD":/src:ro -v /tmp:/tmp rust:1.92-bookworm bash -c '
apt-get update -qq && apt-get install -y -qq libxkbcommon-dev libfontconfig1-dev pkg-config
cd /src && export RUSTFLAGS="-D warnings" CARGO_TARGET_DIR=/tmp/lt SLINT_BACKEND=headless
cargo test --workspace --all-features'DL_UI_PLATFORM=generic cargo run -p xtask -- screenshots --out artifacts/generic
renders the Windows and Linux look from any host.
Build Braid for Android
git clone https://github.com/InfoDiveLabs/braid-android && cd braid-android
./gradlew :relay:test # 99 JVM tests
./gradlew :app:assembleDebug # debug APK in app/build/outputs/apk/debug
./gradlew :app:dist # signed release APK in dist/JDK 17 and the Android SDK with platform 35. Point local.properties at the SDK with
sdk.dir=..., or set ANDROID_HOME.
The project is two modules. relay/ is plain JVM Kotlin with no Android dependency:
the proxy on port 8710, request parsing, pairing, lane routing and egress checks, all
testable without a phone. app/ is the Android app around it: binding each lane to an
Android Network, the foreground service, the screen and pairing.
The rule the proxy lives by:
A request leaves by the network it names, or not at all.
A lane that cannot serve answers 503, and nothing substitutes another lane. The
proxy never adds Accept-Encoding, decompresses, caches or retries: the bytes are not
the phone's to touch.
Checks that need a real phone, over adb with both devices on one network:
curl -x "http://cell:$KEY@[phone]:8710" https://api.ipify.org # leaves from cell's egress
curl -x "http://cell:$KEY@[phone]:8710" -r 100-199 <url> # 206, exactly 100 bytes
adb shell svc data disable # cell becomes 503, never Wi-FiRelease builds read the signing key from .env or the environment: BRAID_KEYSTORE,
BRAID_KEYSTORE_PASSWORD, BRAID_KEY_ALIAS, BRAID_KEY_PASSWORD, or
BRAID_KEYSTORE_BASE64 for CI. Pushing a v* tag builds, signs and attaches the APK
to a release.
Contributing
Open an issue first for anything beyond a bug fix. Every change must leave these green:
cargo fmt --all
cargo clippy --workspace --all-targets # silent, warnings included
cargo test --workspace- Tests describe the property.
a_run_to_midnight_does_not_wrap, nottest_schedule_2. - New behaviour needs a test that fails without it, especially in storage and networking, where failures are silent.
- Nothing in the UI may claim to do something it does not. A control that does nothing is worse than an absent one.
- Per-interface binding changes say which platforms they ran on and how you confirmed the socket option took effect.
- Pull requests adding search, tracker lists or any content directory are declined.
The most useful contribution right now needs no Rust: if you have a Windows or Linux machine, install Braid and report what happens.
What is not verified
- Windows at runtime. CI builds and tests it; nobody has installed and used it.
IP_UNICAST_IFbinding is written from Microsoft's documentation. - The Linux desktop. The
.desktopfile andxdg-mimeregistration have not been run on a Linux desktop. - Real multi-NIC hardware. CI proves binding with virtual interfaces, not two real uplinks.
- Braid for Android beyond one phone. Tested on a Pixel 7 Pro on Jio, an IPv6-only carrier. Other makers, IPv4 carriers and dual-SIM phones have not been tried, and the app is not on Google Play.
Licences
Braid is free software under the GNU General Public License version 3. Braid for Android is source-available under the PolyForm Noncommercial License 1.0.0: free for any noncommercial use, and commercial use needs a licence from InfoDive Labs.