axevpn_flutter 2.0.0
axevpn_flutter: ^2.0.0 copied to clipboard
Dual-protocol VPN plugin for Flutter supporting OpenVPN and WireGuard on Android (16 KB page-size ready) and iOS.
axevpn_flutter #
A Flutter plugin that provides OpenVPN and WireGuard VPN connectivity for Android and iOS.
Built with Android 15+ 16 KB page-size compatibility and modern Flutter 3.10+ / Dart 3.0+ support.
Table of Contents #
- Features
- Platform Support
- Installation
- Quick Start
- Android Setup
- iOS Setup
- API Reference
- Advanced Usage
- Troubleshooting
- Changelog
- License
Features #
| Feature | OpenVPN | WireGuard |
|---|---|---|
| Android support | ✅ | ✅ |
| iOS support | ✅ | ✅ |
| Real-time status monitoring | ✅ | ✅ |
| Connection stats (bytes in/out) | ✅ | ✅ |
| Auto-reconnect handling | ✅ | ✅ |
| Split tunneling (package bypass) | ✅ | ➖ |
| TCP & UDP protocol | ✅ | ➖ |
| Android 15+ 16 KB page-size | ✅ | ✅ |
| Modern cryptography | ✅ | ✅ |
Platform Support #
| Platform | Minimum Version |
|---|---|
| Android | 7.0 (API 24) |
| iOS | 16.0 |
Installation #
Add to your pubspec.yaml:
dependencies:
axevpn_flutter: ^2.0.0
Then run:
flutter pub get
Quick Start #
OpenVPN #
import 'package:axevpn_flutter/openvpn_flutter.dart';
// 1. Instantiate
final vpn = OpenVPN(
onVpnStatusChanged: (VpnStatus? status) {
print('Duration: ${status?.duration}');
print('Bytes in: ${status?.byteIn} Bytes out: ${status?.byteOut}');
},
onVpnStageChanged: (VPNStage stage, String rawStage) {
print('Stage: $stage');
},
);
// 2. Initialize (required before connect)
await vpn.initialize(
// iOS only ↓
groupIdentifier: 'group.com.example.vpn',
providerBundleIdentifier: 'com.example.app.VPNExtension',
localizedDescription: 'My VPN',
);
// 3. Connect
final ovpnConfig = '''
client
dev tun
proto udp
remote vpn.example.com 1194
...
''';
await vpn.connect(ovpnConfig, 'My Server');
// 4. Disconnect
vpn.disconnect();
WireGuard #
import 'package:axevpn_flutter/wireguard_flutter.dart';
// 1. Instantiate
final wg = WireGuard(
onVpnStatusChanged: (WireGuardStatus? status) {
print('Duration: ${status?.duration}');
print('Bytes in: ${status?.byteIn} Bytes out: ${status?.byteOut}');
},
onVpnStageChanged: (WGStage stage, String rawStage) {
print('Stage: $stage');
},
);
// 2. Initialize
await wg.initialize(
// iOS only ↓
groupIdentifier: 'group.com.example.vpn',
providerBundleIdentifier: 'com.example.app.WGExtension',
localizedDescription: 'My VPN',
);
// 3. Connect — pass raw WireGuard .conf content
final wgConfig = '''
[Interface]
PrivateKey = <your_private_key>
Address = 10.0.0.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = <server_public_key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
''';
await wg.connect(wgConfig, 'My WireGuard');
// 4. Disconnect
await wg.disconnect();
Android Setup #
1. Handle VPN permission result in MainActivity #
Kotlin (MainActivity.kt):
import com.axevpn.flutter.openvpn.AxeVPNFlutterPlugin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AxeVPNFlutterPlugin.connectWhileGranted(requestCode == 24 && resultCode == RESULT_OK)
super.onActivityResult(requestCode, resultCode, data)
}
Java (MainActivity.java):
import com.axevpn.flutter.openvpn.AxeVPNFlutterPlugin;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
AxeVPNFlutterPlugin.connectWhileGranted(requestCode == 24 && resultCode == RESULT_OK);
super.onActivityResult(requestCode, resultCode, data);
}
2. app/build.gradle (Kotlin DSL) #
android {
compileSdk = 36
ndkVersion = "27.0.12077973" // Required for 16 KB page-size support
defaultConfig {
minSdk = 24
targetSdk = 36
}
// Enable Android 15+ 16 KB memory page size
experimentalProperties["android.experimental.enable16KPageSize"] = true
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
3. android/gradle.properties #
android.experimental.enable16KPageSize=true
android.bundle.enableUncompressedNativeLibs=false
4. AndroidManifest.xml #
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:extractNativeLibs="true"
...>
iOS Setup #
1. Enable capabilities in Xcode #
In your Runner target (and Network Extension target), enable:
- App Groups – create
group.com.yourapp.vpn - Network Extensions – enable Packet Tunnel
2. Add Network Extension target #
In Xcode → File → New → Target → Network Extension.
Name it (e.g., VPNExtension) and set the same App Group.
OpenVPN Extension
Add to ios/Podfile under the new target:
target 'VPNExtension' do
use_frameworks!
pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.8.0'
end
Copy PacketTunnelProvider.swift to VPNExtension/:
// See example/ios/VPNExtension/ in the repository
WireGuard Extension
target 'WGExtension' do
use_frameworks!
pod 'WireGuardKit', :git => 'https://git.zx2c4.com/wireguard-apple', :tag => '1.0.15-26'
end
3. Info.plist — Network Extension target #
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.packet-tunnel</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>
API Reference #
OpenVPN API #
OpenVPN constructor
OpenVPN({
Function(VpnStatus? data)? onVpnStatusChanged,
Function(VPNStage stage, String rawStage)? onVpnStageChanged,
})
| Parameter | Type | Description |
|---|---|---|
onVpnStatusChanged |
Function(VpnStatus?) |
Called when bytes/duration update |
onVpnStageChanged |
Function(VPNStage, String) |
Called on every stage transition |
initialize()
Future<void> initialize({
String? groupIdentifier, // iOS: App Group ID
String? providerBundleIdentifier, // iOS: Extension bundle ID
String? localizedDescription, // iOS: Description in Settings
Function(VpnStatus)? lastStatus, // Callback with last known status
Function(VPNStage)? lastStage, // Callback with last known stage
})
connect()
Future connect(
String config, // Raw .ovpn file content
String name, // Display name for notification
{
String? username,
String? password,
List<String>? bypassPackages, // Android: packages to exclude from VPN
bool certIsRequired = false,
}
)
disconnect()
void disconnect()
Other methods
Future<VPNStage> stage()
Future<VpnStatus> status()
Future<bool> isConnected()
Future<bool> requestPermissionAndroid()
static Future<String?> filteredConfig(String? config)
VpnStatus
class VpnStatus {
final DateTime? connectedOn; // Time VPN connected
final String? duration; // e.g. "00:05:32"
final String? byteIn; // Bytes received (as string)
final String? byteOut; // Bytes sent (as string)
final String? packetsIn;
final String? packetsOut;
}
WireGuard API #
WireGuard constructor
WireGuard({
Function(WireGuardStatus? data)? onVpnStatusChanged,
Function(WGStage stage, String rawStage)? onVpnStageChanged,
})
initialize()
Future<void> initialize({
String? groupIdentifier,
String? providerBundleIdentifier,
String? localizedDescription,
Function(WireGuardStatus)? lastStatus,
Function(WGStage)? lastStage,
})
connect()
Future connect(
String config, // Raw WireGuard .conf content
String tunnelName, // Display name
)
Other methods
Future<void> disconnect()
Future<WGStage> stage()
Future<WireGuardStatus> status()
WireGuardStatus
class WireGuardStatus {
final Duration? duration; // Connection duration
final String? lastPacketReceive; // Last handshake timestamp
final String? byteIn; // Bytes received
final String? byteOut; // Bytes sent
}
VPN Stages #
OpenVPN (VPNStage)
| Stage | Description |
|---|---|
prepare |
Preparing to connect |
authenticating |
Verifying credentials |
connecting |
Establishing tunnel |
connected |
Tunnel is up |
disconnecting |
Tearing down tunnel |
disconnected |
Tunnel is down |
denied |
VPN permission denied |
error |
Connection error |
wait_connection |
Waiting for network |
get_config |
Fetching configuration |
tcp_connect |
TCP handshake |
udp_connect |
UDP handshake |
assign_ip |
IP assignment |
resolve |
DNS resolution |
exiting |
Process exiting |
WireGuard (WGStage)
| Stage | Description |
|---|---|
preparing |
Preparing to connect |
connecting |
Establishing tunnel |
connected |
Tunnel is up |
disconnecting |
Tearing down tunnel |
disconnected |
Tunnel is down |
denied |
VPN permission denied |
error |
Error occurred |
Advanced Usage #
Split Tunneling (OpenVPN only) #
Exclude specific apps from the VPN tunnel on Android:
await vpn.connect(
ovpnConfig,
'My Server',
bypassPackages: [
'com.google.android.youtube',
'com.whatsapp',
],
);
Filter Duplicate Remotes #
If your .ovpn has many remote entries and causes ANR on some devices:
String? singleRemoteConfig = await OpenVPN.filteredConfig(rawConfig);
Request Android Permission Manually #
bool granted = await vpn.requestPermissionAndroid();
if (granted) {
await vpn.connect(config, 'Server');
}
Toggle Connection #
if (await vpn.isConnected()) {
vpn.disconnect();
} else {
await vpn.connect(config, 'Server');
}
Troubleshooting #
Android #
| Problem | Solution |
|---|---|
| Build fails with 16 KB error | Install NDK 27.0.12077973; set enable16KPageSize=true |
| VPN permission dialog not shown | Call requestPermissionAndroid() before connect() |
OpenVPN need to be initialized |
Call initialize() before connect() |
| ANR on connect | Use OpenVPN.filteredConfig() to reduce remote entries |
iOS #
| Problem | Solution |
|---|---|
groupIdentifier is required |
Pass all three iOS params to initialize() |
| Network Extension not found | Verify bundle IDs match providerBundleIdentifier in Xcode |
| Status not updating after disconnect | Register onVpnStageChanged before calling initialize() |
Changelog #
See CHANGELOG.md for a complete version history.
License #
Licensed under the GNU General Public License v3.0 – see LICENSE for details.
OpenVPN® is a registered trademark of OpenVPN Inc.
WireGuard® is a registered trademark of Jason A. Donenfeld.
Forked from openvpn_flutter and extended with WireGuard support and Android 15+ 16 KB page-size compatibility.