mirror of
https://github.com/cmz0228/hysteria-dev.git
synced 2025-08-08 02:11:48 +00:00
Implement client side ACL for SOCKS5 TCP
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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
1
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
|
||||
|
2
go.sum
2
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=
|
||||
|
@@ -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, ""
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user