Feature: local socks5/http(s) auth (#216)

This commit is contained in:
gVisor bot 2019-06-27 17:04:25 +08:00
parent d516009673
commit b115c369db
8 changed files with 215 additions and 19 deletions

View file

@ -109,6 +109,11 @@ external-controller: 127.0.0.1:9090
experimental: experimental:
ignore-resolve-fail: true # ignore dns resolve fail, default value is true 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: # dns:
# enable: true # set true to enable dns (default is false) # enable: true # set true to enable dns (default is false)
# ipv6: false # default is false # ipv6: false # default is false

46
component/auth/auth.go Normal file
View file

@ -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
}

View file

@ -6,6 +6,8 @@ import (
"io" "io"
"net" "net"
"strconv" "strconv"
"github.com/Dreamacro/clash/component/auth"
) )
// Error represents a SOCKS error // Error represents a SOCKS error
@ -35,6 +37,9 @@ const (
// MaxAddrLen is the maximum size of SOCKS address in bytes. // MaxAddrLen is the maximum size of SOCKS address in bytes.
const MaxAddrLen = 1 + 1 + 255 + 2 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. // Addr represents a SOCKS address as defined in RFC 1928 section 5.
type Addr = []byte type Addr = []byte
@ -50,13 +55,16 @@ const (
ErrAddressNotSupported = Error(8) ErrAddressNotSupported = Error(8)
) )
// Auth errors used to return a specific "Auth failed" error
var ErrAuth = errors.New("auth failed")
type User struct { type User struct {
Username string Username string
Password string Password string
} }
// ServerHandshake fast-tracks SOCKS initialization to get target address to connect on server side. // 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. // Read RFC 1928 for request and reply structure and sizes.
buf := make([]byte, MaxAddrLen) buf := make([]byte, MaxAddrLen)
// read VER, NMETHODS, METHODS // 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 { if _, err = io.ReadFull(rw, buf[:nmethods]); err != nil {
return return
} }
// write VER METHOD // write VER METHOD
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 { if _, err = rw.Write([]byte{5, 0}); err != nil {
return return
} }
}
// read VER CMD RSV ATYP DST.ADDR DST.PORT // read VER CMD RSV ATYP DST.ADDR DST.PORT
if _, err = io.ReadFull(rw, buf[:3]); err != nil { if _, err = io.ReadFull(rw, buf[:3]); err != nil {
return return

View file

@ -11,6 +11,7 @@ import (
adapters "github.com/Dreamacro/clash/adapters/outbound" adapters "github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
@ -26,6 +27,7 @@ type General struct {
Port int `json:"port"` Port int `json:"port"`
SocksPort int `json:"socks-port"` SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"` RedirPort int `json:"redir-port"`
Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"` AllowLan bool `json:"allow-lan"`
Mode T.Mode `json:"mode"` Mode T.Mode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"` LogLevel log.LogLevel `json:"log-level"`
@ -56,6 +58,7 @@ type Config struct {
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser
Proxies map[string]C.Proxy Proxies map[string]C.Proxy
} }
@ -73,6 +76,7 @@ type rawConfig struct {
Port int `yaml:"port"` Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"` SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"` RedirPort int `yaml:"redir-port"`
Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"` AllowLan bool `yaml:"allow-lan"`
Mode T.Mode `yaml:"mode"` Mode T.Mode `yaml:"mode"`
LogLevel log.LogLevel `yaml:"log-level"` LogLevel log.LogLevel `yaml:"log-level"`
@ -119,6 +123,7 @@ func readConfig(path string) (*rawConfig, error) {
rawConfig := &rawConfig{ rawConfig := &rawConfig{
AllowLan: false, AllowLan: false,
Mode: T.Rule, Mode: T.Rule,
Authentication: []string{},
LogLevel: log.INFO, LogLevel: log.INFO,
Rule: []string{}, Rule: []string{},
Proxy: []map[string]interface{}{}, Proxy: []map[string]interface{}{},
@ -169,6 +174,8 @@ func Parse(path string) (*Config, error) {
} }
config.DNS = dnsCfg config.DNS = dnsCfg
config.Users = parseAuthentication(rawCfg.Authentication)
return config, nil return config, nil
} }
@ -520,3 +527,14 @@ func parseDNS(cfg rawDNS) (*DNS, error) {
return dnsCfg, nil 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
}

View file

@ -1,11 +1,13 @@
package executor package executor
import ( import (
"github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
P "github.com/Dreamacro/clash/proxy" P "github.com/Dreamacro/clash/proxy"
authStore "github.com/Dreamacro/clash/proxy/auth"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
) )
@ -21,6 +23,7 @@ func ParseWithPath(path string) (*config.Config, error) {
// ApplyConfig dispatch configure to all parts // ApplyConfig dispatch configure to all parts
func ApplyConfig(cfg *config.Config, force bool) { func ApplyConfig(cfg *config.Config, force bool) {
updateUsers(cfg.Users)
if force { if force {
updateGeneral(cfg.General) updateGeneral(cfg.General)
} }
@ -36,6 +39,7 @@ func GetGeneral() *config.General {
Port: ports.Port, Port: ports.Port,
SocksPort: ports.SocksPort, SocksPort: ports.SocksPort,
RedirPort: ports.RedirPort, RedirPort: ports.RedirPort,
Authentication: authStore.Authenticator().Users(),
AllowLan: P.AllowLan(), AllowLan: P.AllowLan(),
Mode: T.Instance().Mode(), Mode: T.Instance().Mode(),
LogLevel: log.Level(), LogLevel: log.Level(),
@ -90,6 +94,7 @@ func updateGeneral(general *config.General) {
allowLan := general.AllowLan allowLan := general.AllowLan
P.SetAllowLan(allowLan) P.SetAllowLan(allowLan)
if err := P.ReCreateHTTP(general.Port); err != nil { if err := P.ReCreateHTTP(general.Port); err != nil {
log.Errorln("Start HTTP server error: %s", err.Error()) 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()) 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")
}
}

17
proxy/auth/auth.go Normal file
View file

@ -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
}

View file

@ -2,11 +2,17 @@ package http
import ( import (
"bufio" "bufio"
"encoding/base64"
"net" "net"
"net/http" "net/http"
"strings"
"time"
adapters "github.com/Dreamacro/clash/adapters/inbound" adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
authStore "github.com/Dreamacro/clash/proxy/auth"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
) )
@ -18,6 +24,7 @@ type HttpListener struct {
net.Listener net.Listener
address string address string
closed bool closed bool
cache *cache.Cache
} }
func NewHttpProxy(addr string) (*HttpListener, error) { func NewHttpProxy(addr string) (*HttpListener, error) {
@ -25,10 +32,11 @@ func NewHttpProxy(addr string) (*HttpListener, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
hl := &HttpListener{l, addr, false} hl := &HttpListener{l, addr, false, cache.New(30 * time.Second)}
go func() { go func() {
log.Infoln("HTTP proxy listening at: %s", addr) log.Infoln("HTTP proxy listening at: %s", addr)
for { for {
c, err := hl.Accept() c, err := hl.Accept()
if err != nil { if err != nil {
@ -37,7 +45,7 @@ func NewHttpProxy(addr string) (*HttpListener, error) {
} }
continue continue
} }
go handleConn(c) go handleConn(c, hl.cache)
} }
}() }()
@ -53,7 +61,19 @@ func (l *HttpListener) Address() string {
return l.address 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) br := bufio.NewReader(conn)
request, err := http.ReadRequest(br) request, err := http.ReadRequest(br)
if err != nil || request.URL.Host == "" { if err != nil || request.URL.Host == "" {
@ -61,6 +81,20 @@ func handleConn(conn net.Conn) {
return 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 { if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) _, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil { if err != nil {

View file

@ -7,6 +7,7 @@ import (
"github.com/Dreamacro/clash/component/socks5" "github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
authStore "github.com/Dreamacro/clash/proxy/auth"
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
) )
@ -54,7 +55,7 @@ func (l *SockListener) Address() string {
} }
func handleSocks(conn net.Conn) { func handleSocks(conn net.Conn) {
target, command, err := socks5.ServerHandshake(conn) target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator())
if err != nil { if err != nil {
conn.Close() conn.Close()
return return