Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a840ddf
fronted/scanner: discover working CDN fronts per-client
myleshorton May 19, 2026
3219e54
fronted/scanner: Service orchestrator + persistent cache
myleshorton May 19, 2026
cea6449
kindling/meek: Provider wiring scanner output into meek outbound
myleshorton May 19, 2026
96b745f
fronted/scanner: raw-range-primary discovery by default
myleshorton May 19, 2026
f19761d
fronted/scanner: force https scheme on probe requests
myleshorton May 19, 2026
4e74a93
fronted/scanner: CloudFront probes — no SNI, verify InnerHost
myleshorton May 19, 2026
fe676af
vpn,kindling/meek: inject meek outbound into running sing-box
myleshorton May 19, 2026
7a9ceb0
meek: add DefaultURL constant for production endpoint
myleshorton May 23, 2026
46789b7
iran: add LikelyIran device-local heuristic for meek activation
myleshorton May 24, 2026
3fb4bee
backend: wire MCC + activate meek scanner when likelyIran
myleshorton May 24, 2026
7c0423a
backend: add RADIANCE_FORCE_MEEK_ONLY env override for local testing
myleshorton May 24, 2026
b2e8c0a
cmd/meek-client-smoke: standalone meek-client smoke test
myleshorton May 24, 2026
e2c2e02
cmd/meek-client-smoke: drop goroutine workaround for SetReadDeadline
myleshorton May 24, 2026
6f5fbdb
fronted/scanner: mix in named SNIs for Akamai candidates
myleshorton May 25, 2026
3740edc
kindling/fronted: sync embedded fronted.yaml.gz with Keith's working …
myleshorton May 25, 2026
04754c7
kindling/fronted: fetch + embed from getlantern/domainfront
myleshorton May 26, 2026
5b8be0e
scanner: include FrontingSNIs.ArbitrarySNIs in SNIsForProvider
myleshorton May 26, 2026
f554219
bump getlantern/domainfront to FrontingSNIs yaml-tag fix
myleshorton May 26, 2026
65f353f
cmd/meek-client-smoke: lower AkamaiSample to production default
myleshorton May 26, 2026
3fc145c
scanner: publish working fronts incrementally during scan
myleshorton May 26, 2026
a788c4f
bump domainfront to merged main (FrontingSNIs yaml-tag fix)
myleshorton May 26, 2026
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
9 changes: 5 additions & 4 deletions .github/workflows/refresh-fronted-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ name: Refresh embedded fronted.yaml.gz

# kindling/fronted/fronted.yaml.gz is the last-resort fallback config used
# when both the live fetch and on-disk cache miss. The canonical copy lives
# in getlantern/fronted and is updated daily by an external pipeline. Mirror
# that here so a fresh install bootstrap doesn't fall back to a stale copy.
# in getlantern/domainfront (getlantern/fronted is deprecated) and is
# updated daily by an external pipeline. Mirror that here so a fresh
# install bootstrap doesn't fall back to a stale copy.

on:
schedule:
Expand Down Expand Up @@ -32,7 +33,7 @@ jobs:
run: |
curl -fsSL \
-o kindling/fronted/fronted.yaml.gz.new \
https://raw.githubusercontent.com/getlantern/fronted/refs/heads/main/fronted.yaml.gz
https://raw.githubusercontent.com/getlantern/domainfront/refs/heads/main/fronted.yaml.gz
gzip -t kindling/fronted/fronted.yaml.gz.new
test -s kindling/fronted/fronted.yaml.gz.new

Expand All @@ -59,7 +60,7 @@ jobs:
title: "fronted: refresh embedded fronted.yaml.gz"
body: |
Automated daily refresh of `kindling/fronted/fronted.yaml.gz`
from `getlantern/fronted@main`. Safe to merge once CI passes.
from `getlantern/domainfront@main`. Safe to merge once CI passes.
branch: chore/refresh-fronted-config
delete-branch: true
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
Expand Down
80 changes: 80 additions & 0 deletions backend/radiance.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"

"time"

Expand All @@ -34,6 +35,9 @@ import (
"github.com/getlantern/radiance/internal"
"github.com/getlantern/radiance/issue"
"github.com/getlantern/radiance/kindling"
"github.com/getlantern/radiance/kindling/fronted"
"github.com/getlantern/radiance/kindling/iran"
"github.com/getlantern/radiance/kindling/meek"
"github.com/getlantern/radiance/log"
"github.com/getlantern/radiance/servers"
"github.com/getlantern/radiance/telemetry"
Expand Down Expand Up @@ -77,6 +81,13 @@ type LocalBackend struct {

stopURLTestListener context.CancelFunc
urlTestMu sync.Mutex

// meekProvider is non-nil only when the device is classified as
// likely in Iran. getBoxOptions reads it atomically so a slow
// scanner startup can't block VPN connects: the meek outbound is
// absent from the first connect and present once the scanner
// populates it.
meekProvider atomic.Pointer[meek.Provider]
}

type Options struct {
Expand All @@ -87,6 +98,11 @@ type Options struct {
// this should be the platform device ID on mobile devices, desktop platforms will generate their
// own device ID and ignore this value
DeviceID string
// MCC is the network Mobile Country Code reported by the cellular
// stack (Android: first 3 chars of TelephonyManager.getNetworkOperator()).
// Empty on WiFi-only, between-tower, or platforms that don't expose it.
// Used to gate activation of the heavier meek transport.
MCC string
// User choice for telemetry consent
TelemetryConsent bool
PlatformInterface vpn.PlatformInterface
Expand Down Expand Up @@ -135,6 +151,7 @@ func NewLocalBackend(ctx context.Context, opts Options) (*LocalBackend, error) {
settings.Patch(settings.Settings{
settings.LocaleKey: opts.Locale,
settings.DeviceIDKey: platformDeviceID,
settings.MCCKey: opts.MCC,
settings.ConfigFetchDisabledKey: disableFetch,
settings.TelemetryKey: opts.TelemetryConsent,
})
Expand Down Expand Up @@ -205,6 +222,8 @@ func (r *LocalBackend) Start() {
}
}()

r.maybeStartMeek()

if settings.GetBool(settings.TelemetryKey) {
if err := r.startTelemetry(); err != nil {
slog.Error("Failed to start telemetry", "error", err)
Expand Down Expand Up @@ -696,6 +715,33 @@ func (r *LocalBackend) runURLTestListener(ctx context.Context, storage vpn.URLTe
}
}

func (r *LocalBackend) maybeStartMeek() {
force := env.GetBool(env.ForceMeekOnly)
if !force && !iran.LikelyIran(settings.GetString(settings.MCCKey), iran.LocalTZName()) {
return
}
go func() {
dataDir := settings.GetString(settings.DataPathKey)
cfg, err := fronted.LoadCachedConfig(dataDir)
if err != nil {
slog.Warn("meek: failed to load fronted config", "err", err)
return
}
p, err := meek.NewProvider(meek.ProviderConfig{
Config: cfg,
CacheFile: filepath.Join(dataDir, "meek_fronts_cache.json"),
})
if err != nil {
slog.Warn("meek: NewProvider failed", "err", err)
return
}
p.Start(r.ctx)
r.meekProvider.Store(p)
r.shutdownFuncs = append(r.shutdownFuncs, func() error { return p.Close() })
slog.Info("meek: scanner started", "forced", force)
}()
}

func (r *LocalBackend) flushURLTestResults(storage vpn.URLTestHistoryStorage) {
results := make(map[string]servers.URLTestResult)
for _, srv := range r.srvManager.AllServers() {
Expand Down Expand Up @@ -737,6 +783,9 @@ func (r *LocalBackend) ConnectVPN(tag string) error {
}

func (r *LocalBackend) getBoxOptions() vpn.BoxOptions {
if env.GetBool(env.ForceMeekOnly) {
return r.meekOnlyBoxOptions()
}
// ignore error, we can still connect with default options if config is not available for some reason
cfg, _ := r.confHandler.GetConfig()
bOptions := vpn.BoxOptions{
Expand Down Expand Up @@ -773,6 +822,37 @@ func (r *LocalBackend) getBoxOptions() vpn.BoxOptions {
if len(seed) > 0 {
bOptions.URLTestSeed = seed
}
if p := r.meekProvider.Load(); p != nil {
if out, ok := meek.BuildOutbound("meek-fronted", meek.DefaultURL, p.FrontSpecs(3)); ok {
bOptions.MeekOutbound = &out
}
}
return bOptions
}

// meekOnlyBoxOptions returns a stripped-down config in which meek is
// the sole outbound and InitialServer pins it. All API-provided
// outbounds, bandit configuration, and selector arms are omitted —
// VPN traffic must traverse meek or fail. Used when env.ForceMeekOnly
// is set (local testing). When the scanner hasn't produced fronts
// yet the meek outbound is absent; Connect will fail and the user
// retries after a few seconds.
func (r *LocalBackend) meekOnlyBoxOptions() vpn.BoxOptions {
bOptions := vpn.BoxOptions{
BasePath: settings.GetString(settings.DataPathKey),
}
p := r.meekProvider.Load()
if p == nil {
slog.Warn("meek-only mode: provider not yet ready, returning empty options")
return bOptions
}
out, ok := meek.BuildOutbound("meek-fronted", meek.DefaultURL, p.FrontSpecs(3))
if !ok {
slog.Warn("meek-only mode: scanner has no working fronts yet")
return bOptions
}
bOptions.MeekOutbound = &out
bOptions.InitialServer = out.Tag
return bOptions
}

Expand Down
Loading