diff --git a/README.md b/README.md index d3456818..6ee6a3f9 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,11 @@ external-controller: 127.0.0.1:9090 experimental: ignore-resolve-fail: true # ignore dns resolve fail, default value is true +# authentication of local SOCKS5/HTTP(S) server +# authentication: +# - "user1:pass1" +# - "user2:pass2" + # dns: # enable: true # set true to enable dns (default is false) # ipv6: false # default is false diff --git a/component/auth/auth.go b/component/auth/auth.go new file mode 100644 index 00000000..98414d8b --- /dev/null +++ b/component/auth/auth.go @@ -0,0 +1,46 @@ +package auth + +import ( + "sync" +) + +type Authenticator interface { + Verify(user string, pass string) bool + Users() []string +} + +type AuthUser struct { + User string + Pass string +} + +type inMemoryAuthenticator struct { + storage *sync.Map + usernames []string +} + +func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { + realPass, ok := au.storage.Load(user) + return ok && realPass == pass +} + +func (au *inMemoryAuthenticator) Users() []string { return au.usernames } + +func NewAuthenticator(users []AuthUser) Authenticator { + if len(users) == 0 { + return nil + } + + au := &inMemoryAuthenticator{storage: &sync.Map{}} + for _, user := range users { + au.storage.Store(user.User, user.Pass) + } + usernames := make([]string, 0, len(users)) + au.storage.Range(func(key, value interface{}) bool { + usernames = append(usernames, key.(string)) + return true + }) + au.usernames = usernames + + return au +} diff --git a/component/socks5/socks5.go b/component/socks5/socks5.go index 3dbe2229..e945fe2c 100644 --- a/component/socks5/socks5.go +++ b/component/socks5/socks5.go @@ -6,6 +6,8 @@ import ( "io" "net" "strconv" + + "github.com/Dreamacro/clash/component/auth" ) // Error represents a SOCKS error @@ -35,6 +37,9 @@ const ( // MaxAddrLen is the maximum size of SOCKS address in bytes. const MaxAddrLen = 1 + 1 + 255 + 2 +// MaxAuthLen is the maximum size of user/password field in SOCKS5 Auth +const MaxAuthLen = 255 + // Addr represents a SOCKS address as defined in RFC 1928 section 5. type Addr = []byte @@ -50,13 +55,16 @@ const ( ErrAddressNotSupported = Error(8) ) +// Auth errors used to return a specific "Auth failed" error +var ErrAuth = errors.New("auth failed") + type User struct { Username string Password string } // ServerHandshake fast-tracks SOCKS initialization to get target address to connect on server side. -func ServerHandshake(rw io.ReadWriter) (addr Addr, command Command, err error) { +func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, err error) { // Read RFC 1928 for request and reply structure and sizes. buf := make([]byte, MaxAddrLen) // read VER, NMETHODS, METHODS @@ -67,10 +75,64 @@ func ServerHandshake(rw io.ReadWriter) (addr Addr, command Command, err error) { if _, err = io.ReadFull(rw, buf[:nmethods]); err != nil { return } + // write VER METHOD - if _, err = rw.Write([]byte{5, 0}); err != nil { - return + if authenticator != nil { + if _, err = rw.Write([]byte{5, 2}); err != nil { + return + } + + // Get header + header := make([]byte, 2) + if _, err = io.ReadFull(rw, header); err != nil { + return + } + + authBuf := make([]byte, MaxAuthLen) + // Get username + userLen := int(header[1]) + if userLen <= 0 { + rw.Write([]byte{1, 1}) + err = ErrAuth + return + } + if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil { + return + } + user := string(authBuf[:userLen]) + + // Get password + if _, err = rw.Read(header[:1]); err != nil { + return + } + passLen := int(header[0]) + if passLen <= 0 { + rw.Write([]byte{1, 1}) + err = ErrAuth + return + } + if _, err = io.ReadFull(rw, authBuf[:passLen]); err != nil { + return + } + pass := string(authBuf[:passLen]) + + // Verify + if ok := authenticator.Verify(string(user), string(pass)); !ok { + rw.Write([]byte{1, 1}) + err = ErrAuth + return + } + + // Response auth state + if _, err = rw.Write([]byte{1, 0}); err != nil { + return + } + } else { + if _, err = rw.Write([]byte{5, 0}); err != nil { + return + } } + // read VER CMD RSV ATYP DST.ADDR DST.PORT if _, err = io.ReadFull(rw, buf[:3]); err != nil { return diff --git a/config/config.go b/config/config.go index 8b4e0bc9..6107bde5 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,7 @@ import ( adapters "github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/common/structure" + "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/fakeip" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" @@ -26,6 +27,7 @@ type General struct { Port int `json:"port"` SocksPort int `json:"socks-port"` RedirPort int `json:"redir-port"` + Authentication []string `json:"authentication"` AllowLan bool `json:"allow-lan"` Mode T.Mode `json:"mode"` LogLevel log.LogLevel `json:"log-level"` @@ -56,6 +58,7 @@ type Config struct { DNS *DNS Experimental *Experimental Rules []C.Rule + Users []auth.AuthUser Proxies map[string]C.Proxy } @@ -73,6 +76,7 @@ type rawConfig struct { Port int `yaml:"port"` SocksPort int `yaml:"socks-port"` RedirPort int `yaml:"redir-port"` + Authentication []string `yaml:"authentication"` AllowLan bool `yaml:"allow-lan"` Mode T.Mode `yaml:"mode"` LogLevel log.LogLevel `yaml:"log-level"` @@ -117,12 +121,13 @@ func readConfig(path string) (*rawConfig, error) { // config with some default value rawConfig := &rawConfig{ - AllowLan: false, - Mode: T.Rule, - LogLevel: log.INFO, - Rule: []string{}, - Proxy: []map[string]interface{}{}, - ProxyGroup: []map[string]interface{}{}, + AllowLan: false, + Mode: T.Rule, + Authentication: []string{}, + LogLevel: log.INFO, + Rule: []string{}, + Proxy: []map[string]interface{}{}, + ProxyGroup: []map[string]interface{}{}, Experimental: Experimental{ IgnoreResolveFail: true, }, @@ -169,6 +174,8 @@ func Parse(path string) (*Config, error) { } config.DNS = dnsCfg + config.Users = parseAuthentication(rawCfg.Authentication) + return config, nil } @@ -520,3 +527,14 @@ func parseDNS(cfg rawDNS) (*DNS, error) { return dnsCfg, nil } + +func parseAuthentication(rawRecords []string) []auth.AuthUser { + users := make([]auth.AuthUser, 0) + for _, line := range rawRecords { + userData := strings.SplitN(line, ":", 2) + if len(userData) == 2 { + users = append(users, auth.AuthUser{User: userData[0], Pass: userData[1]}) + } + } + return users +} diff --git a/hub/executor/executor.go b/hub/executor/executor.go index ee401edd..0686cd0c 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -1,11 +1,13 @@ package executor import ( + "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/log" P "github.com/Dreamacro/clash/proxy" + authStore "github.com/Dreamacro/clash/proxy/auth" T "github.com/Dreamacro/clash/tunnel" ) @@ -21,6 +23,7 @@ func ParseWithPath(path string) (*config.Config, error) { // ApplyConfig dispatch configure to all parts func ApplyConfig(cfg *config.Config, force bool) { + updateUsers(cfg.Users) if force { updateGeneral(cfg.General) } @@ -33,12 +36,13 @@ func ApplyConfig(cfg *config.Config, force bool) { func GetGeneral() *config.General { ports := P.GetPorts() return &config.General{ - Port: ports.Port, - SocksPort: ports.SocksPort, - RedirPort: ports.RedirPort, - AllowLan: P.AllowLan(), - Mode: T.Instance().Mode(), - LogLevel: log.Level(), + Port: ports.Port, + SocksPort: ports.SocksPort, + RedirPort: ports.RedirPort, + Authentication: authStore.Authenticator().Users(), + AllowLan: P.AllowLan(), + Mode: T.Instance().Mode(), + LogLevel: log.Level(), } } @@ -90,6 +94,7 @@ func updateGeneral(general *config.General) { allowLan := general.AllowLan P.SetAllowLan(allowLan) + if err := P.ReCreateHTTP(general.Port); err != nil { log.Errorln("Start HTTP server error: %s", err.Error()) } @@ -102,3 +107,11 @@ func updateGeneral(general *config.General) { log.Errorln("Start Redir server error: %s", err.Error()) } } + +func updateUsers(users []auth.AuthUser) { + authenticator := auth.NewAuthenticator(users) + authStore.SetAuthenticator(authenticator) + if authenticator != nil { + log.Infoln("Authentication of local server updated") + } +} diff --git a/proxy/auth/auth.go b/proxy/auth/auth.go new file mode 100644 index 00000000..2c29e186 --- /dev/null +++ b/proxy/auth/auth.go @@ -0,0 +1,17 @@ +package auth + +import ( + "github.com/Dreamacro/clash/component/auth" +) + +var ( + authenticator auth.Authenticator +) + +func Authenticator() auth.Authenticator { + return authenticator +} + +func SetAuthenticator(au auth.Authenticator) { + authenticator = au +} diff --git a/proxy/http/server.go b/proxy/http/server.go index 1ed91332..51941ef6 100644 --- a/proxy/http/server.go +++ b/proxy/http/server.go @@ -2,11 +2,17 @@ package http import ( "bufio" + "encoding/base64" "net" "net/http" + "strings" + "time" adapters "github.com/Dreamacro/clash/adapters/inbound" + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/log" + authStore "github.com/Dreamacro/clash/proxy/auth" "github.com/Dreamacro/clash/tunnel" ) @@ -18,6 +24,7 @@ type HttpListener struct { net.Listener address string closed bool + cache *cache.Cache } func NewHttpProxy(addr string) (*HttpListener, error) { @@ -25,10 +32,11 @@ func NewHttpProxy(addr string) (*HttpListener, error) { if err != nil { return nil, err } - hl := &HttpListener{l, addr, false} + hl := &HttpListener{l, addr, false, cache.New(30 * time.Second)} go func() { log.Infoln("HTTP proxy listening at: %s", addr) + for { c, err := hl.Accept() if err != nil { @@ -37,7 +45,7 @@ func NewHttpProxy(addr string) (*HttpListener, error) { } continue } - go handleConn(c) + go handleConn(c, hl.cache) } }() @@ -53,7 +61,19 @@ func (l *HttpListener) Address() string { return l.address } -func handleConn(conn net.Conn) { +func canActivate(loginStr string, authenticator auth.Authenticator, cache *cache.Cache) (ret bool) { + if result := cache.Get(loginStr); result != nil { + ret = result.(bool) + } + loginData, err := base64.StdEncoding.DecodeString(loginStr) + login := strings.Split(string(loginData), ":") + ret = err == nil && len(login) == 2 && authenticator.Verify(login[0], login[1]) + + cache.Put(loginStr, ret, time.Minute) + return +} + +func handleConn(conn net.Conn, cache *cache.Cache) { br := bufio.NewReader(conn) request, err := http.ReadRequest(br) if err != nil || request.URL.Host == "" { @@ -61,6 +81,20 @@ func handleConn(conn net.Conn) { return } + authenticator := authStore.Authenticator() + if authenticator != nil { + if authStrings := strings.Split(request.Header.Get("Proxy-Authorization"), " "); len(authStrings) != 2 { + _, err = conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n")) + conn.Close() + return + } else if !canActivate(authStrings[1], authenticator, cache) { + conn.Write([]byte("HTTP/1.1 403 Forbidden\r\n\r\n")) + log.Infoln("Auth failed from %s", conn.RemoteAddr().String()) + conn.Close() + return + } + } + if request.Method == http.MethodConnect { _, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) if err != nil { diff --git a/proxy/socks/tcp.go b/proxy/socks/tcp.go index 08789cbe..47bfa037 100644 --- a/proxy/socks/tcp.go +++ b/proxy/socks/tcp.go @@ -7,6 +7,7 @@ import ( "github.com/Dreamacro/clash/component/socks5" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" + authStore "github.com/Dreamacro/clash/proxy/auth" "github.com/Dreamacro/clash/tunnel" ) @@ -54,7 +55,7 @@ func (l *SockListener) Address() string { } func handleSocks(conn net.Conn) { - target, command, err := socks5.ServerHandshake(conn) + target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator()) if err != nil { conn.Close() return