Compare commits

...
20 changed files with 175 additions and 107 deletions
+16
View File
@@ -141,6 +141,22 @@ release_android: lib_android update_android_version build_android upload_android
publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
build_desktop_windows:
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:win
build_desktop_linux:
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:linux
upload_desktop:
mkdir -p dist/release_desktop
cp ../sing-box-for-desktop/release/SFW-*.exe dist/release_desktop 2>/dev/null || true
cp ../sing-box-for-desktop/release/SFL-*.deb ../sing-box-for-desktop/release/SFL-*.rpm ../sing-box-for-desktop/release/SFL-*.pkg.tar.zst dist/release_desktop 2>/dev/null || true
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_desktop
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_desktop
rm -rf dist/release_desktop
release_desktop: update_desktop_version build_desktop_windows build_desktop_linux upload_desktop
# TODO: find why and remove `-destination 'generic/platform=iOS'`
# TODO: remove xcode clean when fix control widget fixed
build_ios:
+1 -1
View File
@@ -34,7 +34,7 @@ Sing-box with extended features.
- **Rate Limiter** — Request rate limiting
### Encryption & Obfuscation
- **Amnezia 3.0** — WireGuard traffic obfuscation
- **Amnezia 3.1** — WireGuard traffic obfuscation
- **VLESS encryption** — XRAY encryption for VLESS protocol
### Transports
+1 -1
View File
@@ -43,7 +43,7 @@ type CacheFile interface {
StoreWARPConfig() bool
StoreMASQUEConfig() bool
StoreProviders() bool
StoreSubscriptions() bool
StoreDNS() bool
DNSCacheStore
+15 -5
View File
@@ -36,11 +36,12 @@ type Adapter struct {
callbackAccess sync.Mutex
callbacks list.List[adapter.ProviderUpdateCallback]
link string
enabled bool
removeEmojis bool
timeout time.Duration
interval time.Duration
link string
enabled bool
removeEmojis bool
overrideDialerOptions *option.DialerOptions
timeout time.Duration
interval time.Duration
}
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
@@ -75,6 +76,10 @@ func (a *Adapter) SetRemoveEmojis(remove bool) {
a.removeEmojis = remove
}
func (a *Adapter) SetOverrideDialerOptions(options *option.DialerOptions) {
a.overrideDialerOptions = options
}
func (a *Adapter) Start() error {
a.history = service.PtrFromContext[urltest.HistoryStorage](a.ctx)
if a.history == nil {
@@ -119,6 +124,11 @@ func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Ou
oldOptByTag[opt.Tag] = opt
}
for i, opt := range newOpts {
if a.overrideDialerOptions != nil {
if wrapper, ok := opt.Options.(option.DialerOptionsWrapper); ok {
wrapper.ReplaceDialerOptions(*a.overrideDialerOptions)
}
}
var tag string
if opt.Tag != "" {
tag = F.ToString(a.providerTag, "/", opt.Tag)
@@ -5,6 +5,7 @@ import (
"flag"
"os"
"path/filepath"
"regexp"
"runtime"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
@@ -41,6 +42,9 @@ func main() {
common.Must(json.NewDecoder(versionFile).Decode(&metadata))
common.Must(versionFile.Close())
newGoVersion := runtime.Version()
if match := regexp.MustCompile(`^go\d+\.\d+(?:\.\d+)?`).FindString(newGoVersion); match != "" {
newGoVersion = match
}
versionUpdated := metadata.Version != newVersion
goVersionUpdated := metadata.GoVersion != newGoVersion
if !(versionUpdated || goVersionUpdated) {
+4
View File
@@ -50,6 +50,10 @@
"path": "subscriptions/my-sub.txt",
// Remove emoji flags from proxy names.
"remove_emojis": true,
// Override dial fields on every outbound from this provider.
"override_dialer_options": {
// Dial fields
},
"health_check": {
"enabled": true,
"url": "https://www.gstatic.com/generate_204",
+5 -1
View File
@@ -82,6 +82,10 @@
"include": "(?i)hk|jp|sg|us",
// Remove emoji flags from proxy names.
"remove_emojis": true,
// Override dial fields on every outbound from this provider.
"override_dialer_options": {
// Dial fields
},
"health_check": {
"enabled": true,
"url": "https://www.gstatic.com/generate_204",
@@ -98,7 +102,7 @@
"experimental": {
"cache_file": {
"enabled": true,
"store_providers": true // For persisting fetched subscriptions across restarts
"store_subscriptions": true // For persisting fetched subscriptions across restarts
}
}
}
+20 -20
View File
@@ -51,7 +51,7 @@ type CacheFile struct {
storeDNS bool
storeWARPConfig bool
storeMASQUEConfig bool
storeProviders bool
storeSubscriptions bool
disableExpire bool
rdrcTimeout time.Duration
optimisticTimeout time.Duration
@@ -106,23 +106,23 @@ func New(ctx context.Context, logger logger.Logger, options option.CacheFileOpti
}
}
return &CacheFile{
ctx: ctx,
logger: logger,
path: filemanager.BasePath(ctx, path),
cacheID: cacheIDBytes,
cacheIDText: options.CacheID,
storeFakeIP: options.StoreFakeIP,
storeRDRC: options.StoreRDRC,
storeDNS: options.StoreDNS,
storeWARPConfig: options.StoreWARPConfig,
storeMASQUEConfig: options.StoreMASQUEConfig,
storeProviders: options.StoreProviders,
rdrcTimeout: rdrcTimeout,
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr),
saveAddress6: make(map[string]netip.Addr),
saveRDRC: make(map[saveCacheKey]bool),
saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry),
ctx: ctx,
logger: logger,
path: filemanager.BasePath(ctx, path),
cacheID: cacheIDBytes,
cacheIDText: options.CacheID,
storeFakeIP: options.StoreFakeIP,
storeRDRC: options.StoreRDRC,
storeDNS: options.StoreDNS,
storeWARPConfig: options.StoreWARPConfig,
storeMASQUEConfig: options.StoreMASQUEConfig,
storeSubscriptions: options.StoreSubscriptions,
rdrcTimeout: rdrcTimeout,
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr),
saveAddress6: make(map[string]netip.Addr),
saveRDRC: make(map[saveCacheKey]bool),
saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry),
}
}
@@ -487,8 +487,8 @@ func (c *CacheFile) StoreMASQUEConfig() bool {
return c.storeMASQUEConfig
}
func (c *CacheFile) StoreProviders() bool {
return c.storeProviders
func (c *CacheFile) StoreSubscriptions() bool {
return c.storeSubscriptions
}
func (c *CacheFile) LoadWARPConfig(tag string) *adapter.SavedBinary {
+2 -2
View File
@@ -76,7 +76,7 @@ require (
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.102.1-sing-box-1.14-mod.4
github.com/sagernet/wireguard-go v0.0.5-0.20260823125007-8bd032a91a30
github.com/sagernet/wireguard-go v0.0.5-extended-1.6.1
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/shtorm-7/go-cache/v2 v2.1.0-extended-1.0.2
github.com/shtorm-7/workerpool v0.5.0
@@ -271,7 +271,7 @@ require (
zombiezen.com/go/capnproto2 v2.18.2+incompatible // indirect
)
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.0
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.1
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.4-extended-1.0.3
+2
View File
@@ -519,6 +519,8 @@ github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.4-extended-1.0.3 h1:AG4
github.com/shtorm-7/tailscale v1.102.1-sing-box-1.14-mod.4-extended-1.0.3/go.mod h1:+1R9TnmCDbxb7h7ZOBKxi5UI6aWf+polOO/fc0WB/4o=
github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.0 h1:cLCyRR5mVNuhR6iNkcDHItt0SZ6O1GYWfP86XmVmjUo=
github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.0/go.mod h1:1VZmkU7yPAZVE0XKufUqqAhEY1RnWUcyZLmry85y+7A=
github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.1 h1:lLd8c5SNbm01idBlaSc18Urn4i4/ME/Umu8BeQLJ/io=
github.com/shtorm-7/wireguard-go v0.0.5-extended-1.6.1/go.mod h1:1VZmkU7yPAZVE0XKufUqqAhEY1RnWUcyZLmry85y+7A=
github.com/shtorm-7/workerpool v0.5.0 h1:NPZuNgyH0EUm4aQsTL09xR1iV+7GCFw6jX9Z4aAVp2s=
github.com/shtorm-7/workerpool v0.5.0/go.mod h1:NI0pUZgmGu0BdKO9j3mct1DNZmgXbyTS9foorljdH6E=
github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
+10 -10
View File
@@ -11,16 +11,16 @@ type ExperimentalOptions struct {
}
type CacheFileOptions struct {
Enabled bool `json:"enabled,omitempty"`
Path string `json:"path,omitempty"`
CacheID string `json:"cache_id,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty" schema:"omit"`
StoreWARPConfig bool `json:"store_warp_config,omitempty"`
StoreMASQUEConfig bool `json:"store_masque_config,omitempty"`
StoreProviders bool `json:"store_providers,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
StoreDNS bool `json:"store_dns,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Path string `json:"path,omitempty"`
CacheID string `json:"cache_id,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty" schema:"omit"`
StoreWARPConfig bool `json:"store_warp_config,omitempty"`
StoreMASQUEConfig bool `json:"store_masque_config,omitempty"`
StoreSubscriptions bool `json:"store_subscriptions,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
StoreDNS bool `json:"store_dns,omitempty"`
}
type ClashAPIOptions struct {
+18 -15
View File
@@ -47,28 +47,31 @@ func (h *Provider) UnmarshalJSONContext(ctx context.Context, content []byte) err
}
type ProviderLocalOptions struct {
Path string `json:"path"`
RemoveEmojis bool `json:"remove_emojis,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
Path string `json:"path"`
RemoveEmojis bool `json:"remove_emojis,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
OverrideDialerOptions *DialerOptions `json:"override_dialer_options,omitempty"`
}
type ProviderRemoteOptions struct {
URL string `json:"url"`
UserAgent string `json:"user_agent,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
URL string `json:"url"`
UserAgent string `json:"user_agent,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
Exclude *badoption.Regexp `json:"exclude,omitempty"`
Include *badoption.Regexp `json:"include,omitempty"`
RemoveEmojis bool `json:"remove_emojis,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
Exclude *badoption.Regexp `json:"exclude,omitempty"`
Include *badoption.Regexp `json:"include,omitempty"`
RemoveEmojis bool `json:"remove_emojis,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
OverrideDialerOptions *DialerOptions `json:"override_dialer_options,omitempty"`
}
type ProviderInlineOptions struct {
Outbounds []Outbound `json:"outbounds,omitempty"`
RemoveEmojis bool `json:"remove_emojis,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
RemoveEmojis bool `json:"remove_emojis,omitempty"`
HealthCheck ProviderHealthCheckOptions `json:"health_check,omitempty"`
OverrideDialerOptions *DialerOptions `json:"override_dialer_options,omitempty"`
}
type ProviderHealthCheckOptions struct {
+17 -5
View File
@@ -27,6 +27,7 @@ type Failover struct {
outbound.Adapter
ctx context.Context
outbound adapter.OutboundManager
outbounds []adapter.Outbound
logger logger.ContextLogger
dial DialStrategy
uotClient *uot.Client
@@ -50,11 +51,12 @@ func NewFailover(ctx context.Context, router adapter.Router, logger log.ContextL
return nil, err
}
outbound := &Failover{
Adapter: outbound.NewAdapter(C.TypeFailover, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
ctx: ctx,
outbound: service.FromContext[adapter.OutboundManager](ctx),
logger: logger,
dial: dial,
Adapter: outbound.NewAdapter(C.TypeFailover, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
ctx: ctx,
outbound: service.FromContext[adapter.OutboundManager](ctx),
outbounds: outbounds,
logger: logger,
dial: dial,
}
outbound.uotClient = &uot.Client{
Dialer: outbound,
@@ -63,6 +65,16 @@ func NewFailover(ctx context.Context, router adapter.Router, logger log.ContextL
return outbound, nil
}
func (f *Failover) Start(stage adapter.StartStage) error {
for _, outbound := range f.outbounds {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
return err
}
}
return nil
}
func (f *Failover) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if N.NetworkName(network) == N.NetworkUDP {
return f.uotClient.DialContext(ctx, network, destination)
+13 -11
View File
@@ -24,6 +24,8 @@ func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.BandwidthLimiterOutboundOptions](registry, C.TypeBandwidthLimiter, NewOutbound)
}
var _ adapter.Lifecycle = (*Outbound)(nil)
type Outbound struct {
outbound.Adapter
ctx context.Context
@@ -87,19 +89,19 @@ func (h *Outbound) Network() []string {
return []string{N.NetworkTCP, N.NetworkUDP}
}
func (h *Outbound) Start() error {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
err := h.router.Start(stage)
if err != nil {
return err
func (h *Outbound) Start(stage adapter.StartStage) error {
if stage == adapter.StartStateStart {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
}
return nil
return h.router.Start(stage)
}
func (h *Outbound) Close() error {
return h.router.Close()
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+13 -11
View File
@@ -24,6 +24,8 @@ func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.ConnectionLimiterOutboundOptions](registry, C.TypeConnectionLimiter, NewOutbound)
}
var _ adapter.Lifecycle = (*Outbound)(nil)
type Outbound struct {
outbound.Adapter
ctx context.Context
@@ -87,19 +89,19 @@ func (h *Outbound) Network() []string {
return []string{N.NetworkTCP, N.NetworkUDP}
}
func (h *Outbound) Start() error {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
err := h.router.Start(stage)
if err != nil {
return err
func (h *Outbound) Start(stage adapter.StartStage) error {
if stage == adapter.StartStateStart {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
}
return nil
return h.router.Start(stage)
}
func (h *Outbound) Close() error {
return h.router.Close()
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+11 -11
View File
@@ -85,19 +85,19 @@ func (h *Outbound) Network() []string {
return []string{N.NetworkTCP, N.NetworkUDP}
}
func (h *Outbound) Start() error {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
err := h.router.Start(stage)
if err != nil {
return err
func (h *Outbound) Start(stage adapter.StartStage) error {
if stage == adapter.StartStateStart {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
}
return nil
return h.router.Start(stage)
}
func (h *Outbound) Close() error {
return h.router.Close()
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+13 -11
View File
@@ -23,6 +23,8 @@ func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.TrafficLimiterOutboundOptions](registry, C.TypeTrafficLimiter, NewOutbound)
}
var _ adapter.Lifecycle = (*Outbound)(nil)
type Outbound struct {
outbound.Adapter
ctx context.Context
@@ -63,19 +65,19 @@ func (h *Outbound) Network() []string {
return []string{N.NetworkTCP, N.NetworkUDP}
}
func (h *Outbound) Start() error {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
for _, stage := range []adapter.StartStage{adapter.StartStateStart, adapter.StartStatePostStart, adapter.StartStateStarted} {
err := h.router.Start(stage)
if err != nil {
return err
func (h *Outbound) Start(stage adapter.StartStage) error {
if stage == adapter.StartStateStart {
detour, loaded := h.outbound.Outbound(h.outboundTag)
if !loaded {
return E.New("outbound not found: ", h.outboundTag)
}
h.detour = detour
}
return nil
return h.router.Start(stage)
}
func (h *Outbound) Close() error {
return h.router.Close()
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+4
View File
@@ -78,6 +78,10 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
}
func (c *ClientEndpoint) Start(stage adapter.StartStage) error {
err := adapter.LegacyStart(c.outbound, stage)
if err != nil {
return err
}
if stage != adapter.StartStatePostStart {
return nil
}
+2
View File
@@ -52,6 +52,7 @@ func NewProviderInline(ctx context.Context, router adapter.Router, logFactory lo
logger: logger,
}
p.SetRemoveEmojis(options.RemoveEmojis)
p.SetOverrideDialerOptions(options.OverrideDialerOptions)
p.UpdateOutbounds(nil, options.Outbounds)
return p, nil
}
@@ -71,6 +72,7 @@ func NewProviderLocal(ctx context.Context, router adapter.Router, logFactory log
provider: service.FromContext[adapter.ProviderManager](ctx),
}
provider.SetRemoveEmojis(options.RemoveEmojis)
provider.SetOverrideDialerOptions(options.OverrideDialerOptions)
filePath := filemanager.BasePath(ctx, options.Path)
provider.path, _ = filepath.Abs(filePath)
watcher, err := fswatch.NewWatcher(fswatch.Options{
+4 -3
View File
@@ -101,12 +101,13 @@ func NewProviderRemote(ctx context.Context, router adapter.Router, logFactory lo
include: (*regexp.Regexp)(options.Include),
}
p.SetRemoveEmojis(options.RemoveEmojis)
p.SetOverrideDialerOptions(options.OverrideDialerOptions)
return p, nil
}
func (s *ProviderRemote) Start() error {
s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
if s.cacheFile != nil && s.cacheFile.StoreProviders() {
if s.cacheFile != nil && s.cacheFile.StoreSubscriptions() {
if saveSub := s.cacheFile.LoadSubscription(s.Tag()); saveSub != nil {
content, _ := boxCommon.DecodeBase64URLSafe(string(saveSub.Content))
firstLine, others := getFirstLine(content)
@@ -207,7 +208,7 @@ func (s *ProviderRemote) fetch(ctx context.Context) error {
case http.StatusNotModified:
s.subscriptionInfo = info
s.lastUpdated = time.Now()
if s.cacheFile != nil && s.cacheFile.StoreProviders() {
if s.cacheFile != nil && s.cacheFile.StoreSubscriptions() {
saveSub := s.cacheFile.LoadSubscription(s.Tag())
if saveSub != nil {
if hasInfo {
@@ -251,7 +252,7 @@ func (s *ProviderRemote) fetch(ctx context.Context) error {
s.UpdateGroups()
s.subscriptionInfo = info
s.lastUpdated = time.Now()
if s.cacheFile != nil && s.cacheFile.StoreProviders() {
if s.cacheFile != nil && s.cacheFile.StoreSubscriptions() {
content, _ := json.Marshal(option.Options{
Outbounds: s.lastOutOpts,
})