mirror of
https://github.com/coredns/coredns.git
synced 2026-10-15 23:10:43 -04:00
plugin/forward: support DNS-over-QUIC upstreams (#8474)
Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
196
plugin/forward/doq_test.go
Normal file
196
plugin/forward/doq_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package forward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
func TestForwardDoQIntegration(t *testing.T) {
|
||||
serverTLS, roots := makeForwardDoQTestTLS(t)
|
||||
listener, err := quic.ListenAddr("127.0.0.1:0", serverTLS, &quic.Config{MaxIncomingStreams: 16})
|
||||
if err != nil {
|
||||
t.Fatalf("quic.ListenAddr() failed: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
serverResult := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept(context.Background())
|
||||
if err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
if got := conn.ConnectionState().TLS.NegotiatedProtocol; got != "doq" {
|
||||
serverResult <- fmt.Errorf("negotiated ALPN = %q, want doq", got)
|
||||
return
|
||||
}
|
||||
stream, err := conn.AcceptStream(context.Background())
|
||||
if err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
_ = stream.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
query, err := readForwardDoQMessage(stream)
|
||||
if err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
var extra [1]byte
|
||||
if n, err := stream.Read(extra[:]); n != 0 || !errors.Is(err, io.EOF) {
|
||||
serverResult <- fmt.Errorf("query stream did not end with FIN: n=%d err=%v", n, err)
|
||||
return
|
||||
}
|
||||
if query.Id != 0 {
|
||||
serverResult <- fmt.Errorf("query ID = %d, want 0", query.Id)
|
||||
return
|
||||
}
|
||||
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(query)
|
||||
record, err := dns.NewRR("example.org. 60 IN A 192.0.2.53")
|
||||
if err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
response.Answer = []dns.RR{record}
|
||||
wire, err := response.Pack()
|
||||
if err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
frame := make([]byte, 2+len(wire))
|
||||
binary.BigEndian.PutUint16(frame, uint16(len(wire))) // #nosec G115 -- DNS wire size is bounded by Pack
|
||||
copy(frame[2:], wire)
|
||||
for len(frame) > 0 {
|
||||
n, err := stream.Write(frame)
|
||||
if err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
serverResult <- io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
frame = frame[n:]
|
||||
}
|
||||
if err := stream.Close(); err != nil {
|
||||
serverResult <- err
|
||||
return
|
||||
}
|
||||
serverResult <- nil
|
||||
}()
|
||||
|
||||
c := caddy.NewTestController("dns", fmt.Sprintf(`forward . quic://%s {
|
||||
tls_servername doq.test
|
||||
}`, listener.Addr()))
|
||||
fs, err := parseForward(c)
|
||||
if err != nil {
|
||||
t.Fatalf("parseForward() failed: %v", err)
|
||||
}
|
||||
f := fs[0]
|
||||
clientTLS := f.proxies[0].GetTransport().GetTLSConfig().Clone()
|
||||
clientTLS.RootCAs = roots
|
||||
f.proxies[0].SetTLSConfig(clientTLS)
|
||||
if err := f.OnStartup(); err != nil {
|
||||
t.Fatalf("OnStartup() failed: %v", err)
|
||||
}
|
||||
defer f.OnShutdown()
|
||||
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion("example.org.", dns.TypeA)
|
||||
query.Id = 0x4321
|
||||
recorder := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
if _, err := f.ServeDNS(context.Background(), recorder, query); err != nil {
|
||||
t.Fatalf("ServeDNS() failed: %v", err)
|
||||
}
|
||||
if recorder.Msg == nil || recorder.Msg.Id != 0x4321 {
|
||||
t.Fatalf("response ID = %v, want %d", recorder.Msg, 0x4321)
|
||||
}
|
||||
if len(recorder.Msg.Answer) != 1 || recorder.Msg.Answer[0].String() != "example.org.\t60\tIN\tA\t192.0.2.53" {
|
||||
t.Fatalf("unexpected response answers: %v", recorder.Msg.Answer)
|
||||
}
|
||||
select {
|
||||
case err := <-serverResult:
|
||||
if err != nil {
|
||||
t.Fatalf("DoQ upstream failed: %v", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("DoQ upstream did not finish")
|
||||
}
|
||||
|
||||
transfer := new(dns.Msg)
|
||||
transfer.SetQuestion("example.org.", dns.TypeAXFR)
|
||||
rcode, err := f.ServeDNS(context.Background(), &test.ResponseWriter{}, transfer)
|
||||
if rcode != dns.RcodeNotImplemented || err == nil {
|
||||
t.Fatalf("AXFR over DoQ returned rcode=%d err=%v, want NOTIMP with an error", rcode, err)
|
||||
}
|
||||
}
|
||||
|
||||
func readForwardDoQMessage(r io.Reader) (*dns.Msg, error) {
|
||||
var size [2]byte
|
||||
if _, err := io.ReadFull(r, size[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wire := make([]byte, int(binary.BigEndian.Uint16(size[:])))
|
||||
if len(wire) == 0 {
|
||||
return nil, errors.New("zero-length DoQ message")
|
||||
}
|
||||
if _, err := io.ReadFull(r, wire); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := new(dns.Msg)
|
||||
if err := msg.Unpack(wire); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func makeForwardDoQTestTLS(t *testing.T) (*tls.Config, *x509.CertPool) {
|
||||
t.Helper()
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("ecdsa.GenerateKey() failed: %v", err)
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "doq.test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{"doq.test"},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("x509.CreateCertificate() failed: %v", err)
|
||||
}
|
||||
parsed, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
t.Fatalf("x509.ParseCertificate() failed: %v", err)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(parsed)
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{{Certificate: [][]byte{der}, PrivateKey: privateKey}},
|
||||
NextProtos: []string{"doq"},
|
||||
}, roots
|
||||
}
|
||||
Reference in New Issue
Block a user