From 127e9e1b6cda583f189b160b8a75b1355854d218 Mon Sep 17 00:00:00 2001 From: Toby Date: Sun, 26 Apr 2020 14:58:50 -0700 Subject: [PATCH] Implement client side ACL for SOCKS5 TCP --- cmd/proxy_client.go | 34 ++++++-- cmd/proxy_config.go | 1 + go.mod | 1 + go.sum | 2 + pkg/acl/engine.go | 51 +++++++++-- pkg/acl/engine_test.go | 3 + pkg/acl/entry.go | 29 ++++++- pkg/socks5/server.go | 188 ++++++++++++++++++++++++++++------------- 8 files changed, 232 insertions(+), 77 deletions(-) diff --git a/cmd/proxy_client.go b/cmd/proxy_client.go index 8daad71..96440fe 100644 --- a/cmd/proxy_client.go +++ b/cmd/proxy_client.go @@ -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" + } +} diff --git a/cmd/proxy_config.go b/cmd/proxy_config.go index 3807641..93183b0 100644 --- a/cmd/proxy_config.go +++ b/cmd/proxy_config.go @@ -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"` diff --git a/go.mod b/go.mod index c6631fd..07dd908 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 538985b..3a0eac9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/acl/engine.go b/pkg/acl/engine.go index d37da65..43cfb6f 100644 --- a/pkg/acl/engine.go +++ b/pkg/acl/engine.go @@ -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 { + 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.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, "" } - for _, entry := range e.Entries { - if entry.Match(domain, ip) { - return entry.Action, entry.ActionArg - } - } - return e.DefaultAction, "" } diff --git a/pkg/acl/engine_test.go b/pkg/acl/engine_test.go index a43fa09..b382416 100644 --- a/pkg/acl/engine_test.go +++ b/pkg/acl/engine_test.go @@ -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 diff --git a/pkg/acl/entry.go b/pkg/acl/entry.go index 171cd78..18f7275 100644 --- a/pkg/acl/entry.go +++ b/pkg/acl/entry.go @@ -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 diff --git a/pkg/socks5/server.go b/pkg/socks5/server.go index 4e11e9e..c593f62 100644 --- a/pkg/socks5/server.go +++ b/pkg/socks5/server.go @@ -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,69 +151,115 @@ 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()) - var closeErr error - defer func() { - s.RequestClosedFunc(c.RemoteAddr(), r.Address(), closeErr) - }() - rc, err := s.HyClient.Dial(false, r.Address()) - 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 + return s.handleTCP(c, r) } else if r.Cmd == socks5.CmdUDP { // UDP - s.NewUDPAssociateFunc(c.RemoteAddr()) - var closeErr error - defer func() { - s.UDPAssociateClosedFunc(c.RemoteAddr(), closeErr) - }() - udpConn, err := net.ListenUDP("udp", &net.UDPAddr{ - IP: s.TCPAddr.IP, - Zone: s.TCPAddr.Zone, - }) - if err != nil { - _ = sendReply(c, socks5.RepServerFailure) - closeErr = err - return err - } - defer udpConn.Close() - // Send UDP server addr to the client - atyp, addr, port, err := socks5.ParseAddress(udpConn.LocalAddr().String()) - if err != nil { - _ = sendReply(c, socks5.RepServerFailure) - closeErr = err - return err - } - _, _ = 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) - buf := make([]byte, 1024) - for { - if s.TCPDeadline != 0 { - _ = c.SetDeadline(time.Now().Add(time.Duration(s.TCPDeadline) * time.Second)) - } - _, err := c.Read(buf) - if err != nil { - closeErr = err - break - } - } - // As the TCP connection closes, so does the UDP listener - return nil + return s.handleUDP(c, r) } else { _ = sendReply(c, socks5.RepCommandNotSupported) return ErrUnsupportedCmd } } -func (s *Server) handleUDP(c *net.UDPConn) { +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(), addr, closeErr) + }() + // 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() + _ = sendReply(c, socks5.RepSuccess) + closeErr = pipePair(c, rc, s.TCPDeadline) + return nil + 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() { + s.UDPAssociateClosedFunc(c.RemoteAddr(), closeErr) + }() + udpConn, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: s.TCPAddr.IP, + Zone: s.TCPAddr.Zone, + }) + if err != nil { + _ = sendReply(c, socks5.RepServerFailure) + closeErr = err + return err + } + defer udpConn.Close() + // Send UDP server addr to the client + atyp, addr, port, err := socks5.ParseAddress(udpConn.LocalAddr().String()) + if err != nil { + _ = sendReply(c, socks5.RepServerFailure) + closeErr = err + return err + } + _, _ = socks5.NewReply(socks5.RepSuccess, atyp, addr, port).WriteTo(c) + // Let UDP server do its job, we hold the TCP connection here + go s.udpServer(udpConn) + buf := make([]byte, 1024) + for { + if s.TCPDeadline != 0 { + _ = c.SetDeadline(time.Now().Add(time.Duration(s.TCPDeadline) * time.Second)) + } + _, err := c.Read(buf) + if err != nil { + closeErr = err + break + } + } + // As the TCP connection closes, so does the UDP listener + return nil +} + +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