Merge tag 'v1.14.0'

This commit is contained in:
Shtorm
2026-09-04 01:08:30 +03:00
987 changed files with 141481 additions and 11302 deletions
+164 -51
View File
@@ -12,9 +12,11 @@ import (
"github.com/sagernet/bbolt"
bboltErrors "github.com/sagernet/bbolt/errors"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service/filemanager"
)
@@ -30,6 +32,7 @@ var (
string(bucketMode),
string(bucketRuleSet),
string(bucketRDRC),
string(bucketDNSCache),
}
cacheIDDefault = []byte("default")
@@ -38,32 +41,48 @@ var (
var _ adapter.CacheFile = (*CacheFile)(nil)
type CacheFile struct {
ctx context.Context
path string
cacheID []byte
storeFakeIP bool
storeRDRC bool
storeWARPConfig bool
storeMASQUEConfig bool
rdrcTimeout time.Duration
DB *bbolt.DB
resetAccess sync.Mutex
saveMetadataTimer *time.Timer
saveFakeIPAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
saveRDRCAccess sync.RWMutex
saveRDRC map[saveRDRCCacheKey]bool
ctx context.Context
logger logger.Logger
path string
cacheID []byte
cacheIDText string
storeFakeIP bool
storeRDRC bool
storeDNS bool
storeWARPConfig bool
storeMASQUEConfig bool
disableExpire bool
rdrcTimeout time.Duration
optimisticTimeout time.Duration
DB *bbolt.DB
dbAccess sync.RWMutex
saveMetadataAccess sync.Mutex
saveMetadata *adapter.FakeIPMetadata
saveMetadataTimer *time.Timer
saveFakeIPAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
saveRDRCAccess sync.RWMutex
saveRDRC map[saveCacheKey]bool
saveDNSCacheAccess sync.RWMutex
saveDNSCache map[saveCacheKey]saveDNSCacheEntry
}
type saveRDRCCacheKey struct {
type saveCacheKey struct {
TransportName string
QuestionName string
QType uint16
}
func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
type saveDNSCacheEntry struct {
rawMessage []byte
expireAt time.Time
sequence uint64
saving bool
}
func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile {
var path string
if options.Path != "" {
path = options.Path
@@ -74,6 +93,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
if options.CacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
}
if options.StoreRDRC {
deprecated.Report(ctx, deprecated.OptionStoreRDRC)
}
var rdrcTimeout time.Duration
if options.StoreRDRC {
if options.RDRCTimeout > 0 {
@@ -84,17 +106,21 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
}
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,
rdrcTimeout: rdrcTimeout,
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr),
saveAddress6: make(map[string]netip.Addr),
saveRDRC: make(map[saveRDRCCacheKey]bool),
saveRDRC: make(map[saveCacheKey]bool),
saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry),
}
}
@@ -106,31 +132,81 @@ func (c *CacheFile) Dependencies() []string {
return nil
}
func (c *CacheFile) CacheID() string {
return c.cacheIDText
}
func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) {
c.optimisticTimeout = timeout
}
func (c *CacheFile) SetDisableExpire(disableExpire bool) {
c.disableExpire = disableExpire
}
func (c *CacheFile) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
switch stage {
case adapter.StartStateInitialize:
return c.start()
case adapter.StartStateStart:
c.startCacheCleanup()
}
return nil
}
func (c *CacheFile) startCacheCleanup() {
if c.storeDNS {
c.clearRDRC()
c.cleanupDNSCache()
interval := c.optimisticTimeout / 2
if interval <= 0 {
interval = time.Hour
}
go c.loopCacheCleanup(interval, c.cleanupDNSCache)
} else if c.storeRDRC {
c.cleanupRDRC()
interval := c.rdrcTimeout / 2
if interval <= 0 {
interval = time.Hour
}
go c.loopCacheCleanup(interval, c.cleanupRDRC)
}
}
func (c *CacheFile) start() error {
const fileMode = 0o666
cacheFile, err := filemanager.OpenFile(c.ctx, c.path, os.O_RDWR|os.O_CREATE, fileMode)
if err != nil {
return err
}
cacheFile.Close()
options := bbolt.Options{Timeout: time.Second}
var (
db *bbolt.DB
err error
)
var db *bbolt.DB
for range 10 {
db, err = bbolt.Open(c.path, fileMode, &options)
if err != nil {
if errors.Is(err, bboltErrors.ErrTimeout) {
continue
}
if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
rmErr := filemanager.Remove(c.ctx, c.path)
if rmErr != nil {
return err
}
}
time.Sleep(100 * time.Millisecond)
continue
}
err = checkDatabase(db)
if err == nil {
break
}
if errors.Is(err, bboltErrors.ErrTimeout) {
continue
c.logger.Error("database corrupted: ", err, ": resetting")
db.Close()
rmErr := filemanager.Remove(c.ctx, c.path)
if rmErr != nil {
return err
}
if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
rmErr := os.Remove(c.path)
if rmErr != nil {
return err
}
}
time.Sleep(100 * time.Millisecond)
}
if err != nil {
return err
@@ -168,47 +244,84 @@ func (c *CacheFile) Start(stage adapter.StartStage) error {
}
func (c *CacheFile) Close() error {
if c.DB == nil {
c.dbAccess.RLock()
db := c.DB
c.dbAccess.RUnlock()
if db == nil {
return nil
}
return c.DB.Close()
return db.Close()
}
func checkDatabase(db *bbolt.DB) error {
return db.View(func(tx *bbolt.Tx) error {
var checkErr error
for txErr := range tx.Check() {
if checkErr == nil {
checkErr = txErr
}
}
return checkErr
})
}
func (c *CacheFile) database() *bbolt.DB {
c.dbAccess.RLock()
defer c.dbAccess.RUnlock()
return c.DB
}
func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) {
db := c.database()
defer func() {
if r := recover(); r != nil {
c.resetDB()
r := recover()
if r != nil {
c.resetDB(db, r)
err = E.New("database corrupted: ", r)
}
}()
return c.DB.View(fn)
return db.View(fn)
}
func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) {
db := c.database()
defer func() {
if r := recover(); r != nil {
c.resetDB()
r := recover()
if r != nil {
c.resetDB(db, r)
err = E.New("database corrupted: ", r)
}
}()
return c.DB.Batch(fn)
err = db.Batch(fn)
var panicErr bbolt.PanickedError
if errors.As(err, &panicErr) {
c.resetDB(db, panicErr.Reason)
return E.New("database corrupted: ", panicErr.Reason)
}
return err
}
func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) {
db := c.database()
defer func() {
if r := recover(); r != nil {
c.resetDB()
r := recover()
if r != nil {
c.resetDB(db, r)
err = E.New("database corrupted: ", r)
}
}()
return c.DB.Update(fn)
return db.Update(fn)
}
func (c *CacheFile) resetDB() {
c.resetAccess.Lock()
defer c.resetAccess.Unlock()
c.DB.Close()
os.Remove(c.path)
func (c *CacheFile) resetDB(failedDB *bbolt.DB, reason any) {
c.dbAccess.Lock()
defer c.dbAccess.Unlock()
if c.DB != failedDB {
return
}
c.logger.Error("database corrupted: ", reason, ": resetting")
failedDB.Close()
filemanager.Remove(c.ctx, c.path)
db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second})
if err == nil {
_ = filemanager.Chown(c.ctx, c.path)