github.com/EagleQL/Xray-core@v1.4.3/app/reverse/portal.go (about)

     1  package reverse
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/golang/protobuf/proto"
     9  	"github.com/xtls/xray-core/common"
    10  	"github.com/xtls/xray-core/common/buf"
    11  	"github.com/xtls/xray-core/common/mux"
    12  	"github.com/xtls/xray-core/common/net"
    13  	"github.com/xtls/xray-core/common/session"
    14  	"github.com/xtls/xray-core/common/task"
    15  	"github.com/xtls/xray-core/features/outbound"
    16  	"github.com/xtls/xray-core/transport"
    17  	"github.com/xtls/xray-core/transport/pipe"
    18  )
    19  
    20  type Portal struct {
    21  	ohm    outbound.Manager
    22  	tag    string
    23  	domain string
    24  	picker *StaticMuxPicker
    25  	client *mux.ClientManager
    26  }
    27  
    28  func NewPortal(config *PortalConfig, ohm outbound.Manager) (*Portal, error) {
    29  	if config.Tag == "" {
    30  		return nil, newError("portal tag is empty")
    31  	}
    32  
    33  	if config.Domain == "" {
    34  		return nil, newError("portal domain is empty")
    35  	}
    36  
    37  	picker, err := NewStaticMuxPicker()
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	return &Portal{
    43  		ohm:    ohm,
    44  		tag:    config.Tag,
    45  		domain: config.Domain,
    46  		picker: picker,
    47  		client: &mux.ClientManager{
    48  			Picker: picker,
    49  		},
    50  	}, nil
    51  }
    52  
    53  func (p *Portal) Start() error {
    54  	return p.ohm.AddHandler(context.Background(), &Outbound{
    55  		portal: p,
    56  		tag:    p.tag,
    57  	})
    58  }
    59  
    60  func (p *Portal) Close() error {
    61  	return p.ohm.RemoveHandler(context.Background(), p.tag)
    62  }
    63  
    64  func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) error {
    65  	outboundMeta := session.OutboundFromContext(ctx)
    66  	if outboundMeta == nil {
    67  		return newError("outbound metadata not found").AtError()
    68  	}
    69  
    70  	if isDomain(outboundMeta.Target, p.domain) {
    71  		muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
    72  		if err != nil {
    73  			return newError("failed to create mux client worker").Base(err).AtWarning()
    74  		}
    75  
    76  		worker, err := NewPortalWorker(muxClient)
    77  		if err != nil {
    78  			return newError("failed to create portal worker").Base(err)
    79  		}
    80  
    81  		p.picker.AddWorker(worker)
    82  		return nil
    83  	}
    84  
    85  	return p.client.Dispatch(ctx, link)
    86  }
    87  
    88  type Outbound struct {
    89  	portal *Portal
    90  	tag    string
    91  }
    92  
    93  func (o *Outbound) Tag() string {
    94  	return o.tag
    95  }
    96  
    97  func (o *Outbound) Dispatch(ctx context.Context, link *transport.Link) {
    98  	if err := o.portal.HandleConnection(ctx, link); err != nil {
    99  		newError("failed to process reverse connection").Base(err).WriteToLog(session.ExportIDToError(ctx))
   100  		common.Interrupt(link.Writer)
   101  	}
   102  }
   103  
   104  func (o *Outbound) Start() error {
   105  	return nil
   106  }
   107  
   108  func (o *Outbound) Close() error {
   109  	return nil
   110  }
   111  
   112  type StaticMuxPicker struct {
   113  	access  sync.Mutex
   114  	workers []*PortalWorker
   115  	cTask   *task.Periodic
   116  }
   117  
   118  func NewStaticMuxPicker() (*StaticMuxPicker, error) {
   119  	p := &StaticMuxPicker{}
   120  	p.cTask = &task.Periodic{
   121  		Execute:  p.cleanup,
   122  		Interval: time.Second * 30,
   123  	}
   124  	p.cTask.Start()
   125  	return p, nil
   126  }
   127  
   128  func (p *StaticMuxPicker) cleanup() error {
   129  	p.access.Lock()
   130  	defer p.access.Unlock()
   131  
   132  	var activeWorkers []*PortalWorker
   133  	for _, w := range p.workers {
   134  		if !w.Closed() {
   135  			activeWorkers = append(activeWorkers, w)
   136  		}
   137  	}
   138  
   139  	if len(activeWorkers) != len(p.workers) {
   140  		p.workers = activeWorkers
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func (p *StaticMuxPicker) PickAvailable() (*mux.ClientWorker, error) {
   147  	p.access.Lock()
   148  	defer p.access.Unlock()
   149  
   150  	if len(p.workers) == 0 {
   151  		return nil, newError("empty worker list")
   152  	}
   153  
   154  	var minIdx int = -1
   155  	var minConn uint32 = 9999
   156  	for i, w := range p.workers {
   157  		if w.draining {
   158  			continue
   159  		}
   160  		if w.client.Closed() {
   161  			continue
   162  		}
   163  		if w.client.ActiveConnections() < minConn {
   164  			minConn = w.client.ActiveConnections()
   165  			minIdx = i
   166  		}
   167  	}
   168  
   169  	if minIdx == -1 {
   170  		for i, w := range p.workers {
   171  			if w.IsFull() {
   172  				continue
   173  			}
   174  			if w.client.ActiveConnections() < minConn {
   175  				minConn = w.client.ActiveConnections()
   176  				minIdx = i
   177  			}
   178  		}
   179  	}
   180  
   181  	if minIdx != -1 {
   182  		return p.workers[minIdx].client, nil
   183  	}
   184  
   185  	return nil, newError("no mux client worker available")
   186  }
   187  
   188  func (p *StaticMuxPicker) AddWorker(worker *PortalWorker) {
   189  	p.access.Lock()
   190  	defer p.access.Unlock()
   191  
   192  	p.workers = append(p.workers, worker)
   193  }
   194  
   195  type PortalWorker struct {
   196  	client   *mux.ClientWorker
   197  	control  *task.Periodic
   198  	writer   buf.Writer
   199  	reader   buf.Reader
   200  	draining bool
   201  }
   202  
   203  func NewPortalWorker(client *mux.ClientWorker) (*PortalWorker, error) {
   204  	opt := []pipe.Option{pipe.WithSizeLimit(16 * 1024)}
   205  	uplinkReader, uplinkWriter := pipe.New(opt...)
   206  	downlinkReader, downlinkWriter := pipe.New(opt...)
   207  
   208  	ctx := context.Background()
   209  	ctx = session.ContextWithOutbound(ctx, &session.Outbound{
   210  		Target: net.UDPDestination(net.DomainAddress(internalDomain), 0),
   211  	})
   212  	f := client.Dispatch(ctx, &transport.Link{
   213  		Reader: uplinkReader,
   214  		Writer: downlinkWriter,
   215  	})
   216  	if !f {
   217  		return nil, newError("unable to dispatch control connection")
   218  	}
   219  	w := &PortalWorker{
   220  		client: client,
   221  		reader: downlinkReader,
   222  		writer: uplinkWriter,
   223  	}
   224  	w.control = &task.Periodic{
   225  		Execute:  w.heartbeat,
   226  		Interval: time.Second * 2,
   227  	}
   228  	w.control.Start()
   229  	return w, nil
   230  }
   231  
   232  func (w *PortalWorker) heartbeat() error {
   233  	if w.client.Closed() {
   234  		return newError("client worker stopped")
   235  	}
   236  
   237  	if w.draining || w.writer == nil {
   238  		return newError("already disposed")
   239  	}
   240  
   241  	msg := &Control{}
   242  	msg.FillInRandom()
   243  
   244  	if w.client.TotalConnections() > 256 {
   245  		w.draining = true
   246  		msg.State = Control_DRAIN
   247  
   248  		defer func() {
   249  			common.Close(w.writer)
   250  			common.Interrupt(w.reader)
   251  			w.writer = nil
   252  		}()
   253  	}
   254  
   255  	b, err := proto.Marshal(msg)
   256  	common.Must(err)
   257  	mb := buf.MergeBytes(nil, b)
   258  	return w.writer.WriteMultiBuffer(mb)
   259  }
   260  
   261  func (w *PortalWorker) IsFull() bool {
   262  	return w.client.IsFull()
   263  }
   264  
   265  func (w *PortalWorker) Closed() bool {
   266  	return w.client.Closed()
   267  }