Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@
"secureTransport": "Use v3 transport",
"secureTransportDescription": "Native proxy sessions are always encrypted. V3 uses separate direction keys without an extra handshake round trip; upgrade every node first. Off uses the encrypted legacy protocol.",
"tunDnsServer": "TUN DNS resolver IP",
"tunDnsServerDescription": "Default: 8.8.8.8. The proxy server must be able to reach this resolver. Set your private resolver here for internal domains. Reconnect to apply.",
"tunDnsServerDescription": "Desktop DNS uses AliDNS and Tencent DNS over the physical interface, then adapter DNS and this fallback address. Mobile DNS uses this address through the proxy. Existing settings are preserved. Reconnect to apply.",
"invalidTunDnsServer": "Enter a valid IPv4 or IPv6 DNS resolver address.",
"tunFakeIp": "TUN Fake-IP DNS",
"tunFakeIpDescription": "Off by default: DNS returns real addresses through the proxy. Enable to use virtual addresses for domain routing. Takes effect after reconnecting."
Expand Down
2 changes: 1 addition & 1 deletion lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@
"secureTransport": "使用 v3 传输协议",
"secureTransportDescription": "原生代理连接默认已全程加密。v3 使用独立方向密钥且不增加握手往返;启用前需升级全部节点。关闭时使用兼容旧节点的加密协议。",
"tunDnsServer": "TUN DNS 解析器 IP",
"tunDnsServerDescription": "默认 8.8.8.8,需可从代理服务器访问。解析内网域名时请填写相应的私有 DNS 地址。重新连接后生效。",
"tunDnsServerDescription": "桌面端优先通过物理网卡查询阿里云、腾讯云 DNS,再回退到网卡 DNS 和此地址;移动端通过代理访问此地址。已有配置保留,重新连接后生效。",
"invalidTunDnsServer": "请输入有效的 IPv4 或 IPv6 DNS 解析器地址。",
"tunFakeIp": "TUN Fake-IP DNS",
"tunFakeIpDescription": "默认关闭:通过代理查询真实 IP。开启后使用虚拟地址保留域名路由信息。重新连接后生效。"
Expand Down
2 changes: 1 addition & 1 deletion lib/src/ffi/proxy_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ class ProxyService {
bool udpDirectFallback = true,
bool tunEnabled = false,
bool tunFakeIp = false,
String tunDnsServer = '8.8.8.8',
String tunDnsServer = '223.5.5.5',
List<String> tunBypassProcesses = const [],
bool reverseGeo = false,
bool secureTransport = false,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/models/node_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class NodeInfo {
bool udpDirectFallback = true,
bool tunEnabled = false,
bool tunFakeIp = false,
String tunDnsServer = '8.8.8.8',
String tunDnsServer = '223.5.5.5',
List<String> tunBypassProcesses = const [],
AndroidVpnRoutingMode androidVpnRoutingMode = AndroidVpnRoutingMode.all,
List<String> androidVpnPackages = const [],
Expand Down
4 changes: 2 additions & 2 deletions lib/src/models/proxy_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class ProxyConfigModel {
this.udpDirectFallback = true,
this.tunEnabled = false,
this.tunFakeIp = false,
this.tunDnsServer = '8.8.8.8',
this.tunDnsServer = '223.5.5.5',
List<String> tunBypassProcesses = const [],
this.androidVpnRoutingMode = AndroidVpnRoutingMode.all,
List<String> androidVpnPackages = const [],
Expand Down Expand Up @@ -133,7 +133,7 @@ class ProxyConfigModel {
// routes after an application upgrade.
tunEnabled: _parseBool(json['tunEnabled'], false),
tunFakeIp: _parseBool(json['tunFakeIp'], false),
tunDnsServer: (json['tunDnsServer'] ?? '8.8.8.8').toString().trim(),
tunDnsServer: (json['tunDnsServer'] ?? '223.5.5.5').toString().trim(),
tunBypassProcesses: _parseStringList(json['tunBypassProcesses']),
androidVpnRoutingMode: AndroidVpnRoutingMode.fromWireName(
json['androidVpnRoutingMode'],
Expand Down
9 changes: 4 additions & 5 deletions lib/src/providers/proxy_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,10 @@ class ProxyState extends ChangeNotifier {
);
}

/// Persist and, when TUN is active, immediately apply process exclusions.
/// Persist and apply exclusions to the running listener and TUN session.
Future<bool> updateTunBypassProcesses(List<String> processes) async {
if (_isDisposed) return false;
if (_isRunning && _isTunRunning) {
if (_isRunning) {
final result = _service.setTunBypassProcesses(processes);
if (result != ProxyResult.ok) {
_lastError = _service.lastError ?? ProxyResult.message(result);
Expand Down Expand Up @@ -707,9 +707,8 @@ class ProxyState extends ChangeNotifier {
void setMinLogLevel(int level) {
_minLogLevel = LogLevel.normalize(level);
_filteredLogs = null;
// Also raise the native floor so the levels the UI filters out are never
// shipped across the FFI boundary in the first place.
_service.setLogLevel(_minLogLevel);
// Disk diagnostics retain INFO even while the view only shows errors.
_service.setLogLevel(_minLogLevel.clamp(0, 2));
if (!_isDisposed) logRevision.value++;
_safeNotifyListeners();
}
Expand Down
38 changes: 11 additions & 27 deletions lib/src/widgets/tun_process_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ class _TunProcessDialogState extends State<TunProcessDialog> {
setState(() {
_processes = sorted;
_selfProcess = selfProcess;
_removeInheritedSelections(forest);
if (!_initializedExpansion) {
_expanded.addAll(expandableTunProcessNames(forest));
_initializedExpansion = true;
Expand All @@ -115,23 +114,6 @@ class _TunProcessDialogState extends State<TunProcessDialog> {
});
}

void _removeInheritedSelections(List<TunProcessTreeNode> forest) {
void visit(TunProcessTreeNode node, bool ancestorSelected) {
if (ancestorSelected) {
_selected.remove(node.process.name);
}
final selectedHere =
ancestorSelected || _selected.contains(node.process.name);
for (final child in node.children) {
visit(child, selectedHere);
}
}

for (final root in forest) {
visit(root, false);
}
}

void _expandSelectedPaths(List<TunProcessTreeNode> forest) {
bool visit(TunProcessTreeNode node) {
final containsSelection =
Expand Down Expand Up @@ -188,11 +170,8 @@ class _TunProcessDialogState extends State<TunProcessDialog> {
setState(() {
if (selected) {
_selected.add(node.process.name);
// Parent matching already includes every descendant. Removing redundant
// explicit names keeps the persisted policy stable across PID changes.
for (final descendant in node.descendants) {
_selected.remove(descendant.process.name);
}
// Explicit rules survive launcher exit/restart and changing PID trees.
// Inheritance only affects the display, never the stored selection.
if (node.children.isNotEmpty) {
_expanded.add(node.process.name);
}
Expand Down Expand Up @@ -425,7 +404,9 @@ class _TunProcessDialogState extends State<TunProcessDialog> {
.withValues(alpha: .38)
: Colors.transparent,
child: InkWell(
onTap: isSelf || inherited ? null : () => _setSelected(node, !selected),
onTap: isSelf
? null
: () => _setSelected(node, !_selected.contains(name)),
child: Padding(
padding: EdgeInsets.only(left: row.depth * 24.0, right: 8),
child: SizedBox(
Expand Down Expand Up @@ -453,10 +434,13 @@ class _TunProcessDialogState extends State<TunProcessDialog> {
),
),
Checkbox(
value: selected,
onChanged: isSelf || inherited
tristate: true,
value: inherited && !_selected.contains(name)
? null
: selected,
onChanged: isSelf
? null
: (value) => _setSelected(node, value ?? false),
: (_) => _setSelected(node, !_selected.contains(name)),
),
const SizedBox(width: 6),
_ProcessIcon(process: process),
Expand Down
2 changes: 1 addition & 1 deletion test/app_language_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ class LanguageTestProxyService extends ProxyService {
bool udpDirectFallback = true,
bool tunEnabled = false,
bool tunFakeIp = false,
String tunDnsServer = '8.8.8.8',
String tunDnsServer = '223.5.5.5',
List<String> tunBypassProcesses = const [],
bool reverseGeo = false,
bool secureTransport = false,
Expand Down
132 changes: 132 additions & 0 deletions test/bypass_selection_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:proxy_ui/l10n/app_language.dart';
import 'package:proxy_ui/src/ffi/proxy_service.dart';
import 'package:proxy_ui/src/models/proxy_config.dart';
import 'package:proxy_ui/src/providers/proxy_provider.dart';
import 'package:proxy_ui/src/services/desktop_log_service.dart';
import 'package:proxy_ui/src/widgets/tun_process_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:toastification/toastification.dart';

void main() {
testWidgets(
'refreshing a launcher tree preserves explicit child exclusions',
(tester) async {
SharedPreferences.setMockInitialValues({});
tester.view.physicalSize = const Size(1200, 1000);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final service = _Service();
final state = ProxyState(
service: service,
desktopLogService: DesktopLogService(enabled: false),
);
await tester.runAsync(() async {
while (!state.isInitialized) {
await Future<void>.delayed(const Duration(milliseconds: 1));
}
});
state.updateConfig(
ProxyConfigModel(tunBypassProcesses: ['wegame', 'leagueclient']),
);
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: state,
child: MaterialApp(
builder: (context, child) => ToastificationWrapper(child: child!),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => ChangeNotifierProvider.value(
value: state,
child: const TunProcessDialog(),
),
),
child: const Text('open'),
),
),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
// Selecting the parent again must also keep explicit child rules.
final parent = find.text('wegame${Platform.isWindows ? '.exe' : ''}');
await tester.tap(parent);
await tester.pump();
await tester.tap(parent);
await tester.pump();
// Previously loading the parent+child snapshot removed leagueclient.
service.parentAlive = false;
await tester.tap(find.byIcon(Icons.refresh));
await tester.pumpAndSettle();
final child = find.ancestor(
of: find.text('leagueclient${Platform.isWindows ? '.exe' : ''}'),
matching: find.byType(InkWell),
);
expect(
tester
.widget<Checkbox>(
find.descendant(of: child, matching: find.byType(Checkbox)),
)
.value,
isTrue,
);
await tester.tap(find.byType(FilledButton));
await tester.pumpAndSettle();
expect(state.config.tunBypassProcesses, ['leagueclient', 'wegame']);
final saved = (await SharedPreferences.getInstance()).getString(
'proxy_config',
);
expect(saved, contains('leagueclient'));
toastification.dismissAll(delayForAnimation: false);
await tester.pumpAndSettle();
await tester.pumpWidget(const SizedBox.shrink());
state.dispose();
},
);
}

class _Service extends ProxyService {
bool parentAlive = true;
@override
void initLogging() {}
@override
bool restoreOrphanedSystemProxy() => false;
@override
bool get isRunning => false;
@override
bool get isTunRunning => false;
@override
String? get tunSelfProcess => 'cipherrelay';
@override
List<TunProcessInfo> listTunProcessDetails() => [
if (parentAlive)
const TunProcessInfo(
name: 'wegame',
pids: [100],
executablePaths: [],
instances: [TunProcessInstance(pid: 100)],
),
TunProcessInfo(
name: 'leagueclient',
pids: const [101],
executablePaths: const [],
instances: [
TunProcessInstance(pid: 101, parentPid: parentAlive ? 100 : null),
],
),
];
@override
void dispose() {}
}
47 changes: 46 additions & 1 deletion test/log_notification_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:proxy_ui/src/ffi/proxy_service.dart';
import 'package:proxy_ui/src/providers/proxy_provider.dart';
Expand All @@ -14,6 +16,43 @@ void main() {
SharedPreferences.setMockInitialValues({});
});

test(
'ERROR view still persists INFO diagnostics and keeps the native floor',
() async {
final directory = await Directory.systemTemp.createTemp(
'proxy-view-log-',
);
final disk = DesktopLogService(
enabled: true,
directoryProvider: () async => directory,
);
final service = _SilentProxyService();
final state = ProxyState(service: service, desktopLogService: disk);
await _waitUntilInitialized(state);
state.setMinLogLevel(4);
expect(service.nativeLevel, 2);
ProxyService.debugEmitLog(
LogEntry(level: 2, message: 'bypassing UDP game.exe via physical NIC'),
);
ProxyService.debugEmitLog(
LogEntry(level: 4, message: 'failed destination example.test:443'),
);
await _settle();
expect(state.filteredLogs.map((entry) => entry.level), [4]);
state.dispose();
await disk.dispose();
final text = await directory
.list()
.where((file) => file is File)
.cast<File>()
.asyncMap((file) => file.readAsString())
.join();
expect(text, contains('bypassing UDP game.exe via physical NIC'));
expect(text, contains('failed destination example.test:443'));
await directory.delete(recursive: true);
},
);

test('log arrivals notify only the log channel, and coalesce', () async {
final state = ProxyState(
service: _SilentProxyService(),
Expand Down Expand Up @@ -123,11 +162,17 @@ Future<void> _settle() =>
/// so log arrivals never trip the connection reconciliation that legitimately
/// does notify listeners.
class _SilentProxyService extends ProxyService {
int? nativeLevel;
@override
void initLogging() {}

@override
void setLogLevel(int level) {}
void setLogLevel(int level) {
nativeLevel = level;
}

@override
bool restoreOrphanedSystemProxy() => false;

@override
bool get isRunning => false;
Expand Down
2 changes: 1 addition & 1 deletion test/proxy_config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ void main() {
}
});
test('TUN resolver persists across imports and copies', () {
expect(ProxyConfigModel.fromJson({}).tunDnsServer, '8.8.8.8');
expect(ProxyConfigModel.fromJson({}).tunDnsServer, '223.5.5.5');
final config = ProxyConfigModel.fromJson({'tunDnsServer': ' 10.20.30.53 '});
expect(config.tunDnsServer, '10.20.30.53');
expect(
Expand Down
4 changes: 2 additions & 2 deletions test/tun_hot_switch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ void main() {
);
expect(await state.start(), isTrue);
expect(service.lastTunFakeIp, isFalse);
expect(service.lastTunDnsServer, '8.8.8.8');
expect(service.lastTunDnsServer, '223.5.5.5');
expect(await state.stop(), isTrue);
state.updateConfig(
state.config.copyWith(tunFakeIp: true, tunDnsServer: '10.20.30.53'),
Expand Down Expand Up @@ -368,7 +368,7 @@ class _FakeProxyService extends ProxyService {
bool udpDirectFallback = true,
bool tunEnabled = false,
bool tunFakeIp = false,
String tunDnsServer = '8.8.8.8',
String tunDnsServer = '223.5.5.5',
List<String> tunBypassProcesses = const [],
bool reverseGeo = false,
bool secureTransport = false,
Expand Down
Loading