mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-07-19 21:08:40 +08:00
The URL test history update hook and the Clash mode update hook were single-slot: the API service's attached service overwrote the hook set by the daemon, so clients stopped receiving group updates. Replace both with multicast hook lists. Also share a single URL test history storage via context: Clash API looked it up under a key nobody registered and fell back to its own empty storage, so dashboards showed no delay once an API service was configured. Selector changes now notify through the shared storage, covering selections made from any API surface.
136 lines
3.0 KiB
Go
136 lines
3.0 KiB
Go
package urltest
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
"github.com/sagernet/sing/common/ntp"
|
|
"github.com/sagernet/sing/common/observable"
|
|
)
|
|
|
|
type HistoryStorage struct {
|
|
access sync.RWMutex
|
|
delayHistory map[string]*adapter.URLTestHistory
|
|
updateHooks []*observable.Subscriber[struct{}]
|
|
}
|
|
|
|
func NewHistoryStorage() *HistoryStorage {
|
|
return &HistoryStorage{
|
|
delayHistory: make(map[string]*adapter.URLTestHistory),
|
|
}
|
|
}
|
|
|
|
func (s *HistoryStorage) AddUpdateHook(hook *observable.Subscriber[struct{}]) {
|
|
s.access.Lock()
|
|
defer s.access.Unlock()
|
|
s.updateHooks = append(s.updateHooks, hook)
|
|
}
|
|
|
|
func (s *HistoryStorage) NotifyUpdated() {
|
|
s.access.RLock()
|
|
defer s.access.RUnlock()
|
|
s.notifyUpdated()
|
|
}
|
|
|
|
func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
s.access.RLock()
|
|
defer s.access.RUnlock()
|
|
return s.delayHistory[tag]
|
|
}
|
|
|
|
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
|
s.access.Lock()
|
|
delete(s.delayHistory, tag)
|
|
s.notifyUpdated()
|
|
s.access.Unlock()
|
|
}
|
|
|
|
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
|
s.access.Lock()
|
|
s.delayHistory[tag] = history
|
|
s.notifyUpdated()
|
|
s.access.Unlock()
|
|
}
|
|
|
|
func (s *HistoryStorage) notifyUpdated() {
|
|
for _, updateHook := range s.updateHooks {
|
|
updateHook.Emit(struct{}{})
|
|
}
|
|
}
|
|
|
|
func (s *HistoryStorage) Close() error {
|
|
s.access.Lock()
|
|
defer s.access.Unlock()
|
|
s.updateHooks = nil
|
|
return nil
|
|
}
|
|
|
|
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
|
|
if link == "" {
|
|
link = "https://www.gstatic.com/generate_204"
|
|
}
|
|
linkURL, err := url.Parse(link)
|
|
if err != nil {
|
|
return
|
|
}
|
|
hostname := linkURL.Hostname()
|
|
port := linkURL.Port()
|
|
if port == "" {
|
|
switch linkURL.Scheme {
|
|
case "http":
|
|
port = "80"
|
|
case "https":
|
|
port = "443"
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port))
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer instance.Close()
|
|
if N.NeedHandshakeForWrite(instance) {
|
|
start = time.Now()
|
|
}
|
|
req, err := http.NewRequest(http.MethodHead, link, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
client := http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return instance, nil
|
|
},
|
|
TLSClientConfig: &tls.Config{
|
|
Time: ntp.TimeFuncFromContext(ctx),
|
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
|
},
|
|
},
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
Timeout: C.TCPTimeout,
|
|
}
|
|
defer client.CloseIdleConnections()
|
|
resp, err := client.Do(req.WithContext(ctx))
|
|
if err != nil {
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
t = uint16(time.Since(start) / time.Millisecond)
|
|
return
|
|
}
|