From bcaf51a6ad8e21d0fd7b44ba356b8681bfbfbc77 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=96=B9=E7=A7=8B=E9=B8=A3?=
Date: Wed, 21 Jun 2017 14:46:24 +0800
Subject: [PATCH 01/19] Fix dockerfile
Fix incorrect filenames
---
Dockerfile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 2f220887..d1ed20ba 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,8 +6,8 @@ RUN cd /go/src/github.com/fatedier/frp \
&& make \
&& mv bin/frpc /frpc \
&& mv bin/frps /frps \
- && mv conf/frpc_min.ini /frpc.ini \
- && mv conf/frps_min.ini /frps.ini \
+ && mv conf/frpc.ini /frpc.ini \
+ && mv conf/frps.ini /frps.ini \
&& make clean
WORKDIR /
From 9f736558e26feac6d5c6bac1dc4b2eac8d94627f Mon Sep 17 00:00:00 2001
From: Iansmith's win10
Date: Sat, 24 Jun 2017 12:17:09 +0800
Subject: [PATCH 02/19] fix backquote
---
README_zh.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README_zh.md b/README_zh.md
index 504d2da1..be8187a2 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -488,7 +488,7 @@ http_proxy = http://user:pwd@192.168.1.128:8080
插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 **unix_domain_socket**、**http_proxy**。具体使用方式请查看[使用示例](#使用示例)。
-通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port 不再需要配置。
+通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port` 不再需要配置。
使用 **http_proxy** 插件的示例:
From 171bc8dd223da4b28db7a3d237c148f4c4d8c69e Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 26 Jun 2017 03:02:33 +0800
Subject: [PATCH 03/19] new proxy type: stcp(secret tcp)
---
client/control.go | 98 +++++++++++++++++----------
client/proxy.go | 35 +++++++++-
client/service.go | 4 +-
client/vistor.go | 145 ++++++++++++++++++++++++++++++++++++++++
cmd/frpc/main.go | 4 +-
conf/frpc_full.ini | 21 ++++++
models/config/proxy.go | 100 +++++++++++++++++++++++++--
models/consts/consts.go | 1 +
models/msg/msg.go | 42 +++++++++---
server/manager.go | 74 ++++++++++++++++++++
server/proxy.go | 32 +++++++++
server/service.go | 30 +++++++--
utils/log/log.go | 5 ++
utils/net/listener.go | 53 +++++++++++++++
14 files changed, 581 insertions(+), 63 deletions(-)
create mode 100644 client/vistor.go
diff --git a/client/control.go b/client/control.go
index c7020580..45f617b3 100644
--- a/client/control.go
+++ b/client/control.go
@@ -25,7 +25,7 @@ import (
"github.com/fatedier/frp/models/msg"
"github.com/fatedier/frp/utils/crypto"
"github.com/fatedier/frp/utils/log"
- "github.com/fatedier/frp/utils/net"
+ frpNet "github.com/fatedier/frp/utils/net"
"github.com/fatedier/frp/utils/util"
"github.com/fatedier/frp/utils/version"
"github.com/xtaci/smux"
@@ -48,8 +48,14 @@ type Control struct {
// proxies
proxies map[string]Proxy
+ // vistor configures
+ vistorCfgs map[string]config.ProxyConf
+
+ // vistors
+ vistors map[string]Vistor
+
// control connection
- conn net.Conn
+ conn frpNet.Conn
// tcp stream multiplexing, if enabled
session *smux.Session
@@ -77,7 +83,7 @@ type Control struct {
log.Logger
}
-func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf) *Control {
+func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf, vistorCfgs map[string]config.ProxyConf) *Control {
loginMsg := &msg.Login{
Arch: runtime.GOARCH,
Os: runtime.GOOS,
@@ -86,14 +92,16 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf) *Control {
Version: version.Full(),
}
return &Control{
- svr: svr,
- loginMsg: loginMsg,
- pxyCfgs: pxyCfgs,
- proxies: make(map[string]Proxy),
- sendCh: make(chan msg.Message, 10),
- readCh: make(chan msg.Message, 10),
- closedCh: make(chan int),
- Logger: log.NewPrefixLogger(""),
+ svr: svr,
+ loginMsg: loginMsg,
+ pxyCfgs: pxyCfgs,
+ vistorCfgs: vistorCfgs,
+ proxies: make(map[string]Proxy),
+ vistors: make(map[string]Vistor),
+ sendCh: make(chan msg.Message, 10),
+ readCh: make(chan msg.Message, 10),
+ closedCh: make(chan int),
+ Logger: log.NewPrefixLogger(""),
}
}
@@ -105,16 +113,17 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf) *Control {
// 6. In controler(): ini readCh, sendCh, closedCh
// 7. In controler(): start new reader(), writer(), manager()
// controler() will keep running
-func (ctl *Control) Run() error {
+func (ctl *Control) Run() (err error) {
for {
- err := ctl.login()
+ err = ctl.login()
if err != nil {
+ ctl.Warn("login to server failed: %v", err)
+
// if login_fail_exit is true, just exit this program
// otherwise sleep a while and continues relogin to server
if config.ClientCommonCfg.LoginFailExit {
- return err
+ return
} else {
- ctl.Warn("login to server fail: %v", err)
time.Sleep(30 * time.Second)
}
} else {
@@ -133,29 +142,25 @@ func (ctl *Control) Run() error {
cfg.UnMarshalToMsg(&newProxyMsg)
ctl.sendCh <- &newProxyMsg
}
- return nil
-}
-func (ctl *Control) NewWorkConn() {
- var (
- workConn net.Conn
- err error
- )
- if config.ClientCommonCfg.TcpMux {
- stream, err := ctl.session.OpenStream()
+ // start all local vistors
+ for _, cfg := range ctl.vistorCfgs {
+ vistor := NewVistor(ctl, cfg)
+ err = vistor.Run()
if err != nil {
- ctl.Warn("start new work connection error: %v", err)
- return
+ vistor.Warn("start error: %v", err)
+ continue
}
- workConn = net.WrapConn(stream)
+ ctl.vistors[cfg.GetName()] = vistor
+ vistor.Info("start vistor success")
+ }
+ return nil
+}
- } else {
- workConn, err = net.ConnectServerByHttpProxy(config.ClientCommonCfg.HttpProxy, config.ClientCommonCfg.Protocol,
- fmt.Sprintf("%s:%d", config.ClientCommonCfg.ServerAddr, config.ClientCommonCfg.ServerPort))
- if err != nil {
- ctl.Warn("start new work connection error: %v", err)
- return
- }
+func (ctl *Control) NewWorkConn() {
+ workConn, err := ctl.connectServer()
+ if err != nil {
+ return
}
m := &msg.NewWorkConn{
@@ -199,7 +204,7 @@ func (ctl *Control) login() (err error) {
ctl.session.Close()
}
- conn, err := net.ConnectServerByHttpProxy(config.ClientCommonCfg.HttpProxy, config.ClientCommonCfg.Protocol,
+ conn, err := frpNet.ConnectServerByHttpProxy(config.ClientCommonCfg.HttpProxy, config.ClientCommonCfg.Protocol,
fmt.Sprintf("%s:%d", config.ClientCommonCfg.ServerAddr, config.ClientCommonCfg.ServerPort))
if err != nil {
return err
@@ -221,7 +226,7 @@ func (ctl *Control) login() (err error) {
session.Close()
return errRet
}
- conn = net.WrapConn(stream)
+ conn = frpNet.WrapConn(stream)
ctl.session = session
}
@@ -261,6 +266,27 @@ func (ctl *Control) login() (err error) {
return nil
}
+func (ctl *Control) connectServer() (conn frpNet.Conn, err error) {
+ if config.ClientCommonCfg.TcpMux {
+ stream, errRet := ctl.session.OpenStream()
+ if errRet != nil {
+ err = errRet
+ ctl.Warn("start new connection to server error: %v", err)
+ return
+ }
+ conn = frpNet.WrapConn(stream)
+
+ } else {
+ conn, err = frpNet.ConnectServerByHttpProxy(config.ClientCommonCfg.HttpProxy, config.ClientCommonCfg.Protocol,
+ fmt.Sprintf("%s:%d", config.ClientCommonCfg.ServerAddr, config.ClientCommonCfg.ServerPort))
+ if err != nil {
+ ctl.Warn("start new connection to server error: %v", err)
+ return
+ }
+ }
+ return
+}
+
func (ctl *Control) reader() {
defer func() {
if err := recover(); err != nil {
diff --git a/client/proxy.go b/client/proxy.go
index cd7994c6..147a3fbd 100644
--- a/client/proxy.go
+++ b/client/proxy.go
@@ -31,7 +31,7 @@ import (
frpNet "github.com/fatedier/frp/utils/net"
)
-// Proxy defines how to work for different proxy type.
+// Proxy defines how to deal with work connections for different proxy type.
type Proxy interface {
Run() error
@@ -67,6 +67,11 @@ func NewProxy(ctl *Control, pxyConf config.ProxyConf) (pxy Proxy) {
BaseProxy: baseProxy,
cfg: cfg,
}
+ case *config.StcpProxyConf:
+ pxy = &StcpProxy{
+ BaseProxy: baseProxy,
+ cfg: cfg,
+ }
}
return
}
@@ -162,6 +167,34 @@ func (pxy *HttpsProxy) InWorkConn(conn frpNet.Conn) {
HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn)
}
+// STCP
+type StcpProxy struct {
+ BaseProxy
+
+ cfg *config.StcpProxyConf
+ proxyPlugin plugin.Plugin
+}
+
+func (pxy *StcpProxy) 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 *StcpProxy) Close() {
+ if pxy.proxyPlugin != nil {
+ pxy.proxyPlugin.Close()
+ }
+}
+
+func (pxy *StcpProxy) InWorkConn(conn frpNet.Conn) {
+ HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn)
+}
+
// UDP
type UdpProxy struct {
BaseProxy
diff --git a/client/service.go b/client/service.go
index 36d0a7e8..776951aa 100644
--- a/client/service.go
+++ b/client/service.go
@@ -23,11 +23,11 @@ type Service struct {
closedCh chan int
}
-func NewService(pxyCfgs map[string]config.ProxyConf) (svr *Service) {
+func NewService(pxyCfgs map[string]config.ProxyConf, vistorCfgs map[string]config.ProxyConf) (svr *Service) {
svr = &Service{
closedCh: make(chan int),
}
- ctl := NewControl(svr, pxyCfgs)
+ ctl := NewControl(svr, pxyCfgs, vistorCfgs)
svr.ctl = ctl
return
}
diff --git a/client/vistor.go b/client/vistor.go
new file mode 100644
index 00000000..8787ebfe
--- /dev/null
+++ b/client/vistor.go
@@ -0,0 +1,145 @@
+// Copyright 2017 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 client
+
+import (
+ "io"
+ "sync"
+ "time"
+
+ "github.com/fatedier/frp/models/config"
+ "github.com/fatedier/frp/models/msg"
+ frpIo "github.com/fatedier/frp/utils/io"
+ "github.com/fatedier/frp/utils/log"
+ frpNet "github.com/fatedier/frp/utils/net"
+ "github.com/fatedier/frp/utils/util"
+)
+
+// Vistor is used for forward traffics from local port tot remote service.
+type Vistor interface {
+ Run() error
+ Close()
+ log.Logger
+}
+
+func NewVistor(ctl *Control, pxyConf config.ProxyConf) (vistor Vistor) {
+ baseVistor := BaseVistor{
+ ctl: ctl,
+ Logger: log.NewPrefixLogger(pxyConf.GetName()),
+ }
+ switch cfg := pxyConf.(type) {
+ case *config.StcpProxyConf:
+ vistor = &StcpVistor{
+ BaseVistor: baseVistor,
+ cfg: cfg,
+ }
+ }
+ return
+}
+
+type BaseVistor struct {
+ ctl *Control
+ l frpNet.Listener
+ closed bool
+ mu sync.RWMutex
+ log.Logger
+}
+
+type StcpVistor struct {
+ BaseVistor
+
+ cfg *config.StcpProxyConf
+}
+
+func (sv *StcpVistor) Run() (err error) {
+ sv.l, err = frpNet.ListenTcp(sv.cfg.BindAddr, int64(sv.cfg.BindPort))
+ if err != nil {
+ return
+ }
+
+ go sv.worker()
+ return
+}
+
+func (sv *StcpVistor) Close() {
+ sv.l.Close()
+}
+
+func (sv *StcpVistor) worker() {
+ for {
+ conn, err := sv.l.Accept()
+ if err != nil {
+ sv.Warn("stcp local listener closed")
+ return
+ }
+
+ go sv.handleConn(conn)
+ }
+}
+
+func (sv *StcpVistor) handleConn(userConn frpNet.Conn) {
+ defer userConn.Close()
+
+ sv.Debug("get a new stcp user connection")
+ vistorConn, err := sv.ctl.connectServer()
+ if err != nil {
+ return
+ }
+ defer vistorConn.Close()
+
+ now := time.Now().Unix()
+ newVistorConnMsg := &msg.NewVistorConn{
+ ProxyName: sv.cfg.ServerName,
+ SignKey: util.GetAuthKey(sv.cfg.Sk, now),
+ Timestamp: now,
+ UseEncryption: sv.cfg.UseEncryption,
+ UseCompression: sv.cfg.UseCompression,
+ }
+ err = msg.WriteMsg(vistorConn, newVistorConnMsg)
+ if err != nil {
+ sv.Warn("send newVistorConnMsg to server error: %v", err)
+ return
+ }
+
+ var newVistorConnRespMsg msg.NewVistorConnResp
+ vistorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
+ err = msg.ReadMsgInto(vistorConn, &newVistorConnRespMsg)
+ if err != nil {
+ sv.Warn("get newVistorConnRespMsg error: %v", err)
+ return
+ }
+ vistorConn.SetReadDeadline(time.Time{})
+
+ if newVistorConnRespMsg.Error != "" {
+ sv.Warn("start new vistor connection error: %s", newVistorConnRespMsg.Error)
+ return
+ }
+
+ var remote io.ReadWriteCloser
+ remote = vistorConn
+ if sv.cfg.UseEncryption {
+ remote, err = frpIo.WithEncryption(remote, []byte(sv.cfg.Sk))
+ if err != nil {
+ sv.Error("create encryption stream error: %v", err)
+ return
+ }
+ }
+
+ if sv.cfg.UseCompression {
+ remote = frpIo.WithCompression(remote)
+ }
+
+ frpIo.Join(userConn, remote)
+}
diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go
index dcbc3a53..870eeae1 100644
--- a/cmd/frpc/main.go
+++ b/cmd/frpc/main.go
@@ -106,7 +106,7 @@ func main() {
}
}
- pxyCfgs, err := config.LoadProxyConfFromFile(config.ClientCommonCfg.User, conf, config.ClientCommonCfg.Start)
+ pxyCfgs, vistorCfgs, err := config.LoadProxyConfFromFile(config.ClientCommonCfg.User, conf, config.ClientCommonCfg.Start)
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -115,7 +115,7 @@ func main() {
log.InitLog(config.ClientCommonCfg.LogWay, config.ClientCommonCfg.LogFile,
config.ClientCommonCfg.LogLevel, config.ClientCommonCfg.LogMaxDays)
- svr := client.NewService(pxyCfgs)
+ svr := client.NewService(pxyCfgs, vistorCfgs)
err = svr.Run()
if err != nil {
fmt.Println(err)
diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini
index a653fc24..4cc6dc17 100644
--- a/conf/frpc_full.ini
+++ b/conf/frpc_full.ini
@@ -110,3 +110,24 @@ remote_port = 6004
plugin = http_proxy
plugin_http_user = abc
plugin_http_passwd = abc
+
+[secret_tcp]
+# If the type is secret tcp, remote_port is useless
+# Who want to connect local port should deploy another frpc with stcp proxy and role is vistor
+type = stcp
+# sk used for authentication for vistors
+sk = abcdefg
+local_ip = 127.0.0.1
+local_port = 22
+use_encryption = false
+use_compression = false
+
+[secret_tcp_vistor]
+# frpc role vistor -> frps -> frpc role server
+role = vistor
+type = stcp
+sk = abcdefg
+bind_addr = 127.0.0.1
+bind_port = 9000
+use_encryption = false
+use_compression = false
diff --git a/models/config/proxy.go b/models/config/proxy.go
index 27fd3f64..90c982bf 100644
--- a/models/config/proxy.go
+++ b/models/config/proxy.go
@@ -35,6 +35,7 @@ func init() {
proxyConfTypeMap[consts.UdpProxy] = reflect.TypeOf(UdpProxyConf{})
proxyConfTypeMap[consts.HttpProxy] = reflect.TypeOf(HttpProxyConf{})
proxyConfTypeMap[consts.HttpsProxy] = reflect.TypeOf(HttpsProxyConf{})
+ proxyConfTypeMap[consts.StcpProxy] = reflect.TypeOf(StcpProxyConf{})
}
// NewConfByType creates a empty ProxyConf object by proxyType.
@@ -388,8 +389,10 @@ func (cfg *HttpProxyConf) LoadFromFile(name string, section ini.Section) (err er
if err = cfg.DomainConf.LoadFromFile(name, section); err != nil {
return
}
- if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
- return
+ if err = cfg.PluginConf.LoadFromFile(name, section); err != nil {
+ if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
+ return
+ }
}
var (
@@ -447,8 +450,10 @@ func (cfg *HttpsProxyConf) LoadFromFile(name string, section ini.Section) (err e
if err = cfg.DomainConf.LoadFromFile(name, section); err != nil {
return
}
- if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
- return
+ if err = cfg.PluginConf.LoadFromFile(name, section); err != nil {
+ if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
+ return
+ }
}
return
}
@@ -466,9 +471,81 @@ func (cfg *HttpsProxyConf) Check() (err error) {
return
}
+// STCP
+type StcpProxyConf struct {
+ BaseProxyConf
+
+ Role string `json:"role"`
+ Sk string `json:"sk"`
+
+ // used in role server
+ LocalSvrConf
+ PluginConf
+
+ // used in role vistor
+ ServerName string `json:"server_name"`
+ BindAddr string `json:"bind_addr"`
+ BindPort int `json:"bind_port"`
+}
+
+// Only for role server.
+func (cfg *StcpProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
+ cfg.BaseProxyConf.LoadFromMsg(pMsg)
+ cfg.Sk = pMsg.Sk
+}
+
+func (cfg *StcpProxyConf) LoadFromFile(name string, section ini.Section) (err error) {
+ if err = cfg.BaseProxyConf.LoadFromFile(name, section); err != nil {
+ return
+ }
+
+ tmpStr := section["role"]
+ if tmpStr == "server" || tmpStr == "vistor" {
+ cfg.Role = tmpStr
+ } else {
+ cfg.Role = "server"
+ }
+
+ cfg.Sk = section["sk"]
+
+ if tmpStr == "vistor" {
+ prefix := section["prefix"]
+ cfg.ServerName = prefix + section["server_name"]
+ if cfg.BindAddr = section["bind_addr"]; cfg.BindAddr == "" {
+ cfg.BindAddr = "127.0.0.1"
+ }
+
+ if tmpStr, ok := section["bind_port"]; ok {
+ if cfg.BindPort, err = strconv.Atoi(tmpStr); err != nil {
+ return fmt.Errorf("Parse conf error: proxy [%s] bind_port error", name)
+ }
+ } else {
+ return fmt.Errorf("Parse conf error: proxy [%s] bind_port not found", name)
+ }
+ } else {
+ if err = cfg.PluginConf.LoadFromFile(name, section); err != nil {
+ if err = cfg.LocalSvrConf.LoadFromFile(name, section); err != nil {
+ return
+ }
+ }
+ }
+ return
+}
+
+func (cfg *StcpProxyConf) UnMarshalToMsg(pMsg *msg.NewProxy) {
+ cfg.BaseProxyConf.UnMarshalToMsg(pMsg)
+ pMsg.Sk = cfg.Sk
+}
+
+func (cfg *StcpProxyConf) Check() (err error) {
+ return
+}
+
// if len(startProxy) is 0, start all
// otherwise just start proxies in startProxy map
-func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]struct{}) (proxyConfs map[string]ProxyConf, err error) {
+func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]struct{}) (
+ proxyConfs map[string]ProxyConf, vistorConfs map[string]ProxyConf, err error) {
+
if prefix != "" {
prefix += "."
}
@@ -478,14 +555,23 @@ func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]s
startAll = false
}
proxyConfs = make(map[string]ProxyConf)
+ vistorConfs = make(map[string]ProxyConf)
for name, section := range conf {
_, shouldStart := startProxy[name]
if name != "common" && (startAll || shouldStart) {
+ // some proxy or visotr configure may be used this prefix
+ section["prefix"] = prefix
cfg, err := NewProxyConfFromFile(name, section)
if err != nil {
- return proxyConfs, err
+ return proxyConfs, vistorConfs, err
+ }
+
+ role := section["role"]
+ if role == "vistor" {
+ vistorConfs[prefix+name] = cfg
+ } else {
+ proxyConfs[prefix+name] = cfg
}
- proxyConfs[prefix+name] = cfg
}
}
return
diff --git a/models/consts/consts.go b/models/consts/consts.go
index 170bd240..5a4bc264 100644
--- a/models/consts/consts.go
+++ b/models/consts/consts.go
@@ -27,4 +27,5 @@ var (
UdpProxy string = "udp"
HttpProxy string = "http"
HttpsProxy string = "https"
+ StcpProxy string = "stcp"
)
diff --git a/models/msg/msg.go b/models/msg/msg.go
index d961befa..59736b6d 100644
--- a/models/msg/msg.go
+++ b/models/msg/msg.go
@@ -20,17 +20,19 @@ import (
)
const (
- TypeLogin = 'o'
- TypeLoginResp = '1'
- TypeNewProxy = 'p'
- TypeNewProxyResp = '2'
- TypeCloseProxy = 'c'
- TypeNewWorkConn = 'w'
- TypeReqWorkConn = 'r'
- TypeStartWorkConn = 's'
- TypePing = 'h'
- TypePong = '4'
- TypeUdpPacket = 'u'
+ TypeLogin = 'o'
+ TypeLoginResp = '1'
+ TypeNewProxy = 'p'
+ TypeNewProxyResp = '2'
+ TypeCloseProxy = 'c'
+ TypeNewWorkConn = 'w'
+ TypeReqWorkConn = 'r'
+ TypeStartWorkConn = 's'
+ TypeNewVistorConn = 'v'
+ TypeNewVistorConnResp = '3'
+ TypePing = 'h'
+ TypePong = '4'
+ TypeUdpPacket = 'u'
)
var (
@@ -50,6 +52,8 @@ func init() {
TypeMap[TypeNewWorkConn] = reflect.TypeOf(NewWorkConn{})
TypeMap[TypeReqWorkConn] = reflect.TypeOf(ReqWorkConn{})
TypeMap[TypeStartWorkConn] = reflect.TypeOf(StartWorkConn{})
+ TypeMap[TypeNewVistorConn] = reflect.TypeOf(NewVistorConn{})
+ TypeMap[TypeNewVistorConnResp] = reflect.TypeOf(NewVistorConnResp{})
TypeMap[TypePing] = reflect.TypeOf(Ping{})
TypeMap[TypePong] = reflect.TypeOf(Pong{})
TypeMap[TypeUdpPacket] = reflect.TypeOf(UdpPacket{})
@@ -100,6 +104,9 @@ type NewProxy struct {
HostHeaderRewrite string `json:"host_header_rewrite"`
HttpUser string `json:"http_user"`
HttpPwd string `json:"http_pwd"`
+
+ // stcp
+ Sk string `json:"sk"`
}
type NewProxyResp struct {
@@ -122,6 +129,19 @@ type StartWorkConn struct {
ProxyName string `json:"proxy_name"`
}
+type NewVistorConn struct {
+ ProxyName string `json:"proxy_name"`
+ SignKey string `json:"sign_key"`
+ Timestamp int64 `json:"timestamp"`
+ UseEncryption bool `json:"use_encryption"`
+ UseCompression bool `json:"use_compression"`
+}
+
+type NewVistorConnResp struct {
+ ProxyName string `json:"proxy_name"`
+ Error string `json:"error"`
+}
+
type Ping struct {
}
diff --git a/server/manager.go b/server/manager.go
index 47456c00..c78037dd 100644
--- a/server/manager.go
+++ b/server/manager.go
@@ -16,7 +16,12 @@ package server
import (
"fmt"
+ "io"
"sync"
+
+ frpIo "github.com/fatedier/frp/utils/io"
+ frpNet "github.com/fatedier/frp/utils/net"
+ "github.com/fatedier/frp/utils/util"
)
type ControlManager struct {
@@ -87,3 +92,72 @@ func (pm *ProxyManager) GetByName(name string) (pxy Proxy, ok bool) {
pxy, ok = pm.pxys[name]
return
}
+
+// Manager for vistor listeners.
+type VistorManager struct {
+ vistorListeners map[string]*frpNet.CustomListener
+ skMap map[string]string
+
+ mu sync.RWMutex
+}
+
+func NewVistorManager() *VistorManager {
+ return &VistorManager{
+ vistorListeners: make(map[string]*frpNet.CustomListener),
+ skMap: make(map[string]string),
+ }
+}
+
+func (vm *VistorManager) Listen(name string, sk string) (l *frpNet.CustomListener, err error) {
+ vm.mu.Lock()
+ defer vm.mu.Unlock()
+
+ if _, ok := vm.vistorListeners[name]; ok {
+ err = fmt.Errorf("custom listener for [%s] is repeated", name)
+ return
+ }
+
+ l = frpNet.NewCustomListener()
+ vm.vistorListeners[name] = l
+ vm.skMap[name] = sk
+ return
+}
+
+func (vm *VistorManager) NewConn(name string, conn frpNet.Conn, timestamp int64, signKey string,
+ useEncryption bool, useCompression bool) (err error) {
+
+ vm.mu.RLock()
+ defer vm.mu.RUnlock()
+
+ if l, ok := vm.vistorListeners[name]; ok {
+ var sk string
+ if sk = vm.skMap[name]; util.GetAuthKey(sk, timestamp) != signKey {
+ err = fmt.Errorf("vistor connection of [%s] auth failed", name)
+ return
+ }
+
+ var rwc io.ReadWriteCloser = conn
+ if useEncryption {
+ if rwc, err = frpIo.WithEncryption(rwc, []byte(sk)); err != nil {
+ err = fmt.Errorf("create encryption connection failed: %v", err)
+ return
+ }
+ }
+ if useCompression {
+ rwc = frpIo.WithCompression(rwc)
+ }
+ err = l.PutConn(frpNet.WrapReadWriteCloserToConn(rwc))
+ } else {
+ err = fmt.Errorf("custom listener for [%s] doesn't exist", name)
+ return
+ }
+ return
+}
+
+func (vm *VistorManager) CloseListener(name string) {
+ vm.mu.Lock()
+ defer vm.mu.Unlock()
+
+ delete(vm.vistorListeners, name)
+ delete(vm.skMap, name)
+}
diff --git a/server/proxy.go b/server/proxy.go
index 6751e245..ac8ddc07 100644
--- a/server/proxy.go
+++ b/server/proxy.go
@@ -143,6 +143,11 @@ func NewProxy(ctl *Control, pxyConf config.ProxyConf) (pxy Proxy, err error) {
BaseProxy: basePxy,
cfg: cfg,
}
+ case *config.StcpProxyConf:
+ pxy = &StcpProxy{
+ BaseProxy: basePxy,
+ cfg: cfg,
+ }
default:
return pxy, fmt.Errorf("proxy type not support")
}
@@ -274,6 +279,33 @@ func (pxy *HttpsProxy) Close() {
pxy.BaseProxy.Close()
}
+type StcpProxy struct {
+ BaseProxy
+ cfg *config.StcpProxyConf
+}
+
+func (pxy *StcpProxy) Run() error {
+ listener, err := pxy.ctl.svr.vistorManager.Listen(pxy.GetName(), pxy.cfg.Sk)
+ if err != nil {
+ return err
+ }
+ listener.AddLogPrefix(pxy.name)
+ pxy.listeners = append(pxy.listeners, listener)
+ pxy.Info("stcp proxy custom listen success")
+
+ pxy.startListenHandler(pxy, HandleUserTcpConnection)
+ return nil
+}
+
+func (pxy *StcpProxy) GetConf() config.ProxyConf {
+ return pxy.cfg
+}
+
+func (pxy *StcpProxy) Close() {
+ pxy.BaseProxy.Close()
+ pxy.ctl.svr.vistorManager.CloseListener(pxy.GetName())
+}
+
type UdpProxy struct {
BaseProxy
cfg *config.UdpProxyConf
diff --git a/server/service.go b/server/service.go
index 6583b1c6..78e7762e 100644
--- a/server/service.go
+++ b/server/service.go
@@ -55,12 +55,16 @@ type Service struct {
// Manage all proxies.
pxyManager *ProxyManager
+
+ // Manage all vistor listeners.
+ vistorManager *VistorManager
}
func NewService() (svr *Service, err error) {
svr = &Service{
- ctlManager: NewControlManager(),
- pxyManager: NewProxyManager(),
+ ctlManager: NewControlManager(),
+ pxyManager: NewProxyManager(),
+ vistorManager: NewVistorManager(),
}
// Init assets.
@@ -176,6 +180,20 @@ func (svr *Service) HandleListener(l frpNet.Listener) {
}
case *msg.NewWorkConn:
svr.RegisterWorkConn(conn, m)
+ case *msg.NewVistorConn:
+ if err = svr.RegisterVistorConn(conn, m); err != nil {
+ conn.Warn("%v", err)
+ msg.WriteMsg(conn, &msg.NewVistorConnResp{
+ ProxyName: m.ProxyName,
+ Error: err.Error(),
+ })
+ conn.Close()
+ } else {
+ msg.WriteMsg(conn, &msg.NewVistorConnResp{
+ ProxyName: m.ProxyName,
+ Error: "",
+ })
+ }
default:
log.Warn("Error message type for the new connection [%s]", conn.RemoteAddr().String())
conn.Close()
@@ -262,9 +280,13 @@ func (svr *Service) RegisterWorkConn(workConn frpNet.Conn, newMsg *msg.NewWorkCo
return
}
+func (svr *Service) RegisterVistorConn(vistorConn frpNet.Conn, newMsg *msg.NewVistorConn) error {
+ return svr.vistorManager.NewConn(newMsg.ProxyName, vistorConn, newMsg.Timestamp, newMsg.SignKey,
+ newMsg.UseEncryption, newMsg.UseCompression)
+}
+
func (svr *Service) RegisterProxy(name string, pxy Proxy) error {
- err := svr.pxyManager.Add(name, pxy)
- return err
+ return svr.pxyManager.Add(name, pxy)
}
func (svr *Service) DelProxy(name string) {
diff --git a/utils/log/log.go b/utils/log/log.go
index ec6e0775..a0e42b8f 100644
--- a/utils/log/log.go
+++ b/utils/log/log.go
@@ -88,6 +88,7 @@ func Trace(format string, v ...interface{}) {
// Logger
type Logger interface {
AddLogPrefix(string)
+ GetPrefixStr() string
GetAllPrefix() []string
ClearLogPrefix()
Error(string, ...interface{})
@@ -119,6 +120,10 @@ func (pl *PrefixLogger) AddLogPrefix(prefix string) {
pl.allPrefix = append(pl.allPrefix, prefix)
}
+func (pl *PrefixLogger) GetPrefixStr() string {
+ return pl.prefix
+}
+
func (pl *PrefixLogger) GetAllPrefix() []string {
return pl.allPrefix
}
diff --git a/utils/net/listener.go b/utils/net/listener.go
index f9345b92..cb3847e5 100644
--- a/utils/net/listener.go
+++ b/utils/net/listener.go
@@ -15,8 +15,11 @@
package net
import (
+ "fmt"
"net"
+ "sync"
+ "github.com/fatedier/frp/utils/errors"
"github.com/fatedier/frp/utils/log"
)
@@ -44,3 +47,53 @@ func (logL *LogListener) Accept() (Conn, error) {
c, err := logL.l.Accept()
return WrapConn(c), err
}
+
+// Custom listener
+type CustomListener struct {
+ conns chan Conn
+ closed bool
+ mu sync.Mutex
+
+ log.Logger
+}
+
+func NewCustomListener() *CustomListener {
+ return &CustomListener{
+ conns: make(chan Conn, 64),
+ Logger: log.NewPrefixLogger(""),
+ }
+}
+
+func (l *CustomListener) Accept() (Conn, error) {
+ conn, ok := <-l.conns
+ if !ok {
+ return nil, fmt.Errorf("listener closed")
+ }
+ conn.AddLogPrefix(l.GetPrefixStr())
+ return conn, nil
+}
+
+func (l *CustomListener) PutConn(conn Conn) error {
+ err := errors.PanicToError(func() {
+ select {
+ case l.conns <- conn:
+ default:
+ conn.Close()
+ }
+ })
+ return err
+}
+
+func (l *CustomListener) Close() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ if !l.closed {
+ close(l.conns)
+ l.closed = true
+ }
+ return nil
+}
+
+func (l *CustomListener) Addr() net.Addr {
+ return (*net.TCPAddr)(nil)
+}
From 07fe05a9d5e8937db98232b93eb1207207e6d8ec Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 26 Jun 2017 20:57:10 +0800
Subject: [PATCH 04/19] update version to v0.13.0
---
utils/version/version.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/utils/version/version.go b/utils/version/version.go
index 1a1e749f..b631dba7 100644
--- a/utils/version/version.go
+++ b/utils/version/version.go
@@ -19,7 +19,7 @@ import (
"strings"
)
-var version string = "0.12.0"
+var version string = "0.13.0"
func Full() string {
return version
From b811a620c35eb98464fa3b45120c87eb8ae4b902 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 26 Jun 2017 22:24:47 +0800
Subject: [PATCH 05/19] vhost: fix 404 page
---
utils/vhost/resource.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/utils/vhost/resource.go b/utils/vhost/resource.go
index 6189b35a..ed8149c6 100644
--- a/utils/vhost/resource.go
+++ b/utils/vhost/resource.go
@@ -49,9 +49,10 @@ Please try again later.
func notFoundResponse() *http.Response {
header := make(http.Header)
header.Set("server", "frp/"+version.Full())
+ header.Set("Content-Type", "text/html")
res := &http.Response{
Status: "Not Found",
- StatusCode: 400,
+ StatusCode: 404,
Proto: "HTTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
From aede4e54f8911a0496049dc3de1edc8f7dbebdde Mon Sep 17 00:00:00 2001
From: fatedier
Date: Tue, 27 Jun 2017 01:59:30 +0800
Subject: [PATCH 06/19] close all proxies if protocol = kcp
---
client/control.go | 37 ++++++++++++++++++++++++++++++++++---
client/service.go | 4 ++++
cmd/frpc/main.go | 18 ++++++++++++++++++
server/control.go | 1 +
4 files changed, 57 insertions(+), 3 deletions(-)
diff --git a/client/control.go b/client/control.go
index 45f617b3..825d024d 100644
--- a/client/control.go
+++ b/client/control.go
@@ -24,6 +24,7 @@ import (
"github.com/fatedier/frp/models/config"
"github.com/fatedier/frp/models/msg"
"github.com/fatedier/frp/utils/crypto"
+ "github.com/fatedier/frp/utils/errors"
"github.com/fatedier/frp/utils/log"
frpNet "github.com/fatedier/frp/utils/net"
"github.com/fatedier/frp/utils/util"
@@ -69,8 +70,8 @@ type Control struct {
// run id got from server
runId string
- // connection or other error happens , control will try to reconnect to server
- closed int32
+ // if we call close() in control, do not reconnect to server
+ exit bool
// goroutines can block by reading from this channel, it will be closed only in reader() when control connection is closed
closedCh chan int
@@ -181,7 +182,10 @@ func (ctl *Control) NewWorkConn() {
workConn.AddLogPrefix(startMsg.ProxyName)
// dispatch this work connection to related proxy
- if pxy, ok := ctl.proxies[startMsg.ProxyName]; ok {
+ ctl.mu.RLock()
+ pxy, ok := ctl.proxies[startMsg.ProxyName]
+ ctl.mu.RUnlock()
+ if ok {
workConn.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
go pxy.InWorkConn(workConn)
} else {
@@ -189,6 +193,20 @@ func (ctl *Control) NewWorkConn() {
}
}
+func (ctl *Control) Close() error {
+ ctl.mu.Lock()
+ ctl.exit = true
+ err := errors.PanicToError(func() {
+ for name, _ := range ctl.proxies {
+ ctl.sendCh <- &msg.CloseProxy{
+ ProxyName: name,
+ }
+ }
+ })
+ ctl.mu.Unlock()
+ return err
+}
+
func (ctl *Control) init() {
ctl.sendCh = make(chan msg.Message, 10)
ctl.readCh = make(chan msg.Message, 10)
@@ -377,7 +395,10 @@ func (ctl *Control) manager() {
ctl.Warn("[%s] no proxy conf found", m.ProxyName)
continue
}
+
+ ctl.mu.RLock()
oldPxy, ok := ctl.proxies[m.ProxyName]
+ ctl.mu.RUnlock()
if ok {
oldPxy.Close()
}
@@ -389,7 +410,9 @@ func (ctl *Control) manager() {
}
continue
}
+ ctl.mu.Lock()
ctl.proxies[m.ProxyName] = pxy
+ ctl.mu.Unlock()
ctl.Info("[%s] start proxy success", m.ProxyName)
case *msg.Pong:
ctl.lastPong = time.Now()
@@ -429,6 +452,14 @@ func (ctl *Control) controler() {
for _, pxy := range ctl.proxies {
pxy.Close()
}
+ // if ctl.exit is true, just exit
+ ctl.mu.RLock()
+ exit := ctl.exit
+ ctl.mu.RUnlock()
+ if exit {
+ return
+ }
+
time.Sleep(time.Second)
// loop util reconnect to server success
diff --git a/client/service.go b/client/service.go
index 776951aa..ff28cc91 100644
--- a/client/service.go
+++ b/client/service.go
@@ -41,3 +41,7 @@ func (svr *Service) Run() error {
<-svr.closedCh
return nil
}
+
+func (svr *Service) Close() error {
+ return svr.ctl.Close()
+}
diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go
index 870eeae1..88fc59b9 100644
--- a/cmd/frpc/main.go
+++ b/cmd/frpc/main.go
@@ -17,8 +17,11 @@ package main
import (
"fmt"
"os"
+ "os/signal"
"strconv"
"strings"
+ "syscall"
+ "time"
docopt "github.com/docopt/docopt-go"
ini "github.com/vaughan0/go-ini"
@@ -116,9 +119,24 @@ func main() {
config.ClientCommonCfg.LogLevel, config.ClientCommonCfg.LogMaxDays)
svr := client.NewService(pxyCfgs, vistorCfgs)
+
+ // Capture the exit signal if we use kcp.
+ if config.ClientCommonCfg.Protocol == "kcp" {
+ go HandleSignal(svr)
+ }
+
err = svr.Run()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
+
+func HandleSignal(svr *client.Service) {
+ ch := make(chan os.Signal)
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+ <-ch
+ svr.Close()
+ time.Sleep(250 * time.Millisecond)
+ os.Exit(0)
+}
diff --git a/server/control.go b/server/control.go
index d6b2c2c6..5a84394a 100644
--- a/server/control.go
+++ b/server/control.go
@@ -378,6 +378,7 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
pxy.Close()
ctl.svr.DelProxy(pxy.GetName())
+ delete(ctl.proxies, closeMsg.ProxyName)
StatsCloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
return
}
From b55a24a27e07fcb22c9d8c963cf6b4f84f5d8823 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Tue, 27 Jun 2017 23:30:09 +0800
Subject: [PATCH 07/19] update mutex used in frpc control
---
client/control.go | 63 +++++++++++++++++++++++++++++++----------------
1 file changed, 42 insertions(+), 21 deletions(-)
diff --git a/client/control.go b/client/control.go
index 825d024d..fb1ba716 100644
--- a/client/control.go
+++ b/client/control.go
@@ -137,13 +137,6 @@ func (ctl *Control) Run() (err error) {
go ctl.writer()
go ctl.reader()
- // send NewProxy message for all configured proxies
- for _, cfg := range ctl.pxyCfgs {
- var newProxyMsg msg.NewProxy
- cfg.UnMarshalToMsg(&newProxyMsg)
- ctl.sendCh <- &newProxyMsg
- }
-
// start all local vistors
for _, cfg := range ctl.vistorCfgs {
vistor := NewVistor(ctl, cfg)
@@ -155,6 +148,13 @@ func (ctl *Control) Run() (err error) {
ctl.vistors[cfg.GetName()] = vistor
vistor.Info("start vistor success")
}
+
+ // send NewProxy message for all configured proxies
+ for _, cfg := range ctl.pxyCfgs {
+ var newProxyMsg msg.NewProxy
+ cfg.UnMarshalToMsg(&newProxyMsg)
+ ctl.sendCh <- &newProxyMsg
+ }
return nil
}
@@ -165,7 +165,7 @@ func (ctl *Control) NewWorkConn() {
}
m := &msg.NewWorkConn{
- RunId: ctl.runId,
+ RunId: ctl.getRunId(),
}
if err = msg.WriteMsg(workConn, m); err != nil {
ctl.Warn("work connection write to server error: %v", err)
@@ -182,9 +182,7 @@ func (ctl *Control) NewWorkConn() {
workConn.AddLogPrefix(startMsg.ProxyName)
// dispatch this work connection to related proxy
- ctl.mu.RLock()
- pxy, ok := ctl.proxies[startMsg.ProxyName]
- ctl.mu.RUnlock()
+ pxy, ok := ctl.getProxy(startMsg.ProxyName)
if ok {
workConn.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
go pxy.InWorkConn(workConn)
@@ -251,7 +249,7 @@ func (ctl *Control) login() (err error) {
now := time.Now().Unix()
ctl.loginMsg.PrivilegeKey = util.GetAuthKey(config.ClientCommonCfg.PrivilegeToken, now)
ctl.loginMsg.Timestamp = now
- ctl.loginMsg.RunId = ctl.runId
+ ctl.loginMsg.RunId = ctl.getRunId()
if err = msg.WriteMsg(conn, ctl.loginMsg); err != nil {
return err
@@ -272,7 +270,7 @@ func (ctl *Control) login() (err error) {
ctl.conn = conn
// update runId got from server
- ctl.runId = loginRespMsg.RunId
+ ctl.setRunId(loginRespMsg.RunId)
ctl.ClearLogPrefix()
ctl.AddLogPrefix(loginRespMsg.RunId)
ctl.Info("login to server success, get run id [%s]", loginRespMsg.RunId)
@@ -349,6 +347,7 @@ func (ctl *Control) writer() {
}
}
+// manager handles all channel events and do corresponding process
func (ctl *Control) manager() {
defer func() {
if err := recover(); err != nil {
@@ -396,9 +395,7 @@ func (ctl *Control) manager() {
continue
}
- ctl.mu.RLock()
- oldPxy, ok := ctl.proxies[m.ProxyName]
- ctl.mu.RUnlock()
+ oldPxy, ok := ctl.getProxy(m.ProxyName)
if ok {
oldPxy.Close()
}
@@ -410,9 +407,7 @@ func (ctl *Control) manager() {
}
continue
}
- ctl.mu.Lock()
- ctl.proxies[m.ProxyName] = pxy
- ctl.mu.Unlock()
+ ctl.addProxy(m.ProxyName, pxy)
ctl.Info("[%s] start proxy success", m.ProxyName)
case *msg.Pong:
ctl.lastPong = time.Now()
@@ -422,7 +417,8 @@ func (ctl *Control) manager() {
}
}
-// control keep watching closedCh, start a new connection if previous control connection is closed
+// controler keep watching closedCh, start a new connection if previous control connection is closed.
+// If controler is notified by closedCh, reader and writer and manager will exit, then recall these functions.
func (ctl *Control) controler() {
var err error
maxDelayTime := 30 * time.Second
@@ -435,7 +431,7 @@ func (ctl *Control) controler() {
case <-checkProxyTicker.C:
// Every 30 seconds, check which proxy registered failed and reregister it to server.
for _, cfg := range ctl.pxyCfgs {
- if _, exist := ctl.proxies[cfg.GetName()]; !exist {
+ if _, exist := ctl.getProxy(cfg.GetName()); !exist {
ctl.Info("try to reregister proxy [%s]", cfg.GetName())
var newProxyMsg msg.NewProxy
cfg.UnMarshalToMsg(&newProxyMsg)
@@ -501,3 +497,28 @@ func (ctl *Control) controler() {
}
}
}
+
+func (ctl *Control) setRunId(runId string) {
+ ctl.mu.Lock()
+ defer ctl.mu.Unlock()
+ ctl.runId = runId
+}
+
+func (ctl *Control) getRunId() string {
+ ctl.mu.RLock()
+ defer ctl.mu.RUnlock()
+ return ctl.runId
+}
+
+func (ctl *Control) getProxy(name string) (pxy Proxy, ok bool) {
+ ctl.mu.RLock()
+ defer ctl.mu.RUnlock()
+ pxy, ok = ctl.proxies[name]
+ return
+}
+
+func (ctl *Control) addProxy(name string, pxy Proxy) {
+ ctl.mu.Lock()
+ defer ctl.mu.Unlock()
+ ctl.proxies[name] = pxy
+}
From 4cc1663a5f9ce5c6f1db764a3799526221b92f09 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Sat, 1 Jul 2017 01:54:37 +0800
Subject: [PATCH 08/19] vhost: add real ip in first request of one connection
1. fix #248 host_header_rewrite bug
2. close #270, #127
---
utils/vhost/http.go | 43 ++++++++++++++++++++++++++++---------------
utils/vhost/vhost.go | 5 +++--
2 files changed, 31 insertions(+), 17 deletions(-)
diff --git a/utils/vhost/http.go b/utils/vhost/http.go
index 2e8208cf..5ecce03c 100644
--- a/utils/vhost/http.go
+++ b/utils/vhost/http.go
@@ -57,30 +57,31 @@ func GetHttpRequestInfo(c frpNet.Conn) (_ frpNet.Conn, _ map[string]string, err
}
func NewHttpMuxer(listener frpNet.Listener, timeout time.Duration) (*HttpMuxer, error) {
- mux, err := NewVhostMuxer(listener, GetHttpRequestInfo, HttpAuthFunc, HttpHostNameRewrite, timeout)
+ mux, err := NewVhostMuxer(listener, GetHttpRequestInfo, HttpAuthFunc, ModifyHttpRequest, timeout)
return &HttpMuxer{mux}, err
}
-func HttpHostNameRewrite(c frpNet.Conn, rewriteHost string) (_ frpNet.Conn, err error) {
+func ModifyHttpRequest(c frpNet.Conn, rewriteHost string) (_ frpNet.Conn, err error) {
sc, rd := frpNet.NewShareConn(c)
var buff []byte
- if buff, err = hostNameRewrite(rd, rewriteHost); err != nil {
+ remoteIP := strings.Split(c.RemoteAddr().String(), ":")[0]
+ if buff, err = hostNameRewrite(rd, rewriteHost, remoteIP); err != nil {
return sc, err
}
err = sc.WriteBuff(buff)
return sc, err
}
-func hostNameRewrite(request io.Reader, rewriteHost string) (_ []byte, err error) {
+func hostNameRewrite(request io.Reader, rewriteHost string, remoteIP string) (_ []byte, err error) {
buf := pool.GetBuf(1024)
defer pool.PutBuf(buf)
request.Read(buf)
- retBuffer, err := parseRequest(buf, rewriteHost)
+ retBuffer, err := parseRequest(buf, rewriteHost, remoteIP)
return retBuffer, err
}
-func parseRequest(org []byte, rewriteHost string) (ret []byte, err error) {
+func parseRequest(org []byte, rewriteHost string, remoteIP string) (ret []byte, err error) {
tp := bytes.NewBuffer(org)
// First line: GET /index.html HTTP/1.0
var b []byte
@@ -106,10 +107,19 @@ func parseRequest(org []byte, rewriteHost string) (ret []byte, err error) {
// GET /index.html HTTP/1.1
// Host: www.google.com
if req.URL.Host == "" {
- changedBuf, err := changeHostName(tp, rewriteHost)
+ var changedBuf []byte
+ if rewriteHost != "" {
+ changedBuf, err = changeHostName(tp, rewriteHost)
+ }
buf := new(bytes.Buffer)
buf.Write(b)
- buf.Write(changedBuf)
+ buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\n", remoteIP))
+ buf.WriteString(fmt.Sprintf("X-Real-IP: %s\n", remoteIP))
+ if len(changedBuf) == 0 {
+ tp.WriteTo(buf)
+ } else {
+ buf.Write(changedBuf)
+ }
return buf.Bytes(), err
}
@@ -117,18 +127,21 @@ func parseRequest(org []byte, rewriteHost string) (ret []byte, err error) {
// GET http://www.google.com/index.html HTTP/1.1
// Host: doesntmatter
// In this case, any Host line is ignored.
- hostPort := strings.Split(req.URL.Host, ":")
- if len(hostPort) == 1 {
- req.URL.Host = rewriteHost
- } else if len(hostPort) == 2 {
- req.URL.Host = fmt.Sprintf("%s:%s", rewriteHost, hostPort[1])
+ if rewriteHost != "" {
+ hostPort := strings.Split(req.URL.Host, ":")
+ if len(hostPort) == 1 {
+ req.URL.Host = rewriteHost
+ } else if len(hostPort) == 2 {
+ req.URL.Host = fmt.Sprintf("%s:%s", rewriteHost, hostPort[1])
+ }
}
firstLine := req.Method + " " + req.URL.String() + " " + req.Proto
buf := new(bytes.Buffer)
buf.WriteString(firstLine)
+ buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\n", remoteIP))
+ buf.WriteString(fmt.Sprintf("X-Real-IP: %s\n", remoteIP))
tp.WriteTo(buf)
return buf.Bytes(), err
-
}
// parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
@@ -164,7 +177,7 @@ func changeHostName(buff *bytes.Buffer, rewriteHost string) (_ []byte, err error
if portPos == -1 {
hostHeader = fmt.Sprintf("Host: %s\n", rewriteHost)
} else {
- hostHeader = fmt.Sprintf("Host: %s:%s\n", rewriteHost, kv[portPos+1:])
+ hostHeader = fmt.Sprintf("Host: %s:%s\n", rewriteHost, kv[j+portPos+2:])
}
retBuf.WriteString(hostHeader)
peek = peek[i+1:]
diff --git a/utils/vhost/vhost.go b/utils/vhost/vhost.go
index 21771d04..bb2b4ad5 100644
--- a/utils/vhost/vhost.go
+++ b/utils/vhost/vhost.go
@@ -182,9 +182,10 @@ func (l *Listener) Accept() (frpNet.Conn, error) {
return nil, fmt.Errorf("Listener closed")
}
- // if rewriteFunc is exist and rewriteHost is set
+ // if rewriteFunc is exist
// rewrite http requests with a modified host header
- if l.mux.rewriteFunc != nil && l.rewriteHost != "" {
+ // if l.rewriteHost is empty, nothing to do
+ if l.mux.rewriteFunc != nil {
sConn, err := l.mux.rewriteFunc(conn, l.rewriteHost)
if err != nil {
l.Warn("host header rewrite failed: %v", err)
From 0752508469d81786bf8c5a7b0adb42125906f613 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Sat, 1 Jul 2017 12:13:44 +0800
Subject: [PATCH 09/19] vhost: a bug fix of reading request
---
utils/vhost/http.go | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/utils/vhost/http.go b/utils/vhost/http.go
index 5ecce03c..5ecee78c 100644
--- a/utils/vhost/http.go
+++ b/utils/vhost/http.go
@@ -76,8 +76,12 @@ func hostNameRewrite(request io.Reader, rewriteHost string, remoteIP string) (_
buf := pool.GetBuf(1024)
defer pool.PutBuf(buf)
- request.Read(buf)
- retBuffer, err := parseRequest(buf, rewriteHost, remoteIP)
+ var n int
+ n, err = request.Read(buf)
+ if err != nil {
+ return
+ }
+ retBuffer, err := parseRequest(buf[:n], rewriteHost, remoteIP)
return retBuffer, err
}
@@ -113,8 +117,8 @@ func parseRequest(org []byte, rewriteHost string, remoteIP string) (ret []byte,
}
buf := new(bytes.Buffer)
buf.Write(b)
- buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\n", remoteIP))
- buf.WriteString(fmt.Sprintf("X-Real-IP: %s\n", remoteIP))
+ buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\r\n", remoteIP))
+ buf.WriteString(fmt.Sprintf("X-Real-IP: %s\r\n", remoteIP))
if len(changedBuf) == 0 {
tp.WriteTo(buf)
} else {
@@ -138,8 +142,8 @@ func parseRequest(org []byte, rewriteHost string, remoteIP string) (ret []byte,
firstLine := req.Method + " " + req.URL.String() + " " + req.Proto
buf := new(bytes.Buffer)
buf.WriteString(firstLine)
- buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\n", remoteIP))
- buf.WriteString(fmt.Sprintf("X-Real-IP: %s\n", remoteIP))
+ buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\r\n", remoteIP))
+ buf.WriteString(fmt.Sprintf("X-Real-IP: %s\r\n", remoteIP))
tp.WriteTo(buf)
return buf.Bytes(), err
}
@@ -175,9 +179,9 @@ func changeHostName(buff *bytes.Buffer, rewriteHost string) (_ []byte, err error
var hostHeader string
portPos := bytes.IndexByte(kv[j+1:], ':')
if portPos == -1 {
- hostHeader = fmt.Sprintf("Host: %s\n", rewriteHost)
+ hostHeader = fmt.Sprintf("Host: %s\r\n", rewriteHost)
} else {
- hostHeader = fmt.Sprintf("Host: %s:%s\n", rewriteHost, kv[j+portPos+2:])
+ hostHeader = fmt.Sprintf("Host: %s:%s\r\n", rewriteHost, kv[j+portPos+2:])
}
retBuf.WriteString(hostHeader)
peek = peek[i+1:]
From 7bb8c1064755c0c6ab08922afba51c4422602e31 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Sat, 1 Jul 2017 15:56:48 +0800
Subject: [PATCH 10/19] plugin: add socks5 plugin
---
models/plugin/socks5.go | 65 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
create mode 100644 models/plugin/socks5.go
diff --git a/models/plugin/socks5.go b/models/plugin/socks5.go
new file mode 100644
index 00000000..d3b82e12
--- /dev/null
+++ b/models/plugin/socks5.go
@@ -0,0 +1,65 @@
+// Copyright 2017 fatedier, fatedier@gmail.com
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package plugin
+
+import (
+ "io"
+ "io/ioutil"
+ "log"
+
+ frpNet "github.com/fatedier/frp/utils/net"
+
+ gosocks5 "github.com/armon/go-socks5"
+)
+
+const PluginSocks5 = "socks5"
+
+func init() {
+ Register(PluginSocks5, NewSocks5Plugin)
+}
+
+type Socks5Plugin struct {
+ Server *gosocks5.Server
+}
+
+func NewSocks5Plugin(params map[string]string) (p Plugin, err error) {
+ sp := &Socks5Plugin{}
+ sp.Server, err = gosocks5.New(&gosocks5.Config{
+ Logger: log.New(ioutil.Discard, "", log.LstdFlags),
+ })
+ p = sp
+ return
+}
+
+func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser) {
+ defer conn.Close()
+
+ var wrapConn frpNet.Conn
+ if realConn, ok := conn.(frpNet.Conn); ok {
+ wrapConn = realConn
+ } else {
+ wrapConn = frpNet.WrapReadWriteCloserToConn(conn)
+ }
+
+ sp.Server.ServeConn(wrapConn)
+}
+
+func (sp *Socks5Plugin) Name() string {
+ return PluginSocks5
+}
+
+func (sp *Socks5Plugin) Close() error {
+ return nil
+}
From fe32a7c4bbfa1c694a537c26740c5440c0751c75 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Sat, 1 Jul 2017 16:03:13 +0800
Subject: [PATCH 11/19] doc: update
---
README.md | 6 +++---
README_zh.md | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 20254e6b..f7087e7a 100644
--- a/README.md
+++ b/README.md
@@ -237,14 +237,14 @@ Configure frps same as above.
[http_proxy]
type = tcp
remote_port = 6000
- plugin = http_proxy
+ plugin = http_proxy # or socks5
```
4. Start frpc:
`./frpc -c ./frpc.ini`
-5. Set http proxy `x.x.x.x:6000` in your browser and visit website through frpc's network.
+5. Set http proxy or socks5 proxy `x.x.x.x:6000` in your browser and visit website through frpc's network.
## Features
@@ -469,7 +469,7 @@ http_proxy = http://user:pwd@192.168.1.128:8080
frpc only forward request to local tcp or udp port by default.
-Plugin is used for providing rich features. There are built-in plugins such as **unix_domain_socket**, **http_proxy** and you can see [example usage](#example-usage).
+Plugin is used for providing rich features. There are built-in plugins such as **unix_domain_socket**, **http_proxy**, **socks5** and you can see [example usage](#example-usage).
Specify which plugin to use by `plugin` parameter. Configuration parameters of plugin should be started with `plugin_`. `local_ip` and `local_port` is useless for plugin.
diff --git a/README_zh.md b/README_zh.md
index 504d2da1..5f0a09ba 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -225,11 +225,11 @@ DNS 查询请求通常使用 UDP 协议,frp 支持对内网 UDP 服务的穿
### 通过 frpc 所在机器访问外网
-frpc 内置了 http proxy 插件,可以使其他机器通过 frpc 的网络访问互联网。
+frpc 内置了 http proxy 和 socks5 插件,可以使其他机器通过 frpc 的网络访问互联网。
frps 的部署步骤同上。
-1. 修改 frpc.ini 文件,启用 http_proxy 插件:
+1. 修改 frpc.ini 文件,启用 http_proxy 或 socks5 插件(plugin 换为 socks5 即可):
```ini
# frpc.ini
@@ -247,7 +247,7 @@ frps 的部署步骤同上。
`./frpc -c ./frpc.ini`
-5. 浏览器设置 http 代理地址为 `x.x.x.x:6000`,通过 frpc 机器的网络访问互联网。
+5. 浏览器设置 http 或 socks5 代理地址为 `x.x.x.x:6000`,通过 frpc 机器的网络访问互联网。
## 功能说明
@@ -486,7 +486,7 @@ http_proxy = http://user:pwd@192.168.1.128:8080
默认情况下,frpc 只会转发请求到本地 tcp 或 udp 端口。
-插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 **unix_domain_socket**、**http_proxy**。具体使用方式请查看[使用示例](#使用示例)。
+插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 **unix_domain_socket**、**http_proxy**、**socks5**。具体使用方式请查看[使用示例](#使用示例)。
通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port 不再需要配置。
From c081df40e1417bbb2b54b96c52a997f548b8d3b3 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Sat, 1 Jul 2017 16:09:09 +0800
Subject: [PATCH 12/19] vendor: add github.com/armon/go-socks5
---
Godeps/Godeps.json | 8 +
vendor/github.com/armon/go-socks5/.gitignore | 22 ++
vendor/github.com/armon/go-socks5/.travis.yml | 4 +
vendor/github.com/armon/go-socks5/LICENSE | 20 +
vendor/github.com/armon/go-socks5/README.md | 45 +++
vendor/github.com/armon/go-socks5/auth.go | 151 ++++++++
.../github.com/armon/go-socks5/credentials.go | 17 +
vendor/github.com/armon/go-socks5/request.go | 364 ++++++++++++++++++
vendor/github.com/armon/go-socks5/resolver.go | 23 ++
vendor/github.com/armon/go-socks5/ruleset.go | 41 ++
vendor/github.com/armon/go-socks5/socks5.go | 169 ++++++++
vendor/golang.org/x/net/context/context.go | 156 ++++++++
vendor/golang.org/x/net/context/go17.go | 72 ++++
vendor/golang.org/x/net/context/pre_go17.go | 300 +++++++++++++++
14 files changed, 1392 insertions(+)
create mode 100644 vendor/github.com/armon/go-socks5/.gitignore
create mode 100644 vendor/github.com/armon/go-socks5/.travis.yml
create mode 100644 vendor/github.com/armon/go-socks5/LICENSE
create mode 100644 vendor/github.com/armon/go-socks5/README.md
create mode 100644 vendor/github.com/armon/go-socks5/auth.go
create mode 100644 vendor/github.com/armon/go-socks5/credentials.go
create mode 100644 vendor/github.com/armon/go-socks5/request.go
create mode 100644 vendor/github.com/armon/go-socks5/resolver.go
create mode 100644 vendor/github.com/armon/go-socks5/ruleset.go
create mode 100644 vendor/github.com/armon/go-socks5/socks5.go
create mode 100644 vendor/golang.org/x/net/context/context.go
create mode 100644 vendor/golang.org/x/net/context/go17.go
create mode 100644 vendor/golang.org/x/net/context/pre_go17.go
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index 7a6edc9e..52dc95b4 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -6,6 +6,10 @@
"./..."
],
"Deps": [
+ {
+ "ImportPath": "github.com/armon/go-socks5",
+ "Rev": "e75332964ef517daa070d7c38a9466a0d687e0a5"
+ },
{
"ImportPath": "github.com/davecgh/go-spew/spew",
"Comment": "v1.1.0",
@@ -110,6 +114,10 @@
"ImportPath": "golang.org/x/net/bpf",
"Rev": "e4fa1c5465ad6111f206fc92186b8c83d64adbe1"
},
+ {
+ "ImportPath": "golang.org/x/net/context",
+ "Rev": "e4fa1c5465ad6111f206fc92186b8c83d64adbe1"
+ },
{
"ImportPath": "golang.org/x/net/internal/iana",
"Rev": "e4fa1c5465ad6111f206fc92186b8c83d64adbe1"
diff --git a/vendor/github.com/armon/go-socks5/.gitignore b/vendor/github.com/armon/go-socks5/.gitignore
new file mode 100644
index 00000000..00268614
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/.gitignore
@@ -0,0 +1,22 @@
+# 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
new file mode 100644
index 00000000..8d61700e
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/.travis.yml
@@ -0,0 +1,4 @@
+language: go
+go:
+ - 1.1
+ - tip
diff --git a/vendor/github.com/armon/go-socks5/LICENSE b/vendor/github.com/armon/go-socks5/LICENSE
new file mode 100644
index 00000000..a5df10e6
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/LICENSE
@@ -0,0 +1,20 @@
+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
new file mode 100644
index 00000000..9cd15635
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/README.md
@@ -0,0 +1,45 @@
+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
new file mode 100644
index 00000000..7811e2aa
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/auth.go
@@ -0,0 +1,151 @@
+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
new file mode 100644
index 00000000..96664273
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/credentials.go
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 00000000..b615fcbe
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/request.go
@@ -0,0 +1,364 @@
+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
new file mode 100644
index 00000000..b75a5c4d
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/resolver.go
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 00000000..ba0e3538
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/ruleset.go
@@ -0,0 +1,41 @@
+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
new file mode 100644
index 00000000..a17be68f
--- /dev/null
+++ b/vendor/github.com/armon/go-socks5/socks5.go
@@ -0,0 +1,169 @@
+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/golang.org/x/net/context/context.go b/vendor/golang.org/x/net/context/context.go
new file mode 100644
index 00000000..27dcb951
--- /dev/null
+++ b/vendor/golang.org/x/net/context/context.go
@@ -0,0 +1,156 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package context defines the Context type, which carries deadlines,
+// cancelation signals, and other request-scoped values across API boundaries
+// and between processes.
+//
+// Incoming requests to a server should create a Context, and outgoing calls to
+// servers should accept a Context. The chain of function calls between must
+// propagate the Context, optionally replacing it with a modified copy created
+// using WithDeadline, WithTimeout, WithCancel, or WithValue.
+//
+// Programs that use Contexts should follow these rules to keep interfaces
+// consistent across packages and enable static analysis tools to check context
+// propagation:
+//
+// Do not store Contexts inside a struct type; instead, pass a Context
+// explicitly to each function that needs it. The Context should be the first
+// parameter, typically named ctx:
+//
+// func DoSomething(ctx context.Context, arg Arg) error {
+// // ... use ctx ...
+// }
+//
+// Do not pass a nil Context, even if a function permits it. Pass context.TODO
+// if you are unsure about which Context to use.
+//
+// Use context Values only for request-scoped data that transits processes and
+// APIs, not for passing optional parameters to functions.
+//
+// The same Context may be passed to functions running in different goroutines;
+// Contexts are safe for simultaneous use by multiple goroutines.
+//
+// See http://blog.golang.org/context for example code for a server that uses
+// Contexts.
+package context
+
+import "time"
+
+// A Context carries a deadline, a cancelation signal, and other values across
+// API boundaries.
+//
+// Context's methods may be called by multiple goroutines simultaneously.
+type Context interface {
+ // Deadline returns the time when work done on behalf of this context
+ // should be canceled. Deadline returns ok==false when no deadline is
+ // set. Successive calls to Deadline return the same results.
+ Deadline() (deadline time.Time, ok bool)
+
+ // Done returns a channel that's closed when work done on behalf of this
+ // context should be canceled. Done may return nil if this context can
+ // never be canceled. Successive calls to Done return the same value.
+ //
+ // WithCancel arranges for Done to be closed when cancel is called;
+ // WithDeadline arranges for Done to be closed when the deadline
+ // expires; WithTimeout arranges for Done to be closed when the timeout
+ // elapses.
+ //
+ // Done is provided for use in select statements:
+ //
+ // // Stream generates values with DoSomething and sends them to out
+ // // until DoSomething returns an error or ctx.Done is closed.
+ // func Stream(ctx context.Context, out chan<- Value) error {
+ // for {
+ // v, err := DoSomething(ctx)
+ // if err != nil {
+ // return err
+ // }
+ // select {
+ // case <-ctx.Done():
+ // return ctx.Err()
+ // case out <- v:
+ // }
+ // }
+ // }
+ //
+ // See http://blog.golang.org/pipelines for more examples of how to use
+ // a Done channel for cancelation.
+ Done() <-chan struct{}
+
+ // Err returns a non-nil error value after Done is closed. Err returns
+ // Canceled if the context was canceled or DeadlineExceeded if the
+ // context's deadline passed. No other values for Err are defined.
+ // After Done is closed, successive calls to Err return the same value.
+ Err() error
+
+ // Value returns the value associated with this context for key, or nil
+ // if no value is associated with key. Successive calls to Value with
+ // the same key returns the same result.
+ //
+ // Use context values only for request-scoped data that transits
+ // processes and API boundaries, not for passing optional parameters to
+ // functions.
+ //
+ // A key identifies a specific value in a Context. Functions that wish
+ // to store values in Context typically allocate a key in a global
+ // variable then use that key as the argument to context.WithValue and
+ // Context.Value. A key can be any type that supports equality;
+ // packages should define keys as an unexported type to avoid
+ // collisions.
+ //
+ // Packages that define a Context key should provide type-safe accessors
+ // for the values stores using that key:
+ //
+ // // Package user defines a User type that's stored in Contexts.
+ // package user
+ //
+ // import "golang.org/x/net/context"
+ //
+ // // User is the type of value stored in the Contexts.
+ // type User struct {...}
+ //
+ // // key is an unexported type for keys defined in this package.
+ // // This prevents collisions with keys defined in other packages.
+ // type key int
+ //
+ // // userKey is the key for user.User values in Contexts. It is
+ // // unexported; clients use user.NewContext and user.FromContext
+ // // instead of using this key directly.
+ // var userKey key = 0
+ //
+ // // NewContext returns a new Context that carries value u.
+ // func NewContext(ctx context.Context, u *User) context.Context {
+ // return context.WithValue(ctx, userKey, u)
+ // }
+ //
+ // // FromContext returns the User value stored in ctx, if any.
+ // func FromContext(ctx context.Context) (*User, bool) {
+ // u, ok := ctx.Value(userKey).(*User)
+ // return u, ok
+ // }
+ Value(key interface{}) interface{}
+}
+
+// Background returns a non-nil, empty Context. It is never canceled, has no
+// values, and has no deadline. It is typically used by the main function,
+// initialization, and tests, and as the top-level Context for incoming
+// requests.
+func Background() Context {
+ return background
+}
+
+// TODO returns a non-nil, empty Context. Code should use context.TODO when
+// it's unclear which Context to use or it is not yet available (because the
+// surrounding function has not yet been extended to accept a Context
+// parameter). TODO is recognized by static analysis tools that determine
+// whether Contexts are propagated correctly in a program.
+func TODO() Context {
+ return todo
+}
+
+// A CancelFunc tells an operation to abandon its work.
+// A CancelFunc does not wait for the work to stop.
+// After the first call, subsequent calls to a CancelFunc do nothing.
+type CancelFunc func()
diff --git a/vendor/golang.org/x/net/context/go17.go b/vendor/golang.org/x/net/context/go17.go
new file mode 100644
index 00000000..d20f52b7
--- /dev/null
+++ b/vendor/golang.org/x/net/context/go17.go
@@ -0,0 +1,72 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.7
+
+package context
+
+import (
+ "context" // standard library's context, as of Go 1.7
+ "time"
+)
+
+var (
+ todo = context.TODO()
+ background = context.Background()
+)
+
+// Canceled is the error returned by Context.Err when the context is canceled.
+var Canceled = context.Canceled
+
+// DeadlineExceeded is the error returned by Context.Err when the context's
+// deadline passes.
+var DeadlineExceeded = context.DeadlineExceeded
+
+// WithCancel returns a copy of parent with a new Done channel. The returned
+// context's Done channel is closed when the returned cancel function is called
+// or when the parent context's Done channel is closed, whichever happens first.
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this Context complete.
+func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
+ ctx, f := context.WithCancel(parent)
+ return ctx, CancelFunc(f)
+}
+
+// WithDeadline returns a copy of the parent context with the deadline adjusted
+// to be no later than d. If the parent's deadline is already earlier than d,
+// WithDeadline(parent, d) is semantically equivalent to parent. The returned
+// context's Done channel is closed when the deadline expires, when the returned
+// cancel function is called, or when the parent context's Done channel is
+// closed, whichever happens first.
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this Context complete.
+func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
+ ctx, f := context.WithDeadline(parent, deadline)
+ return ctx, CancelFunc(f)
+}
+
+// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this Context complete:
+//
+// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
+// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
+// defer cancel() // releases resources if slowOperation completes before timeout elapses
+// return slowOperation(ctx)
+// }
+func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
+ return WithDeadline(parent, time.Now().Add(timeout))
+}
+
+// WithValue returns a copy of parent in which the value associated with key is
+// val.
+//
+// Use context Values only for request-scoped data that transits processes and
+// APIs, not for passing optional parameters to functions.
+func WithValue(parent Context, key interface{}, val interface{}) Context {
+ return context.WithValue(parent, key, val)
+}
diff --git a/vendor/golang.org/x/net/context/pre_go17.go b/vendor/golang.org/x/net/context/pre_go17.go
new file mode 100644
index 00000000..0f35592d
--- /dev/null
+++ b/vendor/golang.org/x/net/context/pre_go17.go
@@ -0,0 +1,300 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !go1.7
+
+package context
+
+import (
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+)
+
+// An emptyCtx is never canceled, has no values, and has no deadline. It is not
+// struct{}, since vars of this type must have distinct addresses.
+type emptyCtx int
+
+func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
+ return
+}
+
+func (*emptyCtx) Done() <-chan struct{} {
+ return nil
+}
+
+func (*emptyCtx) Err() error {
+ return nil
+}
+
+func (*emptyCtx) Value(key interface{}) interface{} {
+ return nil
+}
+
+func (e *emptyCtx) String() string {
+ switch e {
+ case background:
+ return "context.Background"
+ case todo:
+ return "context.TODO"
+ }
+ return "unknown empty Context"
+}
+
+var (
+ background = new(emptyCtx)
+ todo = new(emptyCtx)
+)
+
+// Canceled is the error returned by Context.Err when the context is canceled.
+var Canceled = errors.New("context canceled")
+
+// DeadlineExceeded is the error returned by Context.Err when the context's
+// deadline passes.
+var DeadlineExceeded = errors.New("context deadline exceeded")
+
+// WithCancel returns a copy of parent with a new Done channel. The returned
+// context's Done channel is closed when the returned cancel function is called
+// or when the parent context's Done channel is closed, whichever happens first.
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this Context complete.
+func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
+ c := newCancelCtx(parent)
+ propagateCancel(parent, c)
+ return c, func() { c.cancel(true, Canceled) }
+}
+
+// newCancelCtx returns an initialized cancelCtx.
+func newCancelCtx(parent Context) *cancelCtx {
+ return &cancelCtx{
+ Context: parent,
+ done: make(chan struct{}),
+ }
+}
+
+// propagateCancel arranges for child to be canceled when parent is.
+func propagateCancel(parent Context, child canceler) {
+ if parent.Done() == nil {
+ return // parent is never canceled
+ }
+ if p, ok := parentCancelCtx(parent); ok {
+ p.mu.Lock()
+ if p.err != nil {
+ // parent has already been canceled
+ child.cancel(false, p.err)
+ } else {
+ if p.children == nil {
+ p.children = make(map[canceler]bool)
+ }
+ p.children[child] = true
+ }
+ p.mu.Unlock()
+ } else {
+ go func() {
+ select {
+ case <-parent.Done():
+ child.cancel(false, parent.Err())
+ case <-child.Done():
+ }
+ }()
+ }
+}
+
+// parentCancelCtx follows a chain of parent references until it finds a
+// *cancelCtx. This function understands how each of the concrete types in this
+// package represents its parent.
+func parentCancelCtx(parent Context) (*cancelCtx, bool) {
+ for {
+ switch c := parent.(type) {
+ case *cancelCtx:
+ return c, true
+ case *timerCtx:
+ return c.cancelCtx, true
+ case *valueCtx:
+ parent = c.Context
+ default:
+ return nil, false
+ }
+ }
+}
+
+// removeChild removes a context from its parent.
+func removeChild(parent Context, child canceler) {
+ p, ok := parentCancelCtx(parent)
+ if !ok {
+ return
+ }
+ p.mu.Lock()
+ if p.children != nil {
+ delete(p.children, child)
+ }
+ p.mu.Unlock()
+}
+
+// A canceler is a context type that can be canceled directly. The
+// implementations are *cancelCtx and *timerCtx.
+type canceler interface {
+ cancel(removeFromParent bool, err error)
+ Done() <-chan struct{}
+}
+
+// A cancelCtx can be canceled. When canceled, it also cancels any children
+// that implement canceler.
+type cancelCtx struct {
+ Context
+
+ done chan struct{} // closed by the first cancel call.
+
+ mu sync.Mutex
+ children map[canceler]bool // set to nil by the first cancel call
+ err error // set to non-nil by the first cancel call
+}
+
+func (c *cancelCtx) Done() <-chan struct{} {
+ return c.done
+}
+
+func (c *cancelCtx) Err() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.err
+}
+
+func (c *cancelCtx) String() string {
+ return fmt.Sprintf("%v.WithCancel", c.Context)
+}
+
+// cancel closes c.done, cancels each of c's children, and, if
+// removeFromParent is true, removes c from its parent's children.
+func (c *cancelCtx) cancel(removeFromParent bool, err error) {
+ if err == nil {
+ panic("context: internal error: missing cancel error")
+ }
+ c.mu.Lock()
+ if c.err != nil {
+ c.mu.Unlock()
+ return // already canceled
+ }
+ c.err = err
+ close(c.done)
+ for child := range c.children {
+ // NOTE: acquiring the child's lock while holding parent's lock.
+ child.cancel(false, err)
+ }
+ c.children = nil
+ c.mu.Unlock()
+
+ if removeFromParent {
+ removeChild(c.Context, c)
+ }
+}
+
+// WithDeadline returns a copy of the parent context with the deadline adjusted
+// to be no later than d. If the parent's deadline is already earlier than d,
+// WithDeadline(parent, d) is semantically equivalent to parent. The returned
+// context's Done channel is closed when the deadline expires, when the returned
+// cancel function is called, or when the parent context's Done channel is
+// closed, whichever happens first.
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this Context complete.
+func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
+ if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
+ // The current deadline is already sooner than the new one.
+ return WithCancel(parent)
+ }
+ c := &timerCtx{
+ cancelCtx: newCancelCtx(parent),
+ deadline: deadline,
+ }
+ propagateCancel(parent, c)
+ d := deadline.Sub(time.Now())
+ if d <= 0 {
+ c.cancel(true, DeadlineExceeded) // deadline has already passed
+ return c, func() { c.cancel(true, Canceled) }
+ }
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.err == nil {
+ c.timer = time.AfterFunc(d, func() {
+ c.cancel(true, DeadlineExceeded)
+ })
+ }
+ return c, func() { c.cancel(true, Canceled) }
+}
+
+// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
+// implement Done and Err. It implements cancel by stopping its timer then
+// delegating to cancelCtx.cancel.
+type timerCtx struct {
+ *cancelCtx
+ timer *time.Timer // Under cancelCtx.mu.
+
+ deadline time.Time
+}
+
+func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
+ return c.deadline, true
+}
+
+func (c *timerCtx) String() string {
+ return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
+}
+
+func (c *timerCtx) cancel(removeFromParent bool, err error) {
+ c.cancelCtx.cancel(false, err)
+ if removeFromParent {
+ // Remove this timerCtx from its parent cancelCtx's children.
+ removeChild(c.cancelCtx.Context, c)
+ }
+ c.mu.Lock()
+ if c.timer != nil {
+ c.timer.Stop()
+ c.timer = nil
+ }
+ c.mu.Unlock()
+}
+
+// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this Context complete:
+//
+// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
+// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
+// defer cancel() // releases resources if slowOperation completes before timeout elapses
+// return slowOperation(ctx)
+// }
+func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
+ return WithDeadline(parent, time.Now().Add(timeout))
+}
+
+// WithValue returns a copy of parent in which the value associated with key is
+// val.
+//
+// Use context Values only for request-scoped data that transits processes and
+// APIs, not for passing optional parameters to functions.
+func WithValue(parent Context, key interface{}, val interface{}) Context {
+ return &valueCtx{parent, key, val}
+}
+
+// A valueCtx carries a key-value pair. It implements Value for that key and
+// delegates all other calls to the embedded Context.
+type valueCtx struct {
+ Context
+ key, val interface{}
+}
+
+func (c *valueCtx) String() string {
+ return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
+}
+
+func (c *valueCtx) Value(key interface{}) interface{} {
+ if c.key == key {
+ return c.val
+ }
+ return c.Context.Value(key)
+}
From 9d9416ab948037b939fdad8aa06bb0ea75043986 Mon Sep 17 00:00:00 2001
From: Lukaz
Date: Tue, 4 Jul 2017 23:05:24 +0800
Subject: [PATCH 13/19] Update README.md
fix a typo
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 20254e6b..005f7498 100644
--- a/README.md
+++ b/README.md
@@ -505,7 +505,7 @@ Interested in getting involved? We would like to help you!
* Take a look at our [issues list](https://github.com/fatedier/frp/issues) and consider sending a Pull Request to **dev branch**.
* If you want to add a new feature, please create an issue first to describe the new feature, as well as the implementation approach. Once a proposal is accepted, create an implementation of the new features and submit it as a pull request.
* Sorry for my poor english and improvement for this document is welcome even some typo fix.
-* If you have some wanderful ideas, send email to fatedier@gmail.com.
+* If you have some wonderful ideas, send email to fatedier@gmail.com.
**Note: We prefer you to give your advise in [issues](https://github.com/fatedier/frp/issues), so others with a same question can search it quickly and we don't need to answer them repeatly.**
From f63a4f0cddf5c350ae12e8dd348a8fd3eb34a480 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Wed, 5 Jul 2017 01:40:01 +0800
Subject: [PATCH 14/19] frps: new parameter 'proxy_bind_addr'
---
conf/frps_full.ini | 3 +++
models/config/server_common.go | 17 +++++++++++++----
server/proxy.go | 4 ++--
server/service.go | 8 ++++----
4 files changed, 22 insertions(+), 10 deletions(-)
diff --git a/conf/frps_full.ini b/conf/frps_full.ini
index fa728383..446d86aa 100644
--- a/conf/frps_full.ini
+++ b/conf/frps_full.ini
@@ -9,6 +9,9 @@ bind_port = 7000
# if not set, kcp is disabled in frps
kcp_bind_port = 7000
+# specify which address proxy will listen for, default value is same with bind_addr
+# proxy_bind_addr = 127.0.0.1
+
# if you want to support virtual host, you must set the http port for listening (optional)
vhost_http_port = 80
vhost_https_port = 443
diff --git a/models/config/server_common.go b/models/config/server_common.go
index 1bf1f256..1795a2a5 100644
--- a/models/config/server_common.go
+++ b/models/config/server_common.go
@@ -27,10 +27,11 @@ var ServerCommonCfg *ServerCommonConf
// common config
type ServerCommonConf struct {
- ConfigFile string
- BindAddr string
- BindPort int64
- KcpBindPort int64
+ ConfigFile string
+ BindAddr string
+ BindPort int64
+ KcpBindPort int64
+ ProxyBindAddr string
// If VhostHttpPort equals 0, don't listen a public port for http protocol.
VhostHttpPort int64
@@ -66,6 +67,7 @@ func GetDefaultServerCommonConf() *ServerCommonConf {
BindAddr: "0.0.0.0",
BindPort: 7000,
KcpBindPort: 0,
+ ProxyBindAddr: "0.0.0.0",
VhostHttpPort: 0,
VhostHttpsPort: 0,
DashboardPort: 0,
@@ -117,6 +119,13 @@ func LoadServerCommonConf(conf ini.File) (cfg *ServerCommonConf, err error) {
}
}
+ tmpStr, ok = conf.Get("common", "proxy_bind_addr")
+ if ok {
+ cfg.ProxyBindAddr = tmpStr
+ } else {
+ cfg.ProxyBindAddr = cfg.BindAddr
+ }
+
tmpStr, ok = conf.Get("common", "vhost_http_port")
if ok {
cfg.VhostHttpPort, err = strconv.ParseInt(tmpStr, 10, 64)
diff --git a/server/proxy.go b/server/proxy.go
index ac8ddc07..ecc58751 100644
--- a/server/proxy.go
+++ b/server/proxy.go
@@ -161,7 +161,7 @@ type TcpProxy struct {
}
func (pxy *TcpProxy) Run() error {
- listener, err := frpNet.ListenTcp(config.ServerCommonCfg.BindAddr, pxy.cfg.RemotePort)
+ listener, err := frpNet.ListenTcp(config.ServerCommonCfg.ProxyBindAddr, pxy.cfg.RemotePort)
if err != nil {
return err
}
@@ -330,7 +330,7 @@ type UdpProxy struct {
}
func (pxy *UdpProxy) Run() (err error) {
- addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", config.ServerCommonCfg.BindAddr, pxy.cfg.RemotePort))
+ addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", config.ServerCommonCfg.ProxyBindAddr, pxy.cfg.RemotePort))
if err != nil {
return err
}
diff --git a/server/service.go b/server/service.go
index 78e7762e..e06447dc 100644
--- a/server/service.go
+++ b/server/service.go
@@ -95,7 +95,7 @@ func NewService() (svr *Service, err error) {
// Create http vhost muxer.
if config.ServerCommonCfg.VhostHttpPort > 0 {
var l frpNet.Listener
- l, err = frpNet.ListenTcp(config.ServerCommonCfg.BindAddr, config.ServerCommonCfg.VhostHttpPort)
+ l, err = frpNet.ListenTcp(config.ServerCommonCfg.ProxyBindAddr, config.ServerCommonCfg.VhostHttpPort)
if err != nil {
err = fmt.Errorf("Create vhost http listener error, %v", err)
return
@@ -105,13 +105,13 @@ func NewService() (svr *Service, err error) {
err = fmt.Errorf("Create vhost httpMuxer error, %v", err)
return
}
- log.Info("http service listen on %s:%d", config.ServerCommonCfg.BindAddr, config.ServerCommonCfg.VhostHttpPort)
+ log.Info("http service listen on %s:%d", config.ServerCommonCfg.ProxyBindAddr, config.ServerCommonCfg.VhostHttpPort)
}
// Create https vhost muxer.
if config.ServerCommonCfg.VhostHttpsPort > 0 {
var l frpNet.Listener
- l, err = frpNet.ListenTcp(config.ServerCommonCfg.BindAddr, config.ServerCommonCfg.VhostHttpsPort)
+ l, err = frpNet.ListenTcp(config.ServerCommonCfg.ProxyBindAddr, config.ServerCommonCfg.VhostHttpsPort)
if err != nil {
err = fmt.Errorf("Create vhost https listener error, %v", err)
return
@@ -121,7 +121,7 @@ func NewService() (svr *Service, err error) {
err = fmt.Errorf("Create vhost httpsMuxer error, %v", err)
return
}
- log.Info("https service listen on %s:%d", config.ServerCommonCfg.BindAddr, config.ServerCommonCfg.VhostHttpsPort)
+ log.Info("https service listen on %s:%d", config.ServerCommonCfg.ProxyBindAddr, config.ServerCommonCfg.VhostHttpsPort)
}
// Create dashboard web server.
From d246400a71b51bfa33a057367c0a609cad4a4920 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Thu, 13 Jul 2017 02:20:49 +0800
Subject: [PATCH 15/19] frpc: add admin server for reload configure file
---
client/admin.go | 60 +++++++++++++++
client/admin_api.go | 78 +++++++++++++++++++
client/control.go | 107 ++++++++++++++++++++++++--
client/service.go | 13 +++-
cmd/frpc/main.go | 3 +-
conf/frpc_full.ini | 6 ++
conf/frps_full.ini | 2 +-
models/config/client_common.go | 36 ++++++++-
models/config/proxy.go | 133 ++++++++++++++++++++++++++++++++-
server/dashboard.go | 114 ++++------------------------
utils/net/http.go | 105 ++++++++++++++++++++++++++
11 files changed, 546 insertions(+), 111 deletions(-)
create mode 100644 client/admin.go
create mode 100644 client/admin_api.go
create mode 100644 utils/net/http.go
diff --git a/client/admin.go b/client/admin.go
new file mode 100644
index 00000000..f728483e
--- /dev/null
+++ b/client/admin.go
@@ -0,0 +1,60 @@
+// Copyright 2017 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 client
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "time"
+
+ "github.com/fatedier/frp/models/config"
+ frpNet "github.com/fatedier/frp/utils/net"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+var (
+ httpServerReadTimeout = 10 * time.Second
+ httpServerWriteTimeout = 10 * time.Second
+)
+
+func (svr *Service) RunAdminServer(addr string, port int64) (err error) {
+ // url router
+ router := httprouter.New()
+
+ user, passwd := config.ClientCommonCfg.AdminUser, config.ClientCommonCfg.AdminPwd
+
+ // api, see dashboard_api.go
+ router.GET("/api/reload", frpNet.HttprouterBasicAuth(svr.apiReload, user, passwd))
+
+ address := fmt.Sprintf("%s:%d", addr, port)
+ server := &http.Server{
+ Addr: address,
+ Handler: router,
+ ReadTimeout: httpServerReadTimeout,
+ WriteTimeout: httpServerWriteTimeout,
+ }
+ if address == "" {
+ address = ":http"
+ }
+ ln, err := net.Listen("tcp", address)
+ if err != nil {
+ return err
+ }
+
+ go server.Serve(ln)
+ return
+}
diff --git a/client/admin_api.go b/client/admin_api.go
new file mode 100644
index 00000000..72fae04e
--- /dev/null
+++ b/client/admin_api.go
@@ -0,0 +1,78 @@
+// Copyright 2017 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 client
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/julienschmidt/httprouter"
+ ini "github.com/vaughan0/go-ini"
+
+ "github.com/fatedier/frp/models/config"
+ "github.com/fatedier/frp/utils/log"
+)
+
+type GeneralResponse struct {
+ Code int64 `json:"code"`
+ Msg string `json:"msg"`
+}
+
+// api/reload
+type ReloadResp struct {
+ GeneralResponse
+}
+
+func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ var (
+ buf []byte
+ res ReloadResp
+ )
+ defer func() {
+ log.Info("Http response [/api/reload]: code [%d]", res.Code)
+ buf, _ = json.Marshal(&res)
+ w.Write(buf)
+ }()
+
+ log.Info("Http request: [/api/reload]")
+
+ conf, err := ini.LoadFile(config.ClientCommonCfg.ConfigFile)
+ if err != nil {
+ res.Code = 1
+ res.Msg = err.Error()
+ log.Error("reload frpc config file error: %v", err)
+ return
+ }
+
+ newCommonCfg, err := config.LoadClientCommonConf(conf)
+ if err != nil {
+ res.Code = 2
+ res.Msg = err.Error()
+ log.Error("reload frpc common section error: %v", err)
+ return
+ }
+
+ pxyCfgs, vistorCfgs, err := config.LoadProxyConfFromFile(newCommonCfg.User, conf, newCommonCfg.Start)
+ if err != nil {
+ res.Code = 3
+ res.Msg = err.Error()
+ log.Error("reload frpc proxy config error: %v", err)
+ return
+ }
+
+ svr.ctl.reloadConf(pxyCfgs, vistorCfgs)
+ log.Info("success reload conf")
+ return
+}
diff --git a/client/control.go b/client/control.go
index fb1ba716..29dca60c 100644
--- a/client/control.go
+++ b/client/control.go
@@ -388,7 +388,7 @@ func (ctl *Control) manager() {
ctl.Warn("[%s] start error: %s", m.ProxyName, m.Error)
continue
}
- cfg, ok := ctl.pxyCfgs[m.ProxyName]
+ cfg, ok := ctl.getProxyConf(m.ProxyName)
if !ok {
// it will never go to this branch now
ctl.Warn("[%s] no proxy conf found", m.ProxyName)
@@ -424,20 +424,36 @@ func (ctl *Control) controler() {
maxDelayTime := 30 * time.Second
delayTime := time.Second
- checkInterval := 30 * time.Second
+ checkInterval := 10 * time.Second
checkProxyTicker := time.NewTicker(checkInterval)
for {
select {
case <-checkProxyTicker.C:
- // Every 30 seconds, check which proxy registered failed and reregister it to server.
+ // Every 10 seconds, check which proxy registered failed and reregister it to server.
+ ctl.mu.RLock()
for _, cfg := range ctl.pxyCfgs {
- if _, exist := ctl.getProxy(cfg.GetName()); !exist {
- ctl.Info("try to reregister proxy [%s]", cfg.GetName())
+ if _, exist := ctl.proxies[cfg.GetName()]; !exist {
+ ctl.Info("try to register proxy [%s]", cfg.GetName())
var newProxyMsg msg.NewProxy
cfg.UnMarshalToMsg(&newProxyMsg)
ctl.sendCh <- &newProxyMsg
}
}
+
+ for _, cfg := range ctl.vistorCfgs {
+ if _, exist := ctl.vistors[cfg.GetName()]; !exist {
+ ctl.Info("try to start vistor [%s]", cfg.GetName())
+ vistor := NewVistor(ctl, cfg)
+ err = vistor.Run()
+ if err != nil {
+ vistor.Warn("start error: %v", err)
+ continue
+ }
+ ctl.vistors[cfg.GetName()] = vistor
+ vistor.Info("start vistor success")
+ }
+ }
+ ctl.mu.RUnlock()
case _, ok := <-ctl.closedCh:
// we won't get any variable from this channel
if !ok {
@@ -485,11 +501,13 @@ func (ctl *Control) controler() {
go ctl.reader()
// send NewProxy message for all configured proxies
+ ctl.mu.RLock()
for _, cfg := range ctl.pxyCfgs {
var newProxyMsg msg.NewProxy
cfg.UnMarshalToMsg(&newProxyMsg)
ctl.sendCh <- &newProxyMsg
}
+ ctl.mu.RUnlock()
checkProxyTicker.Stop()
checkProxyTicker = time.NewTicker(checkInterval)
@@ -522,3 +540,82 @@ func (ctl *Control) addProxy(name string, pxy Proxy) {
defer ctl.mu.Unlock()
ctl.proxies[name] = pxy
}
+
+func (ctl *Control) getProxyConf(name string) (conf config.ProxyConf, ok bool) {
+ ctl.mu.RLock()
+ defer ctl.mu.RUnlock()
+ conf, ok = ctl.pxyCfgs[name]
+ return
+}
+
+func (ctl *Control) reloadConf(pxyCfgs map[string]config.ProxyConf, vistorCfgs map[string]config.ProxyConf) {
+ ctl.mu.Lock()
+ defer ctl.mu.Unlock()
+
+ removedPxyNames := make([]string, 0)
+ for name, oldCfg := range ctl.pxyCfgs {
+ del := false
+ cfg, ok := pxyCfgs[name]
+ if !ok {
+ del = true
+ } else {
+ if !oldCfg.Compare(cfg) {
+ del = true
+ }
+ }
+
+ if del {
+ removedPxyNames = append(removedPxyNames, name)
+ delete(ctl.pxyCfgs, name)
+ if pxy, ok := ctl.proxies[name]; ok {
+ pxy.Close()
+ }
+ delete(ctl.proxies, name)
+ ctl.sendCh <- &msg.CloseProxy{
+ ProxyName: name,
+ }
+ }
+ }
+ ctl.Info("proxy removed: %v", removedPxyNames)
+
+ addedPxyNames := make([]string, 0)
+ for name, cfg := range pxyCfgs {
+ if _, ok := ctl.pxyCfgs[name]; !ok {
+ ctl.pxyCfgs[name] = cfg
+ addedPxyNames = append(addedPxyNames, name)
+ }
+ }
+ ctl.Info("proxy added: %v", addedPxyNames)
+
+ removedVistorName := make([]string, 0)
+ for name, oldVistorCfg := range ctl.vistorCfgs {
+ del := false
+ cfg, ok := vistorCfgs[name]
+ if !ok {
+ del = true
+ } else {
+ if !oldVistorCfg.Compare(cfg) {
+ del = true
+ }
+ }
+
+ if del {
+ removedVistorName = append(removedVistorName, name)
+ delete(ctl.vistorCfgs, name)
+ if vistor, ok := ctl.vistors[name]; ok {
+ vistor.Close()
+ }
+ delete(ctl.vistors, name)
+ }
+ }
+ ctl.Info("vistor removed: %v", removedVistorName)
+
+ addedVistorName := make([]string, 0)
+ for name, vistorCfg := range vistorCfgs {
+ if _, ok := ctl.vistorCfgs[name]; !ok {
+ ctl.vistorCfgs[name] = vistorCfg
+ addedVistorName = append(addedVistorName, name)
+ }
+ }
+ ctl.Info("vistor added: %v", addedVistorName)
+}
diff --git a/client/service.go b/client/service.go
index ff28cc91..241a435c 100644
--- a/client/service.go
+++ b/client/service.go
@@ -14,7 +14,10 @@
package client
-import "github.com/fatedier/frp/models/config"
+import (
+ "github.com/fatedier/frp/models/config"
+ "github.com/fatedier/frp/utils/log"
+)
type Service struct {
// manager control connection with server
@@ -38,6 +41,14 @@ func (svr *Service) Run() error {
return err
}
+ if config.ClientCommonCfg.AdminPort != 0 {
+ err = svr.RunAdminServer(config.ClientCommonCfg.AdminAddr, config.ClientCommonCfg.AdminPort)
+ if err != nil {
+ log.Warn("run admin server error: %v", err)
+ }
+ log.Info("admin server listen on %s:%d", config.ClientCommonCfg.AdminAddr, config.ClientCommonCfg.AdminPort)
+ }
+
<-svr.closedCh
return nil
}
diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go
index 88fc59b9..986592ff 100644
--- a/cmd/frpc/main.go
+++ b/cmd/frpc/main.go
@@ -54,7 +54,7 @@ Options:
func main() {
var err error
- confFile := "./frpc.ini"
+ confFile := "./frps.ini"
// the configures parsed from file will be replaced by those from command line if exist
args, err := docopt.Parse(usage, nil, true, version.Full(), false)
@@ -73,6 +73,7 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
+ config.ClientCommonCfg.ConfigFile = confFile
if args["-L"] != nil {
if args["-L"].(string) == "console" {
diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini
index 4cc6dc17..3a73e43f 100644
--- a/conf/frpc_full.ini
+++ b/conf/frpc_full.ini
@@ -20,6 +20,12 @@ log_max_days = 3
# for authentication
privilege_token = 12345678
+# set admin address for control frpc's action by http api such as reload
+admin_addr = 127.0.0.1
+admin_port = 7400
+admin_user = admin
+admin_pwd = admin
+
# connections will be established in advance, default value is zero
pool_count = 5
diff --git a/conf/frps_full.ini b/conf/frps_full.ini
index 446d86aa..3b4740ed 100644
--- a/conf/frps_full.ini
+++ b/conf/frps_full.ini
@@ -16,7 +16,7 @@ kcp_bind_port = 7000
vhost_http_port = 80
vhost_https_port = 443
-# if you want to configure or reload frps by dashboard, dashboard_port must be set
+# set dashboard_port to view dashboard of frps
dashboard_port = 7500
# dashboard user and pwd for basic auth protect, if not set, both default value is admin
diff --git a/models/config/client_common.go b/models/config/client_common.go
index 8ec6cd89..749b6b13 100644
--- a/models/config/client_common.go
+++ b/models/config/client_common.go
@@ -36,6 +36,10 @@ type ClientCommonConf struct {
LogLevel string
LogMaxDays int64
PrivilegeToken string
+ AdminAddr string
+ AdminPort int64
+ AdminUser string
+ AdminPwd string
PoolCount int
TcpMux bool
User string
@@ -57,6 +61,10 @@ func GetDeaultClientCommonConf() *ClientCommonConf {
LogLevel: "info",
LogMaxDays: 3,
PrivilegeToken: "",
+ AdminAddr: "127.0.0.1",
+ AdminPort: 0,
+ AdminUser: "",
+ AdminPwd: "",
PoolCount: 1,
TcpMux: true,
User: "",
@@ -111,7 +119,9 @@ func LoadClientCommonConf(conf ini.File) (cfg *ClientCommonConf, err error) {
tmpStr, ok = conf.Get("common", "log_max_days")
if ok {
- cfg.LogMaxDays, _ = strconv.ParseInt(tmpStr, 10, 64)
+ if v, err = strconv.ParseInt(tmpStr, 10, 64); err == nil {
+ cfg.LogMaxDays = v
+ }
}
tmpStr, ok = conf.Get("common", "privilege_token")
@@ -119,6 +129,28 @@ func LoadClientCommonConf(conf ini.File) (cfg *ClientCommonConf, err error) {
cfg.PrivilegeToken = tmpStr
}
+ tmpStr, ok = conf.Get("common", "admin_addr")
+ if ok {
+ cfg.AdminAddr = tmpStr
+ }
+
+ tmpStr, ok = conf.Get("common", "admin_port")
+ if ok {
+ if v, err = strconv.ParseInt(tmpStr, 10, 64); err == nil {
+ cfg.AdminPort = v
+ }
+ }
+
+ tmpStr, ok = conf.Get("common", "admin_user")
+ if ok {
+ cfg.AdminUser = tmpStr
+ }
+
+ tmpStr, ok = conf.Get("common", "admin_pwd")
+ if ok {
+ cfg.AdminPwd = tmpStr
+ }
+
tmpStr, ok = conf.Get("common", "pool_count")
if ok {
v, err = strconv.ParseInt(tmpStr, 10, 64)
@@ -145,7 +177,7 @@ func LoadClientCommonConf(conf ini.File) (cfg *ClientCommonConf, err error) {
if ok {
proxyNames := strings.Split(tmpStr, ",")
for _, name := range proxyNames {
- cfg.Start[name] = struct{}{}
+ cfg.Start[strings.TrimSpace(name)] = struct{}{}
}
}
diff --git a/models/config/proxy.go b/models/config/proxy.go
index 90c982bf..b42f416c 100644
--- a/models/config/proxy.go
+++ b/models/config/proxy.go
@@ -56,6 +56,7 @@ type ProxyConf interface {
LoadFromFile(name string, conf ini.Section) error
UnMarshalToMsg(pMsg *msg.NewProxy)
Check() error
+ Compare(conf ProxyConf) bool
}
func NewProxyConf(pMsg *msg.NewProxy) (cfg ProxyConf, err error) {
@@ -105,6 +106,16 @@ func (cfg *BaseProxyConf) GetBaseInfo() *BaseProxyConf {
return cfg
}
+func (cfg *BaseProxyConf) compare(cmp *BaseProxyConf) bool {
+ if cfg.ProxyName != cmp.ProxyName ||
+ cfg.ProxyType != cmp.ProxyType ||
+ cfg.UseEncryption != cmp.UseEncryption ||
+ cfg.UseCompression != cmp.UseCompression {
+ return false
+ }
+ return true
+}
+
func (cfg *BaseProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.ProxyName = pMsg.ProxyName
cfg.ProxyType = pMsg.ProxyType
@@ -149,8 +160,16 @@ type BindInfoConf struct {
RemotePort int64 `json:"remote_port"`
}
+func (cfg *BindInfoConf) compare(cmp *BindInfoConf) bool {
+ if cfg.BindAddr != cmp.BindAddr ||
+ cfg.RemotePort != cmp.RemotePort {
+ return false
+ }
+ return true
+}
+
func (cfg *BindInfoConf) LoadFromMsg(pMsg *msg.NewProxy) {
- cfg.BindAddr = ServerCommonCfg.BindAddr
+ cfg.BindAddr = ServerCommonCfg.ProxyBindAddr
cfg.RemotePort = pMsg.RemotePort
}
@@ -188,6 +207,14 @@ type DomainConf struct {
SubDomain string `json:"sub_domain"`
}
+func (cfg *DomainConf) compare(cmp *DomainConf) bool {
+ if strings.Join(cfg.CustomDomains, " ") != strings.Join(cmp.CustomDomains, " ") ||
+ cfg.SubDomain != cmp.SubDomain {
+ return false
+ }
+ return true
+}
+
func (cfg *DomainConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.CustomDomains = pMsg.CustomDomains
cfg.SubDomain = pMsg.SubDomain
@@ -246,6 +273,14 @@ type LocalSvrConf struct {
LocalPort int `json:"-"`
}
+func (cfg *LocalSvrConf) compare(cmp *LocalSvrConf) bool {
+ if cfg.LocalIp != cmp.LocalIp ||
+ cfg.LocalPort != cmp.LocalPort {
+ return false
+ }
+ return true
+}
+
func (cfg *LocalSvrConf) LoadFromFile(name string, section ini.Section) (err error) {
if cfg.LocalIp = section["local_ip"]; cfg.LocalIp == "" {
cfg.LocalIp = "127.0.0.1"
@@ -266,6 +301,20 @@ type PluginConf struct {
PluginParams map[string]string `json:"-"`
}
+func (cfg *PluginConf) compare(cmp *PluginConf) bool {
+ if cfg.Plugin != cmp.Plugin ||
+ len(cfg.PluginParams) != len(cmp.PluginParams) {
+ return false
+ }
+ for k, v := range cfg.PluginParams {
+ value, ok := cmp.PluginParams[k]
+ if !ok || v != value {
+ return false
+ }
+ }
+ return true
+}
+
func (cfg *PluginConf) LoadFromFile(name string, section ini.Section) (err error) {
cfg.Plugin = section["plugin"]
cfg.PluginParams = make(map[string]string)
@@ -291,6 +340,21 @@ type TcpProxyConf struct {
PluginConf
}
+func (cfg *TcpProxyConf) Compare(cmp ProxyConf) bool {
+ cmpConf, ok := cmp.(*TcpProxyConf)
+ if !ok {
+ return false
+ }
+
+ if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) ||
+ !cfg.BindInfoConf.compare(&cmpConf.BindInfoConf) ||
+ !cfg.LocalSvrConf.compare(&cmpConf.LocalSvrConf) ||
+ !cfg.PluginConf.compare(&cmpConf.PluginConf) {
+ return false
+ }
+ return true
+}
+
func (cfg *TcpProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.LoadFromMsg(pMsg)
cfg.BindInfoConf.LoadFromMsg(pMsg)
@@ -330,6 +394,20 @@ type UdpProxyConf struct {
LocalSvrConf
}
+func (cfg *UdpProxyConf) Compare(cmp ProxyConf) bool {
+ cmpConf, ok := cmp.(*UdpProxyConf)
+ if !ok {
+ return false
+ }
+
+ if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) ||
+ !cfg.BindInfoConf.compare(&cmpConf.BindInfoConf) ||
+ !cfg.LocalSvrConf.compare(&cmpConf.LocalSvrConf) {
+ return false
+ }
+ return true
+}
+
func (cfg *UdpProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.LoadFromMsg(pMsg)
cfg.BindInfoConf.LoadFromMsg(pMsg)
@@ -372,6 +450,25 @@ type HttpProxyConf struct {
HttpPwd string `json:"-"`
}
+func (cfg *HttpProxyConf) Compare(cmp ProxyConf) bool {
+ cmpConf, ok := cmp.(*HttpProxyConf)
+ if !ok {
+ return false
+ }
+
+ if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) ||
+ !cfg.DomainConf.compare(&cmpConf.DomainConf) ||
+ !cfg.LocalSvrConf.compare(&cmpConf.LocalSvrConf) ||
+ !cfg.PluginConf.compare(&cmpConf.PluginConf) ||
+ strings.Join(cfg.Locations, " ") != strings.Join(cmpConf.Locations, " ") ||
+ cfg.HostHeaderRewrite != cmpConf.HostHeaderRewrite ||
+ cfg.HttpUser != cmpConf.HttpUser ||
+ cfg.HttpPwd != cmpConf.HttpPwd {
+ return false
+ }
+ return true
+}
+
func (cfg *HttpProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.LoadFromMsg(pMsg)
cfg.DomainConf.LoadFromMsg(pMsg)
@@ -438,6 +535,21 @@ type HttpsProxyConf struct {
PluginConf
}
+func (cfg *HttpsProxyConf) Compare(cmp ProxyConf) bool {
+ cmpConf, ok := cmp.(*HttpsProxyConf)
+ if !ok {
+ return false
+ }
+
+ if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) ||
+ !cfg.DomainConf.compare(&cmpConf.DomainConf) ||
+ !cfg.LocalSvrConf.compare(&cmpConf.LocalSvrConf) ||
+ !cfg.PluginConf.compare(&cmpConf.PluginConf) {
+ return false
+ }
+ return true
+}
+
func (cfg *HttpsProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.LoadFromMsg(pMsg)
cfg.DomainConf.LoadFromMsg(pMsg)
@@ -488,6 +600,25 @@ type StcpProxyConf struct {
BindPort int `json:"bind_port"`
}
+func (cfg *StcpProxyConf) Compare(cmp ProxyConf) bool {
+ cmpConf, ok := cmp.(*StcpProxyConf)
+ if !ok {
+ return false
+ }
+
+ if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) ||
+ !cfg.LocalSvrConf.compare(&cmpConf.LocalSvrConf) ||
+ !cfg.PluginConf.compare(&cmpConf.PluginConf) ||
+ cfg.Role != cmpConf.Role ||
+ cfg.Sk != cmpConf.Sk ||
+ cfg.ServerName != cmpConf.ServerName ||
+ cfg.BindAddr != cmpConf.BindAddr ||
+ cfg.BindPort != cmpConf.BindPort {
+ return false
+ }
+ return true
+}
+
// Only for role server.
func (cfg *StcpProxyConf) LoadFromMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.LoadFromMsg(pMsg)
diff --git a/server/dashboard.go b/server/dashboard.go
index 84eac81d..01f71591 100644
--- a/server/dashboard.go
+++ b/server/dashboard.go
@@ -15,16 +15,14 @@
package server
import (
- "compress/gzip"
"fmt"
- "io"
"net"
"net/http"
- "strings"
"time"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/models/config"
+ frpNet "github.com/fatedier/frp/utils/net"
"github.com/julienschmidt/httprouter"
)
@@ -38,20 +36,24 @@ func RunDashboardServer(addr string, port int64) (err error) {
// url router
router := httprouter.New()
+ user, passwd := config.ServerCommonCfg.DashboardUser, config.ServerCommonCfg.DashboardPwd
+
// api, see dashboard_api.go
- router.GET("/api/serverinfo", httprouterBasicAuth(apiServerInfo))
- router.GET("/api/proxy/tcp", httprouterBasicAuth(apiProxyTcp))
- router.GET("/api/proxy/udp", httprouterBasicAuth(apiProxyUdp))
- router.GET("/api/proxy/http", httprouterBasicAuth(apiProxyHttp))
- router.GET("/api/proxy/https", httprouterBasicAuth(apiProxyHttps))
- router.GET("/api/proxy/traffic/:name", httprouterBasicAuth(apiProxyTraffic))
+ router.GET("/api/serverinfo", frpNet.HttprouterBasicAuth(apiServerInfo, user, passwd))
+ router.GET("/api/proxy/tcp", frpNet.HttprouterBasicAuth(apiProxyTcp, user, passwd))
+ router.GET("/api/proxy/udp", frpNet.HttprouterBasicAuth(apiProxyUdp, user, passwd))
+ router.GET("/api/proxy/http", frpNet.HttprouterBasicAuth(apiProxyHttp, user, passwd))
+ router.GET("/api/proxy/https", frpNet.HttprouterBasicAuth(apiProxyHttps, user, passwd))
+ router.GET("/api/proxy/traffic/:name", frpNet.HttprouterBasicAuth(apiProxyTraffic, user, passwd))
// view
router.Handler("GET", "/favicon.ico", http.FileServer(assets.FileSystem))
- router.Handler("GET", "/static/*filepath", MakeGzipHandler(basicAuthWraper(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))))
- router.HandlerFunc("GET", "/", basicAuth(func(w http.ResponseWriter, r *http.Request) {
+ router.Handler("GET", "/static/*filepath", frpNet.MakeHttpGzipHandler(
+ frpNet.NewHttpBasicAuthWraper(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)), user, passwd)))
+
+ router.HandlerFunc("GET", "/", frpNet.HttpBasicAuth(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
- }))
+ }, user, passwd))
address := fmt.Sprintf("%s:%d", addr, port)
server := &http.Server{
@@ -71,91 +73,3 @@ func RunDashboardServer(addr string, port int64) (err error) {
go server.Serve(ln)
return
}
-
-func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
- for _, m := range middleware {
- h = m(h)
- }
- return h
-}
-
-type AuthWraper struct {
- h http.Handler
- user string
- passwd string
-}
-
-func (aw *AuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- user, passwd, hasAuth := r.BasicAuth()
- if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) {
- aw.h.ServeHTTP(w, r)
- } else {
- w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- }
-}
-
-func basicAuthWraper(h http.Handler) http.Handler {
- return &AuthWraper{
- h: h,
- user: config.ServerCommonCfg.DashboardUser,
- passwd: config.ServerCommonCfg.DashboardPwd,
- }
-}
-
-func basicAuth(h http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- user, passwd, hasAuth := r.BasicAuth()
- if (config.ServerCommonCfg.DashboardUser == "" && config.ServerCommonCfg.DashboardPwd == "") ||
- (hasAuth && user == config.ServerCommonCfg.DashboardUser && passwd == config.ServerCommonCfg.DashboardPwd) {
- h.ServeHTTP(w, r)
- } else {
- w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- }
- }
-}
-
-func httprouterBasicAuth(h httprouter.Handle) httprouter.Handle {
- return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
- user, passwd, hasAuth := r.BasicAuth()
- if (config.ServerCommonCfg.DashboardUser == "" && config.ServerCommonCfg.DashboardPwd == "") ||
- (hasAuth && user == config.ServerCommonCfg.DashboardUser && passwd == config.ServerCommonCfg.DashboardPwd) {
- h(w, r, ps)
- } else {
- w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- }
- }
-}
-
-type GzipWraper struct {
- h http.Handler
-}
-
-func (gw *GzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
- gw.h.ServeHTTP(w, r)
- return
- }
- w.Header().Set("Content-Encoding", "gzip")
- gz := gzip.NewWriter(w)
- defer gz.Close()
- gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
- gw.h.ServeHTTP(gzr, r)
-}
-
-func MakeGzipHandler(h http.Handler) http.Handler {
- return &GzipWraper{
- h: h,
- }
-}
-
-type gzipResponseWriter struct {
- io.Writer
- http.ResponseWriter
-}
-
-func (w gzipResponseWriter) Write(b []byte) (int, error) {
- return w.Writer.Write(b)
-}
diff --git a/utils/net/http.go b/utils/net/http.go
new file mode 100644
index 00000000..acc0f43e
--- /dev/null
+++ b/utils/net/http.go
@@ -0,0 +1,105 @@
+// Copyright 2017 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 net
+
+import (
+ "compress/gzip"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+type HttpAuthWraper struct {
+ h http.Handler
+ user string
+ passwd string
+}
+
+func NewHttpBasicAuthWraper(h http.Handler, user, passwd string) http.Handler {
+ return &HttpAuthWraper{
+ h: h,
+ user: user,
+ passwd: passwd,
+ }
+}
+
+func (aw *HttpAuthWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ user, passwd, hasAuth := r.BasicAuth()
+ if (aw.user == "" && aw.passwd == "") || (hasAuth && user == aw.user && passwd == aw.passwd) {
+ aw.h.ServeHTTP(w, r)
+ } else {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ }
+}
+
+func HttpBasicAuth(h http.HandlerFunc, user, passwd string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ reqUser, reqPasswd, hasAuth := r.BasicAuth()
+ if (user == "" && passwd == "") ||
+ (hasAuth && reqUser == user && reqPasswd == passwd) {
+ h.ServeHTTP(w, r)
+ } else {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ }
+ }
+}
+
+func HttprouterBasicAuth(h httprouter.Handle, user, passwd string) httprouter.Handle {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ reqUser, reqPasswd, hasAuth := r.BasicAuth()
+ if (user == "" && passwd == "") ||
+ (hasAuth && reqUser == user && reqPasswd == passwd) {
+ h(w, r, ps)
+ } else {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ }
+ }
+}
+
+type HttpGzipWraper struct {
+ h http.Handler
+}
+
+func (gw *HttpGzipWraper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+ gw.h.ServeHTTP(w, r)
+ return
+ }
+ w.Header().Set("Content-Encoding", "gzip")
+ gz := gzip.NewWriter(w)
+ defer gz.Close()
+ gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
+ gw.h.ServeHTTP(gzr, r)
+}
+
+func MakeHttpGzipHandler(h http.Handler) http.Handler {
+ return &HttpGzipWraper{
+ h: h,
+ }
+}
+
+type gzipResponseWriter struct {
+ io.Writer
+ http.ResponseWriter
+}
+
+func (w gzipResponseWriter) Write(b []byte) (int, error) {
+ return w.Writer.Write(b)
+}
From ee9ddf52cdbe350f3c08b60ede2faf6548ba2fac Mon Sep 17 00:00:00 2001
From: fatedier
Date: Thu, 13 Jul 2017 02:30:25 +0800
Subject: [PATCH 16/19] frpc: support --reload command
---
cmd/frpc/main.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go
index 986592ff..5aa6bec8 100644
--- a/cmd/frpc/main.go
+++ b/cmd/frpc/main.go
@@ -15,7 +15,11 @@
package main
import (
+ "encoding/base64"
+ "encoding/json"
"fmt"
+ "io/ioutil"
+ "net/http"
"os"
"os/signal"
"strconv"
@@ -40,6 +44,7 @@ var usage string = `frpc is the client of frp
Usage:
frpc [-c config_file] [-L log_file] [--log-level=] [--server-addr=]
+ frpc [-c config_file] --reload
frpc -h | --help
frpc -v | --version
@@ -48,6 +53,7 @@ Options:
-L log_file set output log file, including console
--log-level= set log level: debug, info, warn, error
--server-addr= addr which frps is listening for, example: 0.0.0.0:7000
+ --reload reload configure file without program exit
-h --help show this screen
-v --version show version
`
@@ -75,6 +81,46 @@ func main() {
}
config.ClientCommonCfg.ConfigFile = confFile
+ // check if reload command
+ if args["--reload"] != nil {
+ if args["--reload"].(bool) {
+ req, err := http.NewRequest("GET", "http://"+
+ config.ClientCommonCfg.AdminAddr+":"+fmt.Sprintf("%d", config.ClientCommonCfg.AdminPort)+"/api/reload", nil)
+ if err != nil {
+ fmt.Printf("frps reload error: %v\n", err)
+ os.Exit(1)
+ }
+
+ authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(config.ClientCommonCfg.AdminUser+":"+
+ config.ClientCommonCfg.AdminPwd))
+
+ req.Header.Add("Authorization", authStr)
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ fmt.Printf("frpc reload error: %v\n", err)
+ os.Exit(1)
+ } else {
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ fmt.Printf("frpc reload error: %v\n", err)
+ os.Exit(1)
+ }
+ res := &client.GeneralResponse{}
+ err = json.Unmarshal(body, &res)
+ if err != nil {
+ fmt.Printf("http response error: %s\n", strings.TrimSpace(string(body)))
+ os.Exit(1)
+ } else if res.Code != 0 {
+ fmt.Printf("reload error: %s\n", res.Msg)
+ os.Exit(1)
+ }
+ fmt.Printf("reload success\n")
+ os.Exit(0)
+ }
+ }
+ }
+
if args["-L"] != nil {
if args["-L"].(string) == "console" {
config.ClientCommonCfg.LogWay = "console"
From 2b1ba3a94688b2ad8c436b9ab055f3791e77a955 Mon Sep 17 00:00:00 2001
From: fatedier
Date: Thu, 13 Jul 2017 12:01:46 +0800
Subject: [PATCH 17/19] update conf
---
conf/frpc_full.ini | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini
index 3a73e43f..5ae97447 100644
--- a/conf/frpc_full.ini
+++ b/conf/frpc_full.ini
@@ -128,11 +128,15 @@ local_port = 22
use_encryption = false
use_compression = false
+# user of frpc should be same in both stcp server and stcp vistor
[secret_tcp_vistor]
# frpc role vistor -> frps -> frpc role server
role = vistor
type = stcp
+# the server name you want to vistor
+server_name = secret_tcp
sk = abcdefg
+# connect this address to vistor stcp server
bind_addr = 127.0.0.1
bind_port = 9000
use_encryption = false
From 0208ecd1d907ade2b038c47892cf6fa46cc29aee Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 17 Jul 2017 00:57:10 +0800
Subject: [PATCH 18/19] update doc for v0.13.0
---
README.md | 102 ++++++++++++++++++++++++++++++++++----------
README_zh.md | 118 +++++++++++++++++++++++++++++++++++++++------------
2 files changed, 171 insertions(+), 49 deletions(-)
diff --git a/README.md b/README.md
index f7087e7a..27a327e2 100644
--- a/README.md
+++ b/README.md
@@ -19,18 +19,21 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or fi
* [Visit your web service in LAN by custom domains](#visit-your-web-service-in-lan-by-custom-domains)
* [Forward DNS query request](#forward-dns-query-request)
* [Forward unix domain socket](#forward-unix-domain-socket)
+ * [Expose your service in security](#expose-your-service-in-security)
* [Connect website through frpc's network](#connect-website-through-frpcs-network)
* [Features](#features)
+ * [Configuration File](#configuration-file)
* [Dashboard](#dashboard)
* [Authentication](#authentication)
* [Encryption and Compression](#encryption-and-compression)
- * [Reload configures without frps stopped](#reload-configures-without-frps-stopped)
+ * [Hot-Reload frpc configuration](#hot-reload-frpc-configuration)
* [Privilege Mode](#privilege-mode)
* [Port White List](#port-white-list)
* [TCP Stream Multiplexing](#tcp-stream-multiplexing)
* [Support KCP Protocol](#support-kcp-protocol)
* [Connection Pool](#connection-pool)
* [Rewriting the Host Header](#rewriting-the-host-header)
+ * [Get Real IP](#get-real-ip)
* [Password protecting your web service](#password-protecting-your-web-service)
* [Custom subdomain names](#custom-subdomain-names)
* [URL routing](#url-routing)
@@ -187,19 +190,36 @@ However, we can expose a http or https service using frp.
Using tcp port to connect unix domain socket like docker daemon.
-1. Modify frps.ini:
+Configure frps same as above.
+
+1. Start frpc with configurations:
```ini
- # frps.ini
+ # frpc.ini
[common]
- bind_port = 7000
+ server_addr = x.x.x.x
+ server_port = 7000
+
+ [unix_domain_socket]
+ type = tcp
+ remote_port = 6000
+ plugin = unix_domain_socket
+ plugin_unix_path = /var/run/docker.sock
```
-2. Start frps:
+2. Get docker version by curl command:
- `./frps -c ./frps.ini`
+ `curl http://x.x.x.x:6000/version`
+
+### Expose your service in security
+
+For some services, if expose them to the public network directly will be a security risk.
+
+**stcp(secret tcp)** help you create a proxy avoiding any one can access it.
+
+Configure frps same as above.
-3. Modify frpc.ini:
+1. Start frpc, forward ssh port and `remote_port` is useless:
```ini
# frpc.ini
@@ -207,26 +227,39 @@ Using tcp port to connect unix domain socket like docker daemon.
server_addr = x.x.x.x
server_port = 7000
- [unix_domain_socket]
- type = tcp
- remote_port = 6000
- plugin = unix_domain_socket
- plugin_unix_path = /var/run/docker.sock
+ [secret_ssh]
+ type = stcp
+ sk = abcdefg
+ local_ip = 127.0.0.1
+ local_port = 22
```
-4. Start frpc:
+2. Start another frpc in which you want to connect this ssh server:
- `./frpc -c ./frpc.ini`
+ ```ini
+ # frpc.ini
+ [common]
+ server_addr = x.x.x.x
+ server_port = 7000
+
+ [secret_ssh_vistor]
+ type = stcp
+ role = vistor
+ server_name = secret_ssh
+ sk = abcdefg
+ bind_addr = 127.0.0.1
+ bind_port = 6000
+ ```
-5. Get docker version by curl command:
+3. Connect to server in LAN by ssh assuming that username is test:
- `curl http://x.x.x.x:6000/version`
+ `ssh -oPort=6000 test@127.0.0.1`
### Connect website through frpc's network
Configure frps same as above.
-1. Modify frpc.ini:
+1. Start frpc with configurations:
```ini
# frpc.ini
@@ -240,13 +273,17 @@ Configure frps same as above.
plugin = http_proxy # or socks5
```
-4. Start frpc:
+2. Set http proxy or socks5 proxy `x.x.x.x:6000` in your browser and visit website through frpc's network.
- `./frpc -c ./frpc.ini`
+## Features
-5. Set http proxy or socks5 proxy `x.x.x.x:6000` in your browser and visit website through frpc's network.
+### Configuration File
-## Features
+You can find features which this document not metioned from full example configuration files.
+
+[frps full configuration file](./conf/frps_full.ini)
+
+[frpc full configuration file](./conf/frpc_full.ini)
### Dashboard
@@ -288,9 +325,20 @@ use_encryption = true
use_compression = true
```
-### Reload configures without frps stopped
+### Hot-Reload frpc configuration
+
+First you need to set admin port in frpc's configure file to let it provide HTTP API for more features.
+
+```ini
+# frpc.ini
+[common]
+admin_addr = 127.0.0.1
+admin_port = 7400
+```
+
+Then run command `frpc -c ./frpc.ini --reload` and wait for about 10 seconds to let frpc create or update or delete proxies.
-This feature is removed since v0.10.0.
+**Note that parameters in [common] section won't be modified except 'start' now.**
### Privilege Mode
@@ -386,6 +434,14 @@ host_header_rewrite = dev.yourdomain.com
If `host_header_rewrite` is specified, the Host header will be rewritten to match the hostname portion of the forwarding address.
+### Get Real IP
+
+Features for http proxy only.
+
+You can get user's real IP from http request header `X-Forwarded-For` and `X-Real-IP`.
+
+**Note that now you can only get these two headers in first request of each user connection.**
+
### Password protecting your web service
Anyone who can guess your tunnel URL can access your local web server unless you protect it with a password.
diff --git a/README_zh.md b/README_zh.md
index 5f0a09ba..0eec618c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -17,18 +17,21 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
* [通过自定义域名访问部署于内网的 web 服务](#通过自定义域名访问部署于内网的-web-服务)
* [转发 DNS 查询请求](#转发-dns-查询请求)
* [转发 Unix域套接字](#转发-unix域套接字)
+ * [安全地暴露内网服务](#安全地暴露内网服务)
* [通过 frpc 所在机器访问外网](#通过-frpc-所在机器访问外网)
* [功能说明](#功能说明)
+ * [配置文件](#配置文件)
* [Dashboard](#dashboard)
* [身份验证](#身份验证)
* [加密与压缩](#加密与压缩)
- * [服务器端热加载配置文件](#服务器端热加载配置文件)
+ * [客户端热加载配置文件](#客户端热加载配置文件)
* [特权模式](#特权模式)
* [端口白名单](#端口白名单)
* [TCP 多路复用](#tcp-多路复用)
- * [支持 kcp 协议](#支持-kcp-协议)
+ * [底层通信可选 kcp 协议](#底层通信可选-kcp-协议)
* [连接池](#连接池)
* [修改 Host Header](#修改-host-header)
+ * [获取用户真实 IP](#获取用户真实-ip)
* [通过密码保护你的 web 服务](#通过密码保护你的-web-服务)
* [自定义二级域名](#自定义二级域名)
* [URL 路由](#url-路由)
@@ -188,19 +191,9 @@ DNS 查询请求通常使用 UDP 协议,frp 支持对内网 UDP 服务的穿
通过 tcp 端口访问内网的 unix域套接字(和 docker daemon 通信)。
-1. 修改 frps.ini 文件:
-
- ```ini
- # frps.ini
- [common]
- bind_port = 7000
- ```
-
-2. 启动 frps:
-
- `./frps -c ./frps.ini`
+frps 的部署步骤同上。
-3. 修改 frpc.ini 文件,启用 unix_domain_socket 插件:
+1. 启动 frpc,启用 unix_domain_socket 插件,配置如下:
```ini
# frpc.ini
@@ -215,13 +208,59 @@ DNS 查询请求通常使用 UDP 协议,frp 支持对内网 UDP 服务的穿
plugin_unix_path = /var/run/docker.sock
```
-4. 启动 frpc:
+2. 通过 curl 命令查看 docker 版本信息
- `./frpc -c ./frpc.ini`
+ `curl http://x.x.x.x:6000/version`
-5. 通过 curl 命令查看 docker 版本信息
+### 安全地暴露内网服务
- `curl http://x.x.x.x:6000/version`
+对于某些服务来说如果直接暴露于公网上将会存在安全隐患。
+
+使用 **stcp(secret tcp)** 类型的代理可以避免让任何人都能访问到要穿透的服务,但是访问者也需要运行另外一个 frpc。
+
+以下示例将会创建一个只有自己能访问到的 ssh 服务代理。
+
+frps 的部署步骤同上。
+
+1. 启动 frpc,转发内网的 ssh 服务,配置如下,不需要指定远程端口:
+
+ ```ini
+ # frpc.ini
+ [common]
+ server_addr = x.x.x.x
+ server_port = 7000
+
+ [secret_ssh]
+ type = stcp
+ # 只有 sk 一致的用户才能访问到此服务
+ sk = abcdefg
+ local_ip = 127.0.0.1
+ local_port = 22
+ ```
+
+2. 在要访问这个服务的机器上启动另外一个 frpc,配置如下:
+
+ ```ini
+ # frpc.ini
+ [common]
+ server_addr = x.x.x.x
+ server_port = 7000
+
+ [secret_ssh_vistor]
+ type = stcp
+ # stcp 的访问者
+ role = vistor
+ # 要访问的 stcp 代理的名字
+ server_name = secret_ssh
+ sk = abcdefg
+ # 绑定本地端口用于访问 ssh 服务
+ bind_addr = 127.0.0.1
+ bind_port = 6000
+ ```
+
+3. 通过 ssh 访问内网机器,假设用户名为 test:
+
+ `ssh -oPort=6000 test@127.0.0.1`
### 通过 frpc 所在机器访问外网
@@ -229,7 +268,7 @@ frpc 内置了 http proxy 和 socks5 插件,可以使其他机器通过 frpc
frps 的部署步骤同上。
-1. 修改 frpc.ini 文件,启用 http_proxy 或 socks5 插件(plugin 换为 socks5 即可):
+1. 启动 frpc,启用 http_proxy 或 socks5 插件(plugin 换为 socks5 即可), 配置如下:
```ini
# frpc.ini
@@ -243,13 +282,17 @@ frps 的部署步骤同上。
plugin = http_proxy
```
-4. 启动 frpc:
+2. 浏览器设置 http 或 socks5 代理地址为 `x.x.x.x:6000`,通过 frpc 机器的网络访问互联网。
- `./frpc -c ./frpc.ini`
+## 功能说明
-5. 浏览器设置 http 或 socks5 代理地址为 `x.x.x.x:6000`,通过 frpc 机器的网络访问互联网。
+### 配置文件
-## 功能说明
+由于 frp 目前支持的功能和配置项较多,未在文档中列出的功能可以从完整的示例配置文件中发现。
+
+[frps 完整配置文件](./conf/frps_full.ini)
+
+[frpc 完整配置文件](./conf/frpc_full.ini)
### Dashboard
@@ -295,9 +338,26 @@ use_compression = true
如果传输的报文长度较长,通过设置 `use_compression = true` 对传输内容进行压缩,可以有效减小 frpc 与 frps 之间的网络流量,加快流量转发速度,但是会额外消耗一些 cpu 资源。
-### 服务器端热加载配置文件
+### 客户端热加载配置文件
+
+当修改了 frpc 中的代理配置,可以通过 `frpc --reload` 命令来动态加载配置文件,通常会在 10 秒内完成代理的更新。
+
+启用此功能需要在 frpc 中启用 admin 端口,用于提供 API 服务。配置如下:
-由于从 v0.10.0 版本开始,所有 proxy 都在客户端配置,这个功能暂时移除。
+```ini
+# frpc.ini
+[common]
+admin_addr = 127.0.0.1
+admin_port = 7400
+```
+
+之后执行重启命令:
+
+`frpc -c ./frpc.ini --reload`
+
+等待一段时间后客户端会根据新的配置文件创建、更新、删除代理。
+
+**需要注意的是,[common] 中的参数除了 start 外目前无法被修改。**
### 特权模式
@@ -327,7 +387,7 @@ privilege_allow_ports 可以配置允许使用的某个指定端口或者是一
tcp_mux = false
```
-### 支持 kcp 协议
+### 底层通信可选 kcp 协议
从 v0.12.0 版本开始,底层通信协议支持选择 kcp 协议,在弱网环境下传输效率提升明显,但是会有一些额外的流量消耗。
@@ -393,6 +453,12 @@ host_header_rewrite = dev.yourdomain.com
原来 http 请求中的 host 字段 `test.yourdomain.com` 转发到后端服务时会被替换为 `dev.yourdomain.com`。
+### 获取用户真实 IP
+
+目前只有 **http** 类型的代理支持这一功能,可以通过用户请求的 header 中的 `X-Forwarded-For` 和 `X-Real-IP` 来获取用户真实 IP。
+
+**需要注意的是,目前只在每一个用户连接的第一个 HTTP 请求中添加了这两个 header。**
+
### 通过密码保护你的 web 服务
由于所有客户端共用一个 frps 的 http 服务端口,任何知道你的域名和 url 的人都能访问到你部署在内网的 web 服务,但是在某些场景下需要确保只有限定的用户才能访问。
From d5931758b6899c0f8b06889a6989a2c533dc0fef Mon Sep 17 00:00:00 2001
From: fatedier
Date: Mon, 17 Jul 2017 02:14:30 +0800
Subject: [PATCH 19/19] fix user in reload command
---
client/admin_api.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/admin_api.go b/client/admin_api.go
index 72fae04e..5523302b 100644
--- a/client/admin_api.go
+++ b/client/admin_api.go
@@ -64,7 +64,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request, _ httprout
return
}
- pxyCfgs, vistorCfgs, err := config.LoadProxyConfFromFile(newCommonCfg.User, conf, newCommonCfg.Start)
+ pxyCfgs, vistorCfgs, err := config.LoadProxyConfFromFile(config.ClientCommonCfg.User, conf, newCommonCfg.Start)
if err != nil {
res.Code = 3
res.Msg = err.Error()