Skip to content
Merged
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
8 changes: 2 additions & 6 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -2055,9 +2055,9 @@ msgstr "Tap Edit, then Add Widget."
msgid "widget_step_3"
msgstr "Search for Lantern, choose a size, and tap add widget."

# Advanced stats: low-emphasis link at the bottom of the Unbounded status
# Advanced stats: low-emphasis link at the bottom of the Action Mode status
# card and the title of the bottom sheet it opens. "Data relayed" counts
# bytes this device forwarded for the people it helps over Unbounded.
# bytes this device forwarded for the people it helps, in either sharing mode.
msgid "advanced_stats"
msgstr "Advanced stats"

Expand Down Expand Up @@ -2091,10 +2091,6 @@ msgstr "1 person"
msgid "advanced_stats_people"
msgstr "%s people"

# Shown instead of data figures while sharing via Share My Connection.
msgid "advanced_stats_smc_note"
msgstr "Data stats are available only when sharing via Unbounded."

# Sharing-mode names. "Direct connection" is Share My Connection, where people
# connect straight to this device through a port on the user's router.
# "WebRTC" is the Unbounded relay used when that port can't be opened; keep
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ go 1.26.9

// replace github.com/sagernet/sing => github.com/getlantern/sing v0.7.18-lantern

replace github.com/sagernet/sing-box => github.com/getlantern/sing-box-minimal v1.13.20-0.20261017021932-5a256d757969
replace github.com/sagernet/sing-box => github.com/getlantern/sing-box-minimal v1.13.20-0.20261019115333-624aa48e0252

// Backport gVisor reader shutdown fix (upstream be4ce8d320a5).
replace github.com/sagernet/sing-tun => github.com/getlantern/sing-tun v0.0.0-20261017114032-5448fbd8b70b
Expand All @@ -34,7 +34,7 @@ require (
github.com/alecthomas/assert/v2 v2.3.0
github.com/getlantern/common v1.2.1-0.20260910154003-08e030318f24
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
github.com/getlantern/radiance v0.0.0-20261019190429-a0b724700121
github.com/getlantern/radiance v0.0.0-20261019203946-79f0a5bd87ab
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/sing-box v1.13.19
github.com/sagernet/sing-tun v0.8.12-0.20260810140523-7c73233bd0fb
Expand Down Expand Up @@ -180,12 +180,12 @@ require (
github.com/getlantern/domainfront v0.0.0-20260915172349-4984689a8667 // indirect
github.com/getlantern/keepcurrent v0.0.0-20261009194109-22f33eaeac6e // indirect
github.com/getlantern/kindling v0.0.0-20261014210827-0f4a89dbe1ee // indirect
github.com/getlantern/lantern-box v0.0.154 // indirect
github.com/getlantern/lantern-box v0.0.166 // indirect
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 // indirect
github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 // indirect
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 // indirect
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b // indirect
github.com/getlantern/samizdat v0.0.3-0.20260819153658-ebc74116a064 // indirect
github.com/getlantern/samizdat v0.0.3-0.20261019142234-18bd41c612f1 // indirect
github.com/getlantern/semconv v0.0.0-20261011133221-f5e57ec9c1c5 // indirect
github.com/getlantern/soar v0.2.3 // indirect
github.com/getlantern/twiddle v0.0.0-20260914204124-f698a0d67d2f // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ github.com/getlantern/keepcurrent v0.0.0-20261009194109-22f33eaeac6e h1:iJfNXnhg
github.com/getlantern/keepcurrent v0.0.0-20261009194109-22f33eaeac6e/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w=
github.com/getlantern/kindling v0.0.0-20261014210827-0f4a89dbe1ee h1:FXod6NjAD/KDl6Aj5mP1MVCLf2AAJv8qU4kmUrYcMZk=
github.com/getlantern/kindling v0.0.0-20261014210827-0f4a89dbe1ee/go.mod h1:98jp1v8UyXJRGn8slpvzNz2tKnyzvSqtlVYkQ+kYapQ=
github.com/getlantern/lantern-box v0.0.154 h1:EPQjjBqNC1gbDb2SlpbWg1wo7pNM/TccZ/UV+yhVQjI=
github.com/getlantern/lantern-box v0.0.154/go.mod h1:gy3dGHMTQXaFi5DYIJJrMNT3WS2E7GOxuZl2jebcbuw=
github.com/getlantern/lantern-box v0.0.166 h1:Z2qy+5Yvd6s0pQBGlqP/bwRllkynK5Clf94HcHzy13Q=
github.com/getlantern/lantern-box v0.0.166/go.mod h1:RK7qWraZxaDGC6iJYJV6KEUyQvOy2njF5iEENZ1bl/k=
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 h1:6seyD2f9tz2am0YQd/Qn+q7LFiiQgnmxgwWFnVceGZw=
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9/go.mod h1:s0VKrlJf/z+M0U8IKHFL2hfuflocRw3SINmMacrTlMA=
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 h1:grfGavAUp2E9w9ZoJuM3FyWyQ0sCJ64V4ZMKtZKRqTc=
Expand All @@ -262,14 +262,14 @@ 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/publicip v0.0.0-20260328175246-2c460fe80c6b h1:gMYJzEhLrmIqQ+JnjiYNm+UyUDalK3WUmVyecFwmV5g=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b/go.mod h1:NpfXdK4ldEKkjQ4P1R+DBF4ua5VFOlxmgHROTnYrApg=
github.com/getlantern/radiance v0.0.0-20261019190429-a0b724700121 h1:N/s95WcdaC+//C8uL922P1VTp7RxXU1X9p74BlF2SN8=
github.com/getlantern/radiance v0.0.0-20261019190429-a0b724700121/go.mod h1:c0XV/0cz1Q6n/d5fYdl3z2QQE6ZeJa6VfZAkHHaFlSQ=
github.com/getlantern/samizdat v0.0.3-0.20260819153658-ebc74116a064 h1:NhCyQAWGEWjok/jg4gRD6fzCZvyOQy/4dVSFYhzU5xk=
github.com/getlantern/samizdat v0.0.3-0.20260819153658-ebc74116a064/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/radiance v0.0.0-20261019203946-79f0a5bd87ab h1:onIu3VaXT4+luEADvJIYcwEeeltpGLuiVP2RX60yLuI=
github.com/getlantern/radiance v0.0.0-20261019203946-79f0a5bd87ab/go.mod h1:5BAJjWE5MgXtMzxTlbl0Fcq6FQlTTUXb2bO7jbA8/e8=
github.com/getlantern/samizdat v0.0.3-0.20261019142234-18bd41c612f1 h1:Q1AHHHTd3eiHXn0HR0mltq6/4cGnv9cQFKYmoRuXWQo=
github.com/getlantern/samizdat v0.0.3-0.20261019142234-18bd41c612f1/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/semconv v0.0.0-20261011133221-f5e57ec9c1c5 h1:oF5kQTI7RwMvjMnr4WhmQ1beAjSMl1zoQ4I7Cx5nm6I=
github.com/getlantern/semconv v0.0.0-20261011133221-f5e57ec9c1c5/go.mod h1:GkPT5P9JoOTIRXRmFWxYgu1hhXgTFFTNc2hoG7WQc3g=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261017021932-5a256d757969 h1:xDlY1aEy5FILPxlncY5ciHQQY5FdyiNNZ2RC86ACfsw=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261017021932-5a256d757969/go.mod h1:4E8WvF7Vi60N1NtPw1CeG994J/yiJZZkty5dKKwG+V0=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261019115333-624aa48e0252 h1:p6tbxiX1SHqo1FwO9l1uYerSSaMLt/R78QtLIScpP54=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261019115333-624aa48e0252/go.mod h1:4E8WvF7Vi60N1NtPw1CeG994J/yiJZZkty5dKKwG+V0=
github.com/getlantern/sing-tun v0.0.0-20261017114032-5448fbd8b70b h1:e5AdmeQNZZlDB0t7RtglDFZWBULfDCFzR7vkuPwEvbs=
github.com/getlantern/sing-tun v0.0.0-20261017114032-5448fbd8b70b/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
github.com/getlantern/soar v0.2.3 h1:BAahWy3b4J6wtrAJD7WgoO2O+x2Q3T6KN2r0E3ogNgs=
Expand Down
1 change: 1 addition & 0 deletions lantern-core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ func (lc *LanternCore) initialize(opts *utils.Opts, eventEmitter utils.FlutterEv
go lc.listenDataCapEvents()
go lc.listenPeerConnectionEvents()
go lc.listenUnboundedSnapshots()
go lc.listenPeerSnapshots()
go lc.listenPeerStatusEvents()
go lc.listenUserMessageAvailability()
go lc.fetchUserDataIfNeeded()
Expand Down
28 changes: 22 additions & 6 deletions lantern-core/unbounded_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,36 @@ func (lc *LanternCore) listenUnboundedSnapshots() {
}

func pollUnboundedSnapshots(ctx context.Context, ticks <-chan time.Time, read func(context.Context) (unbounded.Snapshot, error), notify func(string, string)) {
unavailable := false
pollSnapshots(ctx, ticks, read, notify, "unbounded-snapshot", "unbounded-unavailable")
}

// listenPeerSnapshots polls the bytes direct sharing has relayed, which the
// app folds the same way as Unbounded's. Unbounded's poller already reports
// when the backend is unreachable, so this one sends no unavailable event.
func (lc *LanternCore) listenPeerSnapshots() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
pollSnapshots(lc.ctx, ticker.C, lc.client.PeerRelaySnapshot, lc.notifyFlutter, "peer-snapshot", "")
}

// pollSnapshots reads a snapshot on every tick and sends it to Flutter as
// event. After a failed read it sends unavailable once, when set, until a read
// succeeds again.
func pollSnapshots[T any](ctx context.Context, ticks <-chan time.Time, read func(context.Context) (T, error), notify func(string, string), event, unavailable string) {
reportedUnavailable := false
for {
requestCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
snapshot, err := read(requestCtx)
cancel()
if err == nil {
unavailable = false
reportedUnavailable = false
if data, err := json.Marshal(snapshot); err == nil {
notify("unbounded-snapshot", string(data))
notify(event, string(data))
}
}
if err != nil && !unavailable && ctx.Err() == nil {
unavailable = true
notify("unbounded-unavailable", "{}")
if err != nil && unavailable != "" && !reportedUnavailable && ctx.Err() == nil {
reportedUnavailable = true
notify(unavailable, "{}")
}
select {
case <-ctx.Done():
Expand Down
28 changes: 28 additions & 0 deletions lantern-core/unbounded_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/getlantern/radiance/peer"
"github.com/getlantern/radiance/unbounded"
)

Expand Down Expand Up @@ -40,6 +41,33 @@ func TestUnboundedUnavailableIsEmittedOncePerOutage(t *testing.T) {
}
}

// The peer poller has no unavailable event: failed reads are skipped, and
// each success is sent under the peer event.
func TestPeerSnapshotsSkipFailedReads(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ticks := make(chan time.Time, 2)
for range 2 {
ticks <- time.Now()
}
calls := 0
var events []string
pollSnapshots(ctx, ticks, func(context.Context) (peer.Snapshot, error) {
calls++
if calls == 2 {
return peer.Snapshot{Epoch: "run-1", BytesDown: 9}, nil
}
return peer.Snapshot{}, errors.New("offline")
}, func(name, data string) {
events = append(events, name+" "+data)
cancel()
}, "peer-snapshot", "")
want := []string{`peer-snapshot {"epoch":"run-1","running":false,"peers":null,"bytesDown":9,"bytesUp":0,"peerBytes":null}`}
if !reflect.DeepEqual(events, want) {
t.Fatalf("events = %v, want %v", events, want)
}
}

func TestUnboundedCancellationDoesNotReportOutage(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
Expand Down
19 changes: 19 additions & 0 deletions lib/core/models/app_setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class AppSetting {
final String unboundedRelayEpoch;
final int unboundedRelayMarkDown;
final int unboundedRelayMarkUp;
// The same for direct sharing (SmC), whose snapshots carry their own epoch.
final String peerRelayEpoch;
final int peerRelayMarkDown;
final int peerRelayMarkUp;

const AppSetting({
this.themeMode = 'system',
Expand All @@ -56,6 +60,9 @@ class AppSetting {
this.unboundedRelayEpoch = '',
this.unboundedRelayMarkDown = 0,
this.unboundedRelayMarkUp = 0,
this.peerRelayEpoch = '',
this.peerRelayMarkDown = 0,
this.peerRelayMarkUp = 0,
});

/// Whether the app points at the staging backend. The notifier writes
Expand All @@ -82,6 +89,9 @@ class AppSetting {
String? unboundedRelayEpoch,
int? unboundedRelayMarkDown,
int? unboundedRelayMarkUp,
String? peerRelayEpoch,
int? peerRelayMarkDown,
int? peerRelayMarkUp,
}) {
return AppSetting(
locale: newLocale ?? locale,
Expand All @@ -106,6 +116,9 @@ class AppSetting {
unboundedRelayMarkDown:
unboundedRelayMarkDown ?? this.unboundedRelayMarkDown,
unboundedRelayMarkUp: unboundedRelayMarkUp ?? this.unboundedRelayMarkUp,
peerRelayEpoch: peerRelayEpoch ?? this.peerRelayEpoch,
peerRelayMarkDown: peerRelayMarkDown ?? this.peerRelayMarkDown,
peerRelayMarkUp: peerRelayMarkUp ?? this.peerRelayMarkUp,
);
}

Expand All @@ -130,6 +143,9 @@ class AppSetting {
'unboundedRelayEpoch': unboundedRelayEpoch,
'unboundedRelayMarkDown': unboundedRelayMarkDown,
'unboundedRelayMarkUp': unboundedRelayMarkUp,
'peerRelayEpoch': peerRelayEpoch,
'peerRelayMarkDown': peerRelayMarkDown,
'peerRelayMarkUp': peerRelayMarkUp,
};

factory AppSetting.fromJson(Map<String, dynamic> json) => AppSetting(
Expand Down Expand Up @@ -160,5 +176,8 @@ class AppSetting {
(json['unboundedRelayMarkDown'] as num?)?.toInt() ?? 0,
unboundedRelayMarkUp:
(json['unboundedRelayMarkUp'] as num?)?.toInt() ?? 0,
peerRelayEpoch: (json['peerRelayEpoch'] ?? '').toString(),
peerRelayMarkDown: (json['peerRelayMarkDown'] as num?)?.toInt() ?? 0,
peerRelayMarkUp: (json['peerRelayMarkUp'] as num?)?.toInt() ?? 0,
);
}
58 changes: 23 additions & 35 deletions lib/features/action_mode/advanced_stats_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:lantern/core/common/app_text_styles.dart';
import 'package:lantern/core/common/common.dart';
import 'package:lantern/core/models/relay_stats.dart';
import 'package:lantern/core/models/share_state.dart';
import 'package:lantern/core/widgets/info_row.dart';
import 'package:lantern/core/widgets/section_label.dart';
import 'package:lantern/features/action_mode/provider/share_notifier.dart';

Expand All @@ -24,8 +23,7 @@ void showAdvancedStatsSheet(BuildContext context) {
);
}

/// Data-relay figures for Unbounded volunteers. Byte counters only exist for
/// Unbounded, so SmC sessions get a note in place of live figures.
/// Data-relay figures for volunteers, in either sharing mode.
class AdvancedStatsView extends ConsumerWidget {
const AdvancedStatsView({super.key, this.scrollController});
final ScrollController? scrollController;
Expand All @@ -36,8 +34,7 @@ class AdvancedStatsView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(shareProvider);
final relay = state.relay;
final smc = state.mode == ShareMode.smc;
final live = !smc && relay.available;
final live = relay.available;
final countries = relay.topCountries(topCountryCount);
final divider = Divider(height: 1, color: context.borderDefault);

Expand All @@ -46,13 +43,6 @@ class AdvancedStatsView extends ConsumerWidget {
controller: scrollController,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 40),
children: [
if (smc) ...[
InfoRow(
key: const Key('advanced-stats.smc-note'),
text: 'advanced_stats_smc_note'.i18n,
),
const SizedBox(height: 16),
],
AppCard(
child: Column(
children: [
Expand All @@ -69,29 +59,27 @@ class AdvancedStatsView extends ConsumerWidget {
: null,
),
divider,
if (!smc) ...[
_StatRow(
label: 'advanced_stats_current_speed'.i18n,
value: live
? _directions(
'${relay.speedDown.round().siBytes}/s',
'${relay.speedUp.round().siBytes}/s',
)
: '—',
),
divider,
_StatRow(
label: 'advanced_stats_session'.i18n,
value: live ? relay.sessionBytes.siBytes : '—',
detail: live
? _directions(
relay.sessionDown.siBytes,
relay.sessionUp.siBytes,
)
: null,
),
divider,
],
_StatRow(
label: 'advanced_stats_current_speed'.i18n,
value: live
? _directions(
'${relay.speedDown.round().siBytes}/s',
'${relay.speedUp.round().siBytes}/s',
)
: '—',
),
divider,
_StatRow(
label: 'advanced_stats_session'.i18n,
value: live ? relay.sessionBytes.siBytes : '—',
detail: live
? _directions(
relay.sessionDown.siBytes,
relay.sessionUp.siBytes,
)
: null,
),
divider,
_StatRow(
label: 'advanced_stats_all_time'.i18n,
value: relay.lifetimeBytes.siBytes,
Expand Down
Loading
Loading