initial commit
This commit is contained in:
commit
772469d21b
27 changed files with 1121 additions and 0 deletions
9
.envrc
Normal file
9
.envrc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Automatically sets up your devbox environment whenever you cd into this
|
||||||
|
# directory via our direnv integration:
|
||||||
|
|
||||||
|
eval "$(devbox generate direnv --print-envrc)"
|
||||||
|
|
||||||
|
# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/
|
||||||
|
# for more details
|
||||||
26
.pre-commit-config.yaml
Normal file
26
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: detect-private-key
|
||||||
|
|
||||||
|
- repo: https://github.com/dnephin/pre-commit-golang
|
||||||
|
rev: v0.5.1
|
||||||
|
hooks:
|
||||||
|
- id: go-fmt
|
||||||
|
- id: go-vet
|
||||||
|
- id: go-imports
|
||||||
|
- id: go-unit-tests
|
||||||
|
- id: go-build
|
||||||
|
- id: go-mod-tidy
|
||||||
|
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v1.62.2
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint
|
||||||
5
README.md
Normal file
5
README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# yaml
|
||||||
|
|
||||||
|
## Name
|
||||||
|
|
||||||
|
*yaml* - enables serving zone data from a YAML file.
|
||||||
7
devbox.json
Normal file
7
devbox.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.2/.schema/devbox.schema.json",
|
||||||
|
"packages": [
|
||||||
|
"go@1.24.10",
|
||||||
|
"goimports@latest"
|
||||||
|
]
|
||||||
|
}
|
||||||
63
devbox.lock
Normal file
63
devbox.lock
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"lockfile_version": "1",
|
||||||
|
"packages": {
|
||||||
|
"github:NixOS/nixpkgs/nixpkgs-unstable": {
|
||||||
|
"last_modified": "2025-11-30T18:29:45Z",
|
||||||
|
"resolved": "github:NixOS/nixpkgs/23258e03aaa49b3a68597e3e50eb0cbce7e42e9d?lastModified=1764527385&narHash=sha256-nA5ywiGKl76atrbdZ5Aucd8SjF%2Fv8ew9b9QsC%2BMKL14%3D"
|
||||||
|
},
|
||||||
|
"go@1.24.10": {
|
||||||
|
"last_modified": "2025-11-23T21:50:36Z",
|
||||||
|
"resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#go_1_24",
|
||||||
|
"source": "devbox-search",
|
||||||
|
"version": "1.24.10",
|
||||||
|
"systems": {
|
||||||
|
"aarch64-darwin": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/mzqh3c6jhjb8nglq6aivgmhqzrgc1416-go-1.24.10",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/mzqh3c6jhjb8nglq6aivgmhqzrgc1416-go-1.24.10"
|
||||||
|
},
|
||||||
|
"aarch64-linux": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/qjw3k4983j843n19xifai1vn79hhd2hh-go-1.24.10",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/qjw3k4983j843n19xifai1vn79hhd2hh-go-1.24.10"
|
||||||
|
},
|
||||||
|
"x86_64-darwin": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/0q1jah9apr7mayvwqgdg0ls0k5mw1586-go-1.24.10",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/0q1jah9apr7mayvwqgdg0ls0k5mw1586-go-1.24.10"
|
||||||
|
},
|
||||||
|
"x86_64-linux": {
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "out",
|
||||||
|
"path": "/nix/store/39x7yrmrxdws2866fbmav3zhwy01fkis-go-1.24.10",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_path": "/nix/store/39x7yrmrxdws2866fbmav3zhwy01fkis-go-1.24.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"goimports@latest": {
|
||||||
|
"last_modified": "2022-02-15T04:21:50Z",
|
||||||
|
"resolved": "github:NixOS/nixpkgs/3a641defd170a4ef25ce8c7c64cb13f91f867fca#goimports",
|
||||||
|
"source": "devbox-search",
|
||||||
|
"version": "2021-01-13"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
go.mod
Normal file
37
go.mod
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
module go.dxs.gay/coredns-yaml
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495
|
||||||
|
github.com/coredns/coredns v1.13.1
|
||||||
|
github.com/goccy/go-yaml v1.18.0
|
||||||
|
github.com/miekg/dns v1.1.68
|
||||||
|
)
|
||||||
|
|
||||||
|
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/kr/text v0.2.0 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
|
golang.org/x/crypto v0.42.0 // indirect
|
||||||
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
|
golang.org/x/net v0.45.0 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||||
|
google.golang.org/grpc v1.75.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
)
|
||||||
109
go.sum
Normal file
109
go.sum
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
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-0.20250930002214-15135a999495 h1:JFeOmbjLnVRhvmLHyuO3M1pfXWlPWpwkdM8UqXZRtBg=
|
||||||
|
github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
|
||||||
|
github.com/coredns/coredns v1.13.1 h1:yhYvf/QVwHNjBK65RkC8d9VW91dP9XKem3BOon4eokg=
|
||||||
|
github.com/coredns/coredns v1.13.1/go.mod h1:UHmBXdGEn/WQ1jdyMYgOxFh/VklkE//arIAxptwVAZI=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||||
|
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||||
|
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/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.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
|
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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
|
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
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.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
|
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||||
|
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||||
|
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
1
testdata/bad_include_multiple_documents.yaml
vendored
Normal file
1
testdata/bad_include_multiple_documents.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!include bad_multiple_documents.yaml
|
||||||
1
testdata/bad_include_tag_value_not_scalar.yaml
vendored
Normal file
1
testdata/bad_include_tag_value_not_scalar.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"@": !include []
|
||||||
3
testdata/bad_invalid_record_missing_type.yaml
vendored
Normal file
3
testdata/bad_invalid_record_missing_type.yaml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
"@":
|
||||||
|
- ttl: 60
|
||||||
|
value: 192.0.2.1
|
||||||
3
testdata/bad_invalid_record_missing_value.yaml
vendored
Normal file
3
testdata/bad_invalid_record_missing_value.yaml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
ttl: 60
|
||||||
1
testdata/bad_invalid_tag.yaml
vendored
Normal file
1
testdata/bad_invalid_tag.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!foo bar
|
||||||
1
testdata/bad_invalid_type.yaml
vendored
Normal file
1
testdata/bad_invalid_type.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
foobar
|
||||||
1
testdata/bad_invalid_yaml.yaml
vendored
Normal file
1
testdata/bad_invalid_yaml.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{
|
||||||
7
testdata/bad_missing_ns.yaml
vendored
Normal file
7
testdata/bad_missing_ns.yaml
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
"@":
|
||||||
|
- type: SOA
|
||||||
|
value: ns1.example.com. admin.example.com. 1 1 1 1 1
|
||||||
|
- type: A
|
||||||
|
value: 192.0.2.100
|
||||||
|
- type: AAAA
|
||||||
|
value: 2001:db8::100
|
||||||
5
testdata/bad_missing_soa.yaml
vendored
Normal file
5
testdata/bad_missing_soa.yaml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
value: 192.0.2.100
|
||||||
|
- type: AAAA
|
||||||
|
value: 2001:db8::100
|
||||||
8
testdata/bad_multiple_documents.yaml
vendored
Normal file
8
testdata/bad_multiple_documents.yaml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
- value: 1.2.3.4
|
||||||
|
---
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
- value: 4.3.2.1
|
||||||
4
testdata/bad_record_type_mismatch.yaml
vendored
Normal file
4
testdata/bad_record_type_mismatch.yaml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
value: 1.2.3.4
|
||||||
|
ttl: "not a number"
|
||||||
7
testdata/bad_recursion_1.yaml
vendored
Normal file
7
testdata/bad_recursion_1.yaml
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
com:
|
||||||
|
example:
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 192.0.2.1
|
||||||
|
cloud: !include bad_recursion_2.yaml
|
||||||
7
testdata/bad_recursion_2.yaml
vendored
Normal file
7
testdata/bad_recursion_2.yaml
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
foo:
|
||||||
|
bar:
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 192.0.2.1
|
||||||
|
cloud: !include bad_recursion_1.yaml
|
||||||
9
testdata/example.org.yaml
vendored
Normal file
9
testdata/example.org.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"@":
|
||||||
|
- type: SOA
|
||||||
|
value: ns1.example.com. admin.example.com. 1 1 1 1 1
|
||||||
|
- type: A
|
||||||
|
value: 192.0.2.100
|
||||||
|
- type: AAAA
|
||||||
|
value: 2001:db8::100
|
||||||
|
- type: NS
|
||||||
|
value: ns1.example.com
|
||||||
4
testdata/nested/multilayer/zone.yaml
vendored
Normal file
4
testdata/nested/multilayer/zone.yaml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 192.0.2.1
|
||||||
2
testdata/nested/zone.yaml
vendored
Normal file
2
testdata/nested/zone.yaml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
nested:
|
||||||
|
multilayer: !include multilayer/zone.yaml
|
||||||
64
testdata/zones.yaml
vendored
Normal file
64
testdata/zones.yaml
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Comment
|
||||||
|
com: # Comment
|
||||||
|
example:
|
||||||
|
# Comment
|
||||||
|
"@":
|
||||||
|
- type: SOA
|
||||||
|
value: ns1.example.com. admin.example.com. 1 1 1 1 1
|
||||||
|
- type: A
|
||||||
|
value: 192.0.2.1
|
||||||
|
- type: AAAA
|
||||||
|
value: 2001:db8::1
|
||||||
|
- type: MX
|
||||||
|
ttl: 3600
|
||||||
|
value: 10 mail.example.com
|
||||||
|
- type: TXT
|
||||||
|
ttl: 300
|
||||||
|
value: "v=spf1 a mx include:mail.example.com ~all"
|
||||||
|
- type: CAA
|
||||||
|
ttl: 86400
|
||||||
|
value: 0 issue "letsencrypt.org"
|
||||||
|
- type: TXT
|
||||||
|
ttl: 3600
|
||||||
|
value: "foo=bar"
|
||||||
|
- type: NS
|
||||||
|
value: ns1.example.com
|
||||||
|
www:
|
||||||
|
- type: CNAME
|
||||||
|
ttl: 3600
|
||||||
|
value: example.com
|
||||||
|
mail:
|
||||||
|
- type: A
|
||||||
|
value: 192.0.2.2
|
||||||
|
status:
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 198.51.100.24
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 203.0.113.24
|
||||||
|
unused: []
|
||||||
|
_tcp:
|
||||||
|
_xmpp-server:
|
||||||
|
- type: SRV
|
||||||
|
ttl: 3600
|
||||||
|
value: 10 0 5269 example.com
|
||||||
|
internal:
|
||||||
|
"@":
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 10.0.0.1
|
||||||
|
ftp:
|
||||||
|
- type: A
|
||||||
|
ttl: 3600
|
||||||
|
value: 10.0.0.2
|
||||||
|
partner:
|
||||||
|
- type: NS
|
||||||
|
ttl: 3600
|
||||||
|
value: ns1.example.org
|
||||||
|
- type: NS
|
||||||
|
ttl: 3600
|
||||||
|
value: ns2.example.org
|
||||||
|
folders: !include nested/zone.yaml
|
||||||
|
org:
|
||||||
|
example: !include example.org.yaml
|
||||||
99
yamlplugin.go
Normal file
99
yamlplugin.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package yamlzone
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/coredns/caddy"
|
||||||
|
"github.com/coredns/coredns/core/dnsserver"
|
||||||
|
"github.com/coredns/coredns/plugin"
|
||||||
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||||
|
"github.com/coredns/coredns/request"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YamlPlugin struct {
|
||||||
|
Next plugin.Handler
|
||||||
|
Zone *Zone
|
||||||
|
Config YamlPluginConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type YamlPluginConfig struct {
|
||||||
|
DefaultTtl uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = clog.NewWithPlugin("yaml")
|
||||||
|
|
||||||
|
func (y YamlPlugin) lookupRRs(qname string, qtype string) ([]dns.RR, bool) {
|
||||||
|
records, ok := y.Zone.LookupType(qname, qtype)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
rrs := []dns.RR{}
|
||||||
|
for _, record := range records {
|
||||||
|
ttl := record.Ttl
|
||||||
|
if ttl == 0 {
|
||||||
|
ttl = y.Config.DefaultTtl
|
||||||
|
}
|
||||||
|
rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s", qname, ttl, record.Type, record.Value))
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
rrs = append(rrs, rr)
|
||||||
|
}
|
||||||
|
return rrs, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y YamlPlugin) Name() string { return "yaml" }
|
||||||
|
|
||||||
|
func (y YamlPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||||
|
state := request.Request{W: w, Req: r}
|
||||||
|
qname := state.Name()
|
||||||
|
qtype := state.Type()
|
||||||
|
|
||||||
|
reply := new(dns.Msg)
|
||||||
|
reply.SetReply(r)
|
||||||
|
reply.Authoritative = true
|
||||||
|
|
||||||
|
rrs, ok := y.lookupRRs(qname, qtype)
|
||||||
|
if !ok {
|
||||||
|
return dns.RcodeNameError, nil
|
||||||
|
}
|
||||||
|
reply.Answer = rrs
|
||||||
|
w.WriteMsg(reply)
|
||||||
|
return dns.RcodeSuccess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(c *caddy.Controller) error {
|
||||||
|
c.Next() // yaml
|
||||||
|
filename := "zones.yaml"
|
||||||
|
if c.NextArg() {
|
||||||
|
filename = c.Val()
|
||||||
|
}
|
||||||
|
defaultTtl := uint64(60)
|
||||||
|
if c.NextArg() {
|
||||||
|
var err error
|
||||||
|
defaultTtl, err = strconv.ParseUint(c.Val(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return plugin.Error("yaml", fmt.Errorf("failed to parse default TTL: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zone, err := LoadZone(filename)
|
||||||
|
if err != nil {
|
||||||
|
return plugin.Error("yaml", fmt.Errorf("failed to load zone from %s: %w", filename, err))
|
||||||
|
}
|
||||||
|
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||||
|
return &YamlPlugin{
|
||||||
|
Next: next,
|
||||||
|
Zone: zone,
|
||||||
|
Config: YamlPluginConfig{
|
||||||
|
DefaultTtl: defaultTtl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { plugin.Register("yaml", setup) }
|
||||||
294
yamlzone.go
Normal file
294
yamlzone.go
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
package yamlzone
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/goccy/go-yaml/ast"
|
||||||
|
"github.com/goccy/go-yaml/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
Type string
|
||||||
|
Ttl uint64
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Zone struct {
|
||||||
|
Subzones map[string]*Zone
|
||||||
|
Records []Record
|
||||||
|
GlueRecords []Record
|
||||||
|
IsDelegationPoint bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ctxDirectory = contextKey("directory")
|
||||||
|
ctxFiles = contextKey("files")
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadZone(filename string) (*Zone, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return LoadZoneBytes(data, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadZoneBytes(data []byte, filename string) (*Zone, error) {
|
||||||
|
// Check for multiple documents before unmarshaling
|
||||||
|
file, err := parser.ParseBytes(data, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(file.Docs) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected exactly one document, got %d", len(file.Docs))
|
||||||
|
}
|
||||||
|
|
||||||
|
z := &Zone{}
|
||||||
|
ctx := context.WithValue(context.TODO(), ctxDirectory, filepath.Dir(filename))
|
||||||
|
ctx = context.WithValue(ctx, ctxFiles, map[string]bool{filename: true})
|
||||||
|
err = yaml.UnmarshalContext(ctx, data, z)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := z.Validate(".", false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return z, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zone) Validate(name string, zoneApexPresent bool) error {
|
||||||
|
nameservers := map[string]bool{}
|
||||||
|
otherRecords := []Record{}
|
||||||
|
isZoneApex := false
|
||||||
|
for _, record := range z.Records {
|
||||||
|
switch record.Type {
|
||||||
|
case "SOA":
|
||||||
|
isZoneApex = true
|
||||||
|
case "NS":
|
||||||
|
nameservers[record.Value] = true
|
||||||
|
default:
|
||||||
|
otherRecords = append(otherRecords, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneApexPresent = zoneApexPresent || isZoneApex
|
||||||
|
z.IsDelegationPoint = !isZoneApex && len(nameservers) > 0
|
||||||
|
|
||||||
|
if !zoneApexPresent {
|
||||||
|
// Outside zone
|
||||||
|
if z.IsDelegationPoint {
|
||||||
|
return fmt.Errorf("%s: delegation point found outside zone", name)
|
||||||
|
} else if len(otherRecords) > 0 {
|
||||||
|
return fmt.Errorf("%s: records found outside zone: %v", name, otherRecords)
|
||||||
|
}
|
||||||
|
} else if isZoneApex {
|
||||||
|
// Zone apex
|
||||||
|
if len(nameservers) == 0 {
|
||||||
|
return fmt.Errorf("%s: zone apex missing NS records", name)
|
||||||
|
}
|
||||||
|
} else if len(nameservers) > 0 {
|
||||||
|
// Delegation point (does not fall through to subzone validation)
|
||||||
|
if len(otherRecords) > 0 {
|
||||||
|
return fmt.Errorf("%s: non-glue, non-NS records found at delegation point: %v", name, otherRecords)
|
||||||
|
}
|
||||||
|
for subname, subzone := range z.Subzones {
|
||||||
|
glueRecords, err := subzone.GetGlueRecords(concatName(name, subname), nameservers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.GlueRecords = append(z.GlueRecords, glueRecords...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Subzone validation
|
||||||
|
// Either we're outside a zone, at a zone apex, or at a non-delegated subzone
|
||||||
|
for subname, subzone := range z.Subzones {
|
||||||
|
if err := subzone.Validate(concatName(name, subname), zoneApexPresent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zone) GetGlueRecords(name string, nameservers map[string]bool) ([]Record, error) {
|
||||||
|
// If the domain is not a nameserver, it must have no records
|
||||||
|
if _, ok := nameservers[name]; !ok {
|
||||||
|
if len(z.Records) > 0 {
|
||||||
|
return nil, fmt.Errorf("%s: non-glue records found under delegation point: %v", name, z.Records)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Any records under a delegation point must be glue records
|
||||||
|
for _, record := range z.Records {
|
||||||
|
if !(record.Type == "A" || record.Type == "AAAA") {
|
||||||
|
return nil, fmt.Errorf("%s: non-glue record found under delegation point: %v", name, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subname, subzone := range z.Subzones {
|
||||||
|
if glueRecords, err := subzone.GetGlueRecords(concatName(name, subname), nameservers); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
z.GlueRecords = append(z.GlueRecords, glueRecords...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return z.GlueRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Record) UnmarshalYAML(ctx context.Context, data []byte) error {
|
||||||
|
type rawRecord struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Ttl uint64 `yaml:"ttl"`
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
var rr rawRecord
|
||||||
|
if err := yaml.UnmarshalContext(ctx, data, &rr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rr.Type == "" {
|
||||||
|
return fmt.Errorf("record: missing type")
|
||||||
|
}
|
||||||
|
if rr.Value == "" {
|
||||||
|
return fmt.Errorf("record: missing value")
|
||||||
|
}
|
||||||
|
*r = Record(rr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zone) UnmarshalYAML(ctx context.Context, data []byte) error {
|
||||||
|
// Store the current directory so recursive includes can be resolved
|
||||||
|
dir, ok := ctx.Value(ctxDirectory).(string)
|
||||||
|
if !ok {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the list of files that have been included in this chain to avoid
|
||||||
|
// infinite recursion
|
||||||
|
files, ok := ctx.Value(ctxFiles).(map[string]bool)
|
||||||
|
if !ok {
|
||||||
|
files = map[string]bool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the YAML data into an AST to check for !include tags and handle
|
||||||
|
// mapping vs sequence nodes
|
||||||
|
file, err := parser.ParseBytes(data, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
node := file.Docs[0].Body
|
||||||
|
|
||||||
|
switch node.Type() {
|
||||||
|
case ast.TagType:
|
||||||
|
tagNode := node.(*ast.TagNode)
|
||||||
|
if tagNode.Start.Value != "!include" {
|
||||||
|
return fmt.Errorf("encountered unexpected tag %s", tagNode.Start.Value)
|
||||||
|
}
|
||||||
|
includeFilenameNode, ok := tagNode.Value.(ast.ScalarNode)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("include: expected scalar")
|
||||||
|
}
|
||||||
|
includeFilename := includeFilenameNode.GetValue().(string)
|
||||||
|
qualifiedIncludeFilename := filepath.Join(dir, includeFilename)
|
||||||
|
includedData, err := os.ReadFile(qualifiedIncludeFilename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for multiple documents in included file before unmarshaling
|
||||||
|
includedFile, err := parser.ParseBytes(includedData, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(includedFile.Docs) != 1 {
|
||||||
|
return fmt.Errorf("%s: expected exactly one document, got %d", qualifiedIncludeFilename, len(includedFile.Docs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := files[qualifiedIncludeFilename]; ok {
|
||||||
|
return fmt.Errorf("infinite recursion detected in %s", qualifiedIncludeFilename)
|
||||||
|
}
|
||||||
|
files[qualifiedIncludeFilename] = true
|
||||||
|
subctx := context.WithValue(ctx, ctxFiles, files)
|
||||||
|
subctx = context.WithValue(subctx, ctxDirectory, filepath.Dir(qualifiedIncludeFilename))
|
||||||
|
err = yaml.UnmarshalContext(subctx, includedData, z)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case ast.SequenceType:
|
||||||
|
var records []Record
|
||||||
|
if err := yaml.UnmarshalContext(ctx, data, &records); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.Records = records
|
||||||
|
case ast.MappingType:
|
||||||
|
var subzones map[string]*Zone
|
||||||
|
if err := yaml.UnmarshalContext(ctx, data, &subzones); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := subzones["@"]; ok {
|
||||||
|
z.Records = subzones["@"].Records
|
||||||
|
delete(subzones, "@")
|
||||||
|
}
|
||||||
|
z.Subzones = subzones
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("expected a sequence, mapping, or !include tag")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a DNS name to the corresponding path in the zone tree
|
||||||
|
// "www.example.com" -> ["com", "example", "www"]
|
||||||
|
func nameToPath(name string) []string {
|
||||||
|
parts := strings.Split(name, ".")
|
||||||
|
path := []string{}
|
||||||
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
|
path = append(path, parts[i])
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zone) Lookup(name string) ([]Record, bool) {
|
||||||
|
path := nameToPath(name)
|
||||||
|
for _, label := range path {
|
||||||
|
// Support empty name and trailing dot
|
||||||
|
if label == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sz, ok := z.Subzones[label]; ok {
|
||||||
|
z = sz
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return z.Records, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zone) FilterRecords(records []Record, recordType string) []Record {
|
||||||
|
filtered := []Record{}
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Type == recordType {
|
||||||
|
filtered = append(filtered, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zone) LookupType(name string, recordType string) ([]Record, bool) {
|
||||||
|
records, ok := z.Lookup(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return z.FilterRecords(records, recordType), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func concatName(name string, subname string) string {
|
||||||
|
if name == "." {
|
||||||
|
return subname + "."
|
||||||
|
}
|
||||||
|
return subname + "." + name
|
||||||
|
}
|
||||||
344
yamlzone_test.go
Normal file
344
yamlzone_test.go
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
package yamlzone
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertOk(t *testing.T, ok bool) {
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected ok, got false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNotOk(t *testing.T, ok bool, records []Record) {
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("Expected not ok, got ok (%v)", records)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRecordCount(t *testing.T, records []Record, expected int) {
|
||||||
|
if len(records) != expected {
|
||||||
|
t.Fatalf("Expected %d records, got %d (%v)", expected, len(records), records)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRecordType(t *testing.T, record Record, expected string) {
|
||||||
|
if record.Type != expected {
|
||||||
|
t.Fatalf("Expected %s record, got %s", expected, record.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRecordTtl(t *testing.T, record Record, expected uint64) {
|
||||||
|
if record.Ttl != expected {
|
||||||
|
t.Fatalf("Expected TTL %d, got %d", expected, record.Ttl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRecordValue(t *testing.T, record Record, expected string) {
|
||||||
|
if record.Value != expected {
|
||||||
|
t.Fatalf("Expected %s, got %s", expected, record.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRecord(t *testing.T, record Record, expectedType string, expectedTtl uint64, expectedValue string) {
|
||||||
|
assertRecordType(t, record, expectedType)
|
||||||
|
assertRecordTtl(t, record, expectedTtl)
|
||||||
|
assertRecordValue(t, record, expectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyZone(t *testing.T) {
|
||||||
|
var zEmpty Zone
|
||||||
|
|
||||||
|
t.Run("Unmarshal", func(t *testing.T) {
|
||||||
|
err := yaml.Unmarshal([]byte("{}"), &zEmpty)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if t.Failed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Lookup empty string", func(t *testing.T) {
|
||||||
|
records, ok := zEmpty.Lookup("")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup single dot", func(t *testing.T) {
|
||||||
|
records, ok := zEmpty.Lookup(".")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup example.com", func(t *testing.T) {
|
||||||
|
records, ok := zEmpty.Lookup("example.com")
|
||||||
|
assertNotOk(t, ok, records)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LookupType example.com A", func(t *testing.T) {
|
||||||
|
records, ok := zEmpty.LookupType("example.com", "A")
|
||||||
|
assertNotOk(t, ok, records)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleZone(t *testing.T) {
|
||||||
|
const zSimpleFile = "testdata/example.org.yaml"
|
||||||
|
var zSimple *Zone
|
||||||
|
|
||||||
|
t.Run("Load", func(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
zSimple, err = LoadZone(zSimpleFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got \"%v\"", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if t.Failed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Lookup", func(t *testing.T) {
|
||||||
|
records, ok := zSimple.Lookup("")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 4)
|
||||||
|
assertRecord(t, records[0], "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1")
|
||||||
|
assertRecord(t, records[1], "A", 0, "192.0.2.100")
|
||||||
|
assertRecord(t, records[2], "AAAA", 0, "2001:db8::100")
|
||||||
|
assertRecord(t, records[3], "NS", 0, "ns1.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LookupType", func(t *testing.T) {
|
||||||
|
records, ok := zSimple.LookupType("", "A")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected ok, got false")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertRecordCount(t, records, 1)
|
||||||
|
assertRecord(t, records[0], "A", 0, "192.0.2.100")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFullZone(t *testing.T) {
|
||||||
|
const zFullFile = "testdata/zones.yaml"
|
||||||
|
var zFull *Zone
|
||||||
|
|
||||||
|
t.Run("Load", func(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
zFull, err = LoadZone(zFullFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got \"%v\"", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if t.Failed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Lookup example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("example.com")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected ok, got false")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertRecordCount(t, records, 8)
|
||||||
|
assertRecord(t, records[0], "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1")
|
||||||
|
assertRecord(t, records[1], "A", 0, "192.0.2.1")
|
||||||
|
assertRecord(t, records[2], "AAAA", 0, "2001:db8::1")
|
||||||
|
assertRecord(t, records[3], "MX", 3600, "10 mail.example.com") // Default TTL
|
||||||
|
assertRecord(t, records[4], "TXT", 300, "v=spf1 a mx include:mail.example.com ~all")
|
||||||
|
assertRecord(t, records[5], "CAA", 86400, "0 issue \"letsencrypt.org\"")
|
||||||
|
assertRecord(t, records[6], "TXT", 3600, "foo=bar")
|
||||||
|
assertRecord(t, records[7], "NS", 0, "ns1.example.com")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LookupType example.com TXT", func(t *testing.T) {
|
||||||
|
records, ok := zFull.LookupType("example.com", "TXT")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 2)
|
||||||
|
assertRecord(t, records[0], "TXT", 300, "v=spf1 a mx include:mail.example.com ~all")
|
||||||
|
assertRecord(t, records[1], "TXT", 3600, "foo=bar")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup www.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("www.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 1)
|
||||||
|
assertRecord(t, records[0], "CNAME", 3600, "example.com")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LookupType www.example.com CNAME", func(t *testing.T) {
|
||||||
|
records, ok := zFull.LookupType("www.example.com", "CNAME")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 1)
|
||||||
|
assertRecord(t, records[0], "CNAME", 3600, "example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup www.example.com TXT", func(t *testing.T) {
|
||||||
|
records, ok := zFull.LookupType("www.example.com", "TXT")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup status.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("status.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 2)
|
||||||
|
assertRecord(t, records[0], "A", 3600, "198.51.100.24")
|
||||||
|
assertRecord(t, records[1], "A", 3600, "203.0.113.24")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup partner.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("partner.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 2)
|
||||||
|
assertRecord(t, records[0], "NS", 3600, "ns1.example.org")
|
||||||
|
assertRecord(t, records[1], "NS", 3600, "ns2.example.org")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup unused.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("unused.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup ftp.internal.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("ftp.internal.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 1)
|
||||||
|
assertRecord(t, records[0], "A", 3600, "10.0.0.2")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup _xmpp-server._tcp.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("_xmpp-server._tcp.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 1)
|
||||||
|
assertRecord(t, records[0], "SRV", 3600, "10 0 5269 example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Lookup multilayer.nested.folders.example.com", func(t *testing.T) {
|
||||||
|
records, ok := zFull.Lookup("multilayer.nested.folders.example.com")
|
||||||
|
assertOk(t, ok)
|
||||||
|
assertRecordCount(t, records, 1)
|
||||||
|
assertRecord(t, records[0], "A", 3600, "192.0.2.1")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadZones(t *testing.T) {
|
||||||
|
type badZone struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
errorSubstring string
|
||||||
|
}
|
||||||
|
var badZones = []badZone{
|
||||||
|
{
|
||||||
|
name: "NonexistentFile",
|
||||||
|
filename: "testdata/bad_nonexistent.yaml",
|
||||||
|
errorSubstring: "open testdata/bad_nonexistent.yaml: no such file or directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory",
|
||||||
|
filename: "testdata/bad_directory.yaml",
|
||||||
|
errorSubstring: "read testdata/bad_directory.yaml: is a directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidYaml",
|
||||||
|
filename: "testdata/bad_invalid_yaml.yaml",
|
||||||
|
errorSubstring: "[1:1]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidTag",
|
||||||
|
filename: "testdata/bad_invalid_tag.yaml",
|
||||||
|
errorSubstring: "encountered unexpected tag !foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidType",
|
||||||
|
filename: "testdata/bad_invalid_type.yaml",
|
||||||
|
errorSubstring: "expected a sequence, mapping, or !include tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleDocuments",
|
||||||
|
filename: "testdata/bad_multiple_documents.yaml",
|
||||||
|
errorSubstring: "expected exactly one document, got 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidRecordMissingType",
|
||||||
|
filename: "testdata/bad_invalid_record_missing_type.yaml",
|
||||||
|
errorSubstring: "record: missing type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidRecordMissingValue",
|
||||||
|
filename: "testdata/bad_invalid_record_missing_value.yaml",
|
||||||
|
errorSubstring: "record: missing value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InfiniteRecursion",
|
||||||
|
filename: "testdata/bad_recursion_1.yaml",
|
||||||
|
errorSubstring: "infinite recursion detected in testdata/bad_recursion_1.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IncludeTagValueNotScalar",
|
||||||
|
filename: "testdata/bad_include_tag_value_not_scalar.yaml",
|
||||||
|
errorSubstring: "include: expected scalar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RecordTypeMismatch",
|
||||||
|
filename: "testdata/bad_record_type_mismatch.yaml",
|
||||||
|
errorSubstring: "cannot unmarshal string into Go struct field",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingSoa",
|
||||||
|
filename: "testdata/bad_missing_soa.yaml",
|
||||||
|
errorSubstring: "records found outside zone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingNs",
|
||||||
|
filename: "testdata/bad_missing_ns.yaml",
|
||||||
|
errorSubstring: "zone apex missing NS records",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, badZone := range badZones {
|
||||||
|
t.Run(badZone.name, func(t *testing.T) {
|
||||||
|
z, err := LoadZone(badZone.filename)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error, got nil (%v)", z)
|
||||||
|
} else if !strings.Contains(err.Error(), badZone.errorSubstring) {
|
||||||
|
t.Errorf("Expected error \"%s\", got \"%s\"", badZone.errorSubstring, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Include"+badZone.name, func(t *testing.T) {
|
||||||
|
z, err := LoadZoneBytes([]byte("!include "+badZone.filename), ".")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error, got nil (%v)", z)
|
||||||
|
} else if !strings.Contains(err.Error(), badZone.errorSubstring) {
|
||||||
|
t.Errorf("Expected error \"%s\", got \"%s\"", badZone.errorSubstring, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test exists solely to reach 100% test coverage. It triggers an error
|
||||||
|
// condition that should never normally be possible, but is guarded against for
|
||||||
|
// completeness.
|
||||||
|
func TestDirectUnmarshalWithInvalidBytes(t *testing.T) {
|
||||||
|
// Directly call Zone.UnmarshalYAML with invalid bytes to trigger
|
||||||
|
// yamlzone.go:103 (error in parser.ParseBytes)
|
||||||
|
z := &Zone{}
|
||||||
|
invalidBytes := []byte("[invalid yaml")
|
||||||
|
|
||||||
|
err := z.UnmarshalYAML(context.TODO(), invalidBytes)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "[1:") {
|
||||||
|
t.Errorf("Expected parser error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue