Fix windows tun issues

Optimize android get system dns

Optimize more details
This commit is contained in:
chen08209
2025-06-12 10:47:02 +08:00
parent a06e813249
commit 1477f9bd9c
20 changed files with 166 additions and 122 deletions

View File

@@ -75,7 +75,7 @@ jobs:
with: with:
channel: 'stable' channel: 'stable'
cache: true cache: true
flutter-version: 3.29.3 # flutter-version: 3.29.3
- name: Get Flutter Dependency - name: Get Flutter Dependency
run: flutter pub get run: flutter pub get

View File

@@ -104,7 +104,7 @@ fun ConnectivityManager.resolveDns(network: Network?): List<String> {
fun InetAddress.asSocketAddressText(port: Int): String { fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) { return when (this) {
is Inet6Address -> is Inet6Address ->
"[${numericToTextFormat(this.address)}]:$port" "[${numericToTextFormat(this)}]:$port"
is Inet4Address -> is Inet4Address ->
"${this.hostAddress}:$port" "${this.hostAddress}:$port"
@@ -141,7 +141,8 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
} }
} }
private fun numericToTextFormat(src: ByteArray): String { private fun numericToTextFormat(address: Inet6Address): String {
val src = address.address
val sb = StringBuilder(39) val sb = StringBuilder(39)
for (i in 0 until 8) { for (i in 0 until 8) {
sb.append( sb.append(
@@ -154,6 +155,10 @@ private fun numericToTextFormat(src: ByteArray): String {
sb.append(":") sb.append(":")
} }
} }
if (address.scopeId > 0) {
sb.append("%")
sb.append(address.scopeId)
}
return sb.toString() return sb.toString()
} }

View File

@@ -100,6 +100,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
fun handleStart(options: VpnOptions): Boolean { fun handleStart(options: VpnOptions): Boolean {
onUpdateNetwork();
if (options.enable != this.options?.enable) { if (options.enable != this.options?.enable) {
this.flClashService = null this.flClashService = null
} }

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@@ -90,37 +91,37 @@ class Request {
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
}; };
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) { var failureCount = 0;
try { final futures = _ipInfoSources.entries.map((source) async {
final response = await Dio() final Completer<Result<IpInfo?>> completer = Completer();
.get<Map<String, dynamic>>( final future = Dio().get<Map<String, dynamic>>(
source.key, source.key,
cancelToken: cancelToken, cancelToken: cancelToken,
options: Options( options: Options(
responseType: ResponseType.json, responseType: ResponseType.json,
), ),
) );
.timeout( future.then((res) {
Duration( if (res.statusCode == HttpStatus.ok && res.data != null) {
seconds: 30, completer.complete(Result.success(source.value(res.data!)));
), } else {
); failureCount++;
if (response.statusCode != 200 || response.data == null) { if (failureCount == _ipInfoSources.length) {
continue; completer.complete(Result.success(null));
}
} }
if (response.data == null) { }).catchError((e) {
continue; failureCount++;
if (e == DioExceptionType.cancel) {
completer.complete(Result.error("cancelled"));
} }
return source.value(response.data!); });
} catch (e) { return completer.future;
commonPrint.log("checkIp error ===> $e"); });
if (e is DioException && e.type == DioExceptionType.cancel) { final res = await Future.any(futures);
throw "cancelled"; cancelToken?.cancel();
} return res;
}
}
return null;
} }
Future<bool> pingHelper() async { Future<bool> pingHelper() async {

View File

@@ -62,6 +62,7 @@ Future<void> _service(List<String> flags) async {
vpn?.addListener( vpn?.addListener(
_VpnListenerWithService( _VpnListenerWithService(
onDnsChanged: (String dns) { onDnsChanged: (String dns) {
print("handle dns $dns");
clashLibHandler.updateDns(dns); clashLibHandler.updateDns(dns);
}, },
), ),

View File

@@ -91,7 +91,7 @@ class MessageManagerState extends State<MessageManager> {
key: Key(messages.last.id), key: Key(messages.last.id),
builder: (_, constraints) { builder: (_, constraints) {
return Card( return Card(
shape: const RoundedRectangleBorder( shape: const RoundedSuperellipseBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(12.0), Radius.circular(12.0),
), ),

View File

@@ -200,6 +200,24 @@ class Tun with _$Tun {
} }
} }
extension TunExt on Tun {
Tun getRealTun(RouteMode routeMode) {
final mRouteAddress = routeMode == RouteMode.bypassPrivate
? defaultBypassPrivateRouteAddress
: routeAddress;
return switch (system.isDesktop) {
true => copyWith(
autoRoute: true,
routeAddress: [],
),
false => copyWith(
autoRoute: mRouteAddress.isEmpty ? true : false,
routeAddress: mRouteAddress,
),
};
}
}
@freezed @freezed
class FallbackFilter with _$FallbackFilter { class FallbackFilter with _$FallbackFilter {
const factory FallbackFilter({ const factory FallbackFilter({

View File

@@ -94,7 +94,7 @@ final coreStateProvider = AutoDisposeProvider<CoreState>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef CoreStateRef = AutoDisposeProviderRef<CoreState>; typedef CoreStateRef = AutoDisposeProviderRef<CoreState>;
String _$updateParamsHash() => r'79fd7a5a8650fabac3a2ca7ce903c1d9eb363ed2'; String _$updateParamsHash() => r'012df72ab0e769a51c573f4692031506d7b1f1b4';
/// See also [updateParams]. /// See also [updateParams].
@ProviderFor(updateParams) @ProviderFor(updateParams)
@@ -126,7 +126,7 @@ final proxyStateProvider = AutoDisposeProvider<ProxyState>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef ProxyStateRef = AutoDisposeProviderRef<ProxyState>; typedef ProxyStateRef = AutoDisposeProviderRef<ProxyState>;
String _$trayStateHash() => r'39ff84c50ad9c9cc666fa2538fe13ec0d7236b2e'; String _$trayStateHash() => r'61c99bbae2cb7ed69dc9ee0f2149510eb6a87df4';
/// See also [trayState]. /// See also [trayState].
@ProviderFor(trayState) @ProviderFor(trayState)

View File

@@ -105,10 +105,15 @@ CoreState coreState(Ref ref) {
@riverpod @riverpod
UpdateParams updateParams(Ref ref) { UpdateParams updateParams(Ref ref) {
final routeMode = ref.watch(
networkSettingProvider.select(
(state) => state.routeMode,
),
);
return ref.watch( return ref.watch(
patchClashConfigProvider.select( patchClashConfigProvider.select(
(state) => UpdateParams( (state) => UpdateParams(
tun: state.tun, tun: state.tun.getRealTun(routeMode),
allowLan: state.allowLan, allowLan: state.allowLan,
findProcessMode: state.findProcessMode, findProcessMode: state.findProcessMode,
mode: state.mode, mode: state.mode,
@@ -153,9 +158,11 @@ TrayState trayState(Ref ref) {
final appSetting = ref.watch( final appSetting = ref.watch(
appSettingProvider, appSettingProvider,
); );
final groups = ref.watch( final groups = ref
groupsProvider, .watch(
); currentGroupsStateProvider,
)
.value;
final brightness = ref.watch( final brightness = ref.watch(
appBrightnessProvider, appBrightnessProvider,
); );

View File

@@ -315,19 +315,9 @@ class GlobalState {
final profileId = profile.id; final profileId = profile.id;
final configMap = await getProfileConfig(profileId); final configMap = await getProfileConfig(profileId);
final rawConfig = await handleEvaluate(configMap); final rawConfig = await handleEvaluate(configMap);
final routeAddress = final realPatchConfig = patchConfig.copyWith(
config.networkProps.routeMode == RouteMode.bypassPrivate tun: patchConfig.tun.getRealTun(config.networkProps.routeMode),
? defaultBypassPrivateRouteAddress );
: patchConfig.tun.routeAddress;
final realPatchConfig = !system.isDesktop
? patchConfig.copyWith.tun(
autoRoute: routeAddress.isEmpty ? true : false,
routeAddress: routeAddress,
)
: patchConfig.copyWith.tun(
autoRoute: true,
routeAddress: [],
);
rawConfig["external-controller"] = realPatchConfig.externalController.value; rawConfig["external-controller"] = realPatchConfig.externalController.value;
rawConfig["external-ui"] = ""; rawConfig["external-ui"] = "";
rawConfig["interface-name"] = ""; rawConfig["interface-name"] = "";
@@ -411,21 +401,23 @@ class GlobalState {
for (final host in realPatchConfig.hosts.entries) { for (final host in realPatchConfig.hosts.entries) {
rawConfig["hosts"][host.key] = host.value.splitByMultipleSeparators; rawConfig["hosts"][host.key] = host.value.splitByMultipleSeparators;
} }
if (rawConfig["dns"] == null) {
rawConfig["dns"] = {};
}
final isEnableDns = rawConfig["dns"]["enable"] == true;
final overrideDns = globalState.config.overrideDns; final overrideDns = globalState.config.overrideDns;
if (overrideDns) { if (overrideDns || !isEnableDns) {
rawConfig["dns"] = realPatchConfig.dns.toJson(); final dns = switch (!isEnableDns) {
true => realPatchConfig.dns.copyWith(
nameserver: [...realPatchConfig.dns.nameserver, "system://"]),
false => realPatchConfig.dns,
};
rawConfig["dns"] = dns.toJson();
rawConfig["dns"]["nameserver-policy"] = {}; rawConfig["dns"]["nameserver-policy"] = {};
for (final entry in realPatchConfig.dns.nameserverPolicy.entries) { for (final entry in dns.nameserverPolicy.entries) {
rawConfig["dns"]["nameserver-policy"][entry.key] = rawConfig["dns"]["nameserver-policy"][entry.key] =
entry.value.splitByMultipleSeparators; entry.value.splitByMultipleSeparators;
} }
} else {
if (rawConfig["dns"] == null) {
rawConfig["dns"] = {};
}
if (rawConfig["dns"]["enable"] != false) {
rawConfig["dns"]["enable"] = true;
}
} }
var rules = []; var rules = [];
if (rawConfig["rules"] != null) { if (rawConfig["rules"] != null) {
@@ -509,6 +501,9 @@ class DetectionState {
debouncer.call( debouncer.call(
FunctionTag.checkIp, FunctionTag.checkIp,
_checkIp, _checkIp,
duration: Duration(
milliseconds: 1200,
),
); );
} }
@@ -533,36 +528,35 @@ class DetectionState {
cancelToken = null; cancelToken = null;
} }
cancelToken = CancelToken(); cancelToken = CancelToken();
try { state.value = state.value.copyWith(
isTesting: true,
);
final res = await request.checkIp(cancelToken: cancelToken);
if (res.isError) {
state.value = state.value.copyWith( state.value = state.value.copyWith(
isTesting: true, isLoading: true,
ipInfo: null,
); );
final ipInfo = await request.checkIp(cancelToken: cancelToken); return;
state.value = state.value.copyWith(
isTesting: false,
);
if (ipInfo != null) {
state.value = state.value.copyWith(
isLoading: false,
ipInfo: ipInfo,
);
return;
}
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
state.value = state.value.copyWith(
isLoading: false,
ipInfo: null,
);
});
} catch (e) {
if (e.toString() == "cancelled") {
state.value = state.value.copyWith(
isLoading: true,
ipInfo: null,
);
}
} }
final ipInfo = res.data;
state.value = state.value.copyWith(
isTesting: false,
);
if (ipInfo != null) {
state.value = state.value.copyWith(
isLoading: false,
ipInfo: ipInfo,
);
return;
}
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
state.value = state.value.copyWith(
isLoading: false,
ipInfo: null,
);
});
} }
_clearSetTimeoutTimer() { _clearSetTimeoutTimer() {

View File

@@ -135,10 +135,12 @@ class TrafficUsage extends StatelessWidget {
Container( Container(
width: 20, width: 20,
height: 8, height: 8,
decoration: BoxDecoration( decoration: ShapeDecoration(
color: primaryColor, color: primaryColor,
borderRadius: shape: RoundedSuperellipseBorder(
BorderRadius.circular(2), borderRadius:
BorderRadius.circular(3),
),
), ),
), ),
SizedBox( SizedBox(
@@ -161,10 +163,12 @@ class TrafficUsage extends StatelessWidget {
Container( Container(
width: 20, width: 20,
height: 8, height: 8,
decoration: BoxDecoration( decoration: ShapeDecoration(
color: secondaryColor, color: secondaryColor,
borderRadius: shape: RoundedSuperellipseBorder(
BorderRadius.circular(2), borderRadius:
BorderRadius.circular(3),
),
), ),
), ),
SizedBox( SizedBox(

View File

@@ -418,9 +418,11 @@ class _ListHeaderState extends State<ListHeader> {
width: constraints.maxWidth, width: constraints.maxWidth,
alignment: Alignment.center, alignment: Alignment.center,
padding: EdgeInsets.all(6.ap), padding: EdgeInsets.all(6.ap),
decoration: BoxDecoration( decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(12),
),
color: context.colorScheme.secondaryContainer, color: context.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: CommonTargetIcon( child: CommonTargetIcon(

View File

@@ -185,7 +185,7 @@ class CommonCard extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
padding: const WidgetStatePropertyAll(EdgeInsets.zero), padding: const WidgetStatePropertyAll(EdgeInsets.zero),
shape: WidgetStatePropertyAll( shape: WidgetStatePropertyAll(
RoundedRectangleBorder( RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
), ),
), ),

View File

@@ -1,6 +1,7 @@
import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'card.dart'; import 'card.dart';
import 'grid.dart'; import 'grid.dart';

View File

@@ -279,7 +279,7 @@ class CommonPopupMenu extends StatelessWidget {
elevation: 12, elevation: 12,
color: context.colorScheme.surfaceContainer, color: context.colorScheme.surfaceContainer,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder( shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(

View File

@@ -148,9 +148,11 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
final handleSize = Size(32, 4); final handleSize = Size(32, 4);
return Container( return Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: ShapeDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)),
color: backgroundColor, color: backgroundColor,
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)),
),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -161,8 +163,10 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
alignment: Alignment.center, alignment: Alignment.center,
height: handleSize.height, height: handleSize.height,
width: handleSize.width, width: handleSize.width,
decoration: BoxDecoration( decoration: ShapeDecoration(
borderRadius: BorderRadius.circular(handleSize.height / 2), shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(handleSize.height / 2),
),
color: context.colorScheme.onSurfaceVariant, color: context.colorScheme.onSurfaceVariant,
), ),
), ),

View File

@@ -83,7 +83,7 @@ class _SideSheetState extends State<SideSheet> {
final Color shadowColor = widget.shadowColor ?? Colors.transparent; final Color shadowColor = widget.shadowColor ?? Colors.transparent;
final double elevation = widget.elevation ?? 0; final double elevation = widget.elevation ?? 0;
final ShapeBorder shape = widget.shape ?? final ShapeBorder shape = widget.shape ??
RoundedRectangleBorder( RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(0), borderRadius: BorderRadius.circular(0),
); );

View File

@@ -373,8 +373,10 @@ class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
child: Container( child: Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
padding: widget.padding.resolve(Directionality.of(context)), padding: widget.padding.resolve(Directionality.of(context)),
decoration: BoxDecoration( decoration: ShapeDecoration(
borderRadius: const BorderRadius.all(_kCornerRadius), shape: RoundedSuperellipseBorder(
borderRadius: const BorderRadius.all(_kCornerRadius),
),
color: widget.backgroundColor, color: widget.backgroundColor,
), ),
child: AnimatedBuilder( child: AnimatedBuilder(

View File

@@ -85,10 +85,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.13.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -373,10 +373,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -706,10 +706,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -770,10 +770,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.9"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@@ -1024,10 +1024,11 @@ packages:
re_editor: re_editor:
dependency: "direct main" dependency: "direct main"
description: description:
name: re_editor path: "."
sha256: "17e430f0591dd361992ec2dd6f69191c1853fa46e05432e095310a8f82ee820e" ref: main
url: "https://pub.dev" resolved-ref: "7cda330fc33d5ef9e00333048b70ce65a5f5d550"
source: hosted url: "https://github.com/chen08209/re-editor.git"
source: git
version: "0.7.0" version: "0.7.0"
re_highlight: re_highlight:
dependency: "direct main" dependency: "direct main"
@@ -1494,10 +1495,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "15.0.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.85+202506071 version: 0.8.86+2025061501
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
@@ -37,7 +37,10 @@ dependencies:
dio: ^5.8.0+1 dio: ^5.8.0+1
win32: ^5.5.1 win32: ^5.5.1
ffi: ^2.1.2 ffi: ^2.1.2
re_editor: ^0.7.0 re_editor:
git:
url: https://github.com/chen08209/re-editor.git
ref: main
re_highlight: ^0.0.3 re_highlight: ^0.0.3
archive: ^3.6.1 archive: ^3.6.1
lpinyin: ^2.0.3 lpinyin: ^2.0.3