github.com/v2fly/v2ray-core/v4@v4.45.2/app/reverse/portal.go (about)

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