Skip to content

Commit 116c439

Browse files
reflogclaude
andauthored
Add novpn build variants for tunnel and split tunnel (#527)
* Add novpn build variants for tunnel and split tunnel Introduce build-tagged vpn implementations that remove TUN and split-tunnel behavior in `novpn` builds while preserving API compatibility. Add `novpn` stubs for split tunnel, force SOCKS-only mode with required address validation, and split inbound/split-tunnel option construction into default vs novpn files. Move shared split-tunnel types/constants to a common file and gate affected tests with build tags, adding novpn- specific coverage. * Address novpn review feedback * Simplify novpn build per review: fold inbounds/socks into boxoptions Collapse the novpn divergence to a single build-tag surface: - Merge inbounds_{default,novpn}.go and splittunnel_options_{default, novpn}.go into boxoptions_{default,novpn}.go. - Drop the UseSocks env var and socks_only_{default,novpn}.go. The novpn baseInbounds now builds the SOCKS inbound directly, with a default address overridable by RADIANCE_SOCKS_ADDRESS. - Fold per-platform TUN tweaks into the default baseInbounds and move kernelBelow/minAndroidSystemStackKernel there; OverrideAndroidVPN is now set in baseOpts gated on TUN-inbound presence, eliminating the applyPlatformTunnelOptions stub. - Drop the bypass inbound and its routing rule from novpn via a build-tagged bypassRoutingRules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 886b1ab commit 116c439

11 files changed

Lines changed: 504 additions & 330 deletions

common/env/env.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ var (
2424
PrintCurl _key = "RADIANCE_PRINT_CURL"
2525
DisableStdout _key = "RADIANCE_DISABLE_STDOUT_LOG"
2626
ENV _key = "RADIANCE_ENV"
27-
UseSocks _key = "RADIANCE_USE_SOCKS_PROXY"
2827
SocksAddress _key = "RADIANCE_SOCKS_ADDRESS"
2928
Country _key = "RADIANCE_COUNTRY"
3029
FeatureOverrides _key = "RADIANCE_FEATURE_OVERRIDES"

vpn/boxoptions.go

Lines changed: 33 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ import (
99
"io/fs"
1010
"log/slog"
1111
"net"
12-
"net/netip"
1312
"os"
1413
"path/filepath"
1514
"slices"
16-
"strconv"
1715
"strings"
1816
"time"
1917

@@ -29,12 +27,9 @@ import (
2927
C "github.com/sagernet/sing-box/constant"
3028
O "github.com/sagernet/sing-box/option"
3129
"github.com/sagernet/sing/common/json"
32-
"github.com/sagernet/sing/common/json/badoption"
3330

34-
"github.com/getlantern/radiance/bypass"
3531
"github.com/getlantern/radiance/common"
3632
"github.com/getlantern/radiance/common/atomicfile"
37-
"github.com/getlantern/radiance/common/env"
3833
"github.com/getlantern/radiance/common/fileperm"
3934
"github.com/getlantern/radiance/internal"
4035
"github.com/getlantern/radiance/log"
@@ -50,11 +45,6 @@ const (
5045
cacheID = "lantern"
5146
cacheFileName = "lantern.cache"
5247
cacheClearMarkerName = "lantern.cache.clear"
53-
// minAndroidSystemStackKernel is the minimum Linux kernel version (major.minor) required
54-
// for the system TUN stack to work reliably on Android only. Devices running a
55-
// kernel below this version fall back to gvisor. This constant has no effect on
56-
// other platforms.
57-
minAndroidSystemStackKernel = "5.10"
5848
)
5949

6050
var reservedTags = []string{AutoSelectTag, ManualSelectTag, "direct", "block"}
@@ -169,23 +159,10 @@ func hasGlobalIPv6Using(getSnapshots func() ([]ifaceSnapshot, error)) bool {
169159
// baseOpts returns the minimum sing-box options required for the tunnel to
170160
// function. Do not modify without understanding the downstream effects.
171161
func baseOpts(basePath string) O.Options {
172-
// ensure split tunnel file exists
173-
splitTunnelPath := newSplitTunnel(basePath, slog.Default()).ruleFile
174-
175162
cacheFile := cacheFilePath(basePath)
176-
loopbackAddr := badoption.Addr(netip.MustParseAddr("127.0.0.1"))
163+
inbounds := baseInbounds()
177164

178-
// v6 ULA conditional on system v6 — see hasGlobalIPv6.
179-
tunAddress := []netip.Prefix{
180-
netip.MustParsePrefix("10.10.1.1/30"),
181-
}
182-
if hasGlobalIPv6() {
183-
tunAddress = append(tunAddress, netip.MustParsePrefix("fdfe:dcba:9876::1/126"))
184-
slog.Info("vpn: TUN with IPv6 ULA (system has global v6)")
185-
} else {
186-
slog.Info("vpn: TUN IPv4-only (no global v6 detected)")
187-
}
188-
return O.Options{
165+
opts := O.Options{
189166
Log: &O.LogOptions{
190167
Level: "debug",
191168
Output: "lantern-box.log",
@@ -200,30 +177,7 @@ func baseOpts(basePath string) O.Options {
200177
Final: "dns_local",
201178
},
202179
},
203-
Inbounds: []O.Inbound{
204-
{
205-
Type: "tun",
206-
Tag: "tun-in",
207-
Options: &O.TunInboundOptions{
208-
InterfaceName: "utun225",
209-
Address: tunAddress,
210-
AutoRoute: true,
211-
StrictRoute: true,
212-
EndpointIndependentNat: true, // needed for QUIC migration and hole-punching
213-
Stack: "system", // fallback to gvisor on older Android kernels in buildOptions
214-
},
215-
},
216-
{
217-
Type: C.TypeMixed,
218-
Tag: bypass.BypassInboundTag,
219-
Options: &O.HTTPMixedInboundOptions{
220-
ListenOptions: O.ListenOptions{
221-
Listen: &loopbackAddr,
222-
ListenPort: bypass.ProxyPort,
223-
},
224-
},
225-
},
226-
},
180+
Inbounds: inbounds,
227181
Outbounds: []O.Outbound{
228182
{
229183
Type: C.TypeDirect,
@@ -239,16 +193,7 @@ func baseOpts(basePath string) O.Options {
239193
Route: &O.RouteOptions{
240194
AutoDetectInterface: true,
241195
Rules: baseRoutingRules(),
242-
RuleSet: []O.RuleSet{
243-
{
244-
Type: C.RuleSetTypeLocal,
245-
Tag: splitTunnelTag,
246-
LocalOptions: O.LocalRuleSet{
247-
Path: splitTunnelPath,
248-
},
249-
Format: C.RuleSetFormatSource,
250-
},
251-
},
196+
RuleSet: splitTunnelRuleSet(basePath),
252197
DefaultDomainResolver: &O.DomainResolveOptions{
253198
Server: "dns_local",
254199
},
@@ -268,6 +213,22 @@ func baseOpts(basePath string) O.Options {
268213
},
269214
},
270215
}
216+
217+
// OverrideAndroidVPN is meaningless without a TUN inbound, so gate on its
218+
// presence — the novpn build has none.
219+
if common.Platform == "android" && hasTunInbound(inbounds) {
220+
opts.Route.OverrideAndroidVPN = true
221+
}
222+
return opts
223+
}
224+
225+
func hasTunInbound(inbounds []O.Inbound) bool {
226+
for _, in := range inbounds {
227+
if in.Type == "tun" {
228+
return true
229+
}
230+
}
231+
return false
271232
}
272233

273234
func baseRoutingRules() []O.Rule {
@@ -323,49 +284,23 @@ func baseRoutingRules() []O.Rule {
323284
},
324285
},
325286
},
326-
{ // Route bypass proxy traffic directly (for kindling connections)
327-
Type: C.RuleTypeDefault,
328-
DefaultOptions: O.DefaultRule{
329-
RawDefaultRule: O.RawDefaultRule{
330-
Inbound: []string{bypass.BypassInboundTag},
331-
},
332-
RuleAction: O.RuleAction{
333-
Action: C.RuleActionTypeRoute,
334-
RouteOptions: O.RouteActionOptions{
335-
Outbound: "direct",
336-
},
337-
},
338-
},
339-
},
340-
{
341-
Type: C.RuleTypeDefault,
342-
DefaultOptions: O.DefaultRule{
343-
RawDefaultRule: O.RawDefaultRule{
344-
IPIsPrivate: true,
345-
},
346-
RuleAction: O.RuleAction{
347-
Action: C.RuleActionTypeRoute,
348-
RouteOptions: O.RouteActionOptions{
349-
Outbound: "direct",
350-
},
351-
},
287+
}
288+
rules = append(rules, bypassRoutingRules()...)
289+
rules = append(rules, O.Rule{
290+
Type: C.RuleTypeDefault,
291+
DefaultOptions: O.DefaultRule{
292+
RawDefaultRule: O.RawDefaultRule{
293+
IPIsPrivate: true,
352294
},
353-
},
354-
{ // split tunnel rule
355-
Type: C.RuleTypeDefault,
356-
DefaultOptions: O.DefaultRule{
357-
RawDefaultRule: O.RawDefaultRule{
358-
RuleSet: []string{splitTunnelTag},
359-
},
360-
RuleAction: O.RuleAction{
361-
Action: C.RuleActionTypeRoute,
362-
RouteOptions: O.RouteActionOptions{
363-
Outbound: "direct",
364-
},
295+
RuleAction: O.RuleAction{
296+
Action: C.RuleActionTypeRoute,
297+
RouteOptions: O.RouteActionOptions{
298+
Outbound: "direct",
365299
},
366300
},
367301
},
368-
}
302+
})
303+
rules = append(rules, splitTunnelRoutingRules()...)
369304
return rules
370305
}
371306

@@ -489,47 +424,6 @@ func buildOptions(bOptions BoxOptions) (O.Options, error) {
489424
opts := baseOpts(bOptions.BasePath)
490425
slog.Debug("Base options initialized")
491426

492-
if env.GetBool(env.UseSocks) {
493-
socksAddr, _ := env.Get(env.SocksAddress)
494-
slog.Info("Using SOCKS proxy for inbound as per environment variable", "socksAddr", socksAddr)
495-
addrPort, err := netip.ParseAddrPort(socksAddr)
496-
if err != nil {
497-
return O.Options{}, fmt.Errorf("invalid SOCKS address: %w", err)
498-
}
499-
addr := badoption.Addr(addrPort.Addr())
500-
socksIn := O.Inbound{
501-
Type: C.TypeMixed,
502-
Tag: "http-socks-in",
503-
Options: &O.HTTPMixedInboundOptions{
504-
ListenOptions: O.ListenOptions{
505-
Listen: &addr,
506-
ListenPort: addrPort.Port(),
507-
},
508-
},
509-
}
510-
opts.Inbounds = []O.Inbound{socksIn}
511-
} else {
512-
switch common.Platform {
513-
case "android":
514-
opts.Route.OverrideAndroidVPN = true
515-
kv := kernelVersion()
516-
slog.Debug("detected kernel version", "kernel", kv)
517-
if kv == "" {
518-
slog.Warn("kernel version unknown, keeping default TUN stack")
519-
} else if kernelBelow(kv, minAndroidSystemStackKernel) {
520-
opts.Inbounds[0].Options.(*O.TunInboundOptions).Stack = "gvisor"
521-
slog.Info("kernel below 5.10, using gvisor TUN stack", "kernel", kv)
522-
}
523-
slog.Debug("Android platform detected, OverrideAndroidVPN set to true")
524-
case "ios":
525-
opts.Inbounds[0].Options.(*O.TunInboundOptions).Stack = ""
526-
slog.Debug("iOS platform detected, using default TUN stack with no override")
527-
case "linux":
528-
opts.Inbounds[0].Options.(*O.TunInboundOptions).AutoRedirect = true
529-
slog.Debug("Linux platform detected, AutoRedirect set to true")
530-
}
531-
}
532-
533427
// add smart routing and ad block rules
534428
smartRoutingRules := normalizeSmartRoutingRules(bOptions.SmartRouting)
535429
if len(smartRoutingRules) > 0 {
@@ -731,47 +625,6 @@ func selectModeRule(mode string) O.Rule {
731625
}
732626
}
733627

734-
// kernelBelow reports whether the kernel version string v is below min.
735-
// Only the first two components (major.minor) are compared, e.g. "5.10" or "4.19.0-android13".
736-
// Returns false if either version string cannot be parsed.
737-
func kernelBelow(v, min string) bool {
738-
parseKernelMajorMinor := func(s string) (int, int, bool) {
739-
p := strings.SplitN(s, ".", 3)
740-
if len(p) < 2 {
741-
return 0, 0, false
742-
}
743-
// Strip non-numeric suffixes (e.g. "19" from "19-android13")
744-
numericPrefix := func(part string) string {
745-
for i, r := range part {
746-
if r < '0' || r > '9' {
747-
return part[:i]
748-
}
749-
}
750-
return part
751-
}
752-
majorStr := numericPrefix(p[0])
753-
minorStr := numericPrefix(p[1])
754-
if majorStr == "" || minorStr == "" {
755-
return 0, 0, false
756-
}
757-
major, err := strconv.Atoi(majorStr)
758-
if err != nil {
759-
return 0, 0, false
760-
}
761-
minor, err := strconv.Atoi(minorStr)
762-
if err != nil {
763-
return 0, 0, false
764-
}
765-
return major, minor, true
766-
}
767-
vMaj, vMin, vok := parseKernelMajorMinor(v)
768-
mMaj, mMin, mok := parseKernelMajorMinor(min)
769-
if !vok || !mok {
770-
return false
771-
}
772-
return vMaj < mMaj || (vMaj == mMaj && vMin < mMin)
773-
}
774-
775628
func catchAllBlockerRule() O.Rule {
776629
return O.Rule{
777630
Type: C.RuleTypeDefault,

0 commit comments

Comments
 (0)