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