Flutter VPN Integration
VPN Integration Overview
Learn how to integrate OpenVPN functionality into your Flutter app, manage VPN connections, handle authentication, and implement robust error handling with automatic fallback mechanisms.
VPN Setup & Dependencies
1. Required Dependencies
Add these dependencies to your pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
# VPN Integration
flutter_openvpn: ^2.0.0
# HTTP Client for API calls
dio: ^5.3.2
# State Management
provider: ^6.0.5
# Secure Storage
flutter_secure_storage: ^9.0.0
# Connectivity Checking
connectivity_plus: ^4.0.2
# Logging
logger: ^2.0.2
# Firebase (for notifications)
firebase_core: ^2.15.1
firebase_messaging: ^14.6.7
firebase_analytics: ^10.4.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
2. Platform Configuration
Add VPN permissions to android/app/src/main/AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- Add VPN service to application -->
<service android:name="de.blinkt.openvpn.core.OpenVPNService"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
Add capabilities in ios/Runner/Runner.entitlements
:
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
API Integration
1. API Client Setup
Create a robust API client for server communication:
// lib/services/api_service.dart
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
class ApiService {
static const String baseUrl = 'https://axe.linkze.me/api';
static const String fallbackUrl = 'http://127.0.0.1:8000/api';
late final Dio _dio;
final Logger _logger = Logger();
ApiService() {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
_setupInterceptors();
}
void _setupInterceptors() {
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
_logger.i('API Request: ${options.method} ${options.path}');
handler.next(options);
},
onResponse: (response, handler) {
_logger.i('API Response: ${response.statusCode} ${response.requestOptions.path}');
handler.next(response);
},
onError: (error, handler) {
_logger.e('API Error: ${error.message}');
handler.next(error);
},
));
}
// Enhanced server config fetching with retry logic
Future<String> fetchServerConfig(int serverId) async {
try {
final response = await _dio.get(
'/v1/get',
queryParameters: {'id': serverId},
options: Options(
validateStatus: (status) => status! < 500,
),
);
if (response.statusCode == 200) {
if (response.data is Map<String, dynamic>) {
return response.data['config'] ?? '';
}
return response.data.toString();
} else {
throw Exception('Failed to load config: ${response.data['error'] ?? 'Unknown error'}');
}
} on DioException catch (e) {
_logger.e('DioException in fetchServerConfig: ${e.message}');
// Retry with fallback URL
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
_logger.i('Retrying with fallback URL...');
return await _retryWithFallback(serverId);
}
throw Exception('Network error: ${e.message}');
}
}
Future<String> _retryWithFallback(int serverId) async {
try {
final fallbackDio = Dio(BaseOptions(baseUrl: fallbackUrl));
final response = await fallbackDio.get(
'/v1/get',
queryParameters: {'id': serverId},
);
if (response.statusCode == 200) {
if (response.data is Map<String, dynamic>) {
return response.data['config'] ?? '';
}
return response.data.toString();
}
throw Exception('Fallback also failed');
} catch (e) {
_logger.e('Fallback request failed: $e');
rethrow;
}
}
// Get server health status
Future<Map<String, dynamic>> getServerHealth(int serverId) async {
try {
final response = await _dio.get('/v1/server/$serverId/health');
return response.data;
} catch (e) {
_logger.e('Failed to get server health: $e');
rethrow;
}
}
// Get all available servers
Future<List<dynamic>> getServerList() async {
try {
final response = await _dio.get('/v1/list');
return List.from(response.data);
} catch (e) {
_logger.e('Failed to get server list: $e');
rethrow;
}
}
}
2. Server Model
// lib/models/server.dart
class VpnServer {
final int id;
final String countryCode;
final String city;
final String ip;
final String protocol;
final int port;
final String? vpnUsername;
final String? vpnPassword;
final bool isFree;
final int order;
final bool useFile;
final int? freeConnectDuration;
final int connectedDevices;
VpnServer({
required this.id,
required this.countryCode,
required this.city,
required this.ip,
required this.protocol,
required this.port,
this.vpnUsername,
this.vpnPassword,
required this.isFree,
required this.order,
required this.useFile,
this.freeConnectDuration,
required this.connectedDevices,
});
factory VpnServer.fromJson(Map<String, dynamic> json) {
return VpnServer(
id: json['id'],
countryCode: json['country_code'],
city: json['city'],
ip: json['ip'],
protocol: json['protocol'],
port: json['port'],
vpnUsername: json['vpn_username'],
vpnPassword: json['vpn_password'],
isFree: json['is_free'] == 1,
order: json['order'],
useFile: json['use_file'] == 1,
freeConnectDuration: json['free_connect_duration'],
connectedDevices: json['connected_devices'],
);
}
String get displayName => '$city, $countryCode';
String get flagEmoji {
// Convert country code to flag emoji
return countryCode.toUpperCase()
.split('')
.map((char) => String.fromCharCode(char.codeUnitAt(0) + 0x1F1A5))
.join('');
}
}
Connection Management
1. VPN Connection Service
// lib/services/vpn_service.dart
import 'package:flutter_openvpn/flutter_openvpn.dart';
import 'package:logger/logger.dart';
import 'api_service.dart';
import '../models/server.dart';
enum VpnConnectionState {
disconnected,
connecting,
connected,
disconnecting,
error,
reconnecting,
}
class VpnService extends ChangeNotifier {
final ApiService _apiService = ApiService();
final Logger _logger = Logger();
VpnConnectionState _state = VpnConnectionState.disconnected;
VpnServer? _currentServer;
String? _lastError;
int _reconnectAttempts = 0;
bool _usedFallbackConfig = false;
// Getters
VpnConnectionState get state => _state;
VpnServer? get currentServer => _currentServer;
String? get lastError => _lastError;
bool get isConnected => _state == VpnConnectionState.connected;
VpnService() {
_initializeVpn();
}
void _initializeVpn() {
// Initialize OpenVPN and set up state listeners
FlutterOpenVpn.onStateChanged.listen((state) {
_handleVpnStateChange(state);
});
}
void _handleVpnStateChange(VpnState state) {
_logger.i('VPN State changed to: $state');
switch (state) {
case VpnState.connecting:
_updateState(VpnConnectionState.connecting);
break;
case VpnState.connected:
_updateState(VpnConnectionState.connected);
_reconnectAttempts = 0; // Reset on successful connection
break;
case VpnState.disconnected:
_updateState(VpnConnectionState.disconnected);
break;
case VpnState.noprocess:
if (_state == VpnConnectionState.connected) {
_logger.w('VPN process terminated unexpectedly');
_scheduleReconnect();
}
break;
case VpnState.error:
_updateState(VpnConnectionState.error);
_scheduleReconnect();
break;
}
}
void _updateState(VpnConnectionState newState) {
if (_state != newState) {
_state = newState;
notifyListeners();
}
}
// Main connection method with fallback logic
Future<void> connectToServer(VpnServer server) async {
_currentServer = server;
_lastError = null;
_usedFallbackConfig = false;
_updateState(VpnConnectionState.connecting);
try {
// 1. Attempt primary config from server
String config = await _apiService.fetchServerConfig(server.id);
if (config.isNotEmpty) {
await _connectWithConfig(config, server.displayName);
_logConnectionAttempt(true);
return;
}
throw Exception('Empty configuration received');
} catch (e) {
_logger.e('Primary config failed: $e');
// 2. Fallback to minimal config
try {
final minimalConfig = _createMinimalConfig(server);
_usedFallbackConfig = true;
await _connectWithConfig(minimalConfig, '${server.displayName} (Fallback)');
_logConnectionAttempt(true);
} catch (fallbackError) {
_logger.e('Fallback config also failed: $fallbackError');
_lastError = 'Connection failed: ${fallbackError.toString()}';
_updateState(VpnConnectionState.error);
_logConnectionAttempt(false);
}
}
}
Future<void> _connectWithConfig(String config, String serverName) async {
await FlutterOpenVpn.connect(
config: config,
serverName: serverName,
username: _currentServer?.vpnUsername,
password: _currentServer?.vpnPassword,
);
}
String _createMinimalConfig(VpnServer server) {
return '''
client
dev tun
proto ${server.protocol.toLowerCase()}
remote ${server.ip} ${server.port}
resolv-retry infinite
nobind
persist-key
persist-tun
verb 1
pull
route-method exe
route-delay 2
cipher AES-256-CBC
auth SHA256
${(server.vpnUsername?.isNotEmpty == true) ? 'auth-user-pass' : ''}
${(server.vpnUsername?.isNotEmpty == true) ? 'auth-nocache' : ''}
''';
}
void _scheduleReconnect() {
if (_reconnectAttempts < 3 && _currentServer != null) {
_reconnectAttempts++;
_updateState(VpnConnectionState.reconnecting);
Future.delayed(Duration(seconds: 2 * _reconnectAttempts), () {
if (_state == VpnConnectionState.reconnecting) {
connectToServer(_currentServer!);
}
});
} else {
_lastError = 'Maximum reconnection attempts reached';
_updateState(VpnConnectionState.error);
}
}
Future<void> disconnect() async {
_updateState(VpnConnectionState.disconnecting);
await FlutterOpenVpn.disconnect();
_currentServer = null;
}
void _logConnectionAttempt(bool success) {
// Log to Firebase Analytics
if (_currentServer != null) {
// FirebaseAnalytics.instance.logEvent(
// name: 'vpn_connection',
// parameters: {
// 'server_id': _currentServer!.id,
// 'success': success,
// 'used_fallback': _usedFallbackConfig,
// },
// );
}
}
}
Error Handling & Fallback
1. Connection Error Types
Network Errors
- Connection Timeout: Server unreachable
- DNS Resolution: Domain lookup failed
- SSL/TLS Errors: Certificate issues
- Firewall Block: Port blocked
Configuration Errors
- Invalid Config: Malformed OVPN
- Auth Failure: Wrong credentials
- Missing Certs: No certificates
- Protocol Mismatch: UDP/TCP issues
2. Fallback Strategy Implementation
// lib/services/connection_fallback.dart
class ConnectionFallbackService {
static const List<Map<String, dynamic>> fallbackConfigs = [
{'protocol': 'udp', 'port': 1194},
{'protocol': 'tcp', 'port': 443},
{'protocol': 'tcp', 'port': 80},
{'protocol': 'udp', 'port': 53},
];
static String generateFallbackConfig(VpnServer server, int attemptIndex) {
final config = fallbackConfigs[attemptIndex % fallbackConfigs.length];
return '''
client
dev tun
proto ${config['protocol']}
remote ${server.ip} ${config['port']}
resolv-retry infinite
nobind
persist-key
persist-tun
verb 1
pull
route-method exe
route-delay 2
cipher AES-256-CBC
auth SHA256
comp-lzo
fast-io
script-security 2
redirect-gateway def1 bypass-dhcp
dhcp-option DNS 8.8.8.8
dhcp-option DNS 8.8.4.4
''';
}
static Future<bool> testServerConnectivity(String ip, int port) async {
try {
final socket = await Socket.connect(ip, port, timeout: Duration(seconds: 5));
socket.destroy();
return true;
} catch (e) {
return false;
}
}
}
Connection Monitoring
1. Real-time Connection Monitoring
// lib/services/connection_monitor.dart
class ConnectionMonitor {
static const Duration checkInterval = Duration(seconds: 10);
Timer? _monitorTimer;
final VpnService _vpnService;
final Logger _logger = Logger();
ConnectionMonitor(this._vpnService);
void startMonitoring() {
_monitorTimer?.cancel();
_monitorTimer = Timer.periodic(checkInterval, (timer) {
if (_vpnService.isConnected) {
_checkConnectionHealth();
}
});
}
void stopMonitoring() {
_monitorTimer?.cancel();
}
Future<void> _checkConnectionHealth() async {
try {
// Check actual internet connectivity through VPN
final response = await http.get(
Uri.parse('https://httpbin.org/ip'),
headers: {'timeout': '5'},
).timeout(Duration(seconds: 5));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final currentIp = data['origin'];
// Verify IP has changed (indicating VPN is working)
if (currentIp != null) {
_logger.i('VPN connection healthy, IP: $currentIp');
}
}
} catch (e) {
_logger.w('Connection health check failed: $e');
// Optionally trigger reconnection
}
}
}
2. Usage Statistics Tracking
// lib/services/usage_tracker.dart
class UsageTracker {
static const String _connectionTimeKey = 'total_connection_time';
static const String _dataUsageKey = 'total_data_usage';
DateTime? _connectionStartTime;
int _totalConnectionTime = 0;
void startSession() {
_connectionStartTime = DateTime.now();
_loadStoredData();
}
void endSession() {
if (_connectionStartTime != null) {
final duration = DateTime.now().difference(_connectionStartTime!);
_totalConnectionTime += duration.inSeconds;
_saveConnectionTime();
_connectionStartTime = null;
}
}
Future<void> _loadStoredData() async {
final prefs = await SharedPreferences.getInstance();
_totalConnectionTime = prefs.getInt(_connectionTimeKey) ?? 0;
}
Future<void> _saveConnectionTime() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_connectionTimeKey, _totalConnectionTime);
}
String get formattedTotalTime {
final hours = _totalConnectionTime ~/ 3600;
final minutes = (_totalConnectionTime % 3600) ~/ 60;
return '${hours}h ${minutes}m';
}
}
Best Practices
- Always validate server configs
- Implement automatic reconnection
- Set appropriate timeouts
- Log all connection attempts
- Handle background/foreground transitions
- Optimize for battery usage
- Don't ignore connection errors
- Don't store credentials in plain text
- Don't skip SSL certificate validation
- Don't make unlimited reconnection attempts
- Don't block the UI during connections
- Don't forget to clean up resources
Next Steps
You've learned VPN integration fundamentals. Continue with these advanced topics: