Allow DNS chain

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-07-27 08:30:13 +02:00
parent 4e1ecd41c2
commit 427365b1f2
3 changed files with 315 additions and 34 deletions

View File

@@ -22,6 +22,7 @@ package dnsclient
import (
"context"
"strconv"
"testing"
"time"
@@ -50,6 +51,204 @@ func TestCheckCNAME(t *testing.T) {
require.NoError(t, err)
})
t.Run("accepts alias chain reaching the target", func(t *testing.T) {
t.Parallel()
chain := map[string]string{
"trust.example.com.": "cname.eu.console.getprobo.com.",
"cname.eu.console.getprobo.com.": "custom.getprobo.com.",
}
var queried []string
client := &Client{
exchange: func(_ context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
name := msg.Question[0].Header().Name
queried = append(queried, name)
target, ok := chain[name]
if !ok {
return &dns.Msg{}, nil
}
cname := &dns.CNAME{Hdr: dns.Header{Name: name}}
cname.Target = target
return &dns.Msg{Answer: []dns.RR{cname}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.NoError(t, err)
assert.Equal(
t,
[]string{"trust.example.com.", "cname.eu.console.getprobo.com."},
queried,
)
})
t.Run("accepts chain returned in a single answer", func(t *testing.T) {
t.Parallel()
var queried []string
client := &Client{
exchange: func(_ context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
queried = append(queried, msg.Question[0].Header().Name)
first := &dns.CNAME{Hdr: dns.Header{Name: "trust.example.com."}}
first.Target = "cname.eu.console.getprobo.com."
second := &dns.CNAME{Hdr: dns.Header{Name: "cname.eu.console.getprobo.com."}}
second.Target = "custom.getprobo.com."
third := &dns.CNAME{Hdr: dns.Header{Name: "custom.getprobo.com."}}
third.Target = "lb.example-cloud.com."
return &dns.Msg{Answer: []dns.RR{first, second, third}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.NoError(t, err)
assert.Equal(t, []string{"trust.example.com."}, queried)
})
t.Run("rejects chain ending before the target", func(t *testing.T) {
t.Parallel()
client := &Client{
exchange: func(_ context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
name := msg.Question[0].Header().Name
if name != "trust.example.com." {
return &dns.Msg{}, nil
}
cname := &dns.CNAME{Hdr: dns.Header{Name: name}}
cname.Target = "other.example.net."
return &dns.Msg{Answer: []dns.RR{cname}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.Error(t, err)
assert.Contains(t, err.Error(), `stops at "other.example.net"`)
})
t.Run("rejects looping chain", func(t *testing.T) {
t.Parallel()
chain := map[string]string{
"trust.example.com.": "alias.example.net.",
"alias.example.net.": "trust.example.com.",
}
client := &Client{
exchange: func(_ context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
name := msg.Question[0].Header().Name
cname := &dns.CNAME{Hdr: dns.Header{Name: name}}
cname.Target = chain[name]
return &dns.Msg{Answer: []dns.RR{cname}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "loops back")
})
t.Run("rejects chain longer than the lookup budget", func(t *testing.T) {
t.Parallel()
var queried []string
client := &Client{
exchange: func(_ context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
name := msg.Question[0].Header().Name
queried = append(queried, name)
cname := &dns.CNAME{Hdr: dns.Header{Name: name}}
cname.Target = "hop" + strconv.Itoa(len(queried)) + ".example.net."
return &dns.Msg{Answer: []dns.RR{cname}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "does not reach")
assert.Len(t, queried, maxCNAMELookups)
})
t.Run("rejects multiple records owned by the same name", func(t *testing.T) {
t.Parallel()
client := &Client{
exchange: func(_ context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
name := msg.Question[0].Header().Name
first := &dns.CNAME{Hdr: dns.Header{Name: name}}
first.Target = "custom.getprobo.com."
second := &dns.CNAME{Hdr: dns.Header{Name: name}}
second.Target = "other.example.net."
return &dns.Msg{Answer: []dns.RR{first, second}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "multiple cname records found")
})
t.Run("applies exchange timeout per hop", func(t *testing.T) {
t.Parallel()
chain := map[string]string{
"trust.example.com.": "alias.example.net.",
"alias.example.net.": "custom.getprobo.com.",
}
var deadlines []time.Time
client := &Client{
ExchangeTimeout: 2 * time.Second,
exchange: func(ctx context.Context, msg *dns.Msg, _ string) (*dns.Msg, error) {
deadline, ok := ctx.Deadline()
require.True(t, ok)
deadlines = append(deadlines, deadline)
name := msg.Question[0].Header().Name
cname := &dns.CNAME{Hdr: dns.Header{Name: name}}
cname.Target = chain[name]
return &dns.Msg{Answer: []dns.RR{cname}}, nil
},
}
err := client.CheckCNAME(context.Background(), "trust.example.com", "custom.getprobo.com")
require.NoError(t, err)
require.Len(t, deadlines, 2)
assert.True(
t,
deadlines[1].After(deadlines[0]),
"expected a fresh per-hop deadline, got shared chain deadline",
)
})
t.Run("rejects apex owned record for subdomain query", func(t *testing.T) {
t.Parallel()