Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Available variables:
* `RADIANCE_USE_SOCKS_PROXY`: If set to `true`, replace the TUN with a SOCKS proxy for inbound connections.
* `RADIANCE_SOCKS_ADDRESS`: Specifies the address (`host:port`) for the SOCKS proxy to use for inbound connections.
* `RADIANCE_ENV`: Sets whether we're running in production or development mode. Set to `dev` for additional debugging output, such as the sing-box config actually in use. `prod` is the default.
* `RADIANCE_FEATURE_OVERRIDE`: Comma-separated list of feature flags to force-enable on the server side. If set, the value is sent as the `X-Lantern-Feature-Override` header on config requests in any environment, and it is recommended for testing/non-production use. For example, `RADIANCE_FEATURE_OVERRIDE=bandit_assignment` enables bandit-based proxy assignment during testing.


## Packages
Expand Down
14 changes: 11 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,17 @@ func (ch *ConfigHandler) fetchConfig() error {
locs[k] = *v
}
opts := servers.Options{
Outbounds: cfg.Options.Outbounds,
Endpoints: cfg.Options.Endpoints,
Locations: locs,
Outbounds: cfg.Options.Outbounds,
Endpoints: cfg.Options.Endpoints,
Locations: locs,
URLOverrides: cfg.BanditURLOverrides,
}
if len(cfg.BanditURLOverrides) > 0 {
slog.Info("Config includes bandit URL overrides",
"override_count", len(cfg.BanditURLOverrides),
"outbound_count", len(cfg.Options.Outbounds),
"endpoint_count", len(cfg.Options.Endpoints),
)
}
if err := ch.svrManager.SetServers(servers.SGLantern, opts); err != nil {
slog.Error("setting servers in manager", "error", err)
Expand Down
4 changes: 4 additions & 0 deletions config/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ func (f *fetcher) send(ctx context.Context, body io.Reader) ([]byte, error) {
slog.Info("Setting x-lantern-client-country header", "country", val)
req.Header.Set("x-lantern-client-country", val)
}
if val, exists := os.LookupEnv("RADIANCE_FEATURE_OVERRIDE"); exists && val != "" {
slog.Info("Setting X-Lantern-Feature-Override header", "features", val)
req.Header.Set("X-Lantern-Feature-Override", val)
}

// Add If-Modified-Since header to the request
// Note that on the first run, lastModified is zero, so the server will return the latest config.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
github.com/getlantern/fronted v0.0.0-20260319225233-cf2160f85053
github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae
github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171
github.com/getlantern/lantern-box v0.0.49
github.com/getlantern/lantern-box v0.0.50-0.20260323160457-f763bd127d8c
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535
github.com/getlantern/timezone v0.0.0-20210901200113-3f9de9d360c9
github.com/go-resty/resty/v2 v2.16.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae h1:NMq3K7h3
github.com/getlantern/keepcurrent v0.0.0-20260304213122-017d542145ae/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w=
github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171 h1:UEjX+Gg+T6oGVUbzHJ4JfLhlsIh8Wl8PmTXZYWGS43A=
github.com/getlantern/kindling v0.0.0-20260319225424-4736208dd171/go.mod h1:c5cFjpNrqX8wQ0PUE2blHrO7knAlRCVx3j1/G6zaVlY=
github.com/getlantern/lantern-box v0.0.49 h1:ZEurOeyceCkrNbWptrbEhjS5xVphZm1v8XFnISRo8C4=
github.com/getlantern/lantern-box v0.0.49/go.mod h1:Luj0rLyuokADHg2B+eXlAdxVXYO+T5Reeds+hKuQkZA=
github.com/getlantern/lantern-box v0.0.50-0.20260323160457-f763bd127d8c h1:4OhNZ95H/5EqGmuiMD/iDXnHgEsvbe4TafL94hl6YUo=
github.com/getlantern/lantern-box v0.0.50-0.20260323160457-f763bd127d8c/go.mod h1:Luj0rLyuokADHg2B+eXlAdxVXYO+T5Reeds+hKuQkZA=
github.com/getlantern/lantern-water v0.0.0-20260317143726-e0ee64a11d90 h1:P9JX1yAu2uq3b5YiT0sLtHkTrkZuttV8gPZh81nUuag=
github.com/getlantern/lantern-water v0.0.0-20260317143726-e0ee64a11d90/go.mod h1:3JpJgwi4KEI6rS9loOAvcBp+F2jP65d0tTg2GQcTPBU=
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag=
Expand Down
33 changes: 21 additions & 12 deletions servers/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ type ServerCredentials struct {
}

type Options struct {
Outbounds []option.Outbound `json:"outbounds,omitempty"`
Endpoints []option.Endpoint `json:"endpoints,omitempty"`
Locations map[string]C.ServerLocation `json:"locations,omitempty"`
Credentials map[string]ServerCredentials `json:"credentials,omitempty"`
Outbounds []option.Outbound `json:"outbounds,omitempty"`
Endpoints []option.Endpoint `json:"endpoints,omitempty"`
Locations map[string]C.ServerLocation `json:"locations,omitempty"`
URLOverrides map[string]string `json:"url_overrides,omitempty"`
Credentials map[string]ServerCredentials `json:"credentials,omitempty"`
}

// MarshalJSON encodes Options using the sing-box context so that type-specific outbound/endpoint
Expand Down Expand Up @@ -164,10 +165,11 @@ func (m *Manager) Servers() Servers {
result := make(Servers, len(m.servers))
for group, opts := range m.servers {
result[group] = Options{
Outbounds: append([]option.Outbound{}, opts.Outbounds...),
Endpoints: append([]option.Endpoint{}, opts.Endpoints...),
Locations: maps.Clone(opts.Locations),
Credentials: maps.Clone(opts.Credentials),
Outbounds: append([]option.Outbound{}, opts.Outbounds...),
Endpoints: append([]option.Endpoint{}, opts.Endpoints...),
Locations: maps.Clone(opts.Locations),
URLOverrides: maps.Clone(opts.URLOverrides),
Credentials: maps.Clone(opts.Credentials),
}
}
return result
Expand Down Expand Up @@ -302,10 +304,11 @@ func (m *Manager) setServers(group ServerGroup, options Options) error {

slog.Log(nil, internal.LevelTrace, "Setting servers", "group", group, "options", options)
opts := Options{
Outbounds: append([]option.Outbound{}, options.Outbounds...),
Endpoints: append([]option.Endpoint{}, options.Endpoints...),
Locations: make(map[string]C.ServerLocation, len(options.Locations)),
Credentials: make(map[string]ServerCredentials, len(options.Credentials)),
Outbounds: append([]option.Outbound{}, options.Outbounds...),
Endpoints: append([]option.Endpoint{}, options.Endpoints...),
Locations: make(map[string]C.ServerLocation, len(options.Locations)),
URLOverrides: maps.Clone(options.URLOverrides),
Credentials: make(map[string]ServerCredentials, len(options.Credentials)),
}
maps.Copy(opts.Locations, options.Locations)
maps.Copy(opts.Credentials, options.Credentials)
Expand Down Expand Up @@ -387,6 +390,12 @@ func (m *Manager) merge(group ServerGroup, options Options) []string {
servers.Credentials[out.Tag] = creds
}
}
for k, v := range options.URLOverrides {
if servers.URLOverrides == nil {
servers.URLOverrides = make(map[string]string)
}
servers.URLOverrides[k] = v
}
m.servers[group] = servers
return existingTags
}
Expand Down
21 changes: 11 additions & 10 deletions vpn/boxoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func buildOptions(ctx context.Context, path string) (O.Options, error) {
lanternTags = mergeAndCollectTags(&opts, &configOpts)
slog.Debug("Merged config options", "tags", lanternTags)

appendGroupOutbounds(&opts, servers.SGLantern, autoLanternTag, lanternTags)
appendGroupOutbounds(&opts, servers.SGLantern, autoLanternTag, lanternTags, cfg.BanditURLOverrides)

// Load user servers
slog.Debug("Loading user servers")
Expand All @@ -324,14 +324,14 @@ func buildOptions(ctx context.Context, path string) (O.Options, error) {
userTags = mergeAndCollectTags(&opts, &userOpts)
slog.Debug("Merged user server options", "tags", userTags)
}
appendGroupOutbounds(&opts, servers.SGUser, autoUserTag, userTags)
appendGroupOutbounds(&opts, servers.SGUser, autoUserTag, userTags, nil)

if len(lanternTags) == 0 && len(userTags) == 0 {
return O.Options{}, errors.New("no outbounds or endpoints found in config or user servers")
}

// Add auto all outbound
opts.Outbounds = append(opts.Outbounds, urlTestOutbound(autoAllTag, []string{autoLanternTag, autoUserTag}))
opts.Outbounds = append(opts.Outbounds, urlTestOutbound(autoAllTag, []string{autoLanternTag, autoUserTag}, nil))

// Add routing rules for the groups
opts.Route.Rules = append(opts.Route.Rules, groupRule(autoAllTag))
Expand Down Expand Up @@ -429,8 +429,8 @@ func useIfNotZero[T comparable](newVal, oldVal T) T {
return oldVal
}

func appendGroupOutbounds(opts *O.Options, serverGroup, autoTag string, tags []string) {
opts.Outbounds = append(opts.Outbounds, urlTestOutbound(autoTag, tags))
func appendGroupOutbounds(opts *O.Options, serverGroup, autoTag string, tags []string, urlOverrides map[string]string) {
opts.Outbounds = append(opts.Outbounds, urlTestOutbound(autoTag, tags, urlOverrides))
opts.Outbounds = append(opts.Outbounds, selectorOutbound(serverGroup, append([]string{autoTag}, tags...)))
slog.Log(
nil, internal.LevelTrace, "Added group outbounds",
Expand All @@ -453,15 +453,16 @@ func groupAutoTag(group string) string {
}
}

func urlTestOutbound(tag string, outbounds []string) O.Outbound {
func urlTestOutbound(tag string, outbounds []string, urlOverrides map[string]string) O.Outbound {
return O.Outbound{
Type: lbC.TypeMutableURLTest,
Tag: tag,
Options: &lbO.MutableURLTestOutboundOptions{
Outbounds: outbounds,
URL: "https://google.com/generate_204",
Interval: badoption.Duration(urlTestInterval),
IdleTimeout: badoption.Duration(urlTestIdleTimeout),
Outbounds: outbounds,
URL: "https://google.com/generate_204",
URLOverrides: urlOverrides,
Interval: badoption.Duration(urlTestInterval),
IdleTimeout: badoption.Duration(urlTestIdleTimeout),
},
}
}
Expand Down
30 changes: 30 additions & 0 deletions vpn/boxoptions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,36 @@ func TestBuildOptions_Rulesets(t *testing.T) {
})
}

func TestBuildOptions_BanditURLOverrides(t *testing.T) {
testOpts, _, err := testBoxOptions("")
require.NoError(t, err)
lanternTags, lanternOuts := filterOutbounds(*testOpts, constant.TypeHTTP)
require.NotEmpty(t, lanternTags, "need at least one HTTP outbound for test")

overrides := map[string]string{
lanternTags[0]: "https://example.com/callback?token=abc",
}
cfg := config.Config{
ConfigResponse: LC.ConfigResponse{
Options: O.Options{Outbounds: lanternOuts},
BanditURLOverrides: overrides,
},
}

path := t.TempDir()
testOptsToFile(t, cfg, filepath.Join(path, common.ConfigFileName))

opts, err := buildOptions(context.Background(), path)
require.NoError(t, err)

out := findOutbound(opts.Outbounds, autoLanternTag)
require.NotNil(t, out, "auto-lantern outbound not found")

mutOpts, ok := out.Options.(*lbO.MutableURLTestOutboundOptions)
require.True(t, ok, "auto-lantern outbound should be MutableURLTestOutboundOptions")
assert.Equal(t, overrides, mutOpts.URLOverrides, "URLOverrides should be wired from config")
}

func contains[S ~[]E, E any](t *testing.T, s S, e E) bool {
for _, v := range s {
if optsEqual(t, v, e) {
Expand Down
28 changes: 25 additions & 3 deletions vpn/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,26 @@ func (t *tunnel) addOutbounds(group string, options servers.Options) (err error)
added++
}
}

if len(options.URLOverrides) > 0 {
slog.Info("Applying bandit URL overrides to URL test group",
"group", autoTag,
"override_count", len(options.URLOverrides),
)
}
if err := t.mutGrpMgr.SetURLOverrides(autoTag, options.URLOverrides); err != nil {
slog.Warn("Failed to set URL overrides", "group", autoTag, "error", err)
} else if len(options.URLOverrides) > 0 {
// Trigger an immediate URL test cycle when we have bandit overrides so
// callback probes are hit within seconds of config receipt rather than
// waiting for the next scheduled interval (3 min).
if err := t.mutGrpMgr.CheckOutbounds(autoTag); err != nil {
slog.Warn("Failed to trigger immediate URL test after bandit overrides", "group", autoTag, "error", err)
} else {
slog.Info("Triggered immediate URL test for bandit callbacks", "group", autoTag)
}
}

slog.Debug("Added servers to group", "group", group, "added", added)
return errors.Join(errs...)
}
Expand Down Expand Up @@ -451,9 +471,11 @@ func (t *tunnel) updateOutbounds(new servers.Servers) error {
func removeDuplicates(ctx context.Context, curr *lsync.TypedMap[string, []byte], new servers.Options, group string) servers.Options {
slog.Log(nil, internal.LevelTrace, "Removing duplicate outbounds/endpoints", "group", group)
deduped := servers.Options{
Outbounds: []O.Outbound{},
Endpoints: []O.Endpoint{},
Locations: map[string]lcommon.ServerLocation{},
Outbounds: []O.Outbound{},
Endpoints: []O.Endpoint{},
Locations: map[string]lcommon.ServerLocation{},
URLOverrides: new.URLOverrides,
Credentials: new.Credentials,
}
var dropped []string
for _, out := range new.Outbounds {
Expand Down
4 changes: 2 additions & 2 deletions vpn/tunnel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ func TestUpdateServers(t *testing.T) {
outs := []sbO.Outbound{
allOutbounds["direct"], allOutbounds["block"],
allOutbounds["http1-out"], allOutbounds["http2-out"], allOutbounds["socks1-out"],
urlTestOutbound(autoLanternTag, lanternTags), urlTestOutbound(autoUserTag, userTags),
urlTestOutbound(autoLanternTag, lanternTags, nil), urlTestOutbound(autoUserTag, userTags, nil),
selectorOutbound(servers.SGLantern, append(lanternTags, autoLanternTag)),
selectorOutbound(servers.SGUser, append(userTags, autoUserTag)),
urlTestOutbound(autoAllTag, []string{autoLanternTag, autoUserTag}),
urlTestOutbound(autoAllTag, []string{autoLanternTag, autoUserTag}, nil),
}

testOpts.Outbounds = outs
Expand Down
2 changes: 1 addition & 1 deletion vpn/vpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ func preTest(path string) (map[string]uint16, error) {
for _, ob := range outbounds {
tags = append(tags, ob.Tag)
}
outbounds = append(outbounds, urlTestOutbound("preTest", tags))
outbounds = append(outbounds, urlTestOutbound("preTest", tags, cfg.BanditURLOverrides))
options := option.Options{
Log: &option.LogOptions{Disabled: true},
Outbounds: outbounds,
Expand Down
Loading