Feature: local socks5/http(s) auth (#216)
This commit is contained in:
parent
d516009673
commit
b115c369db
8 changed files with 215 additions and 19 deletions
|
@ -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
|
||||
|
|
46
component/auth/auth.go
Normal file
46
component/auth/auth.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
17
proxy/auth/auth.go
Normal file
17
proxy/auth/auth.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue