mirror of
https://github.com/bakito/adguardhome-sync.git
synced 2025-01-10 01:02:11 +08:00
541 lines
13 KiB
Go
541 lines
13 KiB
Go
package sync
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/bakito/adguardhome-sync/pkg/client"
|
|
"github.com/bakito/adguardhome-sync/pkg/log"
|
|
"github.com/bakito/adguardhome-sync/pkg/types"
|
|
"github.com/bakito/adguardhome-sync/pkg/versions"
|
|
"github.com/bakito/adguardhome-sync/version"
|
|
"github.com/robfig/cron/v3"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var l = log.GetLogger("sync")
|
|
|
|
// Sync config from origin to replica
|
|
func Sync(cfg *types.Config) error {
|
|
if cfg.Origin.URL == "" {
|
|
return fmt.Errorf("origin URL is required")
|
|
}
|
|
|
|
if len(cfg.UniqueReplicas()) == 0 {
|
|
return fmt.Errorf("no replicas configured")
|
|
}
|
|
|
|
l.With(
|
|
"version", version.Version,
|
|
"build", version.Build,
|
|
"os", runtime.GOOS,
|
|
"arch", runtime.GOARCH,
|
|
).Info("AdGuardHome sync")
|
|
cfg.Log(l)
|
|
cfg.Features.LogDisabled(l)
|
|
cfg.Origin.AutoSetup = false
|
|
|
|
w := &worker{
|
|
cfg: cfg,
|
|
createClient: func(ai types.AdGuardInstance) (client.Client, error) {
|
|
return client.New(ai)
|
|
},
|
|
}
|
|
if cfg.Cron != "" {
|
|
w.cron = cron.New()
|
|
cl := l.With("cron", cfg.Cron)
|
|
sched, err := cron.ParseStandard(cfg.Cron)
|
|
if err != nil {
|
|
cl.With("error", err).Error("Error parsing cron expression")
|
|
return err
|
|
}
|
|
cl = cl.With("next-execution", sched.Next(time.Now()))
|
|
_, err = w.cron.AddFunc(cfg.Cron, func() {
|
|
w.sync()
|
|
})
|
|
if err != nil {
|
|
cl.With("error", err).Error("Error during cron job setup")
|
|
return err
|
|
}
|
|
cl.Info("Setup cronjob")
|
|
if cfg.API.Port != 0 {
|
|
w.cron.Start()
|
|
} else {
|
|
w.cron.Run()
|
|
}
|
|
}
|
|
if cfg.API.Port != 0 {
|
|
if cfg.RunOnStart {
|
|
go func() {
|
|
l.Info("Running sync on startup")
|
|
w.sync()
|
|
}()
|
|
}
|
|
w.listenAndServe()
|
|
} else if cfg.RunOnStart {
|
|
l.Info("Running sync on startup")
|
|
w.sync()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type worker struct {
|
|
cfg *types.Config
|
|
running bool
|
|
cron *cron.Cron
|
|
createClient func(instance types.AdGuardInstance) (client.Client, error)
|
|
}
|
|
|
|
func (w *worker) sync() {
|
|
if w.running {
|
|
l.Info("Sync already running")
|
|
return
|
|
}
|
|
w.running = true
|
|
defer func() { w.running = false }()
|
|
|
|
oc, err := w.createClient(w.cfg.Origin)
|
|
if err != nil {
|
|
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
|
|
return
|
|
}
|
|
|
|
sl := l.With("from", oc.Host())
|
|
|
|
o := &origin{}
|
|
o.status, err = oc.Status()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting origin status")
|
|
return
|
|
}
|
|
|
|
if versions.IsNewerThan(versions.MinAgh, o.status.Version) {
|
|
sl.With("error", err, "version", o.status.Version).Errorf("Origin AdGuard Home version must be >= %s", versions.MinAgh)
|
|
return
|
|
}
|
|
|
|
sl.With("version", o.status.Version).Info("Connected to origin")
|
|
|
|
o.parental, err = oc.Parental()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting parental status")
|
|
return
|
|
}
|
|
o.safeSearch, err = oc.SafeSearch()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting safe search status")
|
|
return
|
|
}
|
|
o.safeBrowsing, err = oc.SafeBrowsing()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting safe browsing status")
|
|
return
|
|
}
|
|
|
|
o.rewrites, err = oc.RewriteList()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting origin rewrites")
|
|
return
|
|
}
|
|
|
|
o.services, err = oc.Services()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting origin services")
|
|
return
|
|
}
|
|
|
|
o.filters, err = oc.Filtering()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting origin filters")
|
|
return
|
|
}
|
|
o.clients, err = oc.Clients()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting origin clients")
|
|
return
|
|
}
|
|
o.queryLogConfig, err = oc.QueryLogConfig()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting query log config")
|
|
return
|
|
}
|
|
o.statsConfig, err = oc.StatsConfig()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting stats config")
|
|
return
|
|
}
|
|
|
|
o.accessList, err = oc.AccessList()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting access list")
|
|
return
|
|
}
|
|
|
|
o.dnsConfig, err = oc.DNSConfig()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting dns config")
|
|
return
|
|
}
|
|
|
|
if w.cfg.Features.DHCP.ServerConfig || w.cfg.Features.DHCP.StaticLeases {
|
|
o.dhcpServerConfig, err = oc.DHCPServerConfig()
|
|
if err != nil {
|
|
sl.With("error", err).Error("Error getting dhcp server config")
|
|
return
|
|
}
|
|
}
|
|
|
|
replicas := w.cfg.UniqueReplicas()
|
|
for _, replica := range replicas {
|
|
w.syncTo(sl, o, replica)
|
|
}
|
|
}
|
|
|
|
func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
|
|
rc, err := w.createClient(replica)
|
|
if err != nil {
|
|
l.With("error", err, "url", replica.URL).Error("Error creating replica client")
|
|
return
|
|
}
|
|
|
|
rl := l.With("to", rc.Host())
|
|
rl.Info("Start sync")
|
|
|
|
rs, err := w.statusWithSetup(rl, replica, rc)
|
|
if err != nil {
|
|
rl.With("error", err).Error("Error getting replica status")
|
|
return
|
|
}
|
|
|
|
rl.With("version", rs.Version).Info("Connected to replica")
|
|
|
|
if versions.IsNewerThan(versions.MinAgh, rs.Version) {
|
|
rl.With("error", err, "version", rs.Version).Errorf("Replica AdGuard Home version must be >= %s", versions.MinAgh)
|
|
return
|
|
}
|
|
|
|
if versions.IsSame(rs.Version, versions.IncompatibleAPI) {
|
|
rl.With("error", err, "version", rs.Version).Errorf("Replica AdGuard Home runs with an incompatible API - Please ugrade to version %s or newer", versions.FixedIncompatibleAPI)
|
|
return
|
|
}
|
|
|
|
if o.status.Version != rs.Version {
|
|
rl.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
|
|
}
|
|
|
|
err = w.syncGeneralSettings(o, rs, rc)
|
|
if err != nil {
|
|
rl.With("error", err).Error("Error syncing general settings")
|
|
return
|
|
}
|
|
|
|
err = w.syncConfigs(o, rc)
|
|
if err != nil {
|
|
rl.With("error", err).Error("Error syncing configs")
|
|
return
|
|
}
|
|
|
|
err = w.syncRewrites(rl, o.rewrites, rc)
|
|
if err != nil {
|
|
rl.With("error", err).Error("Error syncing rewrites")
|
|
return
|
|
}
|
|
err = w.syncFilters(o.filters, rc)
|
|
if err != nil {
|
|
rl.With("error", err).Error("Error syncing filters")
|
|
return
|
|
}
|
|
|
|
err = w.syncServices(o.services, rc)
|
|
if err != nil {
|
|
rl.With("error", err).Error("Error syncing services")
|
|
return
|
|
}
|
|
|
|
if err = w.syncClients(o.clients, rc); err != nil {
|
|
rl.With("error", err).Error("Error syncing clients")
|
|
return
|
|
}
|
|
|
|
if err = w.syncDNS(o.accessList, o.dnsConfig, rc); err != nil {
|
|
rl.With("error", err).Error("Error syncing dns")
|
|
return
|
|
}
|
|
|
|
if w.cfg.Features.DHCP.ServerConfig || w.cfg.Features.DHCP.StaticLeases {
|
|
if err = w.syncDHCPServer(o.dhcpServerConfig, rc, replica); err != nil {
|
|
rl.With("error", err).Error("Error syncing dhcp")
|
|
return
|
|
}
|
|
}
|
|
|
|
rl.Info("Sync done")
|
|
}
|
|
|
|
func (w *worker) statusWithSetup(rl *zap.SugaredLogger, replica types.AdGuardInstance, rc client.Client) (*types.Status, error) {
|
|
rs, err := rc.Status()
|
|
if err != nil {
|
|
if replica.AutoSetup && errors.Is(err, client.ErrSetupNeeded) {
|
|
if serr := rc.Setup(); serr != nil {
|
|
rl.With("error", serr).Error("Error setup AdGuardHome")
|
|
return nil, err
|
|
}
|
|
return rc.Status()
|
|
}
|
|
return nil, err
|
|
}
|
|
return rs, err
|
|
}
|
|
|
|
func (w *worker) syncServices(os types.Services, replica client.Client) error {
|
|
if w.cfg.Features.Services {
|
|
rs, err := replica.Services()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !os.Equals(rs) {
|
|
if err := replica.SetServices(os); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncFilters(of *types.FilteringStatus, replica client.Client) error {
|
|
if w.cfg.Features.Filters {
|
|
rf, err := replica.Filtering()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = w.syncFilterType(of.Filters, rf.Filters, false, replica); err != nil {
|
|
return err
|
|
}
|
|
if err = w.syncFilterType(of.WhitelistFilters, rf.WhitelistFilters, true, replica); err != nil {
|
|
return err
|
|
}
|
|
|
|
if of.UserRules.String() != rf.UserRules.String() {
|
|
return replica.SetCustomRules(of.UserRules)
|
|
}
|
|
|
|
if of.Enabled != rf.Enabled || of.Interval != rf.Interval {
|
|
if err = replica.ToggleFiltering(of.Enabled, of.Interval); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncFilterType(of types.Filters, rFilters types.Filters, whitelist bool, replica client.Client) error {
|
|
fa, fu, fd := rFilters.Merge(of)
|
|
|
|
if err := replica.DeleteFilters(whitelist, fd...); err != nil {
|
|
return err
|
|
}
|
|
if err := replica.AddFilters(whitelist, fa...); err != nil {
|
|
return err
|
|
}
|
|
if err := replica.UpdateFilters(whitelist, fu...); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(fa) > 0 || len(fu) > 0 {
|
|
if err := replica.RefreshFilters(whitelist); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncRewrites(rl *zap.SugaredLogger, or *types.RewriteEntries, replica client.Client) error {
|
|
if w.cfg.Features.DNS.Rewrites {
|
|
replicaRewrites, err := replica.RewriteList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a, r, d := replicaRewrites.Merge(or)
|
|
|
|
if err = replica.DeleteRewriteEntries(r...); err != nil {
|
|
return err
|
|
}
|
|
if err = replica.AddRewriteEntries(a...); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, dupl := range d {
|
|
rl.With("domain", dupl.Domain, "answer", dupl.Answer).Warn("Skipping duplicated rewrite from source")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncClients(oc *types.Clients, replica client.Client) error {
|
|
if w.cfg.Features.ClientSettings {
|
|
rc, err := replica.Clients()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a, u, r := rc.Merge(oc)
|
|
|
|
if err = replica.DeleteClients(r...); err != nil {
|
|
return err
|
|
}
|
|
if err = replica.AddClients(a...); err != nil {
|
|
return err
|
|
}
|
|
if err = replica.UpdateClients(u...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncGeneralSettings(o *origin, rs *types.Status, replica client.Client) error {
|
|
if w.cfg.Features.GeneralSettings {
|
|
if o.status.ProtectionEnabled != rs.ProtectionEnabled {
|
|
if err := replica.ToggleProtection(o.status.ProtectionEnabled); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if rp, err := replica.Parental(); err != nil {
|
|
return err
|
|
} else if o.parental != rp {
|
|
if err = replica.ToggleParental(o.parental); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if rs, err := replica.SafeSearch(); err != nil {
|
|
return err
|
|
} else if o.safeSearch != rs {
|
|
if err = replica.ToggleSafeSearch(o.safeSearch); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if rs, err := replica.SafeBrowsing(); err != nil {
|
|
return err
|
|
} else if o.safeBrowsing != rs {
|
|
if err = replica.ToggleSafeBrowsing(o.safeBrowsing); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncConfigs(o *origin, rc client.Client) error {
|
|
if w.cfg.Features.QueryLogConfig {
|
|
qlc, err := rc.QueryLogConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !o.queryLogConfig.Equals(qlc) {
|
|
if err = rc.SetQueryLogConfig(o.queryLogConfig.Enabled, o.queryLogConfig.Interval, o.queryLogConfig.AnonymizeClientIP); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if w.cfg.Features.StatsConfig {
|
|
sc, err := rc.StatsConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if o.statsConfig.Interval != sc.Interval {
|
|
if err = rc.SetStatsConfig(o.statsConfig.Interval); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncDNS(oal *types.AccessList, odc *types.DNSConfig, rc client.Client) error {
|
|
if w.cfg.Features.DNS.AccessLists {
|
|
al, err := rc.AccessList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !al.Equals(oal) {
|
|
if err = rc.SetAccessList(oal); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if w.cfg.Features.DNS.ServerConfig {
|
|
dc, err := rc.DNSConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !dc.Equals(odc) {
|
|
if err = rc.SetDNSConfig(odc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *worker) syncDHCPServer(osc *types.DHCPServerConfig, rc client.Client, replica types.AdGuardInstance) error {
|
|
if !w.cfg.Features.DHCP.ServerConfig && !w.cfg.Features.DHCP.StaticLeases {
|
|
return nil
|
|
}
|
|
sc, err := rc.DHCPServerConfig()
|
|
if w.cfg.Features.DHCP.ServerConfig {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
origClone := osc.Clone()
|
|
if replica.InterfaceName != "" {
|
|
// overwrite interface name
|
|
origClone.InterfaceName = replica.InterfaceName
|
|
}
|
|
if replica.DHCPServerEnabled != nil {
|
|
// overwrite dhcp enabled
|
|
origClone.Enabled = *replica.DHCPServerEnabled
|
|
}
|
|
|
|
if !sc.Equals(origClone) {
|
|
if err = rc.SetDHCPServerConfig(origClone); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if w.cfg.Features.DHCP.StaticLeases {
|
|
a, r := sc.StaticLeases.Merge(osc.StaticLeases)
|
|
|
|
if err = rc.DeleteDHCPStaticLeases(r...); err != nil {
|
|
return err
|
|
}
|
|
if err = rc.AddDHCPStaticLeases(a...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type origin struct {
|
|
status *types.Status
|
|
rewrites *types.RewriteEntries
|
|
services types.Services
|
|
filters *types.FilteringStatus
|
|
clients *types.Clients
|
|
queryLogConfig *types.QueryLogConfig
|
|
statsConfig *types.IntervalConfig
|
|
accessList *types.AccessList
|
|
dnsConfig *types.DNSConfig
|
|
dhcpServerConfig *types.DHCPServerConfig
|
|
parental bool
|
|
safeSearch bool
|
|
safeBrowsing bool
|
|
}
|