Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions core/dnsserver/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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
Expand Down
194 changes: 194 additions & 0 deletions core/dnsserver/server_https3.go
Original file line number Diff line number Diff line change
@@ -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 changes: 62 additions & 0 deletions core/dnsserver/server_https3_test.go
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,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.3 // indirect
github.com/spf13/pflag v1.0.6 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,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=
Expand Down
7 changes: 7 additions & 0 deletions plugin/metrics/vars/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions plugin/pkg/parse/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
13 changes: 7 additions & 6 deletions plugin/pkg/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package transport

// These transports are supported by CoreDNS.
const (
DNS = "dns"
TLS = "tls"
QUIC = "quic"
GRPC = "grpc"
HTTPS = "https"
UNIX = "unix"
DNS = "dns"
TLS = "tls"
QUIC = "quic"
GRPC = "grpc"
HTTPS = "https"
HTTPS3 = "https3"
UNIX = "unix"
)

// Port numbers for the various transports.
Expand Down