feat(app): add secure routing and runtime resilience

Add explicit TUN and SOCKS5 connection modes, app routing controls, authenticated per-session local SOCKS5, Android network monitoring, persistent haptics, resilient session recovery, notification hardening and client-side Hivemind cleanup.

Keep the hosted flutter_vless_android 1.1.5 runtime as the base and apply the Android compatibility/security patches at build time instead of restoring the unstable vendored runtime.
This commit is contained in:
Naveax committed 2026-08-31 19:31:12 +03:00
1 parent 371db8da9f
commit 8ead3ff249
30 files changed
+3959 -484

No files matched your search

+160 -50
View File
@@ -13,6 +13,78 @@ enum UpdateStatus { upToDate, updateAvailable, checkFailed }
enum InstallSource { playStore, sideload }
Uri? _trustedHttpsUri(String raw, Set<String> allowedHosts) {
final uri = Uri.tryParse(raw);
if (uri == null || uri.scheme != 'https' || !allowedHosts.contains(uri.host)) {
return null;
}
return uri;
}
class _ParsedVersion {
final List<int> core;
final List<String> preRelease;
const _ParsedVersion(this.core, this.preRelease);
static _ParsedVersion? tryParse(String input) {
var value = input.trim();
if (value.startsWith('v') || value.startsWith('V')) {
value = value.substring(1);
}
value = value.split('+').first;
final dash = value.indexOf('-');
final corePart = dash < 0 ? value : value.substring(0, dash);
final prePart = dash < 0 ? '' : value.substring(dash + 1);
final rawCore = corePart.split('.');
if (rawCore.isEmpty || rawCore.any((p) => int.tryParse(p) == null)) {
return null;
}
final core = rawCore.map(int.parse).toList(growable: false);
while (core.length < 3) {
core.add(0);
}
final pre = prePart.isEmpty
? const <String>[]
: prePart.split('.').where((e) => e.isNotEmpty).toList(growable: false);
return _ParsedVersion(core, pre);
}
int compareTo(_ParsedVersion other) {
final length = core.length > other.core.length ? core.length : other.core.length;
for (var i = 0; i < length; i++) {
final left = i < core.length ? core[i] : 0;
final right = i < other.core.length ? other.core[i] : 0;
if (left != right) return left.compareTo(right);
}
if (preRelease.isEmpty && other.preRelease.isEmpty) return 0;
if (preRelease.isEmpty) return 1;
if (other.preRelease.isEmpty) return -1;
final preLength = preRelease.length > other.preRelease.length
? preRelease.length
: other.preRelease.length;
for (var i = 0; i < preLength; i++) {
if (i >= preRelease.length) return -1;
if (i >= other.preRelease.length) return 1;
final left = preRelease[i];
final right = other.preRelease[i];
final leftNum = int.tryParse(left);
final rightNum = int.tryParse(right);
if (leftNum != null && rightNum != null) {
if (leftNum != rightNum) return leftNum.compareTo(rightNum);
continue;
}
if (leftNum != null) return -1;
if (rightNum != null) return 1;
final lexical = left.compareTo(right);
if (lexical != 0) return lexical;
}
return 0;
}
}
class PlayStoreUpdater {
PlayStoreUpdater._();
@@ -20,9 +92,13 @@ class PlayStoreUpdater {
'https://play.google.com/store/apps/details?id=${AppConfig.applicationId}';
static Future<bool> open() async {
final uri = _trustedHttpsUri(_url, const {'play.google.com'});
if (uri == null) {
debugPrint('[Updater] Refusing invalid Play Store URL.');
return false;
}
try {
return await launchUrl(Uri.parse(_url),
mode: LaunchMode.externalApplication);
return await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {
debugPrint('[Updater] Failed to open Play Store: $e');
return false;
@@ -36,13 +112,14 @@ class GitHubUpdater {
static String? _cachedDownloadUrl;
static Future<String?> fetchLatestVersion() async {
final url =
'https://api.github.com/repos/${AppConfig.githubOwner}/${AppConfig.githubRepo}/releases/latest';
final apiUri = Uri.https(
'api.github.com',
'/repos/${AppConfig.githubOwner}/${AppConfig.githubRepo}/releases/latest',
);
try {
final response = await http
.get(Uri.parse(url),
headers: {'Accept': 'application/vnd.github+json'})
.get(apiUri, headers: {'Accept': 'application/vnd.github+json'})
.timeout(const Duration(seconds: 8));
if (response.statusCode != 200) {
@@ -50,38 +127,48 @@ class GitHubUpdater {
return null;
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
_cachedDownloadUrl = data['html_url'] as String?;
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) {
debugPrint('[Updater] Invalid GitHub release payload.');
return null;
}
final tag = data['tag_name'] as String? ?? '0.0.0';
return tag.startsWith('v') ? tag.substring(1) : tag;
final htmlUrl = decoded['html_url'];
if (htmlUrl is String &&
_trustedHttpsUri(htmlUrl, const {'github.com'}) != null) {
_cachedDownloadUrl = htmlUrl;
} else {
_cachedDownloadUrl = null;
}
final rawTag = decoded['tag_name'];
if (rawTag is! String || rawTag.isEmpty || rawTag.length > 64) {
debugPrint('[Updater] Invalid release tag.');
return null;
}
final normalized = rawTag.startsWith('v') ? rawTag.substring(1) : rawTag;
if (_ParsedVersion.tryParse(normalized) == null) {
debugPrint('[Updater] Release tag is not a supported version: $rawTag');
return null;
}
return normalized;
} catch (e) {
debugPrint('[Updater] GitHub API error: $e');
return null;
}
}
static bool _isTrustedReleaseUrl(String url) {
final uri = Uri.tryParse(url);
if (uri == null || uri.scheme != 'https' || uri.host != 'github.com') {
static Future<bool> open() async {
final raw = _cachedDownloadUrl ?? AppConfig.githubReleasesUrl;
final uri = _trustedHttpsUri(raw, const {'github.com'});
if (uri == null) {
debugPrint('[Updater] Refusing untrusted GitHub update URL.');
return false;
}
// Host alone is not enough: anyone can host a release on github.com.
return uri.path
.startsWith('/${AppConfig.githubOwner}/${AppConfig.githubRepo}/');
}
static Future<bool> open() async {
final cached = _cachedDownloadUrl;
final url = (cached != null && _isTrustedReleaseUrl(cached))
? cached
: AppConfig.githubReleasesUrl;
if (cached != null && url != cached) {
debugPrint('[Updater] Rejected untrusted release URL: $cached');
}
try {
return await launchUrl(Uri.parse(url),
mode: LaunchMode.externalApplication);
return await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {
debugPrint('[Updater] Failed to open GitHub: $e');
return false;
@@ -116,14 +203,13 @@ class Updater {
}
static int _compareVersions(String a, String b) {
final aParts = a.split('.').map((p) => int.tryParse(p) ?? 0).toList();
final bParts = b.split('.').map((p) => int.tryParse(p) ?? 0).toList();
for (int i = 0; i < 3; i++) {
final diff = (aParts.length > i ? aParts[i] : 0) -
(bParts.length > i ? bParts[i] : 0);
if (diff != 0) return diff;
final left = _ParsedVersion.tryParse(a);
final right = _ParsedVersion.tryParse(b);
if (left == null || right == null) {
// Invalid local metadata should not invent an ordering.
return 0;
}
return 0;
return left.compareTo(right);
}
static String _versionLabel(String v, bool isLocal) {
@@ -131,15 +217,27 @@ class Updater {
return '$prefix$v${isLocal ? ' (installed)' : ''}';
}
static Future<void> check({
static Future<void> check(BuildContext context) {
final navigator = Navigator.of(context, rootNavigator: true);
final messenger = ScaffoldMessenger.of(context);
return checkWithHandles(
navigator: navigator,
messenger: messenger,
);
}
static Future<void> checkWithHandles({
required NavigatorState navigator,
required ScaffoldMessengerState messenger,
}) async {
final info = await PackageInfo.fromPlatform();
final localVersion = info.version;
if (localVersion.isEmpty) {
_showSnackBar(messenger, 'Could not determine app version',
isError: true);
if (localVersion.isEmpty || _ParsedVersion.tryParse(localVersion) == null) {
_showSnackBar(
messenger,
'Could not determine app version',
isError: true,
);
return;
}
@@ -155,15 +253,18 @@ class Updater {
}
final source = await installSource;
final storeName = source == InstallSource.playStore ? 'Play Store' : 'GitHub';
final storeName =
source == InstallSource.playStore ? 'Play Store' : 'GitHub';
if (!navigator.mounted) return;
showDialog(
context: navigator.context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColors.bgCard,
title: const Text('Update available',
style: TextStyle(color: AppColors.textWhite)),
title: const Text(
'Update available',
style: TextStyle(color: AppColors.textWhite),
),
content: Text(
'${_versionLabel(latestVersion, false)} is available on $storeName.\n'
'You have ${_versionLabel(localVersion, true)}.',
@@ -172,8 +273,10 @@ class Updater {
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Later',
style: TextStyle(color: AppColors.slate70)),
child: const Text(
'Later',
style: TextStyle(color: AppColors.slate70),
),
),
TextButton(
onPressed: () {
@@ -182,24 +285,31 @@ class Updater {
? PlayStoreUpdater.open()
: GitHubUpdater.open();
},
child: Text('Open $storeName',
style: const TextStyle(color: AppColors.accent)),
child: Text(
'Open $storeName',
style: const TextStyle(color: AppColors.accent),
),
),
],
),
);
}
static void _showSnackBar(ScaffoldMessengerState messenger, String message,
{bool isError = false}) {
static void _showSnackBar(
ScaffoldMessengerState messenger,
String message, {
bool isError = false,
}) {
if (!messenger.mounted) return;
messenger.showSnackBar(
SnackBar(
content: Row(
children: [
Icon(isError ? Icons.error_outline : Icons.check_circle,
color: isError ? AppColors.red : AppColors.green,
size: 18),
Icon(
isError ? Icons.error_outline : Icons.check_circle,
color: isError ? AppColors.red : AppColors.green,
size: 18,
),
const SizedBox(width: 8),
Text(message),
],