From 4a34566f087c6ea845ff3c4c33bbf7e7935cb318 Mon Sep 17 00:00:00 2001 From: "arraykeys@gmail.com" Date: Wed, 28 Mar 2018 13:14:06 +0800 Subject: [PATCH] Signed-off-by: arraykeys@gmail.com --- CHANGELOG | 6 ++ README_ZH.md | 45 +++++++++- config.go | 9 +- services/args.go | 8 ++ services/sps.go | 204 ++++++++++++++++++++++++------------------ utils/socks/client.go | 2 +- utils/socks/server.go | 19 ++-- utils/structs.go | 23 ++--- 8 files changed, 206 insertions(+), 110 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 020bbda..38e7efa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,10 @@ proxy更新日志 +v4.6 +1.sps,http(s),socks5,内网穿透都做了大量的超时优化处理,更加稳定. +2.sps增加了强大的树形级联认证支持,可以轻松构建你的认证代理网络. +3.手册增加了6.6对sps认证功能的介绍. + + v4.5 1.优化了mux内网穿透连接管理逻辑,增强了稳定性. 2.mux内网穿透增加了tcp和kcp协议支持,之前是tls,现在支持三种协议tcp,tls,kcp. diff --git a/README_ZH.md b/README_ZH.md index dea5c5e..d08694d 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -127,7 +127,8 @@ Proxy是golang实现的高性能http,https,websocket,tcp,udp,socks5代理服务 - [6.3 SOCKS5转HTTP(S)+SOCKS5](#63-socks5转httpssocks5) - [6.4 链式连接](#64-链式连接) - [6.5 监听多个端口](#65-监听多个端口) - - [6.6 查看帮助](#66-查看帮助) + - [6.6 认证功能](#66-认证功能) + - [6.7 查看帮助](#67-查看帮助) - [7. KCP配置](#7kcp配置) - [7.1 配置介绍](#71-配置介绍) - [7.2 详细配置](#72-详细配置) @@ -736,7 +737,47 @@ vps02:3.3.3.3 一般情况下监听一个端口就可以,不过如果作为反向代理需要同时监听80和443两个端口,那么-p参数是支持的, 格式是:`-p 0.0.0.0:80,0.0.0.0:443`,多个绑定用逗号分隔即可。 -#### **6.6 查看帮助** +#### **6.6 认证功能** +sps支持http(s)\socks5代理认证,可以级联认证,有四个重要的信息: +1:用户发送认证信息`user-auth`。 +2:设置的本地认证信息`local-auth`。 +3:设置的连接上级使用的认证信息`parent-auth`。 +4:最终发送给上级的认证信息`auth-info-to-parent`。 +他们的情况关系如下: +| user-auth | local-auth | parent-auth | auth-info-to-parent +| 有/没有 | 有 | 有 | 来自parent-auth +| 有/没有 | 没有 | 有 | 来自parent-auth +| 有/没有 | 有 | 没有 | 无 +| 没有 | 没有 | 没有 | 无 +| 有 | 没有 | 没有 | 来自user-auth + +对于sps代理我们可以进行用户名密码认证,认证的用户名和密码可以在命令行指定 +`./proxy sps -S http -T tcp -P 127.0.0.1:8080 -t tcp -p ":33080" -a "user1:pass1" -a "user2:pass2"` +多个用户,重复-a参数即可. +也可以放在文件中,格式是一行一个"用户名:密码",然后用-F指定. +`./proxy sps -S http -T tcp -P 127.0.0.1:8080 -t tcp -p ":33080" -F auth-file.txt` + +如果上级有认证,下级可以通过-A参数设置认证信息,比如: +上级:`./proxy sps -S http -T tcp -P 127.0.0.1:8080 -t tcp -p ":33080" -a "user1:pass1" -a "user2:pass2"` +下级:`./proxy sps -S http -T tcp -P 127.0.0.1:8080 -A "user1:pass1" -t tcp -p ":33080" ` + +另外,sps代理,本地认证集成了外部HTTP API认证,我们可以通过--auth-url参数指定一个http url接口地址, +然后有用户连接的时候,proxy会GET方式请求这url,带上下面四个参数,如果返回HTTP状态码204,代表认证成功 +其它情况认为认证失败. +比如: +`./proxy sps -S http -T tcp -P 127.0.0.1:8080 -t tcp -p ":33080" --auth-url "http://test.com/auth.php"` +用户连接的时候,proxy会GET方式请求这url("http://test.com/auth.php"), +带上user,pass,ip,三个参数: +http://test.com/auth.php?user={USER}&pass={PASS}&ip={IP}&target={TARGET} +user:用户名 +pass:密码 +ip:用户的IP,比如:192.168.1.200 +target:如果客户端是http(s)代理请求,这里代表的是请求的完整url,其它情况为空. + +如果没有-a或-F或--auth-url参数,就是关闭本地认证. +如果没有-A参数,连接上级不使用认证. + +#### **6.7 查看帮助** `./proxy help sps` ### **7.KCP配置** diff --git a/config.go b/config.go index 5d5e671..70f0a4a 100755 --- a/config.go +++ b/config.go @@ -223,7 +223,14 @@ func initConfig() (err error) { spsArgs.ParentServiceType = sps.Flag("parent-service-type", "parent service type ").Short('S').Enum("http", "socks") spsArgs.DNSAddress = sps.Flag("dns-address", "if set this, proxy will use this dns for resolve doamin").Short('q').Default("").String() spsArgs.DNSTTL = sps.Flag("dns-ttl", "caching seconds of dns query result").Short('e').Default("300").Int() - + spsArgs.AuthFile = sps.Flag("auth-file", "http basic auth file,\"username:password\" each line in file").Short('F').String() + spsArgs.Auth = sps.Flag("auth", "socks auth username and password, mutiple user repeat -a ,such as: -a user1:pass1 -a user2:pass2").Short('a').Strings() + spsArgs.LocalIPS = sps.Flag("local bind ips", "if your host behind a nat,set your public ip here avoid dead loop").Short('g').Strings() + spsArgs.AuthURL = sps.Flag("auth-url", "auth username and password will send to this url,response http code equal to 'auth-code' means ok,others means fail.").Default("").String() + spsArgs.AuthURLTimeout = sps.Flag("auth-timeout", "access 'auth-url' timeout milliseconds").Default("3000").Int() + spsArgs.AuthURLOkCode = sps.Flag("auth-code", "access 'auth-url' success http code").Default("204").Int() + spsArgs.AuthURLRetry = sps.Flag("auth-retry", "access 'auth-url' fail and retry count").Default("0").Int() + spsArgs.ParentAuth = sps.Flag("parent-auth", "parent socks auth username and password, such as: -A user1:pass1").Short('A').String() //parse args serviceName := kingpin.MustParse(app.Parse(os.Args[1:])) diff --git a/services/args.go b/services/args.go index 2786f79..68ccb78 100644 --- a/services/args.go +++ b/services/args.go @@ -215,6 +215,14 @@ type SPSArgs struct { ParentServiceType *string DNSAddress *string DNSTTL *int + AuthFile *string + Auth *[]string + AuthURL *string + AuthURLOkCode *int + AuthURLTimeout *int + AuthURLRetry *int + LocalIPS *[]string + ParentAuth *string } func (a *SPSArgs) Protocol() string { diff --git a/services/sps.go b/services/sps.go index 1b5de03..590e6d9 100644 --- a/services/sps.go +++ b/services/sps.go @@ -2,6 +2,7 @@ package services import ( "bytes" + "encoding/base64" "errors" "fmt" "io/ioutil" @@ -12,18 +13,21 @@ import ( "snail007/proxy/utils/socks" "strconv" "strings" + "time" ) type SPS struct { outPool utils.OutPool cfg SPSArgs domainResolver utils.DomainResolver + basicAuth utils.BasicAuth } func NewSPS() Service { return &SPS{ - outPool: utils.OutPool{}, - cfg: SPSArgs{}, + outPool: utils.OutPool{}, + cfg: SPSArgs{}, + basicAuth: utils.BasicAuth{}, } } func (s *SPS) CheckArgs() { @@ -46,6 +50,10 @@ func (s *SPS) CheckArgs() { } func (s *SPS) InitService() { s.InitOutConnPool() + if *s.cfg.DNSAddress != "" { + (*s).domainResolver = utils.NewDomainResolver(*s.cfg.DNSAddress, *s.cfg.DNSTTL) + } + s.InitBasicAuth() } func (s *SPS) InitOutConnPool() { if *s.cfg.ParentType == TYPE_TLS || *s.cfg.ParentType == TYPE_TCP || *s.cfg.ParentType == TYPE_KCP { @@ -117,7 +125,7 @@ func (s *SPS) callback(inConn net.Conn) { err = fmt.Errorf("unkown parent type %s", *s.cfg.ParentType) } if err != nil { - log.Printf("connect to %s parent %s fail, ERR:%s", *s.cfg.ParentType, *s.cfg.Parent, err) + log.Printf("connect to %s parent %s fail, ERR:%s from %s", *s.cfg.ParentType, *s.cfg.Parent, err, inConn.RemoteAddr()) utils.CloseConn(&inConn) } } @@ -131,51 +139,32 @@ func (s *SPS) OutToTCP(inConn *net.Conn) (err error) { return } address := "" + var auth socks.Auth var forwardBytes []byte //fmt.Printf("%v", header) if header[0] == socks.VERSION_V5 { - //socks - methodReq, e := socks.NewMethodsRequest(*inConn, header) - if e != nil { - log.Printf("new method request err:%s", e) - utils.CloseConn(inConn) - err = e.(error) + //socks5 server + var serverConn *socks.ServerConn + if s.IsBasicAuth() { + serverConn = socks.NewServerConn(inConn, time.Millisecond*time.Duration(*s.cfg.Timeout), &s.basicAuth, "", header) + } else { + serverConn = socks.NewServerConn(inConn, time.Millisecond*time.Duration(*s.cfg.Timeout), nil, "", header) + } + if err = serverConn.Handshake(); err != nil { return } - if !methodReq.Select(socks.Method_NO_AUTH) { - methodReq.Reply(socks.Method_NONE_ACCEPTABLE) - utils.CloseConn(inConn) - log.Printf("none method found : Method_NO_AUTH") - return - } - //method select reply - err = methodReq.Reply(socks.Method_NO_AUTH) - if err != nil { - log.Printf("reply answer data fail,ERR: %s", err) - utils.CloseConn(inConn) - return - } - //request detail - request, e := socks.NewRequest(*inConn) - if e != nil { - log.Printf("read request data fail,ERR: %s", e) - utils.CloseConn(inConn) - err = e.(error) - return - } - if request.CMD() != socks.CMD_CONNECT { - //只支持tcp - request.TCPReply(socks.REP_UNKNOWN) - utils.CloseConn(inConn) - err = errors.New("cmd not supported") - return - } - address = request.Addr() - request.TCPReply(socks.REP_SUCCESS) + address = serverConn.Target() + auth = serverConn.AuthData() } else if bytes.IndexByte(header, '\n') != -1 { //http var request utils.HTTPRequest - request, err = utils.NewHTTPRequest(inConn, 1024, false, nil, header) + (*inConn).SetDeadline(time.Now().Add(time.Millisecond * time.Duration(*s.cfg.Timeout))) + if s.IsBasicAuth() { + request, err = utils.NewHTTPRequest(inConn, 1024, true, &s.basicAuth, header) + } else { + request, err = utils.NewHTTPRequest(inConn, 1024, false, nil, header) + } + (*inConn).SetDeadline(time.Time{}) if err != nil { log.Printf("new http request fail,ERR: %s", err) utils.CloseConn(inConn) @@ -189,6 +178,17 @@ func (s *SPS) OutToTCP(inConn *net.Conn) (err error) { forwardBytes = request.HeadBuf } address = request.Host + var userpass string + if s.IsBasicAuth() { + userpass, err = request.GetAuthDataStr() + if err != nil { + return + } + userpassA := strings.Split(userpass, ":") + if len(userpassA) == 2 { + auth = socks.Auth{User: userpassA[0], Password: userpassA[1]} + } + } } else { log.Printf("unknown request from: %s,%s", (*inConn).RemoteAddr(), string(header)) utils.CloseConn(inConn) @@ -207,12 +207,42 @@ func (s *SPS) OutToTCP(inConn *net.Conn) (err error) { utils.CloseConn(inConn) return } + //ask parent for connect to target address if *s.cfg.ParentServiceType == "http" { //http parent - fmt.Fprintf(outConn, "CONNECT %s HTTP/1.1\r\n", address) + pb := new(bytes.Buffer) + pb.Write([]byte(fmt.Sprintf("CONNECT %s HTTP/1.1\r\nProxy-Connection: Keep-Alive\r\n", address))) + //Proxy-Authorization:\r\n + u := "" + if *s.cfg.ParentAuth != "" { + a := strings.Split(*s.cfg.ParentAuth, ":") + if len(a) != 2 { + err = fmt.Errorf("parent auth data format error") + return + } + u = fmt.Sprintf("%s:%s", a[0], a[1]) + } else { + if !s.IsBasicAuth() && auth.Password != "" && auth.User != "" { + u = fmt.Sprintf("%s:%s", auth.User, auth.Password) + } + } + if u != "" { + pb.Write([]byte(fmt.Sprintf("Proxy-Authorization:Basic %s\r\n", base64.StdEncoding.EncodeToString([]byte(u))))) + } + outConn.SetDeadline(time.Now().Add(time.Millisecond * time.Duration(*s.cfg.Timeout))) + _, err = outConn.Write(pb.Bytes()) + outConn.SetDeadline(time.Time{}) + if err != nil { + log.Printf("write CONNECT to %s , err:%s", *s.cfg.Parent, err) + utils.CloseConn(inConn) + utils.CloseConn(&outConn) + return + } reply := make([]byte, 100) - n, err = outConn.Read(reply) + outConn.SetDeadline(time.Now().Add(time.Millisecond * time.Duration(*s.cfg.Timeout))) + _, err = outConn.Read(reply) + outConn.SetDeadline(time.Time{}) if err != nil { log.Printf("read reply from %s , err:%s", *s.cfg.Parent, err) utils.CloseConn(inConn) @@ -222,53 +252,25 @@ func (s *SPS) OutToTCP(inConn *net.Conn) (err error) { //log.Printf("reply: %s", string(reply[:n])) } else { log.Printf("connect %s", address) - //socks parent - //send auth type - _, err = outConn.Write([]byte{0x05, 0x01, 0x00}) - if err != nil { - log.Printf("write method to %s fail, err:%s", *s.cfg.Parent, err) - utils.CloseConn(inConn) - utils.CloseConn(&outConn) + //socks client + var clientConn *socks.ClientConn + if *s.cfg.ParentAuth != "" { + a := strings.Split(*s.cfg.ParentAuth, ":") + if len(a) != 2 { + err = fmt.Errorf("parent auth data format error") + return + } + clientConn = socks.NewClientConn(&outConn, "tcp", address, time.Millisecond*time.Duration(*s.cfg.Timeout), &socks.Auth{User: a[0], Password: a[1]}, header) + } else { + if !s.IsBasicAuth() && auth.Password != "" && auth.User != "" { + clientConn = socks.NewClientConn(&outConn, "tcp", address, time.Millisecond*time.Duration(*s.cfg.Timeout), &auth, header) + } else { + clientConn = socks.NewClientConn(&outConn, "tcp", address, time.Millisecond*time.Duration(*s.cfg.Timeout), nil, header) + } + } + if err = clientConn.Handshake(); err != nil { return } - //read reply - reply := make([]byte, 512) - n, err = outConn.Read(reply) - if err != nil { - log.Printf("read reply from %s , err:%s", *s.cfg.Parent, err) - utils.CloseConn(inConn) - utils.CloseConn(&outConn) - return - } - //log.Printf("method reply %v", reply[:n]) - - //build request - buf, err = s.buildRequest(address) - if err != nil { - log.Printf("build request to %s fail , err:%s", *s.cfg.Parent, err) - utils.CloseConn(inConn) - utils.CloseConn(&outConn) - return - } - //send address request - _, err = outConn.Write(buf) - if err != nil { - log.Printf("write request to %s fail, err:%s", *s.cfg.Parent, err) - utils.CloseConn(inConn) - utils.CloseConn(&outConn) - return - } - //read reply - reply = make([]byte, 512) - n, err = outConn.Read(reply) - if err != nil { - log.Printf("read reply from %s , err:%s", *s.cfg.Parent, err) - utils.CloseConn(inConn) - utils.CloseConn(&outConn) - return - } - - //log.Printf("request reply %v", reply[:n]) } //forward client data to target,if necessary. if len(forwardBytes) > 0 { @@ -283,6 +285,34 @@ func (s *SPS) OutToTCP(inConn *net.Conn) (err error) { log.Printf("conn %s - %s connected", inAddr, outAddr) return } +func (s *SPS) InitBasicAuth() (err error) { + if *s.cfg.DNSAddress != "" { + s.basicAuth = utils.NewBasicAuth(&(*s).domainResolver) + } else { + s.basicAuth = utils.NewBasicAuth(nil) + } + if *s.cfg.AuthURL != "" { + s.basicAuth.SetAuthURL(*s.cfg.AuthURL, *s.cfg.AuthURLOkCode, *s.cfg.AuthURLTimeout, *s.cfg.AuthURLRetry) + log.Printf("auth from %s", *s.cfg.AuthURL) + } + if *s.cfg.AuthFile != "" { + var n = 0 + n, err = s.basicAuth.AddFromFile(*s.cfg.AuthFile) + if err != nil { + err = fmt.Errorf("auth-file ERR:%s", err) + return + } + log.Printf("auth data added from file %d , total:%d", n, s.basicAuth.Total()) + } + if len(*s.cfg.Auth) > 0 { + n := s.basicAuth.Add(*s.cfg.Auth) + log.Printf("auth data added %d, total:%d", n, s.basicAuth.Total()) + } + return +} +func (s *SPS) IsBasicAuth() bool { + return *s.cfg.AuthFile != "" || len(*s.cfg.Auth) > 0 || *s.cfg.AuthURL != "" +} func (s *SPS) buildRequest(address string) (buf []byte, err error) { host, portStr, err := net.SplitHostPort(address) if err != nil { diff --git a/utils/socks/client.go b/utils/socks/client.go index 7253d21..619e3c7 100644 --- a/utils/socks/client.go +++ b/utils/socks/client.go @@ -63,7 +63,7 @@ func NewClientConn(conn *net.Conn, network, target string, timeout time.Duration // connect takes an existing connection to a socks5 proxy server, // and commands the server to extend that connection to target, // which must be a canonical address with a host and port. -func (s *ClientConn) Connect() error { +func (s *ClientConn) Handshake() error { host, portStr, err := net.SplitHostPort(s.addr) if err != nil { return err diff --git a/utils/socks/server.go b/utils/socks/server.go index 121798a..3cf00be 100644 --- a/utils/socks/server.go +++ b/utils/socks/server.go @@ -106,14 +106,15 @@ func (s *ServerConn) Handshake() (err error) { err = fmt.Errorf("new methods request fail,ERR: %s", e) return } - if s.auth == nil { - if !methodReq.Select(Method_NO_AUTH) { - (*s.conn).SetReadDeadline(time.Now().Add(time.Second * s.timeout)) - methodReq.Reply(Method_NONE_ACCEPTABLE) - (*s.conn).SetReadDeadline(time.Time{}) - err = fmt.Errorf("none method found : Method_NO_AUTH") - return - } + //log.Printf("%v,s.auth == %v && methodReq.Select(Method_NO_AUTH) %v", methodReq.methods, s.auth, methodReq.Select(Method_NO_AUTH)) + if s.auth == nil && methodReq.Select(Method_NO_AUTH) && !methodReq.Select(Method_USER_PASS) { + // if !methodReq.Select(Method_NO_AUTH) { + // (*s.conn).SetReadDeadline(time.Now().Add(time.Second * s.timeout)) + // methodReq.Reply(Method_NONE_ACCEPTABLE) + // (*s.conn).SetReadDeadline(time.Time{}) + // err = fmt.Errorf("none method found : Method_NO_AUTH") + // return + // } s.method = Method_NO_AUTH //method select reply (*s.conn).SetReadDeadline(time.Now().Add(time.Second * s.timeout)) @@ -158,7 +159,7 @@ func (s *ServerConn) Handshake() (err error) { //err = fmt.Errorf("user:%s,pass:%s", user, pass) //auth _addr := strings.Split(remoteAddr.String(), ":") - if s.auth.CheckUserPass(s.user, s.password, _addr[0], "") { + if s.auth == nil || s.auth.CheckUserPass(s.user, s.password, _addr[0], "") { (*s.conn).SetDeadline(time.Now().Add(time.Millisecond * time.Duration(s.timeout))) _, err = (*s.conn).Write([]byte{0x01, 0x00}) (*s.conn).SetDeadline(time.Time{}) diff --git a/utils/structs.go b/utils/structs.go index dc8dcfd..764622d 100644 --- a/utils/structs.go +++ b/utils/structs.go @@ -317,7 +317,7 @@ func NewHTTPRequest(inConn *net.Conn, bufSize int, isBasicAuth bool, basicAuth * req = HTTPRequest{ conn: inConn, } - if len(header) == 1 { + if header != nil && len(header) == 1 && len(header[0]) > 1 { buf = header[0] n = len(header[0]) } else { @@ -402,18 +402,13 @@ func (req *HTTPRequest) IsHTTPS() bool { return req.Method == "CONNECT" } -func (req *HTTPRequest) BasicAuth() (err error) { - +func (req *HTTPRequest) GetAuthDataStr() (basicInfo string, err error) { // log.Printf("request :%s", string(req.HeadBuf)) - code := "407" authorization := req.getHeader("Proxy-Authorization") - // if authorization == "" { - // authorization = req.getHeader("Authorization") - // code = "401" - // } + authorization = strings.Trim(authorization, " \r\n\t") if authorization == "" { - fmt.Fprintf((*req.conn), "HTTP/1.1 %s Unauthorized\r\nWWW-Authenticate: Basic realm=\"\"\r\n\r\nUnauthorized", code) + fmt.Fprintf((*req.conn), "HTTP/1.1 %s Unauthorized\r\nWWW-Authenticate: Basic realm=\"\"\r\n\r\nUnauthorized", "407") CloseConn(req.conn) err = errors.New("require auth header data") return @@ -431,6 +426,10 @@ func (req *HTTPRequest) BasicAuth() (err error) { CloseConn(req.conn) return } + basicInfo = string(user) + return +} +func (req *HTTPRequest) BasicAuth() (err error) { addr := strings.Split((*req.conn).RemoteAddr().String(), ":") URL := "" if req.IsHTTPS() { @@ -438,10 +437,14 @@ func (req *HTTPRequest) BasicAuth() (err error) { } else { URL = req.getHTTPURL() } + user, err := req.GetAuthDataStr() + if err != nil { + return + } authOk := (*req.basicAuth).Check(string(user), addr[0], URL) //log.Printf("auth %s,%v", string(user), authOk) if !authOk { - fmt.Fprintf((*req.conn), "HTTP/1.1 %s Unauthorized\r\n\r\nUnauthorized", code) + fmt.Fprintf((*req.conn), "HTTP/1.1 %s Unauthorized\r\n\r\nUnauthorized", "407") CloseConn(req.conn) err = fmt.Errorf("basic auth fail") return