Initial commit (Security Hardened Slate)

This commit is contained in:
esefxdz committed 2026-05-29 14:42:24 +03:00
commit 157f60aad2
65 files changed
+3735

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
.java-version
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains configure files for the Visual Studio Code
# editor. Although we may keep it, we often ignore it.
.vscode/
# Python cache & User files
TECHNICAL_DEBT.md
RUNNING.md
__pycache__/
*.pyc
# App Config (Hide real IPs/IDs from GitHub)
lib/logic/app_config.dart
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
build/
**/build/
local_packages/*/build/
local_packages/*/.dart_tool/
*.dart_tool/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
**/android/.cxx/
**/android/app/build/
# Keystore files
*.keystore
*.jks
# Crashlytics cache
**/android/app/crashlytics-build.properties
# Secrets
android/key.properties
lib/logic/backup/
00-FULL-REPORT.md
server_hivemind_4_0.py
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 esef
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
Privacy Policy
Last updated: May 2026
PaladinVPN is a free VPN service built and operated by a solo developer.
What we collect
We collect the minimum required to operate the service:
A randomly generated device ID created on your phone. This is not linked to your identity in any way.
Your active session status and data usage for the current session only. This is stored in memory and deleted when your session ends.
What we do not collect
We do not collect your name, email, or any personal information.
We do not log your internet traffic or browsing history.
We do not sell or share any data with third parties.
Ads
PaladinVPN uses Google AdMob to display rewarded video ads. Google may collect data as part of ad delivery. Please refer to Google's Privacy Policy for details.
Your traffic
Your internet traffic is routed through our server located in Finland. We do not inspect, log, or store your traffic.
Contact
If you have any questions, open an issue on the GitHub repository.
+53
View File
@@ -0,0 +1,53 @@
---
# PaladinVPN
A free VPN app I built in my spare time as a student with basically no prior experience.
There are hundreds of free VPN apps on the Play Store. Most of them are fine honestly — some are genuinely good. But a lot of them are vague about how they work, who runs them, and what happens to your traffic. I wanted to build something where the answer to all of those questions is just... public.
So here it is.
---
## How it works
You watch a short ad. You get 60 minutes and 2GB of full-speed traffic. That's it. The ad pays for the server (actually not even enough). No accounts, no emails, no subscriptions.
When your data runs out you get throttled to 1.5 Mbps instead of getting cut off. You can watch a bonus ad to top up anytime.
---
## The stack
- **App** — Flutter (Android only for now)
- **Server** — A single Debian box rented from Hetzner, located in Finland
- **VPN protocol** — AmneziaWG, which is WireGuard with obfuscation so it works on carriers that block standard WireGuard
- **Backend** — A Python script I wrote called Hivemind that manages sessions, data quotas, and throttling
- **Ads** — Google AdMob rewarded ads, verified server-side so fake callbacks don't work
---
## Limitations (being honest)
- One server, one location (Finland). That's all I can afford right now
- 2 cores and 4GB RAM. It will not handle thousands of concurrent users
- iOS is not supported and probably won't be for a while
- Countries with very aggressive DPI (China, Russia, etc.) may still have issues
- I'm a student doing this in my free time. Expect rough edges
---
## Why I built this
Free time project. Wanted to learn how VPNs actually work under the hood. Ended up building the whole thing from scratch — the Flutter app, the AmneziaWG setup, the session management backend, the AdMob integration. It took way longer than expected.
---
## Privacy
Your traffic goes through my server in Finland. I don't log it, I don't sell it, I have no reason to. The session manager (Hivemind) only tracks whether your session is active and how much data you've used — nothing else.
Internet privacy at the cost of watching an ad. Honest business.
You can read the server code yourself, it's in this repo.
---
*PaladinVPN is not affiliated with any VPN company.*
---
+11
View File
@@ -0,0 +1,11 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
+61
View File
@@ -0,0 +1,61 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.paladinvpn.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
resources.excludes.add("META-INF/**")
}
defaultConfig {
applicationId = "com.paladinvpn.app"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = keystoreProperties["storeFile"]?.let { rootProject.file(it) }
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+48
View File
@@ -0,0 +1,48 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Required by url_launcher on Android 11+ to open external browser -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries>
<application
android:label="Paladin VPN"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="false"
android:allowBackup="false">
<!-- AdMob App ID (real ID) -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-5671224884648691~1007152454"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,5 @@
package com.paladinvpn.app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/black" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/white</item>
</style>
</resources>
@@ -0,0 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+30
View File
@@ -0,0 +1,30 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.11.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.layout.buildDirectory
}
+50
View File
@@ -0,0 +1,50 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.esefcloud.vpn"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
applicationId = "com.esefcloud.vpn"
minSdk = 21
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
packaging {
resources {
excludes += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF"
)
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
+8
View File
@@ -0,0 +1,8 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+6
View File
@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+25
View File
@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.11.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
}
include ":app"
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
import 'package:paladinvpn/components/online_dot.dart';
/// BottomBar is the bottom utility panel that surfaces server connectivity status
/// and offers the user an option to support the server when the VPN tunnel is active.
class BottomBar extends StatelessWidget {
const BottomBar({super.key});
// Dark card background color for high contrast overlay
static const Color _cardBg = Color(0xFF1E2533);
@override
Widget build(BuildContext context) {
return Consumer<VpnConnection>(
builder: (context, vpn, _) {
final isOnline = vpn.status == VpnStatus.connected;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: BoxDecoration(
color: _cardBg,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color.fromRGBO(74, 85, 104, 0.15),
),
),
child: Row(
children: [
// Displays the server online/offline chip
OnlineDot(isOnline: isOnline),
],
),
);
},
);
}
}
+152
View File
@@ -0,0 +1,152 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/session_timer.dart';
/// ClockDisplay shows a large circular progress ring and the session countdown timer.
///
/// The timer text glows electric cyan when a session is active and dims to slate gray when idle.
class ClockDisplay extends StatelessWidget {
const ClockDisplay({super.key});
static const Color _cyanGlow = Color(0xFF00E5FF);
static const Color _dimSlate = Color(0xFF4A5568);
@override
Widget build(BuildContext context) {
return Consumer<SessionTimer>(
builder: (context, timer, _) {
final isLive = timer.isRunning;
final color = isLive ? _cyanGlow : _dimSlate;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 220,
height: 220,
child: Stack(
alignment: Alignment.center,
children: [
// Background track ring representing the maximum capacity of the countdown
const CustomPaint(
size: Size(220, 220),
painter: _ArcPainter(
progress: 1.0,
color: Color.fromRGBO(74, 85, 104, 0.2),
strokeWidth: 4,
),
),
// Live progress ring showing current remaining time relative to the session limit
if (isLive)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: timer.progress),
duration: const Duration(milliseconds: 400),
builder: (context, value, _) => CustomPaint(
size: const Size(220, 220),
painter: _ArcPainter(
progress: value,
color: _cyanGlow,
strokeWidth: 4,
glow: true,
),
),
),
// Monospaced digital clock display for session countdown
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontFamily: 'RobotoMono',
fontSize: 48,
fontWeight: FontWeight.w300,
letterSpacing: 4,
color: color,
shadows: isLive
? const [
Shadow(
color: Color.fromRGBO(0, 229, 255, 0.6),
blurRadius: 20,
),
Shadow(
color: Color.fromRGBO(0, 229, 255, 0.3),
blurRadius: 40,
),
]
: const [],
),
child: Text(timer.formatted),
),
],
),
),
const SizedBox(height: 12),
// Subtitle state label below the timer
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
isLive ? 'SESSION ACTIVE' : 'STANDBY',
key: ValueKey(isLive),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 3,
color: isLive
? const Color.fromRGBO(0, 229, 255, 0.7)
: const Color.fromRGBO(74, 85, 104, 0.7),
),
),
),
],
);
},
);
}
}
/// A custom painter to render a circular progress arc or background track ring.
class _ArcPainter extends CustomPainter {
final double progress;
final Color color;
final double strokeWidth;
final bool glow;
const _ArcPainter({
required this.progress,
required this.color,
this.strokeWidth = 4,
this.glow = false,
});
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height)
.deflate(strokeWidth / 2);
// Optional blurred background paint to create an electric neon glow effect
if (glow) {
final glowPaint = Paint()
..color = const Color.fromRGBO(0, 229, 255, 0.15)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth + 8
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8);
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, glowPaint);
}
// Core progress stroke paint
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(rect, -pi / 2, 2 * pi * progress, false, paint);
}
@override
bool shouldRepaint(covariant _ArcPainter old) =>
old.progress != progress || old.color != color;
}
+207
View File
@@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
import 'package:paladinvpn/logic/session_timer.dart';
class ConnectButton extends StatefulWidget {
const ConnectButton({super.key});
@override
State<ConnectButton> createState() => _ConnectButtonState();
}
class _ConnectButtonState extends State<ConnectButton>
with SingleTickerProviderStateMixin {
late AnimationController _pulseController;
late Animation<double> _pulseAnim;
static const Color _cyanGlow = Color(0xFF00E5FF);
static const Color _bgDark = Color(0xFF1A202C);
bool _inCooldown = false;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_pulseAnim = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_pulseController.dispose();
super.dispose();
}
void _handleTap() async {
if (_inCooldown) return;
setState(() {
_inCooldown = true;
});
Future.delayed(const Duration(seconds: 5), () {
if (mounted) {
setState(() {
_inCooldown = false;
});
}
});
final vpn = context.read<VpnConnection>();
final timer = context.read<SessionTimer>();
if (vpn.status == VpnStatus.connected) {
vpn.disconnect();
timer.stop();
_pulseController.stop();
_pulseController.reset();
} else if (vpn.status == VpnStatus.disconnected ||
vpn.status == VpnStatus.error) {
final success = await vpn.connect();
if (success) {
timer.start('main_ad');
_pulseController.repeat(reverse: true);
}
}
}
@override
Widget build(BuildContext context) {
return Consumer2<VpnConnection, SessionTimer>(
builder: (context, vpn, timer, _) {
final isConnected = vpn.status == VpnStatus.connected;
final isConnecting = vpn.status == VpnStatus.connecting;
if (isConnected && !_pulseController.isAnimating) {
_pulseController.repeat(reverse: true);
} else if (!isConnected && !isConnecting && _pulseController.isAnimating) {
_pulseController.stop();
_pulseController.reset();
}
return GestureDetector(
onTap: (isConnecting || _inCooldown) ? null : _handleTap,
child: AnimatedBuilder(
animation: _pulseAnim,
builder: (context, child) {
final glowRadius = isConnected ? 20.0 + (_pulseAnim.value * 15.0) : 0.0;
final glowAlpha = isConnected
? ((0.3 + _pulseAnim.value * 0.2) * 255).round()
: 0;
return Container(
width: 260,
height: 260,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _bgDark,
border: Border.all(
color: _inCooldown
? Colors.grey.withValues(alpha: 0.5)
: isConnected
? _cyanGlow
: isConnecting
? const Color.fromRGBO(0, 229, 255, 0.5)
: const Color.fromRGBO(74, 85, 104, 0.5),
width: isConnected && !_inCooldown ? 3 : 2,
),
boxShadow: isConnected
? [
BoxShadow(
color: Color.fromARGB(glowAlpha, 0, 229, 255),
blurRadius: glowRadius,
spreadRadius: glowRadius * 0.3,
),
]
: const [],
),
child: Center(
child: isConnected
? _buildConnectedStats(timer)
: _buildDisconnectedIcon(vpn.status, _inCooldown),
),
);
},
),
);
},
);
}
Widget _buildDisconnectedIcon(VpnStatus status, bool inCooldown) {
if (status == VpnStatus.connecting || status == VpnStatus.disconnecting) {
return const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Color.fromRGBO(0, 229, 255, 0.7),
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
inCooldown ? Icons.hourglass_bottom : Icons.power_settings_new,
size: 64,
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.7),
),
const SizedBox(height: 12),
Text(
inCooldown ? 'PLEASE WAIT' : 'TAP TO CONNECT',
style: TextStyle(
color: inCooldown ? Colors.grey : const Color.fromRGBO(74, 85, 104, 0.9),
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
],
);
}
Widget _buildConnectedStats(SessionTimer timer) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Countdown Timer
Text(
timer.formatted,
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
// Speed indicator
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.speed, color: Colors.white70, size: 16),
const SizedBox(width: 4),
Text(
'${timer.currentSpeedKbps.toStringAsFixed(1)} KB/s',
style: const TextStyle(
fontSize: 14,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
],
),
],
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
/// Header displays the Paladin brand logo, title, and subtitle.
///
/// It represents the top branding portion of the main dashboard and is designed
/// with clean proportions, consistent typography, and a modern cyberpunk-inspired color scheme.
class Header extends StatelessWidget {
const Header({super.key});
// Cyber-paladin cyan glow accent color used as a fallback for the shield icon
static const Color _cyanGlow = Color(0xFF00E5FF);
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Brand logo image with a fallback Icon in case the asset is missing
Image.asset(
'assets/logo.png',
width: 120, // Enlarged logo to stand out visually
height: 120,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.shield,
size: 120,
color: _cyanGlow,
),
),
const SizedBox(height: 8), // Standard padding between the logo and the title
// Brand primary name
const Text(
'Paladin',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
letterSpacing: 8,
color: Colors.white,
),
),
const SizedBox(height: 2), // Close spacing for secondary information
// Brand secondary label / purpose
const Text(
'SECURE TUNNEL',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 4,
color: Color(0xAA4A5568), // Semi-transparent slate gray
),
),
],
);
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class InfoButton extends StatelessWidget {
const InfoButton({Key? key}) : super(key: key);
Future<void> _launchGitHub() async {
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN');
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
debugPrint('Could not launch $url');
}
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.help_outline, color: Colors.white, size: 28),
onPressed: _launchGitHub,
tooltip: 'About PaladinVPN',
splashRadius: 24,
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
/// OnlineDot displays a glowing green dot when the server is online/connected,
/// and a muted gray dot when disconnected.
class OnlineDot extends StatelessWidget {
/// Represents if the VPN tunnel session is active (online).
final bool isOnline;
const OnlineDot({
super.key,
required this.isOnline,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Glowing status dot: Green when active, muted gray when inactive.
AnimatedContainer(
duration: const Duration(milliseconds: 400),
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOnline ? const Color(0xFF00E676) : const Color(0xFF4A5568),
boxShadow: isOnline
? const [
BoxShadow(
color: Color.fromRGBO(0, 230, 118, 0.5),
blurRadius: 6,
),
]
: const [],
),
),
const SizedBox(width: 10),
// Dynamic status text label
Text(
isOnline ? 'Server Online' : 'Disconnected',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isOnline
? const Color.fromRGBO(255, 255, 255, 0.8)
: const Color(0xFF4A5568),
letterSpacing: 0.5,
),
),
],
);
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class PrivacyPolicyButton extends StatelessWidget {
const PrivacyPolicyButton({super.key});
Future<void> _launchPrivacyPolicy() async {
final Uri url = Uri.parse('https://github.com/esefxdz/PaladinVPN/blob/main/PRIVACY_POLICY.md');
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
debugPrint('Could not launch $url');
}
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.privacy_tip_outlined, color: Colors.white, size: 28),
onPressed: _launchPrivacyPolicy,
tooltip: 'Privacy Policy',
splashRadius: 24,
);
}
}
+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
/// StatusText displays the status of the VPN tunnel.
///
/// When connected, it shows the Finnish flag along with the tunnel location ("Helsinki, Finland").
/// When connecting or disconnected, it shows status messages with clean micro-animations.
class StatusText extends StatelessWidget {
const StatusText({super.key});
@override
Widget build(BuildContext context) {
return Consumer<VpnConnection>(
builder: (context, vpn, _) {
final isConnected = vpn.status == VpnStatus.connected;
final isConnecting = vpn.status == VpnStatus.connecting;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
key: ValueKey(vpn.status),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0x22FFFFFF), // Subtle glass effect background
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isConnected
? const Color(0x8800E5FF) // Glowing cyan border when connected
: const Color(0x33FFFFFF), // Muted translucent border when disconnected/connecting
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isConnected) ...[
// Circular Finnish flag icon representing the active tunnel endpoint
Image.asset(
'assets/finland_flag_256.png',
width: 20,
height: 20,
),
const SizedBox(width: 8),
const Text(
'Helsinki, Finland',
style: TextStyle(
color: Color(0xFF00E5FF),
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 0.5,
),
),
] else ...[
// Dynamic status icon (spinning arrows for loading, outlined shield for idle)
Icon(
isConnecting ? Icons.autorenew : Icons.shield_outlined,
size: 18,
color: isConnecting
? const Color(0xFF00E5FF)
: const Color(0xAA4A5568),
),
const SizedBox(width: 8),
Text(
vpn.statusMessage.toUpperCase(),
style: TextStyle(
color: isConnecting
? const Color(0xFF00E5FF)
: const Color(0xAA4A5568),
fontWeight: FontWeight.w600,
fontSize: 13,
letterSpacing: 1,
),
),
],
],
),
),
);
},
);
}
}
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/session_timer.dart';
import 'package:paladinvpn/logic/ad_manager.dart';
/// WatchAdButton is a voluntary option allowing the user to watch a rewarded video ad
/// and extend their active VPN tunnel session by 30 minutes as a reward.
class WatchAdButton extends StatelessWidget {
const WatchAdButton({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () async {
final success = await context.read<AdManager>().showAd('bonus_ad');
if (success && context.mounted) {
context.read<SessionTimer>().addBonusTime(); // Calls Hivemind API for 30m + 1GB
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'+30 minutes added!',
style: TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: const Color(0xFF006064),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
duration: const Duration(seconds: 2),
),
);
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: const LinearGradient(
colors: [Color(0x1A00E5FF), Color(0x0D00E5FF)],
),
border: Border.all(
color: const Color.fromRGBO(0, 229, 255, 0.25),
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('❤️', style: TextStyle(fontSize: 14)),
SizedBox(width: 6),
Text(
'Support +30m',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF00E5FF),
letterSpacing: 0.5,
),
),
],
),
),
),
);
}
}
+153
View File
@@ -0,0 +1,153 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:paladinvpn/logic/crypto_service.dart';
import 'package:paladinvpn/logic/hivemind_service.dart';
import 'package:paladinvpn/logic/app_config.dart';
/// AdManager manages the lifecycle of rewarded video ads.
///
/// It acts as a strict gatekeeper: users MUST watch an ad to connect.
/// If an ad is not ready when they click connect, it forces them to wait
/// while it loads. If they dismiss the ad early, they do not get the connection.
class AdManager extends ChangeNotifier {
RewardedAd? _rewardedAd;
bool _isAdLoaded = false;
bool get isAdLoaded => _isAdLoaded;
bool _isAdLoading = false;
bool get isAdLoading => _isAdLoading;
Completer<bool>? _loadCompleter;
// Google-provided test ad unit
static String get _adUnitId => AppConfig.adUnitId;
AdManager() {
_preloadAd();
}
/// Internal helper to load an ad and return a Future when it's ready
Future<bool> _preloadAd() async {
// If already loaded, return instantly
if (_isAdLoaded) return true;
// If we're already loading one right now, wait for that same request to finish
if (_isAdLoading) return _loadCompleter?.future ?? Future.value(false);
_isAdLoading = true;
_loadCompleter = Completer<bool>();
notifyListeners();
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 500));
_isAdLoaded = true;
_isAdLoading = false;
notifyListeners();
_loadCompleter?.complete(true);
return true;
}
RewardedAd.load(
adUnitId: _adUnitId,
request: const AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (ad) {
_rewardedAd = ad;
_isAdLoaded = true;
_isAdLoading = false;
notifyListeners();
if (!(_loadCompleter?.isCompleted ?? true)) {
_loadCompleter?.complete(true);
}
},
onAdFailedToLoad: (error) {
debugPrint('Rewarded ad failed to load: ${error.message}');
_isAdLoaded = false;
_isAdLoading = false;
notifyListeners();
if (!(_loadCompleter?.isCompleted ?? true)) {
_loadCompleter?.complete(false);
}
},
),
);
return _loadCompleter!.future;
}
/// Displays the rewarded video ad to the user.
Future<bool> showAd(String adType) async {
// 1. Ensure the ad is fully loaded before trying to show it
if (!_isAdLoaded || _rewardedAd == null) {
final loaded = await _preloadAd();
if (!loaded || _rewardedAd == null) {
debugPrint('[AdManager] Cannot show ad, failed to load.');
return false;
}
}
// 2. Fetch device credentials for Server-Side Verification (SSV)
final deviceId = await CryptoService.getDeviceId();
final keys = await CryptoService.getOrCreateKeys();
final pubKey = keys['publicKey']!;
// 3. Attach custom data to the ad so Google sends it back to our Hivemind server
_rewardedAd!.serverSideVerificationOptions = ServerSideVerificationOptions(
customData: jsonEncode({
'device_id': deviceId,
'public_key': pubKey,
'ad_type': adType,
}),
);
Completer<bool> rewardCompleter = Completer<bool>();
// 4. Setup full screen callbacks to handle dismissal and errors
_rewardedAd!.fullScreenContentCallback = FullScreenContentCallback(
onAdShowedFullScreenContent: (ad) => debugPrint('[AdManager] Ad showing.'),
onAdDismissedFullScreenContent: (ad) {
debugPrint('[AdManager] Ad dismissed.');
ad.dispose();
_isAdLoaded = false;
_rewardedAd = null;
// Start loading the next ad immediately
_preloadAd();
if (!rewardCompleter.isCompleted) {
rewardCompleter.complete(false); // User closed before earning reward
}
},
onAdFailedToShowFullScreenContent: (ad, error) {
debugPrint('[AdManager] Ad failed to show: $error');
ad.dispose();
_isAdLoaded = false;
_rewardedAd = null;
if (!rewardCompleter.isCompleted) {
rewardCompleter.complete(false);
}
},
);
// 5. Show the ad
await _rewardedAd!.show(
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
debugPrint('[AdManager] Reward earned: ${reward.amount} ${reward.type}');
// The SSV callback will hit our server independently. We just need to signal the UI
// that the ad was successfully completed.
if (!rewardCompleter.isCompleted) {
rewardCompleter.complete(true);
}
},
);
return rewardCompleter.future;
}
@override
void dispose() {
_rewardedAd?.dispose();
super.dispose();
}
}
+44
View File
@@ -0,0 +1,44 @@
// =============================================================================
// app_config.example.dart — TEMPLATE FOR SERVER SETTINGS
// ─────────────────────────────────────────────────────────────────────────────
// Rename this file to `app_config.dart` and fill in your real values below.
// Do not commit your real `app_config.dart` to GitHub!
// =============================================================================
class AppConfig {
// ── STEP 1: Paste your Hetzner server IP here ─────────────────────────────
static const String serverIp = '127.0.0.1'; // REPLACE WITH REAL IP
// ── STEP 2: WireGuard UDP traffic port (default: 51820) ───────────────────
static const String wgPort = '4433';
// ── STEP 3: AmneziaWG DPI Obfuscation Settings ───────────────────────────
// These parameters must match exactly what your amnezia-wg-easy server expects.
static const int awgJc = 4;
static const int awgJmin = 20;
static const int awgJmax = 100;
static const int awgS1 = 15;
static const int awgS2 = 23;
static const int awgH1 = 1;
static const int awgH2 = 2;
static const int awgH3 = 3;
static const int awgH4 = 4;
// ── STEP 4: AdMob Rewarded Video ID ──────────────────────────────────────
static const String adUnitId = 'ca-app-pub-0000000000000000/0000000000'; // REPLACE WITH REAL AD UNIT
// ── STEP 5: Network & Tunnel Settings ────────────────────────────────────
static const String dnsServers = '1.1.1.1, 1.0.0.1';
static const String allowedIps = '0.0.0.0/0, ::/0';
static const int mtu = 1280;
static const int persistentKeepalive = 25;
static const int hivemindPort = 5000;
// ─────────────────────────────────────────────────────────────────────────
// Nothing below this line needs to be changed.
// ── STEP 1: Hivemind Server API ──────────────────────────────────────────
static String get hivemindApiBase => 'https://yourdomain.duckdns.org/api'; // REPLACE WITH REAL DOMAIN
/// The full server address passed to the WireGuard tunnel (IP:port).
static String get serverEndpoint => '$serverIp:$wgPort';
}
+46
View File
@@ -0,0 +1,46 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
class CryptoService {
static const String _privKeyPref = 'wg_private_key';
static const String _pubKeyPref = 'wg_public_key';
static const String _deviceIdPref = 'device_uuid';
static const _storage = FlutterSecureStorage();
// ── Get or create the persistent device UUID ──────────────────────────────
static Future<String> getDeviceId() async {
final existing = await _storage.read(key: _deviceIdPref);
if (existing != null) return existing;
final newId = const Uuid().v4();
await _storage.write(key: _deviceIdPref, value: newId);
return newId;
}
// ── Get or create the X25519 keypair for WireGuard ────────────────────────
static Future<Map<String, String>> getOrCreateKeys() async {
String? privBase64 = await _storage.read(key: _privKeyPref);
String? pubBase64 = await _storage.read(key: _pubKeyPref);
if (privBase64 != null && pubBase64 != null) {
return {'privateKey': privBase64, 'publicKey': pubBase64};
}
final algorithm = X25519();
final keyPair = await algorithm.newKeyPair();
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
final publicKey = await keyPair.extractPublicKey();
privBase64 = base64Encode(privateKeyBytes);
pubBase64 = base64Encode(publicKey.bytes);
await _storage.write(key: _privKeyPref, value: privBase64);
await _storage.write(key: _pubKeyPref, value: pubBase64);
return {'privateKey': privBase64, 'publicKey': pubBase64};
}
}
+102
View File
@@ -0,0 +1,102 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:paladinvpn/logic/app_config.dart';
import 'package:paladinvpn/logic/crypto_service.dart';
class HivemindService {
/// Polls the Hivemind server until the session is ready or timeout occurs.
/// Returns the WireGuard config string.
static Future<String> fetchConfigWithPolling() async {
final deviceId = await CryptoService.getDeviceId();
final keys = await CryptoService.getOrCreateKeys();
final privKey = keys['privateKey']!;
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
// ── TEMPORARY AD BYPASS HACK FOR TESTING ──
// Since you disabled the crypto signature check on the Python server,
// we can just send a fake "Ad Watched" ping from the app directly.
try {
final customData = jsonEncode({
'device_id': deviceId,
'public_key': keys['publicKey']!,
'ad_type': 'main_ad',
});
final fakeAdmobPingUrl = Uri.parse(
'${AppConfig.hivemindApiBase}/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}');
await http.get(fakeAdmobPingUrl).timeout(const Duration(seconds: 2));
} catch (_) {}
// ──────────────────────────────────────────
int attempts = 0;
while (attempts < 15) {
try {
final response = await http.get(url).timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// Debugging log to see exactly what the server returned
print('[HivemindService] Polling response: ${response.body}');
if (data['active'] == true && data['client_ip'] != null && data['server_pubkey'] != null) {
final clientIp = data['client_ip'];
final serverPubkey = data['server_pubkey'];
// Construct the WireGuard config locally
// I CHANGED IPS MAY BREAK STUFF
final configText = '''
[Interface]
PrivateKey = $privKey
Address = $clientIp/24
DNS = ${AppConfig.dnsServers}
MTU = ${AppConfig.mtu}
Jc = ${AppConfig.awgJc}
Jmin = ${AppConfig.awgJmin}
Jmax = ${AppConfig.awgJmax}
S1 = ${AppConfig.awgS1}
S2 = ${AppConfig.awgS2}
H1 = ${AppConfig.awgH1}
H2 = ${AppConfig.awgH2}
H3 = ${AppConfig.awgH3}
H4 = ${AppConfig.awgH4}
[Peer]
PublicKey = $serverPubkey
AllowedIPs = ${AppConfig.allowedIps}
Endpoint = ${AppConfig.serverEndpoint}
PersistentKeepalive = ${AppConfig.persistentKeepalive}
''';
return configText;
}
} else {
print('[HivemindService] Non-200 status: ${response.statusCode}');
}
} catch (e) {
print('[HivemindService] Polling exception: $e');
// Ignore network errors and keep polling
}
attempts++;
await Future.delayed(const Duration(seconds: 2));
}
throw Exception("Session not activated. S2S callback failed or timed out.");
}
/// Disconnects the session (cleans up on server)
static Future<void> disconnectAndCleanup() async {
final deviceId = await CryptoService.getDeviceId();
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/stop');
try {
await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'device_id': deviceId}),
).timeout(const Duration(seconds: 3));
} catch (_) {
// Best effort cleanup
}
}
}
+170
View File
@@ -0,0 +1,170 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:paladinvpn/logic/app_config.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
import 'package:paladinvpn/logic/crypto_service.dart';
/// SessionTimer acts as the client-side session authority for the VPN tunnel.
///
/// It connects to the Hivemind API on the server to sync the true remaining
/// time, data quotas, speed, and throttling status.
class SessionTimer extends ChangeNotifier {
Timer? _timer;
int _tickCount = 0;
final VpnConnection vpnConnection;
// ── Session State ──
int _remainingSeconds = 0;
int _quotaBytes = 0;
int _usedBytes = 0;
bool _isThrottled = false;
// ── Speed State ──
int _lastUsedBytes = 0;
double _currentSpeedKbps = 0.0; // Total speed (RX+TX)
SessionTimer({required this.vpnConnection});
int get remaining => _remainingSeconds;
bool get isRunning => _timer != null && _timer!.isActive;
bool get isExpired => _remainingSeconds <= 0 && !isRunning;
int get quotaBytes => _quotaBytes;
int get usedBytes => _usedBytes;
int get remainingBytes => _quotaBytes > _usedBytes ? _quotaBytes - _usedBytes : 0;
bool get isThrottled => _isThrottled;
double get currentSpeedKbps => _currentSpeedKbps;
String get formatted {
final h = (_remainingSeconds ~/ 3600).toString().padLeft(2, '0');
final m = ((_remainingSeconds % 3600) ~/ 60).toString().padLeft(2, '0');
final s = (_remainingSeconds % 60).toString().padLeft(2, '0');
return '$h:$m:$s';
}
String get formattedDataRemaining {
if (_quotaBytes == 0) return '0.00 GB';
final gb = remainingBytes / (1024 * 1024 * 1024);
return '${gb.toStringAsFixed(2)} GB';
}
double get progress {
// Max duration is generally 60 or 90 minutes. We'll base the ring purely on data usage or just standard time
// For UI simplicity, we'll return data progress:
if (_quotaBytes == 0) return 0.0;
return (_quotaBytes - _usedBytes) / _quotaBytes;
}
Future<void> start(String adType) async {
_remainingSeconds = 3600; // optimistic start
_quotaBytes = 2 * 1024 * 1024 * 1024;
_usedBytes = 0;
_lastUsedBytes = 0;
_currentSpeedKbps = 0.0;
_isThrottled = false;
_tickCount = 0;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), _tick);
// Kick off an immediate sync so the UI shows real data right away
_syncWithHivemind();
}
void _tick(Timer t) {
if (_remainingSeconds > 0) {
_remainingSeconds--;
}
_tickCount++;
// Poll the server every 3 seconds to get live stats
if (_tickCount % 3 == 0) {
_syncWithHivemind();
}
notifyListeners();
}
Future<void> _syncWithHivemind() async {
try {
final deviceId = await CryptoService.getDeviceId();
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
final response = await http.get(url).timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final bool active = data['active'] ?? false;
if (!active) {
// Server killed the session!
stop();
vpnConnection.disconnect();
return;
}
_remainingSeconds = data['expires_in_seconds'] ?? _remainingSeconds;
_quotaBytes = data['quota_bytes'] ?? _quotaBytes;
_usedBytes = data['used_bytes'] ?? _usedBytes;
_isThrottled = data['is_throttled'] ?? false;
// Calculate speed over the polling interval
final int deltaBytes = _usedBytes - _lastUsedBytes;
if (_lastUsedBytes > 0 && deltaBytes > 0) {
_currentSpeedKbps = (deltaBytes / 3) / 1024;
} else {
_currentSpeedKbps = 0.0;
}
_lastUsedBytes = _usedBytes;
notifyListeners();
} else if (response.statusCode == 404) {
// Peer not found on server (expired/deleted natively)
stop();
vpnConnection.disconnect();
}
} catch (e) {
debugPrint('Hivemind sync error: $e');
// If we can't reach the server, just let the local timer run
}
}
Future<void> addBonusTime() async {
final ip = vpnConnection.internalIp;
if (ip != null) {
try {
final deviceId = await CryptoService.getDeviceId();
await http.post(
Uri.parse('${AppConfig.hivemindApiBase}/session/start'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'ip': ip,
'device_id': deviceId,
'ad_type': 'bonus_ad'
}),
).timeout(const Duration(seconds: 5));
// Immediately sync to fetch the updated limits and un-throttled status
_syncWithHivemind();
} catch (e) {
debugPrint('Failed to add bonus time with Hivemind: $e');
}
}
}
void stop() {
_timer?.cancel();
_timer = null;
_remainingSeconds = 0;
_currentSpeedKbps = 0.0;
notifyListeners();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}
+216
View File
@@ -0,0 +1,216 @@
// =============================================================================
// vpn_connection.dart — WireGuard Tunnel Manager
// ─────────────────────────────────────────────────────────────────────────────
// This file manages the WireGuard tunnel on the Android device.
// It is the bridge between the UI (the big connect button) and the actual
// VPN tunnel running inside the Android operating system.
//
// HOW IT WORKS:
// 1. When connect() is called, it first asks WgApiService to fetch the config
// file for this device from the server (see wg_api_service.dart).
// 2. It passes that config text to the wireguard_flutter plugin, which handles
// all the native Android VPN stuff automatically.
// 3. It listens to the native tunnel state and updates the UI accordingly.
//
// You never need to edit this file.
// Server settings → app_config.dart
// Server communication → wg_api_service.dart
// =============================================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:wireguard_flutter/wireguard_flutter.dart';
import 'package:paladinvpn/logic/app_config.dart';
import 'package:paladinvpn/logic/hivemind_service.dart';
// ── VPN States ────────────────────────────────────────────────────────────────
// These are the possible states the tunnel can be in.
// The UI uses these to decide what to display (spinner, shield icon, etc.)
enum VpnStatus {
disconnected, // No tunnel active — idle state
connecting, // Fetching config from server + starting tunnel
connected, // Tunnel is up and routing traffic
disconnecting, // Tearing down the tunnel
error, // Something went wrong (server unreachable, etc.)
}
// ── VpnConnection ─────────────────────────────────────────────────────────────
// This is a "ChangeNotifier" — it works like a Python observable/event emitter.
// Whenever the state changes, it automatically notifies all widgets that are
// listening, and they re-render themselves. You never call setState() manually.
class VpnConnection extends ChangeNotifier {
// Current tunnel state (starts disconnected)
VpnStatus _status = VpnStatus.disconnected;
VpnStatus get status => _status;
// Short message shown to the user under the connect button
String _statusMessage = 'Tap to connect';
String get statusMessage => _statusMessage;
// Optional error message shown if something goes wrong
String? _errorMessage;
String? get errorMessage => _errorMessage;
// The internal WireGuard IP assigned to this device (e.g. 10.8.0.2)
// Populated after a successful config fetch and used by SessionTimer.
String? internalIp;
// The wireguard_flutter plugin instance — this is what actually talks to Android
final _wireguard = WireGuardFlutter.instance;
// Subscription to the native tunnel state stream
StreamSubscription? _stageSub;
// ── Initialization ────────────────────────────────────────────────────────
// Called automatically when the app starts (via Provider in main.dart)
VpnConnection() {
_init();
}
Future<void> _init() async {
// WireGuard doesn't work in a web browser — skip on web
if (kIsWeb) return;
try {
// Register our tunnel interface with Android.
// 'Paladin0' is just the name of the network interface — like "eth0"
await _wireguard.initialize(interfaceName: 'Paladin0');
} catch (e) {
debugPrint('WireGuard init error (expected on emulator): $e');
}
// Start listening to native tunnel state changes from Android.
// When Android reports the tunnel went up/down, _mapStage() is called.
try {
_stageSub = _wireguard.vpnStageSnapshot.listen(_mapStage);
} catch (_) {}
}
// ── Native State Mapping ──────────────────────────────────────────────────
// Android sends back low-level states like "authenticating", "preparing", etc.
// This function translates those into our simpler 5-state enum above.
void _mapStage(VpnStage stage) {
switch (stage) {
case VpnStage.connected:
_setStatus(VpnStatus.connected, 'Secured');
case VpnStage.disconnected:
_setStatus(VpnStatus.disconnected, 'Tap to connect');
case VpnStage.connecting:
case VpnStage.waitingConnection:
case VpnStage.authenticating:
case VpnStage.preparing:
case VpnStage.reconnect:
_setStatus(VpnStatus.connecting, 'Establishing tunnel…');
case VpnStage.disconnecting:
case VpnStage.exiting:
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
default:
_setStatus(VpnStatus.disconnected, 'Tap to connect');
}
}
// ── Connect ───────────────────────────────────────────────────────────────
// This is the main function — called after the user watches the rewarded ad.
//
// Step 1: Call the WG-Easy API to get the .conf for this device
// Step 2: Pass that .conf to the WireGuard plugin to start the tunnel
//
// Returns true if the connection was successful, false if it failed.
// The connect button uses this to decide whether to start the timer.
Future<bool> connect() async {
// Don't do anything if we're already connected or currently connecting
if (_status == VpnStatus.connected || _status == VpnStatus.connecting) return false;
_setStatus(VpnStatus.connecting, 'Fetching config…');
_errorMessage = null;
// ── Web / emulator fallback ────────────────────────────────────────────
if (kIsWeb) {
await Future.delayed(const Duration(seconds: 1));
_setStatus(VpnStatus.connected, 'Secured (dev mode)');
return true;
}
// ── Step 2: Fetch config from Hivemind ──
String configText;
try {
configText = await HivemindService.fetchConfigWithPolling();
// Parse the internal IP (e.g. "Address = 10.8.0.2/24" or "/32")
final regex = RegExp(r'Address\s*=\s*([0-9\.]+)/');
final match = regex.firstMatch(configText);
if (match != null) {
internalIp = match.group(1);
}
} catch (e) {
debugPrint('Hivemind API error: $e');
// Set the exact error as the status message so the user can see it on screen
String errorMsg = e.toString().replaceAll('Exception: ', '');
if (errorMsg.length > 30) {
errorMsg = '${errorMsg.substring(0, 30)}...'; // keep it somewhat short for the UI
}
_errorMessage = e.toString();
_setStatus(VpnStatus.error, errorMsg);
return false;
}
// ── Step 2: Start the WireGuard tunnel with the fetched config ─────────
try {
// Re-initialize to ensure VPN permission is granted before starting
await _wireguard.initialize(interfaceName: 'Paladin0');
await _wireguard.startVpn(
serverAddress: AppConfig.serverEndpoint,
wgQuickConfig: configText,
providerBundleIdentifier: 'com.paladinvpn.app.WireGuardProvider',
);
_setStatus(VpnStatus.connected, 'Secured');
return true;
} catch (e) {
debugPrint('WireGuard tunnel error: $e');
_errorMessage = 'Tunnel failed to start.\nTry reconnecting.';
_setStatus(VpnStatus.error, 'Tunnel error');
return false;
}
}
// ── Disconnect ────────────────────────────────────────────────────────────
// Tears down the active tunnel and returns Android to normal networking.
Future<void> disconnect() async {
if (_status == VpnStatus.disconnected) return;
_setStatus(VpnStatus.disconnecting, 'Tearing down…');
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 500));
_setStatus(VpnStatus.disconnected, 'Tap to connect');
return;
}
try {
await _wireguard.stopVpn();
} catch (e) {
debugPrint('WireGuard stop error: $e');
}
// Clean up the peer from the server so we don't leave dead connections lying around
await HivemindService.disconnectAndCleanup();
_setStatus(VpnStatus.disconnected, 'Tap to connect');
}
// ── Internal state updater ────────────────────────────────────────────────
// Updates state and triggers a UI re-render (like calling setState() in Python/Qt)
void _setStatus(VpnStatus s, String msg) {
_status = s;
_statusMessage = msg;
notifyListeners(); // tells all listening widgets to rebuild
}
@override
void dispose() {
_stageSub?.cancel();
super.dispose();
}
}
+89
View File
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:paladinvpn/logic/vpn_connection.dart';
import 'package:paladinvpn/logic/session_timer.dart';
import 'package:paladinvpn/screens/main_screen.dart';
import 'package:paladinvpn/screens/intro.dart';
/// The entry point for the Paladin VPN application.
void main() {
// Ensure that the Flutter engine is fully initialized before using platform channels
// (e.g., for SystemChrome or MobileAds).
WidgetsFlutterBinding.ensureInitialized();
// Lock the application to portrait orientation since the dashboard is a single-screen utility app
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
// Configure a stealth system bar layout: transparent status bar with light icons,
// blending seamlessly into the dark background gradient.
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: Color(0xFF0D1117), // Deep space black
systemNavigationBarIconBrightness: Brightness.light,
));
// Inflate the root widget
runApp(const PaladinApp());
}
/// The root application widget wrapping the app in necessary state providers and themes.
class PaladinApp extends StatelessWidget {
const PaladinApp({super.key});
@override
Widget build(BuildContext context) {
// MultiProvider injects global state controllers down the widget tree.
return MultiProvider(
providers: [
// Manages the WireGuard tunnel lifecycle
ChangeNotifierProvider(create: (_) => VpnConnection()),
// Manages the 1-hour session clock.
// Uses ProxyProvider to automatically inject the VpnConnection,
// allowing the timer to auto-disconnect the VPN when time expires.
ChangeNotifierProxyProvider<VpnConnection, SessionTimer>(
create: (ctx) => SessionTimer(
vpnConnection: ctx.read<VpnConnection>(),
),
update: (_, vpn, prev) =>
prev ?? SessionTimer(vpnConnection: vpn),
),
],
child: MaterialApp(
title: 'Paladin VPN',
debugShowCheckedModeBanner: false,
theme: _buildDarkTheme(),
home: const IntroScreen(),
),
);
}
/// Constructs the global dark theme used across the application.
ThemeData _buildDarkTheme() {
const cyanAccent = Color(0xFF00E5FF);
const bgDark = Color(0xFF0D1117);
return ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: bgDark,
colorScheme: const ColorScheme.dark(
primary: cyanAccent,
secondary: cyanAccent,
surface: Color(0xFF151C28),
),
fontFamily: 'Roboto',
snackBarTheme: SnackBarThemeData(
backgroundColor: const Color(0xFF1E2533),
contentTextStyle: const TextStyle(color: Colors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
behavior: SnackBarBehavior.floating,
),
useMaterial3: true,
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:paladinvpn/screens/main_screen.dart';
class IntroScreen extends StatefulWidget {
const IntroScreen({super.key});
@override
State<IntroScreen> createState() => _IntroScreenState();
}
class _IntroScreenState extends State<IntroScreen> {
@override
void initState() {
super.initState();
// Wait for 2.5 seconds then smoothly transition to the main screen
Timer(const Duration(milliseconds: 2500), () {
if (mounted) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 800),
pageBuilder: (_, __, ___) => const MainScreen(),
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117), // Deep space black background
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App Logo
Image.asset(
'assets/logo.png',
width: 140,
height: 140,
errorBuilder: (context, error, stackTrace) {
// Fallback icon if logo.png is missing or misconfigured
return const Icon(
Icons.shield,
size: 140,
color: Color(0xFF00E5FF),
);
},
),
const SizedBox(height: 30),
// PaladinVPN Text
const Text(
'PALADIN VPN',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
),
),
const SizedBox(height: 60),
// Loading Circle
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: Color(0xFF00E5FF),
strokeWidth: 3.0,
),
),
],
),
),
);
}
}
+89
View File
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:paladinvpn/components/header.dart';
import 'package:paladinvpn/components/status_text.dart';
import 'package:paladinvpn/components/connect_button.dart';
import 'package:paladinvpn/components/bottom_bar.dart';
import 'package:paladinvpn/components/info_button.dart';
import 'package:paladinvpn/components/privacy_policy_button.dart';
/// MainScreen serves as the primary home screen for Paladin VPN.
///
/// It provides a dark mode, cyberpunk-inspired visual layout displaying:
/// - Logo and Branding at the top (`Header`)
/// - Session countdown timer displaying remaining time (`ClockDisplay`)
/// - Tap-to-connect action trigger (`ConnectButton`)
/// - Connected status badge displaying tunnel information (`StatusText`)
/// - Bottom bar showing server health status and voluntary support button (`BottomBar`)
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
// Background base color
static const Color _bgTop = Color(0xFF0D1117);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bgTop,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
color: _bgTop,
image: DecorationImage(
image: AssetImage('assets/background.png'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Color(0xAA0D1117), // Deep dark tint overlay to guarantee readability of overlay elements
BlendMode.darken,
),
),
),
child: SafeArea(
child: Stack(
children: [
// ── Main content column ──
LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: const IntrinsicHeight(
child: Column(
children: [
SizedBox(height: 4),
Header(),
Spacer(flex: 1),
ConnectButton(),
SizedBox(height: 24),
StatusText(),
Spacer(flex: 3),
BottomBar(),
SizedBox(height: 16),
],
),
),
),
);
},
),
// ── Info / About buttons (top-right) ──
const Positioned(
top: 4,
right: 4,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
PrivacyPolicyButton(),
InfoButton(),
],
),
),
],
),
),
),
);
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 PT BILLION JAYA GROUP
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,66 @@
group 'billion.group.wireguard_flutter'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.8.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 21
}
namespace 'billion.group.wireguard_flutter'
packaging {
resources {
excludes += ['META-INF/**']
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.zaneschepke:amneziawg-android:2.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
implementation 'com.beust:klaxon:5.5'
implementation 'androidx.appcompat:appcompat:1.6.1'
/// to run wireguard on Android 25 or lower
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
}
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -0,0 +1 @@
rootProject.name = 'wireguard_dart'
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,320 @@
package billion.group.wireguard_flutter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
import android.app.Activity
import io.flutter.embedding.android.FlutterActivity
import android.content.Intent
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.util.Log
import com.beust.klaxon.Klaxon
import org.amnezia.awg.backend.*
import org.amnezia.awg.crypto.Key
import org.amnezia.awg.crypto.KeyPair
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import java.util.*
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
/** WireguardFlutterPlugin */
const val PERMISSIONS_REQUEST_CODE = 10014
const val METHOD_CHANNEL_NAME = "billion.group.wireguard_flutter/wgcontrol"
const val METHOD_EVENT_NAME = "billion.group.wireguard_flutter/wgstage"
class WireguardFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
PluginRegistry.ActivityResultListener {
private lateinit var channel: MethodChannel
private lateinit var events: EventChannel
private lateinit var tunnelName: String
private val futureBackend = CompletableDeferred<Backend>()
private var vpnStageSink: EventChannel.EventSink? = null
private val scope = CoroutineScope(Job() + Dispatchers.Main.immediate)
private var backend: Backend? = null
private var havePermission = false
private lateinit var context: Context
private var activity: Activity? = null
private var config: org.amnezia.awg.config.Config? = null
private var tunnel: WireGuardTunnel? = null
private val TAG = "NVPN"
var isVpnChecked = false
companion object {
private var state: String = "no_connection"
fun getStatus(): String {
return state
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
this.havePermission =
(requestCode == PERMISSIONS_REQUEST_CODE) && (resultCode == Activity.RESULT_OK)
return havePermission
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
this.activity = activityPluginBinding.activity as FlutterActivity
}
override fun onDetachedFromActivityForConfigChanges() {
this.activity = null
}
override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) {
this.activity = activityPluginBinding.activity as FlutterActivity
}
override fun onDetachedFromActivity() {
this.activity = null
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL_NAME)
events = EventChannel(flutterPluginBinding.binaryMessenger, METHOD_EVENT_NAME)
context = flutterPluginBinding.applicationContext
scope.launch(Dispatchers.IO) {
try {
backend = createBackend()
futureBackend.complete(backend!!)
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
}
}
channel.setMethodCallHandler(this)
events.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
isVpnChecked = false
vpnStageSink = events
}
override fun onCancel(arguments: Any?) {
isVpnChecked = false
vpnStageSink = null
}
})
}
private fun createBackend(): Backend {
if (backend == null) {
backend = GoBackend(context, object : TunnelActionHandler {
override fun runPreUp(commands: Collection<String>) {}
override fun runPostUp(commands: Collection<String>) {}
override fun runPreDown(commands: Collection<String>) {}
override fun runPostDown(commands: Collection<String>) {}
})
}
return backend as Backend
}
private fun flutterSuccess(result: Result, o: Any) {
scope.launch(Dispatchers.Main) {
result.success(o)
}
}
private fun flutterError(result: Result, error: String) {
scope.launch(Dispatchers.Main) {
result.error(error, null, null)
}
}
private fun flutterNotImplemented(result: Result) {
scope.launch(Dispatchers.Main) {
result.notImplemented()
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"initialize" -> setupTunnel(call.argument<String>("localizedDescription").toString(), result)
"start" -> {
connect(call.argument<String>("wgQuickConfig").toString(), result)
if (!isVpnChecked) {
if (isVpnActive()) {
state = "connected"
isVpnChecked = true
println("VPN is active")
} else {
state = "disconnected"
isVpnChecked = true
println("VPN is not active")
}
}
}
"stop" -> {
disconnect(result)
}
"stage" -> {
result.success(getStatus())
}
"checkPermission" -> {
checkPermission()
result.success(null)
}
else -> flutterNotImplemented(result)
}
}
private fun isVpnActive(): Boolean {
try {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val activeNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
return networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
} else {
return false
}
} catch (e: Exception) {
Log.e(TAG, "isVpnActive - ERROR - ${e.message}")
return false
}
}
private fun updateStage(stage: String?) {
scope.launch(Dispatchers.Main) {
val updatedStage = stage ?: "no_connection"
state = updatedStage
vpnStageSink?.success(updatedStage.lowercase(Locale.ROOT))
}
}
private fun updateStageFromState(state: Tunnel.State) {
scope.launch(Dispatchers.Main) {
when (state) {
Tunnel.State.UP -> updateStage("connected")
Tunnel.State.DOWN -> updateStage("disconnected")
else -> updateStage("wait_connection")
}
}
}
private fun disconnect(result: Result) {
scope.launch(Dispatchers.IO) {
try {
if (futureBackend.await().getRunningTunnelNames().isEmpty()) {
updateStage("disconnected")
throw Exception("Tunnel is not running")
}
updateStage("disconnecting")
futureBackend.await().setState(
tunnel(tunnelName) { state ->
scope.launch(Dispatchers.Main) {
Log.i(TAG, "onStateChange - $state")
updateStageFromState(state)
}
}, Tunnel.State.DOWN, config
)
Log.i(TAG, "Disconnect - success!")
flutterSuccess(result, "")
} catch (e: BackendException) {
Log.e(TAG, "Disconnect - BackendException - ERROR - ${e.reason}", e)
flutterError(result, e.reason.toString())
} catch (e: Throwable) {
Log.e(TAG, "Disconnect - Can't disconnect from tunnel: ${e.message}")
flutterError(result, e.message.toString())
}
}
}
private fun connect(wgQuickConfig: String, result: Result) {
scope.launch(Dispatchers.IO) {
try {
if (!havePermission) {
checkPermission()
throw Exception("Permissions are not given")
}
updateStage("prepare")
val inputStream = ByteArrayInputStream(wgQuickConfig.toByteArray())
config = org.amnezia.awg.config.Config.parse(inputStream)
updateStage("connecting")
futureBackend.await().setState(
tunnel(tunnelName) { state ->
scope.launch(Dispatchers.Main) {
Log.i(TAG, "onStateChange - $state")
updateStageFromState(state)
}
}, Tunnel.State.UP, config
)
Log.i(TAG, "Connect - success!")
flutterSuccess(result, "")
} catch (e: BackendException) {
Log.e(TAG, "Connect - BackendException - ERROR - ${e.reason}", e)
flutterError(result, e.reason.toString())
} catch (e: Throwable) {
Log.e(TAG, "Connect - Can't connect to tunnel: $e", e)
flutterError(result, e.message.toString())
}
}
}
private fun setupTunnel(localizedDescription: String, result: Result) {
scope.launch(Dispatchers.IO) {
if (Tunnel.isNameInvalid(localizedDescription)) {
flutterError(result, "Invalid Name")
return@launch
}
tunnelName = localizedDescription
checkPermission()
result.success(null)
}
}
private fun checkPermission() {
val intent = android.net.VpnService.prepare(this.activity)
if (intent != null) {
havePermission = false
this.activity?.startActivityForResult(intent, PERMISSIONS_REQUEST_CODE)
} else {
havePermission = true
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
events.setStreamHandler(null)
isVpnChecked = false
}
private fun tunnel(name: String, callback: StateChangeCallback? = null): WireGuardTunnel {
if (tunnel == null) {
tunnel = WireGuardTunnel(name, callback)
}
return tunnel as WireGuardTunnel
}
}
typealias StateChangeCallback = (Tunnel.State) -> Unit
class WireGuardTunnel(
private val name: String, private val onStateChanged: StateChangeCallback? = null
) : Tunnel {
override fun getName() = name
override fun onStateChange(newState: Tunnel.State) {
onStateChanged?.invoke(newState)
}
override fun isIpv4ResolutionPreferred(): Boolean? = true
override fun isMetered(): Boolean? = false
}
@@ -0,0 +1,117 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:process_run/shell.dart';
import '../wireguard_flutter_platform_interface.dart';
class WireGuardFlutterLinux extends WireGuardFlutterInterface {
String? name;
File? configFile;
VpnStage _stage = VpnStage.noConnection;
final _stageController = StreamController<VpnStage>.broadcast();
void _setStage(VpnStage stage) {
_stage = stage;
_stageController.add(stage);
}
final shell = Shell(runInShell: true, verbose: kDebugMode);
@override
Future<void> initialize({required String interfaceName}) async {
name = interfaceName.replaceAll(' ', '_');
await refreshStage();
}
Future<String> get filePath async {
final tempDir = await getTemporaryDirectory();
return '${tempDir.path}${Platform.pathSeparator}$name.conf';
}
@override
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
}) async {
final isAlreadyConnected = await isConnected();
if (!isAlreadyConnected) {
_setStage(VpnStage.preparing);
} else {
debugPrint('Already connected');
}
try {
configFile = await File(await filePath).create();
await configFile!.writeAsString(wgQuickConfig);
} on PathAccessException {
debugPrint('Denied to write file. Trying to start interface');
if (isAlreadyConnected) {
return _setStage(VpnStage.connected);
}
try {
await shell.run('sudo wg-quick up $name');
} catch (_) {
} finally {
_setStage(VpnStage.denied);
}
}
if (!isAlreadyConnected) {
_setStage(VpnStage.connecting);
await shell.run('sudo wg-quick up ${configFile?.path ?? await filePath}');
_setStage(VpnStage.connected);
}
}
@override
Future<void> stopVpn() async {
assert(
(await isConnected()),
'Bad state: vpn has not been started. Call startVpn',
);
_setStage(VpnStage.disconnecting);
try {
await shell
.run('sudo wg-quick down ${configFile?.path ?? (await filePath)}');
} catch (e) {
await refreshStage();
rethrow;
}
await refreshStage();
}
@override
Future<VpnStage> stage() async => _stage;
@override
Stream<VpnStage> get vpnStageSnapshot => _stageController.stream;
@override
Future<void> refreshStage() async {
if (await isConnected()) {
_setStage(VpnStage.connected);
} else if (name == null) {
_setStage(VpnStage.waitingConnection);
} else if (configFile == null) {
_setStage(VpnStage.noConnection);
} else {
_setStage(VpnStage.disconnected);
}
}
@override
Future<bool> isConnected() async {
assert(
name != null,
'Bad state: not initialized. Call "initialize" before calling this command',
);
final processResultList = await shell.run('sudo wg');
final process = processResultList.first;
return process.outLines.any((line) => line.trim() == 'interface: $name');
}
}
@@ -0,0 +1,62 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:wireguard_flutter/linux/wireguard_flutter_linux.dart';
import 'package:wireguard_flutter/wireguard_flutter_method_channel.dart';
import 'wireguard_flutter_platform_interface.dart';
export 'wireguard_flutter_platform_interface.dart' show VpnStage;
class WireGuardFlutter extends WireGuardFlutterInterface {
static WireGuardFlutterInterface? __instance;
static WireGuardFlutterInterface get _instance => __instance!;
static WireGuardFlutterInterface get instance {
registerWith();
return _instance;
}
static void registerWith() {
if (__instance == null) {
if (kIsWeb) {
throw UnsupportedError('The web platform is not supported');
} else if (Platform.isLinux) {
__instance = WireGuardFlutterLinux();
} else {
__instance = WireGuardFlutterMethodChannel();
}
}
}
WireGuardFlutter._();
@override
Stream<VpnStage> get vpnStageSnapshot => _instance.vpnStageSnapshot;
@override
Future<void> initialize({required String interfaceName}) {
return _instance.initialize(interfaceName: interfaceName);
}
@override
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
}) async {
return _instance.startVpn(
serverAddress: serverAddress,
wgQuickConfig: wgQuickConfig,
providerBundleIdentifier: providerBundleIdentifier,
);
}
@override
Future<void> stopVpn() => _instance.stopVpn();
@override
Future<void> refreshStage() => _instance.refreshStage();
@override
Future<VpnStage> stage() => _instance.stage();
}
@@ -0,0 +1,60 @@
import 'package:flutter/services.dart';
import 'wireguard_flutter_platform_interface.dart';
class WireGuardFlutterMethodChannel extends WireGuardFlutterInterface {
static const _methodChannelVpnControl =
"billion.group.wireguard_flutter/wgcontrol";
static const _methodChannel = MethodChannel(_methodChannelVpnControl);
static const _eventChannelVpnStage =
'billion.group.wireguard_flutter/wgstage';
static const _eventChannel = EventChannel(_eventChannelVpnStage);
@override
Stream<VpnStage> get vpnStageSnapshot =>
_eventChannel.receiveBroadcastStream().map(
(event) => event == VpnStage.denied.code
? VpnStage.disconnected
: VpnStage.values.firstWhere(
(stage) => stage.code == event,
orElse: () => VpnStage.noConnection,
),
);
@override
Future<void> initialize({required String interfaceName}) {
return _methodChannel.invokeMethod("initialize", {
"localizedDescription": interfaceName,
"win32ServiceName": interfaceName,
});
}
@override
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
}) async {
return _methodChannel.invokeMethod("start", {
"serverAddress": serverAddress,
"wgQuickConfig": wgQuickConfig,
"providerBundleIdentifier": providerBundleIdentifier,
});
}
@override
Future<void> stopVpn() => _methodChannel.invokeMethod('stop');
@override
Future<void> refreshStage() => _methodChannel.invokeMethod("refresh");
@override
Future<VpnStage> stage() => _methodChannel.invokeMethod("stage").then(
(value) => value != null
? VpnStage.values.firstWhere(
(stage) => stage.code == value.toString(),
orElse: () => VpnStage.disconnected,
)
: VpnStage.disconnected,
);
}
@@ -0,0 +1,36 @@
abstract class WireGuardFlutterInterface {
Stream<VpnStage> get vpnStageSnapshot;
Future<void> initialize({required String interfaceName});
Future<void> startVpn({
required String serverAddress,
required String wgQuickConfig,
required String providerBundleIdentifier,
});
Future<void> stopVpn();
Future<void> refreshStage();
Future<VpnStage> stage();
Future<bool> isConnected() =>
stage().then((stage) => stage == VpnStage.connected);
}
enum VpnStage {
connected('connected'),
connecting('connecting'),
disconnecting('disconnecting'),
disconnected('disconnected'),
waitingConnection('wait_connection'),
authenticating('authenticating'),
reconnect('reconnect'),
noConnection('no_connection'),
preparing('prepare'),
denied('denied'),
exiting('exiting');
final String code;
const VpnStage(this.code);
}
@@ -0,0 +1,28 @@
name: wireguard_flutter
description: Wireguard Flutter SDK
version: 0.1.3
homepage: https://flutterflux.com
repository: https://github.com/Caqil/wireguard_flutter
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.13.7'
dependencies:
flutter:
sdk: flutter
path_provider: ^2.1.2
plugin_platform_interface: ^2.0.2
process_run: ^0.14.0+1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
plugin:
platforms:
android:
package: billion.group.wireguard_flutter
pluginClass: WireguardFlutterPlugin
+729
View File
@@ -0,0 +1,729 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cryptography:
dependency: "direct main"
description:
name: cryptography
sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
url: "https://pub.dev"
source: hosted
version: "10.3.1"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
google_mobile_ads:
dependency: "direct main"
description:
name: google_mobile_ads
sha256: d2ef5ec1e1f31137fc241bdeab3037c31062d387dd221fd884fb1160444c788b
url: "https://pub.dev"
source: hosted
version: "4.0.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
url: "https://pub.dev"
source: hosted
version: "2.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
process_run:
dependency: transitive
description:
name: process_run
sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e"
url: "https://pub.dev"
source: hosted
version: "0.14.2"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev"
source: hosted
version: "3.4.0+1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
url: "https://pub.dev"
source: hosted
version: "6.3.30"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736
url: "https://pub.dev"
source: hosted
version: "4.9.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
url: "https://pub.dev"
source: hosted
version: "3.16.9"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04"
url: "https://pub.dev"
source: hosted
version: "2.15.1"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2"
url: "https://pub.dev"
source: hosted
version: "3.25.1"
win32:
dependency: transitive
description:
name: win32
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
url: "https://pub.dev"
source: hosted
version: "6.3.0"
wireguard_flutter:
dependency: "direct main"
description:
path: "local_packages/wireguard_flutter"
relative: true
source: path
version: "0.1.3"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.4"
+85
View File
@@ -0,0 +1,85 @@
name: paladinvpn
description: Paladin VPN — Minimalist high-performance WireGuard tunnel
# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.1+2
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.5
google_fonts: ^6.1.0
provider: ^6.1.1
google_mobile_ads: ^4.0.0
wireguard_flutter:
path: local_packages/wireguard_flutter
http: ^1.6.0
shared_preferences: ^2.5.5
cryptography: ^2.9.0
url_launcher: ^6.3.2
flutter_secure_storage: ^10.3.1
uuid: ^4.5.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- assets/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages