diff --git a/assets/images/proxyall.svg b/assets/images/proxyall.svg new file mode 100644 index 000000000..998103a93 --- /dev/null +++ b/assets/images/proxyall.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/locales/en-us.po b/assets/locales/en-us.po index c29702a2d..26d118ed4 100644 --- a/assets/locales/en-us.po +++ b/assets/locales/en-us.po @@ -284,6 +284,12 @@ msgstr "" "If enabled all traffic will be sent through Lantern (more secure). If " "disabled, only blocked traffic will be sent through Lantern (faster)." +msgid "description_proxyless_dialog" +msgstr "" +"If enabled, Lantern will always try to access sites directly using " +"various tricks to bypass censorship (faster). If disabled, Lantern will " +"never try to use proxyless strategies." + msgid "Account" msgstr "Account" @@ -1860,5 +1866,8 @@ msgstr "Accept and Continue" msgid "decline" msgstr "Decline" +msgid "proxyless" +msgstr "Proxyless Dialing" + diff --git a/desktop/app/app.go b/desktop/app/app.go index 29eaa2b37..476c15e38 100644 --- a/desktop/app/app.go +++ b/desktop/app/app.go @@ -210,6 +210,9 @@ func (app *App) Run(ctx context.Context) error { flashlight.WithOnConfig(app.onConfigUpdate), flashlight.WithOnProxies(app.onProxiesUpdate), flashlight.WithOnSucceedingProxy(app.onSucceedingProxy), + flashlight.WithUseProxyless(func() bool { + return !settings.GetProxyAll() + }), // Use proxyless if ProxyAll is not set ) if err != nil { return err diff --git a/hit_proxy.bash b/hit_proxy.bash index 709ab2fbf..b0032c038 100755 --- a/hit_proxy.bash +++ b/hit_proxy.bash @@ -11,10 +11,13 @@ TMPDIR="${TMP:-/tmp}/hit_lc_proxy/$PROXY" OUTFILE="$TMPDIR/user.conf" rm -rf "$TMPDIR" -mkdir -p "$TMPDIR" +mkdir -pv "$TMPDIR" echo "Generating config for ${PROXY} in ${OUTFILE}..." -CONFIG=$($LANTERN_CLOUD/bin/lc route dump-config $PROXY) +CONFIG=$($LANTERN_CLOUD/bin/lc route dump-config $PROXY) || { + echo "Failed to fetch proxy configuration from lantern-cloud. On Tailnet?" + exit 1 +} # wrap the proxy config to match the format expected by flashlight. # [ConfigResponse (getlantern/flashlight/apipb/types.proto)] OUTPUT="{\"country\": \"US\",\"proxy\":{\"proxies\":[${CONFIG}]}}" diff --git a/hit_track.bash b/hit_track.bash index 068a57e53..220ce2fe1 100755 --- a/hit_track.bash +++ b/hit_track.bash @@ -8,7 +8,7 @@ echo "Fetching all proxies for "$@"" # First check for all proxies in a temporary directory from a prior run, and use them # if they exist. If not, fetch them from the lantern-cloud. -TMPDIR="${TMP:-/tmp}/hit_lc_proxy" +TMPDIR="${TMP:-/tmp}/hit_track" mkdir -pv "$TMPDIR" OUTFILE="$TMPDIR/"$@"_all_proxies.txt" # If the OUTFILE is older than 1 hour, delete it to force a refresh. @@ -22,7 +22,7 @@ if [ -f "$OUTFILE" ]; then else echo "No cached proxies found. Fetching from lantern-cloud..." $LANTERN_CLOUD/bin/lc routes list -T "$@" > "$OUTFILE" || { - echo "Failed to fetch proxies from lantern-cloud." + echo "Failed to fetch proxies from lantern-cloud. Are you running Tailscale?" exit 1 } ALLPROXIES=$(cat "$OUTFILE") diff --git a/internalsdk/android.go b/internalsdk/android.go index 00a0f8867..c2fd74cc8 100644 --- a/internalsdk/android.go +++ b/internalsdk/android.go @@ -120,6 +120,7 @@ type Session interface { // used to implement GetInternalHeaders() map[string]string // Should return a JSON encoded map[string]string {"key":"val","key2":"val", ...} SerializedInternalHeaders() (string, error) + ProxylessEnabled() (bool, error) } // PanickingSession wraps the Session interface but panics instead of returning errors @@ -239,6 +240,15 @@ func (s *panickingSessionImpl) SplitTunnelingEnabled() bool { panicIfNecessary(err) return result } + +func (s *panickingSessionImpl) ProxylessEnabled() (bool, error) { + result, err := s.wrapped.ProxylessEnabled() + if err != nil { + panicIfNecessary(err) + } + return result, nil +} + func (s *panickingSessionImpl) ChatEnable() bool { return s.wrapped.ChatEnable() } @@ -582,6 +592,14 @@ func run(configDir, locale string, settings Settings, wrappedSession Session) { flashlight.WithOnSucceedingProxy(func() { session.SetOnSuccess(true) }), + flashlight.WithUseProxyless(func() bool { + proxyless, err := session.ProxylessEnabled() + if err != nil { + log.Errorf("Error checking if proxyless is enabled: %v", err) + return false + } + return proxyless + }), ) if err != nil { log.Fatalf("failed to start flashlight: %v", err) diff --git a/internalsdk/android_test.go b/internalsdk/android_test.go index b636af8ea..22331cec4 100644 --- a/internalsdk/android_test.go +++ b/internalsdk/android_test.go @@ -86,6 +86,7 @@ func (c testSession) SetShowAppOpenAds(enabled bool) {} func (c testSession) SetHasConfigFetched(enabled bool) {} func (c testSession) SetHasProxyFetched(enabled bool) {} func (c testSession) ChatEnable() bool { return false } +func (c testSession) ProxylessEnabled() (bool, error) { return false, nil } func (c testSession) SetOnSuccess(enabled bool) { if !enabled { diff --git a/internalsdk/ios/ios.go b/internalsdk/ios/ios.go index 93a8ad766..1ac51c2e4 100644 --- a/internalsdk/ios/ios.go +++ b/internalsdk/ios/ios.go @@ -154,8 +154,9 @@ type iosClient struct { started time.Time bandwidthTracker BandwidthTracker statsTracker StatsTracker - dialer dialer.Dialer + dialer *protectedDialer tracker stats.Tracker + useProxyless func() bool } func Client(packetsOut Writer, udpDialer UDPDialer, memChecker MemChecker, configDir string, mtu int, @@ -178,8 +179,13 @@ func Client(packetsOut Writer, udpDialer UDPDialer, memChecker MemChecker, confi started: time.Now(), bandwidthTracker: bandwidthTracker, statsTracker: statsTracker, - dialer: dialer.NewProxylessDialer(), - tracker: stats.NewTracker(), + dialer: &protectedDialer{ + // This starts out as a purely proxyless dialer until we have + // proxies to use (either loaded from disk or fetched from the + // server). + dialer: dialer.NewProxylessDialer(), + }, + tracker: stats.NewTracker(), } optimizeMemoryUsage(&c.memoryAvailable) go c.gcPeriodically() @@ -227,7 +233,7 @@ func (c *iosClient) start() (ClientWriter, error) { return nil, errors.New("Unable to start dnsgrab: %v", err) } - c.tcpHandler = newProxiedTCPHandler(c, c.dialer, grabber) + c.tcpHandler = newProxiedTCPHandler(c, c.dialer.Get, grabber) c.udpHandler = newDirectUDPHandler(c, c.udpDialer, grabber, c.capturedDNSHost) ipStack := tun2socks.NewLWIPStack() @@ -257,21 +263,32 @@ func (c *iosClient) reconfigure() { } func (c *iosClient) onDialers(dialers []dialer.ProxyDialer) { - c.dialer.OnOptions(&dialer.Options{ - Dialers: dialers, - OnSuccess: func(pd dialer.ProxyDialer) { - c.tracker.SetHasSucceedingProxy(true) - countryCode, country, city := pd.Location() - previousStats := c.tracker.Latest() - if previousStats.CountryCode == "" || previousStats.CountryCode != countryCode { - c.tracker.SetActiveProxyLocation( - city, - country, - countryCode, - ) - } + newDialer := c.dialer.Get().OnOptions( + &dialer.Options{ + Dialers: dialers, + OnError: func(err error, hasSucceeding bool) { + log.Errorf("Error in dialer: %v", err) + }, + OnSuccess: func(d dialer.ProxyDialer) { + c.tracker.SetHasSucceedingProxy(true) + countryCode, country, city := d.Location() + previousStats := c.tracker.Latest() + if previousStats.CountryCode == "" || previousStats.CountryCode != countryCode { + c.tracker.SetActiveProxyLocation( + city, + country, + countryCode, + ) + } + }, + BanditDir: filepath.Join(c.configDir, "bandit"), + OnNewDialer: func(dialer dialer.Dialer) { + c.dialer.set(dialer) + }, + UseProxyless: c.useProxyless, }, - }) + ) + c.dialer.set(newDialer) } func bandwidthUpdates(bt BandwidthTracker) { @@ -358,3 +375,22 @@ func userConfigFor(userID int, proToken, deviceID string) *UserConfig { ), } } + +// protectedDialer protects a dialer.Dialer with a RWMutex. We can't use an atomic.Value here +// because dialer.Dialer is an interface. +type protectedDialer struct { + sync.RWMutex + dialer dialer.Dialer +} + +func (pd *protectedDialer) Get() dialer.Dialer { + pd.RLock() + defer pd.RUnlock() + return pd.dialer +} + +func (pd *protectedDialer) set(dialer dialer.Dialer) { + pd.Lock() + defer pd.Unlock() + pd.dialer = dialer +} diff --git a/internalsdk/ios/tcp.go b/internalsdk/ios/tcp.go index 8ecb77602..aa0f6dcda 100644 --- a/internalsdk/ios/tcp.go +++ b/internalsdk/ios/tcp.go @@ -38,9 +38,9 @@ type proxiedTCPHandler struct { mx sync.RWMutex } -func newProxiedTCPHandler(c *iosClient, dialer dialer.Dialer, grabber dnsgrab.Server) *proxiedTCPHandler { +func newProxiedTCPHandler(c *iosClient, dialer func() dialer.Dialer, grabber dnsgrab.Server) *proxiedTCPHandler { result := &proxiedTCPHandler{ - dialOut: dialer.DialContext, + dialOut: dialer().DialContext, client: c, grabber: grabber, mtu: c.mtu, diff --git a/internalsdk/session_model.go b/internalsdk/session_model.go index ebe01c4bd..628979621 100644 --- a/internalsdk/session_model.go +++ b/internalsdk/session_model.go @@ -80,6 +80,7 @@ const ( pathCurrencyCode = "currency_Code" pathReplicaAddr = "replicaAddr" pathSplitTunneling = "/splitTunneling" + pathProxyless = "/proxyless" pathLang = "lang" pathAcceptedTermsVersion = "accepted_terms_version" pathAdsEnabled = "adsEnabled" @@ -589,6 +590,13 @@ func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (inter return nil, err } return true, nil + case "setProxyless": + proxyless := arguments.Get("on").Bool() + err := m.setProxyless(proxyless) + if err != nil { + return nil, err + } + return true, nil case "denyAppAccess": appName := arguments.Get("packageName").String() @@ -850,6 +858,12 @@ func (session *SessionModel) setSplitTunneling(tunneling bool) error { }) } +func (session *SessionModel) setProxyless(proxyless bool) error { + return pathdb.Mutate(session.db, func(tx pathdb.TX) error { + return pathdb.Put(tx, pathProxyless, proxyless, "") + }) +} + func (session *SessionModel) paymentMethods() error { plans, err := session.proClient.PaymentMethodsV4(context.Background()) if err != nil { @@ -1310,6 +1324,10 @@ func (m *SessionModel) SplitTunnelingEnabled() (bool, error) { return pathdb.Get[bool](m.db, pathSplitTunneling) } +func (m *SessionModel) ProxylessEnabled() (bool, error) { + return pathdb.Get[bool](m.db, pathProxyless) +} + func (m *SessionModel) SetShowInterstitialAds(adsEnable bool) { log.Debugf("SetShowInterstitialAds %v", adsEnable) err := pathdb.Mutate(m.db, func(tx pathdb.TX) error { diff --git a/lib/common/ui/image_paths.dart b/lib/common/ui/image_paths.dart index 582363e1c..e9c926e6e 100644 --- a/lib/common/ui/image_paths.dart +++ b/lib/common/ui/image_paths.dart @@ -25,6 +25,7 @@ class ImagePaths { static const email = 'assets/images/email.svg'; static const translate = 'assets/images/translate.svg'; static const update = 'assets/images/update.svg'; + static const proxyall = 'assets/images/proxyall.svg'; static const mastercard = 'assets/images/mastercard.svg'; static const visa = 'assets/images/visa.svg'; static const unionpay = 'assets/images/unionpay.svg'; diff --git a/lib/features/account/settings.dart b/lib/features/account/settings.dart index eaee7c66e..687e25eb4 100644 --- a/lib/features/account/settings.dart +++ b/lib/features/account/settings.dart @@ -1,4 +1,5 @@ import 'package:intl/intl.dart'; +import 'package:flutter/cupertino.dart'; import 'package:lantern/core/app/app_loading_dialog.dart'; import 'package:lantern/core/localization/localization_constants.dart'; import 'package:lantern/core/utils/common.dart'; @@ -20,6 +21,15 @@ class Settings extends StatelessWidget { ); } + void openInfoProxyless(BuildContext context) { + CDialog.showInfo( + context, + title: 'proxyless'.i18n, + description: 'description_proxyless_dialog'.i18n, + iconPath: ImagePaths.key, + ); + } + void changeLanguage(BuildContext context) async => await context.pushRoute(Language()); @@ -80,14 +90,14 @@ class Settings extends StatelessWidget { ), ), ), - mirrorLTR(context: context, child: const ContinueArrow()) + mirrorLTR(context: context, child: const ContinueArrow()), ], ), ListItemFactory.settingsItem( icon: ImagePaths.update, content: 'check_for_updates'.i18n, trailingArray: [ - mirrorLTR(context: context, child: const ContinueArrow()) + mirrorLTR(context: context, child: const ContinueArrow()), ], onTap: () => checkForUpdateTap(context), ), @@ -103,7 +113,7 @@ class Settings extends StatelessWidget { mirrorLTR( context: context, child: const ContinueArrow(), - ) + ), ], onTap: () => context.pushRoute(BlockedUsers()), ) @@ -144,7 +154,52 @@ class Settings extends StatelessWidget { mirrorLTR( context: context, child: const ContinueArrow(), - ) + ), + ], + ), + ), + if (Platform.isAndroid) + sessionModel.proxyless( + (BuildContext context, bool proxylessEnabled, Widget? child) => + ListItemFactory.settingsItem( + icon: ImagePaths.proxyall, + content: CInkWell( + onTap: () => openInfoProxyless(context), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: CText( + 'proxyless'.i18n, + softWrap: false, + style: tsSubtitle1.short, + ), + ), + const Padding( + padding: EdgeInsetsDirectional.only(start: 4.0), + child: CAssetImage( + key: ValueKey('proxyless_icon'), + path: ImagePaths.info, + size: 12, + ), + ), + ], + ), + ), + trailingArray: [ + SizedBox( + width: 44.0, + height: 24.0, + child: CupertinoSwitch( + value: proxylessEnabled, + activeTrackColor: CupertinoColors.activeGreen, + onChanged: (bool? value) { + var newValue = value ?? false; + sessionModel.setProxyless(newValue); + }, + ), + ), ], ), ), diff --git a/lib/features/account/split_tunneling.dart b/lib/features/account/split_tunneling.dart index c26c25b86..7afe99ca7 100644 --- a/lib/features/account/split_tunneling.dart +++ b/lib/features/account/split_tunneling.dart @@ -50,7 +50,7 @@ class _SplitTunnelingState extends State { height: 24.0, child: CupertinoSwitch( value: splitTunnelingEnabled, - activeColor: CupertinoColors.activeGreen, + activeTrackColor: CupertinoColors.activeGreen, onChanged: (bool? value) { var newValue = value ?? false; sessionModel.setSplitTunneling(newValue); diff --git a/lib/features/home/session_model.dart b/lib/features/home/session_model.dart index e7f521b30..a46dcb776 100644 --- a/lib/features/home/session_model.dart +++ b/lib/features/home/session_model.dart @@ -37,6 +37,7 @@ class SessionModel extends Model { late ValueNotifier proxyAvailable; late ValueNotifier country; late ValueNotifier proxyAllNotifier; + late ValueNotifier proxylessNotifier; late ValueNotifier serverInfoNotifier; late ValueNotifier userEmail; late ValueNotifier linkingCodeNotifier; @@ -91,6 +92,7 @@ class SessionModel extends Model { isTestPlayVersion = ValueNotifier(false); } if (Platform.isAndroid) { + proxylessNotifier = ValueNotifier(true); // By default when user starts the app we need to make sure that screenshot is disabled // if user goes to chat then screenshot will be disabled enableScreenShot(); @@ -961,7 +963,7 @@ class SessionModel extends Model { Widget splitTunneling(ValueWidgetBuilder builder) { if (isMobile()) { return subscribedSingleValueBuilder('/splitTunneling', - builder: builder, defaultValue: false); + builder: builder, defaultValue: false,); } return ValueListenableBuilder( valueListenable: configNotifier, @@ -987,6 +989,27 @@ class SessionModel extends Model { ); } + Widget proxyless(ValueWidgetBuilder builder) { + if (Platform.isAndroid) { + return subscribedSingleValueBuilder('/proxyless', + builder: builder, defaultValue: true,); + } + return ValueListenableBuilder( + valueListenable: proxylessNotifier, + builder: builder, + ); + } + + Future setProxyless(bool on) async { + if (Platform.isAndroid) { + unawaited( + methodChannel.invokeMethod('setProxyless', { + 'on': on, + }), + ); + } + } + Widget appsData({ required ValueWidgetBuilder>> builder, }) {