From a57679f8375986abc970d22bad52644ba62a4969 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sun, 8 Dec 2019 21:01:58 +0800 Subject: [PATCH 01/11] 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 14ca6ed3..8c86acba 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 fe87e08d..2b5006b4 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 85c353ed..1efaf38a 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 11d2542f..ce41c9ec 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 6f5965e6..570b3f9c 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 af5b6af9..093a74f4 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 02/11] 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 268b317d..c51364f8 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 095df0aa..5ad08855 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 ed507cef..030a3b3a 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 a190a61a..df6b7a10 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 00000000..155c470a --- /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 00000000..94642932 --- /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 00000000..fd16b145 --- /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 00000000..2f4f2ccc --- /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 00000000..4e392b71 --- /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 0db61987..e5e4901c 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 a4d2e9df..122555af 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 03/11] 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 dde6de96..27eb88a4 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 04/11] 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 2890d645..9ca3a4d5 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 6f62e387..a7c45242 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 00000000..7d9be120 --- /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 00000000..78b71a32 --- /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 05/11] 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 c51364f8..c9ef8dd0 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 From 52ecd84d8a634ab30d04926aaa3d5e5eb32781de Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 6 Jan 2020 15:43:25 +0800 Subject: [PATCH 06/11] fix panic if set meta in proxy config, fix #1595 --- models/config/proxy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/config/proxy.go b/models/config/proxy.go index 1efaf38a..f4ddba50 100644 --- a/models/config/proxy.go +++ b/models/config/proxy.go @@ -218,6 +218,7 @@ func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section i cfg.HealthCheckUrl = s + cfg.HealthCheckUrl } + cfg.Metas = make(map[string]string) for k, v := range section { if strings.HasPrefix(k, "meta_") { cfg.Metas[strings.TrimPrefix(k, "meta_")] = v From 4832a2a1e92fe7c13e1201cdfa0790987a768395 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 6 Jan 2020 15:44:18 +0800 Subject: [PATCH 07/11] 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 27eb88a4..dac39749 100644 --- a/utils/version/version.go +++ b/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.31.0" +var version string = "0.31.1" func Full() string { return version From 0f59b8f329d398c5446e3a4d8550fbb436bc8913 Mon Sep 17 00:00:00 2001 From: Joe Cloud Date: Tue, 14 Jan 2020 22:11:12 -0600 Subject: [PATCH 08/11] English grammar fix. (#1619) --- utils/vhost/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/vhost/resource.go b/utils/vhost/resource.go index 9553e7ef..5c084306 100644 --- a/utils/vhost/resource.go +++ b/utils/vhost/resource.go @@ -41,7 +41,7 @@ const ( -

The page you visit not found.

+

The page you requested was not found.

Sorry, the page you are looking for is currently unavailable.
Please try again later.

The server is powered by frp.

From 4a4cf552af78a3461e95f9c42038198c536549d1 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 4 Feb 2020 19:41:39 +0800 Subject: [PATCH 09/11] send closeProxy msg to server then client start proxy error, fix #1606 --- client/proxy/proxy_wrapper.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 458fa438..418ef01b 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -112,6 +112,7 @@ func (pw *ProxyWrapper) SetRunningStatus(remoteAddr string, respErr string) erro } if err := pw.pxy.Run(); err != nil { + pw.close() pw.Status = ProxyStatusStartErr pw.Err = err.Error() pw.lastStartErr = time.Now() @@ -140,7 +141,10 @@ func (pw *ProxyWrapper) Stop() { pw.monitor.Stop() } pw.Status = ProxyStatusClosed + pw.close() +} +func (pw *ProxyWrapper) close() { pw.handler(event.EvCloseProxy, &event.CloseProxyPayload{ CloseProxyMsg: &msg.CloseProxy{ ProxyName: pw.Name, @@ -178,11 +182,7 @@ func (pw *ProxyWrapper) checkWorker() { } else { pw.mu.Lock() if pw.Status == ProxyStatusRunning || pw.Status == ProxyStatusWaitStart { - pw.handler(event.EvCloseProxy, &event.CloseProxyPayload{ - CloseProxyMsg: &msg.CloseProxy{ - ProxyName: pw.Name, - }, - }) + pw.close() xl.Trace("change status from [%s] to [%s]", pw.Status, ProxyStatusCheckFailed) pw.Status = ProxyStatusCheckFailed } From 4feb74cb89400a23b7db9c1f059191f0db6f05d3 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 4 Feb 2020 21:34:46 +0800 Subject: [PATCH 10/11] doc typo --- README_zh.md | 4 ++-- conf/frpc_full.ini | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README_zh.md b/README_zh.md index a7c45242..f01efdf5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -546,10 +546,10 @@ allow_ports = 2000-3000,3001,3003,4000-50000 type = tcp local_port = 22 remote_port = 6000 -bandwith_limit = 1MB +bandwidth_limit = 1MB ``` -在代理配置中增加 `bandwith_limit` 字段启用此功能,目前仅支持 `MB` 和 `KB` 单位。 +在代理配置中增加 `bandwidth_limit` 字段启用此功能,目前仅支持 `MB` 和 `KB` 单位。 ### TCP 多路复用 diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 8c86acba..c6d3d406 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -75,8 +75,8 @@ meta_var2 = 234 type = tcp local_ip = 127.0.0.1 local_port = 22 -# limit bandwith for this proxy, unit is KB and MB -bandwith_limit = 1MB +# limit bandwidth for this proxy, unit is KB and MB +bandwidth_limit = 1MB # true or false, if true, messages between frps and frpc will be encrypted, default is false use_encryption = false # if true, message will be compressed From 69fa7ed16e22f1aab8284f508ee0267c3ba84ca7 Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 4 Feb 2020 21:43:37 +0800 Subject: [PATCH 11/11] bump version to v0.31.2 --- 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 dac39749..f0fe22f3 100644 --- a/utils/version/version.go +++ b/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.31.1" +var version string = "0.31.2" func Full() string { return version