github.com/xmplusdev/xmcore@v1.8.11-0.20240412132628-5518b55526af/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/xmcore/common"
    13  	v2net "github.com/xmplusdev/xmcore/common/net"
    14  	"github.com/xmplusdev/xmcore/common/session"
    15  	"github.com/xmplusdev/xmcore/common/signal/done"
    16  	"github.com/xmplusdev/xmcore/common/task"
    17  	"github.com/xmplusdev/xmcore/core"
    18  	"github.com/xmplusdev/xmcore/features/extension"
    19  	"github.com/xmplusdev/xmcore/features/outbound"
    20  	"github.com/xmplusdev/xmcore/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  }