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:
|
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
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"
|
"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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
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 (
|
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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue