Fix build and polish codebase to OSS quality
Synthetic CoreDNS Plugin CI/CD Build / test (push) Has been cancelled

Dockerfile used golang:1.21 but go.mod requires 1.25; CI was broken.
Refactored plugin code with proper godoc comments, extracted helpers to
eliminate duplicated response-building logic, modernized tests with
t.Run subtests and shared assertion helpers, and rewrote README with
configuration reference table and professional structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-05 13:33:25 -05:00
parent d9e08599cd
commit ff3ec65719
5 changed files with 515 additions and 504 deletions
+4 -3
View File
@@ -1,8 +1,9 @@
# run go test inside a docker container for consistency as acceptance testing # Dockerfile for CI: runs vet and tests inside a consistent Go environment.
FROM golang:1.21 FROM golang:1.25
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go test -v RUN go vet ./...
RUN go test -v ./...
+74 -50
View File
@@ -1,88 +1,112 @@
# synthetic # synthetic
----- A [CoreDNS](https://coredns.io) plugin that generates DNS records from IP addresses embedded in hostnames.
## Overview Inspired by dnsmasq's [`synth-domain`](http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) option, `synthetic` provides automatic alignment between forward and reverse lookups — eliminating a common class of DNS misconfigurations described in [RFC 1912 Section 2.1](https://tools.ietf.org/html/rfc1912#section-2.1).
*synthetic* is a [CoreDNS](http://coredns.io) plugin to synthetically handle DNS records with IP addresses embedded. ## Features
Named after DNSMASQ's "synth-domain" [option](http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html).
`synthetic` aims to provide an easy mechanism for alignment between forward and reverse lookups. - **Forward lookups** (A/AAAA) — extract an IP from a prefixed hostname and respond directly
This is a common DNS operational and configuration error as noted in [RFC1912](https://tools.ietf.org/html/rfc1912#section-2.1). - **Reverse lookups** (PTR) — generate a synthetic hostname from a reverse query
- **Static record priority** — responses from upstream plugins (e.g., `file`) always take precedence over synthetic records
- **Dual-stack** — full IPv4 and IPv6 support, including compressed IPv6 notation
- **Configurable** — custom prefix, TTL, and per-network scoping
This plugin supports works nicely with the file plugin such that records present in the file will take precedence over this plugin. ## How It Works
### Forward Lookups ### Forward Lookups (A / AAAA)
Forward Lookups are hostname -> IP address. A hostname with an embedded IP address is resolved directly. Dots (IPv4) or colons (IPv6) are replaced with dashes in the hostname label:
`synthetic` supports IPs "embedded" in the DNS hostname.
For IP addresses embedded in DNS hostnames the general model is `ip-<address>.example.com`
(where "address" can be either IPv6 or IPv4, and "example.com" is a domain of your choosing).
In IPv6 the colons are converted to a dash; in IPv4 the dots are converted to a dash.
The following are all considered valid for AAAA or A queries. | Hostname | Record | Address |
|---|---|---|
| `ip-192-0-2-1.example.com` | A | `192.0.2.1` |
| `ip-2001-db8-abcd--1.example.com` | AAAA | `2001:db8:abcd::1` |
| `ip-2001-0db8-0000-0000-0000-0000-0000-0001.example.com` | AAAA | `2001:db8::1` |
* `ip-2001-0db8-0000-0000-0000-0000-0000-0001.example.com` Only addresses within a configured `net` CIDR are resolved. All other queries pass through to the next plugin.
* `ip-2001-db8--1.example.com`
* `ip-192-0-2-0.example.com`
### Reverse Lookups ### Reverse Lookups (PTR)
Reverse Lookups are IP -> hostname, and are known as pointer records (PTR). When a PTR query arrives, the plugin first consults the next plugin in the chain. If that plugin provides a successful answer (e.g., from a zone file), it is used as-is. Otherwise, a synthetic PTR record is generated pointing back to the corresponding forward hostname.
`synthetic` will respond to a PTR query and return a result that is also supported by the forward lookup mechanism.
Reverse lookups for IPv6 addresses will return a fully compressed IPv6 address (per [RFC5952](https://tools.ietf.org/html/rfc5952#section-2.2)).
## Corefile Configuration Examples IPv6 reverse responses use the compressed address form per [RFC 5952](https://tools.ietf.org/html/rfc5952#section-2.2).
Reverse Lookup Example ## Configuration
### Directives
| Directive | Description | Default |
|---|---|---|
| `net` | CIDR network(s) for which synthetic forward responses are generated. May be specified multiple times. | *(none — required for forward lookups)* |
| `forward` | Domain name appended to synthetic hostnames in PTR responses. | *(none — required for reverse lookups)* |
| `ttl` | Time-to-live for synthetic responses, in seconds. | `0` |
| `prefix` | Hostname label prefix identifying synthetic queries. A trailing dash is added automatically. | `ip` |
### Examples
**Forward lookups** for two IPv6 prefixes and one IPv4 prefix:
``` ```
2001:db8:abcd::/48 { example.com {
synthetic {
net 2001:db8:abcd::/48
net 2001:db8:1234::/48
net 192.0.2.0/24
prefix ip
ttl 300
}
file db.example.com
}
```
**Reverse lookups** for an IPv6 prefix, with zone file records taking priority:
```
d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa {
synthetic { synthetic {
forward example.com forward example.com
prefix ip
} }
file d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa file d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa
} }
``` ```
Forward Lookup Example
```
example.com {
synthetic {
net 2001:db8:abcd::/64
net 2001:db8:1234::/64
}
file db.example.com
```
## Compiling into CoreDNS ## Compiling into CoreDNS
To compile this with CoreDNS you can follow the [normal procedure](https://coredns.io/manual/plugins/#plugins) for external plugins. Follow the standard [external plugin procedure](https://coredns.io/manual/plugins/#plugins). Add the following line to `plugin.cfg` in the CoreDNS source tree:
This plugin can be used by adding the following to `plugin.cfg`:
``` ```
synthetic:code.fritzlab.net/dns/synthetic synthetic:code.fritzlab.net/dns/synthetic
``` ```
Then rebuild CoreDNS:
```sh
go generate
go build
```
## FAQ ## FAQ
### Why not use templates? ### Why not use the `template` plugin?
1- It appears that the `template` plugin is the recommended pattern for providing the resolution pattern we're after here. Two reasons:
However, it's not possible to have the `file` plugin provide the primary source of data and use a `template` at the same time.
See [this](https://github.com/coredns/coredns/issues/2977#issuecomment-555938144) GitHub comment.
Thus, it's not possible to have a PTR response from a file take priority over a template.
2- Using regex in a template for IPv4 and IPv6 addresses is very challanging with CIDR notation. 1. The `template` plugin cannot coexist with `file` for the same zone in a way that lets file records take priority. See this [upstream discussion](https://github.com/coredns/coredns/issues/2977#issuecomment-555938144). The `synthetic` plugin is designed to work alongside `file`, deferring to it for static records.
This plugin provides an easier experience by just providing an IP prefix in CIDR notation.
2. Writing regex patterns to match arbitrary IPv4 and IPv6 addresses within CIDR ranges is impractical. This plugin accepts CIDR notation directly.
## Development ## Development
Standard Go development practices apply. ```sh
# Run tests
go test -v ./...
### Tests # Lint
go vet ./...
```
``` ## License
$ go test -v
``` [Apache License 2.0](LICENSE)
+75 -60
View File
@@ -2,82 +2,42 @@ package synthetic
import ( import (
"fmt" "fmt"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"net" "net"
"strconv" "strconv"
"strings" "strings"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
) )
// syntheticConfig holds the configuration options for the synthetic plugin. // syntheticConfig holds the parsed Corefile configuration for the synthetic plugin.
type syntheticConfig struct { type syntheticConfig struct {
// net is the set of IP networks for which synthetic forward responses are
// generated. Only addresses falling within these CIDRs will be resolved.
net []*net.IPNet net []*net.IPNet
// forward is the DNS zone appended to synthetic hostnames when generating
// PTR records (e.g., "example.com").
forward string forward string
// ttl is the time-to-live value applied to all synthetic responses.
// Defaults to 0 (no caching).
ttl uint32 ttl uint32
// prefix is the hostname label prefix used to identify synthetic queries
// (e.g., "ip-"). A trailing dash is appended automatically if absent.
prefix string prefix string
} }
// init registers this plugin.
func init() { plugin.Register("synthetic", setup) } func init() { plugin.Register("synthetic", setup) }
// setup is the function that gets called when the config parser see the token "synthetic". // setup parses the Corefile configuration block for the synthetic plugin and
// registers it in the CoreDNS handler chain.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
var config syntheticConfig config, err := parseConfig(c)
// Parse the configuration file
for c.Next() {
for c.NextBlock() {
switch v := c.Val(); v {
// Configuration for forward lookup zones for which to do resolution
case "net":
args := c.RemainingArgs()
for _, arg := range args {
_, cidr, err := net.ParseCIDR(arg)
if err == nil {
config.net = append(config.net, cidr)
} else {
return fmt.Errorf("synthetic: invalid reverse lookup cidr: %v", arg)
}
}
// Configuration for reverse lookup zones for the forward lookup zone name
case "forward":
args := c.RemainingArgs()
for _, arg := range args {
config.forward = arg
}
// Configuration for the TTL value
case "ttl":
args := c.RemainingArgs()
for _, arg := range args {
ttl64, err := strconv.ParseUint(arg, 10, 32)
if err != nil { if err != nil {
return fmt.Errorf("synthetic: invalid ttl value: %v", arg) return err
}
config.ttl = uint32(ttl64)
}
// configuration for the prefix value (defaults to `ip`)
case "prefix":
args := c.RemainingArgs()
for _, arg := range args {
config.prefix = arg
}
default:
return c.Errf("unknown property '%s'", v)
}
}
}
if config.prefix == "" {
config.prefix = "ip"
}
if !strings.HasSuffix(config.prefix, "-") {
config.prefix = config.prefix + "-"
} }
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
@@ -86,3 +46,58 @@ func setup(c *caddy.Controller) error {
return nil return nil
} }
// parseConfig reads the synthetic { ... } block from the Corefile and returns
// a validated syntheticConfig.
func parseConfig(c *caddy.Controller) (syntheticConfig, error) {
var config syntheticConfig
for c.Next() {
for c.NextBlock() {
switch v := c.Val(); v {
case "net":
for _, arg := range c.RemainingArgs() {
_, cidr, err := net.ParseCIDR(arg)
if err != nil {
return config, fmt.Errorf("synthetic: invalid CIDR notation: %v", arg)
}
config.net = append(config.net, cidr)
}
case "forward":
args := c.RemainingArgs()
if len(args) > 0 {
config.forward = args[0]
}
case "ttl":
for _, arg := range c.RemainingArgs() {
ttl64, err := strconv.ParseUint(arg, 10, 32)
if err != nil {
return config, fmt.Errorf("synthetic: invalid ttl value: %v", arg)
}
config.ttl = uint32(ttl64)
}
case "prefix":
args := c.RemainingArgs()
if len(args) > 0 {
config.prefix = args[0]
}
default:
return config, c.Errf("unknown property '%s'", v)
}
}
}
// Apply defaults.
if config.prefix == "" {
config.prefix = "ip"
}
if !strings.HasSuffix(config.prefix, "-") {
config.prefix += "-"
}
return config, nil
}
+166 -112
View File
@@ -1,183 +1,239 @@
// Package synthetic implements a CoreDNS plugin that generates DNS records
// from IP addresses embedded in hostnames, inspired by dnsmasq's "synth-domain"
// option. It provides automatic alignment between forward (A/AAAA) and reverse
// (PTR) lookups, eliminating a common class of DNS misconfigurations described
// in RFC 1912 Section 2.1.
//
// Forward lookups extract an IP address from a prefixed hostname
// (e.g., ip-192-0-2-1.example.com -> 192.0.2.1) and respond only if the
// address falls within a configured network. Reverse lookups generate a
// synthetic PTR record, but defer to the next plugin in the chain when it
// returns a successful answer, allowing static zone entries to take precedence.
package synthetic package synthetic
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"net"
"strings"
"github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/dnstest"
clog "github.com/coredns/coredns/plugin/pkg/log" clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
"net"
"strings"
) )
var log = clog.NewWithPlugin("synthetic") var log = clog.NewWithPlugin("synthetic")
func (s synthetic) Name() string { return "synthetic" } // synthetic is a CoreDNS plugin that resolves DNS queries for hostnames
// containing embedded IP addresses.
type synthetic struct { type synthetic struct {
Next plugin.Handler Next plugin.Handler
Config syntheticConfig Config syntheticConfig
} }
func (s synthetic) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // Name returns the name of the plugin as registered with CoreDNS.
func (s synthetic) Name() string { return "synthetic" }
// Create a new state for this request. This is used to store state and allows us to pass this // ServeDNS implements the plugin.Handler interface. It processes A, AAAA, and
// PTR queries, generating synthetic responses for IP-embedded hostnames while
// deferring all other query types to the next plugin in the chain.
func (s synthetic) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}
log.Debug("Received request for ", state.QName(), " of type ", state.QType()) log.Debug("Received request for ", state.QName(), " of type ", state.QType())
// // Forward lookups (A/AAAA): respond directly when the hostname embeds
// FOR FORWARD LOOKUPS // a valid IP within a configured network. No chain continuation needed.
// we always respond directly (no need to continue the plugin chain)
// need valid networks to check against
//
if state.QType() == dns.TypeA || state.QType() == dns.TypeAAAA { if state.QType() == dns.TypeA || state.QType() == dns.TypeAAAA {
if strings.HasPrefix(state.Name(), s.Config.prefix) { if rcode, handled := s.handleForward(state, w, r); handled {
return rcode, nil
}
}
// For all other query types (including PTR), invoke the next plugin first.
// A successful response from the chain takes priority, ensuring that
// static zone entries always win over synthetic records.
rec := dnstest.NewRecorder(&test.ResponseWriter{})
rc, err := plugin.NextOrFailure(s.Name(), s.Next, ctx, rec, r)
if rc == dns.RcodeSuccess && len(rec.Msg.Answer) > 0 {
log.Debug("Next plugin returned authoritative answer; skipping synthetic response")
w.WriteMsg(rec.Msg)
return rc, err
}
// Reverse lookups (PTR): generate a synthetic hostname from the queried
// address when the chain did not provide a definitive answer.
if state.QType() == dns.TypePTR {
if rcode, handled := s.handleReverse(state, w, r); handled {
return rcode, nil
}
}
// Fall back to whatever the chain produced.
log.Debug("Synthetic plugin not applicable for ", state.QName(), " of type ", state.QType())
w.WriteMsg(rec.Msg)
return rc, err
}
// handleForward attempts to resolve an A or AAAA query from an IP-embedded
// hostname. It returns the DNS rcode and true if the request was handled, or
// (0, false) if the request should fall through to the plugin chain.
func (s synthetic) handleForward(state request.Request, w dns.ResponseWriter, r *dns.Msg) (int, bool) {
if !strings.HasPrefix(state.Name(), s.Config.prefix) {
return 0, false
}
log.Debug("Possible synthetic response for: ", state.QName()) log.Debug("Possible synthetic response for: ", state.QName())
// pull out the ip address // Extract the IP address from the hostname label. IPv4 separators are
ipStr := strings.TrimPrefix(strings.Split(state.Name(), ".")[0], s.Config.prefix) // dots converted to dashes; IPv6 separators are colons converted to dashes.
label := strings.Split(state.Name(), ".")[0]
ipStr := strings.TrimPrefix(label, s.Config.prefix)
ip := net.ParseIP(strings.ReplaceAll(ipStr, "-", ".")) ip := net.ParseIP(strings.ReplaceAll(ipStr, "-", "."))
if ip == nil { if ip == nil {
ip = net.ParseIP(strings.ReplaceAll(ipStr, "-", ":")) ip = net.ParseIP(strings.ReplaceAll(ipStr, "-", ":"))
} }
if ip == nil {
log.Debug("Invalid IP from hostname: ", state.QName())
return 0, false
}
// respond according to the IP type and the request type
if ip != nil {
log.Debug("Valid IP from hostname: ", ip) log.Debug("Valid IP from hostname: ", ip)
// check if ip is within the synthetic network // Verify the parsed IP falls within at least one configured network.
var found bool if !s.containsIP(ip) {
for _, n := range s.Config.net { log.Debug("IP not in a configured network: ", ip)
if n.Contains(ip) { return 0, false
found = true
break
} }
}
// don't continue if the IP is not in the synthetic network (fall back to recorded response)
if found {
log.Debug("IP ", ip, " is in synthetic network") log.Debug("IP ", ip, " is in synthetic network")
if ip.To4() == nil && state.QType() == dns.TypeAAAA { // Build a response appropriate to the query type and address family.
log.Debug("Responding to AAAA request for ", state.QName()) isV4 := ip.To4() != nil
m := new(dns.Msg) switch {
m.SetReply(r) case !isV4 && state.QType() == dns.TypeAAAA:
hdr := dns.RR_Header{Name: state.QName(), Rrtype: state.QType(), Class: state.QClass(), Ttl: s.Config.ttl} return s.writeAnswer(state, w, r, &dns.AAAA{
m.Answer = append(m.Answer, &dns.AAAA{Hdr: hdr, AAAA: ip.To16()}) Hdr: s.rrHeader(state),
w.WriteMsg(m) AAAA: ip.To16(),
return dns.RcodeSuccess, nil }), true
}
if ip.To4() != nil && state.QType() == dns.TypeA { case isV4 && state.QType() == dns.TypeA:
log.Debug("Responding to A request for ", state.QName()) return s.writeAnswer(state, w, r, &dns.A{
m := new(dns.Msg) Hdr: s.rrHeader(state),
m.SetReply(r) A: ip.To4(),
hdr := dns.RR_Header{Name: state.QName(), Rrtype: state.QType(), Class: state.QClass(), Ttl: s.Config.ttl} }), true
m.Answer = append(m.Answer, &dns.A{Hdr: hdr, A: ip.To4()})
w.WriteMsg(m) default:
return dns.RcodeSuccess, nil // Type mismatch (e.g., AAAA query for an IPv4 address). Return an
} // empty success response to prevent further lookups.
if ip.To4() == nil && state.QType() == dns.TypeA { log.Debug("Type mismatch for ", state.QName(), "; returning empty answer")
log.Debug("Responding to A request for ", state.QName(), " with empty answer") return s.writeEmpty(w, r), true
m := new(dns.Msg)
m.SetReply(r)
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
if ip.To4() != nil && state.QType() == dns.TypeAAAA {
log.Debug("Responding to AAAA request for ", state.QName(), " with empty answer")
m := new(dns.Msg)
m.SetReply(r)
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
log.Debug("Unexpected code path for: ", state.QName())
}
log.Debug("IP not in a valid network: ", ip)
}
log.Debug("Invalid IP from hostname: ", state.QName())
} }
} }
// // handleReverse attempts to resolve a PTR query by constructing a synthetic
// FOR REVERSE LOOKUPS // forward hostname from the queried reverse address. It returns the DNS rcode
// must check next plugin in chain to see if it's a success (static reverse lookups override synthetic) // and true if the request was handled.
// no need to check valid networks (guaranteed to be in the synthetic network based on the coredns config) func (s synthetic) handleReverse(state request.Request, w dns.ResponseWriter, r *dns.Msg) (int, bool) {
// log.Debug("Attempting synthetic reverse response for: ", state.QName())
// Continue to the next plugin in the chain, and record the response. ip := inArpaToIP(state.QName())
rec := dnstest.NewRecorder(&test.ResponseWriter{}) if ip == nil {
rc, err := plugin.NextOrFailure(s.Name(), s.Next, ctx, rec, r) return 0, false
// If the next plugin in the chain's recorded response is success, we go with that.
if rc == dns.RcodeSuccess && len(rec.Msg.Answer) > 0 {
log.Debug("Next Plugin's answers are acceptable. no synthetic response")
w.WriteMsg(rec.Msg)
return rc, err
} }
if state.QType() == dns.TypePTR {
log.Debug("Attempting to inject synthetic response for reverse lookup: ", state.QName())
ip := inArpaToIp(state.QName())
log.Debug("Parsed IP: ", ip)
if ip != nil {
forward := ipToDomainName(s.Config.prefix, ip, s.Config.forward) forward := ipToDomainName(s.Config.prefix, ip, s.Config.forward)
log.Debug("Responding to PTR request for ", state.QName(), " with ", forward) log.Debug("Responding to PTR request for ", state.QName(), " with ", forward)
return s.writeAnswer(state, w, r, &dns.PTR{
Hdr: s.rrHeader(state),
Ptr: forward,
}), true
}
// containsIP reports whether ip falls within any of the configured networks.
func (s synthetic) containsIP(ip net.IP) bool {
for _, n := range s.Config.net {
if n.Contains(ip) {
return true
}
}
return false
}
// rrHeader constructs a dns.RR_Header from the current request state and the
// plugin's configured TTL.
func (s synthetic) rrHeader(state request.Request) dns.RR_Header {
return dns.RR_Header{
Name: state.QName(),
Rrtype: state.QType(),
Class: state.QClass(),
Ttl: s.Config.ttl,
}
}
// writeAnswer writes a single-record DNS response to the client.
func (s synthetic) writeAnswer(state request.Request, w dns.ResponseWriter, r *dns.Msg, rr dns.RR) int {
m := new(dns.Msg) m := new(dns.Msg)
m.SetReply(r) m.SetReply(r)
hdr := dns.RR_Header{Name: state.QName(), Rrtype: state.QType(), Class: state.QClass(), Ttl: s.Config.ttl} m.Answer = append(m.Answer, rr)
m.Answer = append(m.Answer, &dns.PTR{Hdr: hdr, Ptr: forward})
w.WriteMsg(m) w.WriteMsg(m)
return dns.RcodeSuccess, nil return dns.RcodeSuccess
}
} }
// if we got here, we couldn't handle the request (fall back to recorded response) // writeEmpty writes an empty success response (NOERROR with no answer section).
log.Debug("synthetic plugin not needed for ", state.QName(), " of type ", state.QType()) func (s synthetic) writeEmpty(w dns.ResponseWriter, r *dns.Msg) int {
w.WriteMsg(rec.Msg) m := new(dns.Msg)
return rc, err m.SetReply(r)
w.WriteMsg(m)
return dns.RcodeSuccess
} }
func inArpaToIp(name string) net.IP { // inArpaToIP converts a reverse DNS name (in-addr.arpa or ip6.arpa) to the
ipv4Suffix := ".in-addr.arpa." // corresponding net.IP. It returns nil if the name cannot be parsed.
ipv6Suffix := ".ip6.arpa." func inArpaToIP(name string) net.IP {
const (
ipv4Suffix = ".in-addr.arpa."
ipv6Suffix = ".ip6.arpa."
)
// IPv4 reverse: four dot-separated octets in reverse order.
if idx := strings.Index(name, ipv4Suffix); idx > 6 { if idx := strings.Index(name, ipv4Suffix); idx > 6 {
name = name[:idx] parts := strings.Split(name[:idx], ".")
parts := strings.Split(name, ".")
if len(parts) != 4 { if len(parts) != 4 {
return nil return nil
} }
reversed := parts[3] + "." + parts[2] + "." + parts[1] + "." + parts[0]
name = parts[3] + "." + parts[2] + "." + parts[1] + "." + parts[0] return net.ParseIP(reversed)
return net.ParseIP(name)
} }
// IPv6 reverse: exactly 73 characters — 32 hex nibbles separated by dots
// in reverse order, followed by ".ip6.arpa.".
if len(name) == 73 && name[63:] == ipv6Suffix { if len(name) == 73 && name[63:] == ipv6Suffix {
// we can rely on the fact that v6 reverse hostnames have a fixed length nibbles := make([]byte, 32)
// read the characters from the hostname into a buffer in reverse
v6chars := make([]byte, 32)
for i, j := 62, 0; i >= 0; i -= 2 { for i, j := 62, 0; i >= 0; i -= 2 {
v6chars[j] = name[i] nibbles[j] = name[i]
j++ j++
} }
// decode the characters in the buffer into 16 bytes and return it addr := make([]byte, 16)
v6bytes := make([]byte, 16) if _, err := hex.Decode(addr, nibbles); err != nil {
if _, err := hex.Decode(v6bytes, v6chars); err != nil {
return nil return nil
} }
return net.IP(addr)
return v6bytes
} }
return nil return nil
} }
// ipToDomainName converts an IP address to a synthetic forward hostname by
// joining the address octets/groups with dashes and appending the zone suffix.
// For example, 192.0.2.1 with prefix "ip-" and zone "example.com" becomes
// "ip-192-0-2-1.example.com.".
func ipToDomainName(prefix string, ip net.IP, zone string) string { func ipToDomainName(prefix string, ip net.IP, zone string) string {
if ip == nil { if ip == nil {
return "" return ""
@@ -186,7 +242,6 @@ func ipToDomainName(prefix string, ip net.IP, zone string) string {
if !strings.HasPrefix(zone, ".") { if !strings.HasPrefix(zone, ".") {
zone = "." + zone zone = "." + zone
} }
if !strings.HasSuffix(zone, ".") { if !strings.HasSuffix(zone, ".") {
zone = zone + "." zone = zone + "."
} }
@@ -196,6 +251,5 @@ func ipToDomainName(prefix string, ip net.IP, zone string) string {
sep = "." sep = "."
} }
response := strings.Join(strings.Split(ip.String(), sep), "-") return prefix + strings.Join(strings.Split(ip.String(), sep), "-") + zone
return prefix + response + zone
} }
+169 -252
View File
@@ -2,378 +2,295 @@ package synthetic
import ( import (
"context" "context"
"fmt" "net"
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns" "github.com/miekg/dns"
"net"
"testing"
) )
func TestServeDNSv4(t *testing.T) { // testCase describes a single DNS query and the expected response.
ip, ipNet, err := net.ParseCIDR("192.0.2.0/24") type testCase struct {
if err != nil { name string
log.Fatal(err)
}
s := synthetic{
Next: test.ErrorHandler(),
Config: syntheticConfig{
net: []*net.IPNet{{IP: ip, Mask: ipNet.Mask}},
forward: "example.com",
prefix: "ip-",
},
}
testCases := []struct {
qname string qname string
qtype uint16 qtype uint16
wantrcode int wantRcode int
wantAnswer []string wantAnswer []string
wantTTL uint32 wantTTL uint32
}{
{
qname: "ip-192-0-2-0.example.com",
qtype: dns.TypeA,
wantrcode: dns.RcodeSuccess,
wantAnswer: []string{"192.0.2.0"},
wantTTL: 0,
},
{
qname: "ip-192-0-2-255.example.com",
qtype: dns.TypeA,
wantrcode: dns.RcodeSuccess,
wantAnswer: []string{"192.0.2.255"},
wantTTL: 0,
},
{
qname: "ip-203-0-113-100.example.com",
qtype: dns.TypeA,
wantrcode: dns.RcodeServerFailure,
wantAnswer: nil,
wantTTL: 0,
},
{
qname: "123.2.0.192.in-addr.arpa.",
qtype: dns.TypePTR,
wantrcode: dns.RcodeSuccess,
wantAnswer: []string{"ip-192-0-2-123.example.com."},
wantTTL: 0,
},
{
qname: "ip-2001-db8-abcd--.example.com",
qtype: dns.TypeA,
wantrcode: dns.RcodeServerFailure,
wantAnswer: nil,
wantTTL: 0,
},
} }
for i, tc := range testCases { // assertResponse validates the DNS response from ServeDNS against tc.
errorMsgPrefix := fmt.Sprintf("Test case %v for '%v' failed. Expected", i, tc.qname) func assertResponse(t *testing.T, tc testCase, rc int, w *dnstest.Recorder) {
ctx := context.TODO() t.Helper()
if rc != tc.wantRcode {
t.Errorf("rcode: got %d, want %d", rc, tc.wantRcode)
}
if tc.wantAnswer == nil {
if len(w.Msg.Answer) > 0 {
t.Errorf("answer: got %v, want none", w.Msg.Answer[0])
}
return
}
if len(w.Msg.Answer) == 0 {
t.Fatal("answer: got none, want an answer")
}
hdr := w.Msg.Answer[0].Header()
if hdr.Name != tc.qname {
t.Errorf("name: got %s, want %s", hdr.Name, tc.qname)
}
if hdr.Rrtype != tc.qtype {
t.Errorf("rrtype: got %d, want %d", hdr.Rrtype, tc.qtype)
}
if hdr.Ttl != tc.wantTTL {
t.Errorf("ttl: got %d, want %d", hdr.Ttl, tc.wantTTL)
}
switch rr := w.Msg.Answer[0].(type) {
case *dns.A:
if rr.A.String() != tc.wantAnswer[0] {
t.Errorf("A record: got %s, want %s", rr.A, tc.wantAnswer[0])
}
case *dns.AAAA:
if rr.AAAA.String() != tc.wantAnswer[0] {
t.Errorf("AAAA record: got %s, want %s", rr.AAAA, tc.wantAnswer[0])
}
case *dns.PTR:
if rr.Ptr != tc.wantAnswer[0] {
t.Errorf("PTR record: got %s, want %s", rr.Ptr, tc.wantAnswer[0])
}
}
}
// runTestCases executes a table of test cases against the given synthetic plugin.
func runTestCases(t *testing.T, s synthetic, cases []testCase) {
t.Helper()
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
w := dnstest.NewRecorder(&test.ResponseWriter{}) w := dnstest.NewRecorder(&test.ResponseWriter{})
r := new(dns.Msg) r := new(dns.Msg)
r.SetQuestion(tc.qname, tc.qtype) r.SetQuestion(tc.qname, tc.qtype)
rc, err := s.ServeDNS(ctx, w, r) rc, err := s.ServeDNS(context.TODO(), w, r)
if err != nil { if err != nil {
t.Errorf("%v no error, but got %v", errorMsgPrefix, err) t.Fatalf("unexpected error: %v", err)
} }
if rc != tc.wantrcode {
t.Errorf("%v rcode %v, but got %v", errorMsgPrefix, tc.wantrcode, rc) assertResponse(t, tc, rc, w)
} })
if len(w.Msg.Answer) == 0 && tc.wantAnswer != nil {
t.Errorf("%v an answer, but got none", errorMsgPrefix)
continue
}
if tc.wantAnswer == nil {
if len(w.Msg.Answer) > 0 {
t.Errorf("%v no answer, but got %v", errorMsgPrefix, w.Msg.Answer[0])
}
continue
}
if w.Msg.Answer[0].Header().Ttl != 0 {
t.Errorf("%v TTL to be 0, but got %v", errorMsgPrefix, w.Msg.Answer[0].Header().Ttl)
}
if w.Msg.Answer[0].Header().Name != tc.qname {
t.Errorf("%v Name to be %s, but got %s", errorMsgPrefix, tc.qname, w.Msg.Answer[0].Header().Name)
}
if w.Msg.Answer[0].Header().Rrtype != tc.qtype {
t.Errorf("%v Type to be %d, but got %d", errorMsgPrefix, tc.qtype, w.Msg.Answer[0].Header().Rrtype)
}
if w.Msg.Answer[0].Header().Ttl != tc.wantTTL {
t.Errorf("%v TTL to be %d, but got %d", errorMsgPrefix, tc.wantTTL, w.Msg.Answer[0].Header().Ttl)
}
switch tc.qtype {
case dns.TypeA:
if w.Msg.Answer[0].(*dns.A).A.String() != tc.wantAnswer[0] {
t.Errorf("%v answer %s, but got %s", errorMsgPrefix, tc.wantAnswer[0], w.Msg.Answer[0].(*dns.A).A.String())
}
case dns.TypePTR:
if w.Msg.Answer[0].(*dns.PTR).Ptr != tc.wantAnswer[0] {
t.Errorf("%v answer %s, but got %s", errorMsgPrefix, tc.wantAnswer[0], w.Msg.Answer[0].(*dns.PTR).Ptr)
} }
} }
func TestServeDNSv4(t *testing.T) {
_, ipNet, err := net.ParseCIDR("192.0.2.0/24")
if err != nil {
t.Fatal(err)
} }
s := synthetic{
Next: test.ErrorHandler(),
Config: syntheticConfig{net: []*net.IPNet{ipNet}, forward: "example.com", prefix: "ip-"},
}
runTestCases(t, s, []testCase{
{
name: "v4 first address",
qname: "ip-192-0-2-0.example.com",
qtype: dns.TypeA,
wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"192.0.2.0"},
},
{
name: "v4 last address",
qname: "ip-192-0-2-255.example.com",
qtype: dns.TypeA,
wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"192.0.2.255"},
},
{
name: "v4 out of range",
qname: "ip-203-0-113-100.example.com",
qtype: dns.TypeA,
wantRcode: dns.RcodeServerFailure,
},
{
name: "v4 PTR reverse",
qname: "123.2.0.192.in-addr.arpa.",
qtype: dns.TypePTR,
wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"ip-192-0-2-123.example.com."},
},
{
name: "v6 address with v4 config",
qname: "ip-2001-db8-abcd--.example.com",
qtype: dns.TypeA,
wantRcode: dns.RcodeServerFailure,
},
})
} }
func TestServeDNSv6(t *testing.T) { func TestServeDNSv6(t *testing.T) {
ip, ipNet, err := net.ParseCIDR("2001:db8:abcd::/48") _, ipNet, err := net.ParseCIDR("2001:db8:abcd::/48")
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
s := synthetic{ s := synthetic{
Next: test.ErrorHandler(), Next: test.ErrorHandler(),
Config: syntheticConfig{ Config: syntheticConfig{
net: []*net.IPNet{{IP: ip, Mask: ipNet.Mask}}, net: []*net.IPNet{ipNet},
forward: "example.com.", forward: "example.com.",
ttl: 1800, ttl: 1800,
prefix: "ip6-", prefix: "ip6-",
}, },
} }
testCases := []struct { runTestCases(t, s, []testCase{
qname string
qtype uint16
wantrcode int
wantAnswer []string
wantTTL uint32
}{
{ {
name: "v6 zero-padded",
qname: "ip6-2001-db8-abcd--.example.com", qname: "ip6-2001-db8-abcd--.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"2001:db8:abcd::"}, wantAnswer: []string{"2001:db8:abcd::"},
wantTTL: 1800, wantTTL: 1800,
}, },
{ {
name: "v6 fully expanded",
qname: "ip6-2001-db8-abcd-1234-4567-890a-bcde-f123.example.com", qname: "ip6-2001-db8-abcd-1234-4567-890a-bcde-f123.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"2001:db8:abcd:1234:4567:890a:bcde:f123"}, wantAnswer: []string{"2001:db8:abcd:1234:4567:890a:bcde:f123"},
wantTTL: 1800, wantTTL: 1800,
}, },
{ {
name: "v6 out of range",
qname: "ip6-2001-db8-1234--.example.com", qname: "ip6-2001-db8-1234--.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeServerFailure, wantRcode: dns.RcodeServerFailure,
wantAnswer: nil,
wantTTL: 1800,
}, },
{ {
name: "v6 PTR reverse",
qname: "3.2.1.f.e.d.c.b.a.0.9.8.7.6.5.4.4.3.2.1.d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa.", qname: "3.2.1.f.e.d.c.b.a.0.9.8.7.6.5.4.4.3.2.1.d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa.",
qtype: dns.TypePTR, qtype: dns.TypePTR,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"ip6-2001-db8-abcd-1234-4567-890a-bcde-f123.example.com."}, wantAnswer: []string{"ip6-2001-db8-abcd-1234-4567-890a-bcde-f123.example.com."},
wantTTL: 1800, wantTTL: 1800,
}, },
{ {
name: "v4 address with v6 config",
qname: "ip6-192-0-2-0.example.com", qname: "ip6-192-0-2-0.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeServerFailure, wantRcode: dns.RcodeServerFailure,
wantAnswer: nil,
wantTTL: 0,
}, },
})
} }
for i, tc := range testCases { // mockSuccessPlugin always returns a successful answer, simulating a static
errorMsgPrefix := fmt.Sprintf("Test case %v for '%v' failed. Expected", i, tc.qname) // zone file or upstream resolver that provides authoritative records.
ctx := context.TODO() type mockSuccessPlugin struct{}
w := dnstest.NewRecorder(&test.ResponseWriter{})
r := new(dns.Msg)
r.SetQuestion(tc.qname, tc.qtype)
rc, err := s.ServeDNS(ctx, w, r) func (m mockSuccessPlugin) Name() string { return "mock" }
if err != nil { func (m mockSuccessPlugin) ServeDNS(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
t.Errorf("%v no error, but got %v", errorMsgPrefix, err) msg := new(dns.Msg)
} msg.SetReply(r)
if rc != tc.wantrcode { hdr := dns.RR_Header{
t.Errorf("%v rcode %v, but got %v", errorMsgPrefix, tc.wantrcode, rc) Name: r.Question[0].Name,
} Rrtype: r.Question[0].Qtype,
if len(w.Msg.Answer) == 0 && tc.wantAnswer != nil { Class: r.Question[0].Qclass,
t.Errorf("%v an answer, but got none", errorMsgPrefix)
continue
}
if tc.wantAnswer == nil {
if len(w.Msg.Answer) > 0 {
t.Errorf("%v no answer, but got %v", errorMsgPrefix, w.Msg.Answer[0])
}
continue
}
if w.Msg.Answer[0].Header().Name != tc.qname {
t.Errorf("%v Name to be %s, but got %s", errorMsgPrefix, tc.qname, w.Msg.Answer[0].Header().Name)
}
if w.Msg.Answer[0].Header().Rrtype != tc.qtype {
t.Errorf("%v Type to be %d, but got %d", errorMsgPrefix, tc.qtype, w.Msg.Answer[0].Header().Rrtype)
}
if w.Msg.Answer[0].Header().Ttl != tc.wantTTL {
t.Errorf("%v TTL to be %d, but got %d", errorMsgPrefix, tc.wantTTL, w.Msg.Answer[0].Header().Ttl)
}
switch tc.qtype {
case dns.TypeA:
if w.Msg.Answer[0].(*dns.A).A.String() != tc.wantAnswer[0] {
t.Errorf("%v answer %s, but got %s", errorMsgPrefix, tc.wantAnswer[0], w.Msg.Answer[0].(*dns.A).A.String())
}
case dns.TypePTR:
if w.Msg.Answer[0].(*dns.PTR).Ptr != tc.wantAnswer[0] {
t.Errorf("%v answer %s, but got %s", errorMsgPrefix, tc.wantAnswer[0], w.Msg.Answer[0].(*dns.PTR).Ptr)
}
}
}
} }
// MockSuccessPlugin is a mock plugin that always responds with a successful DNS response.
type MockSuccessPlugin struct{}
// Name returns the plugin name.
func (m MockSuccessPlugin) Name() string { return "mock" }
// ServeDNS simulates a successful DNS response.
func (m MockSuccessPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m1 := new(dns.Msg)
m1.SetReply(r)
hdr := dns.RR_Header{Name: r.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: 0}
switch r.Question[0].Qtype { switch r.Question[0].Qtype {
case dns.TypeA: case dns.TypeA:
m1.Answer = append(m1.Answer, &dns.A{Hdr: hdr, A: net.ParseIP("192.0.2.100")}) msg.Answer = append(msg.Answer, &dns.A{Hdr: hdr, A: net.ParseIP("192.0.2.100")})
case dns.TypeAAAA: case dns.TypeAAAA:
m1.Answer = append(m1.Answer, &dns.AAAA{Hdr: hdr, AAAA: net.ParseIP("2001:db8::100")}) msg.Answer = append(msg.Answer, &dns.AAAA{Hdr: hdr, AAAA: net.ParseIP("2001:db8::100")})
} }
w.WriteMsg(m1)
w.WriteMsg(msg)
return dns.RcodeSuccess, nil return dns.RcodeSuccess, nil
} }
func TestServeDNSNextPluginRespondsSuccess(t *testing.T) { // TestServeDNSNextPluginPrecedence verifies that answers from the next plugin
ip, ipNet, err := net.ParseCIDR("192.0.2.0/24") // in the chain take priority over synthetic responses for reverse lookups,
// while forward lookups still produce synthetic answers directly.
func TestServeDNSNextPluginPrecedence(t *testing.T) {
_, ipNet4, err := net.ParseCIDR("192.0.2.0/24")
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
ip6, ipNet6, err := net.ParseCIDR("2001:db8:abcd::/48") _, ipNet6, err := net.ParseCIDR("2001:db8:abcd::/48")
if err != nil { if err != nil {
log.Fatal(err) t.Fatal(err)
} }
s := synthetic{ s := synthetic{
Next: MockSuccessPlugin{}, Next: mockSuccessPlugin{},
Config: syntheticConfig{ Config: syntheticConfig{
net: []*net.IPNet{{IP: ip, Mask: ipNet.Mask}, {IP: ip6, Mask: ipNet6.Mask}}, net: []*net.IPNet{ipNet4, ipNet6},
forward: "example.com", forward: "example.com",
prefix: "ip-", prefix: "ip-",
}, },
} }
testCases := []struct { runTestCases(t, s, []testCase{
qname string
qtype uint16
wantrcode int
wantAnswer []string
wantTTL uint32
}{
{ {
name: "forward v4 synthetic wins",
qname: "ip-192-0-2-1.example.com", qname: "ip-192-0-2-1.example.com",
qtype: dns.TypeA, qtype: dns.TypeA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"192.0.2.1"}, wantAnswer: []string{"192.0.2.1"},
wantTTL: 0,
}, },
{ {
name: "forward non-synthetic uses next plugin",
qname: "foobar.example.com", qname: "foobar.example.com",
qtype: dns.TypeA, qtype: dns.TypeA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"192.0.2.100"}, wantAnswer: []string{"192.0.2.100"},
wantTTL: 0,
}, },
{ {
name: "forward v6 synthetic wins",
qname: "ip-2001-db8-abcd--1.example.com", qname: "ip-2001-db8-abcd--1.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"2001:db8:abcd::1"}, wantAnswer: []string{"2001:db8:abcd::1"},
wantTTL: 0,
}, },
{ {
name: "forward non-synthetic AAAA uses next plugin",
qname: "foobar.example.com", qname: "foobar.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"2001:db8:abcd::100"}, wantAnswer: []string{"2001:db8::100"},
wantTTL: 0,
}, },
{ {
name: "v6 address queried as A returns empty",
qname: "ip-2001-db8-abcd--1.example.com", qname: "ip-2001-db8-abcd--1.example.com",
qtype: dns.TypeA, qtype: dns.TypeA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: nil,
wantTTL: 0,
}, },
{ {
name: "v4 address queried as AAAA returns empty",
qname: "ip-192-0-2-1.example.com", qname: "ip-192-0-2-1.example.com",
qtype: dns.TypeAAAA, qtype: dns.TypeAAAA,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: nil,
wantTTL: 0,
}, },
{ {
name: "PTR v6 synthetic (mock has no PTR)",
qname: "3.2.1.f.e.d.c.b.a.0.9.8.7.6.5.4.4.3.2.1.d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa.", qname: "3.2.1.f.e.d.c.b.a.0.9.8.7.6.5.4.4.3.2.1.d.c.b.a.8.b.d.0.1.0.0.2.ip6.arpa.",
qtype: dns.TypePTR, qtype: dns.TypePTR,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"ip6-2001-db8-abcd-1234-4567-890a-bcde-f123.example.com."}, wantAnswer: []string{"ip-2001-db8-abcd-1234-4567-890a-bcde-f123.example.com."},
wantTTL: 0,
}, },
{ {
name: "PTR v4 synthetic (mock has no PTR)",
qname: "123.2.0.192.in-addr.arpa.", qname: "123.2.0.192.in-addr.arpa.",
qtype: dns.TypePTR, qtype: dns.TypePTR,
wantrcode: dns.RcodeSuccess, wantRcode: dns.RcodeSuccess,
wantAnswer: []string{"ip-192-0-2-123.example.com."}, wantAnswer: []string{"ip-192-0-2-123.example.com."},
wantTTL: 0,
}, },
} })
for i, tc := range testCases {
errorMsgPrefix := fmt.Sprintf("Test case %v for '%v' failed. Expected", i, tc.qname)
ctx := context.TODO()
w := dnstest.NewRecorder(&test.ResponseWriter{})
r := new(dns.Msg)
r.SetQuestion(tc.qname, tc.qtype)
rc, err := s.ServeDNS(ctx, w, r)
if err != nil {
t.Errorf("%v no error, but got %v", errorMsgPrefix, err)
}
if rc != tc.wantrcode {
t.Errorf("%v rcode %v, but got %v", errorMsgPrefix, tc.wantrcode, rc)
}
if len(w.Msg.Answer) == 0 && tc.wantAnswer != nil {
t.Errorf("%v an answer, but got none", errorMsgPrefix)
continue
}
if tc.wantAnswer == nil {
if len(w.Msg.Answer) > 0 {
t.Errorf("%v no answer, but got %v", errorMsgPrefix, w.Msg.Answer[0])
}
continue
}
if w.Msg.Answer[0].Header().Ttl != 0 {
t.Errorf("%v TTL to be 0, but got %v", errorMsgPrefix, w.Msg.Answer[0].Header().Ttl)
}
if w.Msg.Answer[0].Header().Name != tc.qname {
t.Errorf("%v Name to be %s, but got %s", errorMsgPrefix, tc.qname, w.Msg.Answer[0].Header().Name)
}
if w.Msg.Answer[0].Header().Rrtype != tc.qtype {
t.Errorf("%v Type to be %d, but got %d", errorMsgPrefix, tc.qtype, w.Msg.Answer[0].Header().Rrtype)
}
if w.Msg.Answer[0].Header().Ttl != tc.wantTTL {
t.Errorf("%v TTL to be %d, but got %d", errorMsgPrefix, tc.wantTTL, w.Msg.Answer[0].Header().Ttl)
}
switch tc.qtype {
case dns.TypeA:
if w.Msg.Answer[0].(*dns.A).A.String() != tc.wantAnswer[0] {
t.Errorf("%v answer %s, but got %s", errorMsgPrefix, tc.wantAnswer[0], w.Msg.Answer[0].(*dns.A).A.String())
}
}
}
} }