vpn_windows_service.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:dio/dio.dart';
  5. import 'package:nomo/app/constants/errors.dart';
  6. import 'package:shelf/shelf_io.dart' as shelf_io;
  7. import 'package:shelf_web_socket/shelf_web_socket.dart';
  8. import '../../../utils/crypto.dart';
  9. import '../../../utils/log/logger.dart';
  10. import '../../../utils/misc.dart';
  11. import '../../constants/enums.dart';
  12. import 'vpn_exception.dart';
  13. import 'vpn_message.dart';
  14. class _RpcResult {
  15. final bool success;
  16. final String message;
  17. final dynamic data;
  18. _RpcResult(this.success, this.message, this.data);
  19. }
  20. class VpnWindowsService {
  21. static const _tag = 'VpnWindowsService';
  22. static const _rpcPort = 29764;
  23. static const _rpcFileName = 'ixrpc.exe';
  24. static const _rpcHeartbeatTimeout = 5;
  25. static const _rpcBaseUrl = 'http://127.0.0.1:$_rpcPort';
  26. static const String _accessKeySecret =
  27. '{C5E098D8-5487-4F8A-8B23-7F1F839A3A0B}';
  28. final _rpcDio = Dio();
  29. ConnectionState _status = ConnectionState.disconnected;
  30. final _ecStatus = StreamController<(ConnectionState, dynamic)>.broadcast();
  31. bool _isOnline = false;
  32. final _ecOnline = StreamController<bool>.broadcast();
  33. Process? _process;
  34. Timer? _refreshTimer;
  35. Timer? _startupTimer;
  36. String? _logPath;
  37. bool _vpnDebug = false;
  38. bool _isStarted = false;
  39. int _rpcHostPort = 0;
  40. DateTime _lastAliveTime = DateTime.now();
  41. bool _needRecoverConnection = false;
  42. Map<String, dynamic>? _connectionParams;
  43. void _setStatus(ConnectionState value, dynamic data) {
  44. if (_status != value) {
  45. _status = value;
  46. _ecStatus.add((_status, data));
  47. }
  48. }
  49. void _setIsOnline(bool value) {
  50. if (_isOnline != value) {
  51. _isOnline = value;
  52. _ecOnline.add(_isOnline);
  53. }
  54. }
  55. Future<_RpcResult> _rpcGet(String action) async {
  56. final url = Uri.parse('$_rpcBaseUrl/api/$action').toString();
  57. final response = await _rpcDio.get(url);
  58. if (response.statusCode != 200) {
  59. log(_tag, 'rpcGet HTTP error: ${response.statusCode}');
  60. return _RpcResult(false, 'HTTP ${response.statusCode}', null);
  61. }
  62. final json = response.data as Map<String, dynamic>;
  63. final int code = json['code'] as int;
  64. final String msg = json['msg'] as String;
  65. return _RpcResult(code == 200, msg, json);
  66. }
  67. Future<_RpcResult> _rpcPost(
  68. String action,
  69. Map<String, dynamic> params,
  70. ) async {
  71. if (!isOnline) {
  72. throw RpcException('Vpn rpc service not available');
  73. }
  74. final url = Uri.parse('$_rpcBaseUrl/api/action').toString();
  75. final data = jsonEncode({'action': action, 'data': params});
  76. try {
  77. final timestamp = DateTime.now().millisecondsSinceEpoch;
  78. final signature = await Crypto.signature(
  79. '$data:$timestamp',
  80. _accessKeySecret,
  81. );
  82. log(_tag, '[VpnWindowsService] url:$url');
  83. log(_tag, '[VpnWindowsService] >>:$data');
  84. final response = await _rpcDio.post(
  85. url,
  86. data: data,
  87. options: Options(
  88. headers: {
  89. 'Accept': 'application/json',
  90. 'Timestamp': '$timestamp',
  91. 'Signature': signature,
  92. },
  93. ),
  94. );
  95. log(_tag, '[VpnWindowsService] <<:${response.data}');
  96. if (response.statusCode != 200) {
  97. log(_tag, 'rpcPost HTTP error: ${response.statusCode}');
  98. return _RpcResult(false, 'HTTP ${response.statusCode}', null);
  99. }
  100. final json = response.data as Map<String, dynamic>; // Dio 自动反序列化JSON的数据
  101. final int code = json['code'] as int;
  102. final String msg = json['msg'] as String;
  103. return _RpcResult(code == 200, msg, json);
  104. } on DioException catch (e, stack) {
  105. log(_tag, 'DioException: ${e.message}\nStack: $stack');
  106. throw RpcException('DioException: ${e.message}');
  107. } catch (e, stack) {
  108. log(_tag, 'request error: $e\nStack: $stack');
  109. throw RpcException('request error: $e');
  110. }
  111. }
  112. Future<void> _check() {
  113. return _rpcGet('ping');
  114. }
  115. Future<void> _resetProcess(int hostPort, bool debug, {int? rpcPort}) async {
  116. try {
  117. _killProcess();
  118. final List<String> args = [];
  119. args.add('-l');
  120. args.add('127.0.0.1:$hostPort');
  121. if (rpcPort != null) {
  122. args.add('-p');
  123. args.add(rpcPort.toString());
  124. }
  125. if (_logPath != null) {
  126. args.add('-d');
  127. args.add(_logPath!);
  128. }
  129. log(_tag, '_resetProcess: starting $_rpcFileName with args: $args');
  130. _process = await Process.start(
  131. _rpcFileName,
  132. args,
  133. runInShell: false,
  134. mode: ProcessStartMode.detached,
  135. );
  136. log(_tag, '_resetProcess: process started successfully.');
  137. } catch (e, stack) {
  138. log(_tag, '_resetProcess error: $e\nStack: $stack');
  139. }
  140. }
  141. void _killProcess() {
  142. log(_tag, 'VpnWindowsService kill process');
  143. _process?.kill();
  144. _process = null;
  145. _setIsOnline(false);
  146. }
  147. void dispose() async {
  148. log(_tag, 'VpnWindowsService dispose start.');
  149. await _shutdown();
  150. _ecStatus.close();
  151. _ecOnline.close();
  152. }
  153. Future<void> start(Map<String, dynamic> params) async {
  154. _connectionParams = null;
  155. // 通知正在连接
  156. _setStatus(ConnectionState.connecting, params);
  157. try {
  158. final result = await _rpcPost('connect', params);
  159. if (!result.success) {
  160. log(_tag, 'start call error: ${result.message}');
  161. _setStatus(ConnectionState.error, Errors.ERROR_RPC_RETURN_FALSE);
  162. } else {
  163. _connectionParams = params;
  164. log(_tag, 'start call success.');
  165. }
  166. } catch (e, stack) {
  167. log(_tag, 'start call exception: $e\nStack: $stack');
  168. // 通知连接失败
  169. _setStatus(ConnectionState.error, Errors.ERROR_RPC_CALL_FAILED);
  170. }
  171. }
  172. Future<void> stop() async {
  173. _needRecoverConnection = false;
  174. try {
  175. final result = await _rpcPost('disconnect', {});
  176. if (result.success) {
  177. log(_tag, 'disconnect call success.');
  178. } else {
  179. log(_tag, 'disconnect call error: ${result.message}');
  180. }
  181. } catch (e, stack) {
  182. log(_tag, 'disconnect call exception: $e\nStack: $stack');
  183. }
  184. }
  185. Future<String> getLocationName() async {
  186. return '';
  187. }
  188. Future<String> getRemoteAddress() async {
  189. try {
  190. final result = await _rpcPost('get_remote_ip', {});
  191. if (!result.success) {
  192. log(_tag, 'getRemoteAddress error: ${result.message}');
  193. return '';
  194. }
  195. return result.message;
  196. } catch (e) {
  197. log(_tag, 'getRemoteAddress error: $e');
  198. return '';
  199. }
  200. }
  201. Future<bool> setBypassAddresses(List<String> list) async {
  202. try {
  203. if (list.isEmpty) {
  204. log(_tag, 'setBypassAddresses: empty list');
  205. return false;
  206. }
  207. final result = await _rpcPost('set_pass_ips', {'ips': list.join(',')});
  208. if (!result.success) {
  209. log(_tag, 'setBypassAddresses error: ${result.message}');
  210. return false;
  211. }
  212. return true;
  213. } catch (e) {
  214. log(_tag, 'setBypassAddresses error: $e');
  215. return false;
  216. }
  217. }
  218. Future<void> _startup({bool debug = false}) async {
  219. log(_tag, 'startup called. debug: $debug');
  220. _vpnDebug = debug;
  221. if (_isStarted) {
  222. log(_tag, 'startup abort! _isStarted: $_isStarted');
  223. return;
  224. }
  225. try {
  226. // 处理VPN 消息
  227. final handler = webSocketHandler((webSocket, _) {
  228. webSocket.stream.listen((message) {
  229. _lastAliveTime = DateTime.now();
  230. try {
  231. final vpnMessage = VpnMessage.fromMap(jsonDecode(message));
  232. switch (vpnMessage.type) {
  233. case VpnMessageType.login:
  234. log(_tag, 'login message got');
  235. // rpc已经上线
  236. _setIsOnline(true);
  237. // 保存重连状态
  238. try {
  239. if (_status == ConnectionState.connected ||
  240. _status == ConnectionState.connecting) {
  241. _needRecoverConnection = true;
  242. }
  243. } catch (e, stack) {
  244. log(_tag, 'login Exception: $e\nStack: $stack');
  245. }
  246. break;
  247. case VpnMessageType.heartbeat:
  248. log(_tag, 'heartbeat message got');
  249. // 应答心跳
  250. webSocket.sink.add(
  251. VpnMessage.create(VpnMessageType.heartbeat, null).toJson(),
  252. );
  253. break;
  254. case VpnMessageType.stateSync:
  255. log(_tag, 'stateSync message got');
  256. try {
  257. final data = vpnMessage.data;
  258. if (data == null) {
  259. return;
  260. }
  261. final int state = data['state'] as int;
  262. final dynamic param = data['param'];
  263. _setStatus(ConnectionState.values[state], param);
  264. // 恢复连接
  265. if (_needRecoverConnection) {
  266. if (_connectionParams != null) {
  267. start(_connectionParams!);
  268. }
  269. _needRecoverConnection = false;
  270. }
  271. } catch (e, stack) {
  272. log(_tag, 'stateSync Exception: $e\nStack: $stack');
  273. }
  274. break;
  275. default:
  276. }
  277. } catch (e, stack) {
  278. log(_tag, 'webSocketHandler Exception: $e\nStack: $stack');
  279. }
  280. });
  281. });
  282. shelf_io.serve(handler, '127.0.0.1', 35461).then((server) async {
  283. _isStarted = true;
  284. log(_tag, 'startup finished. _isStarted: $_isStarted');
  285. log(_tag, 'Serving at ws://${server.address.host}:${server.port}');
  286. // websocket 服务器端
  287. _rpcHostPort = server.port;
  288. // 启动vpn客户端进程
  289. await _resetProcess(_rpcHostPort, rpcPort: _rpcPort, _vpnDebug);
  290. // 启动守护定时器
  291. _refreshTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
  292. final dt = DateTime.now().difference(_lastAliveTime).inSeconds;
  293. if (dt > _rpcHeartbeatTimeout) {
  294. // 尝试check一下
  295. try {
  296. await _check();
  297. _lastAliveTime = DateTime.now();
  298. log(_tag, 'RPC process has been woken up, continuing...');
  299. } catch (e) {
  300. log(_tag, 'RPC process has timedout, need reset.');
  301. _lastAliveTime = DateTime.now();
  302. await _resetProcess(_rpcHostPort, rpcPort: _rpcPort, _vpnDebug);
  303. }
  304. }
  305. });
  306. });
  307. } catch (e, stack) {
  308. log(_tag, 'startup error: $e\nStack: $stack');
  309. }
  310. }
  311. Future<void> _shutdown() async {
  312. log(_tag, 'shutdown called.');
  313. if (!_isStarted) {
  314. log(_tag, 'shutdown abort! _isStarted: $_isStarted');
  315. return;
  316. }
  317. try {
  318. // 断开连接
  319. await stop();
  320. } catch (e, stack) {
  321. log(_tag, 'shutdown disconnect error: $e\nStack: $stack');
  322. }
  323. try {
  324. // 关闭守护定时器
  325. _startupTimer?.cancel();
  326. } catch (e, stack) {
  327. log(_tag, 'shutdown _startupTimer cancel error: $e\nStack: $stack');
  328. }
  329. try {
  330. _refreshTimer?.cancel();
  331. } catch (e, stack) {
  332. log(_tag, 'shutdown _refreshTimer cancel error: $e\nStack: $stack');
  333. }
  334. try {
  335. // 关闭vpn客户端进程
  336. _killProcess();
  337. } catch (e, stack) {
  338. log(_tag, 'shutdown _killProcess error: $e\nStack: $stack');
  339. }
  340. _isStarted = false;
  341. _needRecoverConnection = false;
  342. _setStatus(ConnectionState.disconnected, null);
  343. log(_tag, 'shutdown finished. _isStarted: $_isStarted');
  344. }
  345. Future<void> initialize(int timedout, bool debug) async {
  346. log(_tag, 'vpn service initialize...');
  347. _logPath = (await logFileDirectory()).path;
  348. final startTime = DateTime.now();
  349. bool isTimedout = false;
  350. // 启动服务
  351. await _startup(debug: debug);
  352. // 等待vpn客户端就绪信号
  353. if (!isOnline) {
  354. final completer = Completer();
  355. Timer.periodic(const Duration(milliseconds: 500), (timer) {
  356. // 客户端就绪
  357. if (isOnline) {
  358. log(_tag, 'vpn service start successed.');
  359. completer.complete();
  360. timer.cancel();
  361. }
  362. // 等待超时
  363. final dt = DateTime.now().difference(startTime).inSeconds;
  364. if (dt > timedout) {
  365. log(_tag, 'vpn service start timedout.');
  366. completer.complete();
  367. timer.cancel();
  368. isTimedout = true;
  369. }
  370. });
  371. await completer.future;
  372. log(_tag, 'vpn service initialize finished.');
  373. }
  374. if (isTimedout) {
  375. _shutdown();
  376. throw WaitOnlineTimedOutException();
  377. }
  378. }
  379. bool get isOnline => _isOnline;
  380. ConnectionState get status => _status;
  381. Stream<bool> get onOnlineChanged => _ecOnline.stream;
  382. Stream<(ConnectionState, dynamic)> get onStatusChanged => _ecStatus.stream;
  383. }