diff --git a/doc/server_plugin.md b/doc/server_plugin.md index 6ddef827470..3d54398798c 100644 --- a/doc/server_plugin.md +++ b/doc/server_plugin.md @@ -70,7 +70,7 @@ The response can look like any of the following: ### Operation -Currently `Login`, `NewProxy`, `CloseProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported. +Currently `Login`, `NewProxy`, `ProxyStarted`, `CloseProxy`, `Ping`, `NewWorkConn` and `NewUserConn` operations are supported. #### Login @@ -138,6 +138,28 @@ Create new proxy } ``` +#### ProxyStarted + +Proxy has been successfully started and port allocation is complete. This event is sent **after** the proxy is running, +so it includes the actual allocated port (which may differ from the requested port when `remotePort=0`). + +This is a notification-only event - plugins cannot reject or modify the content since the proxy is already running. + +``` +{ + "content": { + "user": { + "user": , + "metas": mapstring + "run_id": + }, + "proxy_name": , + "proxy_type": , + "remote_addr": // The actual allocated address, e.g., ":6000" + } +} +``` + #### CloseProxy A previously created proxy is closed. diff --git a/pkg/config/v1/validation/validation.go b/pkg/config/v1/validation/validation.go index 4ca6b67f0f0..2ecdffa1a83 100644 --- a/pkg/config/v1/validation/validation.go +++ b/pkg/config/v1/validation/validation.go @@ -51,6 +51,7 @@ var ( SupportedHTTPPluginOps = []string{ splugin.OpLogin, splugin.OpNewProxy, + splugin.OpProxyStarted, splugin.OpCloseProxy, splugin.OpPing, splugin.OpNewWorkConn, diff --git a/pkg/plugin/server/manager.go b/pkg/plugin/server/manager.go index dabfb46cbd0..c5d85c01135 100644 --- a/pkg/plugin/server/manager.go +++ b/pkg/plugin/server/manager.go @@ -25,22 +25,24 @@ import ( ) type Manager struct { - loginPlugins []Plugin - newProxyPlugins []Plugin - closeProxyPlugins []Plugin - pingPlugins []Plugin - newWorkConnPlugins []Plugin - newUserConnPlugins []Plugin + loginPlugins []Plugin + newProxyPlugins []Plugin + proxyStartedPlugins []Plugin + closeProxyPlugins []Plugin + pingPlugins []Plugin + newWorkConnPlugins []Plugin + newUserConnPlugins []Plugin } func NewManager() *Manager { return &Manager{ - loginPlugins: make([]Plugin, 0), - newProxyPlugins: make([]Plugin, 0), - closeProxyPlugins: make([]Plugin, 0), - pingPlugins: make([]Plugin, 0), - newWorkConnPlugins: make([]Plugin, 0), - newUserConnPlugins: make([]Plugin, 0), + loginPlugins: make([]Plugin, 0), + newProxyPlugins: make([]Plugin, 0), + proxyStartedPlugins: make([]Plugin, 0), + closeProxyPlugins: make([]Plugin, 0), + pingPlugins: make([]Plugin, 0), + newWorkConnPlugins: make([]Plugin, 0), + newUserConnPlugins: make([]Plugin, 0), } } @@ -51,6 +53,9 @@ func (m *Manager) Register(p Plugin) { if p.IsSupport(OpNewProxy) { m.newProxyPlugins = append(m.newProxyPlugins, p) } + if p.IsSupport(OpProxyStarted) { + m.proxyStartedPlugins = append(m.proxyStartedPlugins, p) + } if p.IsSupport(OpCloseProxy) { m.closeProxyPlugins = append(m.closeProxyPlugins, p) } @@ -133,6 +138,34 @@ func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) { return content, nil } +// ProxyStarted is called after a proxy has been successfully started and port +// allocation is complete. This is a notification-only event (fire and forget), +// plugins cannot reject or modify the content since the proxy is already running. +func (m *Manager) ProxyStarted(content *ProxyStartedContent) error { + if len(m.proxyStartedPlugins) == 0 { + return nil + } + + errs := make([]string, 0) + reqid, _ := util.RandID() + xl := xlog.New().AppendPrefix("reqid: " + reqid) + ctx := xlog.NewContext(context.Background(), xl) + ctx = NewReqidContext(ctx, reqid) + + for _, p := range m.proxyStartedPlugins { + _, _, err := p.Handle(ctx, OpProxyStarted, *content) + if err != nil { + xl.Warnf("send ProxyStarted request to plugin [%s] error: %v", p.Name(), err) + errs = append(errs, fmt.Sprintf("[%s]: %v", p.Name(), err)) + } + } + + if len(errs) > 0 { + return fmt.Errorf("send ProxyStarted request to plugin errors: %s", strings.Join(errs, "; ")) + } + return nil +} + func (m *Manager) CloseProxy(content *CloseProxyContent) error { if len(m.closeProxyPlugins) == 0 { return nil diff --git a/pkg/plugin/server/plugin.go b/pkg/plugin/server/plugin.go index 3d3c8cfdd65..3330335db12 100644 --- a/pkg/plugin/server/plugin.go +++ b/pkg/plugin/server/plugin.go @@ -21,12 +21,13 @@ import ( const ( APIVersion = "0.1.0" - OpLogin = "Login" - OpNewProxy = "NewProxy" - OpCloseProxy = "CloseProxy" - OpPing = "Ping" - OpNewWorkConn = "NewWorkConn" - OpNewUserConn = "NewUserConn" + OpLogin = "Login" + OpNewProxy = "NewProxy" + OpProxyStarted = "ProxyStarted" + OpCloseProxy = "CloseProxy" + OpPing = "Ping" + OpNewWorkConn = "NewWorkConn" + OpNewUserConn = "NewUserConn" ) type Plugin interface { diff --git a/pkg/plugin/server/types.go b/pkg/plugin/server/types.go index 4a5b7527185..bba863383fd 100644 --- a/pkg/plugin/server/types.go +++ b/pkg/plugin/server/types.go @@ -69,3 +69,13 @@ type NewUserConnContent struct { ProxyType string `json:"proxy_type"` RemoteAddr string `json:"remote_addr"` } + +// ProxyStartedContent is sent after a proxy has been successfully started +// and port allocation is complete. This includes the actual allocated port +// which may differ from the requested port (e.g., when remotePort=0). +type ProxyStartedContent struct { + User UserInfo `json:"user"` + ProxyName string `json:"proxy_name"` + ProxyType string `json:"proxy_type"` + RemoteAddr string `json:"remote_addr"` // The actual allocated address (e.g., ":6000") +} diff --git a/server/control.go b/server/control.go index b70d8d1268a..28149dd315c 100644 --- a/server/control.go +++ b/server/control.go @@ -398,6 +398,21 @@ func (ctl *Control) handleNewProxy(m msg.Message) { resp.RemoteAddr = remoteAddr xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType) metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType) + + // Notify plugins that proxy has started with the allocated port + startedContent := &plugin.ProxyStartedContent{ + User: plugin.UserInfo{ + User: ctl.loginMsg.User, + Metas: ctl.loginMsg.Metas, + RunID: ctl.loginMsg.RunID, + }, + ProxyName: inMsg.ProxyName, + ProxyType: inMsg.ProxyType, + RemoteAddr: remoteAddr, + } + go func() { + _ = ctl.pluginManager.ProxyStarted(startedContent) + }() } _ = ctl.msgDispatcher.Send(resp) }