From a57679f8375986abc970d22bad52644ba62a4969 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Dec 2019 21:01:58 +0800 Subject: [PATCH 1/5] support meta info for client and proxy --- conf/frpc_full.ini | 7 +++++++ models/config/client_common.go | 8 ++++++++ models/config/proxy.go | 14 +++++++++++++- models/msg/msg.go | 30 ++++++++++++++++-------------- models/plugin/http2https.go | 3 +-- models/plugin/https2http.go | 2 +- 6 files changed, 46 insertions(+), 18 deletions(-) diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 14ca6ed33de..8c86acba84f 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -64,6 +64,10 @@ tls_enable = true # heartbeat_interval = 30 # heartbeat_timeout = 90 +# additional meta info for client +meta_var1 = 123 +meta_var2 = 234 + # 'ssh' is the unique proxy name # if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh' [ssh] @@ -92,6 +96,9 @@ health_check_timeout_s = 3 health_check_max_failed = 3 # every 10 seconds will do a health check health_check_interval_s = 10 +# additional meta info for each proxy +meta_var1 = 123 +meta_var2 = 234 [ssh_random] type = tcp diff --git a/models/config/client_common.go b/models/config/client_common.go index fe87e08d06c..2b5006b43d0 100644 --- a/models/config/client_common.go +++ b/models/config/client_common.go @@ -115,6 +115,8 @@ type ClientCommonConf struct { // before the connection is terminated, in seconds. It is not recommended // to change this value. By default, this value is 90. HeartBeatTimeout int64 `json:"heartbeat_timeout"` + // Client meta info + Metas map[string]string `json:"metas"` } // GetDefaultClientConf returns a client configuration with default values. @@ -144,6 +146,7 @@ func GetDefaultClientConf() ClientCommonConf { TLSEnable: false, HeartBeatInterval: 30, HeartBeatTimeout: 90, + Metas: make(map[string]string), } } @@ -294,6 +297,11 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error cfg.HeartBeatInterval = v } } + for k, v := range conf.Section("common") { + if strings.HasPrefix(k, "meta_") { + cfg.Metas[strings.TrimPrefix(k, "meta_")] = v + } + } return } diff --git a/models/config/proxy.go b/models/config/proxy.go index 85c353ed9ca..1efaf38a57b 100644 --- a/models/config/proxy.go +++ b/models/config/proxy.go @@ -130,6 +130,9 @@ type BaseProxyConf struct { // 0 means no limit BandwidthLimit BandwidthQuantity `json:"bandwidth_limit"` + // meta info for each proxy + Metas map[string]string `json:"metas"` + LocalSvrConf HealthCheckConf } @@ -146,7 +149,8 @@ func (cfg *BaseProxyConf) compare(cmp *BaseProxyConf) bool { cfg.Group != cmp.Group || cfg.GroupKey != cmp.GroupKey || cfg.ProxyProtocolVersion != cmp.ProxyProtocolVersion || - cfg.BandwidthLimit.Equal(&cmp.BandwidthLimit) { + cfg.BandwidthLimit.Equal(&cmp.BandwidthLimit) || + !reflect.DeepEqual(cfg.Metas, cmp.Metas) { return false } if !cfg.LocalSvrConf.compare(&cmp.LocalSvrConf) { @@ -165,6 +169,7 @@ func (cfg *BaseProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { cfg.UseCompression = pMsg.UseCompression cfg.Group = pMsg.Group cfg.GroupKey = pMsg.GroupKey + cfg.Metas = pMsg.Metas } func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) error { @@ -212,6 +217,12 @@ func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section i } cfg.HealthCheckUrl = s + cfg.HealthCheckUrl } + + for k, v := range section { + if strings.HasPrefix(k, "meta_") { + cfg.Metas[strings.TrimPrefix(k, "meta_")] = v + } + } return nil } @@ -222,6 +233,7 @@ func (cfg *BaseProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { pMsg.UseCompression = cfg.UseCompression pMsg.Group = cfg.Group pMsg.GroupKey = cfg.GroupKey + pMsg.Metas = cfg.Metas } func (cfg *BaseProxyConf) checkForCli() (err error) { diff --git a/models/msg/msg.go b/models/msg/msg.go index 11d2542f56d..ce41c9ec5a7 100644 --- a/models/msg/msg.go +++ b/models/msg/msg.go @@ -62,14 +62,15 @@ var ( // When frpc start, client send this message to login to server. type Login struct { - Version string `json:"version"` - Hostname string `json:"hostname"` - Os string `json:"os"` - Arch string `json:"arch"` - User string `json:"user"` - PrivilegeKey string `json:"privilege_key"` - Timestamp int64 `json:"timestamp"` - RunId string `json:"run_id"` + Version string `json:"version"` + Hostname string `json:"hostname"` + Os string `json:"os"` + Arch string `json:"arch"` + User string `json:"user"` + PrivilegeKey string `json:"privilege_key"` + Timestamp int64 `json:"timestamp"` + RunId string `json:"run_id"` + Metas map[string]string `json:"metas"` // Some global configures. PoolCount int `json:"pool_count"` @@ -84,12 +85,13 @@ type LoginResp struct { // When frpc login success, send this message to frps for running a new proxy. type NewProxy struct { - ProxyName string `json:"proxy_name"` - ProxyType string `json:"proxy_type"` - UseEncryption bool `json:"use_encryption"` - UseCompression bool `json:"use_compression"` - Group string `json:"group"` - GroupKey string `json:"group_key"` + ProxyName string `json:"proxy_name"` + ProxyType string `json:"proxy_type"` + UseEncryption bool `json:"use_encryption"` + UseCompression bool `json:"use_compression"` + Group string `json:"group"` + GroupKey string `json:"group_key"` + Metas map[string]string `json:"metas"` // tcp and udp only RemotePort int `json:"remote_port"` diff --git a/models/plugin/http2https.go b/models/plugin/http2https.go index 6f5965e6acf..570b3f9c670 100644 --- a/models/plugin/http2https.go +++ b/models/plugin/http2https.go @@ -94,7 +94,6 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) { return p, nil } - func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) p.l.PutConn(wrapConn) @@ -105,7 +104,7 @@ func (p *HTTP2HTTPSPlugin) Name() string { } func (p *HTTP2HTTPSPlugin) Close() error { - if err := p.s.Close();err != nil { + if err := p.s.Close(); err != nil { return err } return nil diff --git a/models/plugin/https2http.go b/models/plugin/https2http.go index af5b6af9b75..093a74f4e52 100644 --- a/models/plugin/https2http.go +++ b/models/plugin/https2http.go @@ -126,7 +126,7 @@ func (p *HTTPS2HTTPPlugin) Name() string { } func (p *HTTPS2HTTPPlugin) Close() error { - if err := p.s.Close();err != nil { + if err := p.s.Close(); err != nil { return err } return nil From 91e46a2c5318b66088b169d9ea91b5bd261e5bf5 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 20 Dec 2019 20:28:28 +0800 Subject: [PATCH 2/5] support server plugin feature --- client/proxy/proxy.go | 2 +- client/service.go | 1 + conf/frps_full.ini | 10 ++ models/config/server_common.go | 24 ++++ models/plugin/{ => client}/http2https.go | 0 models/plugin/{ => client}/http_proxy.go | 0 models/plugin/{ => client}/https2http.go | 0 models/plugin/{ => client}/plugin.go | 0 models/plugin/{ => client}/socks5.go | 0 models/plugin/{ => client}/static_file.go | 0 .../plugin/{ => client}/unix_domain_socket.go | 0 models/plugin/server/http.go | 104 +++++++++++++++++ models/plugin/server/manager.go | 105 ++++++++++++++++++ models/plugin/server/plugin.go | 32 ++++++ models/plugin/server/tracer.go | 34 ++++++ models/plugin/server/types.go | 46 ++++++++ server/control.go | 33 +++++- server/service.go | 28 ++++- 18 files changed, 410 insertions(+), 9 deletions(-) rename models/plugin/{ => client}/http2https.go (100%) rename models/plugin/{ => client}/http_proxy.go (100%) rename models/plugin/{ => client}/https2http.go (100%) rename models/plugin/{ => client}/plugin.go (100%) rename models/plugin/{ => client}/socks5.go (100%) rename models/plugin/{ => client}/static_file.go (100%) rename models/plugin/{ => client}/unix_domain_socket.go (100%) create mode 100644 models/plugin/server/http.go create mode 100644 models/plugin/server/manager.go create mode 100644 models/plugin/server/plugin.go create mode 100644 models/plugin/server/tracer.go create mode 100644 models/plugin/server/types.go diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 268b317de5d..c51364f81d6 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -28,7 +28,7 @@ import ( "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" - "github.com/fatedier/frp/models/plugin" + plugin "github.com/fatedier/frp/models/plugin/client" "github.com/fatedier/frp/models/proto/udp" "github.com/fatedier/frp/utils/limit" frpNet "github.com/fatedier/frp/utils/net" diff --git a/client/service.go b/client/service.go index 095df0aa315..5ad088550f4 100644 --- a/client/service.go +++ b/client/service.go @@ -222,6 +222,7 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) { PrivilegeKey: util.GetAuthKey(svr.cfg.Token, now), Timestamp: now, RunId: svr.runId, + Metas: svr.cfg.Metas, } if err = msg.WriteMsg(conn, loginMsg); err != nil { diff --git a/conf/frps_full.ini b/conf/frps_full.ini index ed507cef228..030a3b3a779 100644 --- a/conf/frps_full.ini +++ b/conf/frps_full.ini @@ -71,3 +71,13 @@ tcp_mux = true # custom 404 page for HTTP requests # custom_404_page = /path/to/404.html + +[plugin.user-manager] +addr = 127.0.0.1:9000 +path = /handler +ops = Login + +[plugin.port-manager] +addr = 127.0.0.1:9001 +path = /handler +ops = NewProxy diff --git a/models/config/server_common.go b/models/config/server_common.go index a190a61ac51..df6b7a10a6c 100644 --- a/models/config/server_common.go +++ b/models/config/server_common.go @@ -21,6 +21,7 @@ import ( ini "github.com/vaughan0/go-ini" + plugin "github.com/fatedier/frp/models/plugin/server" "github.com/fatedier/frp/utils/util" ) @@ -134,6 +135,8 @@ type ServerCommonConf struct { // UserConnTimeout specifies the maximum time to wait for a work // connection. By default, this value is 10. UserConnTimeout int64 `json:"user_conn_timeout"` + // HTTPPlugins specify the server plugins support HTTP protocol. + HTTPPlugins map[string]plugin.HTTPPluginOptions `json:"http_plugins"` } // GetDefaultServerConf returns a server configuration with reasonable @@ -167,6 +170,7 @@ func GetDefaultServerConf() ServerCommonConf { HeartBeatTimeout: 90, UserConnTimeout: 10, Custom404Page: "", + HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), } } @@ -181,6 +185,8 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error return ServerCommonConf{}, err } + UnmarshalPluginsFromIni(conf, &cfg) + var ( tmpStr string ok bool @@ -375,6 +381,24 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error return } +func UnmarshalPluginsFromIni(sections ini.File, cfg *ServerCommonConf) { + for name, section := range sections { + if strings.HasPrefix(name, "plugin.") { + name = strings.TrimSpace(strings.TrimPrefix(name, "plugin.")) + options := plugin.HTTPPluginOptions{ + Name: name, + Addr: section["addr"], + Path: section["path"], + Ops: strings.Split(section["ops"], ","), + } + for i, _ := range options.Ops { + options.Ops[i] = strings.TrimSpace(options.Ops[i]) + } + cfg.HTTPPlugins[name] = options + } + } +} + func (cfg *ServerCommonConf) Check() (err error) { return } diff --git a/models/plugin/http2https.go b/models/plugin/client/http2https.go similarity index 100% rename from models/plugin/http2https.go rename to models/plugin/client/http2https.go diff --git a/models/plugin/http_proxy.go b/models/plugin/client/http_proxy.go similarity index 100% rename from models/plugin/http_proxy.go rename to models/plugin/client/http_proxy.go diff --git a/models/plugin/https2http.go b/models/plugin/client/https2http.go similarity index 100% rename from models/plugin/https2http.go rename to models/plugin/client/https2http.go diff --git a/models/plugin/plugin.go b/models/plugin/client/plugin.go similarity index 100% rename from models/plugin/plugin.go rename to models/plugin/client/plugin.go diff --git a/models/plugin/socks5.go b/models/plugin/client/socks5.go similarity index 100% rename from models/plugin/socks5.go rename to models/plugin/client/socks5.go diff --git a/models/plugin/static_file.go b/models/plugin/client/static_file.go similarity index 100% rename from models/plugin/static_file.go rename to models/plugin/client/static_file.go diff --git a/models/plugin/unix_domain_socket.go b/models/plugin/client/unix_domain_socket.go similarity index 100% rename from models/plugin/unix_domain_socket.go rename to models/plugin/client/unix_domain_socket.go diff --git a/models/plugin/server/http.go b/models/plugin/server/http.go new file mode 100644 index 00000000000..155c470ad23 --- /dev/null +++ b/models/plugin/server/http.go @@ -0,0 +1,104 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" +) + +type HTTPPluginOptions struct { + Name string + Addr string + Path string + Ops []string +} + +type httpPlugin struct { + options HTTPPluginOptions + + url string + client *http.Client +} + +func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin { + return &httpPlugin{ + options: options, + url: fmt.Sprintf("http://%s%s", options.Addr, options.Path), + client: &http.Client{}, + } +} + +func (p *httpPlugin) Name() string { + return p.options.Name +} + +func (p *httpPlugin) IsSupport(op string) bool { + for _, v := range p.options.Ops { + if v == op { + return true + } + } + return false +} + +func (p *httpPlugin) Handle(ctx context.Context, op string, content interface{}) (*Response, interface{}, error) { + r := &Request{ + Version: APIVersion, + Op: op, + Content: content, + } + var res Response + res.Content = reflect.New(reflect.TypeOf(content)).Interface() + if err := p.do(ctx, r, &res); err != nil { + return nil, nil, err + } + return &res, res.Content, nil +} + +func (p *httpPlugin) do(ctx context.Context, r *Request, res *Response) error { + buf, err := json.Marshal(r) + if err != nil { + return err + } + req, err := http.NewRequest("POST", p.url, bytes.NewReader(buf)) + if err != nil { + return err + } + req = req.WithContext(ctx) + req.Header.Set("X-Frp-Reqid", GetReqidFromContext(ctx)) + resp, err := p.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("do http request error code: %d", resp.StatusCode) + } + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + if err = json.Unmarshal(buf, res); err != nil { + return err + } + return nil +} diff --git a/models/plugin/server/manager.go b/models/plugin/server/manager.go new file mode 100644 index 00000000000..94642932d60 --- /dev/null +++ b/models/plugin/server/manager.go @@ -0,0 +1,105 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "context" + "errors" + "fmt" + + "github.com/fatedier/frp/utils/util" + "github.com/fatedier/frp/utils/xlog" +) + +type Manager struct { + loginPlugins []Plugin + newProxyPlugins []Plugin +} + +func NewManager() *Manager { + return &Manager{ + loginPlugins: make([]Plugin, 0), + newProxyPlugins: make([]Plugin, 0), + } +} + +func (m *Manager) Register(p Plugin) { + if p.IsSupport(OpLogin) { + m.loginPlugins = append(m.loginPlugins, p) + } + if p.IsSupport(OpNewProxy) { + m.newProxyPlugins = append(m.newProxyPlugins, p) + } +} + +func (m *Manager) Login(content *LoginContent) (*LoginContent, error) { + var ( + res = &Response{ + Reject: false, + Unchange: true, + } + retContent interface{} + err error + ) + reqid, _ := util.RandId() + xl := xlog.New().AppendPrefix("reqid: " + reqid) + ctx := xlog.NewContext(context.Background(), xl) + ctx = NewReqidContext(ctx, reqid) + + for _, p := range m.loginPlugins { + res, retContent, err = p.Handle(ctx, OpLogin, *content) + if err != nil { + xl.Warn("send Login request to plugin [%s] error: %v", p.Name(), err) + return nil, errors.New("send Login request to plugin error") + } + if res.Reject { + return nil, fmt.Errorf("%s", res.RejectReason) + } + if !res.Unchange { + content = retContent.(*LoginContent) + } + } + return content, nil +} + +func (m *Manager) NewProxy(content *NewProxyContent) (*NewProxyContent, error) { + var ( + res = &Response{ + Reject: false, + Unchange: true, + } + retContent interface{} + err error + ) + reqid, _ := util.RandId() + xl := xlog.New().AppendPrefix("reqid: " + reqid) + ctx := xlog.NewContext(context.Background(), xl) + ctx = NewReqidContext(ctx, reqid) + + for _, p := range m.newProxyPlugins { + res, retContent, err = p.Handle(ctx, OpNewProxy, *content) + if err != nil { + xl.Warn("send NewProxy request to plugin [%s] error: %v", p.Name(), err) + return nil, errors.New("send NewProxy request to plugin error") + } + if res.Reject { + return nil, fmt.Errorf("%s", res.RejectReason) + } + if !res.Unchange { + content = retContent.(*NewProxyContent) + } + } + return content, nil +} diff --git a/models/plugin/server/plugin.go b/models/plugin/server/plugin.go new file mode 100644 index 00000000000..fd16b145168 --- /dev/null +++ b/models/plugin/server/plugin.go @@ -0,0 +1,32 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "context" +) + +const ( + APIVersion = "0.1.0" + + OpLogin = "Login" + OpNewProxy = "NewProxy" +) + +type Plugin interface { + Name() string + IsSupport(op string) bool + Handle(ctx context.Context, op string, content interface{}) (res *Response, retContent interface{}, err error) +} diff --git a/models/plugin/server/tracer.go b/models/plugin/server/tracer.go new file mode 100644 index 00000000000..2f4f2ccc25d --- /dev/null +++ b/models/plugin/server/tracer.go @@ -0,0 +1,34 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "context" +) + +type key int + +const ( + reqidKey key = 0 +) + +func NewReqidContext(ctx context.Context, reqid string) context.Context { + return context.WithValue(ctx, reqidKey, reqid) +} + +func GetReqidFromContext(ctx context.Context) string { + ret, _ := ctx.Value(reqidKey).(string) + return ret +} diff --git a/models/plugin/server/types.go b/models/plugin/server/types.go new file mode 100644 index 00000000000..4e392b715ac --- /dev/null +++ b/models/plugin/server/types.go @@ -0,0 +1,46 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "github.com/fatedier/frp/models/msg" +) + +type Request struct { + Version string `json:"version"` + Op string `json:"op"` + Content interface{} `json:"content"` +} + +type Response struct { + Reject bool `json:"reject"` + RejectReason string `json:"reject_reason"` + Unchange bool `json:"unchange"` + Content interface{} `json:"content"` +} + +type LoginContent struct { + msg.Login +} + +type UserInfo struct { + User string `json:"user"` + Metas map[string]string `json:"metas"` +} + +type NewProxyContent struct { + User UserInfo `json:"user"` + msg.NewProxy +} diff --git a/server/control.go b/server/control.go index 0db61987ffb..e5e4901cde0 100644 --- a/server/control.go +++ b/server/control.go @@ -27,6 +27,7 @@ import ( "github.com/fatedier/frp/models/consts" frpErr "github.com/fatedier/frp/models/errors" "github.com/fatedier/frp/models/msg" + plugin "github.com/fatedier/frp/models/plugin/server" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/proxy" "github.com/fatedier/frp/server/stats" @@ -86,6 +87,9 @@ type Control struct { // proxy manager pxyManager *proxy.ProxyManager + // plugin manager + pluginManager *plugin.Manager + // stats collector to store stats info of clients and proxies statsCollector stats.Collector @@ -138,9 +142,16 @@ type Control struct { ctx context.Context } -func NewControl(ctx context.Context, rc *controller.ResourceController, pxyManager *proxy.ProxyManager, - statsCollector stats.Collector, ctlConn net.Conn, loginMsg *msg.Login, - serverCfg config.ServerCommonConf) *Control { +func NewControl( + ctx context.Context, + rc *controller.ResourceController, + pxyManager *proxy.ProxyManager, + pluginManager *plugin.Manager, + statsCollector stats.Collector, + ctlConn net.Conn, + loginMsg *msg.Login, + serverCfg config.ServerCommonConf, +) *Control { poolCount := loginMsg.PoolCount if poolCount > int(serverCfg.MaxPoolCount) { @@ -149,6 +160,7 @@ func NewControl(ctx context.Context, rc *controller.ResourceController, pxyManag return &Control{ rc: rc, pxyManager: pxyManager, + pluginManager: pluginManager, statsCollector: statsCollector, conn: ctlConn, loginMsg: loginMsg, @@ -407,8 +419,21 @@ func (ctl *Control) manager() { switch m := rawMsg.(type) { case *msg.NewProxy: + content := &plugin.NewProxyContent{ + User: plugin.UserInfo{ + User: ctl.loginMsg.User, + Metas: ctl.loginMsg.Metas, + }, + NewProxy: *m, + } + var remoteAddr string + retContent, err := ctl.pluginManager.NewProxy(content) + if err == nil { + m = &retContent.NewProxy + remoteAddr, err = ctl.RegisterProxy(m) + } + // register proxy in this control - remoteAddr, err := ctl.RegisterProxy(m) resp := &msg.NewProxyResp{ ProxyName: m.ProxyName, } diff --git a/server/service.go b/server/service.go index a4d2e9dff22..122555af98a 100644 --- a/server/service.go +++ b/server/service.go @@ -33,6 +33,7 @@ import ( "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" "github.com/fatedier/frp/models/nathole" + plugin "github.com/fatedier/frp/models/plugin/server" "github.com/fatedier/frp/server/controller" "github.com/fatedier/frp/server/group" "github.com/fatedier/frp/server/ports" @@ -76,6 +77,9 @@ type Service struct { // Manage all proxies pxyManager *proxy.ProxyManager + // Manage all plugins + pluginManager *plugin.Manager + // HTTP vhost router httpVhostRouter *vhost.VhostRouters @@ -92,8 +96,9 @@ type Service struct { func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { svr = &Service{ - ctlManager: NewControlManager(), - pxyManager: proxy.NewProxyManager(), + ctlManager: NewControlManager(), + pxyManager: proxy.NewProxyManager(), + pluginManager: plugin.NewManager(), rc: &controller.ResourceController{ VisitorManager: controller.NewVisitorManager(), TcpPortManager: ports.NewPortManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts), @@ -104,6 +109,12 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { cfg: cfg, } + // Init all plugins + for name, options := range cfg.HTTPPlugins { + svr.pluginManager.Register(plugin.NewHTTPPluginOptions(options)) + log.Info("plugin [%s] has been registered", name) + } + // Init group controller svr.rc.TcpGroupCtl = group.NewTcpGroupCtl(svr.rc.TcpPortManager) @@ -295,7 +306,16 @@ func (svr *Service) HandleListener(l net.Listener) { switch m := rawMsg.(type) { case *msg.Login: - err = svr.RegisterControl(conn, m) + // server plugin hook + content := &plugin.LoginContent{ + Login: *m, + } + retContent, err := svr.pluginManager.Login(content) + if err == nil { + m = &retContent.Login + err = svr.RegisterControl(conn, m) + } + // If login failed, send error message there. // Otherwise send success message in control's work goroutine. if err != nil { @@ -384,7 +404,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err return } - ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.statsCollector, ctlConn, loginMsg, svr.cfg) + ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.statsCollector, ctlConn, loginMsg, svr.cfg) if oldCtl := svr.ctlManager.Add(loginMsg.RunId, ctl); oldCtl != nil { oldCtl.allShutdown.WaitDone() From 31e2cb76bb3a420b114851064ad9bc84f993bb75 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 23 Dec 2019 20:00:59 +0800 Subject: [PATCH 3/5] bump version --- utils/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/version/version.go b/utils/version/version.go index dde6de964b4..27eb88a4436 100644 --- a/utils/version/version.go +++ b/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.30.0" +var version string = "0.31.0" func Full() string { return version From e91c9473be33a1c9fccdcb20b78e113c8872f5fa Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 3 Jan 2020 11:35:12 +0800 Subject: [PATCH 4/5] add server manage plugin doc --- README.md | 9 ++- README_zh.md | 11 ++- doc/server_plugin.md | 171 ++++++++++++++++++++++++++++++++++++++++ doc/server_plugin_zh.md | 171 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 doc/server_plugin.md create mode 100644 doc/server_plugin_zh.md diff --git a/README.md b/README.md index 2890d645355..9ca3a4d539e 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ frp also has a P2P connect mode. * [URL routing](#url-routing) * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy) * [Range ports mapping](#range-ports-mapping) - * [Plugins](#plugins) + * [Client Plugins](#client-plugins) + * [Server Manage Plugins](#server-manage-plugins) * [Development Plan](#development-plan) * [Contributing](#contributing) * [Donation](#donation) @@ -806,7 +807,7 @@ remote_port = 6000-6006,6007 frpc will generate 8 proxies like `test_tcp_0`, `test_tcp_1`, ..., `test_tcp_7`. -### Plugins +### Client Plugins frpc only forwards requests to local TCP or UDP ports by default. @@ -828,6 +829,10 @@ plugin_http_passwd = abc `plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin. +### Server Manage Plugins + +Read the [document](/doc/server_plugin.md). + ## Development Plan * Log HTTP request information in frps. diff --git a/README_zh.md b/README_zh.md index 6f62e38717e..a7c4524254d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -50,7 +50,8 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [URL 路由](#url-路由) * [通过代理连接 frps](#通过代理连接-frps) * [范围端口映射](#范围端口映射) - * [插件](#插件) + * [客户端插件](#客户端插件) + * [服务端管理插件](#服务端管理插件) * [开发计划](#开发计划) * [为 frp 做贡献](#为-frp-做贡献) * [捐助](#捐助) @@ -858,11 +859,11 @@ remote_port = 6000-6006,6007 实际连接成功后会创建 8 个 proxy,命名为 `test_tcp_0, test_tcp_1 ... test_tcp_7`。 -### 插件 +### 客户端插件 默认情况下,frpc 只会转发请求到本地 tcp 或 udp 端口。 -插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。 +客户端插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。 通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port` 不再需要配置。 @@ -880,6 +881,10 @@ plugin_http_passwd = abc `plugin_http_user` 和 `plugin_http_passwd` 即为 `http_proxy` 插件可选的配置参数。 +### 服务端管理插件 + +[使用说明](/doc/server_plugin_zh.md) + ## 开发计划 计划在后续版本中加入的功能与优化,排名不分先后,如果有其他功能建议欢迎在 [issues](https://github.com/fatedier/frp/issues) 中反馈。 diff --git a/doc/server_plugin.md b/doc/server_plugin.md new file mode 100644 index 00000000000..7d9be1202c3 --- /dev/null +++ b/doc/server_plugin.md @@ -0,0 +1,171 @@ +### Manage Plugin + +frp manage plugin is aim to extend frp's ability without modifing self code. + +It runs as a process and listen on a port to provide RPC interface. Before frps doing some operations, frps will send RPC requests to manage plugin and do operations by it's response. + +### RPC request + +Support HTTP first. + +When manage plugin accept the operation request, it can give three different responses. + +* Reject operation and return the reason. +* Allow operation and keep original content. +* Allow operation and return modified content. + +### Interface + +HTTP path can be configured for each manage plugin in frps. Assume here is `/handler`. + +Request + +``` +POST /handler +{ + "version": "0.1.0", + "op": "Login", + "content": { + ... // Operation info + } +} + +Request Header +X-Frp-Reqid: for tracing +``` + +Response + +Error if not return 200 http code. + +Reject opeartion + +``` +{ + "reject": true, + "reject_reason": "invalid user" +} +``` + +Allow operation and keep original content + +``` +{ + "reject": false, + "unchange": true +} +``` + +Allow opeartion and modify content + +``` +{ + "unchange": "false", + "content": { + ... // Replaced content + } +} +``` + +### Operation + +Now it supports `Login` and `NewProxy`. + +#### Login + +Client login operation + +``` +{ + "content": { + "version": , + "hostname": , + "os": , + "arch": , + "user": , + "timestamp": , + "privilege_key": , + "run_id": , + "pool_count": , + "metas": mapstring + } +} +``` + +#### NewProxy + +Create new proxy + +``` +{ + "content": { + "user": { + "user": , + "metas": mapstring + }, + "proxy_name": , + "proxy_type": , + "use_encryption": , + "use_compression": , + "group": , + "group_key": , + + // tcp and udp only + "remote_port": , + + // http and https only + "custom_domains": [], + "subdomain": , + "locations": , + "http_user": , + "http_pwd": , + "host_header_rewrite": , + "headers": mapstring, + + "metas": mapstring + } +} +``` + +### manage plugin configure + +```ini +[common] +bind_port = 7000 + +[plugin.user-manager] +addr = 127.0.0.1:9000 +path = /handler +ops = Login + +[plugin.port-manager] +addr = 127.0.0.1:9001 +path = /handler +ops = NewProxy +``` + +addr: plugin listen on. +path: http request url path. +ops: opeartions plugin needs handle. + +### meta data + +Meta data will be sent to manage plugin in each RCP request. + +Meta data start with `meta_`. It can be configured in `common` and each proxy. + +``` +# frpc.ini +[common] +server_addr = 127.0.0.1 +server_port = 7000 +user = fake +meta_token = fake +meta_version = 1.0.0 + +[ssh] +type = tcp +local_port = 22 +remote_port = 6000 +meta_id = 123 +``` diff --git a/doc/server_plugin_zh.md b/doc/server_plugin_zh.md new file mode 100644 index 00000000000..78b71a32553 --- /dev/null +++ b/doc/server_plugin_zh.md @@ -0,0 +1,171 @@ +### 服务端管理插件 + +frp 管理插件的作用是在不侵入自身代码的前提下,扩展 frp 服务端的能力。 + +frp 管理插件会以单独进程的形式运行,并且监听在一个端口上,对外提供 RPC 接口,响应 frps 的请求。 + +frps 在执行某些操作前,会根据配置向管理插件发送 RPC 请求,根据管理插件的响应来执行相应的操作。 + +### RPC 请求 + +管理插件接收到操作请求后,可以给出三种回应。 + +* 拒绝操作,需要返回拒绝操作的原因。 +* 允许操作,不需要修改操作内容。 +* 允许操作,对操作请求进行修改后,返回修改后的内容。 + +### 接口 + +接口路径可以在 frps 配置中为每个插件单独配置,这里以 `/handler` 为例。 + +Request + +``` +POST /handler +{ + "version": "0.1.0", + "op": "Login", + "content": { + ... // 具体的操作信息 + } +} + +请求 Header +X-Frp-Reqid: 用于追踪请求 +``` + +Response + +非 200 的返回都认为是请求异常。 + +拒绝执行操作 + +``` +{ + "reject": true, + "reject_reason": "invalid user" +} +``` + +允许且内容不需要变动 + +``` +{ + "reject": false, + "unchange": true +} +``` + +允许且需要替换操作内容 + +``` +{ + "unchange": "false", + "content": { + ... // 替换后的操作信息,格式必须和请求时的一致 + } +} +``` + +### 操作类型 + +目前插件支持管理的操作类型有 `Login` 和 `NewProxy`。 + +#### Login + +用户登录操作信息 + +``` +{ + "content": { + "version": , + "hostname": , + "os": , + "arch": , + "user": , + "timestamp": , + "privilege_key": , + "run_id": , + "pool_count": , + "metas": mapstring + } +} +``` + +#### NewProxy + +创建代理的相关信息 + +``` +{ + "content": { + "user": { + "user": , + "metas": mapstring + }, + "proxy_name": , + "proxy_type": , + "use_encryption": , + "use_compression": , + "group": , + "group_key": , + + // tcp and udp only + "remote_port": , + + // http and https only + "custom_domains": [], + "subdomain": , + "locations": , + "http_user": , + "http_pwd": , + "host_header_rewrite": , + "headers": mapstring, + + "metas": mapstring + } +} +``` + +### frps 中插件配置 + +```ini +[common] +bind_port = 7000 + +[plugin.user-manager] +addr = 127.0.0.1:9000 +path = /handler +ops = Login + +[plugin.port-manager] +addr = 127.0.0.1:9001 +path = /handler +ops = NewProxy +``` + +addr: 插件监听的网络地址。 +path: 插件监听的 HTTP 请求路径。 +ops: 插件需要处理的操作列表,多个 op 以英文逗号分隔。 + +### 元数据 + +为了减少 frps 的代码修改,同时提高管理插件的扩展能力,在 frpc 的配置文件中引入自定义元数据的概念。元数据会在调用 RPC 请求时发送给插件。 + +元数据以 `meta_` 开头,可以配置多个,元数据分为两种,一种配置在 `common` 下,一种配置在各个 proxy 中。 + +``` +# frpc.ini +[common] +server_addr = 127.0.0.1 +server_port = 7000 +user = fake +meta_token = fake +meta_version = 1.0.0 + +[ssh] +type = tcp +local_port = 22 +remote_port = 6000 +meta_id = 123 +``` From 42014eea2321ac6df900553d1edefb34f9045871 Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 3 Jan 2020 11:39:44 +0800 Subject: [PATCH 5/5] improve xtcp, fix #1585 --- client/proxy/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index c51364f81d6..c9ef8dd001f 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -349,7 +349,7 @@ func (pxy *XtcpProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { lConn.WriteToUDP(sidBuf[:n], uAddr) - kcpConn, err := frpNet.NewKcpConnFromUdp(lConn, false, natHoleRespMsg.VisitorAddr) + kcpConn, err := frpNet.NewKcpConnFromUdp(lConn, false, uAddr.String()) if err != nil { xl.Error("create kcp connection from udp connection error: %v", err) return