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
88 changes: 64 additions & 24 deletions lantern-core/mobile/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ import (
)

var (
lanternCore atomic.Value
errLanternNotReady = errors.New("radiance not initialized")

ipcServer *ipc.Server
ipcClient *ipc.Client // loopback client for extension process
ipcBackend *backend.LocalBackend
ipcMu sync.Mutex
ipcOnce sync.Once
lanternCore atomic.Value
errLanternNotReady = errors.New("radiance not initialized")
errIPCLifecycleBusy = errors.New("IPC server lifecycle operation in progress")
errIPCStartCanceled = errors.New("IPC server startup canceled")

ipcServer *ipc.Server
ipcClient atomic.Pointer[ipc.Client] // loopback client for extension process
ipcBackend *backend.LocalBackend
ipcMu sync.Mutex
ipcStarting bool
ipcClosing bool
ipcGeneration uint64
)

func getCore() (lanterncore.Core, error) {
Expand Down Expand Up @@ -77,9 +81,7 @@ func withCoreR[T any](fn func(c lanterncore.Core) (T, error)) (T, error) {
// StartIPCServer (extension process), falling back to lanternCore's client
// (main app process).
func getClient() (*ipc.Client, error) {
ipcMu.Lock()
c := ipcClient
ipcMu.Unlock()
c := ipcClient.Load()
if c != nil {
return c, nil
}
Expand Down Expand Up @@ -285,10 +287,23 @@ func StopVPN() error {
func StartIPCServer(platform utils.PlatformInterface, opts *utils.Opts) error {
_, err := utils.RunOffCgoStack(func() (struct{}, error) {
ipcMu.Lock()
defer ipcMu.Unlock()
if ipcServer != nil {
ipcMu.Unlock()
return struct{}{}, nil
}
if ipcStarting || ipcClosing {
ipcMu.Unlock()
return struct{}{}, errIPCLifecycleBusy
}
ipcStarting = true
generation := ipcGeneration
ipcMu.Unlock()
defer func() {
ipcMu.Lock()
ipcStarting = false
ipcMu.Unlock()
}()

// The backend's config fetcher captures common.GetBaseURL() at
// construction, so the environment must be set before
// NewLocalBackend — SetupRadiance's SetStagingEnv runs too late on
Expand All @@ -310,12 +325,23 @@ func StartIPCServer(platform utils.PlatformInterface, opts *utils.Opts) error {
return struct{}{}, fmt.Errorf("error creating backend for IPC server: %v", err)
}
be.Start()
ipcBackend = be
ipcServer = ipc.NewServer(be, !common.IsMobile())
ipcClient = newLoopbackClient(be)
if err := ipcServer.Start(); err != nil {
server := ipc.NewServer(be, !common.IsMobile())
if err := server.Start(); err != nil {
be.Close()
return struct{}{}, err
}

ipcMu.Lock()
if generation != ipcGeneration {
ipcMu.Unlock()
_ = server.Close()
be.Close()
return struct{}{}, errIPCStartCanceled
}
ipcBackend = be
ipcServer = server
ipcClient.Store(newLoopbackClient(be))
ipcMu.Unlock()
return struct{}{}, nil
})
return err
Expand All @@ -324,16 +350,30 @@ func StartIPCServer(platform utils.PlatformInterface, opts *utils.Opts) error {
func CloseIPCServer() error {
_, err := utils.RunOffCgoStack(func() (struct{}, error) {
ipcMu.Lock()
defer ipcMu.Unlock()
if ipcBackend != nil {
ipcBackend.Close()
ipcBackend = nil
if ipcClosing {
ipcMu.Unlock()
return struct{}{}, nil
}
if ipcServer != nil {
ipcServer.Close()
ipcServer = nil
ipcClosing = true
ipcGeneration++
ipcClient.Store(nil)
be := ipcBackend
server := ipcServer
ipcBackend = nil
ipcServer = nil
ipcMu.Unlock()
defer func() {
ipcMu.Lock()
ipcClosing = false
ipcMu.Unlock()
}()

if server != nil {
_ = server.Close()
}
if be != nil {
be.Close()
}
ipcClient = nil
return struct{}{}, nil
})
return err
Expand Down
140 changes: 56 additions & 84 deletions lantern-core/mobile/mobile_test.go
Original file line number Diff line number Diff line change
@@ -1,86 +1,58 @@
package mobile

// // todo implement a mock for all test cases
// func radianceOptions() radiance.Options {
// return radiance.Options{
// DataDir: os.TempDir(),
// LogDir: os.TempDir(),
// DeviceID: "test-123",
// Locale: "en-us",
// }
// }

// func TestSetupRadiance(t *testing.T) {
// rr, err := radiance.NewRadiance(radianceOptions())
// assert.Nil(t, err)
// assert.NotNil(t, rr)

// }

// // // skip this test for now
// // func TestStartVPN(t *testing.T) {
// // data := radianceOptions().DataDir
// // log := radianceOptions().LogDir
// // rr, err := client.NewVPNClient(data, log, nil, false)
// // assert.Nil(t, err)
// // assert.NotNil(t, rr)
// // err1 := rr.StartVPN()
// // assert.Nil(t, err1)
// // }

// func TestCreateUser(t *testing.T) {
// rr, err := radiance.NewRadiance(radianceOptions())
// api := rr.APIHandler()
// assert.Nil(t, err)
// assert.NotNil(t, rr)
// user, err := api.NewUser(context.Background())
// assert.Nil(t, err)
// assert.NotNil(t, user)
// }

// func TestSubscriptionRedirect(t *testing.T) {
// rr, err := radiance.NewRadiance(radianceOptions())
// apiClient := rr.APIHandler()
// assert.Nil(t, err)
// assert.NotNil(t, rr)
// data := api.PaymentRedirectData{
// Provider: "stripe",
// Plan: "monthly",
// DeviceName: "test-123",
// Email: "test@getlantern.org",
// BillingType: api.SubscriptionTypeSubscription,
// }
// user, err := apiClient.SubscriptionPaymentRedirectURL(context.Background(), data)
// assert.Nil(t, err)
// assert.NotNil(t, user)
// }

// func TestUserData(t *testing.T) {
// rr, err := radiance.NewRadiance(radianceOptions())
// api := rr.APIHandler()
// assert.Nil(t, err)
// assert.NotNil(t, rr)
// user, err := api.UserData(context.Background())
// assert.Nil(t, err)
// assert.NotNil(t, user)
// }

// func TestPlans(t *testing.T) {
// rr, err := radiance.NewRadiance(radianceOptions())
// api := rr.APIHandler()
// assert.Nil(t, err)
// assert.NotNil(t, rr)
// plans, err := api.SubscriptionPlans(context.Background(), "non-store")
// assert.Nil(t, err)
// assert.NotNil(t, plans)
// }

// func TestOAuthLoginUrl(t *testing.T) {
// rr, err := radiance.NewRadiance(radianceOptions())
// api := rr.APIHandler()
// assert.Nil(t, err)
// assert.NotNil(t, rr)
// user, err := api.OAuthLoginUrl(context.Background(), "google")
// assert.Nil(t, err)
// assert.NotNil(t, user)
// }
import (
"errors"
"testing"
"time"

"github.com/getlantern/radiance/ipc"
)

func TestGetClientDoesNotWaitForIPCLifecycleLock(t *testing.T) {
want := &ipc.Client{}
previousClient := ipcClient.Swap(want)
t.Cleanup(func() {
ipcClient.Store(previousClient)
})

ipcMu.Lock()
defer ipcMu.Unlock()

result := make(chan *ipc.Client, 1)
go func() {
client, _ := getClient()
result <- client
}()

select {
case got := <-result:
if got != want {
t.Fatalf("getClient() = %p, want %p", got, want)
}
case <-time.After(time.Second):
t.Fatal("getClient blocked on the IPC lifecycle lock")
}
}

func TestStartIPCServerReportsLifecycleBusy(t *testing.T) {
ipcMu.Lock()
previousServer := ipcServer
previousStarting := ipcStarting
previousClosing := ipcClosing
ipcServer = nil
ipcStarting = true
ipcClosing = false
ipcMu.Unlock()
t.Cleanup(func() {
ipcMu.Lock()
ipcServer = previousServer
ipcStarting = previousStarting
ipcClosing = previousClosing
ipcMu.Unlock()
})

err := StartIPCServer(nil, nil)
Comment thread
atavism marked this conversation as resolved.
if !errors.Is(err, errIPCLifecycleBusy) {
t.Fatalf("StartIPCServer() error = %v, want %v", err, errIPCLifecycleBusy)
}
}
36 changes: 21 additions & 15 deletions lantern-core/vpn_tunnel/vpn_tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ const (
InternalTagAutoAll InternalTag = "auto-all"
)

// StartVPN is the gomobile entry point for Mobile.StartVPN (Android
// MainActivity / iOS VPNManager). It is also reached from Jigar's
// onSmartLocation rewrite in server_selection.dart via startVPN(force: true)
// → lantern.startVPN() → Mobile.StartVPN, which expects "switch back to
// auto" to work on a live tunnel. Delegate to ConnectToServer so the
// VPNStatus → /server/selected dispatch handles that case.
var connectSem = make(chan struct{}, 1)

type vpnClient interface {
VPNStatus(context.Context) (vpn.VPNStatus, error)
ConnectVPN(context.Context, string) error
SelectServer(context.Context, string) error
}

// StartVPN starts the tunnel with automatic server selection.
func StartVPN(ctx context.Context, client *ipc.Client) error {
slog.Info("StartVPN called")
return ConnectToServer(ctx, client, vpn.AutoSelectTag)
Expand All @@ -30,16 +33,19 @@ func StopVPN(ctx context.Context, client *ipc.Client) error {
return client.DisconnectVPN(ctx)
}

// ConnectToServer switches the live tunnel to a specific server or, when the
// caller passes an empty tag or vpn.AutoSelectTag, back to auto-select.
// Radiance normalizes the empty-tag case server-side (fac9089) for both
// ConnectVPN and SelectServer.
//
// The caller is responsible for putting a deadline on ctx — the connect
// path involves real network work (DNS, TLS, sing-box bring-up) and we
// don't want a hung lanternd to stall the UI forever. LanternCore.ConnectVPN
// uses 60 s.
// ConnectToServer starts the tunnel or changes the selected server.
func ConnectToServer(ctx context.Context, client *ipc.Client, tag string) error {
return connectToServer(ctx, client, tag)
}

func connectToServer(ctx context.Context, client vpnClient, tag string) error {
select {
case connectSem <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
defer func() { <-connectSem }()

Comment thread
atavism marked this conversation as resolved.
Comment thread
atavism marked this conversation as resolved.
slog.Debug("Connecting to VPN server", "tag", tag)

// Switch outbounds on the live tunnel when already connected;
Expand Down
Loading
Loading