diff --git a/.gitignore b/.gitignore index 56a043c7..02ab6b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ _testmain.go bin/ packages/ test/bin/ +vendor/ # Cache *.swp diff --git a/.travis.yml b/.travis.yml index 16695fc9..3e5b993a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: go go: - 1.12.x + - 1.13.x install: - make diff --git a/Makefile b/Makefile index cc6ee6f0..3877f95e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ export PATH := $(GOPATH)/bin:$(PATH) +export GO111MODULE=on all: fmt build diff --git a/README.md b/README.md index 9ca3a4d5..f39b9515 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,11 @@ frp also has a P2P connect mode. * [Using Environment Variables](#using-environment-variables) * [Dashboard](#dashboard) * [Admin UI](#admin-ui) + * [Monitor](#monitor) + * [Prometheus](#prometheus) * [Authenticating the Client](#authenticating-the-client) + * [Token Authentication](#token-authentication) + * [OIDC Authentication](#oidc-authentication) * [Encryption and Compression](#encryption-and-compression) * [TLS](#tls) * [Hot-Reloading frpc configuration](#hot-reloading-frpc-configuration) @@ -49,9 +53,10 @@ frp also has a P2P connect mode. * [Get Real IP](#get-real-ip) * [HTTP X-Forwarded-For](#http-x-forwarded-for) * [Proxy Protocol](#proxy-protocol) - * [Require HTTP Basic auth (password) for web services](#require-http-basic-auth-password-for-web-services) - * [Custom subdomain names](#custom-subdomain-names) - * [URL routing](#url-routing) + * [Require HTTP Basic Auth (Password) for Web Services](#require-http-basic-auth-password-for-web-services) + * [Custom Subdomain Names](#custom-subdomain-names) + * [URL Routing](#url-routing) + * [TCP Port Multiplexing](#tcp-port-multiplexing) * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy) * [Range ports mapping](#range-ports-mapping) * [Client Plugins](#client-plugins) @@ -435,9 +440,59 @@ admin_pwd = admin Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin` by default. +### Monitor + +When dashboard is enabled, frps will save monitor data in cache. It will be cleared after process restart. + +Prometheus is also supported. + +#### Prometheus + +Enable dashboard first, then configure `enable_prometheus = true` in `frps.ini`. + +`http://{dashboard_addr}/metrics` will provide prometheus monitor data. + ### Authenticating the Client -Always use the same `token` in the `[common]` section in `frps.ini` and `frpc.ini`. +There are 2 authentication methods to authenticate frpc with frps. + +You can decide which one to use by configuring `authentication_method` under `[common]` in `frpc.ini` and `frps.ini`. + +Configuring `authenticate_heartbeats = true` under `[common]` will use the configured authentication method to add and validate authentication on every heartbeat between frpc and frps. + +Configuring `authenticate_new_work_conns = true` under `[common]` will do the same for every new work connection between frpc and frps. + +#### Token Authentication + +When specifying `authentication_method = token` under `[common]` in `frpc.ini` and `frps.ini` - token based authentication will be used. + +Make sure to specify the same `token` in the `[common]` section in `frps.ini` and `frpc.ini` for frpc to pass frps validation + +#### OIDC Authentication + +When specifying `authentication_method = oidc` under `[common]` in `frpc.ini` and `frps.ini` - OIDC based authentication will be used. + +OIDC stands for OpenID Connect, and the flow used is called [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). + +To use this authentication type - configure `frpc.ini` and `frps.ini` as follows: + +```ini +# frps.ini +[common] +authentication_method = oidc +oidc_issuer = https://example-oidc-issuer.com/ +oidc_audience = https://oidc-audience.com/.default +``` + +```ini +# frpc.ini +[common] +authentication_method = oidc +oidc_client_id = 98692467-37de-409a-9fac-bb2585826f18 # Replace with OIDC client ID +oidc_client_secret = oidc_secret +oidc_audience = https://oidc-audience.com/.default +oidc_token_endpoint_url = https://example-oidc-endpoint.com/oauth2/v2.0/token +``` ### Encryption and Compression @@ -461,6 +516,8 @@ Config `tls_enable = true` in the `[common]` section to `frpc.ini` to enable thi For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. +To enforce `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. + ### Hot-Reloading frpc configuration The `admin_addr` and `admin_port` fields are required for enabling HTTP API: @@ -712,7 +769,7 @@ proxy_protocol_version = v2 You can enable Proxy Protocol support in nginx to expose user's real IP in HTTP header `X-Real-IP`, and then read `X-Real-IP` header in your web service for the real IP. -### Require HTTP Basic auth (password) for web services +### Require HTTP Basic Auth (Password) for Web Services Anyone who can guess your tunnel URL can access your local web server unless you protect it with a password. @@ -732,7 +789,7 @@ http_pwd = abc Visit `http://test.example.com` in the browser and now you are prompted to enter the username and password. -### Custom subdomain names +### Custom Subdomain Names It is convenient to use `subdomain` configure for http and https types when many people share one frps server. @@ -755,7 +812,7 @@ Now you can visit your web service on `test.frps.com`. Note that if `subdomain_host` is not empty, `custom_domains` should not be the subdomain of `subdomain_host`. -### URL routing +### URL Routing frp supports forwarding HTTP requests to different backend web services by url routing. @@ -778,6 +835,49 @@ locations = /news,/about HTTP requests with URL prefix `/news` or `/about` will be forwarded to **web02** and other requests to **web01**. +### TCP Port Multiplexing + +frp supports receiving TCP sockets directed to different proxies on a single port on frps, similar to `vhost_http_port` and `vhost_https_port`. + +The only supported TCP port multiplexing method available at the moment is `httpconnect` - HTTP CONNECT tunnel. + +When setting `tcpmux_httpconnect_port` to anything other than 0 in frps under `[common]`, frps will listen on this port for HTTP CONNECT requests. + +The host of the HTTP CONNECT request will be used to match the proxy in frps. Proxy hosts can be configured in frpc by configuring `custom_domain` and / or `subdomain` under `type = tcpmux` proxies, when `multiplexer = httpconnect`. + +For example: + +```ini +# frps.ini +[common] +bind_port = 7000 +tcpmux_httpconnect_port = 1337 +``` + +```ini +# frpc.ini +[common] +server_addr = x.x.x.x +server_port = 7000 + +[proxy1] +type = tcpmux +multiplexer = httpconnect +custom_domains = test1 + +[proxy2] +type = tcpmux +multiplexer = httpconnect +custom_domains = test2 +``` + +In the above configuration - frps can be contacted on port 1337 with a HTTP CONNECT header such as: + +``` +CONNECT test1 HTTP/1.1\r\n\r\n +``` +and the connection will be routed to `proxy1`. + ### Connecting to frps via HTTP PROXY frpc can connect to frps using HTTP proxy if you set OS environment variable `HTTP_PROXY`, or if `http_proxy` is set in frpc.ini file. diff --git a/README_zh.md b/README_zh.md index f01efdf5..0d167018 100644 --- a/README_zh.md +++ b/README_zh.md @@ -26,7 +26,11 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [配置文件模版渲染](#配置文件模版渲染) * [Dashboard](#dashboard) * [Admin UI](#admin-ui) - * [身份验证](#身份验证) + * [监控](#监控) + * [Prometheus](#prometheus) + * [客户端身份验证](#客户端身份验证) + * [Token](#token) + * [OIDC](#oidc) * [加密与压缩](#加密与压缩) * [TLS](#tls) * [客户端热加载配置文件](#客户端热加载配置文件) @@ -48,6 +52,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [通过密码保护你的 web 服务](#通过密码保护你的-web-服务) * [自定义二级域名](#自定义二级域名) * [URL 路由](#url-路由) + * [TCP 端口复用类型](#tcp-端口复用类型) * [通过代理连接 frps](#通过代理连接-frps) * [范围端口映射](#范围端口映射) * [客户端插件](#客户端插件) @@ -459,9 +464,56 @@ admin_pwd = admin 如果想要在外网环境访问 Admin UI,将 7400 端口映射出去即可,但需要重视安全风险。 -### 身份验证 +### 监控 -服务端和客户端的 common 配置中的 `token` 参数一致则身份验证通过。 +frps 当启用 Dashboard 后,会默认开启内部的监控,数据存放在内存中,每次重启进程后会清空,监控数据可以通过 dashboard 的地址发送 HTTP 请求获取。 + +目前还支持 Prometheus 作为可选的监控系统。 + +#### Prometheus + +在 `frps.ini` 中启用 Dashboard,并且设置 `enable_prometheus = true`,则通过 `http://{dashboard_addr}/metrics` 可以获取到 Prometheus 的监控数据。 + +### 客户端身份验证 + +目前 frpc 和 frps 之间支持两种身份验证方式,`token` 和 `oidc`。 + +通过 `frpc.ini` 和 `frps.ini` 中 `[common]` section 的 `authentication_method` 参数配置需要使用的验证方法。 + +`authenticate_heartbeats = true` 将会在每一个心跳包中附加上鉴权信息。 + +`authenticate_new_work_conns = true` 将会在每次建立新的工作连接时附加上鉴权信息。 + +#### Token + +当 `authentication_method = token`,将会启用基于 token 的验证方式。 + +需要在 `frpc.ini` 和 `frps.ini` 的 `[common]` section 中设置相同的 `token`。 + +#### OIDC + +当 `authentication_method = oidc`,将会启用基于 OIDC 的身份验证。 + +验证流程参考 [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4) + +启用这一验证方式,配置 `frpc.ini` 和 `frps.ini` 如下: + +```ini +# frps.ini +[common] +authentication_method = oidc +oidc_issuer = https://example-oidc-issuer.com/ +oidc_audience = https://oidc-audience.com/.default +``` + +```ini +[common] +authentication_method = oidc +oidc_client_id = 98692467-37de-409a-9fac-bb2585826f18 # Replace with OIDC client ID +oidc_client_secret = oidc_secret +oidc_audience = https://oidc-audience.com/.default +oidc_token_endpoint_url = https://example-oidc-endpoint.com/oauth2/v2.0/token +``` ### 加密与压缩 @@ -487,6 +539,8 @@ use_compression = true 为了端口复用,frp 建立 TLS 连接的第一个字节为 0x17。 +通过将 frps.ini 的 `[common]` 中 `tls_only` 设置为 true,可以强制 frps 只接受 TLS 连接。 + **注意: 启用此功能后除 xtcp 外,不需要再设置 use_encryption。** ### 客户端热加载配置文件 @@ -824,6 +878,50 @@ locations = /news,/about 按照上述的示例配置后,`web.yourdomain.com` 这个域名下所有以 `/news` 以及 `/about` 作为前缀的 URL 请求都会被转发到 web02,其余的请求会被转发到 web01。 +### TCP 端口复用类型 + +frp 支持将单个端口收到的连接路由到不同的代理,类似 `vhost_http_port` 和 `vhost_https_port`。 + +目前支持的复用器只有 `httpconnect`。 + +当在 `frps.ini` 的 `[common]` 中设置 `tcpmux_httpconnect_port`,frps 将会监听在这个端口,接收 HTTP CONNECT 请求。 + +frps 会根据 HTTP CONNECT 请求中的 host 路由到不同的后端代理。 + +示例配置如下: + +```ini +# frps.ini +[common] +bind_port = 7000 +tcpmux_httpconnect_port = 1337 +``` + +```ini +# frpc.ini +[common] +server_addr = x.x.x.x +server_port = 7000 + +[proxy1] +type = tcpmux +multiplexer = httpconnect +custom_domains = test1 + +[proxy2] +type = tcpmux +multiplexer = httpconnect +custom_domains = test2 +``` + +通过上面的配置,frps 如果接收到 HTTP CONNECT 请求内容: + +``` +CONNECT test1 HTTP/1.1\r\n\r\n +``` + +该连接将会被路由到 proxy1 。 + ### 通过代理连接 frps 在只能通过代理访问外网的环境内,frpc 支持通过 HTTP PROXY 和 frps 进行通信。 diff --git a/client/control.go b/client/control.go index 5589817f..f415f65f 100644 --- a/client/control.go +++ b/client/control.go @@ -25,6 +25,7 @@ import ( "time" "github.com/fatedier/frp/client/proxy" + "github.com/fatedier/frp/models/auth" "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" frpNet "github.com/fatedier/frp/utils/net" @@ -82,13 +83,17 @@ type Control struct { // service context ctx context.Context + + // sets authentication based on selected method + authSetter auth.Setter } func NewControl(ctx context.Context, runId string, conn net.Conn, session *fmux.Session, clientCfg config.ClientCommonConf, pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf, - serverUDPPort int) *Control { + serverUDPPort int, + authSetter auth.Setter) *Control { // new xlog instance ctl := &Control{ @@ -107,6 +112,7 @@ func NewControl(ctx context.Context, runId string, conn net.Conn, session *fmux. serverUDPPort: serverUDPPort, xl: xlog.FromContextSafe(ctx), ctx: ctx, + authSetter: authSetter, } ctl.pm = proxy.NewProxyManager(ctl.ctx, ctl.sendCh, clientCfg, serverUDPPort) @@ -136,6 +142,10 @@ func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) { m := &msg.NewWorkConn{ RunId: ctl.runId, } + if err = ctl.authSetter.SetNewWorkConn(m); err != nil { + xl.Warn("error during NewWorkConn authentication: %v", err) + return + } if err = msg.WriteMsg(workConn, m); err != nil { xl.Warn("work connection write to server error: %v", err) workConn.Close() @@ -148,6 +158,11 @@ func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) { workConn.Close() return } + if startMsg.Error != "" { + xl.Error("StartWorkConn contains error: %s", startMsg.Error) + workConn.Close() + return + } // dispatch this work connection to related proxy ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) @@ -282,7 +297,12 @@ func (ctl *Control) msgHandler() { case <-hbSend.C: // send heartbeat to server xl.Debug("send heartbeat to server") - ctl.sendCh <- &msg.Ping{} + pingMsg := &msg.Ping{} + if err := ctl.authSetter.SetPing(pingMsg); err != nil { + xl.Warn("error during ping authentication: %v", err) + return + } + ctl.sendCh <- pingMsg case <-hbCheck.C: if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartBeatTimeout)*time.Second { xl.Warn("heartbeat timeout") @@ -301,6 +321,11 @@ func (ctl *Control) msgHandler() { case *msg.NewProxyResp: ctl.HandleNewProxyResp(m) case *msg.Pong: + if m.Error != "" { + xl.Error("Pong contains error: %s", m.Error) + ctl.conn.Close() + return + } ctl.lastPong = time.Now() xl.Debug("receive heartbeat from server") } diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index c9ef8dd0..c263e1d2 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -72,6 +72,11 @@ func NewProxy(ctx context.Context, pxyConf config.ProxyConf, clientCfg config.Cl BaseProxy: &baseProxy, cfg: cfg, } + case *config.TcpMuxProxyConf: + pxy = &TcpMuxProxy{ + BaseProxy: &baseProxy, + cfg: cfg, + } case *config.UdpProxyConf: pxy = &UdpProxy{ BaseProxy: &baseProxy, @@ -141,6 +146,35 @@ func (pxy *TcpProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { conn, []byte(pxy.clientCfg.Token), m) } +// TCP Multiplexer +type TcpMuxProxy struct { + *BaseProxy + + cfg *config.TcpMuxProxyConf + proxyPlugin plugin.Plugin +} + +func (pxy *TcpMuxProxy) Run() (err error) { + if pxy.cfg.Plugin != "" { + pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) + if err != nil { + return + } + } + return +} + +func (pxy *TcpMuxProxy) Close() { + if pxy.proxyPlugin != nil { + pxy.proxyPlugin.Close() + } +} + +func (pxy *TcpMuxProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { + HandleTcpWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + conn, []byte(pxy.clientCfg.Token), m) +} + // HTTP type HttpProxy struct { *BaseProxy diff --git a/client/service.go b/client/service.go index 5ad08855..297d3a36 100644 --- a/client/service.go +++ b/client/service.go @@ -26,11 +26,11 @@ import ( "time" "github.com/fatedier/frp/assets" + "github.com/fatedier/frp/models/auth" "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" "github.com/fatedier/frp/utils/log" frpNet "github.com/fatedier/frp/utils/net" - "github.com/fatedier/frp/utils/util" "github.com/fatedier/frp/utils/version" "github.com/fatedier/frp/utils/xlog" @@ -46,6 +46,9 @@ type Service struct { ctl *Control ctlMu sync.RWMutex + // Sets authentication based on selected method + authSetter auth.Setter + cfg config.ClientCommonConf pxyCfgs map[string]config.ProxyConf visitorCfgs map[string]config.VisitorConf @@ -70,6 +73,7 @@ func NewService(cfg config.ClientCommonConf, pxyCfgs map[string]config.ProxyConf ctx, cancel := context.WithCancel(context.Background()) svr = &Service{ + authSetter: auth.NewAuthSetter(cfg.AuthClientConfig), cfg: cfg, cfgFile: cfgFile, pxyCfgs: pxyCfgs, @@ -105,7 +109,7 @@ func (svr *Service) Run() error { } } else { // login success - ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort) + ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter) ctl.Run() svr.ctlMu.Lock() svr.ctl = ctl @@ -159,7 +163,7 @@ func (svr *Service) keepControllerWorking() { // reconnect success, init delayTime delayTime = time.Second - ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort) + ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter) ctl.Run() svr.ctlMu.Lock() svr.ctl = ctl @@ -212,17 +216,20 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) { conn = stream } - now := time.Now().Unix() loginMsg := &msg.Login{ - Arch: runtime.GOARCH, - Os: runtime.GOOS, - PoolCount: svr.cfg.PoolCount, - User: svr.cfg.User, - Version: version.Full(), - PrivilegeKey: util.GetAuthKey(svr.cfg.Token, now), - Timestamp: now, - RunId: svr.runId, - Metas: svr.cfg.Metas, + Arch: runtime.GOARCH, + Os: runtime.GOOS, + PoolCount: svr.cfg.PoolCount, + User: svr.cfg.User, + Version: version.Full(), + Timestamp: time.Now().Unix(), + RunId: svr.runId, + Metas: svr.cfg.Metas, + } + + // Add auth + if err = svr.authSetter.SetLogin(loginMsg); err != nil { + return } if err = msg.WriteMsg(conn, loginMsg); err != nil { diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 82a906bd..8f47986a 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" "github.com/fatedier/frp/client" + "github.com/fatedier/frp/models/auth" "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/utils/log" "github.com/fatedier/frp/utils/version" @@ -65,6 +66,7 @@ var ( hostHeaderRewrite string role string sk string + multiplexer string serverName string bindAddr string bindPort int @@ -157,7 +159,6 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { cfg.User = user cfg.Protocol = protocol - cfg.Token = token cfg.LogLevel = logLevel cfg.LogFile = logFile cfg.LogMaxDays = int64(logMaxDays) @@ -168,6 +169,10 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { } cfg.DisableLogColor = disableLogColor + // Only token authentication is supported in cmd mode + cfg.AuthClientConfig = auth.GetDefaultAuthClientConf() + cfg.Token = token + return } diff --git a/cmd/frpc/sub/tcpmux.go b/cmd/frpc/sub/tcpmux.go new file mode 100644 index 00000000..d67d4981 --- /dev/null +++ b/cmd/frpc/sub/tcpmux.go @@ -0,0 +1,91 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 sub + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/models/config" + "github.com/fatedier/frp/models/consts" +) + +func init() { + tcpMuxCmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address") + tcpMuxCmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user") + tcpMuxCmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp or kcp or websocket") + tcpMuxCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") + tcpMuxCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") + tcpMuxCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path") + tcpMuxCmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days") + tcpMuxCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") + + tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") + tcpMuxCmd.PersistentFlags().StringVarP(&localIp, "local_ip", "i", "127.0.0.1", "local ip") + tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") + tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") + tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") + tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer") + tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") + tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") + + rootCmd.AddCommand(tcpMuxCmd) +} + +var tcpMuxCmd = &cobra.Command{ + Use: "tcpmux", + Short: "Run frpc with a single tcpmux proxy", + RunE: func(cmd *cobra.Command, args []string) error { + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cfg := &config.TcpMuxProxyConf{} + var prefix string + if user != "" { + prefix = user + "." + } + cfg.ProxyName = prefix + proxyName + cfg.ProxyType = consts.TcpMuxProxy + cfg.LocalIp = localIp + cfg.LocalPort = localPort + cfg.CustomDomains = strings.Split(customDomains, ",") + cfg.SubDomain = subDomain + cfg.Multiplexer = multiplexer + cfg.UseEncryption = useEncryption + cfg.UseCompression = useCompression + + err = cfg.CheckForCli() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + proxyConfs := map[string]config.ProxyConf{ + cfg.ProxyName: cfg, + } + err = startService(clientCfg, proxyConfs, nil, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return nil + }, +} diff --git a/cmd/frps/main.go b/cmd/frps/main.go index 8264dd70..50be5862 100644 --- a/cmd/frps/main.go +++ b/cmd/frps/main.go @@ -21,6 +21,7 @@ import ( "github.com/fatedier/golib/crypto" _ "github.com/fatedier/frp/assets/frps/statik" + _ "github.com/fatedier/frp/models/metrics" ) func main() { diff --git a/cmd/frps/root.go b/cmd/frps/root.go index ec175fe3..9096b28c 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" + "github.com/fatedier/frp/models/auth" "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/server" "github.com/fatedier/frp/utils/log" @@ -171,8 +172,11 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) { cfg.LogFile = logFile cfg.LogLevel = logLevel cfg.LogMaxDays = logMaxDays - cfg.Token = token cfg.SubDomainHost = subDomainHost + + // Only token authentication is supported in cmd mode + cfg.AuthServerConfig = auth.GetDefaultAuthServerConf() + cfg.Token = token if len(allowPorts) > 0 { // e.g. 1000-2000,2001,2002,3000-4000 ports, errRet := util.ParseRangeNumbers(allowPorts) diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index c6d3d406..35fbb171 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -264,3 +264,10 @@ bind_addr = 127.0.0.1 bind_port = 9001 use_encryption = false use_compression = false + +[tcpmuxhttpconnect] +type = tcpmux +multiplexer = httpconnect +local_ip = 127.0.0.1 +local_port = 10701 +custom_domains = tunnel1 diff --git a/conf/frps_full.ini b/conf/frps_full.ini index 030a3b3a..03896a2d 100644 --- a/conf/frps_full.ini +++ b/conf/frps_full.ini @@ -23,6 +23,12 @@ vhost_https_port = 443 # response header timeout(seconds) for vhost http server, default is 60s # vhost_http_timeout = 60 +# TcpMuxHttpConnectPort specifies the port that the server listens for TCP +# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP +# requests on one single port. If it's not - it will listen on this value for +# HTTP CONNECT requests. By default, this value is 0. +# tcpmux_httpconnect_port = 1337 + # set dashboard_addr and dashboard_port to view dashboard of frps # dashboard_addr's default value is same with bind_addr # dashboard is available only if dashboard_port is set @@ -33,6 +39,9 @@ dashboard_port = 7500 dashboard_user = admin dashboard_pwd = admin +# enable_prometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} in /metrics api. +enable_prometheus = true + # dashboard assets directory(only for debug mode) # assets_dir = ./static # console or real logFile path like ./frps.log @@ -46,9 +55,38 @@ log_max_days = 3 # disable log colors when log_file is console, default is false disable_log_color = false +# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true. +detailed_errors_to_client = true + +# AuthenticationMethod specifies what authentication method to use authenticate frpc with frps. +# If "token" is specified - token will be read into login message. +# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token". +authentication_method = token + +# AuthenticateHeartBeats specifies whether to include authentication token in heartbeats sent to frps. By default, this value is false. +authenticate_heartbeats = false + +# AuthenticateNewWorkConns specifies whether to include authentication token in new work connections sent to frps. By default, this value is false. +authenticate_new_work_conns = false + # auth token token = 12345678 +# OidcClientId specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc". +# By default, this value is "". +oidc_client_id = + +# OidcClientSecret specifies the client secret to use to get a token in OIDC authentication if AuthenticationMethod == "oidc". +# By default, this value is "". +oidc_client_secret = + +# OidcAudience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". +oidc_audience = + +# OidcTokenEndpointUrl specifies the URL which implements OIDC Token Endpoint. +# It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "". +oidc_token_endpoint_url = + # heartbeat configure, it's not recommended to modify the default value # the default value of heartbeat_timeout is 90 # heartbeat_timeout = 90 @@ -62,6 +100,9 @@ max_pool_count = 5 # max ports can be used for each client, default value is 0 means no limit max_ports_per_client = 0 +# TlsOnly specifies whether to only accept TLS-encrypted connections. By default, the value is false. +tls_only = false + # if subdomain_host is not empty, you can set subdomain when type is http or https in frpc's configure file # when subdomain is test, the host used by routing is test.frps.com subdomain_host = frps.com diff --git a/go.mod b/go.mod index a71a97c0..c511e8c2 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 + github.com/coreos/go-oidc v2.2.1+incompatible github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb github.com/fatedier/golib v0.0.0-20181107124048-ff8cd814b049 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible @@ -16,18 +17,21 @@ require ( github.com/klauspost/reedsolomon v1.9.1 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc - github.com/pkg/errors v0.8.0 // indirect + github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/prometheus/client_golang v1.4.1 github.com/rakyll/statik v0.1.1 github.com/rodaine/table v1.0.0 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.1 // indirect - github.com/stretchr/testify v1.3.0 + github.com/stretchr/testify v1.4.0 github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047 // indirect github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554 // indirect github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8 // indirect github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/text v0.3.2 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + gopkg.in/square/go-jose.v2 v2.4.1 // indirect ) diff --git a/go.sum b/go.sum index 26c9004f..fc1f5452 100644 --- a/go.sum +++ b/go.sum @@ -1,51 +1,141 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk= github.com/fatedier/golib v0.0.0-20181107124048-ff8cd814b049 h1:teH578mf2ii42NHhIp3PhgvjU5bv+NFMq9fSQR8NaG8= github.com/fatedier/golib v0.0.0-20181107124048-ff8cd814b049/go.mod h1:DqIrnl0rp3Zybg9zbJmozTy1n8fYJoX+QoAj9slIkKM= github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74= github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/reedsolomon v1.9.1 h1:kYrT1MlR4JH6PqOpC+okdb9CDTcwEC/BqpzK4WFyXL8= github.com/klauspost/reedsolomon v1.9.1/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc h1:lNOt1SMsgHXTdpuGw+RpnJtzUcCb/oRKZP65pBy9pr8= github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc/go.mod h1:6/gX3+E/IYGa0wMORlSMla999awQFdbaeQCHjSMKIzY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= +github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rakyll/statik v0.1.1/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= github.com/rodaine/table v1.0.0/go.mod h1:YAUzwPOji0DUJNEvggdxyQcUAl4g3hDRcFlyjnnR51I= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw= -github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= +gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/models/auth/auth.go b/models/auth/auth.go new file mode 100644 index 00000000..90a0c160 --- /dev/null +++ b/models/auth/auth.go @@ -0,0 +1,151 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 auth + +import ( + "fmt" + + "github.com/fatedier/frp/models/consts" + "github.com/fatedier/frp/models/msg" + + "github.com/vaughan0/go-ini" +) + +type baseConfig struct { + // AuthenticationMethod specifies what authentication method to use to + // authenticate frpc with frps. If "token" is specified - token will be + // read into login message. If "oidc" is specified - OIDC (Open ID Connect) + // token will be issued using OIDC settings. By default, this value is "token". + AuthenticationMethod string `json:"authentication_method"` + // AuthenticateHeartBeats specifies whether to include authentication token in + // heartbeats sent to frps. By default, this value is false. + AuthenticateHeartBeats bool `json:"authenticate_heartbeats"` + // AuthenticateNewWorkConns specifies whether to include authentication token in + // new work connections sent to frps. By default, this value is false. + AuthenticateNewWorkConns bool `json:"authenticate_new_work_conns"` +} + +func getDefaultBaseConf() baseConfig { + return baseConfig{ + AuthenticationMethod: "token", + AuthenticateHeartBeats: false, + AuthenticateNewWorkConns: false, + } +} + +func unmarshalBaseConfFromIni(conf ini.File) baseConfig { + var ( + tmpStr string + ok bool + ) + + cfg := getDefaultBaseConf() + + if tmpStr, ok = conf.Get("common", "authentication_method"); ok { + cfg.AuthenticationMethod = tmpStr + } + + if tmpStr, ok = conf.Get("common", "authenticate_heartbeats"); ok && tmpStr == "true" { + cfg.AuthenticateHeartBeats = true + } else { + cfg.AuthenticateHeartBeats = false + } + + if tmpStr, ok = conf.Get("common", "authenticate_new_work_conns"); ok && tmpStr == "true" { + cfg.AuthenticateNewWorkConns = true + } else { + cfg.AuthenticateNewWorkConns = false + } + + return cfg +} + +type AuthClientConfig struct { + baseConfig + oidcClientConfig + tokenConfig +} + +func GetDefaultAuthClientConf() AuthClientConfig { + return AuthClientConfig{ + baseConfig: getDefaultBaseConf(), + oidcClientConfig: getDefaultOidcClientConf(), + tokenConfig: getDefaultTokenConf(), + } +} + +func UnmarshalAuthClientConfFromIni(conf ini.File) (cfg AuthClientConfig) { + cfg.baseConfig = unmarshalBaseConfFromIni(conf) + cfg.oidcClientConfig = unmarshalOidcClientConfFromIni(conf) + cfg.tokenConfig = unmarshalTokenConfFromIni(conf) + return cfg +} + +type AuthServerConfig struct { + baseConfig + oidcServerConfig + tokenConfig +} + +func GetDefaultAuthServerConf() AuthServerConfig { + return AuthServerConfig{ + baseConfig: getDefaultBaseConf(), + oidcServerConfig: getDefaultOidcServerConf(), + tokenConfig: getDefaultTokenConf(), + } +} + +func UnmarshalAuthServerConfFromIni(conf ini.File) (cfg AuthServerConfig) { + cfg.baseConfig = unmarshalBaseConfFromIni(conf) + cfg.oidcServerConfig = unmarshalOidcServerConfFromIni(conf) + cfg.tokenConfig = unmarshalTokenConfFromIni(conf) + return cfg +} + +type Setter interface { + SetLogin(*msg.Login) error + SetPing(*msg.Ping) error + SetNewWorkConn(*msg.NewWorkConn) error +} + +func NewAuthSetter(cfg AuthClientConfig) (authProvider Setter) { + switch cfg.AuthenticationMethod { + case consts.TokenAuthMethod: + authProvider = NewTokenAuth(cfg.baseConfig, cfg.tokenConfig) + case consts.OidcAuthMethod: + authProvider = NewOidcAuthSetter(cfg.baseConfig, cfg.oidcClientConfig) + default: + panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod)) + } + + return authProvider +} + +type Verifier interface { + VerifyLogin(*msg.Login) error + VerifyPing(*msg.Ping) error + VerifyNewWorkConn(*msg.NewWorkConn) error +} + +func NewAuthVerifier(cfg AuthServerConfig) (authVerifier Verifier) { + switch cfg.AuthenticationMethod { + case consts.TokenAuthMethod: + authVerifier = NewTokenAuth(cfg.baseConfig, cfg.tokenConfig) + case consts.OidcAuthMethod: + authVerifier = NewOidcAuthVerifier(cfg.baseConfig, cfg.oidcServerConfig) + } + + return authVerifier +} diff --git a/models/auth/oidc.go b/models/auth/oidc.go new file mode 100644 index 00000000..b38b1c08 --- /dev/null +++ b/models/auth/oidc.go @@ -0,0 +1,255 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 auth + +import ( + "context" + "fmt" + + "github.com/fatedier/frp/models/msg" + + "github.com/coreos/go-oidc" + "github.com/vaughan0/go-ini" + "golang.org/x/oauth2/clientcredentials" +) + +type oidcClientConfig struct { + // OidcClientId specifies the client ID to use to get a token in OIDC + // authentication if AuthenticationMethod == "oidc". By default, this value + // is "". + OidcClientId string `json:"oidc_client_id"` + // OidcClientSecret specifies the client secret to use to get a token in OIDC + // authentication if AuthenticationMethod == "oidc". By default, this value + // is "". + OidcClientSecret string `json:"oidc_client_secret"` + // OidcAudience specifies the audience of the token in OIDC authentication + //if AuthenticationMethod == "oidc". By default, this value is "". + OidcAudience string `json:"oidc_audience"` + // OidcTokenEndpointUrl specifies the URL which implements OIDC Token Endpoint. + // It will be used to get an OIDC token if AuthenticationMethod == "oidc". + // By default, this value is "". + OidcTokenEndpointUrl string `json:"oidc_token_endpoint_url"` +} + +func getDefaultOidcClientConf() oidcClientConfig { + return oidcClientConfig{ + OidcClientId: "", + OidcClientSecret: "", + OidcAudience: "", + OidcTokenEndpointUrl: "", + } +} + +func unmarshalOidcClientConfFromIni(conf ini.File) oidcClientConfig { + var ( + tmpStr string + ok bool + ) + + cfg := getDefaultOidcClientConf() + + if tmpStr, ok = conf.Get("common", "oidc_client_id"); ok { + cfg.OidcClientId = tmpStr + } + + if tmpStr, ok = conf.Get("common", "oidc_client_secret"); ok { + cfg.OidcClientSecret = tmpStr + } + + if tmpStr, ok = conf.Get("common", "oidc_audience"); ok { + cfg.OidcAudience = tmpStr + } + + if tmpStr, ok = conf.Get("common", "oidc_token_endpoint_url"); ok { + cfg.OidcTokenEndpointUrl = tmpStr + } + + return cfg +} + +type oidcServerConfig struct { + // OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer + // will be used to load public keys to verify signature and will be compared + // with the issuer claim in the OIDC token. It will be used if + // AuthenticationMethod == "oidc". By default, this value is "". + OidcIssuer string `json:"oidc_issuer"` + // OidcAudience specifies the audience OIDC tokens should contain when validated. + // If this value is empty, audience ("client ID") verification will be skipped. + // It will be used when AuthenticationMethod == "oidc". By default, this + // value is "". + OidcAudience string `json:"oidc_audience"` + // OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is + // expired. It will be used when AuthenticationMethod == "oidc". By default, this + // value is false. + OidcSkipExpiryCheck bool `json:"oidc_skip_expiry_check"` + // OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's + // issuer claim matches the issuer specified in OidcIssuer. It will be used when + // AuthenticationMethod == "oidc". By default, this value is false. + OidcSkipIssuerCheck bool `json:"oidc_skip_issuer_check"` +} + +func getDefaultOidcServerConf() oidcServerConfig { + return oidcServerConfig{ + OidcIssuer: "", + OidcAudience: "", + OidcSkipExpiryCheck: false, + OidcSkipIssuerCheck: false, + } +} + +func unmarshalOidcServerConfFromIni(conf ini.File) oidcServerConfig { + var ( + tmpStr string + ok bool + ) + + cfg := getDefaultOidcServerConf() + + if tmpStr, ok = conf.Get("common", "oidc_issuer"); ok { + cfg.OidcIssuer = tmpStr + } + + if tmpStr, ok = conf.Get("common", "oidc_audience"); ok { + cfg.OidcAudience = tmpStr + } + + if tmpStr, ok = conf.Get("common", "oidc_skip_expiry_check"); ok && tmpStr == "true" { + cfg.OidcSkipExpiryCheck = true + } else { + cfg.OidcSkipExpiryCheck = false + } + + if tmpStr, ok = conf.Get("common", "oidc_skip_issuer_check"); ok && tmpStr == "true" { + cfg.OidcSkipIssuerCheck = true + } else { + cfg.OidcSkipIssuerCheck = false + } + + return cfg +} + +type OidcAuthProvider struct { + baseConfig + + tokenGenerator *clientcredentials.Config +} + +func NewOidcAuthSetter(baseCfg baseConfig, cfg oidcClientConfig) *OidcAuthProvider { + tokenGenerator := &clientcredentials.Config{ + ClientID: cfg.OidcClientId, + ClientSecret: cfg.OidcClientSecret, + Scopes: []string{cfg.OidcAudience}, + TokenURL: cfg.OidcTokenEndpointUrl, + } + + return &OidcAuthProvider{ + baseConfig: baseCfg, + tokenGenerator: tokenGenerator, + } +} + +func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) { + tokenObj, err := auth.tokenGenerator.Token(context.Background()) + if err != nil { + return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err) + } + return tokenObj.AccessToken, nil +} + +func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) { + loginMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { + if !auth.AuthenticateHeartBeats { + return nil + } + + pingMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { + if !auth.AuthenticateNewWorkConns { + return nil + } + + newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken() + return err +} + +type OidcAuthConsumer struct { + baseConfig + + verifier *oidc.IDTokenVerifier + subjectFromLogin string +} + +func NewOidcAuthVerifier(baseCfg baseConfig, cfg oidcServerConfig) *OidcAuthConsumer { + provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer) + if err != nil { + panic(err) + } + verifierConf := oidc.Config{ + ClientID: cfg.OidcAudience, + SkipClientIDCheck: cfg.OidcAudience == "", + SkipExpiryCheck: cfg.OidcSkipExpiryCheck, + SkipIssuerCheck: cfg.OidcSkipIssuerCheck, + } + return &OidcAuthConsumer{ + baseConfig: baseCfg, + verifier: provider.Verifier(&verifierConf), + } +} + +func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) { + token, err := auth.verifier.Verify(context.Background(), loginMsg.PrivilegeKey) + if err != nil { + return fmt.Errorf("invalid OIDC token in login: %v", err) + } + auth.subjectFromLogin = token.Subject + return nil +} + +func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err error) { + token, err := auth.verifier.Verify(context.Background(), privilegeKey) + if err != nil { + return fmt.Errorf("invalid OIDC token in ping: %v", err) + } + if token.Subject != auth.subjectFromLogin { + return fmt.Errorf("received different OIDC subject in login and ping. "+ + "original subject: %s, "+ + "new subject: %s", + auth.subjectFromLogin, token.Subject) + } + return nil +} + +func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) { + if !auth.AuthenticateHeartBeats { + return nil + } + + return auth.verifyPostLoginToken(pingMsg.PrivilegeKey) +} + +func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { + if !auth.AuthenticateNewWorkConns { + return nil + } + + return auth.verifyPostLoginToken(newWorkConnMsg.PrivilegeKey) +} diff --git a/models/auth/token.go b/models/auth/token.go new file mode 100644 index 00000000..f7be085c --- /dev/null +++ b/models/auth/token.go @@ -0,0 +1,120 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 auth + +import ( + "fmt" + "time" + + "github.com/fatedier/frp/models/msg" + "github.com/fatedier/frp/utils/util" + + "github.com/vaughan0/go-ini" +) + +type tokenConfig struct { + // Token specifies the authorization token used to create keys to be sent + // to the server. The server must have a matching token for authorization + // to succeed. By default, this value is "". + Token string `json:"token"` +} + +func getDefaultTokenConf() tokenConfig { + return tokenConfig{ + Token: "", + } +} + +func unmarshalTokenConfFromIni(conf ini.File) tokenConfig { + var ( + tmpStr string + ok bool + ) + + cfg := getDefaultTokenConf() + + if tmpStr, ok = conf.Get("common", "token"); ok { + cfg.Token = tmpStr + } + + return cfg +} + +type TokenAuthSetterVerifier struct { + baseConfig + + token string +} + +func NewTokenAuth(baseCfg baseConfig, cfg tokenConfig) *TokenAuthSetterVerifier { + return &TokenAuthSetterVerifier{ + baseConfig: baseCfg, + token: cfg.Token, + } +} + +func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) (err error) { + loginMsg.PrivilegeKey = util.GetAuthKey(auth.token, loginMsg.Timestamp) + return nil +} + +func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { + if !auth.AuthenticateHeartBeats { + return nil + } + + pingMsg.Timestamp = time.Now().Unix() + pingMsg.PrivilegeKey = util.GetAuthKey(auth.token, pingMsg.Timestamp) + return nil +} + +func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { + if !auth.AuthenticateHeartBeats { + return nil + } + + newWorkConnMsg.Timestamp = time.Now().Unix() + newWorkConnMsg.PrivilegeKey = util.GetAuthKey(auth.token, newWorkConnMsg.Timestamp) + return nil +} + +func (auth *TokenAuthSetterVerifier) VerifyLogin(loginMsg *msg.Login) error { + if util.GetAuthKey(auth.token, loginMsg.Timestamp) != loginMsg.PrivilegeKey { + return fmt.Errorf("token in login doesn't match token from configuration") + } + return nil +} + +func (auth *TokenAuthSetterVerifier) VerifyPing(pingMsg *msg.Ping) error { + if !auth.AuthenticateHeartBeats { + return nil + } + + if util.GetAuthKey(auth.token, pingMsg.Timestamp) != pingMsg.PrivilegeKey { + return fmt.Errorf("token in heartbeat doesn't match token from configuration") + } + return nil +} + +func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { + if !auth.AuthenticateNewWorkConns { + return nil + } + + if util.GetAuthKey(auth.token, newWorkConnMsg.Timestamp) != newWorkConnMsg.PrivilegeKey { + return fmt.Errorf("token in NewWorkConn doesn't match token from configuration") + } + return nil +} diff --git a/models/config/client_common.go b/models/config/client_common.go index 2b5006b4..3f8c485d 100644 --- a/models/config/client_common.go +++ b/models/config/client_common.go @@ -21,12 +21,15 @@ import ( "strings" ini "github.com/vaughan0/go-ini" + + "github.com/fatedier/frp/models/auth" ) // ClientCommonConf contains information for a client service. It is // recommended to use GetDefaultClientConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ClientCommonConf struct { + auth.AuthClientConfig // ServerAddr specifies the address of the server to connect to. By // default, this value is "0.0.0.0". ServerAddr string `json:"server_addr"` @@ -56,10 +59,6 @@ type ClientCommonConf struct { // DisableLogColor disables log colors when LogWay == "console" when set to // true. By default, this value is false. DisableLogColor bool `json:"disable_log_color"` - // Token specifies the authorization token used to create keys to be sent - // to the server. The server must have a matching token for authorization - // to succeed. By default, this value is "". - Token string `json:"token"` // AdminAddr specifies the address that the admin server binds to. By // default, this value is "127.0.0.1". AdminAddr string `json:"admin_addr"` @@ -130,7 +129,6 @@ func GetDefaultClientConf() ClientCommonConf { LogLevel: "info", LogMaxDays: 3, DisableLogColor: false, - Token: "", AdminAddr: "127.0.0.1", AdminPort: 0, AdminUser: "", @@ -158,6 +156,8 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error return ClientCommonConf{}, fmt.Errorf("parse ini conf file error: %v", err) } + cfg.AuthClientConfig = auth.UnmarshalAuthClientConfFromIni(conf) + var ( tmpStr string ok bool @@ -203,10 +203,6 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error } } - if tmpStr, ok = conf.Get("common", "token"); ok { - cfg.Token = tmpStr - } - if tmpStr, ok = conf.Get("common", "admin_addr"); ok { cfg.AdminAddr = tmpStr } diff --git a/models/config/proxy.go b/models/config/proxy.go index f4ddba50..591011f9 100644 --- a/models/config/proxy.go +++ b/models/config/proxy.go @@ -34,6 +34,7 @@ var ( func init() { proxyConfTypeMap = make(map[string]reflect.Type) proxyConfTypeMap[consts.TcpProxy] = reflect.TypeOf(TcpProxyConf{}) + proxyConfTypeMap[consts.TcpMuxProxy] = reflect.TypeOf(TcpMuxProxyConf{}) proxyConfTypeMap[consts.UdpProxy] = reflect.TypeOf(UdpProxyConf{}) proxyConfTypeMap[consts.HttpProxy] = reflect.TypeOf(HttpProxyConf{}) proxyConfTypeMap[consts.HttpsProxy] = reflect.TypeOf(HttpsProxyConf{}) @@ -149,7 +150,7 @@ 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 } @@ -574,6 +575,84 @@ func (cfg *TcpProxyConf) CheckForCli() (err error) { func (cfg *TcpProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { return nil } +// TCP Multiplexer +type TcpMuxProxyConf struct { + BaseProxyConf + DomainConf + + Multiplexer string `json:"multiplexer"` +} + +func (cfg *TcpMuxProxyConf) Compare(cmp ProxyConf) bool { + cmpConf, ok := cmp.(*TcpMuxProxyConf) + if !ok { + return false + } + + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || + !cfg.DomainConf.compare(&cmpConf.DomainConf) || + cfg.Multiplexer != cmpConf.Multiplexer { + return false + } + return true +} + +func (cfg *TcpMuxProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) + cfg.DomainConf.UnmarshalFromMsg(pMsg) + cfg.Multiplexer = pMsg.Multiplexer +} + +func (cfg *TcpMuxProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { + if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { + return + } + if err = cfg.DomainConf.UnmarshalFromIni(prefix, name, section); err != nil { + return + } + + cfg.Multiplexer = section["multiplexer"] + if cfg.Multiplexer != consts.HttpConnectTcpMultiplexer { + return fmt.Errorf("parse conf error: proxy [%s] incorrect multiplexer [%s]", name, cfg.Multiplexer) + } + return +} + +func (cfg *TcpMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.MarshalToMsg(pMsg) + cfg.DomainConf.MarshalToMsg(pMsg) + pMsg.Multiplexer = cfg.Multiplexer +} + +func (cfg *TcpMuxProxyConf) CheckForCli() (err error) { + if err = cfg.BaseProxyConf.checkForCli(); err != nil { + return err + } + if err = cfg.DomainConf.checkForCli(); err != nil { + return err + } + if cfg.Multiplexer != consts.HttpConnectTcpMultiplexer { + return fmt.Errorf("parse conf error: incorrect multiplexer [%s]", cfg.Multiplexer) + } + return +} + +func (cfg *TcpMuxProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { + if cfg.Multiplexer != consts.HttpConnectTcpMultiplexer { + return fmt.Errorf("proxy [%s] incorrect multiplexer [%s]", cfg.ProxyName, cfg.Multiplexer) + } + + if cfg.Multiplexer == consts.HttpConnectTcpMultiplexer && serverCfg.TcpMuxHttpConnectPort == 0 { + return fmt.Errorf("proxy [%s] type [tcpmux] with multiplexer [httpconnect] requires tcpmux_httpconnect_port configuration", cfg.ProxyName) + } + + if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { + err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) + return + } + return +} + // UDP type UdpProxyConf struct { BaseProxyConf diff --git a/models/config/server_common.go b/models/config/server_common.go index df6b7a10..a6aa969e 100644 --- a/models/config/server_common.go +++ b/models/config/server_common.go @@ -21,6 +21,7 @@ import ( ini "github.com/vaughan0/go-ini" + "github.com/fatedier/frp/models/auth" plugin "github.com/fatedier/frp/models/plugin/server" "github.com/fatedier/frp/utils/util" ) @@ -29,6 +30,7 @@ import ( // recommended to use GetDefaultServerConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ServerCommonConf struct { + auth.AuthServerConfig // BindAddr specifies the address that the server binds to. By default, // this value is "0.0.0.0". BindAddr string `json:"bind_addr"` @@ -46,25 +48,25 @@ type ServerCommonConf struct { // ProxyBindAddr specifies the address that the proxy binds to. This value // may be the same as BindAddr. By default, this value is "0.0.0.0". ProxyBindAddr string `json:"proxy_bind_addr"` - // VhostHttpPort specifies the port that the server listens for HTTP Vhost // requests. If this value is 0, the server will not listen for HTTP // requests. By default, this value is 0. VhostHttpPort int `json:"vhost_http_port"` - // VhostHttpsPort specifies the port that the server listens for HTTPS // Vhost requests. If this value is 0, the server will not listen for HTTPS // requests. By default, this value is 0. VhostHttpsPort int `json:"vhost_https_port"` - + // TcpMuxHttpConnectPort specifies the port that the server listens for TCP + // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP + // requests on one single port. If it's not - it will listen on this value for + // HTTP CONNECT requests. By default, this value is 0. + TcpMuxHttpConnectPort int `json:"tcpmux_httpconnect_port"` // VhostHttpTimeout specifies the response header timeout for the Vhost // HTTP server, in seconds. By default, this value is 60. VhostHttpTimeout int64 `json:"vhost_http_timeout"` - // DashboardAddr specifies the address that the dashboard binds to. By // default, this value is "0.0.0.0". DashboardAddr string `json:"dashboard_addr"` - // DashboardPort specifies the port that the dashboard listens on. If this // value is 0, the dashboard will not be started. By default, this value is // 0. @@ -75,6 +77,9 @@ type ServerCommonConf struct { // DashboardUser specifies the password that the dashboard will use for // login. By default, this value is "admin". DashboardPwd string `json:"dashboard_pwd"` + // EnablePrometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} + // in /metrics api. + EnablePrometheus bool `json:"enable_prometheus"` // AssetsDir specifies the local directory that the dashboard will load // resources from. If this value is "", assets will be loaded from the // bundled executable using statik. By default, this value is "". @@ -98,10 +103,10 @@ type ServerCommonConf struct { // DisableLogColor disables log colors when LogWay == "console" when set to // true. By default, this value is false. DisableLogColor bool `json:"disable_log_color"` - // Token specifies the authorization token used to authenticate keys - // received from clients. Clients must have a matching token to be - // authorized to use the server. By default, this value is "". - Token string `json:"token"` + // DetailedErrorsToClient defines whether to send the specific error (with + // debug info) to frpc. By default, this value is true. + DetailedErrorsToClient bool `json:"detailed_errors_to_client"` + // SubDomainHost specifies the domain that will be attached to sub-domains // requested by the client when using Vhost proxying. For example, if this // value is set to "frps.com" and the client requested the subdomain @@ -128,6 +133,9 @@ type ServerCommonConf struct { // may proxy to. If this value is 0, no limit will be applied. By default, // this value is 0. MaxPortsPerClient int64 `json:"max_ports_per_client"` + // TlsOnly specifies whether to only accept TLS-encrypted connections. By + // default, the value is false. + TlsOnly bool `json:"tls_only"` // HeartBeatTimeout specifies the maximum time to wait for a heartbeat // before terminating the connection. It is not recommended to change this // value. By default, this value is 90. @@ -143,34 +151,37 @@ type ServerCommonConf struct { // defaults. func GetDefaultServerConf() ServerCommonConf { return ServerCommonConf{ - BindAddr: "0.0.0.0", - BindPort: 7000, - BindUdpPort: 0, - KcpBindPort: 0, - ProxyBindAddr: "0.0.0.0", - VhostHttpPort: 0, - VhostHttpsPort: 0, - VhostHttpTimeout: 60, - DashboardAddr: "0.0.0.0", - DashboardPort: 0, - DashboardUser: "admin", - DashboardPwd: "admin", - AssetsDir: "", - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - DisableLogColor: false, - Token: "", - SubDomainHost: "", - TcpMux: true, - AllowPorts: make(map[int]struct{}), - MaxPoolCount: 5, - MaxPortsPerClient: 0, - HeartBeatTimeout: 90, - UserConnTimeout: 10, - Custom404Page: "", - HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), + BindAddr: "0.0.0.0", + BindPort: 7000, + BindUdpPort: 0, + KcpBindPort: 0, + ProxyBindAddr: "0.0.0.0", + VhostHttpPort: 0, + VhostHttpsPort: 0, + TcpMuxHttpConnectPort: 0, + VhostHttpTimeout: 60, + DashboardAddr: "0.0.0.0", + DashboardPort: 0, + DashboardUser: "admin", + DashboardPwd: "admin", + EnablePrometheus: false, + AssetsDir: "", + LogFile: "console", + LogWay: "console", + LogLevel: "info", + LogMaxDays: 3, + DisableLogColor: false, + DetailedErrorsToClient: true, + SubDomainHost: "", + TcpMux: true, + AllowPorts: make(map[int]struct{}), + MaxPoolCount: 5, + MaxPortsPerClient: 0, + TlsOnly: false, + HeartBeatTimeout: 90, + UserConnTimeout: 10, + Custom404Page: "", + HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), } } @@ -187,6 +198,8 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error UnmarshalPluginsFromIni(conf, &cfg) + cfg.AuthServerConfig = auth.UnmarshalAuthServerConfFromIni(conf) + var ( tmpStr string ok bool @@ -251,6 +264,17 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error cfg.VhostHttpsPort = 0 } + if tmpStr, ok = conf.Get("common", "tcpmux_httpconnect_port"); ok { + if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { + err = fmt.Errorf("Parse conf error: invalid tcpmux_httpconnect_port") + return + } else { + cfg.TcpMuxHttpConnectPort = int(v) + } + } else { + cfg.TcpMuxHttpConnectPort = 0 + } + if tmpStr, ok = conf.Get("common", "vhost_http_timeout"); ok { v, errRet := strconv.ParseInt(tmpStr, 10, 64) if errRet != nil || v < 0 { @@ -286,6 +310,10 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error cfg.DashboardPwd = tmpStr } + if tmpStr, ok = conf.Get("common", "enable_prometheus"); ok && tmpStr == "true" { + cfg.EnablePrometheus = true + } + if tmpStr, ok = conf.Get("common", "assets_dir"); ok { cfg.AssetsDir = tmpStr } @@ -314,7 +342,11 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error cfg.DisableLogColor = true } - cfg.Token, _ = conf.Get("common", "token") + if tmpStr, ok = conf.Get("common", "detailed_errors_to_client"); ok && tmpStr == "false" { + cfg.DetailedErrorsToClient = false + } else { + cfg.DetailedErrorsToClient = true + } if allowPortsStr, ok := conf.Get("common", "allow_ports"); ok { // e.g. 1000-2000,2001,2002,3000-4000 @@ -378,6 +410,12 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error cfg.HeartBeatTimeout = v } } + + if tmpStr, ok = conf.Get("common", "tls_only"); ok && tmpStr == "true" { + cfg.TlsOnly = true + } else { + cfg.TlsOnly = false + } return } diff --git a/models/consts/consts.go b/models/consts/consts.go index 9bf5880b..4c1ca4c7 100644 --- a/models/consts/consts.go +++ b/models/consts/consts.go @@ -23,10 +23,18 @@ var ( Offline string = "offline" // proxy type - TcpProxy string = "tcp" - UdpProxy string = "udp" - HttpProxy string = "http" - HttpsProxy string = "https" - StcpProxy string = "stcp" - XtcpProxy string = "xtcp" + TcpProxy string = "tcp" + UdpProxy string = "udp" + TcpMuxProxy string = "tcpmux" + HttpProxy string = "http" + HttpsProxy string = "https" + StcpProxy string = "stcp" + XtcpProxy string = "xtcp" + + // authentication method + TokenAuthMethod string = "token" + OidcAuthMethod string = "oidc" + + // tcp multiplexer + HttpConnectTcpMultiplexer string = "httpconnect" ) diff --git a/models/metrics/aggregate/server.go b/models/metrics/aggregate/server.go new file mode 100644 index 00000000..39549f97 --- /dev/null +++ b/models/metrics/aggregate/server.go @@ -0,0 +1,93 @@ +// Copyright 2020 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 aggregate + +import ( + "github.com/fatedier/frp/models/metrics/mem" + "github.com/fatedier/frp/models/metrics/prometheus" + "github.com/fatedier/frp/server/metrics" +) + +// EnableMem start to mark metrics to memory monitor system. +func EnableMem() { + sm.Add(mem.ServerMetrics) +} + +// EnablePrometheus start to mark metrics to prometheus. +func EnablePrometheus() { + sm.Add(prometheus.ServerMetrics) +} + +var sm *serverMetrics = &serverMetrics{} + +func init() { + metrics.Register(sm) +} + +type serverMetrics struct { + ms []metrics.ServerMetrics +} + +func (m *serverMetrics) Add(sm metrics.ServerMetrics) { + m.ms = append(m.ms, sm) +} + +func (m *serverMetrics) NewClient() { + for _, v := range m.ms { + v.NewClient() + } +} + +func (m *serverMetrics) CloseClient() { + for _, v := range m.ms { + v.CloseClient() + } +} + +func (m *serverMetrics) NewProxy(name string, proxyType string) { + for _, v := range m.ms { + v.NewProxy(name, proxyType) + } +} + +func (m *serverMetrics) CloseProxy(name string, proxyType string) { + for _, v := range m.ms { + v.CloseProxy(name, proxyType) + } +} + +func (m *serverMetrics) OpenConnection(name string, proxyType string) { + for _, v := range m.ms { + v.OpenConnection(name, proxyType) + } +} + +func (m *serverMetrics) CloseConnection(name string, proxyType string) { + for _, v := range m.ms { + v.CloseConnection(name, proxyType) + } +} + +func (m *serverMetrics) AddTrafficIn(name string, proxyType string, trafficBytes int64) { + for _, v := range m.ms { + v.AddTrafficIn(name, proxyType, trafficBytes) + } +} + +func (m *serverMetrics) AddTrafficOut(name string, proxyType string, trafficBytes int64) { + for _, v := range m.ms { + v.AddTrafficOut(name, proxyType, trafficBytes) + } +} diff --git a/models/metrics/mem/server.go b/models/metrics/mem/server.go new file mode 100644 index 00000000..d7c191fa --- /dev/null +++ b/models/metrics/mem/server.go @@ -0,0 +1,262 @@ +// 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 mem + +import ( + "sync" + "time" + + server "github.com/fatedier/frp/server/metrics" + "github.com/fatedier/frp/utils/log" + "github.com/fatedier/frp/utils/metric" +) + +var sm *serverMetrics = newServerMetrics() +var ServerMetrics server.ServerMetrics +var StatsCollector Collector + +func init() { + ServerMetrics = sm + StatsCollector = sm + sm.run() +} + +type serverMetrics struct { + info *ServerStatistics + mu sync.Mutex +} + +func newServerMetrics() *serverMetrics { + return &serverMetrics{ + info: &ServerStatistics{ + TotalTrafficIn: metric.NewDateCounter(ReserveDays), + TotalTrafficOut: metric.NewDateCounter(ReserveDays), + CurConns: metric.NewCounter(), + + ClientCounts: metric.NewCounter(), + ProxyTypeCounts: make(map[string]metric.Counter), + + ProxyStatistics: make(map[string]*ProxyStatistics), + }, + } +} + +func (m *serverMetrics) run() { + go func() { + for { + time.Sleep(12 * time.Hour) + log.Debug("start to clear useless proxy statistics data...") + m.clearUselessInfo() + log.Debug("finish to clear useless proxy statistics data") + } + }() +} + +func (m *serverMetrics) clearUselessInfo() { + // To check if there are proxies that closed than 7 days and drop them. + m.mu.Lock() + defer m.mu.Unlock() + for name, data := range m.info.ProxyStatistics { + if !data.LastCloseTime.IsZero() && time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour { + delete(m.info.ProxyStatistics, name) + log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String()) + } + } +} + +func (m *serverMetrics) NewClient() { + m.info.ClientCounts.Inc(1) +} + +func (m *serverMetrics) CloseClient() { + m.info.ClientCounts.Dec(1) +} + +func (m *serverMetrics) NewProxy(name string, proxyType string) { + m.mu.Lock() + defer m.mu.Unlock() + counter, ok := m.info.ProxyTypeCounts[proxyType] + if !ok { + counter = metric.NewCounter() + } + counter.Inc(1) + m.info.ProxyTypeCounts[proxyType] = counter + + proxyStats, ok := m.info.ProxyStatistics[name] + if !(ok && proxyStats.ProxyType == proxyType) { + proxyStats = &ProxyStatistics{ + Name: name, + ProxyType: proxyType, + CurConns: metric.NewCounter(), + TrafficIn: metric.NewDateCounter(ReserveDays), + TrafficOut: metric.NewDateCounter(ReserveDays), + } + m.info.ProxyStatistics[name] = proxyStats + } + proxyStats.LastStartTime = time.Now() +} + +func (m *serverMetrics) CloseProxy(name string, proxyType string) { + m.mu.Lock() + defer m.mu.Unlock() + if counter, ok := m.info.ProxyTypeCounts[proxyType]; ok { + counter.Dec(1) + } + if proxyStats, ok := m.info.ProxyStatistics[name]; ok { + proxyStats.LastCloseTime = time.Now() + } +} + +func (m *serverMetrics) OpenConnection(name string, proxyType string) { + m.info.CurConns.Inc(1) + + m.mu.Lock() + defer m.mu.Unlock() + proxyStats, ok := m.info.ProxyStatistics[name] + if ok { + proxyStats.CurConns.Inc(1) + m.info.ProxyStatistics[name] = proxyStats + } +} + +func (m *serverMetrics) CloseConnection(name string, proxyType string) { + m.info.CurConns.Dec(1) + + m.mu.Lock() + defer m.mu.Unlock() + proxyStats, ok := m.info.ProxyStatistics[name] + if ok { + proxyStats.CurConns.Dec(1) + m.info.ProxyStatistics[name] = proxyStats + } +} + +func (m *serverMetrics) AddTrafficIn(name string, proxyType string, trafficBytes int64) { + m.info.TotalTrafficIn.Inc(trafficBytes) + + m.mu.Lock() + defer m.mu.Unlock() + + proxyStats, ok := m.info.ProxyStatistics[name] + if ok { + proxyStats.TrafficIn.Inc(trafficBytes) + m.info.ProxyStatistics[name] = proxyStats + } +} + +func (m *serverMetrics) AddTrafficOut(name string, proxyType string, trafficBytes int64) { + m.info.TotalTrafficOut.Inc(trafficBytes) + + m.mu.Lock() + defer m.mu.Unlock() + + proxyStats, ok := m.info.ProxyStatistics[name] + if ok { + proxyStats.TrafficOut.Inc(trafficBytes) + m.info.ProxyStatistics[name] = proxyStats + } +} + +// Get stats data api. + +func (m *serverMetrics) GetServer() *ServerStats { + m.mu.Lock() + defer m.mu.Unlock() + s := &ServerStats{ + TotalTrafficIn: m.info.TotalTrafficIn.TodayCount(), + TotalTrafficOut: m.info.TotalTrafficOut.TodayCount(), + CurConns: m.info.CurConns.Count(), + ClientCounts: m.info.ClientCounts.Count(), + ProxyTypeCounts: make(map[string]int64), + } + for k, v := range m.info.ProxyTypeCounts { + s.ProxyTypeCounts[k] = v.Count() + } + return s +} + +func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats { + res := make([]*ProxyStats, 0) + m.mu.Lock() + defer m.mu.Unlock() + + for name, proxyStats := range m.info.ProxyStatistics { + if proxyStats.ProxyType != proxyType { + continue + } + + ps := &ProxyStats{ + Name: name, + Type: proxyStats.ProxyType, + TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), + TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), + CurConns: proxyStats.CurConns.Count(), + } + if !proxyStats.LastStartTime.IsZero() { + ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") + } + if !proxyStats.LastCloseTime.IsZero() { + ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") + } + res = append(res, ps) + } + return res +} + +func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName string) (res *ProxyStats) { + m.mu.Lock() + defer m.mu.Unlock() + + for name, proxyStats := range m.info.ProxyStatistics { + if proxyStats.ProxyType != proxyType { + continue + } + + if name != proxyName { + continue + } + + res = &ProxyStats{ + Name: name, + Type: proxyStats.ProxyType, + TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), + TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), + CurConns: proxyStats.CurConns.Count(), + } + if !proxyStats.LastStartTime.IsZero() { + res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") + } + if !proxyStats.LastCloseTime.IsZero() { + res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") + } + break + } + return +} + +func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) { + m.mu.Lock() + defer m.mu.Unlock() + + proxyStats, ok := m.info.ProxyStatistics[name] + if ok { + res = &ProxyTrafficInfo{ + Name: name, + } + res.TrafficIn = proxyStats.TrafficIn.GetLastDaysCount(ReserveDays) + res.TrafficOut = proxyStats.TrafficOut.GetLastDaysCount(ReserveDays) + } + return +} diff --git a/server/stats/stats.go b/models/metrics/mem/types.go similarity index 72% rename from server/stats/stats.go rename to models/metrics/mem/types.go index a09d7daa..bdb1863d 100644 --- a/server/stats/stats.go +++ b/models/metrics/mem/types.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package stats +package mem import ( "time" @@ -24,19 +24,6 @@ const ( ReserveDays = 7 ) -type StatsType int - -const ( - TypeNewClient StatsType = iota - TypeCloseClient - TypeNewProxy - TypeCloseProxy - TypeOpenConnection - TypeCloseConnection - TypeAddTrafficIn - TypeAddTrafficOut -) - type ServerStats struct { TotalTrafficIn int64 TotalTrafficOut int64 @@ -88,42 +75,8 @@ type ServerStatistics struct { } type Collector interface { - Mark(statsType StatsType, payload interface{}) - Run() error GetServer() *ServerStats GetProxiesByType(proxyType string) []*ProxyStats GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats GetProxyTraffic(name string) *ProxyTrafficInfo } - -type NewClientPayload struct{} - -type CloseClientPayload struct{} - -type NewProxyPayload struct { - Name string - ProxyType string -} - -type CloseProxyPayload struct { - Name string - ProxyType string -} - -type OpenConnectionPayload struct { - ProxyName string -} - -type CloseConnectionPayload struct { - ProxyName string -} - -type AddTrafficInPayload struct { - ProxyName string - TrafficBytes int64 -} - -type AddTrafficOutPayload struct { - ProxyName string - TrafficBytes int64 -} diff --git a/models/metrics/metrics.go b/models/metrics/metrics.go new file mode 100644 index 00000000..b9fb3084 --- /dev/null +++ b/models/metrics/metrics.go @@ -0,0 +1,8 @@ +package metrics + +import ( + "github.com/fatedier/frp/models/metrics/aggregate" +) + +var EnableMem = aggregate.EnableMem +var EnablePrometheus = aggregate.EnablePrometheus diff --git a/models/metrics/prometheus/server.go b/models/metrics/prometheus/server.go new file mode 100644 index 00000000..9cfdfda6 --- /dev/null +++ b/models/metrics/prometheus/server.go @@ -0,0 +1,95 @@ +package prometheus + +import ( + "github.com/fatedier/frp/server/metrics" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + namespace = "frp" + serverSubsystem = "server" +) + +var ServerMetrics metrics.ServerMetrics = newServerMetrics() + +type serverMetrics struct { + clientCount prometheus.Gauge + proxyCount *prometheus.GaugeVec + connectionCount *prometheus.GaugeVec + trafficIn *prometheus.CounterVec + trafficOut *prometheus.CounterVec +} + +func (m *serverMetrics) NewClient() { + m.clientCount.Inc() +} + +func (m *serverMetrics) CloseClient() { + m.clientCount.Dec() +} + +func (m *serverMetrics) NewProxy(name string, proxyType string) { + m.proxyCount.WithLabelValues(proxyType).Inc() +} + +func (m *serverMetrics) CloseProxy(name string, proxyType string) { + m.proxyCount.WithLabelValues(proxyType).Dec() +} + +func (m *serverMetrics) OpenConnection(name string, proxyType string) { + m.connectionCount.WithLabelValues(name, proxyType).Inc() +} + +func (m *serverMetrics) CloseConnection(name string, proxyType string) { + m.connectionCount.WithLabelValues(name, proxyType).Dec() +} + +func (m *serverMetrics) AddTrafficIn(name string, proxyType string, trafficBytes int64) { + m.trafficIn.WithLabelValues(name, proxyType).Add(float64(trafficBytes)) +} + +func (m *serverMetrics) AddTrafficOut(name string, proxyType string, trafficBytes int64) { + m.trafficOut.WithLabelValues(name, proxyType).Add(float64(trafficBytes)) +} + +func newServerMetrics() *serverMetrics { + m := &serverMetrics{ + clientCount: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "client_counts", + Help: "The current client counts of frps", + }), + proxyCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "proxy_counts", + Help: "The current proxy counts", + }, []string{"type"}), + connectionCount: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "connection_counts", + Help: "The current connection counts", + }, []string{"name", "type"}), + trafficIn: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "traffic_in", + Help: "The total in traffic", + }, []string{"name", "type"}), + trafficOut: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: serverSubsystem, + Name: "traffic_out", + Help: "The total out traffic", + }, []string{"name", "type"}), + } + prometheus.MustRegister(m.clientCount) + prometheus.MustRegister(m.proxyCount) + prometheus.MustRegister(m.connectionCount) + prometheus.MustRegister(m.trafficIn) + prometheus.MustRegister(m.trafficOut) + return m +} diff --git a/models/msg/msg.go b/models/msg/msg.go index ce41c9ec..b08f1542 100644 --- a/models/msg/msg.go +++ b/models/msg/msg.go @@ -107,6 +107,9 @@ type NewProxy struct { // stcp Sk string `json:"sk"` + + // tcpmux + Multiplexer string `json:"multiplexer"` } type NewProxyResp struct { @@ -120,7 +123,9 @@ type CloseProxy struct { } type NewWorkConn struct { - RunId string `json:"run_id"` + RunId string `json:"run_id"` + PrivilegeKey string `json:"privilege_key"` + Timestamp int64 `json:"timestamp"` } type ReqWorkConn struct { @@ -132,6 +137,7 @@ type StartWorkConn struct { DstAddr string `json:"dst_addr"` SrcPort uint16 `json:"src_port"` DstPort uint16 `json:"dst_port"` + Error string `json:"error"` } type NewVisitorConn struct { @@ -148,9 +154,12 @@ type NewVisitorConnResp struct { } type Ping struct { + PrivilegeKey string `json:"privilege_key"` + Timestamp int64 `json:"timestamp"` } type Pong struct { + Error string `json:"error"` } type UdpPacket struct { diff --git a/models/plugin/server/http.go b/models/plugin/server/http.go index 155c470a..a3c243a0 100644 --- a/models/plugin/server/http.go +++ b/models/plugin/server/http.go @@ -84,6 +84,7 @@ func (p *httpPlugin) do(ctx context.Context, r *Request, res *Response) error { } req = req.WithContext(ctx) req.Header.Set("X-Frp-Reqid", GetReqidFromContext(ctx)) + req.Header.Set("Content-Type", "application/json") resp, err := p.client.Do(req) if err != nil { return err diff --git a/server/control.go b/server/control.go index e5e4901c..f4ed481d 100644 --- a/server/control.go +++ b/server/control.go @@ -23,14 +23,16 @@ import ( "sync" "time" + "github.com/fatedier/frp/models/auth" "github.com/fatedier/frp/models/config" "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/metrics" "github.com/fatedier/frp/server/proxy" - "github.com/fatedier/frp/server/stats" + "github.com/fatedier/frp/utils/util" "github.com/fatedier/frp/utils/version" "github.com/fatedier/frp/utils/xlog" @@ -90,8 +92,8 @@ type Control struct { // plugin manager pluginManager *plugin.Manager - // stats collector to store stats info of clients and proxies - statsCollector stats.Collector + // verifies authentication based on selected method + authVerifier auth.Verifier // login message loginMsg *msg.Login @@ -147,7 +149,7 @@ func NewControl( rc *controller.ResourceController, pxyManager *proxy.ProxyManager, pluginManager *plugin.Manager, - statsCollector stats.Collector, + authVerifier auth.Verifier, ctlConn net.Conn, loginMsg *msg.Login, serverCfg config.ServerCommonConf, @@ -161,7 +163,7 @@ func NewControl( rc: rc, pxyManager: pxyManager, pluginManager: pluginManager, - statsCollector: statsCollector, + authVerifier: authVerifier, conn: ctlConn, loginMsg: loginMsg, sendCh: make(chan msg.Message, 10), @@ -203,7 +205,7 @@ func (ctl *Control) Start() { go ctl.stoper() } -func (ctl *Control) RegisterWorkConn(conn net.Conn) { +func (ctl *Control) RegisterWorkConn(conn net.Conn) error { xl := ctl.xl defer func() { if err := recover(); err != nil { @@ -215,9 +217,10 @@ func (ctl *Control) RegisterWorkConn(conn net.Conn) { select { case ctl.workConnCh <- conn: xl.Debug("new work connection registered") + return nil default: xl.Debug("work connection pool is full, discarding") - conn.Close() + return fmt.Errorf("work connection pool is full, discarding") } } @@ -373,16 +376,12 @@ func (ctl *Control) stoper() { for _, pxy := range ctl.proxies { pxy.Close() ctl.pxyManager.Del(pxy.GetName()) - ctl.statsCollector.Mark(stats.TypeCloseProxy, &stats.CloseProxyPayload{ - Name: pxy.GetName(), - ProxyType: pxy.GetConf().GetBaseInfo().ProxyType, - }) + metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType) } ctl.allShutdown.Done() xl.Info("client exit success") - - ctl.statsCollector.Mark(stats.TypeCloseClient, &stats.CloseClientPayload{}) + metrics.Server.CloseClient() } // block until Control closed @@ -438,21 +437,25 @@ func (ctl *Control) manager() { ProxyName: m.ProxyName, } if err != nil { - resp.Error = err.Error() xl.Warn("new proxy [%s] error: %v", m.ProxyName, err) + resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient) } else { resp.RemoteAddr = remoteAddr xl.Info("new proxy [%s] success", m.ProxyName) - ctl.statsCollector.Mark(stats.TypeNewProxy, &stats.NewProxyPayload{ - Name: m.ProxyName, - ProxyType: m.ProxyType, - }) + metrics.Server.NewProxy(m.ProxyName, m.ProxyType) } ctl.sendCh <- resp case *msg.CloseProxy: ctl.CloseProxy(m) xl.Info("close proxy [%s] success", m.ProxyName) case *msg.Ping: + if err := ctl.authVerifier.VerifyPing(m); err != nil { + xl.Warn("received invalid ping: %v", err) + ctl.sendCh <- &msg.Pong{ + Error: "invalid authentication in ping", + } + return + } ctl.lastPing = time.Now() xl.Debug("receive heartbeat") ctl.sendCh <- &msg.Pong{} @@ -471,7 +474,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err // NewProxy will return a interface Proxy. // In fact it create different proxies by different proxy type, we just call run() here. - pxy, err := proxy.NewProxy(ctl.ctx, ctl.runId, ctl.rc, ctl.statsCollector, ctl.poolCount, ctl.GetWorkConn, pxyConf, ctl.serverCfg) + pxy, err := proxy.NewProxy(ctl.ctx, ctl.runId, ctl.rc, ctl.poolCount, ctl.GetWorkConn, pxyConf, ctl.serverCfg) if err != nil { return remoteAddr, err } @@ -533,9 +536,6 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { delete(ctl.proxies, closeMsg.ProxyName) ctl.mu.Unlock() - ctl.statsCollector.Mark(stats.TypeCloseProxy, &stats.CloseProxyPayload{ - Name: pxy.GetName(), - ProxyType: pxy.GetConf().GetBaseInfo().ProxyType, - }) + metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType) return } diff --git a/server/controller/resource.go b/server/controller/resource.go index 91332b57..5098dfbf 100644 --- a/server/controller/resource.go +++ b/server/controller/resource.go @@ -18,6 +18,7 @@ import ( "github.com/fatedier/frp/models/nathole" "github.com/fatedier/frp/server/group" "github.com/fatedier/frp/server/ports" + "github.com/fatedier/frp/utils/tcpmux" "github.com/fatedier/frp/utils/vhost" ) @@ -46,4 +47,7 @@ type ResourceController struct { // Controller for nat hole connections NatHoleController *nathole.NatHoleController + + // TcpMux HTTP CONNECT multiplexer + TcpMuxHttpConnectMuxer *tcpmux.HttpConnectTcpMuxer } diff --git a/server/dashboard.go b/server/dashboard.go index 0a65dbb2..94a36680 100644 --- a/server/dashboard.go +++ b/server/dashboard.go @@ -24,6 +24,7 @@ import ( frpNet "github.com/fatedier/frp/utils/net" "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( @@ -38,6 +39,11 @@ func (svr *Service) RunDashboardServer(addr string, port int) (err error) { user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd router.Use(frpNet.NewHttpAuthMiddleware(user, passwd).Middleware) + // metrics + if svr.cfg.EnablePrometheus { + router.Handle("/metrics", promhttp.Handler()) + } + // api, see dashboard_api.go router.HandleFunc("/api/serverinfo", svr.ApiServerInfo).Methods("GET") router.HandleFunc("/api/proxy/{type}", svr.ApiProxyByType).Methods("GET") diff --git a/server/dashboard_api.go b/server/dashboard_api.go index 30ee9a68..89d8d8a4 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -20,6 +20,7 @@ import ( "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/consts" + "github.com/fatedier/frp/models/metrics/mem" "github.com/fatedier/frp/utils/log" "github.com/fatedier/frp/utils/version" @@ -62,7 +63,7 @@ func (svr *Service) ApiServerInfo(w http.ResponseWriter, r *http.Request) { }() log.Info("Http request: [%s]", r.URL.Path) - serverStats := svr.statsCollector.GetServer() + serverStats := mem.StatsCollector.GetServer() svrResp := ServerInfoResp{ Version: version.Full(), BindPort: svr.cfg.BindPort, @@ -95,6 +96,12 @@ type TcpOutConf struct { RemotePort int `json:"remote_port"` } +type TcpMuxOutConf struct { + BaseOutConf + config.DomainConf + Multiplexer string `json:"multiplexer"` +} + type UdpOutConf struct { BaseOutConf RemotePort int `json:"remote_port"` @@ -124,6 +131,8 @@ func getConfByType(proxyType string) interface{} { switch proxyType { case consts.TcpProxy: return &TcpOutConf{} + case consts.TcpMuxProxy: + return &TcpMuxOutConf{} case consts.UdpProxy: return &UdpOutConf{} case consts.HttpProxy: @@ -178,7 +187,7 @@ func (svr *Service) ApiProxyByType(w http.ResponseWriter, r *http.Request) { } func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) { - proxyStats := svr.statsCollector.GetProxiesByType(proxyType) + proxyStats := mem.StatsCollector.GetProxiesByType(proxyType) proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats)) for _, ps := range proxyStats { proxyInfo := &ProxyStatsInfo{} @@ -248,7 +257,7 @@ func (svr *Service) ApiProxyByTypeAndName(w http.ResponseWriter, r *http.Request func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) { proxyInfo.Name = proxyName - ps := svr.statsCollector.GetProxiesByTypeAndName(proxyType, proxyName) + ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName) if ps == nil { code = 404 msg = "no proxy info found" @@ -306,7 +315,7 @@ func (svr *Service) ApiProxyTraffic(w http.ResponseWriter, r *http.Request) { trafficResp := GetProxyTrafficResp{} trafficResp.Name = name - proxyTrafficInfo := svr.statsCollector.GetProxyTraffic(name) + proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name) if proxyTrafficInfo == nil { res.Code = 404 diff --git a/server/metrics/metrics.go b/server/metrics/metrics.go new file mode 100644 index 00000000..faf00d14 --- /dev/null +++ b/server/metrics/metrics.go @@ -0,0 +1,37 @@ +package metrics + +import ( + "sync" +) + +type ServerMetrics interface { + NewClient() + CloseClient() + NewProxy(name string, proxyType string) + CloseProxy(name string, proxyType string) + OpenConnection(name string, proxyType string) + CloseConnection(name string, proxyType string) + AddTrafficIn(name string, proxyType string, trafficBytes int64) + AddTrafficOut(name string, proxyType string, trafficBytes int64) +} + +var Server ServerMetrics = noopServerMetrics{} + +var registerMetrics sync.Once + +func Register(m ServerMetrics) { + registerMetrics.Do(func() { + Server = m + }) +} + +type noopServerMetrics struct{} + +func (noopServerMetrics) NewClient() {} +func (noopServerMetrics) CloseClient() {} +func (noopServerMetrics) NewProxy(name string, proxyType string) {} +func (noopServerMetrics) CloseProxy(name string, proxyType string) {} +func (noopServerMetrics) OpenConnection(name string, proxyType string) {} +func (noopServerMetrics) CloseConnection(name string, proxyType string) {} +func (noopServerMetrics) AddTrafficIn(name string, proxyType string, trafficBytes int64) {} +func (noopServerMetrics) AddTrafficOut(name string, proxyType string, trafficBytes int64) {} diff --git a/server/proxy/http.go b/server/proxy/http.go index 09fb3e44..35f65cbd 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -20,7 +20,7 @@ import ( "strings" "github.com/fatedier/frp/models/config" - "github.com/fatedier/frp/server/stats" + "github.com/fatedier/frp/server/metrics" frpNet "github.com/fatedier/frp/utils/net" "github.com/fatedier/frp/utils/util" "github.com/fatedier/frp/utils/vhost" @@ -159,21 +159,16 @@ func (pxy *HttpProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err } workConn = frpNet.WrapReadWriteCloserToConn(rwc, tmpConn) workConn = frpNet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) - pxy.statsCollector.Mark(stats.TypeOpenConnection, &stats.OpenConnectionPayload{ProxyName: pxy.GetName()}) + metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType) return } func (pxy *HttpProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) { name := pxy.GetName() - pxy.statsCollector.Mark(stats.TypeCloseProxy, &stats.CloseConnectionPayload{ProxyName: name}) - pxy.statsCollector.Mark(stats.TypeAddTrafficIn, &stats.AddTrafficInPayload{ - ProxyName: name, - TrafficBytes: totalWrite, - }) - pxy.statsCollector.Mark(stats.TypeAddTrafficOut, &stats.AddTrafficOutPayload{ - ProxyName: name, - TrafficBytes: totalRead, - }) + proxyType := pxy.GetConf().GetBaseInfo().ProxyType + metrics.Server.CloseConnection(name, proxyType) + metrics.Server.AddTrafficIn(name, proxyType, totalWrite) + metrics.Server.AddTrafficOut(name, proxyType, totalRead) } func (pxy *HttpProxy) Close() { diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index 84a04e61..7f86212d 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -25,7 +25,7 @@ import ( "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" "github.com/fatedier/frp/server/controller" - "github.com/fatedier/frp/server/stats" + "github.com/fatedier/frp/server/metrics" frpNet "github.com/fatedier/frp/utils/net" "github.com/fatedier/frp/utils/xlog" @@ -45,14 +45,13 @@ type Proxy interface { } type BaseProxy struct { - name string - rc *controller.ResourceController - statsCollector stats.Collector - listeners []net.Listener - usedPortsNum int - poolCount int - getWorkConnFn GetWorkConnFn - serverCfg config.ServerCommonConf + name string + rc *controller.ResourceController + listeners []net.Listener + usedPortsNum int + poolCount int + getWorkConnFn GetWorkConnFn + serverCfg config.ServerCommonConf mu sync.RWMutex xl *xlog.Logger @@ -116,6 +115,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, SrcPort: uint16(srcPort), DstAddr: dstAddr, DstPort: uint16(dstPort), + Error: "", }) if err != nil { xl.Warn("failed to send message to work connection from pool: %v, times: %d", err, i) @@ -135,7 +135,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, // startListenHandler start a goroutine handler for each listener. // p: p will just be passed to handler(Proxy, frpNet.Conn). // handler: each proxy type can set different handler function to deal with connections accepted from listeners. -func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn, stats.Collector, config.ServerCommonConf)) { +func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn, config.ServerCommonConf)) { xl := xlog.FromContextSafe(pxy.ctx) for _, listener := range pxy.listeners { go func(l net.Listener) { @@ -148,26 +148,25 @@ func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn, return } xl.Debug("get a user connection [%s]", c.RemoteAddr().String()) - go handler(p, c, pxy.statsCollector, pxy.serverCfg) + go handler(p, c, pxy.serverCfg) } }(listener) } } -func NewProxy(ctx context.Context, runId string, rc *controller.ResourceController, statsCollector stats.Collector, poolCount int, +func NewProxy(ctx context.Context, runId string, rc *controller.ResourceController, poolCount int, getWorkConnFn GetWorkConnFn, pxyConf config.ProxyConf, serverCfg config.ServerCommonConf) (pxy Proxy, err error) { xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(pxyConf.GetBaseInfo().ProxyName) basePxy := BaseProxy{ - name: pxyConf.GetBaseInfo().ProxyName, - rc: rc, - statsCollector: statsCollector, - listeners: make([]net.Listener, 0), - poolCount: poolCount, - getWorkConnFn: getWorkConnFn, - serverCfg: serverCfg, - xl: xl, - ctx: xlog.NewContext(ctx, xl), + name: pxyConf.GetBaseInfo().ProxyName, + rc: rc, + listeners: make([]net.Listener, 0), + poolCount: poolCount, + getWorkConnFn: getWorkConnFn, + serverCfg: serverCfg, + xl: xl, + ctx: xlog.NewContext(ctx, xl), } switch cfg := pxyConf.(type) { case *config.TcpProxyConf: @@ -176,6 +175,11 @@ func NewProxy(ctx context.Context, runId string, rc *controller.ResourceControll BaseProxy: &basePxy, cfg: cfg, } + case *config.TcpMuxProxyConf: + pxy = &TcpMuxProxy{ + BaseProxy: &basePxy, + cfg: cfg, + } case *config.HttpProxyConf: pxy = &HttpProxy{ BaseProxy: &basePxy, @@ -210,7 +214,7 @@ func NewProxy(ctx context.Context, runId string, rc *controller.ResourceControll // HandleUserTcpConnection is used for incoming tcp user connections. // It can be used for tcp, http, https type. -func HandleUserTcpConnection(pxy Proxy, userConn net.Conn, statsCollector stats.Collector, serverCfg config.ServerCommonConf) { +func HandleUserTcpConnection(pxy Proxy, userConn net.Conn, serverCfg config.ServerCommonConf) { xl := xlog.FromContextSafe(pxy.Context()) defer userConn.Close() @@ -237,17 +241,13 @@ func HandleUserTcpConnection(pxy Proxy, userConn net.Conn, statsCollector stats. xl.Debug("join connections, workConn(l[%s] r[%s]) userConn(l[%s] r[%s])", workConn.LocalAddr().String(), workConn.RemoteAddr().String(), userConn.LocalAddr().String(), userConn.RemoteAddr().String()) - statsCollector.Mark(stats.TypeOpenConnection, &stats.OpenConnectionPayload{ProxyName: pxy.GetName()}) + name := pxy.GetName() + proxyType := pxy.GetConf().GetBaseInfo().ProxyType + metrics.Server.OpenConnection(name, proxyType) inCount, outCount := frpIo.Join(local, userConn) - statsCollector.Mark(stats.TypeCloseConnection, &stats.CloseConnectionPayload{ProxyName: pxy.GetName()}) - statsCollector.Mark(stats.TypeAddTrafficIn, &stats.AddTrafficInPayload{ - ProxyName: pxy.GetName(), - TrafficBytes: inCount, - }) - statsCollector.Mark(stats.TypeAddTrafficOut, &stats.AddTrafficOutPayload{ - ProxyName: pxy.GetName(), - TrafficBytes: outCount, - }) + metrics.Server.CloseConnection(name, proxyType) + metrics.Server.AddTrafficIn(name, proxyType, inCount) + metrics.Server.AddTrafficOut(name, proxyType, outCount) xl.Debug("join connections closed") } diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go new file mode 100644 index 00000000..108e68d2 --- /dev/null +++ b/server/proxy/tcpmux.go @@ -0,0 +1,95 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 proxy + +import ( + "fmt" + "strings" + + "github.com/fatedier/frp/models/config" + "github.com/fatedier/frp/models/consts" + "github.com/fatedier/frp/utils/util" + "github.com/fatedier/frp/utils/vhost" +) + +type TcpMuxProxy struct { + *BaseProxy + cfg *config.TcpMuxProxyConf + + realPort int +} + +func (pxy *TcpMuxProxy) httpConnectListen(domain string, addrs []string) ([]string, error) { + routeConfig := &vhost.VhostRouteConfig{ + Domain: domain, + } + l, err := pxy.rc.TcpMuxHttpConnectMuxer.Listen(pxy.ctx, routeConfig) + if err != nil { + return nil, err + } + pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s]", routeConfig.Domain) + pxy.listeners = append(pxy.listeners, l) + return append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.TcpMuxHttpConnectPort)), nil +} + +func (pxy *TcpMuxProxy) httpConnectRun() (remoteAddr string, err error) { + addrs := make([]string, 0) + for _, domain := range pxy.cfg.CustomDomains { + if domain == "" { + continue + } + + addrs, err = pxy.httpConnectListen(domain, addrs) + if err != nil { + return "", err + } + } + + if pxy.cfg.SubDomain != "" { + addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, addrs) + if err != nil { + return "", err + } + } + + pxy.startListenHandler(pxy, HandleUserTcpConnection) + remoteAddr = strings.Join(addrs, ",") + return remoteAddr, err +} + +func (pxy *TcpMuxProxy) Run() (remoteAddr string, err error) { + switch pxy.cfg.Multiplexer { + case consts.HttpConnectTcpMultiplexer: + remoteAddr, err = pxy.httpConnectRun() + default: + err = fmt.Errorf("unknown multiplexer [%s]", pxy.cfg.Multiplexer) + } + + if err != nil { + pxy.Close() + } + return remoteAddr, err +} + +func (pxy *TcpMuxProxy) GetConf() config.ProxyConf { + return pxy.cfg +} + +func (pxy *TcpMuxProxy) Close() { + pxy.BaseProxy.Close() + if pxy.cfg.Group == "" { + pxy.rc.TcpPortManager.Release(pxy.realPort) + } +} diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 397f60fb..a0471839 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -23,7 +23,7 @@ import ( "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" "github.com/fatedier/frp/models/proto/udp" - "github.com/fatedier/frp/server/stats" + "github.com/fatedier/frp/server/metrics" "github.com/fatedier/golib/errors" ) @@ -114,10 +114,11 @@ func (pxy *UdpProxy) Run() (remoteAddr string, err error) { if errRet := errors.PanicToError(func() { xl.Trace("get udp message from workConn: %s", m.Content) pxy.readCh <- m - pxy.statsCollector.Mark(stats.TypeAddTrafficOut, &stats.AddTrafficOutPayload{ - ProxyName: pxy.GetName(), - TrafficBytes: int64(len(m.Content)), - }) + metrics.Server.AddTrafficOut( + pxy.GetName(), + pxy.GetConf().GetBaseInfo().ProxyType, + int64(len(m.Content)), + ) }); errRet != nil { conn.Close() xl.Info("reader goroutine for udp work connection closed") @@ -143,10 +144,11 @@ func (pxy *UdpProxy) Run() (remoteAddr string, err error) { return } else { xl.Trace("send message to udp workConn: %s", udpMsg.Content) - pxy.statsCollector.Mark(stats.TypeAddTrafficIn, &stats.AddTrafficInPayload{ - ProxyName: pxy.GetName(), - TrafficBytes: int64(len(udpMsg.Content)), - }) + metrics.Server.AddTrafficIn( + pxy.GetName(), + pxy.GetConf().GetBaseInfo().ProxyType, + int64(len(udpMsg.Content)), + ) continue } case <-ctx.Done(): diff --git a/server/service.go b/server/service.go index 122555af..43870733 100644 --- a/server/service.go +++ b/server/service.go @@ -30,17 +30,20 @@ import ( "time" "github.com/fatedier/frp/assets" + "github.com/fatedier/frp/models/auth" "github.com/fatedier/frp/models/config" + modelmetrics "github.com/fatedier/frp/models/metrics" "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/metrics" "github.com/fatedier/frp/server/ports" "github.com/fatedier/frp/server/proxy" - "github.com/fatedier/frp/server/stats" "github.com/fatedier/frp/utils/log" frpNet "github.com/fatedier/frp/utils/net" + "github.com/fatedier/frp/utils/tcpmux" "github.com/fatedier/frp/utils/util" "github.com/fatedier/frp/utils/version" "github.com/fatedier/frp/utils/vhost" @@ -51,7 +54,8 @@ import ( ) const ( - connReadTimeout time.Duration = 10 * time.Second + connReadTimeout time.Duration = 10 * time.Second + vhostReadWriteTimeout time.Duration = 30 * time.Second ) // Server service @@ -86,8 +90,8 @@ type Service struct { // All resource managers and controllers rc *controller.ResourceController - // stats collector to store server and proxies stats info - statsCollector stats.Collector + // Verifies authentication based on selected method + authVerifier auth.Verifier tlsConfig *tls.Config @@ -105,6 +109,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { UdpPortManager: ports.NewPortManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts), }, httpVhostRouter: vhost.NewVhostRouters(), + authVerifier: auth.NewAuthVerifier(cfg.AuthServerConfig), tlsConfig: generateTLSConfig(), cfg: cfg, } @@ -207,7 +212,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { } } - svr.rc.VhostHttpsMuxer, err = vhost.NewHttpsMuxer(l, 30*time.Second) + svr.rc.VhostHttpsMuxer, err = vhost.NewHttpsMuxer(l, vhostReadWriteTimeout) if err != nil { err = fmt.Errorf("Create vhost httpsMuxer error, %v", err) return @@ -215,6 +220,23 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { log.Info("https service listen on %s:%d", cfg.ProxyBindAddr, cfg.VhostHttpsPort) } + // Create tcpmux httpconnect multiplexer. + if cfg.TcpMuxHttpConnectPort > 0 { + var l net.Listener + l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.ProxyBindAddr, cfg.TcpMuxHttpConnectPort)) + if err != nil { + err = fmt.Errorf("Create server listener error, %v", err) + return + } + + svr.rc.TcpMuxHttpConnectMuxer, err = tcpmux.NewHttpConnectTcpMuxer(l, vhostReadWriteTimeout) + if err != nil { + err = fmt.Errorf("Create vhost tcpMuxer error, %v", err) + return + } + log.Info("tcpmux httpconnect multiplexer listen on %s:%d", cfg.ProxyBindAddr, cfg.TcpMuxHttpConnectPort) + } + // frp tls listener svr.tlsListener = svr.muxer.Listen(1, 1, func(data []byte) bool { return int(data[0]) == frpNet.FRP_TLS_HEAD_BYTE @@ -251,8 +273,12 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { log.Info("Dashboard listen on %s:%d", cfg.DashboardAddr, cfg.DashboardPort) statsEnable = true } - - svr.statsCollector = stats.NewInternalCollector(statsEnable) + if statsEnable { + modelmetrics.EnableMem() + if cfg.EnablePrometheus { + modelmetrics.EnablePrometheus() + } + } return } @@ -284,7 +310,7 @@ func (svr *Service) HandleListener(l net.Listener) { log.Trace("start check TLS connection...") originConn := c - c, err = frpNet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, connReadTimeout) + c, err = frpNet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TlsOnly, connReadTimeout) if err != nil { log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err) originConn.Close() @@ -322,18 +348,20 @@ func (svr *Service) HandleListener(l net.Listener) { xl.Warn("register control error: %v", err) msg.WriteMsg(conn, &msg.LoginResp{ Version: version.Full(), - Error: err.Error(), + Error: util.GenerateResponseErrorString("register control error", err, svr.cfg.DetailedErrorsToClient), }) conn.Close() } case *msg.NewWorkConn: - svr.RegisterWorkConn(conn, m) + if err := svr.RegisterWorkConn(conn, m); err != nil { + conn.Close() + } case *msg.NewVisitorConn: if err = svr.RegisterVisitorConn(conn, m); err != nil { xl.Warn("register visitor conn error: %v", err) msg.WriteMsg(conn, &msg.NewVisitorConnResp{ ProxyName: m.ProxyName, - Error: err.Error(), + Error: util.GenerateResponseErrorString("register visitor conn error", err, svr.cfg.DetailedErrorsToClient), }) conn.Close() } else { @@ -399,13 +427,11 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err } // Check auth. - if util.GetAuthKey(svr.cfg.Token, loginMsg.Timestamp) != loginMsg.PrivilegeKey { - err = fmt.Errorf("authorization failed") + if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil { return } - ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.statsCollector, ctlConn, loginMsg, svr.cfg) - + ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.authVerifier, ctlConn, loginMsg, svr.cfg) if oldCtl := svr.ctlManager.Add(loginMsg.RunId, ctl); oldCtl != nil { oldCtl.allShutdown.WaitDone() } @@ -413,7 +439,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err ctl.Start() // for statistics - svr.statsCollector.Mark(stats.TypeNewClient, &stats.NewClientPayload{}) + metrics.Server.NewClient() go func() { // block until control closed @@ -424,15 +450,22 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err } // RegisterWorkConn register a new work connection to control and proxies need it. -func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) { +func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) error { xl := frpNet.NewLogFromConn(workConn) ctl, exist := svr.ctlManager.GetById(newMsg.RunId) if !exist { xl.Warn("No client control found for run id [%s]", newMsg.RunId) - return + return fmt.Errorf("no client control found for run id [%s]", newMsg.RunId) } - ctl.RegisterWorkConn(workConn) - return + // Check auth. + if err := svr.authVerifier.VerifyNewWorkConn(newMsg); err != nil { + xl.Warn("Invalid authentication in NewWorkConn message on run id [%s]", newMsg.RunId) + msg.WriteMsg(workConn, &msg.StartWorkConn{ + Error: "invalid authentication in NewWorkConn", + }) + return fmt.Errorf("invalid authentication in NewWorkConn message on run id [%s]", newMsg.RunId) + } + return ctl.RegisterWorkConn(workConn) } func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error { diff --git a/server/stats/internal.go b/server/stats/internal.go deleted file mode 100644 index 4b28c2c0..00000000 --- a/server/stats/internal.go +++ /dev/null @@ -1,277 +0,0 @@ -// 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 stats - -import ( - "sync" - "time" - - "github.com/fatedier/frp/utils/log" - "github.com/fatedier/frp/utils/metric" -) - -type internalCollector struct { - enable bool - info *ServerStatistics - mu sync.Mutex -} - -func NewInternalCollector(enable bool) Collector { - return &internalCollector{ - enable: enable, - info: &ServerStatistics{ - TotalTrafficIn: metric.NewDateCounter(ReserveDays), - TotalTrafficOut: metric.NewDateCounter(ReserveDays), - CurConns: metric.NewCounter(), - - ClientCounts: metric.NewCounter(), - ProxyTypeCounts: make(map[string]metric.Counter), - - ProxyStatistics: make(map[string]*ProxyStatistics), - }, - } -} - -func (collector *internalCollector) Run() error { - go func() { - for { - time.Sleep(12 * time.Hour) - log.Debug("start to clear useless proxy statistics data...") - collector.ClearUselessInfo() - log.Debug("finish to clear useless proxy statistics data") - } - }() - return nil -} - -func (collector *internalCollector) ClearUselessInfo() { - // To check if there are proxies that closed than 7 days and drop them. - collector.mu.Lock() - defer collector.mu.Unlock() - for name, data := range collector.info.ProxyStatistics { - if !data.LastCloseTime.IsZero() && time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour { - delete(collector.info.ProxyStatistics, name) - log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String()) - } - } -} - -func (collector *internalCollector) Mark(statsType StatsType, payload interface{}) { - if !collector.enable { - return - } - - switch v := payload.(type) { - case *NewClientPayload: - collector.newClient(v) - case *CloseClientPayload: - collector.closeClient(v) - case *NewProxyPayload: - collector.newProxy(v) - case *CloseProxyPayload: - collector.closeProxy(v) - case *OpenConnectionPayload: - collector.openConnection(v) - case *CloseConnectionPayload: - collector.closeConnection(v) - case *AddTrafficInPayload: - collector.addTrafficIn(v) - case *AddTrafficOutPayload: - collector.addTrafficOut(v) - } -} - -func (collector *internalCollector) newClient(payload *NewClientPayload) { - collector.info.ClientCounts.Inc(1) -} - -func (collector *internalCollector) closeClient(payload *CloseClientPayload) { - collector.info.ClientCounts.Dec(1) -} - -func (collector *internalCollector) newProxy(payload *NewProxyPayload) { - collector.mu.Lock() - defer collector.mu.Unlock() - counter, ok := collector.info.ProxyTypeCounts[payload.ProxyType] - if !ok { - counter = metric.NewCounter() - } - counter.Inc(1) - collector.info.ProxyTypeCounts[payload.ProxyType] = counter - - proxyStats, ok := collector.info.ProxyStatistics[payload.Name] - if !(ok && proxyStats.ProxyType == payload.ProxyType) { - proxyStats = &ProxyStatistics{ - Name: payload.Name, - ProxyType: payload.ProxyType, - CurConns: metric.NewCounter(), - TrafficIn: metric.NewDateCounter(ReserveDays), - TrafficOut: metric.NewDateCounter(ReserveDays), - } - collector.info.ProxyStatistics[payload.Name] = proxyStats - } - proxyStats.LastStartTime = time.Now() -} - -func (collector *internalCollector) closeProxy(payload *CloseProxyPayload) { - collector.mu.Lock() - defer collector.mu.Unlock() - if counter, ok := collector.info.ProxyTypeCounts[payload.ProxyType]; ok { - counter.Dec(1) - } - if proxyStats, ok := collector.info.ProxyStatistics[payload.Name]; ok { - proxyStats.LastCloseTime = time.Now() - } -} - -func (collector *internalCollector) openConnection(payload *OpenConnectionPayload) { - collector.info.CurConns.Inc(1) - - collector.mu.Lock() - defer collector.mu.Unlock() - proxyStats, ok := collector.info.ProxyStatistics[payload.ProxyName] - if ok { - proxyStats.CurConns.Inc(1) - collector.info.ProxyStatistics[payload.ProxyName] = proxyStats - } -} - -func (collector *internalCollector) closeConnection(payload *CloseConnectionPayload) { - collector.info.CurConns.Dec(1) - - collector.mu.Lock() - defer collector.mu.Unlock() - proxyStats, ok := collector.info.ProxyStatistics[payload.ProxyName] - if ok { - proxyStats.CurConns.Dec(1) - collector.info.ProxyStatistics[payload.ProxyName] = proxyStats - } -} - -func (collector *internalCollector) addTrafficIn(payload *AddTrafficInPayload) { - collector.info.TotalTrafficIn.Inc(payload.TrafficBytes) - - collector.mu.Lock() - defer collector.mu.Unlock() - - proxyStats, ok := collector.info.ProxyStatistics[payload.ProxyName] - if ok { - proxyStats.TrafficIn.Inc(payload.TrafficBytes) - collector.info.ProxyStatistics[payload.ProxyName] = proxyStats - } -} - -func (collector *internalCollector) addTrafficOut(payload *AddTrafficOutPayload) { - collector.info.TotalTrafficOut.Inc(payload.TrafficBytes) - - collector.mu.Lock() - defer collector.mu.Unlock() - - proxyStats, ok := collector.info.ProxyStatistics[payload.ProxyName] - if ok { - proxyStats.TrafficOut.Inc(payload.TrafficBytes) - collector.info.ProxyStatistics[payload.ProxyName] = proxyStats - } -} - -func (collector *internalCollector) GetServer() *ServerStats { - collector.mu.Lock() - defer collector.mu.Unlock() - s := &ServerStats{ - TotalTrafficIn: collector.info.TotalTrafficIn.TodayCount(), - TotalTrafficOut: collector.info.TotalTrafficOut.TodayCount(), - CurConns: collector.info.CurConns.Count(), - ClientCounts: collector.info.ClientCounts.Count(), - ProxyTypeCounts: make(map[string]int64), - } - for k, v := range collector.info.ProxyTypeCounts { - s.ProxyTypeCounts[k] = v.Count() - } - return s -} - -func (collector *internalCollector) GetProxiesByType(proxyType string) []*ProxyStats { - res := make([]*ProxyStats, 0) - collector.mu.Lock() - defer collector.mu.Unlock() - - for name, proxyStats := range collector.info.ProxyStatistics { - if proxyStats.ProxyType != proxyType { - continue - } - - ps := &ProxyStats{ - Name: name, - Type: proxyStats.ProxyType, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: proxyStats.CurConns.Count(), - } - if !proxyStats.LastStartTime.IsZero() { - ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } - res = append(res, ps) - } - return res -} - -func (collector *internalCollector) GetProxiesByTypeAndName(proxyType string, proxyName string) (res *ProxyStats) { - collector.mu.Lock() - defer collector.mu.Unlock() - - for name, proxyStats := range collector.info.ProxyStatistics { - if proxyStats.ProxyType != proxyType { - continue - } - - if name != proxyName { - continue - } - - res = &ProxyStats{ - Name: name, - Type: proxyStats.ProxyType, - TodayTrafficIn: proxyStats.TrafficIn.TodayCount(), - TodayTrafficOut: proxyStats.TrafficOut.TodayCount(), - CurConns: proxyStats.CurConns.Count(), - } - if !proxyStats.LastStartTime.IsZero() { - res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") - } - if !proxyStats.LastCloseTime.IsZero() { - res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") - } - break - } - return -} - -func (collector *internalCollector) GetProxyTraffic(name string) (res *ProxyTrafficInfo) { - collector.mu.Lock() - defer collector.mu.Unlock() - - proxyStats, ok := collector.info.ProxyStatistics[name] - if ok { - res = &ProxyTrafficInfo{ - Name: name, - } - res.TrafficIn = proxyStats.TrafficIn.GetLastDaysCount(ReserveDays) - res.TrafficOut = proxyStats.TrafficOut.GetLastDaysCount(ReserveDays) - } - return -} diff --git a/tests/ci/auth_test.go b/tests/ci/auth_test.go new file mode 100644 index 00000000..31074839 --- /dev/null +++ b/tests/ci/auth_test.go @@ -0,0 +1,72 @@ +package ci + +import ( + "os" + "testing" + "time" + + "github.com/fatedier/frp/tests/config" + "github.com/fatedier/frp/tests/consts" + "github.com/fatedier/frp/tests/util" + + "github.com/stretchr/testify/assert" +) + +const FRPS_TOKEN_TCP_CONF = ` +[common] +bind_addr = 0.0.0.0 +bind_port = 20000 +log_file = console +log_level = debug +authentication_method = token +token = 123456 +` + +const FRPC_TOKEN_TCP_CONF = ` +[common] +server_addr = 127.0.0.1 +server_port = 20000 +log_file = console +log_level = debug +authentication_method = token +token = 123456 +protocol = tcp + +[tcp] +type = tcp +local_port = 10701 +remote_port = 20801 +` + +func TestTcpWithTokenAuthentication(t *testing.T) { + assert := assert.New(t) + frpsCfgPath, err := config.GenerateConfigFile(consts.FRPS_NORMAL_CONFIG, FRPS_TOKEN_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpsCfgPath) + } + + frpcCfgPath, err := config.GenerateConfigFile(consts.FRPC_NORMAL_CONFIG, FRPC_TOKEN_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpcCfgPath) + } + + frpsProcess := util.NewProcess(consts.FRPS_BIN_PATH, []string{"-c", frpsCfgPath}) + err = frpsProcess.Start() + if assert.NoError(err) { + defer frpsProcess.Stop() + } + + time.Sleep(200 * time.Millisecond) + + frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) + err = frpcProcess.Start() + if assert.NoError(err) { + defer frpcProcess.Stop() + } + time.Sleep(500 * time.Millisecond) + + // test tcp + res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) +} diff --git a/tests/ci/auto_test_frpc.ini b/tests/ci/auto_test_frpc.ini index b8d90bf2..fbcf971a 100644 --- a/tests/ci/auto_test_frpc.ini +++ b/tests/ci/auto_test_frpc.ini @@ -126,6 +126,13 @@ custom_domains = test6.frp.com host_header_rewrite = test6.frp.com header_X-From-Where = frp +[tcpmuxhttpconnect] +type = tcpmux +multiplexer = httpconnect +local_ip = 127.0.0.1 +local_port = 10701 +custom_domains = tunnel1 + [wildcard_http] type = http local_ip = 127.0.0.1 diff --git a/tests/ci/auto_test_frps.ini b/tests/ci/auto_test_frps.ini index 8948c987..25f7d13d 100644 --- a/tests/ci/auto_test_frps.ini +++ b/tests/ci/auto_test_frps.ini @@ -2,6 +2,7 @@ bind_addr = 0.0.0.0 bind_port = 10700 vhost_http_port = 10804 +tcpmux_httpconnect_port = 10806 log_level = trace token = 123456 allow_ports = 10000-20000,20002,30000-50000 diff --git a/tests/ci/normal_test.go b/tests/ci/normal_test.go index 4f976c81..572fec09 100644 --- a/tests/ci/normal_test.go +++ b/tests/ci/normal_test.go @@ -212,6 +212,17 @@ func TestHttp(t *testing.T) { } } +func TestTcpMux(t *testing.T) { + assert := assert.New(t) + + conn, err := gnet.DialTcpByProxy(fmt.Sprintf("http://%s:%d", "127.0.0.1", consts.TEST_TCP_MUX_FRP_PORT), "tunnel1") + if assert.NoError(err) { + res, err := util.SendTcpMsgByConn(conn, consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) + } +} + func TestWebSocket(t *testing.T) { assert := assert.New(t) diff --git a/tests/ci/tls_test.go b/tests/ci/tls_test.go index d2ad8013..c46e9fa7 100644 --- a/tests/ci/tls_test.go +++ b/tests/ci/tls_test.go @@ -186,3 +186,95 @@ func TestTLSOverWebsocket(t *testing.T) { assert.NoError(err) assert.Equal(consts.TEST_TCP_ECHO_STR, res) } + +const FRPS_TLS_ONLY_TCP_CONF = ` +[common] +bind_addr = 0.0.0.0 +bind_port = 20000 +log_file = console +log_level = debug +token = 123456 +tls_only = true +` + +const FRPC_TLS_ONLY_TCP_CONF = ` +[common] +server_addr = 127.0.0.1 +server_port = 20000 +log_file = console +log_level = debug +token = 123456 +protocol = tcp +tls_enable = true + +[tcp] +type = tcp +local_port = 10701 +remote_port = 20801 +` + +const FRPC_TLS_ONLY_NO_TLS_TCP_CONF = ` +[common] +server_addr = 127.0.0.1 +server_port = 20000 +log_file = console +log_level = debug +token = 123456 +protocol = tcp +tls_enable = false + +[tcp] +type = tcp +local_port = 10701 +remote_port = 20802 +` + +func TestTlsOnlyOverTCP(t *testing.T) { + assert := assert.New(t) + frpsCfgPath, err := config.GenerateConfigFile(consts.FRPS_NORMAL_CONFIG, FRPS_TLS_ONLY_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpsCfgPath) + } + + frpcWithTlsCfgPath, err := config.GenerateConfigFile(consts.FRPC_NORMAL_CONFIG, FRPC_TLS_ONLY_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpcWithTlsCfgPath) + } + + frpsProcess := util.NewProcess(consts.FRPS_BIN_PATH, []string{"-c", frpsCfgPath}) + err = frpsProcess.Start() + if assert.NoError(err) { + defer frpsProcess.Stop() + } + + time.Sleep(200 * time.Millisecond) + + frpcProcessWithTls := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcWithTlsCfgPath}) + err = frpcProcessWithTls.Start() + if assert.NoError(err) { + defer frpcProcessWithTls.Stop() + } + time.Sleep(500 * time.Millisecond) + + // test tcp over tls + res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) + frpcProcessWithTls.Stop() + + frpcWithoutTlsCfgPath, err := config.GenerateConfigFile(consts.FRPC_NORMAL_CONFIG, FRPC_TLS_ONLY_NO_TLS_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpcWithTlsCfgPath) + } + + frpcProcessWithoutTls := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcWithoutTlsCfgPath}) + err = frpcProcessWithoutTls.Start() + if assert.NoError(err) { + defer frpcProcessWithoutTls.Stop() + } + time.Sleep(500 * time.Millisecond) + + // test tcp without tls + _, err = util.SendTcpMsg("127.0.0.1:20802", consts.TEST_TCP_ECHO_STR) + assert.Error(err) +} diff --git a/tests/consts/consts.go b/tests/consts/consts.go index 4e1c1a00..5fc8857d 100644 --- a/tests/consts/consts.go +++ b/tests/consts/consts.go @@ -40,6 +40,8 @@ var ( TEST_HTTP_FOO_STR string = "http foo string: " + TEST_STR TEST_HTTP_BAR_STR string = "http bar string: " + TEST_STR + TEST_TCP_MUX_FRP_PORT int = 10806 + TEST_STCP_FRP_PORT int = 10805 TEST_STCP_EC_FRP_PORT int = 10905 TEST_STCP_ECHO_STR string = "stcp type:" + TEST_STR diff --git a/vendor/github.com/fatedier/golib/errors/errors.go b/utils/metric/metrics.go similarity index 56% rename from vendor/github.com/fatedier/golib/errors/errors.go rename to utils/metric/metrics.go index d818ef36..0fbefc5d 100644 --- a/vendor/github.com/fatedier/golib/errors/errors.go +++ b/utils/metric/metrics.go @@ -1,4 +1,4 @@ -// Copyright 2018 fatedier, fatedier@gmail.com +// Copyright 2020 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. @@ -12,19 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package errors +package metric -import ( - "fmt" -) - -func PanicToError(fn func()) (err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("Panic error: %v", r) - } - }() - - fn() - return +// GaugeMetric represents a single numerical value that can arbitrarily go up +// and down. +type GaugeMetric interface { + Inc() + Dec() + Set(float64) +} + +// CounterMetric represents a single numerical value that only ever +// goes up. +type CounterMetric interface { + Inc() +} + +// HistogramMetric counts individual observations. +type HistogramMetric interface { + Observe(float64) } diff --git a/utils/net/tls.go b/utils/net/tls.go index b9fca317..d3271226 100644 --- a/utils/net/tls.go +++ b/utils/net/tls.go @@ -16,6 +16,7 @@ package net import ( "crypto/tls" + "fmt" "net" "time" @@ -32,7 +33,7 @@ func WrapTLSClientConn(c net.Conn, tlsConfig *tls.Config) (out net.Conn) { return } -func CheckAndEnableTLSServerConnWithTimeout(c net.Conn, tlsConfig *tls.Config, timeout time.Duration) (out net.Conn, err error) { +func CheckAndEnableTLSServerConnWithTimeout(c net.Conn, tlsConfig *tls.Config, tlsOnly bool, timeout time.Duration) (out net.Conn, err error) { sc, r := gnet.NewSharedConnSize(c, 2) buf := make([]byte, 1) var n int @@ -46,6 +47,10 @@ func CheckAndEnableTLSServerConnWithTimeout(c net.Conn, tlsConfig *tls.Config, t if n == 1 && int(buf[0]) == FRP_TLS_HEAD_BYTE { out = tls.Server(c, tlsConfig) } else { + if tlsOnly { + err = fmt.Errorf("non-TLS connection received on a TlsOnly server") + return + } out = sc } return diff --git a/utils/tcpmux/httpconnect.go b/utils/tcpmux/httpconnect.go new file mode 100644 index 00000000..af0a39f9 --- /dev/null +++ b/utils/tcpmux/httpconnect.go @@ -0,0 +1,68 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 tcpmux + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" + "time" + + "github.com/fatedier/frp/utils/util" + "github.com/fatedier/frp/utils/vhost" +) + +type HttpConnectTcpMuxer struct { + *vhost.VhostMuxer +} + +func NewHttpConnectTcpMuxer(listener net.Listener, timeout time.Duration) (*HttpConnectTcpMuxer, error) { + mux, err := vhost.NewVhostMuxer(listener, getHostFromHttpConnect, nil, sendHttpOk, nil, timeout) + return &HttpConnectTcpMuxer{mux}, err +} + +func readHttpConnectRequest(rd io.Reader) (host string, err error) { + bufioReader := bufio.NewReader(rd) + + req, err := http.ReadRequest(bufioReader) + if err != nil { + return + } + + if req.Method != "CONNECT" { + err = fmt.Errorf("connections to tcp vhost must be of method CONNECT") + return + } + + host = util.GetHostFromAddr(req.Host) + return +} + +func sendHttpOk(c net.Conn) error { + return util.OkResponse().Write(c) +} + +func getHostFromHttpConnect(c net.Conn) (_ net.Conn, _ map[string]string, err error) { + reqInfoMap := make(map[string]string, 0) + host, err := readHttpConnectRequest(c) + if err != nil { + return nil, reqInfoMap, err + } + reqInfoMap["Host"] = host + reqInfoMap["Scheme"] = "tcp" + return c, reqInfoMap, nil +} diff --git a/utils/util/http.go b/utils/util/http.go new file mode 100644 index 00000000..bbd3f879 --- /dev/null +++ b/utils/util/http.go @@ -0,0 +1,44 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 util + +import ( + "net/http" + "strings" +) + +func OkResponse() *http.Response { + header := make(http.Header) + + res := &http.Response{ + Status: "OK", + StatusCode: 200, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: header, + } + return res +} + +func GetHostFromAddr(addr string) (host string) { + strs := strings.Split(addr, ":") + if len(strs) > 1 { + host = strs[0] + } else { + host = addr + } + return +} diff --git a/utils/util/util.go b/utils/util/util.go index 7ea4e83c..5b30d543 100644 --- a/utils/util/util.go +++ b/utils/util/util.go @@ -101,3 +101,11 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) { } return } + +func GenerateResponseErrorString(summary string, err error, detailed bool) string { + if detailed { + return err.Error() + } else { + return summary + } +} diff --git a/utils/version/version.go b/utils/version/version.go index f0fe22f3..cdcd0bf0 100644 --- a/utils/version/version.go +++ b/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.31.2" +var version string = "0.32.0" func Full() string { return version diff --git a/utils/vhost/http.go b/utils/vhost/http.go index 7c2d7a3a..1bf6cc30 100644 --- a/utils/vhost/http.go +++ b/utils/vhost/http.go @@ -26,6 +26,7 @@ import ( "time" frpLog "github.com/fatedier/frp/utils/log" + "github.com/fatedier/frp/utils/util" "github.com/fatedier/golib/pool" ) @@ -34,16 +35,6 @@ var ( ErrNoDomain = errors.New("no such domain") ) -func getHostFromAddr(addr string) (host string) { - strs := strings.Split(addr, ":") - if len(strs) > 1 { - host = strs[0] - } else { - host = addr - } - return -} - type HttpReverseProxyOptions struct { ResponseHeaderTimeoutS int64 } @@ -67,7 +58,7 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions, vhostRouter *VhostRoute Director: func(req *http.Request) { req.URL.Scheme = "http" url := req.Context().Value("url").(string) - oldHost := getHostFromAddr(req.Context().Value("host").(string)) + oldHost := util.GetHostFromAddr(req.Context().Value("host").(string)) host := rp.GetRealHost(oldHost, url) if host != "" { req.Host = host @@ -84,7 +75,7 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions, vhostRouter *VhostRoute DisableKeepAlives: true, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { url := ctx.Value("url").(string) - host := getHostFromAddr(ctx.Value("host").(string)) + host := util.GetHostFromAddr(ctx.Value("host").(string)) remote := ctx.Value("remote").(string) return rp.CreateConnection(host, url, remote) }, @@ -183,11 +174,10 @@ func (rp *HttpReverseProxy) getVhost(domain string, location string) (vr *VhostR } domainSplit = domainSplit[1:] } - return } func (rp *HttpReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - domain := getHostFromAddr(req.Host) + domain := util.GetHostFromAddr(req.Host) location := req.URL.Path user, passwd, _ := req.BasicAuth() if !rp.CheckAuth(domain, location, user, passwd) { diff --git a/utils/vhost/https.go b/utils/vhost/https.go index 53177019..41bd01ce 100644 --- a/utils/vhost/https.go +++ b/utils/vhost/https.go @@ -48,7 +48,7 @@ type HttpsMuxer struct { } func NewHttpsMuxer(listener net.Listener, timeout time.Duration) (*HttpsMuxer, error) { - mux, err := NewVhostMuxer(listener, GetHttpsHostname, nil, nil, timeout) + mux, err := NewVhostMuxer(listener, GetHttpsHostname, nil, nil, nil, timeout) return &HttpsMuxer{mux}, err } diff --git a/utils/vhost/vhost.go b/utils/vhost/vhost.go index 57f82394..fec3f525 100644 --- a/utils/vhost/vhost.go +++ b/utils/vhost/vhost.go @@ -29,22 +29,25 @@ import ( type muxFunc func(net.Conn) (net.Conn, map[string]string, error) type httpAuthFunc func(net.Conn, string, string, string) (bool, error) type hostRewriteFunc func(net.Conn, string) (net.Conn, error) +type successFunc func(net.Conn) error type VhostMuxer struct { listener net.Listener timeout time.Duration vhostFunc muxFunc authFunc httpAuthFunc + successFunc successFunc rewriteFunc hostRewriteFunc registryRouter *VhostRouters } -func NewVhostMuxer(listener net.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) { +func NewVhostMuxer(listener net.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, successFunc successFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) { mux = &VhostMuxer{ listener: listener, timeout: timeout, vhostFunc: vhostFunc, authFunc: authFunc, + successFunc: successFunc, rewriteFunc: rewriteFunc, registryRouter: NewVhostRouters(), } @@ -113,7 +116,6 @@ func (v *VhostMuxer) getListener(name, path string) (l *Listener, exist bool) { } domainSplit = domainSplit[1:] } - return } func (v *VhostMuxer) run() { @@ -149,7 +151,15 @@ func (v *VhostMuxer) handle(c net.Conn) { c.Close() return } + xl := xlog.FromContextSafe(l.ctx) + if v.successFunc != nil { + if err := v.successFunc(c); err != nil { + xl.Info("success func failure on vhost connection: %v", err) + c.Close() + return + } + } // if authFunc is exist and userName/password is set // then verify user access diff --git a/vendor/github.com/armon/go-socks5/.gitignore b/vendor/github.com/armon/go-socks5/.gitignore deleted file mode 100644 index 00268614..00000000 --- a/vendor/github.com/armon/go-socks5/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe diff --git a/vendor/github.com/armon/go-socks5/.travis.yml b/vendor/github.com/armon/go-socks5/.travis.yml deleted file mode 100644 index 8d61700e..00000000 --- a/vendor/github.com/armon/go-socks5/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: go -go: - - 1.1 - - tip diff --git a/vendor/github.com/armon/go-socks5/LICENSE b/vendor/github.com/armon/go-socks5/LICENSE deleted file mode 100644 index a5df10e6..00000000 --- a/vendor/github.com/armon/go-socks5/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Armon Dadgar - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/armon/go-socks5/README.md b/vendor/github.com/armon/go-socks5/README.md deleted file mode 100644 index 9cd15635..00000000 --- a/vendor/github.com/armon/go-socks5/README.md +++ /dev/null @@ -1,45 +0,0 @@ -go-socks5 [![Build Status](https://travis-ci.org/armon/go-socks5.png)](https://travis-ci.org/armon/go-socks5) -========= - -Provides the `socks5` package that implements a [SOCKS5 server](http://en.wikipedia.org/wiki/SOCKS). -SOCKS (Secure Sockets) is used to route traffic between a client and server through -an intermediate proxy layer. This can be used to bypass firewalls or NATs. - -Feature -======= - -The package has the following features: -* "No Auth" mode -* User/Password authentication -* Support for the CONNECT command -* Rules to do granular filtering of commands -* Custom DNS resolution -* Unit tests - -TODO -==== - -The package still needs the following: -* Support for the BIND command -* Support for the ASSOCIATE command - - -Example -======= - -Below is a simple example of usage - -```go -// Create a SOCKS5 server -conf := &socks5.Config{} -server, err := socks5.New(conf) -if err != nil { - panic(err) -} - -// Create SOCKS5 proxy on localhost port 8000 -if err := server.ListenAndServe("tcp", "127.0.0.1:8000"); err != nil { - panic(err) -} -``` - diff --git a/vendor/github.com/armon/go-socks5/auth.go b/vendor/github.com/armon/go-socks5/auth.go deleted file mode 100644 index 7811e2aa..00000000 --- a/vendor/github.com/armon/go-socks5/auth.go +++ /dev/null @@ -1,151 +0,0 @@ -package socks5 - -import ( - "fmt" - "io" -) - -const ( - NoAuth = uint8(0) - noAcceptable = uint8(255) - UserPassAuth = uint8(2) - userAuthVersion = uint8(1) - authSuccess = uint8(0) - authFailure = uint8(1) -) - -var ( - UserAuthFailed = fmt.Errorf("User authentication failed") - NoSupportedAuth = fmt.Errorf("No supported authentication mechanism") -) - -// A Request encapsulates authentication state provided -// during negotiation -type AuthContext struct { - // Provided auth method - Method uint8 - // Payload provided during negotiation. - // Keys depend on the used auth method. - // For UserPassauth contains Username - Payload map[string]string -} - -type Authenticator interface { - Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) - GetCode() uint8 -} - -// NoAuthAuthenticator is used to handle the "No Authentication" mode -type NoAuthAuthenticator struct{} - -func (a NoAuthAuthenticator) GetCode() uint8 { - return NoAuth -} - -func (a NoAuthAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) { - _, err := writer.Write([]byte{socks5Version, NoAuth}) - return &AuthContext{NoAuth, nil}, err -} - -// UserPassAuthenticator is used to handle username/password based -// authentication -type UserPassAuthenticator struct { - Credentials CredentialStore -} - -func (a UserPassAuthenticator) GetCode() uint8 { - return UserPassAuth -} - -func (a UserPassAuthenticator) Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error) { - // Tell the client to use user/pass auth - if _, err := writer.Write([]byte{socks5Version, UserPassAuth}); err != nil { - return nil, err - } - - // Get the version and username length - header := []byte{0, 0} - if _, err := io.ReadAtLeast(reader, header, 2); err != nil { - return nil, err - } - - // Ensure we are compatible - if header[0] != userAuthVersion { - return nil, fmt.Errorf("Unsupported auth version: %v", header[0]) - } - - // Get the user name - userLen := int(header[1]) - user := make([]byte, userLen) - if _, err := io.ReadAtLeast(reader, user, userLen); err != nil { - return nil, err - } - - // Get the password length - if _, err := reader.Read(header[:1]); err != nil { - return nil, err - } - - // Get the password - passLen := int(header[0]) - pass := make([]byte, passLen) - if _, err := io.ReadAtLeast(reader, pass, passLen); err != nil { - return nil, err - } - - // Verify the password - if a.Credentials.Valid(string(user), string(pass)) { - if _, err := writer.Write([]byte{userAuthVersion, authSuccess}); err != nil { - return nil, err - } - } else { - if _, err := writer.Write([]byte{userAuthVersion, authFailure}); err != nil { - return nil, err - } - return nil, UserAuthFailed - } - - // Done - return &AuthContext{UserPassAuth, map[string]string{"Username": string(user)}}, nil -} - -// authenticate is used to handle connection authentication -func (s *Server) authenticate(conn io.Writer, bufConn io.Reader) (*AuthContext, error) { - // Get the methods - methods, err := readMethods(bufConn) - if err != nil { - return nil, fmt.Errorf("Failed to get auth methods: %v", err) - } - - // Select a usable method - for _, method := range methods { - cator, found := s.authMethods[method] - if found { - return cator.Authenticate(bufConn, conn) - } - } - - // No usable method found - return nil, noAcceptableAuth(conn) -} - -// noAcceptableAuth is used to handle when we have no eligible -// authentication mechanism -func noAcceptableAuth(conn io.Writer) error { - conn.Write([]byte{socks5Version, noAcceptable}) - return NoSupportedAuth -} - -// readMethods is used to read the number of methods -// and proceeding auth methods -func readMethods(r io.Reader) ([]byte, error) { - header := []byte{0} - if _, err := r.Read(header); err != nil { - return nil, err - } - - numMethods := int(header[0]) - methods := make([]byte, numMethods) - _, err := io.ReadAtLeast(r, methods, numMethods) - return methods, err -} diff --git a/vendor/github.com/armon/go-socks5/credentials.go b/vendor/github.com/armon/go-socks5/credentials.go deleted file mode 100644 index 96664273..00000000 --- a/vendor/github.com/armon/go-socks5/credentials.go +++ /dev/null @@ -1,17 +0,0 @@ -package socks5 - -// CredentialStore is used to support user/pass authentication -type CredentialStore interface { - Valid(user, password string) bool -} - -// StaticCredentials enables using a map directly as a credential store -type StaticCredentials map[string]string - -func (s StaticCredentials) Valid(user, password string) bool { - pass, ok := s[user] - if !ok { - return false - } - return password == pass -} diff --git a/vendor/github.com/armon/go-socks5/request.go b/vendor/github.com/armon/go-socks5/request.go deleted file mode 100644 index b615fcbe..00000000 --- a/vendor/github.com/armon/go-socks5/request.go +++ /dev/null @@ -1,364 +0,0 @@ -package socks5 - -import ( - "fmt" - "io" - "net" - "strconv" - "strings" - - "golang.org/x/net/context" -) - -const ( - ConnectCommand = uint8(1) - BindCommand = uint8(2) - AssociateCommand = uint8(3) - ipv4Address = uint8(1) - fqdnAddress = uint8(3) - ipv6Address = uint8(4) -) - -const ( - successReply uint8 = iota - serverFailure - ruleFailure - networkUnreachable - hostUnreachable - connectionRefused - ttlExpired - commandNotSupported - addrTypeNotSupported -) - -var ( - unrecognizedAddrType = fmt.Errorf("Unrecognized address type") -) - -// AddressRewriter is used to rewrite a destination transparently -type AddressRewriter interface { - Rewrite(ctx context.Context, request *Request) (context.Context, *AddrSpec) -} - -// AddrSpec is used to return the target AddrSpec -// which may be specified as IPv4, IPv6, or a FQDN -type AddrSpec struct { - FQDN string - IP net.IP - Port int -} - -func (a *AddrSpec) String() string { - if a.FQDN != "" { - return fmt.Sprintf("%s (%s):%d", a.FQDN, a.IP, a.Port) - } - return fmt.Sprintf("%s:%d", a.IP, a.Port) -} - -// Address returns a string suitable to dial; prefer returning IP-based -// address, fallback to FQDN -func (a AddrSpec) Address() string { - if 0 != len(a.IP) { - return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port)) - } - return net.JoinHostPort(a.FQDN, strconv.Itoa(a.Port)) -} - -// A Request represents request received by a server -type Request struct { - // Protocol version - Version uint8 - // Requested command - Command uint8 - // AuthContext provided during negotiation - AuthContext *AuthContext - // AddrSpec of the the network that sent the request - RemoteAddr *AddrSpec - // AddrSpec of the desired destination - DestAddr *AddrSpec - // AddrSpec of the actual destination (might be affected by rewrite) - realDestAddr *AddrSpec - bufConn io.Reader -} - -type conn interface { - Write([]byte) (int, error) - RemoteAddr() net.Addr -} - -// NewRequest creates a new Request from the tcp connection -func NewRequest(bufConn io.Reader) (*Request, error) { - // Read the version byte - header := []byte{0, 0, 0} - if _, err := io.ReadAtLeast(bufConn, header, 3); err != nil { - return nil, fmt.Errorf("Failed to get command version: %v", err) - } - - // Ensure we are compatible - if header[0] != socks5Version { - return nil, fmt.Errorf("Unsupported command version: %v", header[0]) - } - - // Read in the destination address - dest, err := readAddrSpec(bufConn) - if err != nil { - return nil, err - } - - request := &Request{ - Version: socks5Version, - Command: header[1], - DestAddr: dest, - bufConn: bufConn, - } - - return request, nil -} - -// handleRequest is used for request processing after authentication -func (s *Server) handleRequest(req *Request, conn conn) error { - ctx := context.Background() - - // Resolve the address if we have a FQDN - dest := req.DestAddr - if dest.FQDN != "" { - ctx_, addr, err := s.config.Resolver.Resolve(ctx, dest.FQDN) - if err != nil { - if err := sendReply(conn, hostUnreachable, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return fmt.Errorf("Failed to resolve destination '%v': %v", dest.FQDN, err) - } - ctx = ctx_ - dest.IP = addr - } - - // Apply any address rewrites - req.realDestAddr = req.DestAddr - if s.config.Rewriter != nil { - ctx, req.realDestAddr = s.config.Rewriter.Rewrite(ctx, req) - } - - // Switch on the command - switch req.Command { - case ConnectCommand: - return s.handleConnect(ctx, conn, req) - case BindCommand: - return s.handleBind(ctx, conn, req) - case AssociateCommand: - return s.handleAssociate(ctx, conn, req) - default: - if err := sendReply(conn, commandNotSupported, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return fmt.Errorf("Unsupported command: %v", req.Command) - } -} - -// handleConnect is used to handle a connect command -func (s *Server) handleConnect(ctx context.Context, conn conn, req *Request) error { - // Check if this is allowed - if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { - if err := sendReply(conn, ruleFailure, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return fmt.Errorf("Connect to %v blocked by rules", req.DestAddr) - } else { - ctx = ctx_ - } - - // Attempt to connect - dial := s.config.Dial - if dial == nil { - dial = func(ctx context.Context, net_, addr string) (net.Conn, error) { - return net.Dial(net_, addr) - } - } - target, err := dial(ctx, "tcp", req.realDestAddr.Address()) - if err != nil { - msg := err.Error() - resp := hostUnreachable - if strings.Contains(msg, "refused") { - resp = connectionRefused - } else if strings.Contains(msg, "network is unreachable") { - resp = networkUnreachable - } - if err := sendReply(conn, resp, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return fmt.Errorf("Connect to %v failed: %v", req.DestAddr, err) - } - defer target.Close() - - // Send success - local := target.LocalAddr().(*net.TCPAddr) - bind := AddrSpec{IP: local.IP, Port: local.Port} - if err := sendReply(conn, successReply, &bind); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - - // Start proxying - errCh := make(chan error, 2) - go proxy(target, req.bufConn, errCh) - go proxy(conn, target, errCh) - - // Wait - for i := 0; i < 2; i++ { - e := <-errCh - if e != nil { - // return from this function closes target (and conn). - return e - } - } - return nil -} - -// handleBind is used to handle a connect command -func (s *Server) handleBind(ctx context.Context, conn conn, req *Request) error { - // Check if this is allowed - if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { - if err := sendReply(conn, ruleFailure, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return fmt.Errorf("Bind to %v blocked by rules", req.DestAddr) - } else { - ctx = ctx_ - } - - // TODO: Support bind - if err := sendReply(conn, commandNotSupported, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return nil -} - -// handleAssociate is used to handle a connect command -func (s *Server) handleAssociate(ctx context.Context, conn conn, req *Request) error { - // Check if this is allowed - if ctx_, ok := s.config.Rules.Allow(ctx, req); !ok { - if err := sendReply(conn, ruleFailure, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return fmt.Errorf("Associate to %v blocked by rules", req.DestAddr) - } else { - ctx = ctx_ - } - - // TODO: Support associate - if err := sendReply(conn, commandNotSupported, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - return nil -} - -// readAddrSpec is used to read AddrSpec. -// Expects an address type byte, follwed by the address and port -func readAddrSpec(r io.Reader) (*AddrSpec, error) { - d := &AddrSpec{} - - // Get the address type - addrType := []byte{0} - if _, err := r.Read(addrType); err != nil { - return nil, err - } - - // Handle on a per type basis - switch addrType[0] { - case ipv4Address: - addr := make([]byte, 4) - if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil { - return nil, err - } - d.IP = net.IP(addr) - - case ipv6Address: - addr := make([]byte, 16) - if _, err := io.ReadAtLeast(r, addr, len(addr)); err != nil { - return nil, err - } - d.IP = net.IP(addr) - - case fqdnAddress: - if _, err := r.Read(addrType); err != nil { - return nil, err - } - addrLen := int(addrType[0]) - fqdn := make([]byte, addrLen) - if _, err := io.ReadAtLeast(r, fqdn, addrLen); err != nil { - return nil, err - } - d.FQDN = string(fqdn) - - default: - return nil, unrecognizedAddrType - } - - // Read the port - port := []byte{0, 0} - if _, err := io.ReadAtLeast(r, port, 2); err != nil { - return nil, err - } - d.Port = (int(port[0]) << 8) | int(port[1]) - - return d, nil -} - -// sendReply is used to send a reply message -func sendReply(w io.Writer, resp uint8, addr *AddrSpec) error { - // Format the address - var addrType uint8 - var addrBody []byte - var addrPort uint16 - switch { - case addr == nil: - addrType = ipv4Address - addrBody = []byte{0, 0, 0, 0} - addrPort = 0 - - case addr.FQDN != "": - addrType = fqdnAddress - addrBody = append([]byte{byte(len(addr.FQDN))}, addr.FQDN...) - addrPort = uint16(addr.Port) - - case addr.IP.To4() != nil: - addrType = ipv4Address - addrBody = []byte(addr.IP.To4()) - addrPort = uint16(addr.Port) - - case addr.IP.To16() != nil: - addrType = ipv6Address - addrBody = []byte(addr.IP.To16()) - addrPort = uint16(addr.Port) - - default: - return fmt.Errorf("Failed to format address: %v", addr) - } - - // Format the message - msg := make([]byte, 6+len(addrBody)) - msg[0] = socks5Version - msg[1] = resp - msg[2] = 0 // Reserved - msg[3] = addrType - copy(msg[4:], addrBody) - msg[4+len(addrBody)] = byte(addrPort >> 8) - msg[4+len(addrBody)+1] = byte(addrPort & 0xff) - - // Send the message - _, err := w.Write(msg) - return err -} - -type closeWriter interface { - CloseWrite() error -} - -// proxy is used to suffle data from src to destination, and sends errors -// down a dedicated channel -func proxy(dst io.Writer, src io.Reader, errCh chan error) { - _, err := io.Copy(dst, src) - if tcpConn, ok := dst.(closeWriter); ok { - tcpConn.CloseWrite() - } - errCh <- err -} diff --git a/vendor/github.com/armon/go-socks5/resolver.go b/vendor/github.com/armon/go-socks5/resolver.go deleted file mode 100644 index b75a5c4d..00000000 --- a/vendor/github.com/armon/go-socks5/resolver.go +++ /dev/null @@ -1,23 +0,0 @@ -package socks5 - -import ( - "net" - - "golang.org/x/net/context" -) - -// NameResolver is used to implement custom name resolution -type NameResolver interface { - Resolve(ctx context.Context, name string) (context.Context, net.IP, error) -} - -// DNSResolver uses the system DNS to resolve host names -type DNSResolver struct{} - -func (d DNSResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { - addr, err := net.ResolveIPAddr("ip", name) - if err != nil { - return ctx, nil, err - } - return ctx, addr.IP, err -} diff --git a/vendor/github.com/armon/go-socks5/ruleset.go b/vendor/github.com/armon/go-socks5/ruleset.go deleted file mode 100644 index ba0e3538..00000000 --- a/vendor/github.com/armon/go-socks5/ruleset.go +++ /dev/null @@ -1,41 +0,0 @@ -package socks5 - -import ( - "golang.org/x/net/context" -) - -// RuleSet is used to provide custom rules to allow or prohibit actions -type RuleSet interface { - Allow(ctx context.Context, req *Request) (context.Context, bool) -} - -// PermitAll returns a RuleSet which allows all types of connections -func PermitAll() RuleSet { - return &PermitCommand{true, true, true} -} - -// PermitNone returns a RuleSet which disallows all types of connections -func PermitNone() RuleSet { - return &PermitCommand{false, false, false} -} - -// PermitCommand is an implementation of the RuleSet which -// enables filtering supported commands -type PermitCommand struct { - EnableConnect bool - EnableBind bool - EnableAssociate bool -} - -func (p *PermitCommand) Allow(ctx context.Context, req *Request) (context.Context, bool) { - switch req.Command { - case ConnectCommand: - return ctx, p.EnableConnect - case BindCommand: - return ctx, p.EnableBind - case AssociateCommand: - return ctx, p.EnableAssociate - } - - return ctx, false -} diff --git a/vendor/github.com/armon/go-socks5/socks5.go b/vendor/github.com/armon/go-socks5/socks5.go deleted file mode 100644 index a17be68f..00000000 --- a/vendor/github.com/armon/go-socks5/socks5.go +++ /dev/null @@ -1,169 +0,0 @@ -package socks5 - -import ( - "bufio" - "fmt" - "log" - "net" - "os" - - "golang.org/x/net/context" -) - -const ( - socks5Version = uint8(5) -) - -// Config is used to setup and configure a Server -type Config struct { - // AuthMethods can be provided to implement custom authentication - // By default, "auth-less" mode is enabled. - // For password-based auth use UserPassAuthenticator. - AuthMethods []Authenticator - - // If provided, username/password authentication is enabled, - // by appending a UserPassAuthenticator to AuthMethods. If not provided, - // and AUthMethods is nil, then "auth-less" mode is enabled. - Credentials CredentialStore - - // Resolver can be provided to do custom name resolution. - // Defaults to DNSResolver if not provided. - Resolver NameResolver - - // Rules is provided to enable custom logic around permitting - // various commands. If not provided, PermitAll is used. - Rules RuleSet - - // Rewriter can be used to transparently rewrite addresses. - // This is invoked before the RuleSet is invoked. - // Defaults to NoRewrite. - Rewriter AddressRewriter - - // BindIP is used for bind or udp associate - BindIP net.IP - - // Logger can be used to provide a custom log target. - // Defaults to stdout. - Logger *log.Logger - - // Optional function for dialing out - Dial func(ctx context.Context, network, addr string) (net.Conn, error) -} - -// Server is reponsible for accepting connections and handling -// the details of the SOCKS5 protocol -type Server struct { - config *Config - authMethods map[uint8]Authenticator -} - -// New creates a new Server and potentially returns an error -func New(conf *Config) (*Server, error) { - // Ensure we have at least one authentication method enabled - if len(conf.AuthMethods) == 0 { - if conf.Credentials != nil { - conf.AuthMethods = []Authenticator{&UserPassAuthenticator{conf.Credentials}} - } else { - conf.AuthMethods = []Authenticator{&NoAuthAuthenticator{}} - } - } - - // Ensure we have a DNS resolver - if conf.Resolver == nil { - conf.Resolver = DNSResolver{} - } - - // Ensure we have a rule set - if conf.Rules == nil { - conf.Rules = PermitAll() - } - - // Ensure we have a log target - if conf.Logger == nil { - conf.Logger = log.New(os.Stdout, "", log.LstdFlags) - } - - server := &Server{ - config: conf, - } - - server.authMethods = make(map[uint8]Authenticator) - - for _, a := range conf.AuthMethods { - server.authMethods[a.GetCode()] = a - } - - return server, nil -} - -// ListenAndServe is used to create a listener and serve on it -func (s *Server) ListenAndServe(network, addr string) error { - l, err := net.Listen(network, addr) - if err != nil { - return err - } - return s.Serve(l) -} - -// Serve is used to serve connections from a listener -func (s *Server) Serve(l net.Listener) error { - for { - conn, err := l.Accept() - if err != nil { - return err - } - go s.ServeConn(conn) - } - return nil -} - -// ServeConn is used to serve a single connection. -func (s *Server) ServeConn(conn net.Conn) error { - defer conn.Close() - bufConn := bufio.NewReader(conn) - - // Read the version byte - version := []byte{0} - if _, err := bufConn.Read(version); err != nil { - s.config.Logger.Printf("[ERR] socks: Failed to get version byte: %v", err) - return err - } - - // Ensure we are compatible - if version[0] != socks5Version { - err := fmt.Errorf("Unsupported SOCKS version: %v", version) - s.config.Logger.Printf("[ERR] socks: %v", err) - return err - } - - // Authenticate the connection - authContext, err := s.authenticate(conn, bufConn) - if err != nil { - err = fmt.Errorf("Failed to authenticate: %v", err) - s.config.Logger.Printf("[ERR] socks: %v", err) - return err - } - - request, err := NewRequest(bufConn) - if err != nil { - if err == unrecognizedAddrType { - if err := sendReply(conn, addrTypeNotSupported, nil); err != nil { - return fmt.Errorf("Failed to send reply: %v", err) - } - } - return fmt.Errorf("Failed to read destination address: %v", err) - } - request.AuthContext = authContext - if client, ok := conn.RemoteAddr().(*net.TCPAddr); ok { - request.RemoteAddr = &AddrSpec{IP: client.IP, Port: client.Port} - } - - // Process the client request - if err := s.handleRequest(request, conn); err != nil { - err = fmt.Errorf("Failed to handle request: %v", err) - s.config.Logger.Printf("[ERR] socks: %v", err) - return err - } - - return nil -} diff --git a/vendor/github.com/coreos/go-oidc/.gitignore b/vendor/github.com/coreos/go-oidc/.gitignore new file mode 100644 index 00000000..c96f2f47 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/.gitignore @@ -0,0 +1,2 @@ +/bin +/gopath diff --git a/vendor/github.com/coreos/go-oidc/.travis.yml b/vendor/github.com/coreos/go-oidc/.travis.yml new file mode 100644 index 00000000..3fddaaac --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/.travis.yml @@ -0,0 +1,16 @@ +language: go + +go: + - "1.12" + - "1.13" + +install: + - go get -v -t github.com/coreos/go-oidc/... + - go get golang.org/x/tools/cmd/cover + - go get golang.org/x/lint/golint + +script: + - ./test + +notifications: + email: false diff --git a/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md b/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md new file mode 100644 index 00000000..6662073a --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# How to Contribute + +CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via +GitHub pull requests. This document outlines some of the conventions on +development workflow, commit message formatting, contact points and other +resources to make it easier to get your contribution accepted. + +# Certificate of Origin + +By contributing to this project you agree to the Developer Certificate of +Origin (DCO). This document was created by the Linux Kernel community and is a +simple statement that you, as a contributor, have the legal right to make the +contribution. See the [DCO](DCO) file for details. + +# Email and Chat + +The project currently uses the general CoreOS email list and IRC channel: +- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev) +- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org + +Please avoid emailing maintainers found in the MAINTAINERS file directly. They +are very busy and read the mailing lists. + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.md) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +