github.com/v2fly/v2ray-core/v5@v5.16.2-0.20240507031116-8191faa6e095/app/reverse/portal.go (about)

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