mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 12:42:34 +02:00
219 lines
4.7 KiB
Go
219 lines
4.7 KiB
Go
package acl
|
|
|
|
import (
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/oschwald/maxminddb-golang"
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
"github.com/rs/zerolog"
|
|
acl "github.com/yusing/go-proxy/internal/acl/types"
|
|
"github.com/yusing/go-proxy/internal/common"
|
|
"github.com/yusing/go-proxy/internal/gperr"
|
|
"github.com/yusing/go-proxy/internal/logging"
|
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
|
"github.com/yusing/go-proxy/internal/task"
|
|
"github.com/yusing/go-proxy/internal/utils"
|
|
)
|
|
|
|
type Config struct {
|
|
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
|
AllowLocal *bool `json:"allow_local"` // default: true
|
|
Allow []string `json:"allow"`
|
|
Deny []string `json:"deny"`
|
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
|
|
|
MaxMind *MaxMindConfig `json:"maxmind" validate:"omitempty"`
|
|
|
|
config
|
|
}
|
|
|
|
type (
|
|
MaxMindDatabaseType string
|
|
MaxMindConfig struct {
|
|
AccountID string `json:"account_id" validate:"required"`
|
|
LicenseKey string `json:"license_key" validate:"required"`
|
|
Database MaxMindDatabaseType `json:"database" validate:"required,oneof=geolite geoip2"`
|
|
|
|
logger zerolog.Logger
|
|
lastUpdate time.Time
|
|
db struct {
|
|
*maxminddb.Reader
|
|
sync.RWMutex
|
|
}
|
|
}
|
|
)
|
|
|
|
type config struct {
|
|
defaultAllow bool
|
|
allowLocal bool
|
|
allow []matcher
|
|
deny []matcher
|
|
ipCache *xsync.MapOf[string, *checkCache]
|
|
logAllowed bool
|
|
logger *accesslog.AccessLogger
|
|
}
|
|
|
|
type checkCache struct {
|
|
*acl.IPInfo
|
|
allow bool
|
|
created time.Time
|
|
}
|
|
|
|
const cacheTTL = 1 * time.Minute
|
|
|
|
func (c *checkCache) Expired() bool {
|
|
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
|
}
|
|
|
|
//TODO: add stats
|
|
|
|
const (
|
|
ACLAllow = "allow"
|
|
ACLDeny = "deny"
|
|
)
|
|
|
|
const (
|
|
MaxMindGeoLite MaxMindDatabaseType = "geolite"
|
|
MaxMindGeoIP2 MaxMindDatabaseType = "geoip2"
|
|
)
|
|
|
|
func (c *Config) Validate() gperr.Error {
|
|
switch c.Default {
|
|
case "", ACLAllow:
|
|
c.defaultAllow = true
|
|
case ACLDeny:
|
|
c.defaultAllow = false
|
|
default:
|
|
return gperr.New("invalid default value").Subject(c.Default)
|
|
}
|
|
|
|
if c.AllowLocal != nil {
|
|
c.allowLocal = *c.AllowLocal
|
|
} else {
|
|
c.allowLocal = true
|
|
}
|
|
|
|
if c.MaxMind != nil {
|
|
c.MaxMind.logger = logging.With().Str("type", string(c.MaxMind.Database)).Logger()
|
|
}
|
|
|
|
if c.Log != nil {
|
|
c.logAllowed = c.Log.LogAllowed
|
|
}
|
|
|
|
errs := gperr.NewBuilder("syntax error")
|
|
c.allow = make([]matcher, 0, len(c.Allow))
|
|
c.deny = make([]matcher, 0, len(c.Deny))
|
|
|
|
for _, s := range c.Allow {
|
|
m, err := c.parseMatcher(s)
|
|
if err != nil {
|
|
errs.Add(err.Subject(s))
|
|
continue
|
|
}
|
|
c.allow = append(c.allow, m)
|
|
}
|
|
for _, s := range c.Deny {
|
|
m, err := c.parseMatcher(s)
|
|
if err != nil {
|
|
errs.Add(err.Subject(s))
|
|
continue
|
|
}
|
|
c.deny = append(c.deny, m)
|
|
}
|
|
|
|
if errs.HasError() {
|
|
c.allow = nil
|
|
c.deny = nil
|
|
return errMatcherFormat.With(errs.Error())
|
|
}
|
|
|
|
c.ipCache = xsync.NewMapOf[string, *checkCache]()
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) Valid() bool {
|
|
return c != nil && (len(c.allow) > 0 || len(c.deny) > 0 || c.allowLocal)
|
|
}
|
|
|
|
func (c *Config) Start(parent *task.Task) gperr.Error {
|
|
if c.MaxMind != nil {
|
|
if err := c.MaxMind.LoadMaxMindDB(parent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if c.Log != nil {
|
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
|
if err != nil {
|
|
return gperr.New("failed to start access logger").With(err)
|
|
}
|
|
c.logger = logger
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) cacheRecord(info *acl.IPInfo, allow bool) {
|
|
if common.ForceResolveCountry && info.City == nil {
|
|
c.MaxMind.lookupCity(info)
|
|
}
|
|
c.ipCache.Store(info.Str, &checkCache{
|
|
IPInfo: info,
|
|
allow: allow,
|
|
created: utils.TimeNow(),
|
|
})
|
|
}
|
|
|
|
func (c *config) log(info *acl.IPInfo, allowed bool) {
|
|
if c.logger == nil {
|
|
return
|
|
}
|
|
if !allowed || c.logAllowed {
|
|
c.logger.LogACL(info, !allowed)
|
|
}
|
|
}
|
|
|
|
func (c *Config) IPAllowed(ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
// always allow loopback
|
|
// loopback is not logged
|
|
if ip.IsLoopback() {
|
|
return true
|
|
}
|
|
|
|
if c.allowLocal && ip.IsPrivate() {
|
|
c.log(&acl.IPInfo{IP: ip, Str: ip.String()}, true)
|
|
return true
|
|
}
|
|
|
|
ipStr := ip.String()
|
|
record, ok := c.ipCache.Load(ipStr)
|
|
if ok && !record.Expired() {
|
|
c.log(record.IPInfo, record.allow)
|
|
return record.allow
|
|
}
|
|
|
|
ipAndStr := &acl.IPInfo{IP: ip, Str: ipStr}
|
|
for _, m := range c.allow {
|
|
if m(ipAndStr) {
|
|
c.log(ipAndStr, true)
|
|
c.cacheRecord(ipAndStr, true)
|
|
return true
|
|
}
|
|
}
|
|
for _, m := range c.deny {
|
|
if m(ipAndStr) {
|
|
c.log(ipAndStr, false)
|
|
c.cacheRecord(ipAndStr, false)
|
|
return false
|
|
}
|
|
}
|
|
|
|
c.log(ipAndStr, c.defaultAllow)
|
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
|
return c.defaultAllow
|
|
}
|