hysteria/extras/outbounds/acl.go

121 lines
3.2 KiB
Go

package outbounds
import (
"errors"
"net"
"os"
"strings"
"github.com/apernet/hysteria/extras/v2/outbounds/acl"
)
const (
aclCacheSize = 1024
)
var errRejected = errors.New("rejected")
// aclEngine is a PluggableOutbound that dispatches connections to different
// outbounds based on ACL rules.
// There are 3 built-in outbounds:
// - direct: directOutbound, auto mode
// - reject: reject the connection
// - default: first outbound in the list, or if the list is empty, equal to direct
// If the user-defined outbounds contain any of the above names, they will
// override the built-in outbounds.
type aclEngine struct {
RuleSet acl.CompiledRuleSet[PluggableOutbound]
Default PluggableOutbound
}
type OutboundEntry struct {
Name string
Outbound PluggableOutbound
}
func NewACLEngineFromString(rules string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) {
trs, err := acl.ParseTextRules(rules)
if err != nil {
return nil, err
}
obMap := outboundsToMap(outbounds)
rs, err := acl.Compile[PluggableOutbound](trs, obMap, aclCacheSize, geoLoader)
if err != nil {
return nil, err
}
return &aclEngine{rs, obMap["default"]}, nil
}
func NewACLEngineFromFile(filename string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) {
bs, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return NewACLEngineFromString(string(bs), outbounds, geoLoader)
}
func outboundsToMap(outbounds []OutboundEntry) map[string]PluggableOutbound {
obMap := make(map[string]PluggableOutbound)
for _, ob := range outbounds {
obMap[strings.ToLower(ob.Name)] = ob.Outbound
}
// Add built-in outbounds if not overridden
if _, ok := obMap["direct"]; !ok {
obMap["direct"] = NewDirectOutboundSimple(DirectOutboundModeAuto)
}
if _, ok := obMap["reject"]; !ok {
obMap["reject"] = &aclRejectOutbound{}
}
if _, ok := obMap["default"]; !ok {
if len(outbounds) > 0 {
obMap["default"] = outbounds[0].Outbound
} else {
obMap["default"] = obMap["direct"]
}
}
return obMap
}
func (a *aclEngine) handle(reqAddr *AddrEx, proto acl.Protocol) PluggableOutbound {
hostInfo := acl.HostInfo{Name: reqAddr.Host}
if reqAddr.ResolveInfo != nil {
hostInfo.IPv4 = reqAddr.ResolveInfo.IPv4
hostInfo.IPv6 = reqAddr.ResolveInfo.IPv6
}
ob, hijackIP := a.RuleSet.Match(hostInfo, proto, reqAddr.Port)
if ob == nil {
// No match, use default outbound
return a.Default
}
if hijackIP != nil {
// We must rewrite both Host & ResolveInfo,
// as some outbounds only care about Host.
reqAddr.Host = hijackIP.String()
if ip4 := hijackIP.To4(); ip4 != nil {
reqAddr.ResolveInfo = &ResolveInfo{IPv4: ip4}
} else {
reqAddr.ResolveInfo = &ResolveInfo{IPv6: hijackIP}
}
}
return ob
}
func (a *aclEngine) TCP(reqAddr *AddrEx) (net.Conn, error) {
ob := a.handle(reqAddr, acl.ProtocolTCP)
return ob.TCP(reqAddr)
}
func (a *aclEngine) UDP(reqAddr *AddrEx) (UDPConn, error) {
ob := a.handle(reqAddr, acl.ProtocolUDP)
return ob.UDP(reqAddr)
}
type aclRejectOutbound struct{}
func (a *aclRejectOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
return nil, errRejected
}
func (a *aclRejectOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
return nil, errRejected
}