Skip to content
Closed
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 @@ -96,6 +96,8 @@ enum class Methods(val method: String) {

//custom/lantern servers
GetLanternAvailableServers("getLanternAvailableServers"),
GetServerLocations("getServerLocations"),
SetPreferredServerLocation("setPreferredServerLocation"),
GetAutoServerLocation("getAutoServerLocation"),

//Split Tunnel methods
Expand Down Expand Up @@ -1090,6 +1092,42 @@ class MethodHandler : FlutterPlugin,
}
}

Methods.GetServerLocations.method -> {
scope.launch {
result.runCatching {
val data = Mobile.getServerLocations()
withContext(Dispatchers.Main) {
success(String(data))
}
}.onFailure { e ->
result.error(
"GetServerLocations",
e.localizedMessage ?: "Error while fetching server locations",
e
)
}
}
}

Methods.SetPreferredServerLocation.method -> {
scope.launch {
result.runCatching {
val country = call.argument<String>("country") ?: ""
val city = call.argument<String>("city") ?: ""
Mobile.setPreferredServerLocation(country, city)
withContext(Dispatchers.Main) {
success("ok")
}
}.onFailure { e ->
result.error(
"SetPreferredServerLocation",
e.localizedMessage ?: "Error setting preferred server location",
e
)
}
}
}

Methods.GetAutoServerLocation.method -> {
scope.launch {
result.runCatching {
Expand Down
3 changes: 3 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ msgstr "Server Selection"
msgid "smart_location"
msgstr "Smart Location"

msgid "smart_protocol"
msgstr "Smart Protocol"

msgid "automatically_chooses_fastest_location"
msgstr "Automatically chooses fastest location"

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/alecthomas/assert/v2 v2.3.0
github.com/getlantern/common v1.2.1-0.20260224184656-5aefb9c21c85
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
github.com/getlantern/radiance v0.0.0-20260324185216-5d134509713d
github.com/getlantern/radiance v0.0.0-20260326115814-5a920585cff5
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.41.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
github.com/getlantern/radiance v0.0.0-20260324185216-5d134509713d h1:Pf0CW29vHudf3JHCmFh8A76HXZcJUGyNgBlul1MdyBI=
github.com/getlantern/radiance v0.0.0-20260324185216-5d134509713d/go.mod h1:bqqrKshKfTPIEOjsedL9p1N5bZBRYTZ2UeUZMtgFWWY=
github.com/getlantern/radiance v0.0.0-20260326115814-5a920585cff5 h1:me5arg7rAtLgwcGSrS592dgdVlxGPGI+NsMr0K5mss4=
github.com/getlantern/radiance v0.0.0-20260326115814-5a920585cff5/go.mod h1:bqqrKshKfTPIEOjsedL9p1N5bZBRYTZ2UeUZMtgFWWY=
github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60 h1:m9eXjDK9vllbVH467+QXbrxUFFM9Yp7YJ90wZLw4dwU=
github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo=
Expand Down
42 changes: 42 additions & 0 deletions ios/Runner/Handlers/MethodHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ class MethodHandler {
case "getLanternAvailableServers":
self.getLanternAvailableServers(result: result)

case "getServerLocations":
self.getServerLocations(result: result)

case "setPreferredServerLocation":
if let args = call.arguments as? [String: Any] {
let country = args["country"] as? String ?? ""
let city = args["city"] as? String ?? ""
self.setPreferredServerLocation(result: result, country: country, city: city)
} else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing country/city", details: nil))
}

case "getAutoServerLocation":
self.getAutoServerLocation(result: result)

Expand Down Expand Up @@ -944,6 +956,36 @@ class MethodHandler {
}
}

func getServerLocations(result: @escaping FlutterResult) {
Task {
var error: NSError?
let locations = MobileGetServerLocations(&error)
if let error {
await self.handleFlutterError(error, result: result, code: "GET_SERVER_LOCATIONS_ERROR")
return
}
guard let locations else {
await MainActor.run { result("[]") }
return
}
await MainActor.run {
result(String(data: locations, encoding: .utf8))
}
}
}

func setPreferredServerLocation(result: @escaping FlutterResult, country: String, city: String) {
Task {
var error: NSError?
MobileSetPreferredServerLocation(country, city, &error)
if let error {
await self.handleFlutterError(error, result: result, code: "SET_PREFERRED_LOCATION_ERROR")
return
}
await MainActor.run { result("ok") }
}
}

func getAutoServerLocation(result: @escaping FlutterResult) {
Task {
var error: NSError?
Expand Down
41 changes: 41 additions & 0 deletions lantern-core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type App interface {
IsRadianceConnected() bool
IsVPNRunning() (bool, error)
GetAvailableServers() []byte
GetServerLocations() ([]byte, error)
GetAllLocations() ([]byte, error)
SetPreferredServerLocation(country, city string)
MyDeviceId() string
GetServerByTagJSON(tag string) ([]byte, bool, error)
ReferralAttachment(referralCode string) (bool, error)
Expand Down Expand Up @@ -406,6 +409,44 @@ func (lc *LanternCore) GetAvailableServers() []byte {
return jsonBytes
}

// GetServerLocations returns the list of all available server locations from
// the config response. Unlike GetAvailableServers (which returns active
// outbounds), this returns every location the user can select — including
// ones that don't have routes yet (the server will provision them on demand
// when the user selects the location via SetPreferredServer).
func (lc *LanternCore) GetServerLocations() ([]byte, error) {
locations, err := lc.rad.ServerLocations()
if err != nil {
return nil, fmt.Errorf("getting server locations: %w", err)
}
data, err := json.Marshal(locations)
if err != nil {
return nil, fmt.Errorf("marshalling server locations: %w", err)
}
return data, nil
}

// GetAllLocations returns a unified list of all server locations, combining
// available locations from the config response with active outbound details.
// Each location indicates whether it's active and its protocol.
func (lc *LanternCore) GetAllLocations() ([]byte, error) {
locations, err := lc.rad.AllLocations()
if err != nil {
return nil, fmt.Errorf("getting all locations: %w", err)
}
data, err := json.Marshal(locations)
if err != nil {
return nil, fmt.Errorf("marshalling all locations: %w", err)
}
return data, nil
}

// SetPreferredServerLocation sets the preferred server location. The next config
// fetch will include routes for this region. Pass empty strings to reset to auto.
func (lc *LanternCore) SetPreferredServerLocation(country, city string) {
lc.rad.SetPreferredServer(context.Background(), country, city)
}

// LoadInstalledApps fetches the app list or rescans if needed using common macOS locations
// currently only works on/enabled for macOS
func (lc *LanternCore) LoadInstalledApps(dataDir string) (string, error) {
Expand Down
69 changes: 39 additions & 30 deletions lantern-core/ffi/ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ import (
// by the GC heap bitmap. Allocating Go pointers (like C.CString or base64
// encoding) on that stack triggers bulkBarrierPreWrite panics.
func runOnGoStack(fn func() *C.char) *C.char {
result, _ := common.RunOffCgoStack(func() (*C.char, error) {
result, err := common.RunOffCgoStack(func() (*C.char, error) {
return fn(), nil
})
if err != nil {
slog.Error("RunOffCgoStack failed", "error", err)
return SendError(err)
}
return result
}

Expand Down Expand Up @@ -290,30 +294,6 @@ 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 Expand Up @@ -349,6 +329,35 @@ func getAvailableServers() *C.char {
return C.CString(string(c.GetAvailableServers()))
}

//export setPreferredServerLocation
func setPreferredServerLocation(_country, _city *C.char) {
country, city := C.GoString(_country), C.GoString(_city)
c, errStr := requireCore()
if errStr != nil {
return
}
c.SetPreferredServerLocation(country, city)
}

//export getServerLocations
func getServerLocations() *C.char {
return runOnGoStack(func() *C.char {
c, errStr := requireCore()
if errStr != nil {
return errStr
}
data, err := c.GetAllLocations()
if err != nil {
// Fall back to old method if AllLocations not available
data, err = c.GetServerLocations()
if err != nil {
return SendError(err)
}
}
return C.CString(string(data))
})
}

func sendStatusToPort(status VPNStatus) {
slog.Debug("sendStatusToPort called", "status", status)
if statusPort == 0 {
Expand Down Expand Up @@ -493,8 +502,8 @@ func oauthLoginUrl(_provider *C.char) *C.char {

//export oAuthLoginCallback
func oAuthLoginCallback(_oAuthToken *C.char) *C.char {
oAuthToken := C.GoString(_oAuthToken)
return runOnGoStack(func() *C.char {
oAuthToken := C.GoString(_oAuthToken)
c, errStr := requireCore()
if errStr != nil {
return errStr
Expand All @@ -513,8 +522,8 @@ func oAuthLoginCallback(_oAuthToken *C.char) *C.char {
//
//export login
func login(_email, _password *C.char) *C.char {
email, password := C.GoString(_email), C.GoString(_password)
return runOnGoStack(func() *C.char {
email, password := C.GoString(_email), C.GoString(_password)
c, errStr := requireCore()
if errStr != nil {
return errStr
Expand All @@ -529,8 +538,8 @@ func login(_email, _password *C.char) *C.char {

//export signup
func signup(_email, _password *C.char) *C.char {
email, password := C.GoString(_email), C.GoString(_password)
return runOnGoStack(func() *C.char {
email, password := C.GoString(_email), C.GoString(_password)
c, errStr := requireCore()
if errStr != nil {
return errStr
Expand All @@ -544,8 +553,8 @@ func signup(_email, _password *C.char) *C.char {

//export logout
func logout(_email *C.char) *C.char {
email := C.GoString(_email)
return runOnGoStack(func() *C.char {
email := C.GoString(_email)
c, errStr := requireCore()
if errStr != nil {
return errStr
Expand Down Expand Up @@ -664,8 +673,8 @@ func completeChangeEmail(_newEmail, _password, _code *C.char) *C.char {
//
//export deleteAccount
func deleteAccount(_email, _password *C.char, _isSSO C.int) *C.char {
email, password, isSSO := C.GoString(_email), C.GoString(_password), _isSSO != 0
return runOnGoStack(func() *C.char {
email, password, isSSO := C.GoString(_email), C.GoString(_password), _isSSO != 0
c, errStr := requireCore()
if errStr != nil {
return errStr
Expand Down
19 changes: 19 additions & 0 deletions lantern-core/mobile/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,25 @@ func GetAvailableServers() ([]byte, error) {
return withCoreR(func(c lanterncore.Core) ([]byte, error) { return c.GetAvailableServers(), nil })
}

// GetServerLocations returns a unified list of all server locations with active status.
func GetServerLocations() ([]byte, error) {
return withCoreR(func(c lanterncore.Core) ([]byte, error) {
data, err := c.GetAllLocations()
if err != nil {
return c.GetServerLocations()
}
return data, nil
})
}

// SetPreferredServerLocation sets the preferred server location for the next config fetch.
func SetPreferredServerLocation(country, city string) error {
return withCore(func(c lanterncore.Core) error {
c.SetPreferredServerLocation(country, city)
return nil
})
}

func IsVPNConnected() bool {
return vpn_tunnel.IsVPNRunning()
}
Expand Down
Loading
Loading