v2ray_box 1.0.0
v2ray_box: ^1.0.0 copied to clipboard
A Flutter plugin for VPN connectivity with dual-core support (Xray-core & sing-box). Supports VLESS, VMess, Trojan, Shadowsocks, Hysteria, TUIC, WireGuard and more. Switch between cores at runtime.
V2Ray Box #
A Flutter plugin for VPN functionality with dual-core support: Xray-core and sing-box. Switch between cores at runtime without reinstalling.
Platform Support #
| Platform | Status | Mode |
|---|---|---|
| Android | Supported | VPN / Proxy |
| iOS | Supported | VPN (via NetworkExtension) |
| macOS | Supported | Proxy (system proxy) |
Features #
- Dual Core Engine — Xray-core and sing-box with runtime switching
- Multiple V2Ray protocols (VMess, VLESS, Trojan, Shadowsocks, Hysteria, Hysteria2, TUIC, WireGuard, SSH)
- Multiple transports (WebSocket, gRPC, HTTP/H2, HTTPUpgrade, xHTTP, QUIC)
- TLS, Reality, uTLS fingerprint, Multiplex support
- VPN and Proxy modes
- Real-time traffic statistics (Xray stats API / sing-box Clash API)
- Connection status monitoring
- Config link parsing and validation
- Single and parallel batch ping testing with streaming results
- Per-app proxy (Android only)
- Customizable notification (Android)
- Persistent total traffic storage
Installation #
dependencies:
v2ray_box:
path: ../v2ray_box
Platform Setup #
Important: Core binary files are not included in this plugin due to their large size. You must download them and place them in the correct directory in your own app.
Android — Xray-core #
- Download
libv2ray.aarfrom AndroidLibXrayLite releases - Place in your app's
android/app/libs/:
your_app/android/app/libs/libv2ray.aar
- Add to
android/app/build.gradle(orbuild.gradle.kts):
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- Enable
useLegacyPackagingfor JNI libs inandroid/app/build.gradle:
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
Android — sing-box #
- Go to sing-box releases and download the Android builds for each architecture you want to support:
| Architecture | Download file | Target devices |
|---|---|---|
arm64-v8a |
sing-box-*-android-arm64.tar.gz |
Most modern phones & tablets |
armeabi-v7a |
sing-box-*-android-armv7.tar.gz |
Older 32-bit ARM devices |
x86_64 |
sing-box-*-android-amd64.tar.gz |
x86 emulators, Chromebooks |
x86 |
sing-box-*-android-386.tar.gz |
Older 32-bit x86 emulators |
-
Extract each
.tar.gzfile. Inside you will find a single binary namedsing-box. -
Rename each
sing-boxbinary tolibsingbox.so(Android only loads native libraries withlibprefix and.soextension). -
Place each renamed binary in the matching
jniLibsfolder inside your app:
your_app/
└── android/
└── app/
└── src/
└── main/
└── jniLibs/
├── arm64-v8a/
│ └── libsingbox.so ← from android-arm64
├── armeabi-v7a/
│ └── libsingbox.so ← from android-armv7
├── x86_64/
│ └── libsingbox.so ← from android-amd64
└── x86/
└── libsingbox.so ← from android-386
Tip: If you only target modern phones you can just add
arm64-v8a. For emulator testing addx86_64too. You don't need all four architectures — only the ones your app supports.
Requirements: minSdk 24, JDK 17
iOS — sing-box (default core) #
iOS uses the HiddifyCore.xcframework (which wraps sing-box) and runs as a VPN via NetworkExtension (PacketTunnel).
-
Download
HiddifyCore.xcframeworkfrom hiddify-core releases (choose the iOS build). -
Place it in your app's
ios/Frameworks/directory:
your_app/
└── ios/
└── Frameworks/
└── HiddifyCore.xcframework/
├── Info.plist
├── ios-arm64/
│ └── HiddifyCore.framework/
└── ios-arm64_x86_64-simulator/
└── HiddifyCore.framework/
-
Add a PacketTunnel Network Extension target to your Xcode project:
- Open
ios/Runner.xcworkspacein Xcode - File → New → Target → Network Extension → Packet Tunnel Provider
- Set the bundle identifier to
$(MAIN_BUNDLE_ID).PacketTunnel - Add the HiddifyCore framework to this target
- Open
-
Create an App Group (e.g.
group.com.example.yourApp) and enable it for both the main app and the PacketTunnel extension. -
Add to your
Info.plist(orRunner.entitlements):com.apple.developer.networking.networkextensioncapability- App Groups capability
Requirements: iOS 15.0+, Xcode 14+
iOS — Xray-core #
Xray-core support on iOS requires a compatible iOS framework (e.g., libXray). The plugin generates Xray-compatible JSON configuration. To use Xray on iOS:
- Download or build a libXray
.xcframeworkfor iOS - Place it in your app's
ios/Frameworks/directory - Update your PacketTunnel extension to use the Xray framework for starting/stopping the tunnel
- Set core engine to
xray:
await v2rayBox.setCoreEngine('xray');
Note: The iOS plugin generates Xray-format JSON configs and passes the
CoreEngineparameter to the PacketTunnel extension. You need to handle the actual Xray framework integration in your PacketTunnel provider.
macOS — sing-box #
macOS uses sing-box as a CLI binary (subprocess) and configures the system proxy automatically.
- Download the macOS sing-box binary from sing-box releases:
| Architecture | Download file |
|---|---|
| Apple Silicon (M1/M2/M3) | sing-box-*-darwin-arm64.tar.gz |
| Intel x86_64 | sing-box-*-darwin-amd64.tar.gz |
| Universal | Use the one matching your development/target Mac |
-
Extract the
.tar.gzfile. Inside you will find a binary namedsing-box. -
Place the binary in your app's
macos/Frameworks/directory:
your_app/
└── macos/
└── Frameworks/
└── sing-box ← the extracted binary
- Make sure it's executable:
chmod +x macos/Frameworks/sing-box
- The binary needs to be bundled with your app. In Xcode, add it to the "Copy Files" build phase targeting
Frameworks.
macOS — Xray-core #
- Download the macOS Xray binary from Xray-core releases:
| Architecture | Download file |
|---|---|
| Apple Silicon (M1/M2/M3) | Xray-macos-arm64-v8a.zip |
| Intel x86_64 | Xray-macos-64.zip |
-
Extract the
.zipfile. Inside you will find a binary namedxray. -
Place the binary in your app's
macos/Frameworks/directory:
your_app/
└── macos/
└── Frameworks/
├── sing-box ← (optional, if using sing-box too)
└── xray ← the extracted binary
- Make sure it's executable:
chmod +x macos/Frameworks/xray
- Set core engine to
xray:
await v2rayBox.setCoreEngine('xray');
Requirements: macOS 10.15+
Usage #
Initialize #
import 'package:v2ray_box/v2ray_box.dart';
final v2rayBox = V2rayBox();
await v2rayBox.initialize(
notificationStopButtonText: 'Disconnect',
);
Switch Core Engine #
// Set core engine (disconnect VPN first if connected)
await v2rayBox.setCoreEngine('xray'); // Xray-core (default)
await v2rayBox.setCoreEngine('singbox'); // sing-box
// Get current core engine
final engine = await v2rayBox.getCoreEngine();
Connect / Disconnect #
await v2rayBox.connect('vless://uuid@server:port?...#Name', name: 'My Config');
await v2rayBox.disconnect();
Monitor Status #
v2rayBox.watchStatus().listen((status) {
// VpnStatus.stopped, starting, started, stopping
});
Monitor Traffic #
v2rayBox.watchStats().listen((stats) {
print('Up: ${stats.formattedUplink}, Down: ${stats.formattedDownlink}');
});
Core Info #
final info = await v2rayBox.getCoreInfo();
// Returns: { "core": "xray", "engine": "xray-core", "version": "26.2.6" }
// or: { "core": "singbox", "engine": "sing-box", "version": "1.12.22" }
Ping #
// Single
final latency = await v2rayBox.ping(configLink);
// Parallel batch with streaming results
final sub = v2rayBox.watchPingResults().listen((result) {
print('${result["link"]}: ${result["latency"]}ms');
});
final results = await v2rayBox.pingAll(links);
await sub.cancel();
VPN Mode #
await v2rayBox.setServiceMode(VpnMode.vpn); // route all traffic
await v2rayBox.setServiceMode(VpnMode.proxy); // local proxy only
Per-App Proxy (Android) #
await v2rayBox.setPerAppProxyMode(PerAppProxyMode.exclude);
await v2rayBox.setPerAppProxyList(['com.example.app'], PerAppProxyMode.exclude);
Total Traffic #
final traffic = await v2rayBox.getTotalTraffic();
print('Total: ${traffic.formattedTotal}');
await v2rayBox.resetTotalTraffic();
Supported Protocols #
| Protocol | Link Format |
|---|---|
| VLESS | vless://uuid@server:port?params#name |
| VMess | vmess://base64_json |
| Trojan | trojan://password@server:port?params#name |
| Shadowsocks | ss://base64(method:password)@server:port#name |
| Hysteria2 | hy2://auth@server:port?params#name |
| Hysteria | hy://server:port?params#name |
| TUIC | tuic://uuid:password@server:port?params#name |
| WireGuard | wg://private_key@server:port?params#name |
| SSH | ssh://user:password@server:port?params#name |
Transports #
type=ws | type=grpc | type=http | type=h2 | type=httpupgrade | type=xhttp | type=quic
TLS Options #
security=tls | security=reality | fp=chrome | alpn=h2,http/1.1 | pbk=...&sid=...
Multiplex #
mux=1 | mux-max-streams=4
Architecture #
Core Engines by Platform #
| Android | iOS | macOS | |
|---|---|---|---|
| Xray-core integration | AAR library (in-process) | xcframework (via PacketTunnel) | CLI binary (subprocess) |
| sing-box integration | CLI binary (subprocess) | HiddifyCore xcframework (via PacketTunnel) | CLI binary (subprocess) |
| VPN Mode | Android VpnService + TUN | NetworkExtension PacketTunnel | N/A (proxy mode only) |
| Proxy Mode | SOCKS/HTTP local proxy | N/A (VPN mode only) | System proxy via networksetup |
| Traffic Stats | Xray stats API / Clash API | Clash API / PacketTunnel IPC | Clash API (sing-box) |
| Config Format | Xray JSON / sing-box JSON | Xray JSON / sing-box JSON | Xray JSON / sing-box JSON |
How Android sing-box VPN Mode Works #
On Android, the plugin uses Xray-core as a TUN bridge for sing-box:
- Android
VpnServicecreates a TUN interface - Xray-core reads from TUN and forwards traffic to sing-box's local SOCKS proxy (
127.0.0.1:10808) - sing-box handles the actual proxy connection to the remote server
This avoids the need for sing-box to manage TUN directly (which requires root on Android when running as a CLI binary).
How iOS VPN Works #
On iOS, the VPN runs as a PacketTunnel Network Extension:
- The main app generates the config JSON (sing-box or Xray format) and passes it to the PacketTunnel extension
- The PacketTunnel extension receives the config + core engine type via VPN tunnel options
- The extension uses the appropriate framework (HiddifyCore for sing-box, libXray for Xray) to start the tunnel
- All device traffic is routed through the tunnel interface
How macOS Proxy Works #
On macOS, both cores run as CLI binaries (subprocesses):
- The plugin starts the selected core binary (
sing-boxorxray) with the generated config - The core opens a local SOCKS/HTTP proxy port
- The plugin configures macOS system proxy settings via
networksetupto route traffic through the local proxy - When stopped, system proxy settings are restored to their previous state
API Reference #
Initialization #
| Method | Returns | Description |
|---|---|---|
initialize({notificationStopButtonText, notificationIconName}) |
Future<void> |
Initialize the VPN core. Must be called first. |
setConfigOptions(options) |
Future<bool> |
Set configuration options for the VPN service |
configOptions |
ConfigOptions |
Get current configuration options (getter) |
Connection #
| Method | Returns | Description |
|---|---|---|
connect(link, {name, notificationTitle}) |
Future<bool> |
Start VPN with a config link |
connectWithJson(configJson, {name}) |
Future<bool> |
Start VPN with raw JSON config |
disconnect() |
Future<bool> |
Stop VPN connection |
restart(link, {name}) |
Future<bool> |
Restart VPN connection |
Status & Streams #
| Method | Returns | Description |
|---|---|---|
watchStatus() |
Stream<VpnStatus> |
Watch VPN status changes |
watchStats() |
Stream<VpnStats> |
Watch real-time traffic statistics |
watchAlerts() |
Stream<Map<String, dynamic>> |
Watch VPN alerts |
watchLogs() |
Stream<Map<String, dynamic>> |
Watch live log stream |
getLogs() |
Future<List<String>> |
Get current log buffer |
Core Engine #
| Method | Returns | Description |
|---|---|---|
getCoreInfo() |
Future<Map<String, dynamic>> |
Get core engine info (name, version) |
setCoreEngine(engine) |
Future<bool> |
Set active core ('xray' or 'singbox') |
getCoreEngine() |
Future<String> |
Get active core engine name |
Ping & Testing #
| Method | Returns | Description |
|---|---|---|
ping(link, {timeout}) |
Future<int> |
Test latency of a single config (ms) |
pingAll(links, {timeout}) |
Future<Map<String, int>> |
Test latency of multiple configs in parallel |
watchPingResults() |
Stream<Map<String, dynamic>> |
Watch individual ping results during pingAll |
setPingTestUrl(url) |
Future<bool> |
Set custom URL for ping testing |
getPingTestUrl() |
Future<String> |
Get current ping test URL |
VPN Mode & Permissions #
| Method | Returns | Description |
|---|---|---|
setServiceMode(mode) |
Future<bool> |
Set VPN mode (VpnMode.vpn or VpnMode.proxy) |
getServiceMode() |
Future<VpnMode> |
Get current VPN mode |
checkVpnPermission() |
Future<bool> |
Check if VPN permission is granted |
requestVpnPermission() |
Future<bool> |
Request VPN permission from user |
Per-App Proxy (Android) #
| Method | Returns | Description |
|---|---|---|
setPerAppProxyMode(mode) |
Future<bool> |
Set per-app proxy mode |
getPerAppProxyMode() |
Future<PerAppProxyMode> |
Get current per-app proxy mode |
setPerAppProxyList(packages, mode) |
Future<bool> |
Set list of packages for per-app proxy |
getPerAppProxyList(mode) |
Future<List<String>> |
Get list of packages for per-app proxy |
getInstalledApps() |
Future<List<AppInfo>> |
Get list of installed applications |
getAppIcon(packageName) |
Future<String?> |
Get app icon as base64 PNG |
Traffic #
| Method | Returns | Description |
|---|---|---|
getTotalTraffic() |
Future<TotalTraffic> |
Get persistent total traffic stats |
resetTotalTraffic() |
Future<bool> |
Reset total traffic to zero |
Config Utilities #
| Method | Returns | Description |
|---|---|---|
parseConfig(link, {debug}) |
Future<String> |
Validate config link (empty = valid) |
generateConfig(link) |
Future<String> |
Generate full JSON config from link |
checkConfigJson(configJson) |
Future<String> |
Validate raw JSON config |
getActiveConfig() |
Future<String> |
Get currently active JSON config |
formatConfig(configJson) |
Future<String> |
Prettify a JSON config |
parseConfigLink(link) |
VpnConfig |
Parse link into VpnConfig object |
isValidConfigLink(link) |
bool |
Check if link is a valid config |
Notification (Android) #
| Method | Returns | Description |
|---|---|---|
setNotificationStopButtonText(text) |
Future<bool> |
Set stop button text |
setNotificationTitle(title) |
Future<bool> |
Set custom notification title |
setNotificationIcon(iconName) |
Future<bool> |
Set notification icon (drawable name) |
Subscription #
| Method | Returns | Description |
|---|---|---|
parseSubscription(link) |
Future<Map<String, dynamic>> |
Parse subscription import link |
generateSubscriptionLink(name, url) |
Future<String> |
Generate subscription link |
Misc #
| Method | Returns | Description |
|---|---|---|
setDebugMode(enabled) |
Future<bool> |
Enable/disable verbose logging |
getDebugMode() |
Future<bool> |
Get current debug mode state |
formatBytes(bytes) |
Future<String> |
Format bytes to human-readable string |
proxyDisplayType(type) |
Future<String> |
Get display name for proxy type |
availablePort({startPort}) |
Future<int> |
Find an available network port |
selectOutbound(groupTag, outboundTag) |
Future<bool> |
Select outbound in a group |
setClashMode(mode) |
Future<bool> |
Set clash routing mode |
setLocale(locale) |
Future<bool> |
Set locale for the core library |
getPlatformVersion() |
Future<String?> |
Get platform version string |
Reducing App Size (Single Core Mode) #
If you want a smaller app, you can ship only one of the two cores on any platform:
Android:
- Xray-core only — Don't place
libsingbox.soinjniLibs/. Only includelibv2ray.aar. - sing-box only — Don't include
libv2ray.aarinlibs/. Only placelibsingbox.soinjniLibs/.
iOS:
- sing-box only — Only include
HiddifyCore.xcframework. No Xray framework needed. - Xray-core only — Only include the Xray framework. Remove HiddifyCore.
macOS:
- Only place the binary you need (
sing-boxorxray) inmacos/Frameworks/.
Important: When shipping a single core, make sure you do not expose the core switching option to users in your app's UI. If a user tries to switch to a core that isn't bundled, the VPN connection will fail. Set the default core engine to the one you've included and hide the engine selector from your settings page.
// Example: sing-box only app — set once at startup, no UI switch needed
await v2rayBox.setCoreEngine('singbox');