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