mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 03:55:21 -04:00
core/dnsserver: support explicit registration for embedded hosts (#8525)
* core/dnsserver: test host-plugin embedding with forward Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * core/dnsserver: add a directive setter for embedded hosts Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * core/dnsserver: support explicit server registration for embedded hosts Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> --------- Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
12
.github/workflows/go.test.yml
vendored
12
.github/workflows/go.test.yml
vendored
@@ -32,6 +32,12 @@ jobs:
|
|||||||
( cd core; go test -race ./... )
|
( cd core; go test -race ./... )
|
||||||
( cd coremain; go test -race ./... )
|
( cd coremain; go test -race ./... )
|
||||||
|
|
||||||
|
- name: Build manual-registration mode
|
||||||
|
run: go build -tags=coredns_manual_registration ./...
|
||||||
|
|
||||||
|
- name: Test manual-registration mode
|
||||||
|
run: go test -race -tags=coredns_manual_registration ./core/dnsserver ./coremain
|
||||||
|
|
||||||
test-windows:
|
test-windows:
|
||||||
name: Test Windows
|
name: Test Windows
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@@ -57,6 +63,12 @@ jobs:
|
|||||||
( cd core; go test -race ./... )
|
( cd core; go test -race ./... )
|
||||||
( cd coremain; go test -race ./... )
|
( cd coremain; go test -race ./... )
|
||||||
|
|
||||||
|
- name: Build manual-registration mode
|
||||||
|
run: go build -tags=coredns_manual_registration ./...
|
||||||
|
|
||||||
|
- name: Test manual-registration mode
|
||||||
|
run: go test -race -tags=coredns_manual_registration ./core/dnsserver ./coremain
|
||||||
|
|
||||||
test-plugins:
|
test-plugins:
|
||||||
name: Test Plugins
|
name: Test Plugins
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
33
core/dnsserver/directives.go
Normal file
33
core/dnsserver/directives.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetDirectives replaces [Directives] with a copy of directives, in execution
|
||||||
|
// order. Names must be nonempty and unique. On error, Directives is unchanged.
|
||||||
|
// An empty or nil list disables all directives.
|
||||||
|
//
|
||||||
|
// This selects directives but does not import or register plugins. A host may
|
||||||
|
// register its plugins after this call, before starting Caddy. Unregistered
|
||||||
|
// directives used in a Corefile are rejected by Caddy during startup.
|
||||||
|
//
|
||||||
|
// The list is process-wide, not per instance. Call SetDirectives before starting
|
||||||
|
// any servers, not while servers are running. The caller must serialize it with
|
||||||
|
// all other access to Directives, including startup and reload. It is not a
|
||||||
|
// runtime reconfiguration API.
|
||||||
|
func SetDirectives(directives []string) error {
|
||||||
|
seen := make(map[string]struct{}, len(directives))
|
||||||
|
for i, name := range directives {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("empty directive name at index %d", i)
|
||||||
|
}
|
||||||
|
if _, ok := seen[name]; ok {
|
||||||
|
return fmt.Errorf("duplicate directive %q", name)
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
}
|
||||||
|
Directives = slices.Clone(directives)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
73
core/dnsserver/directives_test.go
Normal file
73
core/dnsserver/directives_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coredns/caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetDirectives(t *testing.T) {
|
||||||
|
original := Directives
|
||||||
|
t.Cleanup(func() { Directives = original })
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
directives []string
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{name: "ordered", directives: []string{"bind", "test_host", "forward"}},
|
||||||
|
{name: "replacement", directives: []string{"whoami", "bind"}},
|
||||||
|
{name: "nil", directives: nil},
|
||||||
|
{name: "empty", directives: []string{}},
|
||||||
|
{name: "empty name", directives: []string{"bind", ""}, wantError: "empty directive name"},
|
||||||
|
{name: "duplicate", directives: []string{"bind", "forward", "bind"}, wantError: `duplicate directive "bind"`},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
Directives = []string{"test_previous"}
|
||||||
|
previous := Directives
|
||||||
|
input := slices.Clone(tc.directives)
|
||||||
|
err := SetDirectives(input)
|
||||||
|
if tc.wantError != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
|
||||||
|
t.Fatalf("SetDirectives(%v) = %v, want %q", input, err, tc.wantError)
|
||||||
|
}
|
||||||
|
if !slices.Equal(Directives, previous) || &Directives[0] != &previous[0] {
|
||||||
|
t.Fatalf("invalid list changed Directives: got %v, want %v", Directives, previous)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := caddy.ValidDirectives("dns"); !slices.Equal(got, tc.directives) {
|
||||||
|
t.Fatalf("registered server directives = %v, want %v", got, tc.directives)
|
||||||
|
}
|
||||||
|
if len(input) > 0 {
|
||||||
|
input[0] = "test_mutated"
|
||||||
|
if !slices.Equal(Directives, tc.directives) {
|
||||||
|
t.Fatalf("caller mutation changed Directives: %v", Directives)
|
||||||
|
}
|
||||||
|
input[0] = tc.directives[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !slices.Equal(input, tc.directives) {
|
||||||
|
t.Fatalf("SetDirectives changed the input: got %v, want %v", input, tc.directives)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetDirectivesCopiesCurrentList(t *testing.T) {
|
||||||
|
original := Directives
|
||||||
|
t.Cleanup(func() { Directives = original })
|
||||||
|
Directives = []string{"bind", "test_host", "forward"}
|
||||||
|
previous := Directives
|
||||||
|
if err := SetDirectives(Directives[1:]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
previous[1] = "test_mutated"
|
||||||
|
if want := []string{"test_host", "forward"}; !slices.Equal(Directives, want) {
|
||||||
|
t.Fatalf("old list mutation changed Directives: got %v, want %v", Directives, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
340
core/dnsserver/embedding_test.go
Normal file
340
core/dnsserver/embedding_test.go
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
package dnsserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coredns/caddy"
|
||||||
|
"github.com/coredns/coredns/core/dnsserver"
|
||||||
|
"github.com/coredns/coredns/plugin"
|
||||||
|
_ "github.com/coredns/coredns/plugin/bind"
|
||||||
|
_ "github.com/coredns/coredns/plugin/forward"
|
||||||
|
_ "github.com/coredns/coredns/plugin/whoami"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register once, but outside init, as an embedding host would do before Start.
|
||||||
|
var registerEmbeddingObserver sync.Once
|
||||||
|
|
||||||
|
func TestEmbeddingForward(t *testing.T) {
|
||||||
|
configureEmbedding(t)
|
||||||
|
upstream := startEmbeddingUpstream(t)
|
||||||
|
input := caddy.CaddyfileInput{
|
||||||
|
Filepath: "Corefile",
|
||||||
|
Contents: fmt.Appendf(nil, `.:0 {
|
||||||
|
bind 127.0.0.1
|
||||||
|
forward . %s
|
||||||
|
test_observe
|
||||||
|
}
|
||||||
|
`, upstream),
|
||||||
|
ServerTypeName: "dns",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two live instances exercise host-owned plugin state and independent shutdown.
|
||||||
|
first, firstObserver, stopFirst := startEmbeddedForwarder(t, input)
|
||||||
|
second, secondObserver, _ := startEmbeddedForwarder(t, input)
|
||||||
|
for _, instance := range []*caddy.Instance{first, second} {
|
||||||
|
instance.StorageMu.RLock()
|
||||||
|
observer := instance.Storage[embeddingObserverKey{}].(*embeddingObserver)
|
||||||
|
instance.StorageMu.RUnlock()
|
||||||
|
for _, network := range []string{"udp", "tcp"} {
|
||||||
|
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA} {
|
||||||
|
checkEmbeddedQuery(t, instance, observer, network, qtype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopFirst()
|
||||||
|
if firstObserver.stops != 1 || secondObserver.stops != 0 {
|
||||||
|
t.Fatalf("shutdown callbacks ran for the wrong instances: first=%d second=%d", firstObserver.stops, secondObserver.stops)
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", first.Servers()[0].Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("embedded TCP listener was not released: %v", err)
|
||||||
|
}
|
||||||
|
listener.Close()
|
||||||
|
packet, err := net.ListenPacket("udp", first.Servers()[0].LocalAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("embedded UDP listener was not released: %v", err)
|
||||||
|
}
|
||||||
|
packet.Close()
|
||||||
|
checkEmbeddedQuery(t, second, secondObserver, "udp", dns.TypeA)
|
||||||
|
checkEmbeddedQuery(t, second, secondObserver, "tcp", dns.TypeAAAA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbeddingInvalidPlugin(t *testing.T) {
|
||||||
|
configureEmbedding(t)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
directives []string
|
||||||
|
config string
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "imported but not enabled",
|
||||||
|
directives: []string{"bind", "test_observe", "forward"},
|
||||||
|
config: ".:0 {\nbind 127.0.0.1\nwhoami\n}\n",
|
||||||
|
wantError: "Unknown directive 'whoami'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enabled but not registered",
|
||||||
|
directives: []string{"bind", "test_missing"},
|
||||||
|
config: ".:0 {\nbind 127.0.0.1\ntest_missing\n}\n",
|
||||||
|
wantError: "no action found for directive 'test_missing'",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if err := dnsserver.SetDirectives(tc.directives); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
instance, err := caddy.Start(caddy.CaddyfileInput{
|
||||||
|
Filepath: "Corefile",
|
||||||
|
Contents: []byte(tc.config),
|
||||||
|
ServerTypeName: "dns",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
stopEmbeddedInstance(t, instance)
|
||||||
|
t.Fatal("expected a configuration error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.wantError) {
|
||||||
|
t.Fatalf("expected %q, got %v", tc.wantError, err)
|
||||||
|
}
|
||||||
|
if len(instance.Servers()) != 0 {
|
||||||
|
t.Fatal("invalid configuration opened listeners")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbeddingDoesNotRegisterCLIFlags(t *testing.T) {
|
||||||
|
// Check the original process FlagSet, not an empty replacement which would
|
||||||
|
// hide flags registered by imports. The host must not import coremain.
|
||||||
|
for _, name := range []string{"conf", "dns.port", "p", "pidfile", "plugins", "version", "quiet"} {
|
||||||
|
if flag.Lookup(name) != nil {
|
||||||
|
t.Errorf("embedding imports registered the CLI flag %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureEmbedding(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
oldDirectives, oldCaddyQuiet, oldDNSQuiet := dnsserver.Directives, caddy.Quiet, dnsserver.Quiet
|
||||||
|
t.Cleanup(func() {
|
||||||
|
dnsserver.Directives, caddy.Quiet, dnsserver.Quiet = oldDirectives, oldCaddyQuiet, oldDNSQuiet
|
||||||
|
})
|
||||||
|
directives := []string{"bind", "test_observe", "forward"}
|
||||||
|
if err := dnsserver.SetDirectives(directives); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// A host can reuse its input and register a selected plugin after setting
|
||||||
|
// the execution order, as long as both happen before startup.
|
||||||
|
directives[1] = "test_missing"
|
||||||
|
registerEmbeddingObserver.Do(func() {
|
||||||
|
plugin.Register("test_observe", setupEmbeddingObserver)
|
||||||
|
})
|
||||||
|
caddy.Quiet, dnsserver.Quiet = true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type embeddingObserverKey struct{}
|
||||||
|
|
||||||
|
type embeddingObservation struct {
|
||||||
|
question dns.Question
|
||||||
|
response *dns.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
type embeddingObserver struct {
|
||||||
|
next plugin.Handler
|
||||||
|
config *dnsserver.Config
|
||||||
|
observations chan embeddingObservation
|
||||||
|
starts int
|
||||||
|
stops int
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupEmbeddingObserver(c *caddy.Controller) error {
|
||||||
|
for c.Next() {
|
||||||
|
if len(c.RemainingArgs()) != 0 {
|
||||||
|
return c.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
observer := &embeddingObserver{
|
||||||
|
config: dnsserver.GetConfig(c),
|
||||||
|
observations: make(chan embeddingObservation, 1),
|
||||||
|
}
|
||||||
|
observer.config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||||
|
observer.next = next
|
||||||
|
return observer
|
||||||
|
})
|
||||||
|
c.OnStartup(func() error { observer.starts++; return nil })
|
||||||
|
c.OnShutdown(func() error { observer.stops++; return nil })
|
||||||
|
c.Set(embeddingObserverKey{}, observer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *embeddingObserver) Name() string { return "test_observe" }
|
||||||
|
|
||||||
|
func (o *embeddingObserver) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||||
|
writer := &embeddingResponseWriter{ResponseWriter: w}
|
||||||
|
rcode, err := plugin.NextOrFailure(o.Name(), o.next, ctx, writer, r)
|
||||||
|
o.observations <- embeddingObservation{question: r.Question[0], response: writer.response}
|
||||||
|
return rcode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type embeddingResponseWriter struct {
|
||||||
|
dns.ResponseWriter
|
||||||
|
response *dns.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *embeddingResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||||
|
w.response = m.Copy()
|
||||||
|
return w.ResponseWriter.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startEmbeddedForwarder(t *testing.T, input caddy.CaddyfileInput) (*caddy.Instance, *embeddingObserver, func()) {
|
||||||
|
t.Helper()
|
||||||
|
instance, err := caddy.Start(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stop := sync.OnceFunc(func() { stopEmbeddedInstance(t, instance) })
|
||||||
|
t.Cleanup(stop)
|
||||||
|
instance.StorageMu.RLock()
|
||||||
|
observer, ok := instance.Storage[embeddingObserverKey{}].(*embeddingObserver)
|
||||||
|
instance.StorageMu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("custom plugin was not set up")
|
||||||
|
}
|
||||||
|
if observer.starts != 1 {
|
||||||
|
t.Fatalf("startup callbacks: got %d, want 1", observer.starts)
|
||||||
|
}
|
||||||
|
handlers := observer.config.Handlers()
|
||||||
|
names := make([]string, 0, len(handlers))
|
||||||
|
for _, handler := range handlers {
|
||||||
|
names = append(names, handler.Name())
|
||||||
|
}
|
||||||
|
if !slices.Equal(names, []string{"test_observe", "forward"}) {
|
||||||
|
t.Fatalf("handler order: got %v, want [test_observe forward]", names)
|
||||||
|
}
|
||||||
|
return instance, observer, stop
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopEmbeddedInstance(t *testing.T, instance *caddy.Instance) {
|
||||||
|
t.Helper()
|
||||||
|
shutdownErr := errors.Join(instance.ShutdownCallbacks()...)
|
||||||
|
stopErr := instance.Stop()
|
||||||
|
instance.Wait()
|
||||||
|
if err := errors.Join(shutdownErr, stopErr); err != nil {
|
||||||
|
t.Errorf("stop embedded instance: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEmbeddedQuery(t *testing.T, instance *caddy.Instance, observer *embeddingObserver, network string, qtype uint16) {
|
||||||
|
t.Helper()
|
||||||
|
server := instance.Servers()[0]
|
||||||
|
addr := server.LocalAddr()
|
||||||
|
if network == "tcp" {
|
||||||
|
addr = server.Addr()
|
||||||
|
}
|
||||||
|
query := new(dns.Msg)
|
||||||
|
query.SetQuestion("embedded.example.", qtype)
|
||||||
|
client := &dns.Client{Net: network, Timeout: 2 * time.Second}
|
||||||
|
response, _, err := client.Exchange(query, addr.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantAnswer := "embedded.example.\t60\tIN\tA\t192.0.2.53"
|
||||||
|
if qtype == dns.TypeAAAA {
|
||||||
|
wantAnswer = "embedded.example.\t60\tIN\tAAAA\t2001:db8::53"
|
||||||
|
}
|
||||||
|
if response.Rcode != dns.RcodeSuccess || len(response.Answer) != 1 || response.Answer[0].String() != wantAnswer {
|
||||||
|
t.Fatalf("unexpected forwarded response: %v", response)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case observed := <-observer.observations:
|
||||||
|
if observed.question != query.Question[0] || observed.response == nil ||
|
||||||
|
observed.response.Rcode != response.Rcode || len(observed.response.Answer) != 1 ||
|
||||||
|
observed.response.Answer[0].String() != wantAnswer {
|
||||||
|
t.Fatalf("custom plugin did not observe the question and answer: %+v", observed)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("custom plugin did not observe the forwarded query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startEmbeddingUpstream(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
var listener net.Listener
|
||||||
|
var packet net.PacketConn
|
||||||
|
var err error
|
||||||
|
// A free TCP port may already be in use by UDP. Reserve both before serving.
|
||||||
|
for range 5 {
|
||||||
|
listener, err = net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
packet, err = net.ListenPacket("udp", listener.Addr().String())
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
listener.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
listener.Close()
|
||||||
|
packet.Close()
|
||||||
|
})
|
||||||
|
handler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
response := new(dns.Msg)
|
||||||
|
response.SetReply(r)
|
||||||
|
question := r.Question[0]
|
||||||
|
header := dns.RR_Header{Name: question.Name, Rrtype: question.Qtype, Class: dns.ClassINET, Ttl: 60}
|
||||||
|
switch question.Qtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
response.Answer = []dns.RR{&dns.A{Hdr: header, A: net.ParseIP("192.0.2.53")}}
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
response.Answer = []dns.RR{&dns.AAAA{Hdr: header, AAAA: net.ParseIP("2001:db8::53")}}
|
||||||
|
}
|
||||||
|
if err := w.WriteMsg(response); err != nil {
|
||||||
|
t.Errorf("upstream response: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for _, server := range []*dns.Server{
|
||||||
|
{Net: "tcp", Listener: listener, Handler: handler},
|
||||||
|
{Net: "udp", PacketConn: packet, Handler: handler},
|
||||||
|
} {
|
||||||
|
ready := make(chan struct{})
|
||||||
|
done := make(chan error, 1)
|
||||||
|
server.NotifyStartedFunc = func() { close(ready) }
|
||||||
|
go func() { done <- server.ActivateAndServe() }()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(t.Context()), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := server.ShutdownContext(ctx); err != nil {
|
||||||
|
t.Errorf("stop upstream: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("serve upstream: %v", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Error("upstream did not stop")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select {
|
||||||
|
case <-ready:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("upstream did not start")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listener.Addr().String()
|
||||||
|
}
|
||||||
@@ -12,6 +12,27 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ExampleSetDirectives() {
|
||||||
|
oldDirectives := dnsserver.Directives
|
||||||
|
defer func() { dnsserver.Directives = oldDirectives }()
|
||||||
|
|
||||||
|
if err := dnsserver.SetDirectives([]string{"bind", "whoami"}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(dnsserver.Directives)
|
||||||
|
// Output: [bind whoami]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleRegister() {
|
||||||
|
// Required before caddy.Start in builds with coredns_manual_registration;
|
||||||
|
// harmless when the default import-time registration has already run.
|
||||||
|
if err := dnsserver.Register(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("registered")
|
||||||
|
// Output: registered
|
||||||
|
}
|
||||||
|
|
||||||
func Example_embedding() {
|
func Example_embedding() {
|
||||||
oldDirectives := dnsserver.Directives
|
oldDirectives := dnsserver.Directives
|
||||||
oldCaddyQuiet := caddy.Quiet
|
oldCaddyQuiet := caddy.Quiet
|
||||||
@@ -25,6 +46,9 @@ func Example_embedding() {
|
|||||||
// Import only the plugins the host needs and set their execution order
|
// Import only the plugins the host needs and set their execution order
|
||||||
// before starting the first server.
|
// before starting the first server.
|
||||||
dnsserver.Directives = []string{"bind", "whoami"}
|
dnsserver.Directives = []string{"bind", "whoami"}
|
||||||
|
if err := dnsserver.Register(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
caddy.Quiet = true
|
caddy.Quiet = true
|
||||||
dnsserver.Quiet = true
|
dnsserver.Quiet = true
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package dnsserver
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coredns/caddy"
|
"github.com/coredns/caddy"
|
||||||
@@ -16,7 +18,22 @@ import (
|
|||||||
|
|
||||||
const serverType = "dns"
|
const serverType = "dns"
|
||||||
|
|
||||||
func init() {
|
// Register registers the DNS server type with Caddy. Repeated calls return the
|
||||||
|
// result of the first call without registering again. An existing server type
|
||||||
|
// registered by another caller is left unchanged and causes an error.
|
||||||
|
//
|
||||||
|
// Default builds call Register automatically. When built with the
|
||||||
|
// coredns_manual_registration tag, an embedding host must call Register before
|
||||||
|
// starting Caddy. Register neither registers plugins nor starts listeners.
|
||||||
|
//
|
||||||
|
// Concurrent calls to Register are safe, but the first call must not run
|
||||||
|
// concurrently with other Caddy configuration or startup operations.
|
||||||
|
func Register() error { return registerServerType() }
|
||||||
|
|
||||||
|
var registerServerType = sync.OnceValue(func() error {
|
||||||
|
if slices.Contains(caddy.ListPlugins()["server_types"], serverType) {
|
||||||
|
return fmt.Errorf("dnsserver: server type %q already registered", serverType)
|
||||||
|
}
|
||||||
caddy.RegisterServerType(serverType, caddy.ServerType{
|
caddy.RegisterServerType(serverType, caddy.ServerType{
|
||||||
Directives: func() []string { return Directives },
|
Directives: func() []string { return Directives },
|
||||||
DefaultInput: func() caddy.Input {
|
DefaultInput: func() caddy.Input {
|
||||||
@@ -28,7 +45,8 @@ func init() {
|
|||||||
},
|
},
|
||||||
NewContext: newContext,
|
NewContext: newContext,
|
||||||
})
|
})
|
||||||
}
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
func newContext(_i *caddy.Instance) caddy.Context {
|
func newContext(_i *caddy.Instance) caddy.Context {
|
||||||
return &dnsContext{keysToConfigs: make(map[string]*Config)}
|
return &dnsContext{keysToConfigs: make(map[string]*Config)}
|
||||||
|
|||||||
9
core/dnsserver/register_auto.go
Normal file
9
core/dnsserver/register_auto.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !coredns_manual_registration
|
||||||
|
|
||||||
|
package dnsserver
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := Register(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
core/dnsserver/register_auto_test.go
Normal file
19
core/dnsserver/register_auto_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build !coredns_manual_registration
|
||||||
|
|
||||||
|
package dnsserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coredns/caddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Capture this before any test can explicitly call Register.
|
||||||
|
var dnsRegisteredOnImport = slices.Contains(caddy.ListPlugins()["server_types"], "dns")
|
||||||
|
|
||||||
|
func TestAutomaticRegistration(t *testing.T) {
|
||||||
|
if !dnsRegisteredOnImport {
|
||||||
|
t.Fatal("default builds must register the DNS server type on import")
|
||||||
|
}
|
||||||
|
}
|
||||||
125
core/dnsserver/register_manual_test.go
Normal file
125
core/dnsserver/register_manual_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
//go:build coredns_manual_registration
|
||||||
|
|
||||||
|
package dnsserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coredns/caddy"
|
||||||
|
"github.com/coredns/coredns/core/dnsserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const manualRegistrationHelper = "COREDNS_TEST_MANUAL_REGISTRATION"
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Most package tests need a registered server type. Fresh subprocesses skip
|
||||||
|
// this setup so their first Register call exercises the import-time state.
|
||||||
|
if os.Getenv(manualRegistrationHelper) == "" {
|
||||||
|
if err := dnsserver.Register(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManualRegistrationImport(t *testing.T) {
|
||||||
|
if scenario := os.Getenv(manualRegistrationHelper); scenario != "" {
|
||||||
|
checkManualRegistration(t, scenario)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
executable, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, scenario := range []string{"start", "concurrent", "conflict"} {
|
||||||
|
t.Run(scenario, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, executable, "-test.run=^TestManualRegistrationImport$", "-test.count=1")
|
||||||
|
cmd.Env = append(os.Environ(), manualRegistrationHelper+"="+scenario)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("fresh-process registration check: %v\n%s", err, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkManualRegistration(t *testing.T, scenario string) {
|
||||||
|
t.Helper()
|
||||||
|
if slices.Contains(caddy.ListPlugins()["server_types"], "dns") {
|
||||||
|
t.Fatal("importing dnsserver registered the DNS server type in manual mode")
|
||||||
|
}
|
||||||
|
TestEmbeddingDoesNotRegisterCLIFlags(t)
|
||||||
|
configureEmbedding(t)
|
||||||
|
if slices.Contains(caddy.ListPlugins()["server_types"], "dns") {
|
||||||
|
t.Fatal("selecting directives or registering a host plugin registered the server type")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch scenario {
|
||||||
|
case "start":
|
||||||
|
instance, err := caddy.Start(caddy.CaddyfileInput{
|
||||||
|
Filepath: "Corefile",
|
||||||
|
Contents: []byte(".:0 {\nbind 127.0.0.1\n}\n"),
|
||||||
|
ServerTypeName: "dns",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
stopEmbeddedInstance(t, instance)
|
||||||
|
t.Fatal("starting without Register succeeded")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "no server types plugged in") {
|
||||||
|
t.Fatalf("unexpected unregistered startup error: %v", err)
|
||||||
|
}
|
||||||
|
if len(instance.Servers()) != 0 || len(caddy.Instances()) != 0 {
|
||||||
|
t.Fatal("failed startup left servers or instances behind")
|
||||||
|
}
|
||||||
|
case "concurrent":
|
||||||
|
const callers = 32
|
||||||
|
errs := make(chan error, callers)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for range callers {
|
||||||
|
wg.Go(func() { errs <- dnsserver.Register() })
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "conflict":
|
||||||
|
want := []string{"test_foreign_server"}
|
||||||
|
caddy.RegisterServerType("dns", caddy.ServerType{
|
||||||
|
Directives: func() []string { return want },
|
||||||
|
})
|
||||||
|
for range 2 {
|
||||||
|
if err := dnsserver.Register(); err == nil || !strings.Contains(err.Error(), `server type "dns" already registered`) {
|
||||||
|
t.Fatalf("expected registration conflict, got %v", err)
|
||||||
|
}
|
||||||
|
if got := caddy.ValidDirectives("dns"); !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("conflicting server type changed: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t.Fatalf("unknown registration scenario %q", scenario)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range 2 {
|
||||||
|
if err := dnsserver.Register(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := caddy.ValidDirectives("dns"); !slices.Equal(got, dnsserver.Directives) {
|
||||||
|
t.Fatalf("registration changed the host's directive list: %v", got)
|
||||||
|
}
|
||||||
|
t.Run("forward", TestEmbeddingForward)
|
||||||
|
}
|
||||||
@@ -1,15 +1,44 @@
|
|||||||
// Package dnsserver implements CoreDNS as a Caddy server type.
|
// Package dnsserver implements CoreDNS as a Caddy server type.
|
||||||
//
|
//
|
||||||
// Importing this package registers the "dns" server type with Caddy. Programs
|
// By default, importing this package registers the "dns" server type with Caddy.
|
||||||
// embedding CoreDNS can import only the plugins they need, set Directives before
|
// Programs embedding CoreDNS can import only the plugins they need, call
|
||||||
// starting a server, and pass an in-memory Corefile to [caddy.Start]. They should
|
// [SetDirectives] before starting a server, and pass an in-memory Corefile to
|
||||||
// not call coremain.Run, which provides the command-line program behavior such
|
// [caddy.Start]. They should not import coremain or the generated all-plugin
|
||||||
// as flag parsing, signal handling, and blocking until shutdown.
|
// bundle: coremain provides command-line behavior such as flag registration,
|
||||||
|
// signal handling, and blocking until shutdown, and registers the server type.
|
||||||
// Before stopping an embedded instance, run its shutdown callbacks so that
|
// Before stopping an embedded instance, run its shutdown callbacks so that
|
||||||
// plugins can release resources.
|
// plugins can release resources.
|
||||||
//
|
//
|
||||||
|
// A host can register a custom directive with [plugin.Register] before starting
|
||||||
|
// Caddy; it does not need to rebuild CoreDNS or modify plugin.cfg. Include the
|
||||||
|
// directive in the list passed to SetDirectives at the desired execution
|
||||||
|
// position, and use [GetConfig] and [Config.AddPlugin] in its setup function to
|
||||||
|
// add the handler. The setup function can register startup and shutdown callbacks
|
||||||
|
// on the Caddy controller.
|
||||||
|
// Directives determines execution order, not the order in the Corefile.
|
||||||
|
// Each directive must be registered only once per process.
|
||||||
|
//
|
||||||
|
// SetDirectives copies the supplied list and rejects empty or duplicate names.
|
||||||
|
// Direct assignment to Directives remains supported for existing callers.
|
||||||
|
// Neither entry point imports plugins or registers them on the host's behalf.
|
||||||
|
//
|
||||||
// Directives and Caddy's plugin registry are process-wide. Configure them
|
// Directives and Caddy's plugin registry are process-wide. Configure them
|
||||||
// before starting any servers and do not mutate them while servers are running.
|
// before starting any servers and do not mutate them while servers are running.
|
||||||
|
// Automatic server-type registration is retained for existing embedding users;
|
||||||
|
// it does not start listeners or prevent the host from selecting directives.
|
||||||
|
//
|
||||||
|
// To control when the DNS server type is registered, build the host with
|
||||||
|
// -tags=coredns_manual_registration. This excludes this package's registration
|
||||||
|
// init function. After selecting directives and registering host plugins, call
|
||||||
|
// [Register] before caddy.Start. Register is idempotent and also works in default
|
||||||
|
// builds. It returns an error if another caller already registered a DNS server
|
||||||
|
// type, leaving that registration unchanged.
|
||||||
|
//
|
||||||
|
// The build tag does not disable initialization in Caddy or individual plugins,
|
||||||
|
// or make their registries instance-local. Selected plugins must not import
|
||||||
|
// coremain, directly or transitively, to avoid its command-line initialization
|
||||||
|
// and server-type registration. The CoreDNS command-line program explicitly
|
||||||
|
// registers the server type in both build modes.
|
||||||
package dnsserver
|
package dnsserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
if err := dnsserver.Register(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
caddy.DefaultConfigFile = "Corefile"
|
caddy.DefaultConfigFile = "Corefile"
|
||||||
caddy.Quiet = true // don't show init stuff from caddy
|
caddy.Quiet = true // don't show init stuff from caddy
|
||||||
setVersion()
|
setVersion()
|
||||||
|
|||||||
@@ -7,11 +7,18 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coredns/caddy"
|
"github.com/coredns/caddy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestServerTypeRegistration(t *testing.T) {
|
||||||
|
if !slices.Contains(caddy.ListPlugins()["server_types"], serverType) {
|
||||||
|
t.Fatal("coremain must register the DNS server type in both build modes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfLoader(t *testing.T) {
|
func TestConfLoader(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user