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.
