mirror of
https://github.com/coredns/coredns.git
synced 2025-11-22 11:52:17 -05:00
Add basic support for DoH3 (#7677)
This commit is contained in:
12
README.md
12
README.md
@@ -22,6 +22,7 @@ CoreDNS can listen for DNS requests coming in over:
|
||||
* UDP/TCP (go'old DNS).
|
||||
* TLS - DoT ([RFC 7858](https://tools.ietf.org/html/rfc7858)).
|
||||
* DNS over HTTP/2 - DoH ([RFC 8484](https://tools.ietf.org/html/rfc8484)).
|
||||
* DNS over HTTP/3 - DoH3
|
||||
* DNS over QUIC - DoQ ([RFC 9250](https://tools.ietf.org/html/rfc9250)).
|
||||
* [gRPC](https://grpc.io) (not a standard).
|
||||
|
||||
@@ -253,6 +254,17 @@ grpc://example.org:1443 https://example.org:1444 {
|
||||
}
|
||||
~~~
|
||||
|
||||
And for DNS over HTTP/3 (DoH3) use:
|
||||
|
||||
~~~ corefile
|
||||
https3://example.org {
|
||||
whoami
|
||||
tls mycert mykey
|
||||
}
|
||||
~~~
|
||||
in this setup, the CoreDNS will be responsible for TLS termination
|
||||
|
||||
|
||||
When no transport protocol is specified the default `dns://` is assumed.
|
||||
|
||||
## Community
|
||||
|
||||
@@ -88,6 +88,8 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
|
||||
port = transport.GRPCPort
|
||||
case transport.HTTPS:
|
||||
port = transport.HTTPSPort
|
||||
case transport.HTTPS3:
|
||||
port = transport.HTTPSPort
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +349,13 @@ func makeServersForGroup(addr string, group []*Config) ([]caddy.Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case transport.HTTPS3:
|
||||
s, err := NewServerHTTPS3(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
}
|
||||
}
|
||||
return servers, nil
|
||||
|
||||
194
core/dnsserver/server_https3.go
Normal file
194
core/dnsserver/server_https3.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/plugin/pkg/doh"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
// ServerHTTPS3 represents a DNS-over-HTTP/3 server.
|
||||
type ServerHTTPS3 struct {
|
||||
*Server
|
||||
httpsServer *http3.Server
|
||||
listenAddr net.Addr
|
||||
tlsConfig *tls.Config
|
||||
quicConfig *quic.Config
|
||||
validRequest func(*http.Request) bool
|
||||
}
|
||||
|
||||
// NewServerHTTPS3 builds the HTTP/3 (DoH3) server.
|
||||
func NewServerHTTPS3(addr string, group []*Config) (*ServerHTTPS3, error) {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract TLS config (CoreDNS guarantees it is consistent)
|
||||
var tlsConfig *tls.Config
|
||||
for _, z := range s.zones {
|
||||
for _, conf := range z {
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
}
|
||||
if tlsConfig == nil {
|
||||
return nil, fmt.Errorf("DoH3 requires TLS, no TLS config found")
|
||||
}
|
||||
|
||||
// HTTP/3 requires ALPN "h3"
|
||||
tlsConfig.NextProtos = []string{"h3"}
|
||||
|
||||
// Request validator
|
||||
var validator func(*http.Request) bool
|
||||
for _, z := range s.zones {
|
||||
for _, conf := range z {
|
||||
validator = conf.HTTPRequestValidateFunc
|
||||
}
|
||||
}
|
||||
if validator == nil {
|
||||
validator = func(r *http.Request) bool { return r.URL.Path == doh.Path }
|
||||
}
|
||||
|
||||
// QUIC transport config
|
||||
qconf := &quic.Config{
|
||||
MaxIdleTimeout: s.IdleTimeout,
|
||||
Allow0RTT: true,
|
||||
}
|
||||
|
||||
h3srv := &http3.Server{
|
||||
Handler: nil, // set after constructing ServerHTTPS3
|
||||
TLSConfig: tlsConfig,
|
||||
EnableDatagrams: true,
|
||||
QUICConfig: qconf,
|
||||
//Logger: stdlog.New(&loggerAdapter{}, "", 0), TODO: Fix it
|
||||
}
|
||||
|
||||
sh := &ServerHTTPS3{
|
||||
Server: s,
|
||||
tlsConfig: tlsConfig,
|
||||
httpsServer: h3srv,
|
||||
quicConfig: qconf,
|
||||
validRequest: validator,
|
||||
}
|
||||
|
||||
h3srv.Handler = sh
|
||||
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
var _ caddy.GracefulServer = &ServerHTTPS3{}
|
||||
|
||||
// ListenPacket opens the UDP socket for QUIC.
|
||||
func (s *ServerHTTPS3) ListenPacket() (net.PacketConn, error) {
|
||||
return reuseport.ListenPacket("udp", s.Addr[len(transport.HTTPS3+"://"):])
|
||||
}
|
||||
|
||||
// ServePacket starts serving QUIC+HTTP/3 on an existing UDP socket.
|
||||
func (s *ServerHTTPS3) ServePacket(pc net.PacketConn) error {
|
||||
s.m.Lock()
|
||||
s.listenAddr = pc.LocalAddr()
|
||||
s.m.Unlock()
|
||||
// Serve HTTP/3 over QUIC
|
||||
return s.httpsServer.Serve(pc)
|
||||
}
|
||||
|
||||
// Listen function not used in HTTP/3, but defined for compatibility
|
||||
func (s *ServerHTTPS3) Listen() (net.Listener, error) { return nil, nil }
|
||||
func (s *ServerHTTPS3) Serve(l net.Listener) error { return nil }
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *ServerHTTPS3) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
out := startUpZones(transport.HTTPS3+"://", s.Addr, s.zones)
|
||||
if out != "" {
|
||||
fmt.Print(out)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop graceful shutdown. It blocks until the server is totally stopped.
|
||||
func (s *ServerHTTPS3) Stop() error {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
if s.httpsServer != nil {
|
||||
return s.httpsServer.Shutdown(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown stops the server (non gracefully).
|
||||
func (s *ServerHTTPS3) Shutdown() error {
|
||||
if s.httpsServer != nil {
|
||||
s.httpsServer.Shutdown(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP is the handler for the DoH3 requests
|
||||
func (s *ServerHTTPS3) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.validRequest(r) {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
s.countResponse(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := doh.RequestToMsg(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
s.countResponse(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// from HTTP request → DNS writer
|
||||
h, p, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
port, _ := strconv.Atoi(p)
|
||||
dw := &DoHWriter{
|
||||
laddr: s.listenAddr,
|
||||
raddr: &net.UDPAddr{IP: net.ParseIP(h), Port: port},
|
||||
request: r,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), Key{}, s.Server)
|
||||
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
||||
ctx = context.WithValue(ctx, HTTPRequestKey{}, r)
|
||||
|
||||
s.ServeDNS(ctx, dw, msg)
|
||||
|
||||
if dw.Msg == nil {
|
||||
http.Error(w, "No response", http.StatusInternalServerError)
|
||||
s.countResponse(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
buf, _ := dw.Msg.Pack()
|
||||
mt, _ := response.Typify(dw.Msg, time.Now().UTC())
|
||||
age := dnsutil.MinimalTTL(dw.Msg, mt)
|
||||
|
||||
w.Header().Set("Content-Type", doh.MimeType)
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", uint32(age.Seconds())))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(buf)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
s.countResponse(http.StatusOK)
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func (s *ServerHTTPS3) countResponse(status int) {
|
||||
vars.HTTPS3ResponsesCount.WithLabelValues(s.Addr, strconv.Itoa(status)).Inc()
|
||||
}
|
||||
62
core/dnsserver/server_https3_test.go
Normal file
62
core/dnsserver/server_https3_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package dnsserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func testServerHTTPS3(t *testing.T, path string, validator func(*http.Request) bool) *http.Response {
|
||||
t.Helper()
|
||||
c := Config{
|
||||
Zone: "example.com.",
|
||||
Transport: "https",
|
||||
TLSConfig: &tls.Config{},
|
||||
ListenHosts: []string{"127.0.0.1"},
|
||||
Port: "443",
|
||||
HTTPRequestValidateFunc: validator,
|
||||
}
|
||||
s, err := NewServerHTTPS3("127.0.0.1:443", []*Config{&c})
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.Fatal("could not create HTTPS3 server")
|
||||
}
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("example.org.", dns.TypeDNSKEY)
|
||||
buf, err := m.Pack()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(buf))
|
||||
w := httptest.NewRecorder()
|
||||
s.ServeHTTP(w, r)
|
||||
|
||||
return w.Result()
|
||||
}
|
||||
|
||||
func TestCustomHTTP3RequestValidator(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
path string
|
||||
expected int
|
||||
validator func(*http.Request) bool
|
||||
}{
|
||||
"default": {"/dns-query", http.StatusOK, nil},
|
||||
"custom validator": {"/b10cada", http.StatusOK, validator},
|
||||
"no validator set": {"/adb10c", http.StatusNotFound, nil},
|
||||
"invalid path with validator": {"/helloworld", http.StatusNotFound, validator},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
res := testServerHTTPS3(t, tc.path, tc.validator)
|
||||
if res.StatusCode != tc.expected {
|
||||
t.Error("unexpected HTTP code", res.StatusCode)
|
||||
}
|
||||
res.Body.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
1
go.mod
1
go.mod
@@ -150,6 +150,7 @@ require (
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.8-0.20250809033336-ffcdc2b7662f // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -317,6 +317,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY=
|
||||
|
||||
@@ -76,6 +76,13 @@ var (
|
||||
Help: "Counter of DoH responses per server and http status code.",
|
||||
}, []string{"server", "status"})
|
||||
|
||||
HTTPS3ResponsesCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "https3_responses_total",
|
||||
Help: "Counter of DoH3 responses per server and http status code.",
|
||||
}, []string{"server", "status"})
|
||||
|
||||
QUICResponsesCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
|
||||
@@ -27,14 +27,17 @@ func Transport(s string) (trans string, addr string) {
|
||||
s = s[len(transport.GRPC+"://"):]
|
||||
return transport.GRPC, s
|
||||
|
||||
case strings.HasPrefix(s, transport.HTTPS3+"://"):
|
||||
s = s[len(transport.HTTPS3+"://"):]
|
||||
return transport.HTTPS3, s
|
||||
|
||||
case strings.HasPrefix(s, transport.HTTPS+"://"):
|
||||
s = s[len(transport.HTTPS+"://"):]
|
||||
|
||||
return transport.HTTPS, s
|
||||
|
||||
case strings.HasPrefix(s, transport.UNIX+"://"):
|
||||
s = s[len(transport.UNIX+"://"):]
|
||||
return transport.UNIX, s
|
||||
}
|
||||
|
||||
return transport.DNS, s
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
QUIC = "quic"
|
||||
GRPC = "grpc"
|
||||
HTTPS = "https"
|
||||
HTTPS3 = "https3"
|
||||
UNIX = "unix"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user