github.com/inazumav/sing-box@v0.0.0-20230926072359-ab51429a14f1/outbound/urltest.go (about) 1 package outbound 2 3 import ( 4 "context" 5 "net" 6 "sort" 7 "sync" 8 "time" 9 10 "github.com/inazumav/sing-box/adapter" 11 "github.com/inazumav/sing-box/common/urltest" 12 C "github.com/inazumav/sing-box/constant" 13 "github.com/inazumav/sing-box/log" 14 "github.com/inazumav/sing-box/option" 15 "github.com/sagernet/sing/common" 16 "github.com/sagernet/sing/common/atomic" 17 "github.com/sagernet/sing/common/batch" 18 E "github.com/sagernet/sing/common/exceptions" 19 M "github.com/sagernet/sing/common/metadata" 20 N "github.com/sagernet/sing/common/network" 21 "github.com/sagernet/sing/service" 22 "github.com/sagernet/sing/service/pause" 23 ) 24 25 var ( 26 _ adapter.Outbound = (*URLTest)(nil) 27 _ adapter.OutboundGroup = (*URLTest)(nil) 28 _ adapter.InterfaceUpdateListener = (*URLTest)(nil) 29 ) 30 31 type URLTest struct { 32 myOutboundAdapter 33 ctx context.Context 34 tags []string 35 link string 36 interval time.Duration 37 tolerance uint16 38 group *URLTestGroup 39 } 40 41 func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) { 42 outbound := &URLTest{ 43 myOutboundAdapter: myOutboundAdapter{ 44 protocol: C.TypeURLTest, 45 router: router, 46 logger: logger, 47 tag: tag, 48 dependencies: options.Outbounds, 49 }, 50 ctx: ctx, 51 tags: options.Outbounds, 52 link: options.URL, 53 interval: time.Duration(options.Interval), 54 tolerance: options.Tolerance, 55 } 56 if len(outbound.tags) == 0 { 57 return nil, E.New("missing tags") 58 } 59 return outbound, nil 60 } 61 62 func (s *URLTest) Network() []string { 63 if s.group == nil { 64 return []string{N.NetworkTCP, N.NetworkUDP} 65 } 66 return s.group.Select(N.NetworkTCP).Network() 67 } 68 69 func (s *URLTest) Start() error { 70 outbounds := make([]adapter.Outbound, 0, len(s.tags)) 71 for i, tag := range s.tags { 72 detour, loaded := s.router.Outbound(tag) 73 if !loaded { 74 return E.New("outbound ", i, " not found: ", tag) 75 } 76 outbounds = append(outbounds, detour) 77 } 78 s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance) 79 return nil 80 } 81 82 func (s *URLTest) PostStart() error { 83 go s.CheckOutbounds() 84 return nil 85 } 86 87 func (s *URLTest) Close() error { 88 return common.Close( 89 common.PtrOrNil(s.group), 90 ) 91 } 92 93 func (s *URLTest) Now() string { 94 return s.group.Select(N.NetworkTCP).Tag() 95 } 96 97 func (s *URLTest) All() []string { 98 return s.tags 99 } 100 101 func (s *URLTest) URLTest(ctx context.Context, link string) (map[string]uint16, error) { 102 return s.group.URLTest(ctx, link) 103 } 104 105 func (s *URLTest) CheckOutbounds() { 106 s.group.CheckOutbounds(true) 107 } 108 109 func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { 110 s.group.Start() 111 outbound := s.group.Select(network) 112 conn, err := outbound.DialContext(ctx, network, destination) 113 if err == nil { 114 return conn, nil 115 } 116 s.logger.ErrorContext(ctx, err) 117 s.group.history.DeleteURLTestHistory(outbound.Tag()) 118 return nil, err 119 } 120 121 func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { 122 s.group.Start() 123 outbound := s.group.Select(N.NetworkUDP) 124 conn, err := outbound.ListenPacket(ctx, destination) 125 if err == nil { 126 return conn, nil 127 } 128 s.logger.ErrorContext(ctx, err) 129 s.group.history.DeleteURLTestHistory(outbound.Tag()) 130 return nil, err 131 } 132 133 func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { 134 return NewConnection(ctx, s, conn, metadata) 135 } 136 137 func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { 138 return NewPacketConnection(ctx, s, conn, metadata) 139 } 140 141 func (s *URLTest) InterfaceUpdated() { 142 go s.group.CheckOutbounds(true) 143 return 144 } 145 146 type URLTestGroup struct { 147 ctx context.Context 148 router adapter.Router 149 logger log.Logger 150 outbounds []adapter.Outbound 151 link string 152 interval time.Duration 153 tolerance uint16 154 history *urltest.HistoryStorage 155 checking atomic.Bool 156 pauseManager pause.Manager 157 158 access sync.Mutex 159 ticker *time.Ticker 160 close chan struct{} 161 } 162 163 func NewURLTestGroup(ctx context.Context, router adapter.Router, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16) *URLTestGroup { 164 if interval == 0 { 165 interval = C.DefaultURLTestInterval 166 } 167 if tolerance == 0 { 168 tolerance = 50 169 } 170 var history *urltest.HistoryStorage 171 if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { 172 } else if clashServer := router.ClashServer(); clashServer != nil { 173 history = clashServer.HistoryStorage() 174 } else { 175 history = urltest.NewHistoryStorage() 176 } 177 return &URLTestGroup{ 178 ctx: ctx, 179 router: router, 180 logger: logger, 181 outbounds: outbounds, 182 link: link, 183 interval: interval, 184 tolerance: tolerance, 185 history: history, 186 close: make(chan struct{}), 187 pauseManager: pause.ManagerFromContext(ctx), 188 } 189 } 190 191 func (g *URLTestGroup) Start() { 192 if g.ticker != nil { 193 return 194 } 195 g.access.Lock() 196 defer g.access.Unlock() 197 if g.ticker != nil { 198 return 199 } 200 g.ticker = time.NewTicker(g.interval) 201 go g.loopCheck() 202 } 203 204 func (g *URLTestGroup) Close() error { 205 if g.ticker == nil { 206 return nil 207 } 208 g.ticker.Stop() 209 close(g.close) 210 return nil 211 } 212 213 func (g *URLTestGroup) Select(network string) adapter.Outbound { 214 var minDelay uint16 215 var minTime time.Time 216 var minOutbound adapter.Outbound 217 for _, detour := range g.outbounds { 218 if !common.Contains(detour.Network(), network) { 219 continue 220 } 221 history := g.history.LoadURLTestHistory(RealTag(detour)) 222 if history == nil { 223 continue 224 } 225 if minDelay == 0 || minDelay > history.Delay+g.tolerance || minDelay > history.Delay-g.tolerance && minTime.Before(history.Time) { 226 minDelay = history.Delay 227 minTime = history.Time 228 minOutbound = detour 229 } 230 } 231 if minOutbound == nil { 232 for _, detour := range g.outbounds { 233 if !common.Contains(detour.Network(), network) { 234 continue 235 } 236 minOutbound = detour 237 break 238 } 239 } 240 return minOutbound 241 } 242 243 func (g *URLTestGroup) Fallback(used adapter.Outbound) []adapter.Outbound { 244 outbounds := make([]adapter.Outbound, 0, len(g.outbounds)-1) 245 for _, detour := range g.outbounds { 246 if detour != used { 247 outbounds = append(outbounds, detour) 248 } 249 } 250 sort.SliceStable(outbounds, func(i, j int) bool { 251 oi := outbounds[i] 252 oj := outbounds[j] 253 hi := g.history.LoadURLTestHistory(RealTag(oi)) 254 if hi == nil { 255 return false 256 } 257 hj := g.history.LoadURLTestHistory(RealTag(oj)) 258 if hj == nil { 259 return false 260 } 261 return hi.Delay < hj.Delay 262 }) 263 return outbounds 264 } 265 266 func (g *URLTestGroup) loopCheck() { 267 go g.CheckOutbounds(true) 268 for { 269 g.pauseManager.WaitActive() 270 select { 271 case <-g.close: 272 return 273 case <-g.ticker.C: 274 g.CheckOutbounds(false) 275 } 276 } 277 } 278 279 func (g *URLTestGroup) CheckOutbounds(force bool) { 280 _, _ = g.urlTest(g.ctx, g.link, force) 281 } 282 283 func (g *URLTestGroup) URLTest(ctx context.Context, link string) (map[string]uint16, error) { 284 return g.urlTest(ctx, link, false) 285 } 286 287 func (g *URLTestGroup) urlTest(ctx context.Context, link string, force bool) (map[string]uint16, error) { 288 result := make(map[string]uint16) 289 if g.checking.Swap(true) { 290 return result, nil 291 } 292 defer g.checking.Store(false) 293 b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) 294 checked := make(map[string]bool) 295 var resultAccess sync.Mutex 296 for _, detour := range g.outbounds { 297 tag := detour.Tag() 298 realTag := RealTag(detour) 299 if checked[realTag] { 300 continue 301 } 302 history := g.history.LoadURLTestHistory(realTag) 303 if !force && history != nil && time.Now().Sub(history.Time) < g.interval { 304 continue 305 } 306 checked[realTag] = true 307 p, loaded := g.router.Outbound(realTag) 308 if !loaded { 309 continue 310 } 311 b.Go(realTag, func() (any, error) { 312 ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout) 313 defer cancel() 314 t, err := urltest.URLTest(ctx, link, p) 315 if err != nil { 316 g.logger.Debug("outbound ", tag, " unavailable: ", err) 317 g.history.DeleteURLTestHistory(realTag) 318 } else { 319 g.logger.Debug("outbound ", tag, " available: ", t, "ms") 320 g.history.StoreURLTestHistory(realTag, &urltest.History{ 321 Time: time.Now(), 322 Delay: t, 323 }) 324 resultAccess.Lock() 325 result[tag] = t 326 resultAccess.Unlock() 327 } 328 return nil, nil 329 }) 330 } 331 b.Wait() 332 return result, nil 333 }