diff --git a/adapter/experimental.go b/adapter/experimental.go index c26ee8c1..2a6776cd 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -109,7 +109,7 @@ type OutboundGroup interface { type URLTestGroup interface { OutboundGroup - URLTest(ctx context.Context, url string) (map[string]uint16, error) + URLTest(ctx context.Context) (map[string]uint16, error) } func OutboundTag(detour Outbound) string { diff --git a/box.go b/box.go index 7eb28566..4342a4b1 100644 --- a/box.go +++ b/box.go @@ -287,7 +287,7 @@ func (s *Box) start() error { return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") } } - return nil + return s.postStart() } func (s *Box) postStart() error { @@ -298,20 +298,21 @@ func (s *Box) postStart() error { return E.Cause(err, "start ", serviceName) } } - for serviceName, service := range s.outbounds { - if lateService, isLateService := service.(adapter.PostStarter); isLateService { - s.logger.Trace("post-starting ", service) - err := lateService.PostStart() + for _, outbound := range s.outbounds { + if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { + s.logger.Trace("post-starting outbound/", outbound.Tag()) + err := lateOutbound.PostStart() if err != nil { - return E.Cause(err, "post-start ", serviceName) + return E.Cause(err, "post-start outbound/", outbound.Tag()) } } } + s.logger.Trace("post-starting router") err := s.router.PostStart() if err != nil { return E.Cause(err, "post-start router") } - return nil + return s.router.PostStart() } func (s *Box) Close() error { diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go index e0cc5e2a..18163dae 100644 --- a/cmd/sing-box/cmd_geosite_export.go +++ b/cmd/sing-box/cmd_geosite_export.go @@ -5,6 +5,7 @@ import ( "os" "github.com/sagernet/sing-box/common/geosite" + "github.com/sagernet/sing-box/common/json" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" diff --git a/constant/timeout.go b/constant/timeout.go index db0379a4..09038a40 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -3,11 +3,12 @@ package constant import "time" const ( - TCPTimeout = 5 * time.Second - ReadPayloadTimeout = 300 * time.Millisecond - DNSTimeout = 10 * time.Second - QUICTimeout = 30 * time.Second - STUNTimeout = 15 * time.Second - UDPTimeout = 5 * time.Minute - DefaultURLTestInterval = 1 * time.Minute + TCPTimeout = 5 * time.Second + ReadPayloadTimeout = 300 * time.Millisecond + DNSTimeout = 10 * time.Second + QUICTimeout = 30 * time.Second + STUNTimeout = 15 * time.Second + UDPTimeout = 5 * time.Minute + DefaultURLTestInterval = 3 * time.Minute + DefaultURLTestIdleTimeout = 30 * time.Second ) diff --git a/docs/changelog.md b/docs/changelog.md index 0461cdad..d5cb9de1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,25 +4,8 @@ icon: material/alert-decagram # ChangeLog -#### 1.8.0-alpha.8 -* Add context to JSON decode error message **1** -* Reject internal fake-ip queries **2** -* Fixes and improvements - -**1**: - -JSON parse errors will now include the current key path. -Only takes effect when compiled with Go 1.21+. - -**2**: - -All internal DNS queries now skip DNS rules with `server` type `fakeip`, -and the default DNS server can no longer be `fakeip`. - -This change is intended to break incorrect usage and essentially requires no action. - -#### 1.8.0-alpha.7 +#### 1.7.2 * Fixes and improvements diff --git a/docs/configuration/outbound/urltest.md b/docs/configuration/outbound/urltest.md index d905068d..f4b3b0aa 100644 --- a/docs/configuration/outbound/urltest.md +++ b/docs/configuration/outbound/urltest.md @@ -10,9 +10,10 @@ "proxy-b", "proxy-c" ], - "url": "https://www.gstatic.com/generate_204", - "interval": "1m", - "tolerance": 50, + "url": "", + "interval": "", + "tolerance": 0, + "idle_timeout": "", "interrupt_exist_connections": false } ``` @@ -31,12 +32,16 @@ The URL to test. `https://www.gstatic.com/generate_204` will be used if empty. #### interval -The test interval. `1m` will be used if empty. +The test interval. `3m` will be used if empty. #### tolerance The test tolerance in milliseconds. `50` will be used if empty. +#### idle_timeout + +The idle timeout. `30m` will be used if empty. + #### interrupt_exist_connections Interrupt existing connections when the selected outbound has changed. diff --git a/docs/configuration/outbound/urltest.zh.md b/docs/configuration/outbound/urltest.zh.md index 0ad891f6..4372298a 100644 --- a/docs/configuration/outbound/urltest.zh.md +++ b/docs/configuration/outbound/urltest.zh.md @@ -10,9 +10,10 @@ "proxy-b", "proxy-c" ], - "url": "https://www.gstatic.com/generate_204", - "interval": "1m", + "url": "", + "interval": "", "tolerance": 50, + "idle_timeout": "", "interrupt_exist_connections": false } ``` @@ -31,12 +32,16 @@ #### interval -测试间隔。 默认使用 `1m`。 +测试间隔。 默认使用 `3m`。 #### tolerance 以毫秒为单位的测试容差。 默认使用 `50`。 +#### idle_timeout + +空闲超时。默认使用 `30m`。 + #### interrupt_exist_connections 当选定的出站发生更改时,中断现有连接。 diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go index e9c8f0f3..763d3801 100644 --- a/experimental/clashapi/api_meta_group.go +++ b/experimental/clashapi/api_meta_group.go @@ -83,7 +83,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) var result map[string]uint16 if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup { - result, err = urlTestGroup.URLTest(ctx, url) + result, err = urlTestGroup.URLTest(ctx) } else { outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound { itOutbound, _ := server.router.Outbound(it) diff --git a/go.sum b/go.sum index 55b75f35..6641d669 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,7 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4Wk github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= github.com/sagernet/sing v0.2.18-0.20231201060417-575186ed63c2 h1:1ydWkFgLURGlrnwRdjyrpo9lp1g5Qq7XrNBghMntWTs= +github.com/sagernet/sing v0.2.18-0.20231201060417-575186ed63c2/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE= github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE= github.com/sagernet/sing-mux v0.1.5-0.20231109075101-6b086ed6bb07 h1:ncKb5tVOsCQgCsv6UpsA0jinbNb5OQ5GMPJlyQP3EHM= diff --git a/option/group.go b/option/group.go index 58824e80..72a0f637 100644 --- a/option/group.go +++ b/option/group.go @@ -11,5 +11,6 @@ type URLTestOutboundOptions struct { URL string `json:"url,omitempty"` Interval Duration `json:"interval,omitempty"` Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout Duration `json:"idle_timeout,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` } diff --git a/outbound/dns.go b/outbound/dns.go index 81b55b22..74adb3ae 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -165,6 +165,7 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } timeout.Update() responseBuffer := buf.NewPacket() + responseBuffer.Resize(1024, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { cancel(err) @@ -241,6 +242,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa } timeout.Update() responseBuffer := buf.NewPacket() + responseBuffer.Resize(1024, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { cancel(err) diff --git a/outbound/urltest.go b/outbound/urltest.go index 22578571..5cae5236 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -3,7 +3,6 @@ package outbound import ( "context" "net" - "sort" "sync" "time" @@ -36,9 +35,9 @@ type URLTest struct { link string interval time.Duration tolerance uint16 + idleTimeout time.Duration group *URLTestGroup interruptExternalConnections bool - started bool } func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) { @@ -55,6 +54,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo link: options.URL, interval: time.Duration(options.Interval), tolerance: options.Tolerance, + idleTimeout: time.Duration(options.IdleTimeout), interruptExternalConnections: options.InterruptExistConnections, } if len(outbound.tags) == 0 { @@ -79,13 +79,26 @@ func (s *URLTest) Start() error { } outbounds = append(outbounds, detour) } - s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance, s.interruptExternalConnections) + group, err := NewURLTestGroup( + s.ctx, + s.router, + s.logger, + outbounds, + s.link, + s.interval, + s.tolerance, + s.idleTimeout, + s.interruptExternalConnections, + ) + if err != nil { + return err + } + s.group = group return nil } func (s *URLTest) PostStart() error { - s.started = true - go s.CheckOutbounds() + s.group.PostStart() return nil } @@ -103,8 +116,8 @@ func (s *URLTest) All() []string { return s.tags } -func (s *URLTest) URLTest(ctx context.Context, link string) (map[string]uint16, error) { - return s.group.URLTest(ctx, link) +func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) { + return s.group.URLTest(ctx) } func (s *URLTest) CheckOutbounds() { @@ -112,9 +125,7 @@ func (s *URLTest) CheckOutbounds() { } func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - if s.started { - s.group.Start() - } + s.group.Touch() outbound := s.group.Select(network) conn, err := outbound.DialContext(ctx, network, destination) if err == nil { @@ -126,9 +137,7 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M } func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - if s.started { - s.group.Start() - } + s.group.Touch() outbound := s.group.Select(N.NetworkUDP) conn, err := outbound.ListenPacket(ctx, destination) if err == nil { @@ -162,6 +171,7 @@ type URLTestGroup struct { link string interval time.Duration tolerance uint16 + idleTimeout time.Duration history *urltest.HistoryStorage checking atomic.Bool pauseManager pause.Manager @@ -170,9 +180,11 @@ type URLTestGroup struct { interruptGroup *interrupt.Group interruptExternalConnections bool - access sync.Mutex - ticker *time.Ticker - close chan struct{} + access sync.Mutex + ticker *time.Ticker + close chan struct{} + started bool + lastActive atomic.TypedValue[time.Time] } func NewURLTestGroup( @@ -183,14 +195,21 @@ func NewURLTestGroup( link string, interval time.Duration, tolerance uint16, + idleTimeout time.Duration, interruptExternalConnections bool, -) *URLTestGroup { +) (*URLTestGroup, error) { if interval == 0 { interval = C.DefaultURLTestInterval } if tolerance == 0 { tolerance = 50 } + if idleTimeout == 0 { + idleTimeout = C.DefaultURLTestIdleTimeout + } + if interval > idleTimeout { + return nil, E.New("interval must be less or equal than idle_timeout") + } var history *urltest.HistoryStorage if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { } else if clashServer := router.ClashServer(); clashServer != nil { @@ -206,16 +225,27 @@ func NewURLTestGroup( link: link, interval: interval, tolerance: tolerance, + idleTimeout: idleTimeout, history: history, close: make(chan struct{}), pauseManager: pause.ManagerFromContext(ctx), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: interruptExternalConnections, - } + }, nil } -func (g *URLTestGroup) Start() { +func (g *URLTestGroup) PostStart() { + g.started = true + g.lastActive.Store(time.Now()) + go g.CheckOutbounds(false) +} + +func (g *URLTestGroup) Touch() { + if !g.started { + return + } if g.ticker != nil { + g.lastActive.Store(time.Now()) return } g.access.Lock() @@ -266,51 +296,38 @@ func (g *URLTestGroup) Select(network string) adapter.Outbound { return minOutbound } -func (g *URLTestGroup) Fallback(used adapter.Outbound) []adapter.Outbound { - outbounds := make([]adapter.Outbound, 0, len(g.outbounds)-1) - for _, detour := range g.outbounds { - if detour != used { - outbounds = append(outbounds, detour) - } - } - sort.SliceStable(outbounds, func(i, j int) bool { - oi := outbounds[i] - oj := outbounds[j] - hi := g.history.LoadURLTestHistory(RealTag(oi)) - if hi == nil { - return false - } - hj := g.history.LoadURLTestHistory(RealTag(oj)) - if hj == nil { - return false - } - return hi.Delay < hj.Delay - }) - return outbounds -} - func (g *URLTestGroup) loopCheck() { - go g.CheckOutbounds(true) + if time.Now().Sub(g.lastActive.Load()) > g.interval { + g.lastActive.Store(time.Now()) + g.CheckOutbounds(false) + } for { - g.pauseManager.WaitActive() select { case <-g.close: return case <-g.ticker.C: - g.CheckOutbounds(false) } + if time.Now().Sub(g.lastActive.Load()) > g.idleTimeout { + g.access.Lock() + g.ticker.Stop() + g.ticker = nil + g.access.Unlock() + return + } + g.pauseManager.WaitActive() + g.CheckOutbounds(false) } } func (g *URLTestGroup) CheckOutbounds(force bool) { - _, _ = g.urlTest(g.ctx, g.link, force) + _, _ = g.urlTest(g.ctx, force) } -func (g *URLTestGroup) URLTest(ctx context.Context, link string) (map[string]uint16, error) { - return g.urlTest(ctx, link, false) +func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) { + return g.urlTest(ctx, false) } -func (g *URLTestGroup) urlTest(ctx context.Context, link string, force bool) (map[string]uint16, error) { +func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) { result := make(map[string]uint16) if g.checking.Swap(true) { return result, nil @@ -337,7 +354,7 @@ func (g *URLTestGroup) urlTest(ctx context.Context, link string, force bool) (ma b.Go(realTag, func() (any, error) { ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout) defer cancel() - t, err := urltest.URLTest(ctx, link, p) + t, err := urltest.URLTest(ctx, g.link, p) if err != nil { g.logger.Debug("outbound ", tag, " unavailable: ", err) g.history.DeleteURLTestHistory(realTag) diff --git a/route/router.go b/route/router.go index e181d4b9..bb4851e6 100644 --- a/route/router.go +++ b/route/router.go @@ -91,6 +91,7 @@ type Router struct { platformInterface platform.Interface needWIFIState bool wifiState adapter.WIFIState + started bool } func NewRouter( @@ -145,6 +146,9 @@ func NewRouter( router.dnsRules = append(router.dnsRules, dnsRule) } for i, ruleSetOptions := range options.RuleSet { + if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists { + return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag) + } ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) if err != nil { return nil, E.Cause(err, "parse rule-set[", i, "]") @@ -619,6 +623,19 @@ func (r *Router) Close() error { return err } +func (r *Router) PostStart() error { + if len(r.ruleSets) > 0 { + for i, ruleSet := range r.ruleSets { + err := ruleSet.PostStart() + if err != nil { + return E.Cause(err, "post start rule-set[", i, "]") + } + } + } + r.started = true + return nil +} + func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded @@ -1073,8 +1090,11 @@ func (r *Router) notifyNetworkUpdate(event int) { } } - r.ResetNetwork() - return + if !r.started { + return + } + + _ = r.ResetNetwork() } func (r *Router) ResetNetwork() error { diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 9a67f382..8ef9c5da 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -93,6 +93,12 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R s.lastEtag = savedSet.LastEtag } } + if s.lastUpdated.IsZero() { + err := s.fetchOnce(ctx, startContext) + if err != nil { + return E.Cause(err, "initial rule-set: ", s.options.Tag) + } + } s.updateTicker = time.NewTicker(s.updateInterval) go s.loopUpdate() return nil