Commit 1a73d9b8 authored by vicotor's avatar vicotor

add ip blacklist

parent c833b20f
...@@ -32,6 +32,12 @@ Applies to IPs that frequently exceed the rate limit. ...@@ -32,6 +32,12 @@ Applies to IPs that frequently exceed the rate limit.
- `PENALTY_DURATION`: The duration an IP stays in penalty mode (default: `20m`). - `PENALTY_DURATION`: The duration an IP stays in penalty mode (default: `20m`).
- `PENALTY_TRIGGER_COUNT`: The number of consecutive rate limit violations required to trigger penalty mode (default: `5`). - `PENALTY_TRIGGER_COUNT`: The number of consecutive rate limit violations required to trigger penalty mode (default: `5`).
### IP Blacklist Configuration
You can specify a file containing a list of IP addresses to be blacklisted. IPs in this list will be automatically placed in **Penalty Mode**.
- `IP_BLACKLIST_FILE`: The path to the file containing blacklisted IP addresses or domains.
## Usage ## Usage
1. Set the required environment variables. 1. Set the required environment variables.
...@@ -50,3 +56,13 @@ example.com ...@@ -50,3 +56,13 @@ example.com
*.example.com *.example.com
``` ```
## IP Blacklist File Format
The IP blacklist file follows the same format as the whitelist file.
Example:
```
192.168.1.100
bad-actor.com
*.botnet.net
```
...@@ -79,6 +79,12 @@ var ( ...@@ -79,6 +79,12 @@ var (
whitelistPatterns []*regexp.Regexp whitelistPatterns []*regexp.Regexp
whitelistMu sync.RWMutex whitelistMu sync.RWMutex
// IP Blacklist related
ipBlacklistFile string
ipBlacklist map[string]struct{}
ipBlacklistPatterns []*regexp.Regexp
ipBlacklistMu sync.RWMutex
// Rate limiting // Rate limiting
visitors = make(map[string]*visitor) visitors = make(map[string]*visitor)
visitorsMu sync.Mutex visitorsMu sync.Mutex
...@@ -98,6 +104,7 @@ type visitor struct { ...@@ -98,6 +104,7 @@ type visitor struct {
punishedUntil time.Time punishedUntil time.Time
blockedCount int blockedCount int
lastBlockTime time.Time lastBlockTime time.Time
isStaticBlacklisted bool
} }
func main() { func main() {
...@@ -149,6 +156,22 @@ func main() { ...@@ -149,6 +156,22 @@ func main() {
log.Printf("WHITELIST_FILE not set, whitelist feature disabled") log.Printf("WHITELIST_FILE not set, whitelist feature disabled")
} }
// Load IP blacklist file and start watcher
ipBlacklistFile = os.Getenv("IP_BLACKLIST_FILE")
if ipBlacklistFile != "" {
blExact, blPatterns := loadIPBlacklist(ipBlacklistFile)
ipBlacklistMu.Lock()
ipBlacklist = blExact
ipBlacklistPatterns = blPatterns
ipBlacklistMu.Unlock()
log.Printf("loaded ip blacklist entries: %d exact, %d patterns", len(blExact), len(blPatterns))
startIPBlacklistWatcher(ipBlacklistFile)
} else {
ipBlacklist = map[string]struct{}{}
ipBlacklistPatterns = []*regexp.Regexp{}
log.Printf("IP_BLACKLIST_FILE not set, ip blacklist feature disabled")
}
// Start cache janitor for blacklist cache. Interval can be configured via env BLACKLIST_CACHE_CLEANUP_INTERVAL (e.g. "5m"). // Start cache janitor for blacklist cache. Interval can be configured via env BLACKLIST_CACHE_CLEANUP_INTERVAL (e.g. "5m").
cleanupInterval := 5 * time.Minute cleanupInterval := 5 * time.Minute
if s := os.Getenv("BLACKLIST_CACHE_CLEANUP_INTERVAL"); s != "" { if s := os.Getenv("BLACKLIST_CACHE_CLEANUP_INTERVAL"); s != "" {
...@@ -643,6 +666,131 @@ func startWhitelistWatcher(path string) { ...@@ -643,6 +666,131 @@ func startWhitelistWatcher(path string) {
}() }()
} }
// ===== IP Blacklist helper functions (dynamic reload) =====
func loadIPBlacklist(path string) (map[string]struct{}, []*regexp.Regexp) {
result := make(map[string]struct{})
patterns := make([]*regexp.Regexp, 0)
f, err := os.Open(path)
if err != nil {
log.Printf("open ip blacklist file '%s' error: %v", path, err)
return result, patterns
}
defer f.Close()
scanner := bufio.NewScanner(f)
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
// Determine if wildcard pattern present
if strings.Contains(line, "*") {
if re := compilePattern(line); re != nil {
patterns = append(patterns, re)
} else {
log.Printf("skip invalid pattern at line %d: %s", lineNum, line)
}
continue
}
result[line] = struct{}{}
}
if err := scanner.Err(); err != nil {
log.Printf("scan ip blacklist file error: %v", err)
}
return result, patterns
}
func isIPBlacklisted(v string) bool {
if v == "" {
return false
}
val := strings.TrimSpace(v)
if val == "" {
return false
}
// Collect candidate forms: raw, origin base (scheme://host), host (strip port), host:port
candidates := make([]string, 0, 4)
candidates = append(candidates, val)
if u, err := url.Parse(val); err == nil && u.Host != "" {
host := u.Host
// strip port for host-only
if strings.Contains(host, ":") {
parts := strings.Split(host, ":")
hostNoPort := parts[0]
candidates = append(candidates, hostNoPort)
}
candidates = append(candidates, host)
base := fmt.Sprintf("%s://%s", u.Scheme, host)
candidates = append(candidates, base)
}
ipBlacklistMu.RLock()
defer ipBlacklistMu.RUnlock()
for _, c := range candidates {
if _, ok := ipBlacklist[c]; ok {
return true
}
}
// pattern matching
for _, re := range ipBlacklistPatterns {
for _, c := range candidates {
if re.MatchString(c) {
return true
}
}
}
return false
}
func startIPBlacklistWatcher(path string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("create ip blacklist watcher error: %v", err)
return
}
dir := filepath.Dir(path)
if err := watcher.Add(dir); err != nil {
log.Printf("add ip blacklist watch dir error: %v", err)
watcher.Close()
return
}
go func() {
defer watcher.Close()
for {
select {
case ev, ok := <-watcher.Events:
if !ok {
return
}
if ev.Name == path {
if ev.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) != 0 {
exact, pats := loadIPBlacklist(path)
ipBlacklistMu.Lock()
ipBlacklist = exact
ipBlacklistPatterns = pats
ipBlacklistMu.Unlock()
log.Printf("ip blacklist reloaded (%d exact, %d patterns) due to event: %s", len(exact), len(pats), ev.Op.String())
}
if ev.Op&fsnotify.Remove != 0 {
ipBlacklistMu.Lock()
ipBlacklist = map[string]struct{}{}
ipBlacklistPatterns = []*regexp.Regexp{}
ipBlacklistMu.Unlock()
log.Printf("ip blacklist file removed, cleared entries")
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Printf("ip blacklist watcher error: %v", err)
}
}
}()
}
func checkRateLimit(ip string) bool { func checkRateLimit(ip string) bool {
visitorsMu.Lock() visitorsMu.Lock()
defer visitorsMu.Unlock() defer visitorsMu.Unlock()
...@@ -655,7 +803,33 @@ func checkRateLimit(ip string) bool { ...@@ -655,7 +803,33 @@ func checkRateLimit(ip string) bool {
} }
v.lastSeen = time.Now() v.lastSeen = time.Now()
// Check if penalty expired // Check if IP is in static blacklist
if isIPBlacklisted(ip) {
if !v.isStaticBlacklisted {
// First time detected as blacklisted, force penalty mode
v.isStaticBlacklisted = true
v.limiter.SetLimit(penaltyLimit)
v.limiter.SetBurst(penaltyBurst)
// Set a long punishment duration to avoid frequent checks, or just rely on the flag
v.punishedUntil = time.Now().Add(24 * time.Hour)
log.Printf("IP %s is in static blacklist, enforcing penalty mode", ip)
} else {
// Refresh punishment duration to keep it in penalty mode
if time.Now().After(v.punishedUntil) {
v.punishedUntil = time.Now().Add(24 * time.Hour)
}
}
return v.limiter.Allow()
} else if v.isStaticBlacklisted {
// Was blacklisted but now removed from static blacklist
v.isStaticBlacklisted = false
v.punishedUntil = time.Time{} // Clear punishment
v.limiter.SetLimit(normalLimit)
v.limiter.SetBurst(normalBurst)
log.Printf("IP %s removed from static blacklist, restoring normal limits", ip)
}
// Check if penalty expired (for dynamic penalty)
if !v.punishedUntil.IsZero() { if !v.punishedUntil.IsZero() {
if time.Now().After(v.punishedUntil) { if time.Now().After(v.punishedUntil) {
// Penalty expired, restore normal settings // Penalty expired, restore normal settings
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment