Ctrl K
ComunidadGuías…

adguardhome/provider.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}