Implement client side ACL for SOCKS5 TCP

This commit is contained in:
Toby
2020-04-26 14:58:50 -07:00
parent ee8558f2fb
commit 127e9e1b6c
8 changed files with 232 additions and 77 deletions

View File

@@ -5,6 +5,7 @@ import (
"crypto/x509"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/tobyxdd/hysteria/pkg/acl"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
@@ -59,6 +60,14 @@ func proxyClient(args []string) {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
var aclEngine *acl.Engine
if len(config.ACLFile) > 0 {
aclEngine, err = acl.LoadFromFile(config.ACLFile)
if err != nil {
log.Fatalln("Unable to parse ACL:", err)
}
}
client, err := core.NewClient(config.ServerAddr, config.Username, config.Password, tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.SendAlgorithmWithDebugInfos {
@@ -70,9 +79,9 @@ func proxyClient(args []string) {
defer client.Close()
log.Println("Connected to", config.ServerAddr)
socks5server, err := socks5.NewServer(client, config.SOCKS5Addr, nil, config.SOCKS5Timeout,
func(addr net.Addr, reqAddr string) {
log.Printf("[TCP] %s <-> %s\n", addr.String(), reqAddr)
socks5server, err := socks5.NewServer(client, config.SOCKS5Addr, nil, config.SOCKS5Timeout, aclEngine,
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
log.Printf("[TCP] [%s] %s <-> %s\n", actionToString(action, arg), addr.String(), reqAddr)
},
func(addr net.Addr, reqAddr string, err error) {
log.Printf("Closed [TCP] %s <-> %s: %s\n", addr.String(), reqAddr, err.Error())
@@ -83,8 +92,8 @@ func proxyClient(args []string) {
func(addr net.Addr, err error) {
log.Printf("Closed [UDP] Associate %s: %s\n", addr.String(), err.Error())
},
func(addr net.Addr, reqAddr string) {
log.Printf("[UDP] %s <-> %s\n", addr.String(), reqAddr)
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
log.Printf("[UDP] [%s] %s <-> %s\n", actionToString(action, arg), addr.String(), reqAddr)
},
func(addr net.Addr, reqAddr string, err error) {
log.Printf("Closed [UDP] %s <-> %s: %s\n", addr.String(), reqAddr, err.Error())
@@ -96,3 +105,18 @@ func proxyClient(args []string) {
log.Fatalln(socks5server.ListenAndServe())
}
func actionToString(action acl.Action, arg string) string {
switch action {
case acl.ActionDirect:
return "Direct"
case acl.ActionProxy:
return "Proxy"
case acl.ActionBlock:
return "Block"
case acl.ActionHijack:
return "Hijack to " + arg
default:
return "Unknown"
}
}

View File

@@ -7,6 +7,7 @@ const proxyTLSProtocol = "hysteria-proxy"
type proxyClientConfig struct {
SOCKS5Addr string `json:"socks5_addr" desc:"SOCKS5 listen address"`
SOCKS5Timeout int `json:"socks5_timeout" desc:"SOCKS5 connection timeout in seconds"`
ACLFile string `json:"acl" desc:"Access control list"`
ServerAddr string `json:"server" desc:"Server address"`
Username string `json:"username" desc:"Authentication username"`
Password string `json:"password" desc:"Authentication password"`

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.14
require github.com/golang/protobuf v1.3.1
require (
github.com/hashicorp/golang-lru v0.5.4
github.com/lucas-clemente/quic-go v0.15.2
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/txthinking/runnergroup v0.0.0-20200327135940-540a793bb997 // indirect

2
go.sum
View File

@@ -50,6 +50,8 @@ github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE0
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=

View File

@@ -2,14 +2,23 @@ package acl
import (
"bufio"
lru "github.com/hashicorp/golang-lru"
"net"
"os"
"strings"
)
const entryCacheSize = 1024
type Engine struct {
DefaultAction Action
Entries []Entry
Cache *lru.ARCCache
}
type cacheEntry struct {
Action Action
Arg string
}
func LoadFromFile(filename string) (*Engine, error) {
@@ -32,20 +41,48 @@ func LoadFromFile(filename string) (*Engine, error) {
}
entries = append(entries, entry)
}
cache, err := lru.NewARC(entryCacheSize)
if err != nil {
return nil, err
}
return &Engine{
DefaultAction: ActionProxy,
Entries: entries,
Cache: cache,
}, nil
}
func (e *Engine) Lookup(domain string, ip net.IP) (Action, string) {
if len(domain) == 0 && ip == nil {
return e.DefaultAction, ""
if len(domain) > 0 {
// Domain
if v, ok := e.Cache.Get(domain); ok {
// Cache hit
ce := v.(cacheEntry)
return ce.Action, ce.Arg
}
ips, _ := net.LookupIP(domain)
for _, entry := range e.Entries {
if entry.Match(domain, ip) {
if entry.MatchDomain(domain) || (len(ips) > 0 && entry.MatchIPs(ips)) {
e.Cache.Add(domain, cacheEntry{entry.Action, entry.ActionArg})
return entry.Action, entry.ActionArg
}
}
return e.DefaultAction, ""
} else if ip != nil {
// IP
if v, ok := e.Cache.Get(ip.String()); ok {
// Cache hit
ce := v.(cacheEntry)
return ce.Action, ce.Arg
}
for _, entry := range e.Entries {
if entry.MatchIP(ip) {
e.Cache.Add(ip.String(), cacheEntry{entry.Action, entry.ActionArg})
return entry.Action, entry.ActionArg
}
}
return e.DefaultAction, ""
} else {
return e.DefaultAction, ""
}
}

View File

@@ -1,11 +1,13 @@
package acl
import (
lru "github.com/hashicorp/golang-lru"
"net"
"testing"
)
func TestEngine_Lookup(t *testing.T) {
cache, _ := lru.NewARC(4)
e := &Engine{
DefaultAction: ActionDirect,
Entries: []Entry{
@@ -45,6 +47,7 @@ func TestEngine_Lookup(t *testing.T) {
ActionArg: "",
},
},
Cache: cache,
}
type args struct {
domain string

View File

@@ -25,13 +25,10 @@ type Entry struct {
ActionArg string
}
func (e Entry) Match(domain string, ip net.IP) bool {
func (e Entry) MatchDomain(domain string) bool {
if e.All {
return true
}
if e.Net != nil && ip != nil {
return e.Net.Contains(ip)
}
if len(e.Domain) > 0 && len(domain) > 0 {
ld := strings.ToLower(domain)
if e.Suffix {
@@ -43,6 +40,30 @@ func (e Entry) Match(domain string, ip net.IP) bool {
return false
}
func (e Entry) MatchIP(ip net.IP) bool {
if e.All {
return true
}
if e.Net != nil && ip != nil {
return e.Net.Contains(ip)
}
return false
}
func (e Entry) MatchIPs(ips []net.IP) bool {
if e.All {
return true
}
if e.Net != nil && len(ips) > 0 {
for _, ip := range ips {
if e.Net.Contains(ip) {
return true
}
}
}
return false
}
// Format: action cond_type cond arg
// Examples:
// proxy domain-suffix google.com

View File

@@ -1,10 +1,14 @@
package socks5
import (
"encoding/binary"
"errors"
"fmt"
"github.com/tobyxdd/hysteria/internal/utils"
"github.com/tobyxdd/hysteria/pkg/acl"
"github.com/tobyxdd/hysteria/pkg/core"
"io"
"strconv"
)
import (
@@ -24,21 +28,26 @@ type Server struct {
Method byte
TCPAddr *net.TCPAddr
TCPDeadline int
ACLEngine *acl.Engine
NewRequestFunc func(addr net.Addr, reqAddr string)
NewRequestFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string)
RequestClosedFunc func(addr net.Addr, reqAddr string, err error)
NewUDPAssociateFunc func(addr net.Addr)
UDPAssociateClosedFunc func(addr net.Addr, err error)
NewUDPTunnelFunc func(addr net.Addr, reqAddr string)
NewUDPTunnelFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string)
UDPTunnelClosedFunc func(addr net.Addr, reqAddr string, err error)
tcpListener *net.TCPListener
}
func NewServer(hyClient core.Client, addr string, authFunc func(username, password string) bool, tcpDeadline int,
newReqFunc func(addr net.Addr, reqAddr string), reqClosedFunc func(addr net.Addr, reqAddr string, err error),
newUDPAssociateFunc func(addr net.Addr), udpAssociateClosedFunc func(addr net.Addr, err error),
newUDPTunnelFunc func(addr net.Addr, reqAddr string), udpTunnelClosedFunc func(addr net.Addr, reqAddr string, err error)) (*Server, error) {
aclEngine *acl.Engine,
newReqFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string),
reqClosedFunc func(addr net.Addr, reqAddr string, err error),
newUDPAssociateFunc func(addr net.Addr),
udpAssociateClosedFunc func(addr net.Addr, err error),
newUDPTunnelFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string),
udpTunnelClosedFunc func(addr net.Addr, reqAddr string, err error)) (*Server, error) {
taddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
@@ -54,6 +63,7 @@ func NewServer(hyClient core.Client, addr string, authFunc func(username, passwo
Method: m,
TCPAddr: taddr,
TCPDeadline: tcpDeadline,
ACLEngine: aclEngine,
NewRequestFunc: newReqFunc,
RequestClosedFunc: reqClosedFunc,
NewUDPAssociateFunc: newUDPAssociateFunc,
@@ -141,24 +151,74 @@ func (s *Server) ListenAndServe() error {
func (s *Server) handle(c *net.TCPConn, r *socks5.Request) error {
if r.Cmd == socks5.CmdConnect {
// TCP
s.NewRequestFunc(c.RemoteAddr(), r.Address())
return s.handleTCP(c, r)
} else if r.Cmd == socks5.CmdUDP {
// UDP
return s.handleUDP(c, r)
} else {
_ = sendReply(c, socks5.RepCommandNotSupported)
return ErrUnsupportedCmd
}
}
func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
domain, ip, port, addr := parseRequestAddress(r)
action, arg := acl.ActionProxy, ""
if s.ACLEngine != nil {
action, arg = s.ACLEngine.Lookup(domain, ip)
}
s.NewRequestFunc(c.RemoteAddr(), addr, action, arg)
var closeErr error
defer func() {
s.RequestClosedFunc(c.RemoteAddr(), r.Address(), closeErr)
s.RequestClosedFunc(c.RemoteAddr(), addr, closeErr)
}()
rc, err := s.HyClient.Dial(false, r.Address())
// Handle according to the action
switch action {
case acl.ActionDirect:
rc, err := net.Dial("tcp", addr)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
// All good
_ = sendReply(c, socks5.RepSuccess)
closeErr = pipePair(c, rc, s.TCPDeadline)
return nil
} else if r.Cmd == socks5.CmdUDP {
// UDP
case acl.ActionProxy:
rc, err := s.HyClient.Dial(false, addr)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = pipePair(c, rc, s.TCPDeadline)
return nil
case acl.ActionBlock:
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = errors.New("blocked in ACL")
return nil
case acl.ActionHijack:
rc, err := net.Dial("tcp", net.JoinHostPort(arg, port))
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = pipePair(c, rc, s.TCPDeadline)
return nil
default:
_ = sendReply(c, socks5.RepServerFailure)
closeErr = fmt.Errorf("unknown action %d", action)
return nil
}
}
func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
s.NewUDPAssociateFunc(c.RemoteAddr())
var closeErr error
defer func() {
@@ -183,7 +243,7 @@ func (s *Server) handle(c *net.TCPConn, r *socks5.Request) error {
}
_, _ = socks5.NewReply(socks5.RepSuccess, atyp, addr, port).WriteTo(c)
// Let UDP server do its job, we hold the TCP connection here
go s.handleUDP(udpConn)
go s.udpServer(udpConn)
buf := make([]byte, 1024)
for {
if s.TCPDeadline != 0 {
@@ -197,13 +257,9 @@ func (s *Server) handle(c *net.TCPConn, r *socks5.Request) error {
}
// As the TCP connection closes, so does the UDP listener
return nil
} else {
_ = sendReply(c, socks5.RepCommandNotSupported)
return ErrUnsupportedCmd
}
}
func (s *Server) handleUDP(c *net.UDPConn) {
func (s *Server) udpServer(c *net.UDPConn) {
var clientAddr *net.UDPAddr
remoteMap := make(map[string]io.ReadWriteCloser) // Remote addr <-> Remote conn
buf := make([]byte, utils.PipeBufferSize)
@@ -238,7 +294,7 @@ func (s *Server) handleUDP(c *net.UDPConn) {
// The other direction
go udpReversePipe(clientAddr, c, rc)
remoteMap[d.Address()] = rc
s.NewUDPTunnelFunc(clientAddr, d.Address())
s.NewUDPTunnelFunc(clientAddr, d.Address(), acl.ActionProxy, "")
}
_, err = rc.Write(d.Data)
if err != nil {
@@ -261,6 +317,16 @@ func sendReply(conn *net.TCPConn, rep byte) error {
return err
}
func parseRequestAddress(r *socks5.Request) (domain string, ip net.IP, port string, addr string) {
p := strconv.Itoa(int(binary.BigEndian.Uint16(r.DstPort)))
if r.Atyp == socks5.ATYPDomain {
d := string(r.DstAddr[1:])
return d, nil, p, net.JoinHostPort(d, p)
} else {
return "", r.DstAddr, p, net.JoinHostPort(net.IP(r.DstAddr).String(), p)
}
}
func pipePair(conn *net.TCPConn, stream io.ReadWriteCloser, deadline int) error {
errChan := make(chan error, 2)
// TCP to stream