mirror of
https://github.com/safing/portmaster
synced 2025-04-22 03:49:09 +00:00
218 lines
4.7 KiB
Go
218 lines
4.7 KiB
Go
package geoip
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
maxminddb "github.com/oschwald/maxminddb-golang"
|
|
|
|
"github.com/safing/portmaster/base/log"
|
|
"github.com/safing/portmaster/service/mgr"
|
|
"github.com/safing/portmaster/service/updates"
|
|
)
|
|
|
|
var worker *updateWorker
|
|
|
|
func init() {
|
|
worker = &updateWorker{
|
|
trigger: make(chan struct{}),
|
|
v4: updateBroadcaster{
|
|
dbName: v4MMDBResource,
|
|
},
|
|
v6: updateBroadcaster{
|
|
dbName: v6MMDBResource,
|
|
},
|
|
}
|
|
}
|
|
|
|
const (
|
|
v4MMDBResource = "geoipv4.mmdb"
|
|
v6MMDBResource = "geoipv6.mmdb"
|
|
)
|
|
|
|
type geoIPDB struct {
|
|
*maxminddb.Reader
|
|
update *updates.Artifact
|
|
}
|
|
|
|
// updateBroadcaster stores a geoIPDB and provides synchronized
|
|
// access to the MMDB reader. It also supports broadcasting to
|
|
// multiple waiters when a new database becomes available.
|
|
type updateBroadcaster struct {
|
|
rw sync.RWMutex
|
|
db *geoIPDB
|
|
dbName string
|
|
|
|
waiter chan struct{}
|
|
}
|
|
|
|
// AvailableUpdate returns a new update artifact if the current broadcaster
|
|
// needs a database update.
|
|
func (ub *updateBroadcaster) AvailableUpdate() *updates.Artifact {
|
|
ub.rw.RLock()
|
|
defer ub.rw.RUnlock()
|
|
|
|
// Get artifact.
|
|
artifact, err := module.instance.IntelUpdates().GetFile(ub.dbName)
|
|
if err != nil {
|
|
log.Warningf("geoip: failed to get geoip update: %s", err)
|
|
return nil
|
|
}
|
|
|
|
// Return artifact if not yet initialized.
|
|
if ub.db == nil {
|
|
return artifact
|
|
}
|
|
|
|
// Compare and return artifact only when confirmed newer.
|
|
if newer, _ := artifact.IsNewerThan(ub.db.update); newer {
|
|
return artifact
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReplaceDatabase replaces (or initially sets) the mmdb database.
|
|
// It also notifies all waiters about the availability of the new
|
|
// database.
|
|
func (ub *updateBroadcaster) ReplaceDatabase(db *geoIPDB) {
|
|
ub.rw.Lock()
|
|
defer ub.rw.Unlock()
|
|
|
|
if ub.db != nil {
|
|
_ = ub.db.Close()
|
|
}
|
|
ub.db = db
|
|
ub.notifyWaiters()
|
|
}
|
|
|
|
// notifyWaiters notifies and removes all waiters. Must be called
|
|
// with ub.rw locked.
|
|
func (ub *updateBroadcaster) notifyWaiters() {
|
|
if ub.waiter == nil {
|
|
return
|
|
}
|
|
waiter := ub.waiter
|
|
ub.waiter = nil
|
|
close(waiter)
|
|
}
|
|
|
|
// getWaiter appends and returns a new waiter channel that gets closed
|
|
// when a new database version is available. Must be called with
|
|
// ub.rw locked.
|
|
func (ub *updateBroadcaster) getWaiter() chan struct{} {
|
|
if ub.waiter != nil {
|
|
return ub.waiter
|
|
}
|
|
|
|
ub.waiter = make(chan struct{})
|
|
return ub.waiter
|
|
}
|
|
|
|
type updateWorker struct {
|
|
trigger chan struct{}
|
|
once sync.Once
|
|
|
|
v4 updateBroadcaster
|
|
v6 updateBroadcaster
|
|
}
|
|
|
|
// GetReader returns a MMDB reader for either the IPv4 or the IPv6 database.
|
|
// If wait is true GetReader will wait at most 1 second for the database to
|
|
// become available. If no database is available or GetReader times-out while
|
|
// waiting nil is returned.
|
|
func (upd *updateWorker) GetReader(v6 bool, wait bool) *maxminddb.Reader {
|
|
// check which updateBroadcaster we need to use
|
|
ub := &upd.v4
|
|
if v6 {
|
|
ub = &upd.v6
|
|
}
|
|
|
|
// lock the updateBroadcaster and - if we are allowed to wait -
|
|
// create a new waiter channel, trigger an update and wait for at
|
|
// least 1 second for the update to complete.
|
|
ub.rw.Lock()
|
|
if ub.db == nil {
|
|
if wait {
|
|
waiter := ub.getWaiter()
|
|
ub.rw.Unlock()
|
|
|
|
upd.triggerUpdate()
|
|
|
|
select {
|
|
case <-waiter:
|
|
// call this method again but this time we don't allow
|
|
// it to wait since there must be a open database anyway ...
|
|
return upd.GetReader(v6, false)
|
|
case <-time.After(time.Second):
|
|
// we tried hard but failed so give up here
|
|
return nil
|
|
}
|
|
}
|
|
ub.rw.Unlock()
|
|
return nil
|
|
}
|
|
rd := ub.db.Reader
|
|
ub.rw.Unlock()
|
|
|
|
return rd
|
|
}
|
|
|
|
// triggerUpdate triggers a database update check.
|
|
func (upd *updateWorker) triggerUpdate() {
|
|
upd.start()
|
|
|
|
select {
|
|
case upd.trigger <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (upd *updateWorker) start() {
|
|
upd.once.Do(func() {
|
|
module.mgr.Go("geoip-updater", upd.run)
|
|
})
|
|
}
|
|
|
|
func (upd *updateWorker) run(ctx *mgr.WorkerCtx) error {
|
|
for {
|
|
update := upd.v4.AvailableUpdate()
|
|
if update != nil {
|
|
if v4, err := getGeoIPDB(update); err == nil {
|
|
upd.v4.ReplaceDatabase(v4)
|
|
} else {
|
|
log.Warningf("geoip: failed to get v4 database: %s", err)
|
|
}
|
|
}
|
|
|
|
update = upd.v6.AvailableUpdate()
|
|
if update != nil {
|
|
if v6, err := getGeoIPDB(update); err == nil {
|
|
upd.v6.ReplaceDatabase(v6)
|
|
} else {
|
|
log.Warningf("geoip: failed to get v6 database: %s", err)
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case <-upd.trigger:
|
|
}
|
|
}
|
|
}
|
|
|
|
func getGeoIPDB(update *updates.Artifact) (*geoIPDB, error) {
|
|
log.Debugf("geoip: opening database %s", update.Path())
|
|
|
|
reader, err := maxminddb.Open(update.Path())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open: %w", err)
|
|
}
|
|
log.Debugf("geoip: successfully opened database %s", update.Filename)
|
|
|
|
return &geoIPDB{
|
|
Reader: reader,
|
|
update: update,
|
|
}, nil
|
|
}
|