safing-portmaster/service/intel/geoip/database.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
}