github.com/xmplusdev/xray-core@v1.8.10/app/observatory/observer.go (about) 1 package observatory 2 3 import ( 4 "context" 5 "net" 6 "net/http" 7 "net/url" 8 "sort" 9 "sync" 10 "time" 11 12 "github.com/xmplusdev/xray-core/common" 13 v2net "github.com/xmplusdev/xray-core/common/net" 14 "github.com/xmplusdev/xray-core/common/session" 15 "github.com/xmplusdev/xray-core/common/signal/done" 16 "github.com/xmplusdev/xray-core/common/task" 17 "github.com/xmplusdev/xray-core/core" 18 "github.com/xmplusdev/xray-core/features/extension" 19 "github.com/xmplusdev/xray-core/features/outbound" 20 "github.com/xmplusdev/xray-core/transport/internet/tagged" 21 "google.golang.org/protobuf/proto" 22 ) 23 24 type Observer struct { 25 config *Config 26 ctx context.Context 27 28 statusLock sync.Mutex 29 status []*OutboundStatus 30 31 finished *done.Instance 32 33 ohm outbound.Manager 34 } 35 36 func (o *Observer) GetObservation(ctx context.Context) (proto.Message, error) { 37 return &ObservationResult{Status: o.status}, nil 38 } 39 40 func (o *Observer) Type() interface{} { 41 return extension.ObservatoryType() 42 } 43 44 func (o *Observer) Start() error { 45 if o.config != nil && len(o.config.SubjectSelector) != 0 { 46 o.finished = done.New() 47 go o.background() 48 } 49 return nil 50 } 51 52 func (o *Observer) Close() error { 53 if o.finished != nil { 54 return o.finished.Close() 55 } 56 return nil 57 } 58 59 func (o *Observer) background() { 60 for !o.finished.Done() { 61 hs, ok := o.ohm.(outbound.HandlerSelector) 62 if !ok { 63 newError("outbound.Manager is not a HandlerSelector").WriteToLog() 64 return 65 } 66 67 outbounds := hs.Select(o.config.SubjectSelector) 68 69 o.updateStatus(outbounds) 70 71 sleepTime := time.Second * 10 72 if o.config.ProbeInterval != 0 { 73 sleepTime = time.Duration(o.config.ProbeInterval) 74 } 75 76 if !o.config.EnableConcurrency { 77 sort.Strings(outbounds) 78 for _, v := range outbounds { 79 result := o.probe(v) 80 o.updateStatusForResult(v, &result) 81 if o.finished.Done() { 82 return 83 } 84 time.Sleep(sleepTime) 85 } 86 continue 87 } 88 89 ch := make(chan struct{}, len(outbounds)) 90 91 for _, v := range outbounds { 92 go func(v string) { 93 result := o.probe(v) 94 o.updateStatusForResult(v, &result) 95 ch <- struct{}{} 96 }(v) 97 } 98 99 for range outbounds { 100 select { 101 case <-ch: 102 case <-o.finished.Wait(): 103 return 104 } 105 } 106 time.Sleep(sleepTime) 107 } 108 } 109 110 func (o *Observer) updateStatus(outbounds []string) { 111 o.statusLock.Lock() 112 defer o.statusLock.Unlock() 113 // TODO should remove old inbound that is removed 114 _ = outbounds 115 } 116 117 func (o *Observer) probe(outbound string) ProbeResult { 118 errorCollectorForRequest := newErrorCollector() 119 120 httpTransport := http.Transport{ 121 Proxy: func(*http.Request) (*url.URL, error) { 122 return nil, nil 123 }, 124 DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { 125 var connection net.Conn 126 taskErr := task.Run(ctx, func() error { 127 // MUST use Xray's built in context system 128 dest, err := v2net.ParseDestination(network + ":" + addr) 129 if err != nil { 130 return newError("cannot understand address").Base(err) 131 } 132 trackedCtx := session.TrackedConnectionError(o.ctx, errorCollectorForRequest) 133 conn, err := tagged.Dialer(trackedCtx, dest, outbound) 134 if err != nil { 135 return newError("cannot dial remote address ", dest).Base(err) 136 } 137 connection = conn 138 return nil 139 }) 140 if taskErr != nil { 141 return nil, newError("cannot finish connection").Base(taskErr) 142 } 143 return connection, nil 144 }, 145 TLSHandshakeTimeout: time.Second * 5, 146 } 147 httpClient := &http.Client{ 148 Transport: &httpTransport, 149 CheckRedirect: func(req *http.Request, via []*http.Request) error { 150 return http.ErrUseLastResponse 151 }, 152 Jar: nil, 153 Timeout: time.Second * 5, 154 } 155 var GETTime time.Duration 156 err := task.Run(o.ctx, func() error { 157 startTime := time.Now() 158 probeURL := "https://www.google.com/generate_204" 159 if o.config.ProbeUrl != "" { 160 probeURL = o.config.ProbeUrl 161 } 162 response, err := httpClient.Get(probeURL) 163 if err != nil { 164 return newError("outbound failed to relay connection").Base(err) 165 } 166 if response.Body != nil { 167 response.Body.Close() 168 } 169 endTime := time.Now() 170 GETTime = endTime.Sub(startTime) 171 return nil 172 }) 173 if err != nil { 174 fullerr := newError("underlying connection failed").Base(errorCollectorForRequest.UnderlyingError()) 175 fullerr = newError("with outbound handler report").Base(fullerr) 176 fullerr = newError("GET request failed:", err).Base(fullerr) 177 fullerr = newError("the outbound ", outbound, " is dead:").Base(fullerr) 178 fullerr = fullerr.AtInfo() 179 fullerr.WriteToLog() 180 return ProbeResult{Alive: false, LastErrorReason: fullerr.Error()} 181 } 182 newError("the outbound ", outbound, " is alive:", GETTime.Seconds()).AtInfo().WriteToLog() 183 return ProbeResult{Alive: true, Delay: GETTime.Milliseconds()} 184 } 185 186 func (o *Observer) updateStatusForResult(outbound string, result *ProbeResult) { 187 o.statusLock.Lock() 188 defer o.statusLock.Unlock() 189 var status *OutboundStatus 190 if location := o.findStatusLocationLockHolderOnly(outbound); location != -1 { 191 status = o.status[location] 192 } else { 193 status = &OutboundStatus{} 194 o.status = append(o.status, status) 195 } 196 197 status.LastTryTime = time.Now().Unix() 198 status.OutboundTag = outbound 199 status.Alive = result.Alive 200 if result.Alive { 201 status.Delay = result.Delay 202 status.LastSeenTime = status.LastTryTime 203 status.LastErrorReason = "" 204 } else { 205 status.LastErrorReason = result.LastErrorReason 206 status.Delay = 99999999 207 } 208 } 209 210 func (o *Observer) findStatusLocationLockHolderOnly(outbound string) int { 211 for i, v := range o.status { 212 if v.OutboundTag == outbound { 213 return i 214 } 215 } 216 return -1 217 } 218 219 func New(ctx context.Context, config *Config) (*Observer, error) { 220 var outboundManager outbound.Manager 221 err := core.RequireFeatures(ctx, func(om outbound.Manager) { 222 outboundManager = om 223 }) 224 if err != nil { 225 return nil, newError("Cannot get depended features").Base(err) 226 } 227 return &Observer{ 228 config: config, 229 ctx: ctx, 230 ohm: outboundManager, 231 }, nil 232 } 233 234 func init() { 235 common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { 236 return New(ctx, config.(*Config)) 237 })) 238 }