Skip to content

About

Open-source VPN client for Android and iOS - Xray, sing-box, WireGuard and Hysteria 2 configs, built for censorship circumvention in Iran and beyond...

Topics

Resources

Code of conduct

Contributing

Security policy

Accessibility

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

212 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Blackout Kit logo - open-source VPN client for Android and iOS

Blackout Kit

A free, open-source VPN client for Android and iOS that survives censorship.
Paste a config, tap connect. No account, no ads, no telemetry.

Latest release License GitHub stars Platforms: Android and iOS


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.

Install

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.

Screenshots

Blackout Kit connected screen: green CONNECTED button, exit location resolved to Frankfurt, and a status line confirming the tunnel carries traffic Blackout Kit statistics: latency, number of stored configs, how many are working, and measured reliability Blackout Kit DNS leak prevention settings: resolver picker with Cloudflare selected, and the option to change DNS without a VPN

Blackout Kit settings: auto-connect on launch, kill switch, split tunnelling and a manual check for updates Blackout Kit language picker showing English, Persian and Simplified Chinese

Addresses in these screenshots are documentation placeholders (203.0.113.0/24).

Status

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.

Features

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 .conf and .ovpn profiles)
  • 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 PackageManager list)
  • DNS leak prevention, with a 35-provider resolver catalogue (see below)
  • Protocol preference
  • Auto-test intervals

Languages

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.

The guide

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.

Architecture

Tech Stack

  • 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

Patterns

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

Project Structure

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

Setup

Prerequisites

  • Flutter 3.22 or higher
  • Dart 3.0+
  • Android SDK 24+ or iOS 12+
  • Git

Installation

# 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>

Usage

First Run

  1. App automatically fetches configs from trusted sources (GitHub)
  2. Automatic speed/reliability test begins (3-5 minutes)
  3. Home screen shows fastest working config
  4. Tap the big connect button to activate VPN

Add Custom Config Source

  1. Open Library tab
  2. Tap the "+" button in app bar
  3. Enter GitHub repository info (owner, repo, branch)
  4. Confirm — app fetches and tests configs

Manage Settings

  1. Open Settings tab
  2. Toggle preferences (auto-connect, kill switch, theme, etc.)
  3. Set preferred protocol or auto-select fastest
  4. Changes save automatically

Testing

Unit Tests

flutter test

176 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.dart

If 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.

Integration Tests

flutter test integration_test/app_flow_test.dart
flutter test integration_test/engine_hub_test.dart

These 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.

Native Tunnel Test — requires a physical device, never yet run

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.

Manual Testing Checklist

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 isRunning reports true
  • Traffic actually flows — load a page through the tunnel
  • 10.111.222.1/30 interface pairs with the 10.111.222.2/30 core 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

Roadmap

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.md for the authoritative per-phase status.

Phase 1 — Foundation ✅ Complete

  • Project setup with GetX + Hive
  • Config models and share-link parsing for all 11 protocols
  • GitHub service with caching
  • Config persistence

Phase 2 — VPN engine ✅ Android implementation complete; traffic verification incomplete

  • Real VpnService tunnel in Kotlin (BlackoutVpnService)
  • Xray core bundled and running in-process via libv2ray (XrayEngine)
  • Dart-side Xray config generation with a tun inbound
  • 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.

DNS resolver catalogue

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.

Phase 3 — Security features ✅ Complete (corrected design)

  • Split tunneling — addAllowedApplication / addDisallowedApplication, fed from a real PackageManager app list
  • DNS leak prevention — addDnsServer plus 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.Builder inputs, so they are applied at connect time. The earlier "activate after connecting" design was not implementable.

Phase 4 — Tests, docs, release ✅ Complete

  • 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

Phase 4b — Bugs found while auditing the phase claims ✅

  • Saved VLESS / VMess / Trojan / Hysteria2 / TUIC configs were dropped from the library on every app start. ConfigService._configFromMap had explicit branches only for WireGuard, OpenVPN and Shadowsocks and returned null for everything else; getAllConfigs() discards nulls. Every record now falls back to re-parsing its stored rawUri, and a round-trip test covers all six.
  • xhttp was emitted as a transport name the bundled core does not know. Share links using type=xhttp or type=splithttp were normalised to network: "xhttp". A byte scan of libgojni.so finds splithttp 14 times and the literal xhttp never — only the settings key xhttpSettings, which is a different thing. Both spellings now normalise to splithttp.
  • WireGuard profiles lost their AllowedIPs, PresharedKey and PersistentKeepalive, and the parser wrote [Interface] Address into 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, not every. 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 now engineIsRunnable (tested), and the honest count is 3 of 10.

Native engine migration — status 2026-11-01

  • The Android app still includes the established Xray and sing-box/tun2socks paths; do not remove them during Rust development.
  • The new native/blackout-core Rust 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.0 tagged 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.

Phase 6 — Extreme Low-Spec, Potato Android & Sub-25MB Architecture (Planned) 🥔

  • Android 5.0+ (Lollipop API 21) & ARMv7 Support: Support low-RAM legacy devices (ro.config.low_ram=true) without triggering the lmkd (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.

V2 Extensibility

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

Threat Model & Security

Intended Protection Goals

  • 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

Limitations

  • 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

Open Source for Transparency

  • 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

iOS Status

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.


Contributing

To Report Issues

  1. Check existing issues first
  2. Open a new issue with:
    • Device model and OS version
    • Exact steps to reproduce
    • Error messages or logs
    • Expected vs actual behavior

To Contribute Code

  1. Fork the repository
  2. Create a branch: git checkout -b feature/amazing-feature
  3. Follow the code patterns (see Architecture section)
  4. Add tests for new features
  5. Keep commits atomic and descriptive
  6. Open a pull request with description

Code Standards

  • 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

License

Open-source under the MIT License. See LICENSE for details.

Disclaimer

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

Acknowledgments

  • 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

Support


Made with ❤️ by Blackout Kit contributors

Community

About

Open-source VPN client for Android and iOS - Xray, sing-box, WireGuard and Hysteria 2 configs, built for censorship circumvention in Iran and beyond...

Topics

Resources

Code of conduct

Contributing

Security policy

Accessibility

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages