adguardhome/provider.go
Rama main · réplica de lectura del repositorio original.
package adguardhome
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
)
var (
errNotManaged = fmt.Errorf("rule is managed by external-dns")
errArtificialRecord = fmt.Errorf("artificial domain rule")
)
const (
managedBy = "$managed by external-dns"
envURL = "ADGUARD_HOME_URL"
envPassword = "ADGUARD_HOME_PASS"
envUser = "ADGUARD_HOME_USER"
envManagedBy = "ADGUARD_HOME_MANAGED_BY_REF"
envUseDNSRewrite = "ADGUARD_HOME_USE_DNSREWRITE"
)
type AdguardHomeProvider struct {
provider.BaseProvider
client Client
domainFilter *endpoint.DomainFilter
managedBySuffix string
ruleFormat ruleFormat
}
// NewAdguardHomeProvider configures a provider without making HTTP requests.
// Cancelling ctx cancels outgoing AdGuard HTTP requests throughout its lifetime.
func NewAdguardHomeProvider(ctx context.Context, dryRun bool) (*AdguardHomeProvider, error) {
adguardHomeURL, adguardHomeUrlOk := os.LookupEnv(envURL)
if !adguardHomeUrlOk {
return nil, fmt.Errorf("no url was found in environment variable ADGUARD_HOME_URL")
}
// Adjust the URL to match the API requirements
if !strings.HasSuffix(adguardHomeURL, "/") {
adguardHomeURL = adguardHomeURL + "/"
}
if !strings.HasSuffix(adguardHomeURL, "control/") {
adguardHomeURL = adguardHomeURL + "control/"
}
adguardHomeUser, adguardHomeUserOk := os.LookupEnv(envUser)
if !adguardHomeUserOk {
return nil, fmt.Errorf("no user was found in environment variable ADGUARD_HOME_USER")
}
adguardHomePass, adguardHomePassOk := os.LookupEnv(envPassword)
if !adguardHomePassOk {
return nil, fmt.Errorf("no password was found in environment variable ADGUARD_HOME_PASS")
}
var useDNSRewrite bool
if value := os.Getenv(envUseDNSRewrite); value != "" {
var err error
useDNSRewrite, err = strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf("invalid %s: %w", envUseDNSRewrite, err)
}
}
c := newAdguardHomeClient(ctx, adguardHomeURL, adguardHomeUser, adguardHomePass, dryRun)
managedBySuffix, _ := os.LookupEnv(envManagedBy)
format := legacyRuleFormat()
if useDNSRewrite {
format = rewriteRuleFormat()
}
p := &AdguardHomeProvider{
client: c,
domainFilter: &endpoint.DomainFilter{},
managedBySuffix: managedBySuffix,
ruleFormat: format,
}
log.Debugf("AdguardHome provider configured with url %s", adguardHomeURL)
return p, nil
}
// Initialize migrates managed filtering rules before the webhook starts serving.
func (p *AdguardHomeProvider) Initialize(ctx context.Context) error {
if err := ctx.Err(); err != nil {
return err
}
if err := p.migrateRules(ctx); err != nil {
return fmt.Errorf("failed to migrate filtering rules: %w", err)
}
return nil
}
func (p *AdguardHomeProvider) getManagedBy() string {
if p.managedBySuffix == "" {
return managedBy
}
return fmt.Sprintf("%s;ref:%s", managedBy, p.managedBySuffix)
}
// migrateRules runs at startup because ExternalDNS does not call ApplyChanges
// when the desired DNS records have not changed.
func (p *AdguardHomeProvider) migrateRules(ctx context.Context) error {
rules, err := p.client.GetFilteringRules(ctx)
if err != nil {
return err
}
for _, original := range rules {
rule := strings.TrimSpace(original)
e, err := parseRule(rule, p.getManagedBy())
if errors.Is(err, errNotManaged) || errors.Is(err, errArtificialRecord) {
continue
}
if err != nil {
return err
}
if endpointSupported(e) && p.ruleFormat.needsMigration(rule) {
log.Info("Migrating managed filtering rules")
return p.applyChanges(ctx, rules, &plan.Changes{})
}
}
return nil
}
// ApplyChanges implements Provider, syncing desired state with the AdguardHome server Local DNS.
func (p *AdguardHomeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("ApplyChanges: %+v", changes)
originalRules, err := p.client.GetFilteringRules(ctx)
if err != nil {
return err
}
return p.applyChanges(ctx, originalRules, changes)
}
func (p *AdguardHomeProvider) applyChanges(ctx context.Context, originalRules []string, changes *plan.Changes) error {
log.Debugf("loaded existing rules: %+v", originalRules)
suffix := p.getManagedBy()
endpoints, resultingRules, err := parseRules(originalRules, suffix)
if err != nil {
return err
}
for _, deleteEndpoint := range append(changes.UpdateOld, changes.Delete...) {
for _, target := range deleteEndpoint.Targets {
for endpointIndex, endpointValue := range endpoints {
if endpointValue.DNSName == deleteEndpoint.DNSName && endpointValue.RecordType == deleteEndpoint.RecordType && endpointValue.Targets[0] == target {
endpoints = append(endpoints[:endpointIndex], endpoints[endpointIndex+1:]...)
log.Debugf("delete custom rule %s", deleteEndpoint)
break
}
}
}
}
for _, createEndpoint := range append(changes.Create, changes.UpdateNew...) {
if !endpointSupported(createEndpoint) {
continue
}
for _, target := range createEndpoint.Targets {
endpoints = append(endpoints, &endpoint.Endpoint{
DNSName: createEndpoint.DNSName,
Targets: endpoint.Targets{target},
RecordType: createEndpoint.RecordType,
Labels: createEndpoint.Labels,
})
}
log.Debugf("add custom rule %s", createEndpoint)
}
managedRules, err := p.ruleFormat.formatRules(endpoints, suffix)
if err != nil {
return err
}
resultingRules = append(resultingRules, managedRules...)
return p.client.SaveFilteringRules(ctx, resultingRules)
}
// Records implements Provider, populating a slice of endpoints from
// AdguardHome local DNS.
func (p *AdguardHomeProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
resp, err := p.client.GetFilteringRules(ctx)
if err != nil {
log.Errorf("Error %s", err)
return nil, err
}
log.WithFields(log.Fields{
"func": "records",
"rules": resp,
}).Debugf("retrieved AdguardHome rules")
endpoints, _, err := parseRules(resp, p.getManagedBy())
if err != nil {
return nil, err
}
var ret []*endpoint.Endpoint
endpointsByKey := make(map[endpoint.EndpointKey]*endpoint.Endpoint)
for _, e := range endpoints {
if !p.domainFilter.Match(e.DNSName) {
continue
}
key := e.Key()
if endpointsByKey[key] != nil {
endpointsByKey[key].Targets = append(endpointsByKey[key].Targets, e.Targets...)
} else {
ret = append(ret, e)
endpointsByKey[key] = e
}
}
return ret, nil
}
// endpointSupported returns true if the endpoint is supported by the provider
// it is only possible to store A and TXT records in AdguardHome
func endpointSupported(e *endpoint.Endpoint) bool {
return e.RecordType == endpoint.RecordTypeA || e.RecordType == endpoint.RecordTypeTXT
}