Skip to content

Commit 99175e6

Browse files
committed
WIP: trying to integrate SOCKS5 tunneling as VPN
1 parent a52bd8a commit 99175e6

8 files changed

Lines changed: 214 additions & 30 deletions

File tree

lib/features/home/home_screen.dart

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
// import 'package:mycelmob/mycelmob.dart';
23
import '../../app/widgets/app_scaffold.dart';
34
import '../../app/widgets/app_button.dart';
45
import '../../app/widgets/app_card.dart';
@@ -60,6 +61,7 @@ class HomeScreen extends ConsumerWidget {
6061
onDisconnect: () async {
6162
await service.stop();
6263
},
64+
service: service,
6365
),
6466
const SizedBox(height: AppSpacing.xxl),
6567
const _StatsRow(),
@@ -74,11 +76,13 @@ class _HeaderCard extends StatefulWidget {
7476
final NodeStatus status;
7577
final Future<void> Function() onConnect;
7678
final Future<void> Function() onDisconnect;
79+
final dynamic service;
7780

7881
const _HeaderCard({
7982
required this.status,
8083
required this.onConnect,
8184
required this.onDisconnect,
85+
required this.service,
8286
});
8387

8488
@override
@@ -87,6 +91,7 @@ class _HeaderCard extends StatefulWidget {
8791

8892
class _HeaderCardState extends State<_HeaderCard> {
8993
bool _isLoading = false;
94+
bool _isSocks5Enabled = false;
9095

9196
bool get isRestartVisible =>
9297
widget.status == NodeStatus.connected && !_isLoading;
@@ -181,14 +186,35 @@ class _HeaderCardState extends State<_HeaderCard> {
181186
const SizedBox(height: AppSpacing.lg),
182187
AppCard(
183188
margin: EdgeInsets.zero,
184-
child: Row(
185-
mainAxisAlignment: MainAxisAlignment.center,
186-
children: const [
187-
Flexible(
188-
child:
189-
Text('Advanced Options', overflow: TextOverflow.ellipsis),
189+
child: ExpansionTile(
190+
title: Row(
191+
mainAxisAlignment: MainAxisAlignment.center,
192+
children: const [
193+
Flexible(
194+
child: Text('Advanced Options',
195+
overflow: TextOverflow.ellipsis),
196+
),
197+
Icon(Icons.expand_more),
198+
],
199+
),
200+
children: [
201+
ListTile(
202+
title: const Text('Enable SOCKS5 tunneling as VPN'),
203+
trailing: Switch(
204+
value: _isSocks5Enabled,
205+
onChanged: (value) async {
206+
setState(() => _isSocks5Enabled = value);
207+
if (value) {
208+
final result =
209+
await widget.service.proxyConnect('127.0.0.1:1080');
210+
debugPrint('Proxy connect result: $result');
211+
} else {
212+
final result = await widget.service.proxyDisconnect();
213+
debugPrint('Proxy disconnect result: $result');
214+
}
215+
},
216+
),
190217
),
191-
Icon(Icons.chevron_right),
192218
],
193219
),
194220
),

lib/features/settings/settings_screen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class SettingsScreen extends ConsumerWidget {
7171
),
7272
title: 'Mycelium Core',
7373
subtitle: 'Network Protocol Version',
74-
value: 'v2.1.4',
74+
value: 'v0.6.2',
7575
),
7676
const SizedBox(height: AppSpacing.lg),
7777
_InfoRowWithSubtitle(

lib/main.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ Future<void> main() async {
4343
runApp(const ProviderScope(child: MyApp()));
4444
}
4545

46-
class MyApp extends StatefulWidget {
46+
class MyApp extends ConsumerStatefulWidget {
4747
const MyApp({super.key});
4848

4949
@override
50-
State<MyApp> createState() => _MyAppState();
50+
ConsumerState<MyApp> createState() => _MyAppState();
5151
}
5252

53-
class _MyAppState extends State<MyApp> {
53+
class _MyAppState extends ConsumerState<MyApp> {
5454
static const platform = MethodChannel("tech.threefold.mycelium/tun");
5555
String _nodeAddr = '';
5656
var privKey = Uint8List(0);
@@ -199,9 +199,7 @@ class _MyAppState extends State<MyApp> {
199199
@override
200200
Widget build(BuildContext context) {
201201
//_logger.info("ratio: ${MediaQuery.devicePixelRatioOf(context)}");
202-
final settings = ProviderScope.containerOf(context, listen: true)
203-
.read(appSettingsProvider)
204-
.themeMode;
202+
final settings = ref.watch(appSettingsProvider).themeMode;
205203
return MaterialApp.router(
206204
theme: AppTheme.light(),
207205
darkTheme: AppTheme.dark(),

lib/myceliumflut_ffi_binding.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:ffi/ffi.dart';
33
import 'dart:io';
44
import 'package:flutter/foundation.dart';
55
import 'package:path/path.dart' show join;
6+
import 'dart:ffi';
7+
import 'dart:typed_data';
68

79
ffi.DynamicLibrary loadDll() {
810
var dllPath = 'assets/dll/winmycelium.dll';
@@ -200,3 +202,46 @@ Future<List<String>> myFFGetPeerStatus() async {
200202

201203
return peerStatusList;
202204
}
205+
206+
final DynamicLibrary _dylib = Platform.isMacOS || Platform.isWindows
207+
? DynamicLibrary.open('libmycelium.dylib')
208+
: DynamicLibrary.open('libmycelium.so');
209+
210+
typedef _ffiProxyConnectC = Pointer<Utf8> Function(Pointer<Utf8>);
211+
typedef _ffiProxyConnectDart = Pointer<Utf8> Function(Pointer<Utf8>);
212+
213+
typedef _ffiProxyDisconnectC = Pointer<Utf8> Function();
214+
typedef _ffiProxyDisconnectDart = Pointer<Utf8> Function();
215+
216+
final _ffiProxyConnect =
217+
_dylib.lookupFunction<_ffiProxyConnectC, _ffiProxyConnectDart>(
218+
'ffi_proxy_connect');
219+
220+
final _ffiProxyDisconnect =
221+
_dylib.lookupFunction<_ffiProxyDisconnectC, _ffiProxyDisconnectDart>(
222+
'ffi_proxy_disconnect');
223+
224+
/// Helper to free strings
225+
typedef _ffiFreeStringC = Void Function(Pointer<Utf8>);
226+
typedef _ffiFreeStringDart = void Function(Pointer<Utf8>);
227+
final _ffiFreeString = _dylib
228+
.lookupFunction<_ffiFreeStringC, _ffiFreeStringDart>('ffi_free_string');
229+
230+
List<String> myFFProxyConnect(String remote) {
231+
final remotePtr = remote.toNativeUtf8();
232+
final resultPtr = _ffiProxyConnect(remotePtr);
233+
calloc.free(remotePtr);
234+
235+
final result = resultPtr.toDartString();
236+
_ffiFreeString(resultPtr);
237+
238+
return result.split(','); // convert "a,b,c" -> ["a","b","c"]
239+
}
240+
241+
List<String> myFFProxyDisconnect() {
242+
final resultPtr = _ffiProxyDisconnect();
243+
final result = resultPtr.toDartString();
244+
_ffiFreeString(resultPtr);
245+
246+
return result.split(',');
247+
}

lib/services/ffi/mycelium_service.dart

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import '../../myceliumflut_ffi_binding.dart';
88
enum NodeStatus { disconnected, connecting, connected, failed }
99

1010
class MyceliumService {
11-
static const MethodChannel _platform = MethodChannel("tech.threefold.mycelium/tun");
11+
static const MethodChannel _platform =
12+
MethodChannel("tech.threefold.mycelium/tun");
1213

13-
final StreamController<NodeStatus> _statusController = StreamController<NodeStatus>.broadcast();
14+
final StreamController<NodeStatus> _statusController =
15+
StreamController<NodeStatus>.broadcast();
1416
NodeStatus _status = NodeStatus.disconnected;
1517
Uint8List? _privKey;
1618

@@ -29,7 +31,8 @@ class MyceliumService {
2931
if (isUseDylib()) {
3032
privKey = myFFGenerateSecretKey();
3133
} else {
32-
privKey = (await _platform.invokeMethod<Uint8List>('generateSecretKey')) as Uint8List;
34+
privKey = (await _platform.invokeMethod<Uint8List>('generateSecretKey'))
35+
as Uint8List;
3336
}
3437
await file.writeAsBytes(privKey);
3538
_privKey = privKey;
@@ -59,18 +62,22 @@ class MyceliumService {
5962

6063
String? validatePeer(String peer) {
6164
final prefixRegex = RegExp(r'^tcp://');
62-
final ipv4Regex = RegExp(r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)');
63-
final ipv6Regex = RegExp(r'\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\]');
65+
final ipv4Regex = RegExp(
66+
r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)');
67+
final ipv6Regex = RegExp(
68+
r'\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\]');
6469
final portRegex = RegExp(r':9651$');
6570
if (!prefixRegex.hasMatch(peer)) return 'peer must start with tcp://';
6671
final ipPortPart = peer.substring(peer.indexOf('://') + 3);
67-
if (!ipv4Regex.hasMatch(ipPortPart) && !ipv6Regex.hasMatch(ipPortPart)) return 'peer must contain a valid IPv4 or IPv6 address';
72+
if (!ipv4Regex.hasMatch(ipPortPart) && !ipv6Regex.hasMatch(ipPortPart))
73+
return 'peer must contain a valid IPv4 or IPv6 address';
6874
if (!portRegex.hasMatch(ipPortPart)) return 'peer must end with :9651';
6975
return null;
7076
}
7177

7278
String? validatePeers(List<String> peers) {
73-
if (peers.isEmpty || (peers.length == 1 && peers[0].isEmpty)) return "peers can't be empty";
79+
if (peers.isEmpty || (peers.length == 1 && peers[0].isEmpty))
80+
return "peers can't be empty";
7481
for (final p in peers) {
7582
final e = validatePeer(p);
7683
if (e != null) return 'invalid peer:`$p` $e';
@@ -147,7 +154,8 @@ class MyceliumService {
147154
peerStatus = await myFFGetPeerStatus();
148155
} else {
149156
// Android/iOS platform - use platform channel
150-
final result = await _platform.invokeMethod<List<dynamic>>('getPeerStatus');
157+
final result =
158+
await _platform.invokeMethod<List<dynamic>>('getPeerStatus');
151159
peerStatus = result?.cast<String>() ?? [];
152160
}
153161

@@ -165,6 +173,34 @@ class MyceliumService {
165173
void dispose() {
166174
_statusController.close();
167175
}
168-
}
169176

177+
Future<List<String>> proxyConnect(String remote) async {
178+
try {
179+
if (isUseDylib()) {
180+
return myFFProxyConnect(remote);
181+
} else {
182+
final result = await _platform.invokeMethod<List<dynamic>>(
183+
'proxyConnect',
184+
{'remote': remote},
185+
);
186+
return result?.cast<String>() ?? [];
187+
}
188+
} catch (e) {
189+
throw Exception("Failed to proxyConnect: $e");
190+
}
191+
}
170192

193+
Future<List<String>> proxyDisconnect() async {
194+
try {
195+
if (isUseDylib()) {
196+
return myFFProxyDisconnect();
197+
} else {
198+
final result =
199+
await _platform.invokeMethod<List<dynamic>>('proxyDisconnect');
200+
return result?.cast<String>() ?? [];
201+
}
202+
} catch (e) {
203+
throw Exception("Failed to proxyDisconnect: $e");
204+
}
205+
}
206+
}

mycelffi/src/lib.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use mobile;
2-
1+
use mobile::proxy::{proxy_connect, proxy_disconnect};
32
use std::ffi::{CStr, CString};
43
use std::os::raw::c_char;
54

5+
66
#[no_mangle]
77
pub extern "C" fn ff_generate_secret_key(out_ptr: *mut *mut u8, out_len: *mut usize) {
88
let secret_key = mobile::generate_secret_key();
@@ -115,3 +115,19 @@ pub extern "C" fn free_peer_status(ptr: *mut *mut c_char, len: usize) {
115115
Vec::from_raw_parts(ptr, len, len);
116116
}
117117
}
118+
119+
#[no_mangle]
120+
pub extern "C" fn ff_proxy_connect(remote_str: *const c_char) -> bool {
121+
let c_str = unsafe { CStr::from_ptr(remote_str) };
122+
let remote_str = c_str.to_string_lossy();
123+
let rt = tokio::runtime::Runtime::new().unwrap();
124+
let result = rt.block_on(proxy_connect(&remote_str));
125+
result == "ok"
126+
}
127+
128+
#[no_mangle]
129+
pub extern "C" fn ff_proxy_disconnect() -> bool {
130+
let rt = tokio::runtime::Runtime::new().unwrap();
131+
let result = rt.block_on(proxy_disconnect());
132+
result == "ok"
133+
}

mycelmob/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ name = "mycelmob"
1111

1212
[dependencies]
1313
uniffi = { version = "0.28.2", features = ["cli"] }
14-
mobile = { git = "http://github.com/threefoldtech/mycelium", package = "mobile", tag = "v0.6.1", features = [
14+
mobile = { git = "http://github.com/threefoldtech/mycelium", package = "mobile", tag = "v0.6.2", features = [
1515
"mactunfd",
1616
] }
17+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
18+
once_cell = "1.19"
19+
1720
#mobile = { path = "../../mycelium/mobile" }
1821

1922
[build-dependencies]
2023
uniffi = { version = "0.28.2", features = ["build"] }
2124

2225
[[bin]]
2326
name = "uniffi-bindgen"
24-
path = "uniffi-bindgen.rs"
27+
path = "uniffi-bindgen.rs"

0 commit comments

Comments
 (0)