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,
}) {