jarld downloads blocklists (Adblock, hosts or plain domains), checks them daily and reloads named only when a list changed; your own block and allow rules take precedence, and the most specific rule wins. Lists and rules apply per view (default: every view that offers recursion). Web UI Blocking page with name checks, jarl blocklist and jarl block, the /blocking API, blocked counts in the statistics. Database schema 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
7.5 KiB
Go
213 lines
7.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
|
|
"jarl/internal/dnsname"
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
// DiffOps returns operations that turn from into to. It is used to record
|
|
// how to undo an applied changeset and to turn an imported configuration
|
|
// into a changeset. Create operations keep the object IDs of to.
|
|
func DiffOps(from, to *model.State) []model.Op {
|
|
var ops []model.Op
|
|
add := func(kind string, id int64, base int, data any, summary string) {
|
|
ops = append(ops, newOp(kind, id, base, data, summary))
|
|
}
|
|
if !reflect.DeepEqual(stripVersion(from.Settings), stripVersion(to.Settings)) {
|
|
s := to.Settings
|
|
add(OpSettingsUpdate, 0, 0, SettingsInput{DefaultTTL: &s.DefaultTTL, Nameservers: model.Ptr(append([]string{}, s.Nameservers...)), RName: &s.RName,
|
|
Refresh: &s.Refresh, Retry: &s.Retry, Expire: &s.Expire, NegativeTTL: &s.NegativeTTL, SerialPolicy: &s.SerialPolicy}, "restore settings")
|
|
}
|
|
|
|
for _, k := range sortedKeys(to.Keys) {
|
|
n := to.Keys[k]
|
|
in := TSIGInput{Name: &n.Name, Algorithm: &n.Algorithm, Secret: &n.Secret, Comment: &n.Comment}
|
|
if o, ok := from.Keys[k]; !ok {
|
|
add(OpTSIGCreate, k, 0, in, "create TSIG key "+n.Name)
|
|
} else if !sameObject(o, n) {
|
|
add(OpTSIGUpdate, k, o.Version, in, "restore TSIG key "+n.Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.ACLs) {
|
|
n := to.ACLs[k]
|
|
in := ACLInput{Name: &n.Name, Elements: model.Ptr(append([]string{}, n.Elements...)), Comment: &n.Comment}
|
|
if o, ok := from.ACLs[k]; !ok {
|
|
add(OpACLCreate, k, 0, in, "create ACL "+n.Name)
|
|
} else if !sameObject(o, n) {
|
|
add(OpACLUpdate, k, o.Version, in, "restore ACL "+n.Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.Views) {
|
|
n := to.Views[k]
|
|
in := ViewInput{Name: &n.Name, MatchClients: model.Ptr(append([]string{}, n.MatchClients...)),
|
|
MatchDestinations: model.Ptr(append([]string{}, n.MatchDestinations...)), Comment: &n.Comment, Position: model.Ptr(n.Position)}
|
|
if n.MatchRecursiveOnly != nil {
|
|
in.MatchRecursiveOnly = model.Some(*n.MatchRecursiveOnly)
|
|
} else {
|
|
in.MatchRecursiveOnly = model.Opt[bool]{Set: true, Null: true}
|
|
}
|
|
if o, ok := from.Views[k]; !ok {
|
|
add(OpViewCreate, k, 0, in, "create view "+n.Name)
|
|
} else if !sameObject(o, n) {
|
|
add(OpViewUpdate, k, o.Version, in, "restore view "+n.Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.Zones) {
|
|
n := to.Zones[k]
|
|
s := n.SOA
|
|
in := ZoneInput{Name: &n.Name, ViewID: &n.ViewID, Type: &n.Type, InView: &n.InView, File: &n.File,
|
|
DefaultTTL: &n.DefaultTTL, SerialPolicy: &n.SerialPolicy, DNSSECPolicy: &n.DNSSECPolicy, Comment: &n.Comment}
|
|
if n.Type == model.ZonePrimary {
|
|
in.SOA = &SOAInput{MName: &s.MName, RName: &s.RName, Serial: &s.Serial, Refresh: &s.Refresh, Retry: &s.Retry, Expire: &s.Expire, Minimum: &s.Minimum}
|
|
}
|
|
if o, ok := from.Zones[k]; !ok {
|
|
add(OpZoneCreate, k, 0, in, "create zone "+zoneDesc(to, n))
|
|
} else if !sameObject(o, n) {
|
|
in.Name = nil // zones can't be renamed
|
|
if n.Type == model.ZonePrimary && o.SOA.Serial != s.Serial {
|
|
// Never move a serial backwards: let the normal bump
|
|
// happen instead.
|
|
in.SOA.Serial = nil
|
|
}
|
|
add(OpZoneUpdate, k, o.Version, in, "restore zone "+zoneDesc(to, n))
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.Options) {
|
|
n := to.Options[k]
|
|
in := OptionInput{Scope: &n.Scope, ScopeID: &n.ScopeID, Name: &n.Name, Value: &n.Value, Comment: &n.Comment, Position: &n.Position}
|
|
if o, ok := from.Options[k]; !ok {
|
|
add(OpOptionCreate, k, 0, in, "set "+optionLabel(to, n)+" "+n.Value)
|
|
} else if !sameObject(o, n) {
|
|
in.Scope, in.ScopeID = nil, nil
|
|
add(OpOptionUpdate, k, o.Version, in, "restore "+optionLabel(to, n))
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.Records) {
|
|
n := to.Records[k]
|
|
in := RecordInput{ZoneID: n.ZoneID, Name: &n.Name, Type: &n.Type, Data: &n.Data, Comment: &n.Comment, Disabled: &n.Disabled}
|
|
if n.TTL != nil {
|
|
in.TTL = model.Some(*n.TTL)
|
|
} else {
|
|
in.TTL = model.Opt[uint32]{Set: true, Null: true}
|
|
}
|
|
if o, ok := from.Records[k]; !ok {
|
|
add(OpRecordCreate, k, 0, in, fmt.Sprintf("add %s %s %s", n.Name, n.Type, n.Data))
|
|
} else if !sameObject(o, n) {
|
|
in.ZoneID = 0
|
|
add(OpRecordUpdate, k, o.Version, in, fmt.Sprintf("restore %s %s %s", n.Name, n.Type, n.Data))
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.Blocklists) {
|
|
n := to.Blocklists[k]
|
|
in := BlocklistInput{Name: &n.Name, URL: &n.URL, ViewIDs: model.Ptr(append([]int64{}, n.ViewIDs...)), Disabled: &n.Disabled, Comment: &n.Comment}
|
|
if o, ok := from.Blocklists[k]; !ok {
|
|
add(OpBlocklistCreate, k, 0, in, "add blocklist "+n.Name)
|
|
} else if !sameObject(o, n) {
|
|
add(OpBlocklistUpdate, k, o.Version, in, "restore blocklist "+n.Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(to.BlockRules) {
|
|
n := to.BlockRules[k]
|
|
in := BlockRuleInput{Name: &n.Name, Action: &n.Action, Exact: &n.Exact, ViewIDs: model.Ptr(append([]int64{}, n.ViewIDs...)), Comment: &n.Comment}
|
|
if o, ok := from.BlockRules[k]; !ok {
|
|
add(OpBlockRuleCreate, k, 0, in, "add rule: "+ruleLabel(n))
|
|
} else if !sameObject(o, n) {
|
|
add(OpBlockRuleUpdate, k, o.Version, in, "restore rule: "+ruleLabel(n))
|
|
}
|
|
}
|
|
// Deletions, most dependent objects first.
|
|
for _, k := range sortedKeys(from.BlockRules) {
|
|
if _, ok := to.BlockRules[k]; !ok {
|
|
add(OpBlockRuleDelete, k, from.BlockRules[k].Version, nil, "delete rule: "+ruleLabel(from.BlockRules[k]))
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.Blocklists) {
|
|
if _, ok := to.Blocklists[k]; !ok {
|
|
add(OpBlocklistDelete, k, from.Blocklists[k].Version, nil, "delete blocklist "+from.Blocklists[k].Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.Records) {
|
|
if _, ok := to.Records[k]; !ok {
|
|
o := from.Records[k]
|
|
// Records of zones that are deleted go with the zone.
|
|
if _, zoneStays := to.Zones[o.ZoneID]; zoneStays {
|
|
add(OpRecordDelete, k, o.Version, nil, fmt.Sprintf("delete %s %s %s", o.Name, o.Type, o.Data))
|
|
}
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.Options) {
|
|
if _, ok := to.Options[k]; !ok {
|
|
o := from.Options[k]
|
|
stays := true
|
|
switch o.Scope {
|
|
case model.ScopeZone:
|
|
_, stays = to.Zones[o.ScopeID]
|
|
case model.ScopeView:
|
|
_, stays = to.Views[o.ScopeID]
|
|
}
|
|
if stays {
|
|
add(OpOptionDelete, k, o.Version, nil, "delete "+optionLabel(from, o))
|
|
}
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.Zones) {
|
|
if _, ok := to.Zones[k]; !ok {
|
|
o := from.Zones[k]
|
|
add(OpZoneDelete, k, o.Version, nil, "delete zone "+dnsname.Display(o.Name))
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.Views) {
|
|
if _, ok := to.Views[k]; !ok {
|
|
o := from.Views[k]
|
|
add(OpViewDelete, k, o.Version, DeleteInput{Cascade: true}, "delete view "+o.Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.ACLs) {
|
|
if _, ok := to.ACLs[k]; !ok {
|
|
add(OpACLDelete, k, from.ACLs[k].Version, nil, "delete ACL "+from.ACLs[k].Name)
|
|
}
|
|
}
|
|
for _, k := range sortedKeys(from.Keys) {
|
|
if _, ok := to.Keys[k]; !ok {
|
|
add(OpTSIGDelete, k, from.Keys[k].Version, nil, "delete TSIG key "+from.Keys[k].Name)
|
|
}
|
|
}
|
|
for i := range ops {
|
|
ops[i].Seq = i + 1
|
|
}
|
|
return ops
|
|
}
|
|
|
|
func stripVersion(s model.Settings) model.Settings {
|
|
s.Version = 0
|
|
return s
|
|
}
|
|
|
|
func sortedKeys[T any](m map[int64]*T) []int64 {
|
|
keys := make([]int64, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
|
return keys
|
|
}
|
|
|
|
// sameObject compares two objects ignoring their meta data.
|
|
func sameObject[T any](a, b *T) bool {
|
|
ca, cb := *a, *b
|
|
clearMeta(&ca)
|
|
clearMeta(&cb)
|
|
return reflect.DeepEqual(ca, cb)
|
|
}
|
|
|
|
func clearMeta(v any) {
|
|
rv := reflect.ValueOf(v).Elem()
|
|
if f := rv.FieldByName("Meta"); f.IsValid() {
|
|
f.Set(reflect.Zero(f.Type()))
|
|
}
|
|
}
|