Compare commits

...

1 Commits

Author SHA1 Message Date
Patrick Asmus
86db4935b8 fix: restore recurring GeoIP checks 2026-06-10 13:22:12 +02:00
11 changed files with 137 additions and 22 deletions

View File

@@ -66,7 +66,7 @@ Die benötigten Pakete werden vom Installer auf Ubuntu/Debian automatisch instal
```bash
# Release-Archiv herunterladen und entpacken
curl -fL -o adguard-shield-linux-amd64.tar.gz \
https://git.techniverse.net/scriptos/adguard-shield/releases/download/v1.1.2/adguard-shield-linux-amd64.tar.gz
https://git.techniverse.net/scriptos/adguard-shield/releases/download/v1.1.3/adguard-shield-linux-amd64.tar.gz
tar -xzf adguard-shield-linux-amd64.tar.gz
chmod +x ./adguard-shield
```

View File

@@ -92,7 +92,7 @@ ABUSEIPDB_CATEGORIES="4" # 4 = DDoS Attack (siehe abuseipdb.com/categ
GEOIP_ENABLED=false
GEOIP_MODE="blocklist" # blocklist oder allowlist
GEOIP_COUNTRIES="" # ISO 3166-1 Alpha-2 Codes, z.B. "CN,RU,KP,IR"
GEOIP_CHECK_INTERVAL=0 # Legacy: Daemon nutzt den zentralen CHECK_INTERVAL-Poller
GEOIP_CHECK_INTERVAL=0 # 0 = nutzt CHECK_INTERVAL; sonst eigenes GeoIP-Prüfintervall
GEOIP_NOTIFY=true
GEOIP_SKIP_PRIVATE=true # Private IPs ausnehmen
GEOIP_LICENSE_KEY="" # MaxMind GeoLite2 Key (optional, für Auto-Download)

BIN
dist/adguard-shield-v1.1.3-linux-amd64 vendored Normal file

Binary file not shown.

View File

@@ -250,13 +250,13 @@ ABUSEIPDB_API_KEY="..."
### Service gestartet
```text
AdGuard Shield v1.1.2 wurde auf dns1 gestartet.
AdGuard Shield v1.1.3 wurde auf dns1 gestartet.
```
### Service gestoppt
```text
AdGuard Shield v1.1.2 wurde auf dns1 gestoppt.
AdGuard Shield v1.1.3 wurde auf dns1 gestoppt.
```
### Rate-Limit-Sperre

View File

@@ -560,7 +560,7 @@ ABUSEIPDB_CATEGORIES="4"
| `GEOIP_ENABLED` | `false` | GeoIP-Filter aktivieren |
| `GEOIP_MODE` | `blocklist` | Filtermodus |
| `GEOIP_COUNTRIES` | leer | Ländercodes nach ISO 3166-1 Alpha-2 |
| `GEOIP_CHECK_INTERVAL` | `0` | Legacy-Parameter (Go-Version nutzt den zentralen Poller) |
| `GEOIP_CHECK_INTERVAL` | `0` | Eigenes GeoIP-Prüfintervall in Sekunden; `0` nutzt `CHECK_INTERVAL`. |
| `GEOIP_NOTIFY` | `true` | Benachrichtigungen bei GeoIP-Sperren senden |
| `GEOIP_SKIP_PRIVATE` | `true` | Private/lokale IPs überspringen |
| `GEOIP_LICENSE_KEY` | leer | MaxMind-License-Key für automatischen Download |

View File

@@ -34,7 +34,7 @@ Du brauchst ein fertiges Linux-Binary. Das kann aus einem Release, aus CI oder a
```bash
curl -fL -o adguard-shield-linux-amd64.tar.gz \
https://git.techniverse.net/scriptos/adguard-shield/releases/download/v1.1.2/adguard-shield-linux-amd64.tar.gz
https://git.techniverse.net/scriptos/adguard-shield/releases/download/v1.1.3/adguard-shield-linux-amd64.tar.gz
tar -xzf adguard-shield-linux-amd64.tar.gz
chmod +x ./adguard-shield
```

View File

@@ -1,5 +1,5 @@
package appinfo
var Version = "v1.1.2"
var Version = "v1.1.3"
const ProjectURL = "https://git.techniverse.net/scriptos/adguard-shield.git"

View File

@@ -40,7 +40,7 @@ type Daemon struct {
mu sync.Mutex
seen map[string]time.Time
events []queryEvent
geoSeen map[string]bool
geoSeen map[string]time.Time
wl map[string]bool
serviceMu sync.Mutex
@@ -93,7 +93,7 @@ func New(c *config.Config) (*Daemon, error) {
d := &Daemon{
Config: c, Store: st, FW: fw, Logger: logger,
Client: &http.Client{Timeout: 20 * time.Second, Transport: tr},
seen: map[string]time.Time{}, geoSeen: map[string]bool{},
seen: map[string]time.Time{}, geoSeen: map[string]time.Time{},
}
d.Geo = geoip.New(c.GeoIPMMDBPath, c.GeoIPLicenseKey, filepath.Join(filepath.Dir(c.Path), "geoip"), c.GeoIPCacheTTL, st)
return d, nil
@@ -409,13 +409,6 @@ func (d *Daemon) checkGeoIP(ctx context.Context, ip string) {
if d.Config.GeoIPSkipPrivate && geoip.IsPrivateIP(ip) {
return
}
d.mu.Lock()
if d.geoSeen[ip] {
d.mu.Unlock()
return
}
d.geoSeen[ip] = true
d.mu.Unlock()
if d.isWhitelisted(ip) {
return
}
@@ -423,6 +416,9 @@ func (d *Daemon) checkGeoIP(ctx context.Context, ip string) {
if exists {
return
}
if !d.shouldCheckGeoIP(ip, time.Now()) {
return
}
cc, err := d.Geo.Lookup(ip)
if err != nil || cc == "" {
return
@@ -432,6 +428,41 @@ func (d *Daemon) checkGeoIP(ctx context.Context, ip string) {
}
}
func (d *Daemon) shouldCheckGeoIP(ip string, now time.Time) bool {
interval := d.geoIPCheckInterval()
d.mu.Lock()
defer d.mu.Unlock()
if last, ok := d.geoSeen[ip]; ok && now.Sub(last) < interval {
return false
}
d.geoSeen[ip] = now
d.pruneGeoSeenLocked(now, interval)
return true
}
func (d *Daemon) geoIPCheckInterval() time.Duration {
seconds := 0
if d.Config != nil {
seconds = d.Config.GeoIPCheckInterval
if seconds <= 0 {
seconds = d.Config.CheckInterval
}
}
if seconds <= 0 {
seconds = 1
}
return time.Duration(seconds) * time.Second
}
func (d *Daemon) pruneGeoSeenLocked(now time.Time, interval time.Duration) {
cutoff := now.Add(-2 * interval)
for ip, last := range d.geoSeen {
if last.Before(cutoff) {
delete(d.geoSeen, ip)
}
}
}
func (d *Daemon) Ban(ctx context.Context, ip, domain string, count int, proto, reason, source, country string, permanent bool) error {
if d.isWhitelisted(ip) {
return nil

View File

@@ -363,3 +363,37 @@ func TestDryRunDoesNotInsertActiveBan(t *testing.T) {
t.Fatal("dry-run must not create an active ban")
}
}
func TestGeoIPCheckGateUsesConfiguredInterval(t *testing.T) {
now := time.Unix(1000, 0)
d := &Daemon{
Config: &config.Config{CheckInterval: 10, GeoIPCheckInterval: 30},
geoSeen: map[string]time.Time{},
}
if !d.shouldCheckGeoIP("203.0.113.7", now) {
t.Fatal("first GeoIP check should be allowed")
}
if d.shouldCheckGeoIP("203.0.113.7", now.Add(29*time.Second)) {
t.Fatal("GeoIP check inside configured interval should be skipped")
}
if !d.shouldCheckGeoIP("203.0.113.7", now.Add(30*time.Second)) {
t.Fatal("GeoIP check after configured interval should be allowed")
}
}
func TestGeoIPCheckGateFallsBackToPollInterval(t *testing.T) {
now := time.Unix(1000, 0)
d := &Daemon{
Config: &config.Config{CheckInterval: 10},
geoSeen: map[string]time.Time{},
}
if !d.shouldCheckGeoIP("203.0.113.7", now) {
t.Fatal("first GeoIP check should be allowed")
}
if d.shouldCheckGeoIP("203.0.113.7", now.Add(9*time.Second)) {
t.Fatal("GeoIP check inside poll interval should be skipped")
}
if !d.shouldCheckGeoIP("203.0.113.7", now.Add(10*time.Second)) {
t.Fatal("GeoIP check after poll interval should be allowed")
}
}

View File

@@ -14,6 +14,7 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/oschwald/maxminddb-golang"
@@ -31,6 +32,7 @@ type Resolver struct {
Dir string
TTL int64
Store Store
mu sync.RWMutex
reader *maxminddb.Reader
cache map[string]string
mtime int64
@@ -65,7 +67,9 @@ func (r *Resolver) Open(ctx context.Context) error {
r.mtime = st.ModTime().Unix()
if r.Store != nil {
if c, err := r.Store.LoadGeoIPCache(r.TTL, r.mtime); err == nil {
r.mu.Lock()
r.cache = c
r.mu.Unlock()
}
}
return nil
@@ -79,9 +83,12 @@ func (r *Resolver) Close() error {
}
func (r *Resolver) Lookup(ip string) (string, error) {
r.mu.RLock()
if v, ok := r.cache[ip]; ok {
r.mu.RUnlock()
return v, nil
}
r.mu.RUnlock()
if r.reader == nil {
return r.lookupLegacy(ip)
}
@@ -104,33 +111,44 @@ func (r *Resolver) Lookup(ip string) (string, error) {
if cc == "" {
cc = strings.ToUpper(rec.RegisteredCountry.ISOCode)
}
if cc != "" {
r.cache[ip] = cc
if r.Store != nil {
_ = r.Store.UpsertGeoIP(ip, cc, r.mtime)
}
}
r.storeCache(ip, cc)
return cc, nil
}
func (r *Resolver) lookupLegacy(ip string) (string, error) {
if strings.Contains(ip, ":") {
if cc, err := runGeoIPCommand("geoiplookup6", ip); err == nil && cc != "" {
r.storeCache(ip, cc)
return cc, nil
}
} else {
if cc, err := runGeoIPCommand("geoiplookup", ip); err == nil && cc != "" {
r.storeCache(ip, cc)
return cc, nil
}
}
if r.effectivePath != "" {
if cc, err := runGeoIPCommand("mmdblookup", "--file", r.effectivePath, "--ip", ip, "country", "iso_code"); err == nil && cc != "" {
r.storeCache(ip, cc)
return cc, nil
}
}
return "", fmt.Errorf("no GeoIP result for %s", ip)
}
func (r *Resolver) storeCache(ip, country string) {
country = strings.ToUpper(strings.TrimSpace(country))
if country == "" {
return
}
r.mu.Lock()
r.cache[ip] = country
r.mu.Unlock()
if r.Store != nil {
_ = r.Store.UpsertGeoIP(ip, country, r.mtime)
}
}
func runGeoIPCommand(name string, args ...string) (string, error) {
if _, err := exec.LookPath(name); err != nil {
return "", err

View File

@@ -2,6 +2,23 @@ package geoip
import "testing"
type fakeGeoIPStore struct {
ip string
country string
mtime int64
}
func (s *fakeGeoIPStore) LoadGeoIPCache(ttl, dbMtime int64) (map[string]string, error) {
return map[string]string{}, nil
}
func (s *fakeGeoIPStore) UpsertGeoIP(ip, country string, dbMtime int64) error {
s.ip = ip
s.country = country
s.mtime = dbMtime
return nil
}
func TestShouldBlockModes(t *testing.T) {
countries := []string{"CN", "RU"}
if !ShouldBlock("cn", "blocklist", countries) {
@@ -28,3 +45,18 @@ func TestIsPrivateIP(t *testing.T) {
t.Fatal("8.8.8.8 should be public")
}
}
func TestStoreCachePersistsLegacyLookupResult(t *testing.T) {
store := &fakeGeoIPStore{}
r := New("", "", "", 86400, store)
r.mtime = 123
r.storeCache("203.0.113.7", "cn")
if got := r.cache["203.0.113.7"]; got != "CN" {
t.Fatalf("cache = %q, want CN", got)
}
if store.ip != "203.0.113.7" || store.country != "CN" || store.mtime != 123 {
t.Fatalf("store got ip=%q country=%q mtime=%d", store.ip, store.country, store.mtime)
}
}