mirror of
https://github.com/coredns/coredns.git
synced 2026-04-04 19:25:40 -04:00
plugin/file: expand SVCB/HTTPS record support (#7950)
* plugin/file: expand SVCB/HTTPS record support Add proper SVCB (type 64) and HTTPS (type 65) handling: - Additional section processing: include A/AAAA glue for in-bailiwick SVCB/HTTPS targets, matching existing SRV/MX behavior - Target name normalization: lowercase SVCB/HTTPS Target on zone insert, consistent with CNAME/MX handling - Metrics: add TypeSVCB to monitored query types (TypeHTTPS was already present) - Test helpers: add SVCB()/HTTPS() constructors and Section comparison cases - Tests: basic queries with glue, AliasMode, wildcards, NoData, NXDOMAIN, target normalization, and DNS-AID private-use key (65400-65408) round-trip Signed-off-by: Ingmar <ivanglabbeek@infoblox.com> * plugin/file: simplify HTTPS target access via field promotion dns.HTTPS embeds dns.SVCB, so .Target is directly accessible without the redundant .SVCB. qualifier. Fixes gosimple S1027. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Ingmar <ivanglabbeek@infoblox.com> --------- Signed-off-by: Ingmar <ivanglabbeek@infoblox.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
a8caf4c375
commit
12d9457e71
@@ -409,6 +409,10 @@ func (z *Zone) additionalProcessing(answer []dns.RR, do bool) (extra []dns.RR) {
|
||||
name = x.Target
|
||||
case *dns.MX:
|
||||
name = x.Mx
|
||||
case *dns.SVCB:
|
||||
name = x.Target
|
||||
case *dns.HTTPS:
|
||||
name = x.Target
|
||||
}
|
||||
if len(name) == 0 || !dns.IsSubDomain(z.origin, name) {
|
||||
continue
|
||||
|
||||
224
plugin/file/svcb_test.go
Normal file
224
plugin/file/svcb_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var svcbAuth = []dns.RR{
|
||||
test.NS("example.com. 1800 IN NS ns.example.com."),
|
||||
}
|
||||
|
||||
var svcbTestCases = []test.Case{
|
||||
{
|
||||
// Basic SVCB query with additional section glue for in-bailiwick target
|
||||
Qname: "_8443._foo.example.com.", Qtype: dns.TypeSVCB,
|
||||
Answer: []dns.RR{
|
||||
test.SVCB("_8443._foo.example.com. 1800 IN SVCB 1 svc-target.example.com. alpn=\"h2,h3\" port=\"8443\""),
|
||||
},
|
||||
Ns: svcbAuth,
|
||||
Extra: []dns.RR{
|
||||
test.A("svc-target.example.com. 1800 IN A 192.0.2.10"),
|
||||
test.AAAA("svc-target.example.com. 1800 IN AAAA 2001:db8::10"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Basic HTTPS query with additional section glue for in-bailiwick target
|
||||
Qname: "www.example.com.", Qtype: dns.TypeHTTPS,
|
||||
Answer: []dns.RR{
|
||||
test.HTTPS("www.example.com. 1800 IN HTTPS 1 svc-target.example.com. alpn=\"h2,h3\""),
|
||||
},
|
||||
Ns: svcbAuth,
|
||||
Extra: []dns.RR{
|
||||
test.A("svc-target.example.com. 1800 IN A 192.0.2.10"),
|
||||
test.AAAA("svc-target.example.com. 1800 IN AAAA 2001:db8::10"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// SVCB AliasMode (Priority=0) — glue still added for in-bailiwick target
|
||||
Qname: "_alias._foo.example.com.", Qtype: dns.TypeSVCB,
|
||||
Answer: []dns.RR{
|
||||
test.SVCB("_alias._foo.example.com. 1800 IN SVCB 0 svc-target.example.com."),
|
||||
},
|
||||
Ns: svcbAuth,
|
||||
Extra: []dns.RR{
|
||||
test.A("svc-target.example.com. 1800 IN A 192.0.2.10"),
|
||||
test.AAAA("svc-target.example.com. 1800 IN AAAA 2001:db8::10"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// Wildcard SVCB expansion (no additional section — wildcards don't run additionalProcessing)
|
||||
Qname: "_http._tcp.example.com.", Qtype: dns.TypeSVCB,
|
||||
Answer: []dns.RR{
|
||||
test.SVCB("_http._tcp.example.com. 1800 IN SVCB 1 svc-target.example.com. port=\"443\""),
|
||||
},
|
||||
Ns: svcbAuth,
|
||||
},
|
||||
{
|
||||
// NoData: existing name, no SVCB record
|
||||
Qname: "svc-target.example.com.", Qtype: dns.TypeSVCB,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("example.com. 1800 IN SOA ns.example.com. admin.example.com. 2024010100 14400 3600 604800 14400"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// NXDOMAIN
|
||||
Qname: "nonexistent.example.com.", Qtype: dns.TypeSVCB,
|
||||
Rcode: dns.RcodeNameError,
|
||||
Ns: []dns.RR{
|
||||
test.SOA("example.com. 1800 IN SOA ns.example.com. admin.example.com. 2024010100 14400 3600 604800 14400"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestLookupSVCB(t *testing.T) {
|
||||
zone, err := Parse(strings.NewReader(dbSVCBExample), testSVCBOrigin, "stdin", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error when reading zone, got %q", err)
|
||||
}
|
||||
|
||||
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testSVCBOrigin: zone}, Names: []string{testSVCBOrigin}}}
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, tc := range svcbTestCases {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
_, err := fm.ServeDNS(ctx, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for %q/%d, got %v", tc.Qname, tc.Qtype, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
if err := test.SortAndCheck(resp, tc); err != nil {
|
||||
t.Errorf("Test %q/%d: %v", tc.Qname, tc.Qtype, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSVCBTargetNormalization(t *testing.T) {
|
||||
// Zone with mixed-case SVCB target — should be lowercased on insert
|
||||
const dbMixedCase = `
|
||||
$TTL 30M
|
||||
$ORIGIN example.com.
|
||||
@ IN SOA ns.example.com. admin.example.com. ( 2024010100 14400 3600 604800 14400 )
|
||||
IN NS ns.example.com.
|
||||
ns IN A 192.0.2.1
|
||||
svc IN A 192.0.2.10
|
||||
_foo IN SVCB 1 SVC.Example.COM. alpn=h2
|
||||
`
|
||||
zone, err := Parse(strings.NewReader(dbMixedCase), testSVCBOrigin, "stdin", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error when reading zone, got %q", err)
|
||||
}
|
||||
|
||||
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testSVCBOrigin: zone}, Names: []string{testSVCBOrigin}}}
|
||||
ctx := context.TODO()
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("_foo.example.com.", dns.TypeSVCB)
|
||||
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
_, err = fm.ServeDNS(ctx, rec, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
if len(resp.Answer) != 1 {
|
||||
t.Fatalf("Expected 1 answer, got %d", len(resp.Answer))
|
||||
}
|
||||
|
||||
svcb, ok := resp.Answer[0].(*dns.SVCB)
|
||||
if !ok {
|
||||
t.Fatalf("Expected SVCB record, got %T", resp.Answer[0])
|
||||
}
|
||||
if svcb.Target != "svc.example.com." {
|
||||
t.Errorf("Expected lowercased target %q, got %q", "svc.example.com.", svcb.Target)
|
||||
}
|
||||
|
||||
// Verify additional section contains glue (target was normalized so lookup works)
|
||||
if len(resp.Extra) != 1 {
|
||||
t.Fatalf("Expected 1 extra record (A glue), got %d", len(resp.Extra))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSVCBPrivateKeys(t *testing.T) {
|
||||
// Test DNS-AID private-use SvcParamKeys (65400-65408) round-trip
|
||||
zone, err := Parse(strings.NewReader(dbSVCBExample), testSVCBOrigin, "stdin", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error when reading zone, got %q", err)
|
||||
}
|
||||
|
||||
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testSVCBOrigin: zone}, Names: []string{testSVCBOrigin}}}
|
||||
ctx := context.TODO()
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("_dnsaid.example.com.", dns.TypeHTTPS)
|
||||
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
_, err = fm.ServeDNS(ctx, rec, m)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
if len(resp.Answer) != 1 {
|
||||
t.Fatalf("Expected 1 answer, got %d", len(resp.Answer))
|
||||
}
|
||||
|
||||
https, ok := resp.Answer[0].(*dns.HTTPS)
|
||||
if !ok {
|
||||
t.Fatalf("Expected HTTPS record, got %T", resp.Answer[0])
|
||||
}
|
||||
|
||||
// Verify private-use keys survived the round-trip
|
||||
rr := https.String()
|
||||
for _, key := range []string{"key65400=", "key65401=", "key65406="} {
|
||||
if !strings.Contains(rr, key) {
|
||||
t.Errorf("Expected response to contain %s, got: %s", key, rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testSVCBOrigin = "example.com."
|
||||
|
||||
const dbSVCBExample = `
|
||||
$TTL 30M
|
||||
$ORIGIN example.com.
|
||||
@ IN SOA ns.example.com. admin.example.com. (
|
||||
2024010100 ; serial
|
||||
14400 ; refresh (4 hours)
|
||||
3600 ; retry (1 hour)
|
||||
604800 ; expire (1 week)
|
||||
14400 ; minimum (4 hours)
|
||||
)
|
||||
IN NS ns.example.com.
|
||||
|
||||
; A/AAAA records for glue
|
||||
ns IN A 192.0.2.1
|
||||
svc-target IN A 192.0.2.10
|
||||
IN AAAA 2001:db8::10
|
||||
|
||||
; SVCB ServiceMode with in-bailiwick target (tests additional section processing)
|
||||
_8443._foo IN SVCB 1 svc-target.example.com. alpn=h2,h3 port=8443
|
||||
|
||||
; HTTPS ServiceMode with in-bailiwick target
|
||||
www IN HTTPS 1 svc-target.example.com. alpn=h2,h3
|
||||
|
||||
; SVCB AliasMode (Priority=0)
|
||||
_alias._foo IN SVCB 0 svc-target.example.com.
|
||||
|
||||
; Wildcard SVCB
|
||||
*._tcp IN SVCB 1 svc-target.example.com. port=443
|
||||
|
||||
; DNS-AID private-use keys (65400-65408)
|
||||
_dnsaid IN HTTPS 1 svc-target.example.com. alpn=h2 key65400="https://aid.example.com/cap" key65401="sha256:e3b0c44298fc" key65406="apphub-psc"
|
||||
`
|
||||
@@ -118,6 +118,10 @@ func (z *Zone) Insert(r dns.RR) error {
|
||||
r.(*dns.MX).Mx = strings.ToLower(r.(*dns.MX).Mx)
|
||||
case dns.TypeSRV:
|
||||
// r.(*dns.SRV).Target = strings.ToLower(r.(*dns.SRV).Target)
|
||||
case dns.TypeSVCB:
|
||||
r.(*dns.SVCB).Target = strings.ToLower(r.(*dns.SVCB).Target)
|
||||
case dns.TypeHTTPS:
|
||||
r.(*dns.HTTPS).Target = strings.ToLower(r.(*dns.HTTPS).Target)
|
||||
}
|
||||
|
||||
z.Tree.Insert(r)
|
||||
|
||||
@@ -20,6 +20,7 @@ var monitorType = map[uint16]struct{}{
|
||||
dns.TypeSRV: {},
|
||||
dns.TypeTXT: {},
|
||||
dns.TypeHTTPS: {},
|
||||
dns.TypeSVCB: {},
|
||||
// Meta Qtypes
|
||||
dns.TypeIXFR: {},
|
||||
dns.TypeAXFR: {},
|
||||
|
||||
@@ -111,6 +111,12 @@ func DS(rr string) *dns.DS { r, _ := dns.NewRR(rr); return r.(*dns.DS) }
|
||||
// NAPTR returns a NAPTR record from rr. It panics on errors.
|
||||
func NAPTR(rr string) *dns.NAPTR { r, _ := dns.NewRR(rr); return r.(*dns.NAPTR) }
|
||||
|
||||
// SVCB returns a SVCB record from rr. It panics on errors.
|
||||
func SVCB(rr string) *dns.SVCB { r, _ := dns.NewRR(rr); return r.(*dns.SVCB) }
|
||||
|
||||
// HTTPS returns an HTTPS record from rr. It panics on errors.
|
||||
func HTTPS(rr string) *dns.HTTPS { r, _ := dns.NewRR(rr); return r.(*dns.HTTPS) }
|
||||
|
||||
// OPT returns an OPT record with UDP buffer size set to bufsize and the DO bit set to do.
|
||||
func OPT(bufsize int, do bool) *dns.OPT {
|
||||
o := new(dns.OPT)
|
||||
@@ -256,6 +262,28 @@ func Section(tc Case, sec sect, rr []dns.RR) error {
|
||||
if x.Do() != tt.Do() {
|
||||
return fmt.Errorf("OPT DO should be %t, but is %t", tt.Do(), x.Do())
|
||||
}
|
||||
case *dns.SVCB:
|
||||
tt := section[i].(*dns.SVCB)
|
||||
if x.Priority != tt.Priority {
|
||||
return fmt.Errorf("RR %d should have a Priority of %d, but has %d", i, tt.Priority, x.Priority)
|
||||
}
|
||||
if x.Target != tt.Target {
|
||||
return fmt.Errorf("RR %d should have a Target of %q, but has %q", i, tt.Target, x.Target)
|
||||
}
|
||||
if x.String() != tt.String() {
|
||||
return fmt.Errorf("RR %d should have value %q, but has %q", i, tt.String(), x.String())
|
||||
}
|
||||
case *dns.HTTPS:
|
||||
tt := section[i].(*dns.HTTPS)
|
||||
if x.Priority != tt.Priority {
|
||||
return fmt.Errorf("RR %d should have a Priority of %d, but has %d", i, tt.Priority, x.Priority)
|
||||
}
|
||||
if x.Target != tt.Target {
|
||||
return fmt.Errorf("RR %d should have a Target of %q, but has %q", i, tt.Target, x.Target)
|
||||
}
|
||||
if x.String() != tt.String() {
|
||||
return fmt.Errorf("RR %d should have value %q, but has %q", i, tt.String(), x.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user