A free, open-source VPN client for Android and iOS that survives censorship.
Paste a config, tap connect. No account, no ads, no telemetry.
Blackout Kit is an open-source VPN client for Android and iOS, built with Flutter. It turns a subscription link, a config file or a QR code into a one-tap connection, and it is designed for censorship circumvention in Iran — where a VPN that cannot survive filtering is no VPN at all.
It speaks the protocols Iranian providers actually hand out: VLESS, VMess, Trojan, Shadowsocks, WireGuard, Hysteria 2, TUIC, AmneziaWG and Cloudflare WARP, served by an in-process Xray core or a bundled sing-box runtime.
- No account, no ads, no telemetry. Nothing leaves the device unless you turn on the optional exit-IP lookup.
- Configs are encrypted at rest, with the key in the Android Keystore (see SECURITY.md).
- Accessibility is a tracked requirement, not an afterthought — see ACCESSIBILITY.md for what we support, what we know is missing, and how to report a barrier.
- Servers are tested before they are used — a dead config is skipped instead of becoming a "connected but nothing loads" tunnel.
- DNS leak prevention with per-provider selection, including the Iranian resolvers (شکن، بگزار، رادار، الکترو) that exist to get around sanctions.
Download the APK for your phone from the latest release:
| Build | Use it for |
|---|---|
arm64-v8a |
Almost every phone made since 2017 |
armeabi-v7a |
Older 32-bit devices |
universal |
If you are not sure |
Then: Import → paste your vless://, vmess://, trojan://, ss://,
WireGuard .conf or subscription link → Connect.
Building from source instead? See Setup.
Addresses in these screenshots are documentation placeholders (203.0.113.0/24).
Verified on hardware (Android): the in-process Xray path carries live traffic end to end — a real config on a real device, a TikTok video loading in under a second (2026-10-16).
Android runtime status: this repository retains the existing in-process Xray
path and sing-box/tun2socks subprocess path. The Rust native/blackout-core
workspace is experimental scaffolding, not a production tunnel: its current TUN
loop counts/parses packets but does not forward flows to protocol servers or
inject replies. The Rust protocol modules have not been verified for interoperability.
The v1.18.0 tagged build's Rust CI step failed before APK packaging, so that run
produced no APK artifacts. Any later or separately published release must be
checked independently; this statement refers specifically to that failed CI run.
A successful service/engine startup or local SOCKS5 readiness handshake is not proof of end-to-end Internet traffic. Only the Xray path has the hardware evidence recorded above; other protocol paths must be treated according to the current phase/test notes, not inferred from their config parsers or module names.
iOS: the repository does not yet have a verified Rust XCFramework and PacketTunnelProvider integration. Do not treat iOS platform labeling or older libbox references as proof of a currently buildable/tested iOS VPN release.
PHASES.md carries the authoritative, current phase status.
The ✅ marks below describe the intended v1.0 feature set. Items marked 🟡 are implemented but unverified, or depend on an engine that is not bundled.
✅ Config-based Architecture
- Fetch VPN configs from trusted GitHub repositories
- Share-link parsing for 13 protocols (
vless://,vmess://,trojan://,ss://,hy2://,hysteria2://,tuic://,anytls://,shadowtls://,awg:///amneziawg://, plus WireGuard.confand.ovpnprofiles) - Auto-deduplication by SHA256 hash
- Local Hive persistence, encrypted at rest since v1.12.1: the config, source and test-result boxes are AES-encrypted with a key held in the Android Keystore, and an existing plaintext library is migrated in place on first launch
🟡 Automatic Testing
- Real TCP reachability and latency measurement per config
- Site check: top candidates are verified by fetching real websites through them in a headless core instance (no VPN interface, no consent dialog) — a TCP handshake alone cannot prove a config carries traffic. Host list is editable in Settings; the stage can be switched off entirely.
- Reliability scoring (0-100%) from repeated probes
- Config ranking by verified site reachability, then latency
- Non-blocking UI during tests
- Throughput is not measured. The earlier "speed test" derived a number from the config hash and never contacted anything; it has been removed rather than faked.
✅ One-Tap Connection
- Big connect button on home screen
- Auto-select fastest reachable config
- Real-time connection status and uptime
- Kill switch (holds the tunnel open if the engine dies)
✅ Config Library
- Browse all downloaded configs
- Filter by protocol
- Sort by latency, name, or recently added
- Quick details view with metrics
- Add custom GitHub repository sources
✅ Settings Management
- Auto-connect on launch
- Theme selection (light/dark/system)
- Language preferences
- Split tunneling (per-app, real
PackageManagerlist) - DNS leak prevention, with a 35-provider resolver catalogue (see below)
- Protocol preference
- Auto-test intervals
Three, by product decision: English, Persian (فارسی) and Simplified
Chinese (简体中文). The picker is generated from
LocalizationService.supportedLanguages, so adding a language means adding a
complete table — a partial one is what the previous nine-language list was, and
it made the app look broken when someone picked Spanish and the body text
stayed English.
Persian flips the whole tree to RTL. Every visible string goes through the
catalogue — screens, dialogs, snackbars, the status line the connection
controller writes, the feature summaries in Settings, and the launcher label
(values-fa/). test/localization_coverage_test.dart fails the build if a
hardcoded English string appears in a screen, a widget, a controller status
line or a describe() return.
Non-widget code uses LocalizationService.t(key, params) rather than .tr:
.tr reads GetX's active locale, which is null outside the widget tree.
lib/screens/guide_screen.dart is reachable from the help icon on the home
screen and from Settings. Six steps, plain language, no jargon — written for
someone who has never used a VPN and does not know what a "config" is. It is
translated into all three languages like everything else.
- Framework: Flutter 3.22+
- State Management: GetX (reactive .obs, dependency injection)
- Local Storage: Hive key-value persistence (currently unencrypted at rest)
- VPN Bridge: Platform channels (Android/iOS native)
- Testing: Unit + Integration tests
This app replicates architectural patterns from Blackout Kit CLI for consistency:
| Pattern | Purpose |
|---|---|
| Registry | Engine discovery (V2 extensibility) |
| Factory | Config parsing from multiple protocols |
| Config Model | Abstract base + protocol-specific subclasses |
| Atomic Persistence | Write-to-temp + move for safety |
| State Machine | VPN connection lifecycle |
| Dependency Injection | GetX services for decoupling |
| GitHub Integration | Cached API calls with TTL |
lib/
├── models/ # Data models (Config, TestResult, Source)
├── services/ # Business logic (GitHub, Config, Tester, VPN)
├── controllers/ # GetX state management
├── screens/ # UI screens (Home, Library, Settings)
├── widgets/ # Reusable UI components
└── main.dart # App entry + GetX setup
- Flutter 3.22 or higher
- Dart 3.0+
- Android SDK 24+ or iOS 12+
- Git
# Clone the repository
git clone https://github.com/kiacoder/blackout-kit-mobile.git
cd blackout-kit-mobile
# Get dependencies
flutter pub get
# Run the app (Android)
flutter run --release
# Run the app (iOS)
cd ios
pod install
cd ..
flutter run --release -d <device-id>- App automatically fetches configs from trusted sources (GitHub)
- Automatic speed/reliability test begins (3-5 minutes)
- Home screen shows fastest working config
- Tap the big connect button to activate VPN
- Open Library tab
- Tap the "+" button in app bar
- Enter GitHub repository info (owner, repo, branch)
- Confirm — app fetches and tests configs
- Open Settings tab
- Toggle preferences (auto-connect, kill switch, theme, etc.)
- Set preferred protocol or auto-select fastest
- Changes save automatically
flutter test176 tests across ten files:
flutter test test/config_parsing_test.dart
flutter test test/xray_config_test.dart
flutter test test/vpn_session_options_test.dart
flutter test test/settings_claims_test.dart
flutter test test/singbox_config_test.dart
flutter test test/services/engine_availability_test.dart
flutter test test/services/localization_service_test.dart
flutter test test/services/test_result_persistence_test.dart
flutter test test/services/tester_probe_test.dart
flutter test test/services/site_check_settings_test.dartIf flutter test dies with WebSocketException: Invalid WebSocket upgrade request,
a proxy in your environment is intercepting the runner's loopback socket. See the
note in PHASES.md for the workaround.
flutter test integration_test/app_flow_test.dart
flutter test integration_test/engine_hub_test.dartThese run against mocked platform channels. They pass on a machine where the tunnel cannot start at all, so they are regression cover for Dart logic — not proof that the app can connect.
adb shell appops set com.blackoutkit.vpn ACTIVATE_VPN allow # skip the consent dialog
flutter test integration_test/native_tunnel_test.dart -d <device-id>This is the only test that talks to the real android.net.VpnService. It asserts
that prepare() succeeds, that Builder.establish() brings the tunnel up, that
the in-process Xray core starts, that an unbundled protocol is refused rather
than black-holed, and that disconnect tears the interface down. It also holds the
tunnel open for 20 seconds so you can check adb shell ip addr show tun0 from
another shell.
It has never been run as an automated suite, because no Android hardware is available to CI.
On 2026-10-16 the equivalent path was exercised manually on a physical device and passed:
establish() succeeded, the Xray core started against the TUN fd, and real traffic flowed
through the tunnel (a TikTok video loaded in under a second). The remaining value of this
test is regression coverage on a device, not first-light validation.
Dart-level:
- App starts and fetches configs from GitHub
- Configs auto-deduplicate by hash
- Reachability testing completes without freezing the UI
- Library shows correct config count and filtering works
- Settings persist after app restart
- Add custom source adds configs to library
- Engine Hub reports the real bundled-engine count, not 10
On a physical device (nothing below has ever been run):
-
VpnService.prepare()consent dialog appears and is accepted -
Builder.establish()succeeds and the key icon appears in the status bar - The Xray core starts and
isRunningreports true - Traffic actually flows — load a page through the tunnel
-
10.111.222.1/30interface pairs with the10.111.222.2/30core gateway - Disconnect tears the interface down cleanly
- Kill switch holds the tunnel open when the core is killed
- Split tunneling routes only the selected apps
Last corrected: 2026-09-19. Earlier revisions of this section listed the kill switch, split tunneling and DNS leak prevention as future work while marking the VPN integration "complete". Neither was accurate: the native tunnel did not exist, and those three features were implemented as calls to method channels that had no native handler. See
PHASES.mdfor the authoritative per-phase status.
- Project setup with GetX + Hive
- Config models and share-link parsing for all 11 protocols
- GitHub service with caching
- Config persistence
- Real
VpnServicetunnel in Kotlin (BlackoutVpnService) - Xray core bundled and running in-process via
libv2ray(XrayEngine) - Dart-side Xray config generation with a
tuninbound - Connection state machine, honest error surfacing
- Xray-core protocols: VLESS, VMess, Trojan, Shadowsocks, WireGuard
- Sing-box-backed protocols require packaged runtime capability and a local SOCKS5 readiness handshake; this does not verify end-to-end tunnel traffic.
A tunnel can be healthy and still leak every domain to the carrier's resolver,
so the resolver is chosen explicitly rather than inherited. Each entry in
lib/models/dns_provider.dart is a pair, and both addresses go to
VpnService.Builder.addDnsServer — a dead primary falls through to the
secondary instead of to the carrier.
The sanctions and speed groups start with XDNS's own lists (the exact
primary/secondary pairs that single-file Windows client switches between) and
continue with well-known resolvers. The same catalogue is shipped by the
desktop CLI (blackoutkit/dns_engine.py, blackout dns list), so both clients
offer the same addresses.
| Group | Providers |
|---|---|
sanctions |
Shecan, Electro, Begzar, Radar, 403.online, OpenDNS |
speed |
Yandex, Cloudflare (+Security, +Family), Google, DNS.SB, Control D, Level3, Vercara, Comodo, Gcore, Oracle Dyn, Wikimedia, DNS4EU |
extra |
Quad9 (+Unfiltered), AdGuard (+Family, +Unfiltered), CleanBrowsing (+Family), NextDNS, Mullvad, OpenDNS FamilyShield, Yandex Safe/Family, Alibaba, Tencent, 114DNS |
Settings → DNS → DNS resolver lists them grouped by category, with both
addresses on each row; a typed custom address clears the pair. The selection is
stored as dnsProviderKey / customDnsServer / dnsSecondaryServer and is
reset with the rest of the settings.
You do not need a VPN to change your DNS. Settings → DNS → Change DNS only establishes an interface with the resolver attached and no routes, so ordinary traffic keeps going out directly and only name lookups move. No config, no server, no core. This is the answer to "I just want a different resolver".
National resolvers are for direct use, not for tunnelling abroad. A resolver tied to one country answers with that country's frontends, so using Shecan while the tunnel exits in Germany hands the destination a location contradiction — slower, and one more reason for a service to ask you to prove you are human. The app watches for exactly that: when the selected resolver's country and the exit's country disagree, Settings → DNS shows a warning and offers a one-tap switch to a global resolver. It never changes the choice on its own.
- Split tunneling —
addAllowedApplication/addDisallowedApplication, fed from a realPackageManagerapp list - DNS leak prevention —
addDnsServerplus in-core resolution, driven by a 35-provider resolver catalogue (next section) - Kill switch — holds the TUN open when the engine dies; reports Android's always-on VPN state and deep-links to the setting
- These are
VpnService.Builderinputs, so they are applied at connect time. The earlier "activate after connecting" design was not implementable.
- 176 real unit tests covering Xray config generation, option validation, share-link parsing, engine-availability gating, sing-box config, settings claims, test-result persistence and probe behaviour
- R8 + resource shrinking
- APK size: fat release APK 109.8 MB (down from 354 MB debug); per-ABI splits 57.6 MB (arm64-v8a) / 52.6 MB (armeabi-v7a)
- Unused Xray geo databases excluded (−7.5 MB on disk)
- Unreferenced screenshots removed from the asset bundle (−2.6 MB per split)
- Corrected README / PHASES status claims
- Saved VLESS / VMess / Trojan / Hysteria2 / TUIC configs were dropped from the
library on every app start.
ConfigService._configFromMaphad explicit branches only for WireGuard, OpenVPN and Shadowsocks and returnednullfor everything else;getAllConfigs()discards nulls. Every record now falls back to re-parsing its storedrawUri, and a round-trip test covers all six. xhttpwas emitted as a transport name the bundled core does not know. Share links usingtype=xhttportype=splithttpwere normalised tonetwork: "xhttp". A byte scan oflibgojni.sofindssplithttp14 times and the literalxhttpnever — only the settings keyxhttpSettings, which is a different thing. Both spellings now normalise tosplithttp.- WireGuard profiles lost their
AllowedIPs,PresharedKeyandPersistentKeepalive, and the parser wrote[Interface] Addressinto the field the UI shows as the server, so the config list displayed the device's own tunnel IP as the host.validate()also used||where both halves are required. - WireGuard config parsing and runtime path are present. Do not infer native Rust support; see the current migration status below.
- The engine hub's availability count used
any, notevery. AmneziaWG lists['amneziawg', 'wireguard']and WARP lists['warp', 'wireguard'], so both cards would have shown as available the moment WireGuard became servable — despite neither engine being bundled. The rule is nowengineIsRunnable(tested), and the honest count is 3 of 10.
- The Android app still includes the established Xray and sing-box/tun2socks paths; do not remove them during Rust development.
- The new
native/blackout-coreRust code is experimental. The TUN loop counts/parses packets but does not route TCP/UDP flows or inject replies, and protocol modules are not verified for interoperability. - The
v1.18.0tagged CI run failed at the Rust dependency build step before APK packaging. The working-tree Quinn substitution is being tested separately with the Rust-only Android CI workflow. - iOS Rust support is not implemented/verified: no verified Rust XCFramework + PacketTunnelProvider integration or device traffic test.
- End-to-end verification requires an on-device test proving outbound and return traffic traverse the Rust TUN path; config parsing, process startup, and a local SOCKS greeting are not substitutes.
- Android 5.0+ (Lollipop API 21) & ARMv7 Support: Support low-RAM legacy devices (
ro.config.low_ram=true) without triggering thelmkd(Low Memory Killer Daemon) eviction threshold. - Sub-25MB In-Process Tunnel: Optimized native
libblackout.so(~1.8MB) compiled with-fPIC, consuming only ~11MB RSS inside the Android framework thread. - Hardware Crypto Auto-Negotiation: Automatically prioritize ChaCha20-Poly1305 with ARMv7 NEON SIMD vectorization on older devices lacking hardware
AES-NI, delivering full line-rate throughput without battery drain. - Zero-Allocation Socket Ring Buffers: Fixed MTU-sized buffer pools preventing Android Dalvik/ART GC pauses from dropping network packets on weak dual-core/quad-core legacy phones.
The app is designed to scale to Blackout Kit V2 engines without UI changes:
// Add new protocol without modifying UI:
abstract class Config {
String get protocol;
Future<bool> connect();
}
// V2 engines register like this:
EngineRegistry.register('v2ray', V2RayConfig);
EngineRegistry.register('trojan', TrojanConfig);
// UI automatically discovers and displays them- Reduce exposure of traffic to local network observers when a VPN tunnel is successfully established and carrying traffic
- Circumvent regional censorship when the selected server and protocol are reachable
- Protect traffic on public WiFi when the active engine is functioning correctly
- Reduce IP/DNS leaks through the configured tunnel; verify behavior per platform and engine
- Trusts configured VPN servers (supply chain security)
- No protection against application-level leaks (DNS, WebRTC)
- VPN provider can see your traffic
- No multi-hop routing
- Application and engine integration sources are available for review
- Existing third-party native components are documented in their respective licenses
- Community code review
- Reproducible builds planned
The repository contains iOS application and tunnel-extension code, but this
document does not treat source presence, compile claims, or config generation as
proof of a usable iOS VPN. The new Rust core has no verified iOS XCFramework or
PacketTunnelProvider integration. Confirm the current iOS workflow and
entitlements independently; a paid Apple Developer account may be required for
signed VPN installation, but that does not replace code or traffic verification.
No iOS Rust-engine release or end-to-end iOS traffic verification is claimed here.
- Check existing issues first
- Open a new issue with:
- Device model and OS version
- Exact steps to reproduce
- Error messages or logs
- Expected vs actual behavior
- Fork the repository
- Create a branch:
git checkout -b feature/amazing-feature - Follow the code patterns (see Architecture section)
- Add tests for new features
- Keep commits atomic and descriptive
- Open a pull request with description
- Follow Flutter/Dart conventions
- Use GetX for state management (not Provider)
- Hive for persistence (not SQLite)
- No commented-out code or debug prints
- Tests required for new features
Open-source under the MIT License. See LICENSE for details.
Use at your own risk. This VPN app is for educational purposes and comes with no warranty. Users are responsible for:
- Complying with local laws regarding VPN use
- Choosing trustworthy VPN servers
- Understanding the limitations (see Threat Model)
- Protecting their credentials and private keys
- Flutter and Dart teams for the framework
- GetX author for the state management library
- Hive for local persistence (currently unencrypted at rest)
- Community members testing and contributing
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Security: security@blackout-kit.dev (or open private security issue)
Made with ❤️ by Blackout Kit contributors
- 💬 Questions & help → Discussions (start with How to ask for connection help)
- 🐛 Bugs & feature requests → Issues (templates included)
- 🔒 Security vulnerabilities → private advisory — never a public issue
- ♿ Accessibility barriers → issues tagged
accessibility(see ACCESSIBILITY.md) - 📜 Everyone interacting here agrees to the Code of Conduct





