diff --git a/README.md b/README.md index 53c82e2..deb5223 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 开源免费抓包工具,支持Windows、Mac、Android、IOS、Linux 全平台系统 -您可以使用它来拦截、检查和重写HTTP(S)流量,ProxyPin基于Flutter开发,UI美观易用。 +您可以使用它来拦截、检查和重写HTTP(S)流量,支持Flutter应用抓包,ProxyPin基于Flutter开发,UI美观易用。 ## 核心特性 @@ -20,9 +20,7 @@ 国内下载地址: https://gitee.com/wanghongenpin/network-proxy-flutter/releases -AppStore下载地址: https://apps.apple.com/app/proxypin/id6450932949 - -iOS国内TF下载地址(有1万名额限制): https://testflight.apple.com/join/gURGH6B4 +iOS AppStore下载地址: https://apps.apple.com/app/proxypin/id6450932949 TG: https://t.me/proxypin_tg diff --git a/README_EN.md b/README_EN.md index ac9a534..e9ce2f4 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,7 +2,7 @@ English | [中文](README.md) ## Open source free packet capture tool,Support Windows、Mac、Android、IOS、Linux Full platform system -You can use it to intercept, inspect & rewrite HTTP(S) traffic, ProxyPin is based on Flutter develop, and the UI is beautiful +You can use it to intercept, inspect & rewrite HTTP(S) traffic, Support capturing Flutter app traffic, ProxyPin is based on Flutter develop, and the UI is beautiful and easy to use. ## Features * Mobile scan code connection: no need to manually configure WiFi proxy, including configuration synchronization. All terminals can scan codes to connect and forward traffic to each other. diff --git a/ios/ProxyPin/PacketTunnelProvider.swift b/ios/ProxyPin/PacketTunnelProvider.swift index 9bfa1d4..875638e 100644 --- a/ios/ProxyPin/PacketTunnelProvider.swift +++ b/ios/ProxyPin/PacketTunnelProvider.swift @@ -24,7 +24,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let proxyPort = conf["proxyPort"] as! Int let ipProxy = conf["ipProxy"] as! Bool? ?? false - let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: host) // let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: host) NSLog(conf.debugDescription) //http代理 @@ -39,7 +39,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { networkSettings.mtu = 1480 let ipv4Settings = NEIPv4Settings(addresses: ["10.0.0.2"], subnetMasks: ["255.255.255.255"]) - + if (ipProxy){ ipv4Settings.includedRoutes = [NEIPv4Route.default()] // ipv4Settings.excludedRoutes = [ diff --git a/ios/ProxyPin/vpn/Connection.swift b/ios/ProxyPin/vpn/Connection.swift index 33c1ca1..a982005 100644 --- a/ios/ProxyPin/vpn/Connection.swift +++ b/ios/ProxyPin/vpn/Connection.swift @@ -37,7 +37,7 @@ class Connection{ self.connectionCloser = connectionCloser } - //发送缓冲区,用于存储要从vpn客户端发送到目标主机的数据 + //发送缓冲区,用于存储要从vpn客户端发送到目标主机的数据 var sendBuffer = Data() var hasReceivedLastSegment = false @@ -73,30 +73,35 @@ class Connection{ } func addSendData(data: Data) { - QueueFactory.instance.getQueue().sync { -// sendBuffer.append(data) -// if (channel?.state != .ready) { -// return -// } - self.sendToDestination(data: data) + + QueueFactory.instance.getQueue().async(flags: .barrier) { + self.sendBuffer.append(data) + + if (self.nwProtocol == .TCP && self.channel?.state != .ready) { + return + } + self.sendToDestination() } } //发送到目标服务器的数据 - func sendToDestination(data: Data) { -// QueueFactory.instance.getQueue().sync { - // os_log("Sending data to destination key %{public}@", log: OSLog.default, type: .debug, self.description) - //发送数据, 数据包大拆分 - - - self.channel?.send(content: data, completion: .contentProcessed { error in + func sendToDestination() { + QueueFactory.instance.getQueue().async(flags: .barrier) { + os_log("Sending data to destination key %{public}@", log: OSLog.default, type: .debug, self.description) + if (self.sendBuffer.count == 0) { + return + } + + let data = self.sendBuffer + self.sendBuffer.removeAll() + + self.channel?.send(content: data, completion: .contentProcessed({ error in if let error = error { - os_log("Error sending data to destination %{public}@: %{public}@", log: OSLog.default, type: .error, self.description, error.localizedDescription) - return + os_log("Failed to send data to destination key %{public}@ error: %{public}@", log: OSLog.default, type: .error, self.description, error.localizedDescription) + self.closeConnection() } - }) - -// } + })) + } } var description: String { diff --git a/ios/ProxyPin/vpn/ConnectionHandler.swift b/ios/ProxyPin/vpn/ConnectionHandler.swift index a97f5ee..ebd9dda 100644 --- a/ios/ProxyPin/vpn/ConnectionHandler.swift +++ b/ios/ProxyPin/vpn/ConnectionHandler.swift @@ -127,7 +127,7 @@ class ConnectionHandler { // os_log("Handling TCP packet for %{public}@ flags:%d", log: OSLog.default, type: .default, Connection.getConnectionKey(nwProtocol: .TCP, destIp: destinationIP, destPort: destinationPort, sourceIp: sourceIP, sourcePort: sourcePort), tcpHeader.flags) if (tcpHeader.isSYN()) { - os_log("Received SYN packet %{public}@ seq:%u", log: OSLog.default, type: .default, Connection.getConnectionKey(nwProtocol: .TCP, destIp: destinationIP, destPort: destinationPort, sourceIp: sourceIP, sourcePort: sourcePort), tcpHeader.sequenceNumber) +// os_log("Received SYN packet %{public}@ seq:%u", log: OSLog.default, type: .default, Connection.getConnectionKey(nwProtocol: .TCP, destIp: destinationIP, destPort: destinationPort, sourceIp: sourceIP, sourcePort: sourcePort), tcpHeader.sequenceNumber) // 3-way handshake + create new session replySynAck(ipHeader: ipHeader, tcpHeader: tcpHeader) } else if (tcpHeader.isACK()) { diff --git a/ios/ProxyPin/vpn/ConnectionManager.swift b/ios/ProxyPin/vpn/ConnectionManager.swift index 35e2830..09cf094 100644 --- a/ios/ProxyPin/vpn/ConnectionManager.swift +++ b/ios/ProxyPin/vpn/ConnectionManager.swift @@ -18,9 +18,7 @@ class ConnectionManager : CloseableConnection{ public var proxyAddress: NWEndpoint? - private let defaultPorts: [UInt16] = [80, 443] - -// private init() {} + private let defaultPorts: [UInt16] = [80, 8080, 8888, 443] func getConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection? { diff --git a/ios/ProxyPin/vpn/socket/SocketIOService.swift b/ios/ProxyPin/vpn/socket/SocketIOService.swift index 71703e3..1a3c3e0 100644 --- a/ios/ProxyPin/vpn/socket/SocketIOService.swift +++ b/ios/ProxyPin/vpn/socket/SocketIOService.swift @@ -10,7 +10,7 @@ import NetworkExtension import os.log class SocketIOService { - //private static let maxReceiveBufferSize = 16384 +// private static let maxReceiveBufferSize = 16384 private static let maxReceiveBufferSize = 1024 private let queue: DispatchQueue = DispatchQueue(label: "ProxyPin.SocketIOService", attributes: .concurrent) @@ -40,7 +40,7 @@ class SocketIOService { connection.isConnected = true os_log("Connected to %{public}@ on receiveMessage", log: OSLog.default, type: .default, connection.description) //接受远程服务器的数据 -// connection.sendToDestination() + connection.sendToDestination() self.receiveMessage(connection: connection) case .cancelled: connection.isConnected = false @@ -90,8 +90,8 @@ class SocketIOService { return } - channel.receive(minimumIncompleteLength: 1, maximumLength: Self.maxReceiveBufferSize) { (data, context, isComplete, error) in -// os_log("Received TCP data packet %{public}@ length %d", log: OSLog.default, type: .default, connection.description, data?.count ?? 0) + channel.receive(minimumIncompleteLength: 0, maximumLength: Self.maxReceiveBufferSize) { (data, context, isComplete, error) in +// os_log("Received TCP data packet %{public}@ length %d", log: OSLog.default, type: .default, connection.description, data?.count ?? 0) if let error = error { os_log("Failed to read from TCP socket: %@", log: OSLog.default, type: .error, error as CVarArg) self.sendFin(connection: connection) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index f6335cb..cafdd4e 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -20,8 +20,8 @@ import NetworkExtension result(Bool(VpnManager.shared.isRunning())) } else if ("restartVpn" == call.method){ let arguments = call.arguments as? Dictionary -// VpnManager.shared.disconnect() - VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool) +// VpnManager.shared.disconnect() + VpnManager.shared.restartConnect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool) } else { let arguments = call.arguments as? Dictionary VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool) diff --git a/ios/Runner/VpnManager.swift b/ios/Runner/VpnManager.swift index 872ef08..4ba427c 100755 --- a/ios/Runner/VpnManager.swift +++ b/ios/Runner/VpnManager.swift @@ -156,6 +156,19 @@ extension VpnManager{ } } } + + func restartConnect(host: String?, port: Int?, ipProxy: Bool? = false) { + self.proxyHost = host ?? self.proxyHost + self.proxyPort = port ?? self.proxyPort + self.ipProxy = ipProxy ?? false + + if (activeVPN != nil) { + activeVPN?.connection.stopVPNTunnel() + activeVPN = nil + } + + self.connect(host: host, port: port, ipProxy: ipProxy) + } func disconnect() { if (activeVPN != nil) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4b73541..fded419 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -237,6 +237,7 @@ "notConnected": "Not connected", "disconnect": "Disconnect", "ipLayerProxy": "IP Layer Proxy(Beta)", + "ipLayerProxyDesc": "IP layer proxy can capture Flutter app requests, currently not very stable", "inputAddress": "Input Address", "syncConfig": "Sync configuration", "pullConfigFail": "Failed to pull configuration, please check the network connection", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1837b43..568ff7b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -238,6 +238,7 @@ "inputAddress": "输入地址", "disconnect": "断开连接", "ipLayerProxy": "IP层代理(Beta)", + "ipLayerProxyDesc": "IP层代理可抓取Flutter应用请求,目前不是很稳定", "syncConfig": "同步配置", "pullConfigFail": "拉取配置失败, 请检查网络连接", "sync": "同步", diff --git a/lib/network/util/byte_buf.dart b/lib/network/util/byte_buf.dart index 87761b8..a65ad64 100644 --- a/lib/network/util/byte_buf.dart +++ b/lib/network/util/byte_buf.dart @@ -17,95 +17,111 @@ import 'dart:typed_data'; ///类似于netty ByteBuf + class ByteBuf { - final BytesBuilder _buffer = BytesBuilder(); + late Uint8List _buffer; int readerIndex = 0; - - Uint8List get bytes => _buffer.toBytes(); - - int get length => _buffer.length; + int writerIndex = 0; ByteBuf([List? bytes]) { - if (bytes != null) _buffer.add(bytes); + if (bytes != null) { + _buffer = Uint8List.fromList(bytes); + writerIndex = bytes.length; + } else { + _buffer = Uint8List(256); // Initial buffer size + } } - ///添加 + int get length => writerIndex; + + Uint8List get bytes => Uint8List.sublistView(_buffer, 0, writerIndex); + void add(List bytes) { - _buffer.add(bytes); + _ensureCapacity(writerIndex + bytes.length); + _buffer.setRange(writerIndex, writerIndex + bytes.length, bytes); + writerIndex += bytes.length; } - ///清空 - clear() { - _buffer.clear(); + void clear() { readerIndex = 0; + writerIndex = 0; } - ///释放 - clearRead() { - var takeBytes = _buffer.takeBytes(); - _buffer.add(Uint8List.sublistView(takeBytes, readerIndex, takeBytes.length)); - readerIndex = 0; + void clearRead() { + if (readerIndex > 0) { + _buffer.setRange(0, writerIndex - readerIndex, _buffer, readerIndex); + writerIndex -= readerIndex; + readerIndex = 0; + } } - bool isReadable() => readerIndex < _buffer.length; + bool isReadable() => readerIndex < writerIndex; - ///可读字节数 - int readableBytes() { - return _buffer.length - readerIndex; - } + int readableBytes() => writerIndex - readerIndex; - ///读取所有可用字节 - Uint8List readAvailableBytes() { - return readBytes(readableBytes()); - } + Uint8List readAvailableBytes() => readBytes(readableBytes()); - ///读取字节 Uint8List readBytes(int length) { - Uint8List list = bytes.sublist(readerIndex, readerIndex + length); + if (readerIndex + length > writerIndex) { + throw Exception("Not enough readable bytes"); + } + Uint8List result = Uint8List.sublistView(_buffer, readerIndex, readerIndex + length); readerIndex += length; - return list; + return result; } - ///跳过 - skipBytes(int length) { + void skipBytes(int length) { + if (readerIndex + length > writerIndex) { + throw Exception("Not enough readable bytes"); + } readerIndex += length; } - ///读取字节 - int read() { - return bytes[readerIndex++]; - } + int read() => _buffer[readerIndex++]; - ///读取字节 - int readByte() { - return bytes[readerIndex++]; - } + int readByte() => _buffer[readerIndex++]; int readShort() { - int value = bytes[readerIndex++] << 8 | bytes[readerIndex++]; + int value = (_buffer[readerIndex] << 8) | _buffer[readerIndex + 1]; + readerIndex += 2; return value; } int readInt() { - int value = - bytes[readerIndex++] << 24 | bytes[readerIndex++] << 16 | bytes[readerIndex++] << 8 | bytes[readerIndex++]; + int value = (_buffer[readerIndex] << 24) | + (_buffer[readerIndex + 1] << 16) | + (_buffer[readerIndex + 2] << 8) | + _buffer[readerIndex + 3]; + readerIndex += 4; return value; } - int get(int index) { - return bytes[index]; - } + int get(int index) => _buffer[index]; void truncate(int len) { - if (len > readableBytes()) throw Exception("insufficient data"); - var takeBytes = _buffer.takeBytes(); - _buffer.add(takeBytes.sublist(0, readerIndex + len)); + if (len > readableBytes()) { + throw Exception("Insufficient data"); + } + writerIndex = readerIndex + len; } ByteBuf dup() { ByteBuf buf = ByteBuf(); - buf.add(bytes); + buf._buffer = Uint8List.fromList(_buffer); buf.readerIndex = readerIndex; + buf.writerIndex = writerIndex; return buf; } -} + + void _ensureCapacity(int required) { + if (_buffer.length < required) { + int newSize = _buffer.length * 2; + while (newSize < required) { + newSize *= 2; + } + Uint8List newBuffer = Uint8List(newSize); + newBuffer.setRange(0, writerIndex, _buffer); + _buffer = newBuffer; + } + } +} \ No newline at end of file diff --git a/lib/ui/configuration.dart b/lib/ui/configuration.dart index 5103c40..6e9aaa0 100644 --- a/lib/ui/configuration.dart +++ b/lib/ui/configuration.dart @@ -60,7 +60,7 @@ class AppConfiguration { Locale? _language; //是否显示更新内容公告 - bool upgradeNoticeV13 = true; + bool upgradeNoticeV14 = true; /// 是否启用画中画 ValueNotifier pipEnabled = ValueNotifier(true); @@ -174,7 +174,7 @@ class AppConfiguration { _theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true); _theme.color = config['themeColor'] ?? "Blue"; - upgradeNoticeV13 = config['upgradeNoticeV13'] ?? true; + upgradeNoticeV14 = config['upgradeNoticeV14'] ?? true; _language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']); pipEnabled.value = config['pipEnabled'] ?? true; pipIcon.value = config['pipIcon'] ?? false; @@ -218,7 +218,7 @@ class AppConfiguration { 'mode': _theme.mode.name, 'themeColor': _theme.color, 'useMaterial3': _theme.useMaterial3, - 'upgradeNoticeV13': upgradeNoticeV13, + 'upgradeNoticeV14': upgradeNoticeV14, "language": _language?.languageCode, "headerExpanded": headerExpanded, diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 35df03c..189ff5c 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -82,7 +82,7 @@ class _DesktopHomePagePageState extends State implements EventL proxyServer.addListener(this); panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer); - if (widget.appConfiguration.upgradeNoticeV13) { + if (widget.appConfiguration.upgradeNoticeV14) { WidgetsBinding.instance.addPostFrameCallback((_) { showUpgradeNotice(); }); @@ -136,7 +136,7 @@ class _DesktopHomePagePageState extends State implements EventL actions: [ TextButton( onPressed: () { - widget.appConfiguration.upgradeNoticeV13 = false; + widget.appConfiguration.upgradeNoticeV14 = false; widget.appConfiguration.flushConfig(); Navigator.pop(context); }, @@ -149,10 +149,10 @@ class _DesktopHomePagePageState extends State implements EventL isCN ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' '点击HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' - '1. 支持多种主题颜色选择;\n' - '2. 外部代理支持身份验证;\n' - '3. 双击列表tab滚动到顶部;\n' - '4. 修复部分p12证书导入失败的问题;\n' + '1. 手机端增加底部导航,可在设置中切换;\n' + '2. 增加远程设备管理,可快速连接设备;\n' + '3. iOS支持抓取Flutter应用,需要通过设备管理连接到电脑开启IP层代理(Beta);\n' + '4. 工具箱支持Unicode编码;\n' '5. 修复Transfer-Encoding有空格解析错误问题;\n' '6. 脚本增加rawBody原始字节参数, body支持字节数组修改;\n' '7. 修复脚本消息体编码错误导致错误响应;\n' @@ -160,10 +160,10 @@ class _DesktopHomePagePageState extends State implements EventL '9. 修复Websocket Response不展示;\n' : 'Tips:By default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n' 'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\n\n' - '1. Support multiple theme colors;\n' - '2. External proxy support authentication;\n' - '3. Double-click the list tab to scroll to the top;\n' - '4. Fix the issue of partial p12 certificate import failure;\n' + '1. Mobile: Add bottom navigation bar,which can be switched in settings;\n' + '2. Support remote device management to quickly connect to devices;\n' + '3. IOS supports capturing Flutter applications, You need to connect to the computer through device management to enable IP layer proxy (Beta);\n' + '4. Toolbox supports Unicode encode;\n' '5. Fix header Transfer-Encoding with spaces;\n' '6. The script add rawBody raw byte parameter, body supports byte array modification;\n' '7. Fix script message body encoding error causing incorrect response;\n' diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 19747d0..64db059 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -109,7 +109,7 @@ class MobileHomeState extends State implements EventListener, Li proxyServer.addListener(this); proxyServer.start(); - if (widget.appConfiguration.upgradeNoticeV13) { + if (widget.appConfiguration.upgradeNoticeV14) { WidgetsBinding.instance.addPostFrameCallback((_) { showUpgradeNotice(); }); @@ -250,8 +250,8 @@ class MobileHomeState extends State implements EventListener, Li ? '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n\n' '1. 手机端增加底部导航,可在设置中切换;\n' '2. 增加远程设备管理,可快速连接设备;\n' - '3. 双击列表tab滚动到顶部;\n' - '4. 修复部分p12证书导入失败的问题;\n' + '3. iOS支持抓取Flutter应用,需要通过设备管理连接到电脑开启IP层代理(Beta);\n' + '4. 工具箱支持Unicode编码;\n' '5. 脚本增加rawBody原始字节参数, body支持字节数组修改;\n' '6. 修复脚本消息体编码错误导致错误响应;\n' '7. 修复扫码链接多个IP优先级问题;\n' @@ -262,8 +262,8 @@ class MobileHomeState extends State implements EventListener, Li 'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\n\n' '1. Mobile: Add bottom navigation bar,which can be switched in settings;\n' '2. Support remote device management to quickly connect to devices;\n' - '3. Double-click the list tab to scroll to the top;\n' - '4. Fix the issue of partial p12 certificate import failure;\n' + '3. IOS supports capturing Flutter applications, You need to connect to the computer through device management to enable IP layer proxy (Beta);\n' + '4. Toolbox supports Unicode encode;\n' '5. The script add rawBody raw byte parameter, body supports byte array modification;\n' '6. Fix script message body encoding error causing incorrect response;\n' '7. Fix the issue of scanning QR code to connect to multiple IP priorities;\n' @@ -271,8 +271,8 @@ class MobileHomeState extends State implements EventListener, Li '9. Fix export HAR serverIPAddress incorrect;\n' '10. Fix Websocket Response not displayed;\n' ''; - showAlertDialog(isCN ? '更新内容V1.1.3' : "Update content V1.1.3", content, () { - widget.appConfiguration.upgradeNoticeV13 = false; + showAlertDialog(isCN ? '更新内容V1.1.4' : "Update content V1.1.4", content, () { + widget.appConfiguration.upgradeNoticeV14 = false; widget.appConfiguration.flushConfig(); }); } diff --git a/lib/ui/mobile/widgets/remote_device.dart b/lib/ui/mobile/widgets/remote_device.dart index 5665042..36dd0e1 100644 --- a/lib/ui/mobile/widgets/remote_device.dart +++ b/lib/ui/mobile/widgets/remote_device.dart @@ -60,13 +60,7 @@ class RemoteModel { factory RemoteModel.fromJson(Map json) { return RemoteModel( - connect: json['connect'], - host: json['host'], - port: json['port'], - os: json['os'], - hostname: json['hostname'], - ipProxy: json['ipProxy'], - ); + connect: json['connect'], host: json['host'], port: json['port'], os: json['os'], hostname: json['hostname']); } RemoteModel copyWith({ @@ -95,14 +89,7 @@ class RemoteModel { } Map toJson() { - return { - 'connect': connect, - 'host': host, - 'port': port, - 'os': os, - 'hostname': hostname, - 'ipProxy': ipProxy, - }; + return {'connect': connect, 'host': host, 'port': port, 'os': os, 'hostname': hostname}; } } @@ -243,32 +230,38 @@ class _RemoteDevicePageState extends State { child: Column( children: [ const Icon(Icons.check_circle_outline_outlined, size: 55, color: Colors.green), - Row( - children: [ - if (Platform.isIOS) Expanded(child: ListTile(title: Text(localizations.ipLayerProxy))), - const SizedBox(height: 6), - SwitchWidget( - value: widget.remoteDevice.value.ipProxy ?? false, - scale: 0.85, - onChanged: (val) async { - widget.remoteDevice.value = widget.remoteDevice.value.copyWith(ipProxy: val); - SharedPreferences.getInstance().then((prefs) { - var remoteDeviceList = getRemoteDeviceList(prefs); - remoteDeviceList.removeWhere((it) => it.equals(widget.remoteDevice.value)); - remoteDeviceList.insert(0, widget.remoteDevice.value); + const SizedBox(height: 6), + if (Platform.isIOS) + Row( + children: [ + Expanded( + child: ListTile( + title: Text(localizations.ipLayerProxy), subtitle: Text(localizations.ipLayerProxyDesc))), + SwitchWidget( + value: widget.remoteDevice.value.ipProxy ?? false, + scale: 0.85, + onChanged: (val) async { + widget.remoteDevice.value = widget.remoteDevice.value.copyWith(ipProxy: val); + SharedPreferences.getInstance().then((prefs) { + var remoteDeviceList = getRemoteDeviceList(prefs); + remoteDeviceList.removeWhere((it) => it.equals(widget.remoteDevice.value)); + remoteDeviceList.insert(0, widget.remoteDevice.value); - setRemoteDeviceList(prefs, remoteDeviceList); - }); + setRemoteDeviceList(prefs, remoteDeviceList); + }); - if ((await Vpn.isRunning())) { - print('重启VPN'); - Vpn.restartVpn(widget.remoteDevice.value.host!, widget.remoteDevice.value.port!, - widget.proxyServer.configuration, - ipProxy: val); - } - }), - ], - ), + if ((await Vpn.isRunning())) { + Vpn.stopVpn(); + Future.delayed(const Duration(milliseconds: 1500), () { + Vpn.startVpn(widget.remoteDevice.value.host!, widget.remoteDevice.value.port!, + widget.proxyServer.configuration, + ipProxy: val); + }); + } + }), + ], + ), + const SizedBox(height: 6), Text('${localizations.connected}:${widget.remoteDevice.value.hostname}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 6), @@ -524,7 +517,7 @@ class ConfigSyncState extends State { title: Text(localizations.syncConfig, style: const TextStyle(fontSize: 16)), content: Wrap(children: [ SwitchWidget( - title: "${localizations.sync}${localizations.domainWhitelist}", + title: "${localizations.sync} ${localizations.domainWhitelist}", value: syncWhiteList, onChanged: (val) { setState(() { @@ -533,7 +526,7 @@ class ConfigSyncState extends State { }), const SizedBox(height: 5), SwitchWidget( - title: "${localizations.sync}${localizations.domainBlacklist}", + title: "${localizations.sync} ${localizations.domainBlacklist}", value: syncBlackList, onChanged: (val) { setState(() { @@ -542,7 +535,7 @@ class ConfigSyncState extends State { }), const SizedBox(height: 5), SwitchWidget( - title: "${localizations.sync}${localizations.requestRewrite}", + title: "${localizations.sync} ${localizations.requestRewrite}", value: syncRewrite, onChanged: (val) { setState(() { @@ -551,7 +544,7 @@ class ConfigSyncState extends State { }), const SizedBox(height: 5), SwitchWidget( - title: "${localizations.sync}${localizations.script}", + title: "${localizations.sync} ${localizations.script}", value: syncScript, onChanged: (val) { setState(() { @@ -566,7 +559,7 @@ class ConfigSyncState extends State { Navigator.pop(context); }), TextButton( - child: Text('${localizations.start}${localizations.sync}'), + child: Text('${localizations.start} ${localizations.sync}'), onPressed: () async { if (syncWhiteList) { HostFilter.whitelist.load(widget.config['whitelist']);