Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum class Methods(val method: String) {
Stop("stopVPN"),
ConnectToServer("connectToServer"),
IsVpnConnected("isVPNConnected"),
IsTagAvailable("isTagAvailable"),

//Payment methods
StripeSubscription("stripeSubscription"),
Expand Down Expand Up @@ -195,6 +196,23 @@ class MethodHandler : FlutterPlugin,
}
}

Methods.IsTagAvailable.method -> {
scope.launch {
try {
val tag = call.arguments as? String
?: throw IllegalArgumentException("Missing or invalid tag")
val available = Mobile.isTagAvailable(tag)
withContext(Dispatchers.Main) {
result.success(available)
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
result.error("tag_check_failed", e.localizedMessage ?: "Error", e)
}
}
}
}

Methods.IsVpnConnected.method -> {
scope.launch {
runCatching {
Expand Down
11 changes: 11 additions & 0 deletions ios/Runner/Handlers/MethodHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ class MethodHandler {
case "startVPN":
self.startVPN(result: result)

case "isTagAvailable":
guard let tag: String = self.decodeValue(from: call.arguments, result: result) else {
return
}
self.isTagAvailable(result: result, tag: tag)

case "connectToServer":
guard let data = self.decodeDict(from: call.arguments, result: result) else { return }
self.connectToServer(result: result, data: data)
Expand Down Expand Up @@ -301,6 +307,11 @@ class MethodHandler {
}
}

private func isTagAvailable(result: @escaping FlutterResult, tag: String) {
let available = MobileIsTagAvailable(tag)
result(available)
}

private func connectToServer(result: @escaping FlutterResult, data: [String: Any]) {
Task {
do {
Expand Down
24 changes: 24 additions & 0 deletions lantern-core/ffi/ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,30 @@ func getAutoLocation() *C.char {
return C.CString(string(jsonBytes))
}

// isTagAvailable checks if a server with the given tag exists in the server list.
// Returns "true" if found, "false" if not found, or "true" when the check cannot be
// performed (fail-open: allows connection attempts to proceed normally).
//
//export isTagAvailable
func isTagAvailable(_tag *C.char) *C.char {
tag := C.GoString(_tag)
c, errStr := requireCore()
if errStr != nil {
slog.Warn("Unable to check tag availability (core not ready), assuming available", "tag", tag)
C.free(unsafe.Pointer(errStr))
return C.CString("true")
}
_, found, err := c.GetServerByTagJSON(tag)
if err != nil {
slog.Warn("Error checking tag availability, assuming available", "tag", tag, "error", err)
return C.CString("true")
}
if found {
return C.CString("true")
}
return C.CString("false")
}

// startAutoLocationListener starts the auto location listener.
//
//export startAutoLocationListener
Expand Down
15 changes: 15 additions & 0 deletions lantern-core/mobile/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ func CloseIPC() error {
return vpn_tunnel.CloseIPC()
}

// IsTagAvailable checks if a server with the given tag exists in the server list.
// Returns true if the tag is found. Returns true when the check cannot be performed
// (fail-open: allows connection attempts to proceed normally).
func IsTagAvailable(tag string) bool {
found, err := withCoreR(func(c lanterncore.Core) (bool, error) {
_, ok, err := c.GetServerByTagJSON(tag)
return ok, err
})
if err != nil {
slog.Warn("Unable to check tag availability, assuming available", "tag", tag, "error", err)
return true
}
return found
}

// ConnectToServer connects to a server using the provided location type and tag.
// It works with private servers and lantern location servers.
func ConnectToServer(locationType, tag string, platIfce utils.PlatformInterface, options *utils.Opts) error {
Expand Down
2 changes: 1 addition & 1 deletion lantern-core/vpn_tunnel/vpn_tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func StartVPN(platform rvpn.PlatformInterface, opts *utils.Opts) error {
}
// it should use InternalTagLantern so it will connect to best lantern server by default.
// if you want to connect to user server, use ConnectToServer with InternalTagUser
err := vpn.QuickConnect("", platform)
err := vpn.AutoConnect("")
if err != nil {
return fmt.Errorf("failed to start VPN: %w", err)
}
Expand Down
76 changes: 39 additions & 37 deletions lib/features/vpn/provider/vpn_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,42 @@ class VpnNotifier extends _$VpnNotifier {
@override
VPNStatus build() {
ref.read(lanternServiceProvider).isVPNConnected();
ref.listen(
vPNStatusProvider,
(previous, next) {
final previousStatus = previous?.value?.status;
final nextStatus = next.value!.status;
ref.listen(vPNStatusProvider, (previous, next) {
final previousStatus = previous?.value?.status;
final nextStatus = next.value!.status;

if (previous != null &&
previous.value != null &&
previousStatus != nextStatus) {
if (previousStatus != VPNStatus.connecting &&
nextStatus == VPNStatus.disconnected) {
sl<NotificationService>().showNotification(
id: NotificationEvent.vpnDisconnected.id,
title: 'app_name'.i18n,
body: 'vpn_disconnected'.i18n,
);
} else if (nextStatus == VPNStatus.connected) {
if (PlatformUtils.isMobile) {
HapticFeedback.mediumImpact();
}
if (previous != null &&
previous.value != null &&
previousStatus != nextStatus) {
if (previousStatus != VPNStatus.connecting &&
nextStatus == VPNStatus.disconnected) {
sl<NotificationService>().showNotification(
id: NotificationEvent.vpnDisconnected.id,
title: 'app_name'.i18n,
body: 'vpn_disconnected'.i18n,
);
} else if (nextStatus == VPNStatus.connected) {
if (PlatformUtils.isMobile) {
HapticFeedback.mediumImpact();
}

/// Mark successful connection in app settings
ref.read(appSettingProvider.notifier).setSuccessfulConnection(true);
/// Mark successful connection in app settings
ref.read(appSettingProvider.notifier).setSuccessfulConnection(true);

// Server location is updated via the "server-location" push event
// from the Go side (handled by AppEventNotifier), not by polling
// getAutoServerLocation here. This avoids a race where the NE
// reports "connected" before the Go tunnel is fully ready.
// Server location is updated via the "server-location" push event
// from the Go side (handled by AppEventNotifier), not by polling
// getAutoServerLocation here. This avoids a race where the NE
// reports "connected" before the Go tunnel is fully ready.

sl<NotificationService>().showNotification(
id: NotificationEvent.vpnConnected.id,
title: 'app_name'.i18n,
body: 'vpn_connected'.i18n,
);
}
sl<NotificationService>().showNotification(
id: NotificationEvent.vpnConnected.id,
title: 'app_name'.i18n,
body: 'vpn_connected'.i18n,
);
}
state = nextStatus;
},
);
}
state = nextStatus;
});
return VPNStatus.disconnected;
}

Expand All @@ -81,14 +78,17 @@ class VpnNotifier extends _$VpnNotifier {
final type = serverLocation.serverType.toServerLocationType;
if (type == ServerLocationType.auto || force) {
appLogger.debug(
'Got server location with type auto or force is true, starting VPN with auto');
'Got server location with type auto or force is true, starting VPN with auto',
);
return lantern.startVPN();
}

final tag = serverLocation.serverName;
final tagAvailable = await lantern.isTagAvailable(tag);
if (!tagAvailable) {
appLogger.debug('Server tag "$tag" not available, falling back to auto VPN');
appLogger.debug(
'Server tag "$tag" not available, falling back to auto VPN',
);
return lantern.startVPN();
}
return connectToServer(type, tag);
Expand All @@ -97,7 +97,9 @@ class VpnNotifier extends _$VpnNotifier {
/// Connects to a specific server location.
/// it supports lantern locations and private servers.
Future<Either<Failure, String>> connectToServer(
ServerLocationType location, String tag) async {
ServerLocationType location,
String tag,
) async {
appLogger.debug("Connecting to server: $location with tag: $tag");
final result = await ref
.read(lanternServiceProvider)
Expand Down
79 changes: 50 additions & 29 deletions lib/lantern/lantern_ffi_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ class LanternFFIService implements LanternCoreService {
final dataDir = await AppStorageUtils.getAppDirectory();
final logDir = await AppStorageUtils.getAppLogDirectory();
appLogger.info(
"Radiance configuration - env: $env, dataDir: ${dataDir.path}, logDir: $logDir, telemetryConsent: $consent");
"Radiance configuration - env: $env, dataDir: ${dataDir.path}, logDir: $logDir, telemetryConsent: $consent",
);

final dataDirPtr = dataDir.path.toCharPtr;
final logDirPtr = logDir.toCharPtr;
Expand All @@ -218,10 +219,7 @@ class LanternFFIService implements LanternCoreService {

checkAPIError(result);
if (result != 'ok' && result != 'true') {
throw PlatformException(
code: 'radiance_setup_failed',
message: result,
);
throw PlatformException(code: 'radiance_setup_failed', message: result);
}
return right(unit);
} catch (e, st) {
Expand Down Expand Up @@ -340,8 +338,9 @@ class LanternFFIService implements LanternCoreService {
});
checkAPIError(enabledJson);

final enabledKeys =
(jsonDecode(enabledJson) as List).cast<String>().toSet();
final enabledKeys = (jsonDecode(enabledJson) as List)
.cast<String>()
.toSet();

final decoded = jsonDecode(jsonApps) as List<dynamic>;
final rawApps = decoded.cast<Map<String, dynamic>>();
Expand Down Expand Up @@ -429,7 +428,8 @@ class LanternFFIService implements LanternCoreService {
return left(
Failure(
error: result['error'] ?? 'Unknown error',
localizedErrorMessage: result['localizedErrorMessage'] ??
localizedErrorMessage:
result['localizedErrorMessage'] ??
result['error'] ??
'Unknown error',
),
Expand Down Expand Up @@ -525,8 +525,9 @@ class LanternFFIService implements LanternCoreService {
return left(
Failure(
error: e.toString(),
localizedErrorMessage:
(e is Exception) ? e.localizedDescription : e.toString(),
localizedErrorMessage: (e is Exception)
? e.localizedDescription
: e.toString(),
),
);
} finally {
Expand Down Expand Up @@ -623,11 +624,28 @@ class LanternFFIService implements LanternCoreService {
Future<bool> isTagAvailable(String tag) async {
try {
final result = await runInBackground<String>(() async {
return _ffiService.isTagAvailable(tag.toCharPtr).toDartString();
final tagPtr = tag.toCharPtr;
try {
final resultPtr = _ffiService.isTagAvailable(tagPtr);
if (resultPtr == nullptr) {
return 'true';
}
try {
return resultPtr.toDartString();
} finally {
_ffiService.freeCString(resultPtr);
}
} finally {
malloc.free(tagPtr);
}
});
return result == 'true';
} catch (e, st) {
appLogger.error('Error checking tag availability, assuming available', e, st);
appLogger.error(
'Error checking tag availability, assuming available',
e,
st,
);
return true;
}
}
Expand Down Expand Up @@ -1253,19 +1271,21 @@ class LanternFFIService implements LanternCoreService {
}

@override
Future<Either<Failure, Unit>> addServerBasedOnURLs(
{required String urls,
required bool skipCertVerification,
required String serverName}) async {
Future<Either<Failure, Unit>> addServerBasedOnURLs({
required String urls,
required bool skipCertVerification,
required String serverName,
}) async {
try {
final result = await runInBackground<String>(
() async {
return _ffiService
.addServerBasedOnURLs(urls.toCharPtr,
skipCertVerification ? 1 : 0, serverName.toCharPtr)
.toDartString();
},
);
final result = await runInBackground<String>(() async {
return _ffiService
.addServerBasedOnURLs(
urls.toCharPtr,
skipCertVerification ? 1 : 0,
serverName.toCharPtr,
)
.toDartString();
});
checkAPIError(result);
return Right(unit);
} catch (e, stackTrace) {
Expand All @@ -1275,11 +1295,12 @@ class LanternFFIService implements LanternCoreService {
}

@override
Future<Either<Failure, String>> inviteToServerManagerInstance(
{required String ip,
required String port,
required String accessToken,
required String inviteName}) async {
Future<Either<Failure, String>> inviteToServerManagerInstance({
required String ip,
required String port,
required String accessToken,
required String inviteName,
}) async {
try {
final result = await runInBackground<String>(() async {
return _ffiService
Expand Down
13 changes: 13 additions & 0 deletions lib/lantern/lantern_generated_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2602,6 +2602,19 @@ class LanternBindings {
late final _getAutoLocation =
_getAutoLocationPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();

ffi.Pointer<ffi.Char> isTagAvailable(
ffi.Pointer<ffi.Char> _tag,
) {
return _isTagAvailable(_tag);
}

late final _isTagAvailablePtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char>)>>('isTagAvailable');
late final _isTagAvailable = _isTagAvailablePtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();

ffi.Pointer<ffi.Char> startAutoLocationListener() {
return _startAutoLocationListener();
}
Expand Down
Loading
Loading