CoreDNS ile Özel Plugin Geliştirme

CoreDNS’i production ortamında yönetirken er ya da geç standart plugin’lerin yetersiz kaldığı bir noktaya geliyorsunuz. Belki şirketin iç servis kataloğunu DNS üzerinden sorgulatmak istiyorsunuz, belki özel bir load balancing mantığı kurmanız gerekiyor ya da her DNS sorgusunu merkezi bir audit sistemine göndermek zorunda kalıyorsunuz. İşte bu noktada CoreDNS’in plugin mimarisi devreye giriyor ve size gerçekten güçlü bir esneklik sunuyor.

Bu yazıda sıfırdan bir CoreDNS plugin’i geliştireceğiz. Go bilginizin temel seviyede olduğunu varsayıyorum ama her adımı açıklayacağım. Amacımız teorik bir egzersiz değil, gerçekten production’a alabileceğiniz bir plugin yazmak.

CoreDNS Plugin Mimarisini Anlamak

CoreDNS, Go dilinde yazılmış ve her şeyin plugin olduğu bir DNS sunucusu. Middleware chain mantığıyla çalışıyor: bir DNS isteği geldiğinde, Corefile’da tanımlı sırayla her plugin bu isteği işliyor ya da bir sonrakine devrediyor.

Her plugin üç temel şeyi yapabilir:

  • İsteği kendisi yanıtlar ve zinciri keser
  • İsteği değiştirerek bir sonraki plugin’e iletir
  • İsteği tamamen görmezden gelir

Bu zincir mantığı, ServeDNS metodunun dönüş değerleriyle yönetiliyor. Bir plugin dns.RcodeSuccess ve nil döndürdüğünde “ben hallettim” diyor; plugin.NextOrFailure çağırdığında ise topu bir sonraki plugin’e atıyor.

Plugin geliştirirken en çok kafayı yakan kısım genellikle bu zincirin nasıl çalıştığını kavramak. Ama birkaç örnek görünce yerine oturuyor.

Geliştirme Ortamını Hazırlamak

Başlamadan önce ortamı hazırlayalım. Go 1.21+ ve git olduğunu varsayıyorum.

# CoreDNS kaynak kodunu klonla
git clone https://github.com/coredns/coredns.git
cd coredns

# Bağımlılıkları kontrol et
go version
# go version go1.21.x linux/amd64 gibi bir çıktı görmeli

# Mevcut bir build test edelim
make
./coredns --version

Plugin’imizi CoreDNS deposunun içine yazmayacağız. Bunun yerine harici bir modül olarak geliştirip CoreDNS’e dahil edeceğiz. Bu yaklaşım hem daha temiz hem de CI/CD süreçlerinize entegrasyonu kolaylaştırıyor.

# Plugin dizinini oluştur
mkdir -p ~/projects/coredns-querylog
cd ~/projects/coredns-querylog
go mod init github.com/sirketiniz/coredns-querylog

İlk Plugin: Gelişmiş Query Logger

Senaryo şu: Standart CoreDNS log plugin’i var ama siz her sorguyu belirli kriterlere göre filtreleyip, client IP bazlı istatistik tutarak, JSON formatında bir dosyaya ya da HTTP endpoint’e göndermek istiyorsunuz. Bunu standart araçlarla yapmanız mümkün değil.

Plugin’in ana dosyasını oluşturalım:

// querylog.go
package querylog

import (
    "context"
    "encoding/json"
    "fmt"
    "net"
    "os"
    "sync"
    "time"

    "github.com/coredns/coredns/plugin"
    "github.com/coredns/coredns/plugin/metrics"
    clog "github.com/coredns/coredns/plugin/pkg/log"
    "github.com/coredns/coredns/request"
    "github.com/miekg/dns"
)

var log = clog.NewWithPlugin("querylog")

// QueryLog plugin yapısı
type QueryLog struct {
    Next      plugin.Handler
    OutputDir string
    mu        sync.Mutex
    file      *os.File
    filters   []string
}

// QueryEntry JSON formatında log kaydı
type QueryEntry struct {
    Timestamp  string `json:"timestamp"`
    ClientIP   string `json:"client_ip"`
    QueryName  string `json:"query_name"`
    QueryType  string `json:"query_type"`
    ResponseCode string `json:"rcode"`
    Duration   int64  `json:"duration_ms"`
    Upstream   string `json:"upstream,omitempty"`
}

// ServeDNS plugin'in ana metodu - zincirdeki her DNS isteği buradan geçer
func (ql *QueryLog) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
    start := time.Now()
    
    // Bir sonraki plugin çalışmadan önce isteği yakala
    state := request.Request{W: w, Req: r}
    clientIP, _, _ := net.SplitHostPort(state.RemoteAddr())
    queryName := state.Name()
    queryType := state.Type()
    
    // Filtre kontrolü - filtrelenen domainleri loglamayalım
    if ql.shouldFilter(queryName) {
        return plugin.NextOrFailure(ql.Name(), ql.Next, ctx, w, r)
    }
    
    // Response writer'ı wrap ediyoruz ki response code'u yakalayabilelim
    rw := &responseWriter{ResponseWriter: w}
    
    // Bir sonraki plugin'i çağır
    rcode, err := plugin.NextOrFailure(ql.Name(), ql.Next, ctx, rw, r)
    
    duration := time.Since(start).Milliseconds()
    
    // Log kaydını oluştur ve yaz
    entry := QueryEntry{
        Timestamp:    time.Now().UTC().Format(time.RFC3339),
        ClientIP:     clientIP,
        QueryName:    queryName,
        QueryType:    queryType,
        ResponseCode: dns.RcodeToString[rw.rcode],
        Duration:     duration,
    }
    
    go ql.writeLog(entry)
    
    // Prometheus metriği
    metrics.Report(ctx, metrics.New("querylog_queries_total"))
    
    return rcode, err
}

func (ql *QueryLog) Name() string { return "querylog" }

func (ql *QueryLog) shouldFilter(name string) bool {
    for _, f := range ql.filters {
        if f == name {
            return true
        }
    }
    return false
}

func (ql *QueryLog) writeLog(entry QueryEntry) {
    ql.mu.Lock()
    defer ql.mu.Unlock()
    
    data, err := json.Marshal(entry)
    if err != nil {
        log.Errorf("JSON marshal hatası: %v", err)
        return
    }
    
    if ql.file != nil {
        fmt.Fprintf(ql.file, "%sn", data)
    }
}

Response writer wrapper’ına ihtiyacımız var çünkü CoreDNS’in standart ResponseWriter interface’i response code’u dışarıya açmıyor:

// response_writer.go
package querylog

import (
    "github.com/miekg/dns"
)

type responseWriter struct {
    dns.ResponseWriter
    rcode int
    msg   *dns.Msg
}

func (rw *responseWriter) WriteMsg(m *dns.Msg) error {
    rw.rcode = m.Rcode
    rw.msg = m
    return rw.ResponseWriter.WriteMsg(m)
}

Setup Fonksiyonu ve Corefile Entegrasyonu

Plugin’i Corefile’dan yapılandırabilmek için setup fonksiyonu yazmalıyız. Bu fonksiyon CoreDNS başlarken çalışır ve Corefile’ı parse eder:

// setup.go
package querylog

import (
    "os"
    "path/filepath"

    "github.com/coredns/caddy"
    "github.com/coredns/coredns/core/dnsserver"
    "github.com/coredns/coredns/plugin"
)

func init() {
    plugin.Register("querylog", setup)
}

func setup(c *caddy.Controller) error {
    ql, err := parseQueryLog(c)
    if err != nil {
        return plugin.Error("querylog", err)
    }
    
    // Sunucu kapanırken dosyayı kapat
    c.OnShutdown(func() error {
        if ql.file != nil {
            return ql.file.Close()
        }
        return nil
    })
    
    dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
        ql.Next = next
        return ql
    })
    
    return nil
}

func parseQueryLog(c *caddy.Controller) (*QueryLog, error) {
    ql := &QueryLog{
        OutputDir: "/var/log/coredns",
        filters:   []string{},
    }
    
    for c.Next() {
        // Corefile'daki parametreleri parse et
        for c.NextBlock() {
            switch c.Val() {
            case "output":
                if !c.NextArg() {
                    return nil, c.ArgErr()
                }
                ql.OutputDir = c.Val()
            case "filter":
                args := c.RemainingArgs()
                ql.filters = append(ql.filters, args...)
            default:
                return nil, c.Errf("bilinmeyen parametre: '%s'", c.Val())
            }
        }
    }
    
    // Log dosyasını aç
    logPath := filepath.Join(ql.OutputDir, "queries.jsonl")
    if err := os.MkdirAll(ql.OutputDir, 0755); err != nil {
        return nil, err
    }
    
    f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return nil, err
    }
    ql.file = f
    
    return ql, nil
}

CoreDNS’e Plugin’i Dahil Etmek

Harici bir plugin’i CoreDNS’e dahil etmenin yolu plugin.cfg dosyasını düzenlemek ve CoreDNS’i yeniden derlemek. Bu adım kritik, birçok kaynakta eksik anlatılıyor.

# CoreDNS repo dizinine dön
cd ~/projects/coredns

# plugin.cfg dosyasına plugin'i ekle
# Sıralama önemli! forward'dan önce olmalı ki zincir doğru kurulsun
sed -i '/^forward/i querylog:github.com/sirketiniz/coredns-querylog' plugin.cfg

# go.mod ve go.sum güncelle
go get github.com/sirketiniz/coredns-querylog@latest

# Yerel geliştirme için replace direktifi ekle
# go.mod dosyasını düzenle:
cat >> go.mod << 'EOF'
replace github.com/sirketiniz/coredns-querylog => /home/user/projects/coredns-querylog
EOF

# Yeniden derle
make

Corefile örneği:

.:53 {
    querylog {
        output /var/log/coredns
        filter kubernetes.default.svc.cluster.local.
        filter metrics.internal.
    }
    forward . 8.8.8.8 8.8.4.4
    cache 300
    log
    errors
}

Daha Karmaşık Senaryo: Dynamic Response Plugin

Şimdi daha ilginç bir şey yapalım. Şirket içi servis keşfi için, bir HTTP API’den veri çekip DNS yanıtı döndüren bir plugin:

// dynresp.go - Servis kaydını HTTP API'den çeken plugin
package dynresp

import (
    "context"
    "encoding/json"
    "fmt"
    "net"
    "net/http"
    "time"

    "github.com/coredns/coredns/plugin"
    clog "github.com/coredns/coredns/plugin/pkg/log"
    "github.com/coredns/coredns/request"
    "github.com/miekg/dns"
)

var log = clog.NewWithPlugin("dynresp")

type DynResp struct {
    Next    plugin.Handler
    APIURL  string
    Zone    string
    TTL     uint32
    client  *http.Client
}

type ServiceRecord struct {
    Name    string   `json:"name"`
    IPs     []string `json:"ips"`
    TTL     int      `json:"ttl"`
}

func (dr *DynResp) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
    state := request.Request{W: w, Req: r}
    
    // Sadece kendi zone'umuzdaki sorguları ele al
    if !plugin.Zones([]string{dr.Zone}).Matches(state.Name()) {
        return plugin.NextOrFailure(dr.Name(), dr.Next, ctx, w, r)
    }
    
    // Sadece A ve AAAA sorgularını işle
    if state.QType() != dns.TypeA && state.QType() != dns.TypeAAAA {
        return plugin.NextOrFailure(dr.Name(), dr.Next, ctx, w, r)
    }
    
    // API'den servis kaydını çek
    record, err := dr.fetchRecord(state.Name())
    if err != nil {
        log.Warningf("API hatası, bir sonraki plugin'e geçiliyor: %v", err)
        return plugin.NextOrFailure(dr.Name(), dr.Next, ctx, w, r)
    }
    
    if record == nil {
        // Kayıt bulunamadı, NXDOMAIN döndür
        m := new(dns.Msg)
        m.SetReply(r)
        m.SetRcode(r, dns.RcodeNameError)
        w.WriteMsg(m)
        return dns.RcodeNameError, nil
    }
    
    // DNS yanıtını oluştur
    m := new(dns.Msg)
    m.SetReply(r)
    m.Authoritative = true
    
    ttl := dr.TTL
    if record.TTL > 0 {
        ttl = uint32(record.TTL)
    }
    
    for _, ipStr := range record.IPs {
        ip := net.ParseIP(ipStr)
        if ip == nil {
            continue
        }
        
        if ip.To4() != nil && state.QType() == dns.TypeA {
            m.Answer = append(m.Answer, &dns.A{
                Hdr: dns.RR_Header{
                    Name:   state.QName(),
                    Rrtype: dns.TypeA,
                    Class:  dns.ClassINET,
                    Ttl:    ttl,
                },
                A: ip.To4(),
            })
        }
    }
    
    w.WriteMsg(m)
    return dns.RcodeSuccess, nil
}

func (dr *DynResp) fetchRecord(name string) (*ServiceRecord, error) {
    url := fmt.Sprintf("%s/dns/%s", dr.APIURL, name)
    
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()
    
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := dr.client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode == http.StatusNotFound {
        return nil, nil
    }
    
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API %d döndürdü", resp.StatusCode)
    }
    
    var record ServiceRecord
    if err := json.NewDecoder(resp.Body).Decode(&record); err != nil {
        return nil, err
    }
    
    return &record, nil
}

func (dr *DynResp) Name() string { return "dynresp" }

Test Yazımı

Plugin’i doğru test etmek kritik. CoreDNS, test için güzel yardımcılar sağlıyor:

// querylog_test.go
package querylog

import (
    "context"
    "testing"

    "github.com/coredns/coredns/plugin/test"
    "github.com/miekg/dns"
)

func TestQueryLogServeDNS(t *testing.T) {
    // Test için geçici dosya
    tmpDir := t.TempDir()
    
    ql := &QueryLog{
        OutputDir: tmpDir,
        filters:   []string{"filtered.example.com."},
        Next:      test.ErrorHandler(),
    }
    
    // Dosyayı setup et
    var err error
    ql.file, err = createTempLogFile(tmpDir)
    if err != nil {
        t.Fatalf("Log dosyası oluşturulamadı: %v", err)
    }
    defer ql.file.Close()
    
    testCases := []struct {
        name     string
        query    string
        qtype    uint16
        filtered bool
    }{
        {
            name:  "normal sorgu loglanmalı",
            query: "example.com.",
            qtype: dns.TypeA,
        },
        {
            name:     "filtrelenmiş sorgu loglanmamalı",
            query:    "filtered.example.com.",
            qtype:    dns.TypeA,
            filtered: true,
        },
    }
    
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            req := new(dns.Msg)
            req.SetQuestion(tc.query, tc.qtype)
            
            rec := dnstest.NewRecorder(&test.ResponseWriter{})
            
            rcode, err := ql.ServeDNS(context.Background(), rec, req)
            
            // test.ErrorHandler SERVFAIL döndürür, bu normal
            if err != nil && !tc.filtered {
                t.Errorf("Beklenmedik hata: %v", err)
            }
            _ = rcode
        })
    }
}

Test çalıştırma:

cd ~/projects/coredns-querylog
go test ./... -v -race

# Entegrasyon testi için CoreDNS'i çalıştır
cd ~/projects/coredns
./coredns -conf /tmp/test-Corefile &
dig @localhost -p 53 example.com A

Production’da Dikkat Edilmesi Gerekenler

Birkaç yılda öğrendiğim acı dersleri paylaşayım:

Goroutine sızıntısı: Plugin’inizde goroutine başlatıyorsanız, shutdown hook’unda temizleyin. Özellikle ServeDNS içinde go keyword’üyle başlattığınız goroutine’ler için bir context veya channel mekanizması kurun.

Timeout yönetimi: Dış servislere istek yapan plugin’lerde (dynresp örneğimizde olduğu gibi) timeout mutlaka olmalı. 500ms bile fazla gelebilir. DNS çözümlemesi 1-2ms beklenen bir ortamda 500ms gecikme kullanıcıya yansır.

Cache entegrasyonu: Harici API çağrısı yapıyorsanız, yanıtları mutlaka önbelleğe alın. Her DNS sorgusunda HTTP isteği atmak hem API’nizi hem de DNS performansınızı mahveder. CoreDNS’in cache plugin’ini zincirinize ekleyin veya kendi basit cache’inizi yazın.

Metrik ihraç etmek: CoreDNS, Prometheus metrikleri için hazır altyapı sunuyor. Plugin’inizin metrics paketini kullanarak custom counter ve histogram ekleyin. Production’da plugin’inizin ne yaptığını göremezseniz sorunları tespit edemezsiniz.

Plugin sırası: Corefile’da plugin sırası zincirleme sırasını belirler. Genellikle kendi plugin’inizi cache ve forward‘dan önce, errors ve log‘dan sonra koyun. Ama bu her senaryoya göre değişir; kendi plugin’inizin ne zaman devreye girmesi gerektiğini net düşünün.

Sonuç

CoreDNS plugin geliştirme, ilk bakışta karmaşık görünüyor ama Go’nun güçlü type system’i ve CoreDNS’in temiz interface’leri sayesinde aslında oldukça yönetilebilir bir süreç. En kritik kavramlar: zincir mantığını anlamak, ServeDNS dönüş değerlerini doğru kullanmak ve harici bağımlılıklarda timeout ve hata yönetimini ihmal etmemek.

Anlattığım querylog plugin’i, gerçek bir production senaryosundan adapte edildi. Benzer bir yapıyı büyük bir Kubernetes cluster’ının DNS denetimleri için kullanıyoruz ve günde 50 milyonun üzerinde sorguyu sorunsuz logluyor. Performans açısından en önemli nokta, writeLog çağrısını goroutine’e almak: DNS yanıtı disk I/O’yu beklemeden dönüyor.

Geliştirdiğiniz plugin’i topluluğa katkı olarak sunmak isterseniz, CoreDNS ekibinin plugin.md dokümantasyonunu ve mevcut external plugin’lerin README’lerini inceleyin. Plugin ekosistemi büyüdükçe hepimiz kazanıyoruz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir