diff --git a/.github/workflows/go.test.yml b/.github/workflows/go.test.yml index 5b2158032..67f53fdc5 100644 --- a/.github/workflows/go.test.yml +++ b/.github/workflows/go.test.yml @@ -32,6 +32,12 @@ jobs: ( cd core; 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: name: Test Windows runs-on: windows-latest @@ -57,6 +63,12 @@ jobs: ( cd core; 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: name: Test Plugins strategy: diff --git a/core/dnsserver/directives.go b/core/dnsserver/directives.go new file mode 100644 index 000000000..f33390551 --- /dev/null +++ b/core/dnsserver/directives.go @@ -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 +} diff --git a/core/dnsserver/directives_test.go b/core/dnsserver/directives_test.go new file mode 100644 index 000000000..db1cca791 --- /dev/null +++ b/core/dnsserver/directives_test.go @@ -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) + } +} diff --git a/core/dnsserver/embedding_test.go b/core/dnsserver/embedding_test.go new file mode 100644 index 000000000..f0a4404ad --- /dev/null +++ b/core/dnsserver/embedding_test.go @@ -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() +} diff --git a/core/dnsserver/example_test.go b/core/dnsserver/example_test.go index ae0763b07..707b4da49 100644 --- a/core/dnsserver/example_test.go +++ b/core/dnsserver/example_test.go @@ -12,6 +12,27 @@ import ( "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() { oldDirectives := dnsserver.Directives oldCaddyQuiet := caddy.Quiet @@ -25,6 +46,9 @@ func Example_embedding() { // Import only the plugins the host needs and set their execution order // before starting the first server. dnsserver.Directives = []string{"bind", "whoami"} + if err := dnsserver.Register(); err != nil { + panic(err) + } caddy.Quiet = true dnsserver.Quiet = true diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go index bdcaf9fe7..15a4e2d33 100644 --- a/core/dnsserver/register.go +++ b/core/dnsserver/register.go @@ -3,6 +3,8 @@ package dnsserver import ( "fmt" "net" + "slices" + "sync" "time" "github.com/coredns/caddy" @@ -16,7 +18,22 @@ import ( 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{ Directives: func() []string { return Directives }, DefaultInput: func() caddy.Input { @@ -28,7 +45,8 @@ func init() { }, NewContext: newContext, }) -} + return nil +}) func newContext(_i *caddy.Instance) caddy.Context { return &dnsContext{keysToConfigs: make(map[string]*Config)} diff --git a/core/dnsserver/register_auto.go b/core/dnsserver/register_auto.go new file mode 100644 index 000000000..3fd03e3d6 --- /dev/null +++ b/core/dnsserver/register_auto.go @@ -0,0 +1,9 @@ +//go:build !coredns_manual_registration + +package dnsserver + +func init() { + if err := Register(); err != nil { + panic(err) + } +} diff --git a/core/dnsserver/register_auto_test.go b/core/dnsserver/register_auto_test.go new file mode 100644 index 000000000..f47ea7a2e --- /dev/null +++ b/core/dnsserver/register_auto_test.go @@ -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") + } +} diff --git a/core/dnsserver/register_manual_test.go b/core/dnsserver/register_manual_test.go new file mode 100644 index 000000000..ffca90c14 --- /dev/null +++ b/core/dnsserver/register_manual_test.go @@ -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) +} diff --git a/core/dnsserver/server.go b/core/dnsserver/server.go index 32c405621..9bb1bfaef 100644 --- a/core/dnsserver/server.go +++ b/core/dnsserver/server.go @@ -1,15 +1,44 @@ // Package dnsserver implements CoreDNS as a Caddy server type. // -// Importing this package registers the "dns" server type with Caddy. Programs -// embedding CoreDNS can import only the plugins they need, set Directives before -// starting a server, and pass an in-memory Corefile to [caddy.Start]. They should -// not call coremain.Run, which provides the command-line program behavior such -// as flag parsing, signal handling, and blocking until shutdown. +// By default, importing this package registers the "dns" server type with Caddy. +// Programs embedding CoreDNS can import only the plugins they need, call +// [SetDirectives] before starting a server, and pass an in-memory Corefile to +// [caddy.Start]. They should not import coremain or the generated all-plugin +// 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 // 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 // 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 import ( diff --git a/coremain/run.go b/coremain/run.go index 93050c07f..57778c429 100644 --- a/coremain/run.go +++ b/coremain/run.go @@ -17,6 +17,9 @@ import ( ) func init() { + if err := dnsserver.Register(); err != nil { + panic(err) + } caddy.DefaultConfigFile = "Corefile" caddy.Quiet = true // don't show init stuff from caddy setVersion() diff --git a/coremain/run_test.go b/coremain/run_test.go index 4df352d2d..6d377212c 100644 --- a/coremain/run_test.go +++ b/coremain/run_test.go @@ -7,11 +7,18 @@ import ( "os" "path/filepath" "runtime" + "slices" "testing" "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) { tests := []struct { name string