github.com/sagernet/sing-box@v1.9.0-rc.20/outbound/urltest.go (about) 1 package outbound 2 3 import ( 4 "context" 5 "net" 6 "sync" 7 "time" 8 9 "github.com/sagernet/sing-box/adapter" 10 "github.com/sagernet/sing-box/common/interrupt" 11 "github.com/sagernet/sing-box/common/urltest" 12 C "github.com/sagernet/sing-box/constant" 13 "github.com/sagernet/sing-box/log" 14 "github.com/sagernet/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 idleTimeout time.Duration 39 group *URLTestGroup 40 interruptExternalConnections bool 41 } 42 43 func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) { 44 outbound := &URLTest{ 45 myOutboundAdapter: myOutboundAdapter{ 46 protocol: C.TypeURLTest, 47 network: []string{N.NetworkTCP, N.NetworkUDP}, 48 router: router, 49 logger: logger, 50 tag: tag, 51 dependencies: options.Outbounds, 52 }, 53 ctx: ctx, 54 tags: options.Outbounds, 55 link: options.URL, 56 interval: time.Duration(options.Interval), 57 tolerance: options.Tolerance, 58 idleTimeout: time.Duration(options.IdleTimeout), 59 interruptExternalConnections: options.InterruptExistConnections, 60 } 61 if len(outbound.tags) == 0 { 62 return nil, E.New("missing tags") 63 } 64 return outbound, nil 65 } 66 67 func (s *URLTest) Start() error { 68 outbounds := make([]adapter.Outbound, 0, len(s.tags)) 69 for i, tag := range s.tags { 70 detour, loaded := s.router.Outbound(tag) 71 if !loaded { 72 return E.New("outbound ", i, " not found: ", tag) 73 } 74 outbounds = append(outbounds, detour) 75 } 76 group, err := NewURLTestGroup( 77 s.ctx, 78 s.router, 79 s.logger, 80 outbounds, 81 s.link, 82 s.interval, 83 s.tolerance, 84 s.idleTimeout, 85 s.interruptExternalConnections, 86 ) 87 if err != nil { 88 return err 89 } 90 s.group = group 91 return nil 92 } 93 94 func (s *URLTest) PostStart() error { 95 s.group.PostStart() 96 return nil 97 } 98 99 func (s *URLTest) Close() error { 100 return common.Close( 101 common.PtrOrNil(s.group), 102 ) 103 } 104 105 func (s *URLTest) Now() string { 106 if s.group.selectedOutboundTCP != nil { 107 return s.group.selectedOutboundTCP.Tag() 108 } else if s.group.selectedOutboundUDP != nil { 109 return s.group.selectedOutboundUDP.Tag() 110 } 111 return "" 112 } 113 114 func (s *URLTest) All() []string { 115 return s.tags 116 } 117 118 func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) { 119 return s.group.URLTest(ctx) 120 } 121 122 func (s *URLTest) CheckOutbounds() { 123 s.group.CheckOutbounds(true) 124 } 125 126 func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { 127 s.group.Touch() 128 var outbound adapter.Outbound 129 switch N.NetworkName(network) { 130 case N.NetworkTCP: 131 outbound = s.group.selectedOutboundTCP 132 case N.NetworkUDP: 133 outbound = s.group.selectedOutboundUDP 134 default: 135 return nil, E.Extend(N.ErrUnknownNetwork, network) 136 } 137 if outbound == nil { 138 outbound, _ = s.group.Select(network) 139 } 140 if outbound == nil { 141 return nil, E.New("missing supported outbound") 142 } 143 conn, err := outbound.DialContext(ctx, network, destination) 144 if err == nil { 145 return s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil 146 } 147 s.logger.ErrorContext(ctx, err) 148 s.group.history.DeleteURLTestHistory(outbound.Tag()) 149 return nil, err 150 } 151 152 func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { 153 s.group.Touch() 154 outbound := s.group.selectedOutboundUDP 155 if outbound == nil { 156 outbound, _ = s.group.Select(N.NetworkUDP) 157 } 158 if outbound == nil { 159 return nil, E.New("missing supported outbound") 160 } 161 conn, err := outbound.ListenPacket(ctx, destination) 162 if err == nil { 163 return s.group.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil 164 } 165 s.logger.ErrorContext(ctx, err) 166 s.group.history.DeleteURLTestHistory(outbound.Tag()) 167 return nil, err 168 } 169 170 func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { 171 ctx = interrupt.ContextWithIsExternalConnection(ctx) 172 return NewConnection(ctx, s, conn, metadata) 173 } 174 175 func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { 176 ctx = interrupt.ContextWithIsExternalConnection(ctx) 177 return NewPacketConnection(ctx, s, conn, metadata) 178 } 179 180 func (s *URLTest) InterfaceUpdated() { 181 go s.group.CheckOutbounds(true) 182 return 183 } 184 185 type URLTestGroup struct { 186 ctx context.Context 187 router adapter.Router 188 logger log.Logger 189 outbounds []adapter.Outbound 190 link string 191 interval time.Duration 192 tolerance uint16 193 idleTimeout time.Duration 194 history *urltest.HistoryStorage 195 checking atomic.Bool 196 pauseManager pause.Manager 197 selectedOutboundTCP adapter.Outbound 198 selectedOutboundUDP adapter.Outbound 199 interruptGroup *interrupt.Group 200 interruptExternalConnections bool 201 202 access sync.Mutex 203 ticker *time.Ticker 204 close chan struct{} 205 started bool 206 lastActive atomic.TypedValue[time.Time] 207 } 208 209 func NewURLTestGroup( 210 ctx context.Context, 211 router adapter.Router, 212 logger log.Logger, 213 outbounds []adapter.Outbound, 214 link string, 215 interval time.Duration, 216 tolerance uint16, 217 idleTimeout time.Duration, 218 interruptExternalConnections bool, 219 ) (*URLTestGroup, error) { 220 if interval == 0 { 221 interval = C.DefaultURLTestInterval 222 } 223 if tolerance == 0 { 224 tolerance = 50 225 } 226 if idleTimeout == 0 { 227 idleTimeout = C.DefaultURLTestIdleTimeout 228 } 229 if interval > idleTimeout { 230 return nil, E.New("interval must be less or equal than idle_timeout") 231 } 232 var history *urltest.HistoryStorage 233 if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { 234 } else if clashServer := router.ClashServer(); clashServer != nil { 235 history = clashServer.HistoryStorage() 236 } else { 237 history = urltest.NewHistoryStorage() 238 } 239 return &URLTestGroup{ 240 ctx: ctx, 241 router: router, 242 logger: logger, 243 outbounds: outbounds, 244 link: link, 245 interval: interval, 246 tolerance: tolerance, 247 idleTimeout: idleTimeout, 248 history: history, 249 close: make(chan struct{}), 250 pauseManager: service.FromContext[pause.Manager](ctx), 251 interruptGroup: interrupt.NewGroup(), 252 interruptExternalConnections: interruptExternalConnections, 253 }, nil 254 } 255 256 func (g *URLTestGroup) PostStart() { 257 g.started = true 258 g.lastActive.Store(time.Now()) 259 go g.CheckOutbounds(false) 260 } 261 262 func (g *URLTestGroup) Touch() { 263 if !g.started { 264 return 265 } 266 if g.ticker != nil { 267 g.lastActive.Store(time.Now()) 268 return 269 } 270 g.access.Lock() 271 defer g.access.Unlock() 272 if g.ticker != nil { 273 return 274 } 275 g.ticker = time.NewTicker(g.interval) 276 go g.loopCheck() 277 } 278 279 func (g *URLTestGroup) Close() error { 280 if g.ticker == nil { 281 return nil 282 } 283 g.ticker.Stop() 284 close(g.close) 285 return nil 286 } 287 288 func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) { 289 var minDelay uint16 290 var minOutbound adapter.Outbound 291 switch network { 292 case N.NetworkTCP: 293 if g.selectedOutboundTCP != nil { 294 if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundTCP)); history != nil { 295 minOutbound = g.selectedOutboundTCP 296 minDelay = history.Delay 297 } 298 } 299 case N.NetworkUDP: 300 if g.selectedOutboundUDP != nil { 301 if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundUDP)); history != nil { 302 minOutbound = g.selectedOutboundUDP 303 minDelay = history.Delay 304 } 305 } 306 } 307 for _, detour := range g.outbounds { 308 if !common.Contains(detour.Network(), network) { 309 continue 310 } 311 history := g.history.LoadURLTestHistory(RealTag(detour)) 312 if history == nil { 313 continue 314 } 315 if minDelay == 0 || minDelay > history.Delay+g.tolerance { 316 minDelay = history.Delay 317 minOutbound = detour 318 } 319 } 320 if minOutbound == nil { 321 for _, detour := range g.outbounds { 322 if !common.Contains(detour.Network(), network) { 323 continue 324 } 325 return detour, false 326 } 327 return nil, false 328 } 329 return minOutbound, true 330 } 331 332 func (g *URLTestGroup) loopCheck() { 333 if time.Now().Sub(g.lastActive.Load()) > g.interval { 334 g.lastActive.Store(time.Now()) 335 g.CheckOutbounds(false) 336 } 337 for { 338 select { 339 case <-g.close: 340 return 341 case <-g.ticker.C: 342 } 343 if time.Now().Sub(g.lastActive.Load()) > g.idleTimeout { 344 g.access.Lock() 345 g.ticker.Stop() 346 g.ticker = nil 347 g.access.Unlock() 348 return 349 } 350 g.pauseManager.WaitActive() 351 g.CheckOutbounds(false) 352 } 353 } 354 355 func (g *URLTestGroup) CheckOutbounds(force bool) { 356 _, _ = g.urlTest(g.ctx, force) 357 } 358 359 func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) { 360 return g.urlTest(ctx, false) 361 } 362 363 func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) { 364 result := make(map[string]uint16) 365 if g.checking.Swap(true) { 366 return result, nil 367 } 368 defer g.checking.Store(false) 369 b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) 370 checked := make(map[string]bool) 371 var resultAccess sync.Mutex 372 for _, detour := range g.outbounds { 373 tag := detour.Tag() 374 realTag := RealTag(detour) 375 if checked[realTag] { 376 continue 377 } 378 history := g.history.LoadURLTestHistory(realTag) 379 if !force && history != nil && time.Now().Sub(history.Time) < g.interval { 380 continue 381 } 382 checked[realTag] = true 383 p, loaded := g.router.Outbound(realTag) 384 if !loaded { 385 continue 386 } 387 b.Go(realTag, func() (any, error) { 388 ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout) 389 defer cancel() 390 t, err := urltest.URLTest(ctx, g.link, p) 391 if err != nil { 392 g.logger.Debug("outbound ", tag, " unavailable: ", err) 393 g.history.DeleteURLTestHistory(realTag) 394 } else { 395 g.logger.Debug("outbound ", tag, " available: ", t, "ms") 396 g.history.StoreURLTestHistory(realTag, &urltest.History{ 397 Time: time.Now(), 398 Delay: t, 399 }) 400 resultAccess.Lock() 401 result[tag] = t 402 resultAccess.Unlock() 403 } 404 return nil, nil 405 }) 406 } 407 b.Wait() 408 g.performUpdateCheck() 409 return result, nil 410 } 411 412 func (g *URLTestGroup) performUpdateCheck() { 413 var updated bool 414 if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) { 415 g.selectedOutboundTCP = outbound 416 updated = true 417 } 418 if outbound, exists := g.Select(N.NetworkUDP); outbound != nil && (g.selectedOutboundUDP == nil || (exists && outbound != g.selectedOutboundUDP)) { 419 g.selectedOutboundUDP = outbound 420 updated = true 421 } 422 if updated { 423 g.interruptGroup.Interrupt(g.interruptExternalConnections) 424 } 425 }