commit e9d37f483cd988c9d539f7ba6d4c7c4dfbeae513 Author: Ryan Malloy Date: Wed May 20 18:25:36 2026 -0600 Initial commit: plugin skeleton, compiles against CoreDNS 1.14.3 Sets up the package layout for a CoreDNS plugin that will accept RFC 2136 dynamic updates with TSIG authentication, primarily targeting self-hosted ACME DNS-01 cert automation. What this commit gives us: - go.mod against coredns/caddy v1.1.4, coredns/coredns v1.14.3, miekg/dns v1.1.72 - plugin.go: RFC2136 struct + Handler interface (ServeDNS is pass-through) - setup.go: init() registration + Corefile parser (skeleton — recognizes tsig-key, ttl, persist directives but doesn't yet wire them) - README.md, .gitignore go build ./... clean. No tests yet — those come with Phase 1.2 alongside the actual UPDATE handler and in-memory store. Plan: ~/.claude/plans/dood-does-coredns-offer-enumerated-piglet.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722fc32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# Dependency directories (modules use go.sum, not vendor/) +vendor/ + +# IDE / editor +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store + +# Local development +*.local.go +coverage.out +coverage.html + +# Built CoreDNS binary if someone runs `make` from here for testing +/coredns diff --git a/README.md b/README.md new file mode 100644 index 0000000..be46c29 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# coredns-rfc2136 + +A [CoreDNS](https://coredns.io) plugin that accepts **RFC 2136 dynamic DNS +updates** (TSIG-authenticated), filling a gap in the official plugin set. + +CoreDNS as-shipped has no plugin for accepting dynamic updates — its +plugin model treats authoritative data as read-only (loaded from `auto`, +`file`, `secondary`, etc.). This plugin adds the missing piece. + +## Primary use case: self-hosted ACME DNS-01 + +The motivating problem: automate Let's Encrypt cert issuance for many +domains without depending on registrar APIs (Vultr/Route53/Cloudflare). +The architecture: + +``` +_acme-challenge.example.com CNAME .auth.supported.systems + │ + │ delegated NS to your CoreDNS host + ▼ + CoreDNS + rfc2136 plugin + │ + │ accepts TSIG UPDATEs from Caddy + │ (caddy-dns/rfc2136) or any other + │ ACME client + ▼ + Let's Encrypt validates +``` + +One-time per protected domain: add a `CNAME` glue line in your static +zones. After that, all cert issuance + renewal happens via UPDATE +messages — zero static zone-file churn. + +## Status + +**Phase 1 (skeleton)**: compiles, registers with CoreDNS, parses the +Corefile directive. Does not yet handle UPDATE messages or serve any +records. ServeDNS is a pass-through. See `phases.md` for the roadmap. + +## Configuration + +``` +rfc2136 [...] { + tsig-key + ttl + persist +} +``` + +Example: + +``` +.:53 auth.example.com { + rfc2136 auth.example.com { + tsig-key acme-key. hmac-sha256 BASE64SECRET== + ttl 60 + } + errors + log +} +``` + +## Building + +This plugin is consumed by a custom CoreDNS build via `plugin.cfg`: + +``` +# In CoreDNS source's plugin.cfg, BEFORE the `cache` plugin: +rfc2136:git.supportedsystems.net/rpm/coredns-rfc2136 +``` + +Then `go get git.supportedsystems.net/rpm/coredns-rfc2136 && make`. + +## License + +MIT (TODO: add LICENSE file). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..04bef8c --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module git.supportedsystems.net/rpm/coredns-rfc2136 + +go 1.25.0 + +require ( + github.com/coredns/caddy v1.1.4 + github.com/coredns/coredns v1.14.3 + github.com/miekg/dns v1.1.72 +) + +require ( + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pires/go-proxyproto v0.11.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ae5c065 --- /dev/null +++ b/go.sum @@ -0,0 +1,101 @@ +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coredns/caddy v1.1.4 h1:+Lls5xASB0QsA2jpCroCOwpPlb5GjIGlxdjXxdX0XVo= +github.com/coredns/caddy v1.1.4/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/coredns v1.14.3 h1:hWWoTdONblKIWhC8QPkxLEGIbewhR5xyTedqLVPsvvE= +github.com/coredns/coredns v1.14.3/go.mod h1:15BWsGGxupagKQ3p09pIIZ5kcgmyawquey6gqNWRaEI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..ae87aad --- /dev/null +++ b/plugin.go @@ -0,0 +1,55 @@ +// Package rfc2136 implements a CoreDNS plugin that accepts dynamic DNS +// updates per RFC 2136 (UPDATE opcode), authenticated via TSIG. The +// primary use case is self-hosted ACME DNS-01 cert automation: an ACME +// client (e.g. Caddy via caddy-dns/rfc2136) injects _acme-challenge TXT +// records into a delegated sub-zone that this plugin serves. +// +// Scope: +// - Handles UPDATE messages (OPCODE=5) for configured zones. +// - Verifies TSIG signatures (HMAC-SHA256 today; algorithm-pluggable). +// - Stores records in memory; optional periodic snapshot to disk. +// - Serves queries (SOA, NS, A, AAAA, TXT) for the configured zone +// from the in-memory store plus a synthetic SOA/NS apex. +// +// Non-goals: +// - General-purpose authoritative DNS (use `auto`/`file` for that). +// - DNSSEC signing (add later via the `dnssec` plugin in front). +// +// Phase 1 status: skeleton only. ServeDNS passes through to the next +// plugin; setup parses the Corefile but does not yet do anything with +// the parsed configuration. See plan at +// ~/.claude/plans/dood-does-coredns-offer-enumerated-piglet.md +package rfc2136 + +import ( + "context" + + "github.com/coredns/coredns/plugin" + "github.com/miekg/dns" +) + +// RFC2136 is the plugin handler. One instance per Corefile server block. +type RFC2136 struct { + // Next is the downstream plugin in the chain. + Next plugin.Handler + + // Zones is the set of canonical (dot-terminated, lowercase) zone + // names this instance is authoritative for. Queries outside these + // zones pass through to Next. + Zones []string + + // Phase 2 will add: + // - tsigKeys map[string]tsigKey + // - store *recordStore + // - ttl uint32 +} + +// Name implements plugin.Handler. +func (p *RFC2136) Name() string { return "rfc2136" } + +// ServeDNS implements plugin.Handler. Phase 1 is a pass-through so the +// plugin can register, parse config, and live in the chain without +// changing behavior. Phase 2 wires the UPDATE handler and zone serving. +func (p *RFC2136) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r) +} diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..e3be760 --- /dev/null +++ b/setup.go @@ -0,0 +1,92 @@ +package rfc2136 + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" +) + +// log is the package logger, scoped so messages are prefixed `[rfc2136]`. +var log = clog.NewWithPlugin("rfc2136") + +func init() { + plugin.Register("rfc2136", setup) +} + +// setup is invoked by the CoreDNS plugin registry once per Corefile +// `rfc2136` directive. It parses the directive's arguments and block, +// constructs an RFC2136 handler, and links it into the plugin chain. +func setup(c *caddy.Controller) error { + p, err := parse(c) + if err != nil { + return plugin.Error("rfc2136", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + p.Next = next + return p + }) + + log.Infof("registered for zones: %v", p.Zones) + return nil +} + +// parse reads a single `rfc2136 { ... }` block from the Corefile. +// +// Phase 1 grammar (only the surface is parsed; sub-directives are +// accepted but ignored — Phase 2 wires them): +// +// rfc2136 { +// tsig-key +// ttl +// persist +// } +func parse(c *caddy.Controller) (*RFC2136, error) { + p := &RFC2136{} + + for c.Next() { + args := c.RemainingArgs() + if len(args) < 1 { + return nil, c.ArgErr() + } + // Normalize each declared zone to lowercase + trailing dot + // (CoreDNS canonical form). This makes later zone-membership + // checks an exact match against r.Question[0].Name. + for _, z := range args { + p.Zones = append(p.Zones, plugin.Host(z).NormalizeExact()...) + } + + for c.NextBlock() { + switch c.Val() { + case "tsig-key": + kArgs := c.RemainingArgs() + if len(kArgs) != 3 { + return nil, c.Errf("tsig-key requires 3 args (name algorithm secret), got %d", len(kArgs)) + } + // Phase 2: store in p.tsigKeys[name] = tsigKey{algo, secret} + log.Debugf("tsig-key parsed (storage NYI): name=%s alg=%s", kArgs[0], kArgs[1]) + + case "ttl": + tArgs := c.RemainingArgs() + if len(tArgs) != 1 { + return nil, c.ArgErr() + } + // Phase 2: parse uint32, validate range, store in p.ttl + log.Debugf("ttl parsed (storage NYI): %s", tArgs[0]) + + case "persist": + pArgs := c.RemainingArgs() + if len(pArgs) != 1 { + return nil, c.ArgErr() + } + log.Debugf("persist parsed (storage NYI): %s", pArgs[0]) + + default: + return nil, c.Errf("unknown directive: %s", c.Val()) + } + } + } + + return p, nil +}