github.com/xmplusdev/xmcore@v1.8.11-0.20240412132628-5518b55526af/app/reverse/portal.go (about) 1 package reverse 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/xmplusdev/xmcore/common" 9 "github.com/xmplusdev/xmcore/common/buf" 10 "github.com/xmplusdev/xmcore/common/mux" 11 "github.com/xmplusdev/xmcore/common/net" 12 "github.com/xmplusdev/xmcore/common/session" 13 "github.com/xmplusdev/xmcore/common/task" 14 "github.com/xmplusdev/xmcore/features/outbound" 15 "github.com/xmplusdev/xmcore/transport" 16 "github.com/xmplusdev/xmcore/transport/pipe" 17 "google.golang.org/protobuf/proto" 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 }