TUN-9858: Remove proxy-dns feature from cloudflared
Remove the DNS over HTTPS (DoH) proxy feature built on CoreDNS due to security vulnerabilities (GO-2025-3942, GO-2026-4289). This removes: - Standalone proxy-dns command (cloudflared proxy-dns) - Tunnel subcommand (cloudflared tunnel proxy-dns) - Proxy-dns flags for tunnel run (--proxy-dns, --proxy-dns-port, etc.) - Config file resolver section support - tunneldns/ package (CoreDNS-based implementation) - Related component tests BREAKING CHANGE: The proxy-dns feature is no longer available. Users should migrate to alternative DNS over HTTPS solutions.
This commit is contained in:
parent
d6cb78aeb4
commit
9388e7f48c
|
|
@ -32,7 +32,7 @@ echo "====================================="
|
|||
CLEAN_IGNORES=$(grep -v '^\s*#' "$IGNORE_FILE" | cut -d'#' -f1 | sed 's/ //g' | sort -u || true)
|
||||
|
||||
# Filter out the ignored vulnerabilities.
|
||||
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability')
|
||||
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability' || true)
|
||||
|
||||
# If the list of ignored vulnerabilities is not empty, filter them out.
|
||||
if [ -n "$CLEAN_IGNORES" ]; then
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line.
|
||||
# You can also add comments on the same line after the ID.
|
||||
GO-2025-3942 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
|
||||
GO-2026-4289 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
|
||||
|
|
|
|||
11
CHANGES.md
11
CHANGES.md
|
|
@ -1,3 +1,14 @@
|
|||
## 2026.2.0
|
||||
### Breaking Change
|
||||
- Removes the `proxy-dns` feature from cloudflared. This feature allowed running a local DNS over HTTPS (DoH) proxy.
|
||||
Users who relied on this functionality should migrate to alternative solutions.
|
||||
|
||||
Removed commands and flags:
|
||||
- `cloudflared proxy-dns`
|
||||
- `cloudflared tunnel proxy-dns`
|
||||
- `--proxy-dns`, `--proxy-dns-port`, `--proxy-dns-address`, `--proxy-dns-upstream`, `--proxy-dns-max-upstream-conns`, `--proxy-dns-bootstrap`
|
||||
- `resolver` section in configuration file
|
||||
|
||||
## 2025.7.1
|
||||
### Notices
|
||||
- `cloudflared` will no longer officially support Debian and Ubuntu distros that reached end-of-life: `buster`, `bullseye`, `impish`, `trusty`.
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
const (
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
ResolverServiceType = "resolver"
|
||||
|
||||
LogFieldResolverAddress = "resolverAddress"
|
||||
LogFieldResolverPort = "resolverPort"
|
||||
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
|
||||
)
|
||||
|
||||
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
|
||||
// into a service model for the overwatch package.
|
||||
// it also holds a reference to the config object that represents its state
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
// this is just "resolver" since there can only be one DNS resolver running
|
||||
func (s *ResolverService) Name() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Type is used to identify what kind of overwatch service this is
|
||||
func (s *ResolverService) Type() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
|
||||
func (s *ResolverService) Hash() string {
|
||||
return s.resolver.Hash()
|
||||
}
|
||||
|
||||
// Shutdown stops the tunneldns listener
|
||||
func (s *ResolverService) Shutdown() {
|
||||
s.shutdown <- struct{}{}
|
||||
}
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ResolverService) Run() error {
|
||||
// create a listener
|
||||
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start the listener.
|
||||
readySignal := make(chan struct{})
|
||||
err = l.Start(readySignal)
|
||||
if err != nil {
|
||||
_ = l.Stop()
|
||||
return err
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
resolverLog := s.log.With().
|
||||
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
|
||||
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
|
||||
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
|
||||
Logger()
|
||||
|
||||
resolverLog.Info().Msg("Starting resolver")
|
||||
|
||||
// wait for shutdown signal
|
||||
<-s.shutdown
|
||||
resolverLog.Info().Msg("Shutting down resolver")
|
||||
return l.Stop()
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
|
||||
// it manages all the running services such as tunnels, forwarders, etc
|
||||
type AppService struct {
|
||||
configManager config.Manager
|
||||
serviceManager overwatch.Manager
|
||||
|
|
@ -73,14 +73,6 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
|
|||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
// handle resolver changes
|
||||
if c.Resolver.Enabled {
|
||||
service := NewResolverService(c.Resolver, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
|
||||
}
|
||||
|
||||
// TODO: TUN-1451 - tunnels
|
||||
|
||||
// remove any services that are no longer active
|
||||
|
|
|
|||
|
|
@ -111,9 +111,6 @@ const (
|
|||
// ICMPV6Src is the command line flag to set the source address and the interface name to send/receive ICMPv6 messages
|
||||
ICMPV6Src = "icmpv6-src"
|
||||
|
||||
// ProxyDns is the command line flag to run DNS server over HTTPS
|
||||
ProxyDns = "proxy-dns"
|
||||
|
||||
// Name is the command line to set the name of the tunnel
|
||||
Name = "name"
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ To determine if an update happened in a script, check for error code 11.`,
|
|||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, proxydns.Command(false))
|
||||
cmds = append(cmds, proxydns.Command()) // removed feature, only here for error message
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
cmds = append(cmds, tail.Command())
|
||||
return cmds
|
||||
|
|
|
|||
|
|
@ -1,115 +1,54 @@
|
|||
package proxydns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"errors"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
func Command(hidden bool) *cli.Command {
|
||||
const removedMessage = "dns-proxy feature is not supported, since version 2026.2.0"
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "proxy-dns",
|
||||
Action: cliutil.ConfiguredAction(Run),
|
||||
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
},
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
Value: 53,
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "bootstrap",
|
||||
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-upstream-conns",
|
||||
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
|
||||
Value: tunneldns.MaxUpstreamConnsDefault,
|
||||
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: hidden,
|
||||
Usage: removedMessage,
|
||||
SkipFlagParsing: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
err := errors.New(removedMessage)
|
||||
log.Err(err).Msg("DNS Proxy is no longer supported")
|
||||
|
||||
metricsListener, err := net.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, context.Background(), metrics.Config{}, log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
uint16(c.Int("port")),
|
||||
c.StringSlice("upstream"),
|
||||
c.StringSlice("bootstrap"),
|
||||
c.Int("max-upstream-conns"),
|
||||
log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to create the listeners")
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to start the server
|
||||
readySignal := make(chan struct{})
|
||||
err = listener.Start(readySignal)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to start the listeners")
|
||||
return listener.Stop()
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
// Wait for signal
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
<-signals
|
||||
|
||||
// Shut down server
|
||||
err = listener.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to stop")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Old flags used by the proxy-dns command, only kept to not break any script that might be setting these flags
|
||||
func ConfigureProxyDNSFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-dns",
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-port",
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proxy-dns-address",
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-upstream",
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-max-upstream-conns",
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-bootstrap",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import (
|
|||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelstate"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
|
@ -115,12 +114,6 @@ var (
|
|||
cfdflags.LogFile,
|
||||
cfdflags.LogDirectory,
|
||||
cfdflags.TraceOutput,
|
||||
cfdflags.ProxyDns,
|
||||
"proxy-dns-port",
|
||||
"proxy-dns-address",
|
||||
"proxy-dns-upstream",
|
||||
"proxy-dns-max-upstream-conns",
|
||||
"proxy-dns-bootstrap",
|
||||
cfdflags.IsAutoUpdated,
|
||||
cfdflags.Edge,
|
||||
cfdflags.Region,
|
||||
|
|
@ -181,8 +174,7 @@ func Commands() []*cli.Command {
|
|||
buildCleanupCommand(),
|
||||
buildTokenCommand(),
|
||||
buildDiagCommand(),
|
||||
// for compatibility, allow following as tunnel subcommands
|
||||
proxydns.Command(true),
|
||||
proxydns.Command(), // removed feature, only here for error message
|
||||
cliutil.RemovedCommand("db-connect"),
|
||||
}
|
||||
|
||||
|
|
@ -258,9 +250,8 @@ func TunnelCommand(c *cli.Context) error {
|
|||
|
||||
// Run a quick tunnel
|
||||
// A unauthenticated named tunnel hosted on <random>.<quick-tunnels-service>.com
|
||||
// We don't support running proxy-dns and a quick tunnel at the same time as the same process
|
||||
shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag)
|
||||
if !c.IsSet(cfdflags.ProxyDns) && c.String("quick-service") != "" && shouldRunQuickTunnel {
|
||||
if c.String("quick-service") != "" && shouldRunQuickTunnel {
|
||||
return RunQuickTunnel(sc)
|
||||
}
|
||||
|
||||
|
|
@ -274,16 +265,6 @@ func TunnelCommand(c *cli.Context) error {
|
|||
return errDeprecatedClassicTunnel
|
||||
}
|
||||
|
||||
if c.IsSet(cfdflags.ProxyDns) {
|
||||
if shouldRunQuickTunnel {
|
||||
return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported")
|
||||
}
|
||||
// NamedTunnelProperties are nil since proxy dns server does not need it.
|
||||
// This is supported for legacy reasons: dns proxy server is not a tunnel and ideally should
|
||||
// not run as part of cloudflared tunnel.
|
||||
return StartServer(sc.c, buildInfo, nil, sc.log)
|
||||
}
|
||||
|
||||
return errors.New(tunnelCmdErrorMessage)
|
||||
}
|
||||
|
||||
|
|
@ -393,24 +374,12 @@ func StartServer(
|
|||
|
||||
go waitForSignal(graceShutdownC, log)
|
||||
|
||||
if c.IsSet(cfdflags.ProxyDns) {
|
||||
dnsReadySignal := make(chan struct{})
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- runDNSProxyServer(c, dnsReadySignal, ctx.Done(), log)
|
||||
}()
|
||||
// Wait for proxy-dns to come up (if used)
|
||||
<-dnsReadySignal
|
||||
}
|
||||
|
||||
connectedSignal := signal.New(make(chan struct{}))
|
||||
go notifySystemd(connectedSignal)
|
||||
if c.IsSet("pidfile") {
|
||||
go writePidFile(connectedSignal, c.String("pidfile"), log)
|
||||
}
|
||||
|
||||
// update needs to be after DNS proxy is up to resolve equinox server address
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
|
@ -420,15 +389,8 @@ func StartServer(
|
|||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
|
||||
// Serve DNS proxy stand-alone if no tunnel type (quick, adhoc, named) is going to run
|
||||
if dnsProxyStandAlone(c, namedTunnel) {
|
||||
connectedSignal.Notify()
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
|
||||
}
|
||||
|
||||
if namedTunnel == nil {
|
||||
return fmt.Errorf("namedTunnel is nil outside of DNS proxy stand-alone mode")
|
||||
return fmt.Errorf("namedTunnel is nil")
|
||||
}
|
||||
|
||||
logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
|
@ -641,7 +603,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
|||
flags := configureCloudflaredFlags(shouldHide)
|
||||
flags = append(flags, configureProxyFlags(shouldHide)...)
|
||||
flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...)
|
||||
flags = append(flags, configureProxyDNSFlags(shouldHide)...)
|
||||
flags = append(flags, proxydns.ConfigureProxyDNSFlags(shouldHide)...) // removed feature, only kept to not break any script that might be setting these flags
|
||||
flags = append(flags, []cli.Flag{
|
||||
credentialsFileFlag,
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
|
|
@ -1172,57 +1134,6 @@ func sshFlags(shouldHide bool) []cli.Flag {
|
|||
}
|
||||
}
|
||||
|
||||
func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: cfdflags.ProxyDns,
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-port",
|
||||
Value: 53,
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proxy-dns-address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-max-upstream-conns",
|
||||
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
|
||||
Value: tunneldns.MaxUpstreamConnsDefault,
|
||||
Hidden: shouldHide,
|
||||
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-bootstrap",
|
||||
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice(
|
||||
"https://162.159.36.1/dns-query",
|
||||
"https://162.159.46.1/dns-query",
|
||||
"https://[2606:4700:4700::1111]/dns-query",
|
||||
"https://[2606:4700:4700::1001]/dns-query",
|
||||
),
|
||||
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logger) {
|
||||
for {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
|
|
|||
|
|
@ -111,13 +111,6 @@ func isSecretEnvVar(key string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
|
||||
return c.IsSet(flags.ProxyDns) &&
|
||||
!(c.IsSet(flags.Name) || // adhoc-named tunnel
|
||||
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
|
||||
namedTunnel != nil) // named tunnel
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
ctx context.Context,
|
||||
c *cli.Context,
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
}
|
||||
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
|
||||
if maxUpstreamConnections < 0 {
|
||||
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
|
||||
}
|
||||
|
||||
err = listener.Start(dnsReadySignal)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
_ = listener.Stop()
|
||||
log.Info().Msg("DNS server stopped")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import base64
|
|||
|
||||
from dataclasses import dataclass, InitVar
|
||||
|
||||
from constants import METRICS_PORT, PROXY_DNS_PORT
|
||||
from constants import METRICS_PORT
|
||||
|
||||
# frozen=True raises exception when assigning to fields. This emulates immutability
|
||||
|
||||
|
|
@ -99,10 +99,3 @@ class QuickTunnelConfig(BaseConfig):
|
|||
object.__setattr__(self, 'full_config',
|
||||
self.merge_config(additional_config))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProxyDnsConfig(BaseConfig):
|
||||
full_config = {
|
||||
"port": PROXY_DNS_PORT,
|
||||
"no-autoupdate": True,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@ from time import sleep
|
|||
import pytest
|
||||
import yaml
|
||||
|
||||
from config import NamedTunnelConfig, ProxyDnsConfig, QuickTunnelConfig
|
||||
from constants import BACKOFF_SECS, PROXY_DNS_PORT
|
||||
from config import NamedTunnelConfig, QuickTunnelConfig
|
||||
from constants import BACKOFF_SECS
|
||||
from util import LOGGER
|
||||
|
||||
|
||||
class CfdModes(Enum):
|
||||
NAMED = auto()
|
||||
QUICK = auto()
|
||||
PROXY_DNS = auto()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
|
@ -26,16 +25,7 @@ def component_tests_config():
|
|||
config = yaml.safe_load(stream)
|
||||
LOGGER.info(f"component tests base config {config}")
|
||||
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True, provide_ingress=True):
|
||||
if run_proxy_dns:
|
||||
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
|
||||
# So we run all tests with it.
|
||||
additional_config["proxy-dns"] = True
|
||||
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
|
||||
else:
|
||||
additional_config.pop("proxy-dns", None)
|
||||
additional_config.pop("proxy-dns-port", None)
|
||||
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, provide_ingress=True):
|
||||
# Allows the ingress rules to be omitted from the provided config
|
||||
ingress = []
|
||||
if provide_ingress:
|
||||
|
|
@ -51,8 +41,6 @@ def component_tests_config():
|
|||
credentials_file=config['credentials_file'],
|
||||
ingress=ingress,
|
||||
hostname=hostname)
|
||||
elif cfd_mode is CfdModes.PROXY_DNS:
|
||||
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
|
||||
elif cfd_mode is CfdModes.QUICK:
|
||||
return QuickTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'])
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ MAX_RETRIES = 5
|
|||
BACKOFF_SECS = 7
|
||||
MAX_LOG_LINES = 50
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class TestManagement:
|
|||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
|
@ -52,7 +52,7 @@ class TestManagement:
|
|||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
|
|
@ -73,7 +73,7 @@ class TestManagement:
|
|||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
|
|
@ -94,7 +94,7 @@ class TestManagement:
|
|||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import socket
|
||||
from time import sleep
|
||||
|
||||
import constants
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
|
||||
class TestProxyDns:
|
||||
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
|
||||
|
||||
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
|
||||
|
||||
|
||||
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
|
||||
expect_proxy_dns = run_proxy_dns
|
||||
expect_tunnel = False
|
||||
|
||||
if cfd_mode == CfdModes.NAMED:
|
||||
expect_tunnel = True
|
||||
pre_args = ["tunnel", "--ha-connections", "1"]
|
||||
args = ["run"]
|
||||
elif cfd_mode == CfdModes.PROXY_DNS:
|
||||
expect_proxy_dns = True
|
||||
pre_args = []
|
||||
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
|
||||
else:
|
||||
assert False, f"Unknown cfd_mode {cfd_mode}"
|
||||
|
||||
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
|
||||
if expect_tunnel:
|
||||
wait_tunnel_ready()
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
verify_proxy_dns(expect_proxy_dns)
|
||||
|
||||
|
||||
def verify_proxy_dns(should_be_running):
|
||||
# Wait for the Proxy DNS listener to come up.
|
||||
sleep(constants.BACKOFF_SECS)
|
||||
had_failure = False
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(('localhost', constants.PROXY_DNS_PORT))
|
||||
sock.send(b"anything")
|
||||
except:
|
||||
if should_be_running:
|
||||
assert False, "Expected Proxy DNS to be running, but it was not."
|
||||
had_failure = True
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if not should_be_running and not had_failure:
|
||||
assert False, "Proxy DNS should not have been running, but it was."
|
||||
|
|
@ -6,7 +6,7 @@ from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_u
|
|||
|
||||
class TestQuickTunnels:
|
||||
def test_quick_tunnel(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
|
|
@ -15,22 +15,10 @@ class TestQuickTunnels:
|
|||
send_requests(url, 3, True)
|
||||
|
||||
def test_quick_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
time.sleep(10)
|
||||
url = get_quicktunnel_url()
|
||||
send_requests(url+"/ready", 3, True)
|
||||
|
||||
def test_quick_tunnel_proxy_dns_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
|
||||
LOGGER.debug(config)
|
||||
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], expect_success=False)
|
||||
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `hello-world`"
|
||||
|
||||
def test_quick_tunnel_proxy_dns_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
|
||||
LOGGER.debug(config)
|
||||
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], expect_success=False)
|
||||
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `url`"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class TestTail:
|
|||
with the access token and start and stop streaming on-demand.
|
||||
"""
|
||||
print("test_start_stop_streaming")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
|
|
@ -38,7 +38,7 @@ class TestTail:
|
|||
Validates that a streaming logs connection will stream logs
|
||||
"""
|
||||
print("test_streaming_logs")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
|
|
@ -65,7 +65,7 @@ class TestTail:
|
|||
but not http when filters applied.
|
||||
"""
|
||||
print("test_streaming_logs_filters")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
|
|
@ -92,7 +92,7 @@ class TestTail:
|
|||
Validates that a streaming logs connection will stream logs with sampling.
|
||||
"""
|
||||
print("test_streaming_logs_sampling")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
|
|
@ -120,7 +120,7 @@ class TestTail:
|
|||
Validates that a streaming logs session can be overriden by the same actor
|
||||
"""
|
||||
print("test_streaming_logs_actor_override")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ class TestTunnel:
|
|||
'''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only'''
|
||||
|
||||
def test_tunnel_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
|
||||
def test_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
|
|
@ -29,7 +29,7 @@ class TestTunnel:
|
|||
Running a tunnel with no ingress rules provided from either config.yaml or CLI will still work but return 503
|
||||
for all incoming requests.
|
||||
'''
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ import (
|
|||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
// Forwarder represents a client side listener to forward traffic to the edge
|
||||
|
|
@ -26,23 +23,13 @@ type Tunnel struct {
|
|||
ProtocolType string `json:"type"`
|
||||
}
|
||||
|
||||
// DNSResolver represents a client side DNS resolver
|
||||
type DNSResolver struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Bootstraps []string `json:"bootstraps,omitempty"`
|
||||
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
|
||||
}
|
||||
|
||||
// Root is the base options to configure the service
|
||||
// Root is the base options to configure the service.
|
||||
type Root struct {
|
||||
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
|
||||
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
|
||||
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
|
||||
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
|
||||
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
|
||||
// `resolver` key is reserved for a removed feature (proxy-dns) and should not be used.
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
|
|
@ -55,60 +42,3 @@ func (f *Forwarder) Hash() string {
|
|||
_, _ = io.WriteString(h, f.Destination)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
func (r *DNSResolver) Hash() string {
|
||||
h := sha256.New()
|
||||
_, _ = io.WriteString(h, r.Address)
|
||||
_, _ = io.WriteString(h, strings.Join(r.Bootstraps, ","))
|
||||
_, _ = io.WriteString(h, strings.Join(r.Upstreams, ","))
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.Port))
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// EnabledOrDefault returns the enabled property
|
||||
func (r *DNSResolver) EnabledOrDefault() bool {
|
||||
return r.Enabled
|
||||
}
|
||||
|
||||
// AddressOrDefault returns the address or returns the default if empty
|
||||
func (r *DNSResolver) AddressOrDefault() string {
|
||||
if r.Address != "" {
|
||||
return r.Address
|
||||
}
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
// PortOrDefault return the port or returns the default if 0
|
||||
func (r *DNSResolver) PortOrDefault() uint16 {
|
||||
if r.Port > 0 {
|
||||
return r.Port
|
||||
}
|
||||
return 53
|
||||
}
|
||||
|
||||
// UpstreamsOrDefault returns the upstreams or returns the default if empty
|
||||
func (r *DNSResolver) UpstreamsOrDefault() []string {
|
||||
if len(r.Upstreams) > 0 {
|
||||
return r.Upstreams
|
||||
}
|
||||
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
|
||||
}
|
||||
|
||||
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
|
||||
func (r *DNSResolver) BootstrapsOrDefault() []string {
|
||||
if len(r.Bootstraps) > 0 {
|
||||
return r.Bootstraps
|
||||
}
|
||||
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
|
||||
}
|
||||
|
||||
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if negative
|
||||
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
|
||||
if r.MaxUpstreamConnections >= 0 {
|
||||
return r.MaxUpstreamConnections
|
||||
}
|
||||
return tunneldns.MaxUpstreamConnsDefault
|
||||
}
|
||||
|
|
|
|||
10
go.mod
10
go.mod
|
|
@ -3,7 +3,6 @@ module github.com/cloudflare/cloudflared
|
|||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/coredns/coredns v1.12.2
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||
|
|
@ -19,7 +18,6 @@ require (
|
|||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/miekg/dns v1.1.66
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
|
|
@ -50,18 +48,15 @@ require (
|
|||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
|
|
@ -69,24 +64,21 @@ require (
|
|||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
|
|
|
|||
27
go.sum
27
go.sum
|
|
@ -1,8 +1,6 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
|
||||
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
|
||||
|
|
@ -17,10 +15,6 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
|
|||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 h1:c+Epklw9xk6BZ1OFBPWLA2PcL8QalKvl3if8CP9x8uw=
|
||||
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
|
||||
github.com/coredns/coredns v1.12.2 h1:G4oDfi340zlVsriZ8nYiUemiQIew7nqOO+QPvPxIA4Y=
|
||||
github.com/coredns/coredns v1.12.2/go.mod h1:GFz31oVOfCyMArFoypfu1SoaFoNkbdh6lDxtF1B6vfU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
|
|
@ -43,8 +37,6 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
|
|||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
|
|
@ -95,7 +87,6 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
|
|||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
|
@ -109,7 +100,6 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
|||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34=
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
|
|
@ -117,8 +107,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
|||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
|
|
@ -144,10 +132,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -162,12 +146,10 @@ github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus
|
|||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
@ -206,8 +188,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
|
|
@ -257,7 +239,6 @@ golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
|||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
package tunneldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Upstream is a simplified interface for proxy destination
|
||||
type Upstream interface {
|
||||
Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
// ProxyPlugin is a simplified DNS proxy using a generic upstream interface
|
||||
type ProxyPlugin struct {
|
||||
Upstreams []Upstream
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// ServeDNS implements interface for CoreDNS plugin
|
||||
func (p ProxyPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
var reply *dns.Msg
|
||||
var backendErr error
|
||||
|
||||
for _, upstream := range p.Upstreams {
|
||||
reply, backendErr = upstream.Exchange(ctx, r)
|
||||
if backendErr == nil {
|
||||
w.WriteMsg(reply)
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
return dns.RcodeServerFailure, errors.Wrap(backendErr, "failed to contact any of the upstreams")
|
||||
}
|
||||
|
||||
// Name implements interface for CoreDNS plugin
|
||||
func (p ProxyPlugin) Name() string { return "proxy" }
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
package tunneldns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// UpstreamHTTPS is the upstream implementation for DNS over HTTPS service
|
||||
type UpstreamHTTPS struct {
|
||||
client *http.Client
|
||||
endpoint *url.URL
|
||||
bootstraps []string
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint
|
||||
func NewUpstreamHTTPS(endpoint string, bootstraps []string, maxConnections int, log *zerolog.Logger) (Upstream, error) {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UpstreamHTTPS{client: configureClient(u.Hostname(), maxConnections), endpoint: u, bootstraps: bootstraps, log: log}, nil
|
||||
}
|
||||
|
||||
// Exchange provides an implementation for the Upstream interface
|
||||
func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
|
||||
queryBuf, err := query.Pack()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to pack DNS query")
|
||||
}
|
||||
|
||||
if len(query.Question) > 0 && query.Question[0].Name == fmt.Sprintf("%s.", u.endpoint.Hostname()) {
|
||||
for _, bootstrap := range u.bootstraps {
|
||||
endpoint, client, err := configureBootstrap(bootstrap)
|
||||
if err != nil {
|
||||
u.log.Err(err).Msgf("failed to configure bootstrap upstream %s", bootstrap)
|
||||
continue
|
||||
}
|
||||
msg, err := exchange(queryBuf, query.Id, endpoint, client, u.log)
|
||||
if err != nil {
|
||||
u.log.Err(err).Msgf("failed to connect to a bootstrap upstream %s", bootstrap)
|
||||
continue
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to reach any bootstrap upstream: %v", u.bootstraps)
|
||||
}
|
||||
|
||||
return exchange(queryBuf, query.Id, u.endpoint, u.client, u.log)
|
||||
}
|
||||
|
||||
func exchange(msg []byte, queryID uint16, endpoint *url.URL, client *http.Client, log *zerolog.Logger) (*dns.Msg, error) {
|
||||
// No content negotiation for now, use DNS wire format
|
||||
buf, backendErr := exchangeWireformat(msg, endpoint, client)
|
||||
if backendErr == nil {
|
||||
response := &dns.Msg{}
|
||||
if err := response.Unpack(buf); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unpack DNS response from body")
|
||||
}
|
||||
|
||||
response.Id = queryID
|
||||
return response, nil
|
||||
}
|
||||
|
||||
log.Err(backendErr).Msgf("failed to connect to an HTTPS backend %q", endpoint)
|
||||
return nil, backendErr
|
||||
}
|
||||
|
||||
// Perform message exchange with the default UDP wireformat defined in current draft
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-doh-dns-over-https
|
||||
func exchangeWireformat(msg []byte, endpoint *url.URL, client *http.Client) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(msg))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create an HTTPS request")
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/dns-message")
|
||||
req.Host = endpoint.Host
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to perform an HTTPS request")
|
||||
}
|
||||
|
||||
// Check response status code
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read wireformat response from the body
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read the response body")
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) {
|
||||
b, err := url.Parse(bootstrap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ip := net.ParseIP(b.Hostname()); ip == nil {
|
||||
return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname())
|
||||
}
|
||||
|
||||
return b, configureClient(b.Hostname(), MaxUpstreamConnsDefault), nil
|
||||
}
|
||||
|
||||
// configureClient will configure a HTTPS client for upstream DoH requests
|
||||
func configureClient(hostname string, maxUpstreamConnections int) *http.Client {
|
||||
// Update TLS and HTTP client configuration
|
||||
tlsConfig := &tls.Config{ServerName: hostname}
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
DisableCompression: true,
|
||||
MaxIdleConns: 1,
|
||||
MaxConnsPerHost: maxUpstreamConnections,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
_ = http2.ConfigureTransport(transport)
|
||||
|
||||
return &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package tunneldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "cloudflared"
|
||||
)
|
||||
|
||||
// MetricsPlugin is an adapter for CoreDNS and built-in metrics
|
||||
type MetricsPlugin struct {
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// NewMetricsPlugin creates a plugin with configured metrics
|
||||
func NewMetricsPlugin(next plugin.Handler) *MetricsPlugin {
|
||||
return &MetricsPlugin{Next: next}
|
||||
}
|
||||
|
||||
// ServeDNS implements the CoreDNS plugin interface
|
||||
func (p MetricsPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
rw := dnstest.NewRecorder(w)
|
||||
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
|
||||
|
||||
// Update built-in metrics
|
||||
server := metrics.WithServer(ctx)
|
||||
vars.Report(server, state, ".", "", rcode.ToString(rw.Rcode), pluginName, rw.Len, rw.Start)
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// Name implements the CoreDNS plugin interface
|
||||
func (p MetricsPlugin) Name() string { return "metrics" }
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
package tunneldns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/cache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldAddress = "address"
|
||||
LogFieldURL = "url"
|
||||
MaxUpstreamConnsDefault = 5
|
||||
)
|
||||
|
||||
// Listener is an adapter between CoreDNS server and Warp runnable
|
||||
type Listener struct {
|
||||
server *dnsserver.Server
|
||||
wg sync.WaitGroup
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// Create a CoreDNS server plugin from configuration
|
||||
func createConfig(address string, port uint16, p plugin.Handler) *dnsserver.Config {
|
||||
c := &dnsserver.Config{
|
||||
Zone: ".",
|
||||
Transport: "dns",
|
||||
ListenHosts: []string{address},
|
||||
Port: strconv.FormatUint(uint64(port), 10),
|
||||
}
|
||||
|
||||
c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p })
|
||||
return c
|
||||
}
|
||||
|
||||
// Start blocks for serving requests
|
||||
func (l *Listener) Start(readySignal chan struct{}) error {
|
||||
defer close(readySignal)
|
||||
l.log.Info().Str(LogFieldAddress, l.server.Address()).Msg("Starting DNS over HTTPS proxy server")
|
||||
|
||||
// Start UDP listener
|
||||
if udp, err := l.server.ListenPacket(); err == nil {
|
||||
l.wg.Add(1)
|
||||
go func() {
|
||||
_ = l.server.ServePacket(udp)
|
||||
l.wg.Done()
|
||||
}()
|
||||
} else {
|
||||
return errors.Wrap(err, "failed to create a UDP listener")
|
||||
}
|
||||
|
||||
// Start TCP listener
|
||||
tcp, err := l.server.Listen()
|
||||
if err == nil {
|
||||
l.wg.Add(1)
|
||||
go func() {
|
||||
_ = l.server.Serve(tcp)
|
||||
l.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "failed to create a TCP listener")
|
||||
}
|
||||
|
||||
// Stop signals server shutdown and blocks until completed
|
||||
func (l *Listener) Stop() error {
|
||||
if err := l.server.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateListener configures the server and bound sockets
|
||||
func CreateListener(address string, port uint16, upstreams []string, bootstraps []string, maxUpstreamConnections int, log *zerolog.Logger) (*Listener, error) {
|
||||
// Build the list of upstreams
|
||||
upstreamList := make([]Upstream, 0)
|
||||
for _, url := range upstreams {
|
||||
log.Info().Str(LogFieldURL, url).Msg("Adding DNS upstream")
|
||||
upstream, err := NewUpstreamHTTPS(url, bootstraps, maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create HTTPS upstream")
|
||||
}
|
||||
upstreamList = append(upstreamList, upstream)
|
||||
}
|
||||
|
||||
// Create a local cache with HTTPS proxy plugin
|
||||
chain := cache.New()
|
||||
chain.Next = ProxyPlugin{
|
||||
Upstreams: upstreamList,
|
||||
}
|
||||
|
||||
// Format an endpoint
|
||||
endpoint := "dns://" + net.JoinHostPort(address, strconv.FormatUint(uint64(port), 10))
|
||||
|
||||
// Create the actual middleware server
|
||||
server, err := dnsserver.NewServer(endpoint, []*dnsserver.Config{createConfig(address, port, NewMetricsPlugin(chain))})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Listener{server: server, log: log}, nil
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
Copyright (c) 2015 Martin Atkins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
// Package cidr is a collection of assorted utilities for computing
|
||||
// network and host addresses within network ranges.
|
||||
//
|
||||
// It expects a CIDR-type address structure where addresses are divided into
|
||||
// some number of prefix bits representing the network and then the remaining
|
||||
// suffix bits represent the host.
|
||||
//
|
||||
// For example, it can help to calculate addresses for sub-networks of a
|
||||
// parent network, or to calculate host addresses within a particular prefix.
|
||||
//
|
||||
// At present this package is prioritizing simplicity of implementation and
|
||||
// de-prioritizing speed and memory usage. Thus caution is advised before
|
||||
// using this package in performance-critical applications or hot codepaths.
|
||||
// Patches to improve the speed and memory usage may be accepted as long as
|
||||
// they do not result in a significant increase in code complexity.
|
||||
package cidr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Subnet takes a parent CIDR range and creates a subnet within it
|
||||
// with the given number of additional prefix bits and the given
|
||||
// network number.
|
||||
//
|
||||
// For example, 10.3.0.0/16, extended by 8 bits, with a network number
|
||||
// of 5, becomes 10.3.5.0/24 .
|
||||
func Subnet(base *net.IPNet, newBits int, num int) (*net.IPNet, error) {
|
||||
return SubnetBig(base, newBits, big.NewInt(int64(num)))
|
||||
}
|
||||
|
||||
// SubnetBig takes a parent CIDR range and creates a subnet within it with the
|
||||
// given number of additional prefix bits and the given network number. It
|
||||
// differs from Subnet in that it takes a *big.Int for the num, instead of an int.
|
||||
//
|
||||
// For example, 10.3.0.0/16, extended by 8 bits, with a network number of 5,
|
||||
// becomes 10.3.5.0/24 .
|
||||
func SubnetBig(base *net.IPNet, newBits int, num *big.Int) (*net.IPNet, error) {
|
||||
ip := base.IP
|
||||
mask := base.Mask
|
||||
|
||||
parentLen, addrLen := mask.Size()
|
||||
newPrefixLen := parentLen + newBits
|
||||
|
||||
if newPrefixLen > addrLen {
|
||||
return nil, fmt.Errorf("insufficient address space to extend prefix of %d by %d", parentLen, newBits)
|
||||
}
|
||||
|
||||
maxNetNum := uint64(1<<uint64(newBits)) - 1
|
||||
if num.Uint64() > maxNetNum {
|
||||
return nil, fmt.Errorf("prefix extension of %d does not accommodate a subnet numbered %d", newBits, num)
|
||||
}
|
||||
|
||||
return &net.IPNet{
|
||||
IP: insertNumIntoIP(ip, num, newPrefixLen),
|
||||
Mask: net.CIDRMask(newPrefixLen, addrLen),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Host takes a parent CIDR range and turns it into a host IP address with the
|
||||
// given host number.
|
||||
//
|
||||
// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2.
|
||||
func Host(base *net.IPNet, num int) (net.IP, error) {
|
||||
return HostBig(base, big.NewInt(int64(num)))
|
||||
}
|
||||
|
||||
// HostBig takes a parent CIDR range and turns it into a host IP address with
|
||||
// the given host number. It differs from Host in that it takes a *big.Int for
|
||||
// the num, instead of an int.
|
||||
//
|
||||
// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2.
|
||||
func HostBig(base *net.IPNet, num *big.Int) (net.IP, error) {
|
||||
ip := base.IP
|
||||
mask := base.Mask
|
||||
|
||||
parentLen, addrLen := mask.Size()
|
||||
hostLen := addrLen - parentLen
|
||||
|
||||
maxHostNum := big.NewInt(int64(1))
|
||||
maxHostNum.Lsh(maxHostNum, uint(hostLen))
|
||||
maxHostNum.Sub(maxHostNum, big.NewInt(1))
|
||||
|
||||
numUint64 := big.NewInt(int64(num.Uint64()))
|
||||
if num.Cmp(big.NewInt(0)) == -1 {
|
||||
numUint64.Neg(num)
|
||||
numUint64.Sub(numUint64, big.NewInt(int64(1)))
|
||||
num.Sub(maxHostNum, numUint64)
|
||||
}
|
||||
|
||||
if numUint64.Cmp(maxHostNum) == 1 {
|
||||
return nil, fmt.Errorf("prefix of %d does not accommodate a host numbered %d", parentLen, num)
|
||||
}
|
||||
var bitlength int
|
||||
if ip.To4() != nil {
|
||||
bitlength = 32
|
||||
} else {
|
||||
bitlength = 128
|
||||
}
|
||||
return insertNumIntoIP(ip, num, bitlength), nil
|
||||
}
|
||||
|
||||
// AddressRange returns the first and last addresses in the given CIDR range.
|
||||
func AddressRange(network *net.IPNet) (net.IP, net.IP) {
|
||||
// the first IP is easy
|
||||
firstIP := network.IP
|
||||
|
||||
// the last IP is the network address OR NOT the mask address
|
||||
prefixLen, bits := network.Mask.Size()
|
||||
if prefixLen == bits {
|
||||
// Easy!
|
||||
// But make sure that our two slices are distinct, since they
|
||||
// would be in all other cases.
|
||||
lastIP := make([]byte, len(firstIP))
|
||||
copy(lastIP, firstIP)
|
||||
return firstIP, lastIP
|
||||
}
|
||||
|
||||
firstIPInt, bits := ipToInt(firstIP)
|
||||
hostLen := uint(bits) - uint(prefixLen)
|
||||
lastIPInt := big.NewInt(1)
|
||||
lastIPInt.Lsh(lastIPInt, hostLen)
|
||||
lastIPInt.Sub(lastIPInt, big.NewInt(1))
|
||||
lastIPInt.Or(lastIPInt, firstIPInt)
|
||||
|
||||
return firstIP, intToIP(lastIPInt, bits)
|
||||
}
|
||||
|
||||
// AddressCount returns the number of distinct host addresses within the given
|
||||
// CIDR range.
|
||||
//
|
||||
// Since the result is a uint64, this function returns meaningful information
|
||||
// only for IPv4 ranges and IPv6 ranges with a prefix size of at least 65.
|
||||
func AddressCount(network *net.IPNet) uint64 {
|
||||
prefixLen, bits := network.Mask.Size()
|
||||
return 1 << (uint64(bits) - uint64(prefixLen))
|
||||
}
|
||||
|
||||
//VerifyNoOverlap takes a list subnets and supernet (CIDRBlock) and verifies
|
||||
//none of the subnets overlap and all subnets are in the supernet
|
||||
//it returns an error if any of those conditions are not satisfied
|
||||
func VerifyNoOverlap(subnets []*net.IPNet, CIDRBlock *net.IPNet) error {
|
||||
firstLastIP := make([][]net.IP, len(subnets))
|
||||
for i, s := range subnets {
|
||||
first, last := AddressRange(s)
|
||||
firstLastIP[i] = []net.IP{first, last}
|
||||
}
|
||||
for i, s := range subnets {
|
||||
if !CIDRBlock.Contains(firstLastIP[i][0]) || !CIDRBlock.Contains(firstLastIP[i][1]) {
|
||||
return fmt.Errorf("%s does not fully contain %s", CIDRBlock.String(), s.String())
|
||||
}
|
||||
for j := 0; j < len(subnets); j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
|
||||
first := firstLastIP[j][0]
|
||||
last := firstLastIP[j][1]
|
||||
if s.Contains(first) || s.Contains(last) {
|
||||
return fmt.Errorf("%s overlaps with %s", subnets[j].String(), s.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreviousSubnet returns the subnet of the desired mask in the IP space
|
||||
// just lower than the start of IPNet provided. If the IP space rolls over
|
||||
// then the second return value is true
|
||||
func PreviousSubnet(network *net.IPNet, prefixLen int) (*net.IPNet, bool) {
|
||||
startIP := checkIPv4(network.IP)
|
||||
previousIP := make(net.IP, len(startIP))
|
||||
copy(previousIP, startIP)
|
||||
cMask := net.CIDRMask(prefixLen, 8*len(previousIP))
|
||||
previousIP = Dec(previousIP)
|
||||
previous := &net.IPNet{IP: previousIP.Mask(cMask), Mask: cMask}
|
||||
if startIP.Equal(net.IPv4zero) || startIP.Equal(net.IPv6zero) {
|
||||
return previous, true
|
||||
}
|
||||
return previous, false
|
||||
}
|
||||
|
||||
// NextSubnet returns the next available subnet of the desired mask size
|
||||
// starting for the maximum IP of the offset subnet
|
||||
// If the IP exceeds the maxium IP then the second return value is true
|
||||
func NextSubnet(network *net.IPNet, prefixLen int) (*net.IPNet, bool) {
|
||||
_, currentLast := AddressRange(network)
|
||||
mask := net.CIDRMask(prefixLen, 8*len(currentLast))
|
||||
currentSubnet := &net.IPNet{IP: currentLast.Mask(mask), Mask: mask}
|
||||
_, last := AddressRange(currentSubnet)
|
||||
last = Inc(last)
|
||||
next := &net.IPNet{IP: last.Mask(mask), Mask: mask}
|
||||
if last.Equal(net.IPv4zero) || last.Equal(net.IPv6zero) {
|
||||
return next, true
|
||||
}
|
||||
return next, false
|
||||
}
|
||||
|
||||
//Inc increases the IP by one this returns a new []byte for the IP
|
||||
func Inc(IP net.IP) net.IP {
|
||||
IP = checkIPv4(IP)
|
||||
incIP := make([]byte, len(IP))
|
||||
copy(incIP, IP)
|
||||
for j := len(incIP) - 1; j >= 0; j-- {
|
||||
incIP[j]++
|
||||
if incIP[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return incIP
|
||||
}
|
||||
|
||||
//Dec decreases the IP by one this returns a new []byte for the IP
|
||||
func Dec(IP net.IP) net.IP {
|
||||
IP = checkIPv4(IP)
|
||||
decIP := make([]byte, len(IP))
|
||||
copy(decIP, IP)
|
||||
decIP = checkIPv4(decIP)
|
||||
for j := len(decIP) - 1; j >= 0; j-- {
|
||||
decIP[j]--
|
||||
if decIP[j] < 255 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return decIP
|
||||
}
|
||||
|
||||
func checkIPv4(ip net.IP) net.IP {
|
||||
// Go for some reason allocs IPv6len for IPv4 so we have to correct it
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
return v4
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package cidr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
func ipToInt(ip net.IP) (*big.Int, int) {
|
||||
val := &big.Int{}
|
||||
val.SetBytes([]byte(ip))
|
||||
if len(ip) == net.IPv4len {
|
||||
return val, 32
|
||||
} else if len(ip) == net.IPv6len {
|
||||
return val, 128
|
||||
} else {
|
||||
panic(fmt.Errorf("Unsupported address length %d", len(ip)))
|
||||
}
|
||||
}
|
||||
|
||||
func intToIP(ipInt *big.Int, bits int) net.IP {
|
||||
ipBytes := ipInt.Bytes()
|
||||
ret := make([]byte, bits/8)
|
||||
// Pack our IP bytes into the end of the return array,
|
||||
// since big.Int.Bytes() removes front zero padding.
|
||||
for i := 1; i <= len(ipBytes); i++ {
|
||||
ret[len(ret)-i] = ipBytes[len(ipBytes)-i]
|
||||
}
|
||||
return net.IP(ret)
|
||||
}
|
||||
|
||||
func insertNumIntoIP(ip net.IP, bigNum *big.Int, prefixLen int) net.IP {
|
||||
ipInt, totalBits := ipToInt(ip)
|
||||
bigNum.Lsh(bigNum, uint(totalBits-prefixLen))
|
||||
ipInt.Or(ipInt, bigNum)
|
||||
return intToIP(ipInt, totalBits)
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# shell scripts should not use tabs to indent!
|
||||
*.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
|
||||
# files for systemd (shell-similar)
|
||||
*.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
|
||||
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
|
||||
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
|
||||
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
.git* text eol=auto core.whitespace whitespace=trailing-space
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
_gitignore/
|
||||
Vagrantfile
|
||||
.vagrant/
|
||||
/.idea
|
||||
|
||||
dist/builds/
|
||||
dist/release/
|
||||
|
||||
error.log
|
||||
access.log
|
||||
|
||||
/*.conf
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
og_static/
|
||||
|
||||
.vscode/
|
||||
|
||||
*.bat
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
THIS IS A FORK OF CADDY v1 - EVERYTHING IS STRIPPED EXCEPT THE PIECES NEEDED IN COREDNS.
|
||||
|
||||
Issues are not enabled in this repository. Please raise any issues in coredns/coredns.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
|
||||
|
||||
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
|
||||
|
||||
<p align="center">
|
||||
<b>Thanks to our special sponsor:</b>
|
||||
<br><br>
|
||||
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
|
||||
</p>
|
||||
|
||||
## Menu
|
||||
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Running in Production](#running-in-production)
|
||||
- [Contributing](#contributing)
|
||||
- [Donors](#donors)
|
||||
- [About the Project](#about-the-project)
|
||||
|
||||
## Features
|
||||
|
||||
- **Easy configuration** with the Caddyfile
|
||||
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
|
||||
- **HTTP/2** by default
|
||||
- **Virtual hosting** so multiple sites just work
|
||||
- Experimental **QUIC support** for cutting-edge transmissions
|
||||
- TLS session ticket **key rotation** for more secure connections
|
||||
- **Extensible with plugins** because a convenient web server is a helpful one
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
|
||||
[See a more complete list of features built into Caddy.](https://caddyserver.com/#features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
|
||||
|
||||
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
|
||||
|
||||
|
||||
<p align="center">
|
||||
<b>Powered by</b>
|
||||
<br>
|
||||
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
|
||||
|
||||
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
|
||||
- **[Latest release](https://github.com/caddyserver/caddy/releases/latest)** for pre-built, vanilla binaries
|
||||
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
|
||||
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
|
||||
|
||||
**To build Caddy without plugins:**
|
||||
|
||||
- Run `go get github.com/caddyserver/caddy/caddy`
|
||||
|
||||
Caddy will be installed to your `$GOPATH/bin` folder.
|
||||
|
||||
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
|
||||
|
||||
**To build Caddy with plugins (and with version information):**
|
||||
|
||||
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
|
||||
- Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
// plug in plugins here, for example:
|
||||
// _ "import/path/here"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// optional: disable telemetry
|
||||
// caddymain.EnableTelemetry = false
|
||||
caddymain.Run()
|
||||
}
|
||||
```
|
||||
3. `go mod init caddy`
|
||||
4. Run `go get github.com/caddyserver/caddy`
|
||||
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
|
||||
|
||||
**To install Caddy's source code for development:**
|
||||
|
||||
- Run `git clone https://github.com/caddyserver/caddy.git` in any folder (doesn't have to be in GOPATH).
|
||||
|
||||
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
|
||||
|
||||
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
To serve static files from the current working directory, run:
|
||||
|
||||
```
|
||||
caddy
|
||||
```
|
||||
|
||||
Caddy's default port is 2015, so open your browser to [http://localhost:2015](http://localhost:2015).
|
||||
|
||||
### Go from 0 to HTTPS in 5 seconds
|
||||
|
||||
If the `caddy` binary has permission to bind to low ports and your domain name's DNS records point to the machine you're on:
|
||||
|
||||
```
|
||||
caddy -host example.com
|
||||
```
|
||||
|
||||
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
|
||||
|
||||
### Customizing your site
|
||||
|
||||
To customize how your site is served, create a file named Caddyfile by your site and paste this into it:
|
||||
|
||||
```plain
|
||||
localhost
|
||||
|
||||
push
|
||||
browse
|
||||
websocket /echo cat
|
||||
ext .html
|
||||
log /var/log/access.log
|
||||
proxy /api 127.0.0.1:7005
|
||||
header /api Access-Control-Allow-Origin *
|
||||
```
|
||||
|
||||
When you run `caddy` in that directory, it will automatically find and use that Caddyfile.
|
||||
|
||||
This simple file enables server push (via Link headers), allows directory browsing (for folders without an index file), hosts a WebSocket echo server at /echo, serves clean URLs, logs requests to an access log, proxies all API requests to a backend on port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from the API.
|
||||
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
### Doing more with Caddy
|
||||
|
||||
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
|
||||
|
||||
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
|
||||
Caddy has a nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
|
||||
|
||||
## Running in Production
|
||||
|
||||
Caddy is production-ready if you find it to be a good fit for your site and workflow.
|
||||
|
||||
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
||||
|
||||
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/caddyserver/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
|
||||
|
||||
How you choose to run Caddy is up to you. Many users are satisfied with `nohup caddy &`. Others use `screen`. Users who need Caddy to come back up after reboots either do so in the script that caused the reboot, add a command to an init script, or configure a service with their OS.
|
||||
|
||||
If you have questions or concerns about Caddy' underlying crypto implementations, consult Go's [crypto packages](https://golang.org/pkg/crypto), starting with their documentation, then issues, then the code itself; as Caddy uses mainly those libraries.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/)!
|
||||
|
||||
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
|
||||
|
||||
We use GitHub issues and pull requests only for discussing bug reports and the development of specific changes. We welcome all other topics on the [forum](https://caddy.community)!
|
||||
|
||||
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/caddy/issues/new) describing the change that should be made.
|
||||
|
||||
### Good First Issue
|
||||
|
||||
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
|
||||
## Donors
|
||||
|
||||
- [DigitalOcean](https://m.do.co/c/6d7bdafccf96) is hosting the Caddy project.
|
||||
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
|
||||
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
|
||||
|
||||
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://github.com/sponsors/mholt)!**
|
||||
|
||||
|
||||
## About the Project
|
||||
|
||||
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from [spark](https://github.com/rif/spark), [nginx](https://github.com/nginx/nginx), lighttpd,
|
||||
[Websocketd](https://github.com/joewalnes/websocketd) and [Vagrant](https://www.vagrantup.com/), which provides a pleasant mixture of features from each of them.
|
||||
|
||||
**The name "Caddy" is trademarked:** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
|
||||
|
||||
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// AssetsPath returns the path to the folder
|
||||
// where the application may store data. If
|
||||
// CADDYPATH env variable is set, that value
|
||||
// is used. Otherwise, the path is the result
|
||||
// of evaluating "$HOME/.caddy".
|
||||
func AssetsPath() string {
|
||||
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
|
||||
return caddyPath
|
||||
}
|
||||
return filepath.Join(userHomeDir(), ".caddy")
|
||||
}
|
||||
|
||||
// userHomeDir returns the user's home directory according to
|
||||
// environment variables.
|
||||
//
|
||||
// Credit: http://stackoverflow.com/a/7922977/1048862
|
||||
func userHomeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
return home
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,260 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure and has
|
||||
// some really convenient methods.
|
||||
type Dispenser struct {
|
||||
filename string
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
|
||||
func NewDispenser(filename string, input io.Reader) Dispenser {
|
||||
tokens, _ := allTokens(input) // ignoring error because nothing to do with it
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDispenserTokens returns a Dispenser filled with the given tokens.
|
||||
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line. Returns true if a token was loaded; false
|
||||
// otherwise. If false, all tokens on the line have
|
||||
// been consumed. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block. It returns true if a token was
|
||||
// loaded, or false when the block's closing curly brace
|
||||
// was loaded and thus the block ended. Nested blocks are
|
||||
// not supported.
|
||||
func (d *Dispenser) NextBlock() bool {
|
||||
if d.nesting > 0 {
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
d.nesting--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !d.NextArg() { // block must open on same line
|
||||
return false
|
||||
}
|
||||
if d.Val() != "{" {
|
||||
d.cursor-- // roll back if not opening brace
|
||||
return false
|
||||
}
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
// Open and then closed right away
|
||||
return false
|
||||
}
|
||||
d.nesting++
|
||||
return true
|
||||
}
|
||||
|
||||
// Val gets the text of the current token. If there is no token
|
||||
// loaded, it returns empty string.
|
||||
func (d *Dispenser) Val() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token. If there is no token
|
||||
// loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].Line
|
||||
}
|
||||
|
||||
// File gets the filename of the current token. If there is no token loaded,
|
||||
// it returns the filename originally given when parsing started.
|
||||
func (d *Dispenser) File() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return d.filename
|
||||
}
|
||||
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
|
||||
return tokenFilename
|
||||
}
|
||||
return d.filename
|
||||
}
|
||||
|
||||
// Args is a convenience function that loads the next arguments
|
||||
// (tokens on the same line) into an arbitrary number of strings
|
||||
// pointed to in targets. If there are fewer tokens available
|
||||
// than string pointers, the remaining strings will not be changed
|
||||
// and false will be returned. If there were enough tokens available
|
||||
// to fill the arguments, then true will be returned.
|
||||
func (d *Dispenser) Args(targets ...*string) bool {
|
||||
enough := true
|
||||
for i := 0; i < len(targets); i++ {
|
||||
if !d.NextArg() {
|
||||
enough = false
|
||||
break
|
||||
}
|
||||
*targets[i] = d.Val()
|
||||
}
|
||||
return enough
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
d.cursor--
|
||||
break
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ArgErr returns an argument error, meaning that another
|
||||
// argument was expected but not found. In other words,
|
||||
// a line break or open curly brace was encountered instead of
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const filename = "Caddyfile"
|
||||
|
||||
// ToJSON converts caddyfile to its JSON representation.
|
||||
func ToJSON(caddyfile []byte) ([]byte, error) {
|
||||
var j EncodedCaddyfile
|
||||
|
||||
serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
block := EncodedServerBlock{
|
||||
Keys: sb.Keys,
|
||||
Body: [][]interface{}{},
|
||||
}
|
||||
|
||||
// Extract directives deterministically by sorting them
|
||||
var directives = make([]string, len(sb.Tokens))
|
||||
for dir := range sb.Tokens {
|
||||
directives = append(directives, dir)
|
||||
}
|
||||
sort.Strings(directives)
|
||||
|
||||
// Convert each directive's tokens into our JSON structure
|
||||
for _, dir := range directives {
|
||||
disp := NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
for disp.Next() {
|
||||
block.Body = append(block.Body, constructLine(&disp))
|
||||
}
|
||||
}
|
||||
|
||||
// tack this block onto the end of the list
|
||||
j = append(j, block)
|
||||
}
|
||||
|
||||
result, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// constructLine transforms tokens into a JSON-encodable structure;
|
||||
// but only one line at a time, to be used at the top-level of
|
||||
// a server block only (where the first token on each line is a
|
||||
// directive) - not to be used at any other nesting level.
|
||||
func constructLine(d *Dispenser) []interface{} {
|
||||
var args []interface{}
|
||||
|
||||
args = append(args, d.Val())
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
args = append(args, constructBlock(d))
|
||||
continue
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// constructBlock recursively processes tokens into a
|
||||
// JSON-encodable structure. To be used in a directive's
|
||||
// block. Goes to end of block.
|
||||
func constructBlock(d *Dispenser) [][]interface{} {
|
||||
block := [][]interface{}{}
|
||||
|
||||
for d.Next() {
|
||||
if d.Val() == "}" {
|
||||
break
|
||||
}
|
||||
block = append(block, constructLine(d))
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
|
||||
func FromJSON(jsonBytes []byte) ([]byte, error) {
|
||||
var j EncodedCaddyfile
|
||||
var result string
|
||||
|
||||
err := json.Unmarshal(jsonBytes, &j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for sbPos, sb := range j {
|
||||
if sbPos > 0 {
|
||||
result += "\n\n"
|
||||
}
|
||||
for i, key := range sb.Keys {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
//result += standardizeScheme(key)
|
||||
result += key
|
||||
}
|
||||
result += jsonToText(sb.Body, 1)
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
// jsonToText recursively transforms a scope of JSON into plain
|
||||
// Caddyfile text.
|
||||
func jsonToText(scope interface{}, depth int) string {
|
||||
var result string
|
||||
|
||||
switch val := scope.(type) {
|
||||
case string:
|
||||
if strings.ContainsAny(val, "\" \n\t\r") {
|
||||
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
|
||||
} else {
|
||||
result += val
|
||||
}
|
||||
case int:
|
||||
result += strconv.Itoa(val)
|
||||
case float64:
|
||||
result += fmt.Sprintf("%v", val)
|
||||
case bool:
|
||||
result += fmt.Sprintf("%t", val)
|
||||
case [][]interface{}:
|
||||
result += " {\n"
|
||||
for _, arg := range val {
|
||||
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
case []interface{}:
|
||||
for i, v := range val {
|
||||
if block, ok := v.([]interface{}); ok {
|
||||
result += "{\n"
|
||||
for _, arg := range block {
|
||||
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
continue
|
||||
}
|
||||
result += jsonToText(v, depth)
|
||||
if i < len(val)-1 {
|
||||
result += " "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO: Will this function come in handy somewhere else?
|
||||
/*
|
||||
// standardizeScheme turns an address like host:https into https://host,
|
||||
// or "host:" into "host".
|
||||
func standardizeScheme(addr string) string {
|
||||
if hostname, port, err := net.SplitHostPort(addr); err == nil {
|
||||
if port == "http" || port == "https" {
|
||||
addr = port + "://" + hostname
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(addr, ":")
|
||||
}
|
||||
*/
|
||||
|
||||
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
|
||||
type EncodedCaddyfile []EncodedServerBlock
|
||||
|
||||
// EncodedServerBlock represents a server block ripe for encoding.
|
||||
type EncodedServerBlock struct {
|
||||
Keys []string `json:"keys"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type (
|
||||
// lexer is a utility which can get values, token by
|
||||
// token, from a Reader. A token is a word, and tokens
|
||||
// are separated by whitespace. A word can be enclosed
|
||||
// in quotes if it contains whitespace.
|
||||
lexer struct {
|
||||
reader *bufio.Reader
|
||||
token Token
|
||||
line int
|
||||
}
|
||||
|
||||
// Token represents a single parsable unit.
|
||||
Token struct {
|
||||
File string
|
||||
Line int
|
||||
Text string
|
||||
}
|
||||
)
|
||||
|
||||
// load prepares the lexer to scan an input for tokens.
|
||||
// It discards any leading byte order mark.
|
||||
func (l *lexer) load(input io.Reader) error {
|
||||
l.reader = bufio.NewReader(input)
|
||||
l.line = 1
|
||||
|
||||
// discard byte order mark, if present
|
||||
firstCh, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if firstCh != 0xFEFF {
|
||||
err := l.reader.UnreadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// next loads the next token into the lexer.
|
||||
// A token is delimited by whitespace, unless
|
||||
// the token starts with a quotes character (")
|
||||
// in which case the token goes until the closing
|
||||
// quotes (the enclosing quotes are not included).
|
||||
// Inside quoted strings, quotes may be escaped
|
||||
// with a preceding \ character. No other chars
|
||||
// may be escaped. The rest of the line is skipped
|
||||
// if a "#" character is read in. Returns true if
|
||||
// a token was loaded; false otherwise.
|
||||
func (l *lexer) next() bool {
|
||||
var val []rune
|
||||
var comment, quoted, escaped bool
|
||||
|
||||
makeToken := func() bool {
|
||||
l.token.Text = string(val)
|
||||
return true
|
||||
}
|
||||
|
||||
for {
|
||||
ch, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if len(val) > 0 {
|
||||
return makeToken()
|
||||
}
|
||||
if err == io.EOF {
|
||||
return false
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if !escaped {
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
} else if ch == '"' {
|
||||
quoted = false
|
||||
return makeToken()
|
||||
}
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line++
|
||||
}
|
||||
if escaped {
|
||||
// only escape quotes
|
||||
if ch != '"' {
|
||||
val = append(val, '\\')
|
||||
}
|
||||
}
|
||||
val = append(val, ch)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
if ch == '\r' {
|
||||
continue
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line++
|
||||
comment = false
|
||||
}
|
||||
if len(val) > 0 {
|
||||
return makeToken()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '#' {
|
||||
comment = true
|
||||
}
|
||||
|
||||
if comment {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(val) == 0 {
|
||||
l.token = Token{Line: l.line}
|
||||
if ch == '"' {
|
||||
quoted = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
val = append(val, ch)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,490 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
// order, by server block. No further parsing is performed.
|
||||
// Server blocks are returned in the order in which they appear.
|
||||
// Directives that do not appear in validDirectives will cause
|
||||
// an error. If you do not want to check for valid directives,
|
||||
// pass in nil instead.
|
||||
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
|
||||
return p.parseAll()
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) ([]Token, error) {
|
||||
l := new(lexer)
|
||||
err := l.load(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tokens []Token
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
Dispenser
|
||||
block ServerBlock // current server block being parsed
|
||||
validDirectives []string // a directive must be valid or it's an error
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
definedSnippets map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
var blocks []ServerBlock
|
||||
|
||||
for p.Next() {
|
||||
err := p.parseOne()
|
||||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Keys) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseOne() error {
|
||||
p.block = ServerBlock{Tokens: make(map[string][]Token)}
|
||||
|
||||
return p.begin()
|
||||
}
|
||||
|
||||
func (p *parser) begin() error {
|
||||
if len(p.tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := p.addresses()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.eof {
|
||||
// this happens if the Caddyfile consists of only
|
||||
// a line of addresses and nothing else
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, name := p.isSnippet(); ok {
|
||||
if p.definedSnippets == nil {
|
||||
p.definedSnippets = map[string][]Token{}
|
||||
}
|
||||
if _, found := p.definedSnippets[name]; found {
|
||||
return p.Errf("redeclaration of previously declared snippet %s", name)
|
||||
}
|
||||
// consume all tokens til matched close brace
|
||||
tokens, err := p.snippetTokens()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.definedSnippets[name] = tokens
|
||||
// empty block keys so we don't save this block as a real server.
|
||||
p.block.Keys = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.blockContents()
|
||||
}
|
||||
|
||||
func (p *parser) addresses() error {
|
||||
var expectingAnother bool
|
||||
|
||||
for {
|
||||
tkn := replaceEnvVars(p.Val())
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if tkn == "import" && p.isNewLine() {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Open brace definitely indicates end of addresses
|
||||
if tkn == "{" {
|
||||
if expectingAnother {
|
||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed ""
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
if tkn[len(tkn)-1] == ',' {
|
||||
tkn = tkn[:len(tkn)-1]
|
||||
expectingAnother = true
|
||||
} else {
|
||||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
p.block.Keys = append(p.block.Keys, tkn)
|
||||
}
|
||||
|
||||
// Advance token and possibly break out of loop or return error
|
||||
hasNext := p.Next()
|
||||
if expectingAnother && !hasNext {
|
||||
return p.EOFErr()
|
||||
}
|
||||
if !hasNext {
|
||||
p.eof = true
|
||||
break // EOF
|
||||
}
|
||||
if !expectingAnother && p.isNewLine() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) blockContents() error {
|
||||
errOpenCurlyBrace := p.openCurlyBrace()
|
||||
if errOpenCurlyBrace != nil {
|
||||
// single-server configs don't need curly braces
|
||||
p.cursor--
|
||||
}
|
||||
|
||||
err := p.directives()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only look for close curly brace if there was an opening
|
||||
if errOpenCurlyBrace == nil {
|
||||
err = p.closeCurlyBrace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// directives parses through all the lines for directives
|
||||
// and it expects the next token to be the first
|
||||
// directive. It goes until EOF or closing curly brace
|
||||
// which ends the server block.
|
||||
func (p *parser) directives() error {
|
||||
for p.Next() {
|
||||
// end of server block
|
||||
if p.Val() == "}" {
|
||||
break
|
||||
}
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if p.Val() == "import" {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
|
||||
// normal case: parse a directive on this line
|
||||
if err := p.directive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// doImport swaps out the import directive and its argument
|
||||
// (a total of 2 tokens) with the tokens in the specified file
|
||||
// or globbing pattern. When the function returns, the cursor
|
||||
// is on the token before where the import directive was. In
|
||||
// other words, call Next() to access the first token that was
|
||||
// imported.
|
||||
func (p *parser) doImport() error {
|
||||
// syntax checks
|
||||
if !p.NextArg() {
|
||||
return p.ArgErr()
|
||||
}
|
||||
importPattern := replaceEnvVars(p.Val())
|
||||
if importPattern == "" {
|
||||
return p.Err("Import requires a non-empty filepath")
|
||||
}
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
}
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
var importedTokens []Token
|
||||
|
||||
// first check snippets. That is a simple, non-recursive replacement
|
||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||
importedTokens = p.definedSnippets[importPattern]
|
||||
} else {
|
||||
// make path relative to the file of the _token_ being processed rather
|
||||
// than current working directory (issue #867) and then use glob to get
|
||||
// list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.File())
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
|
||||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
|
||||
// See issue #2096 - a pattern with many glob expansions can hang for too long
|
||||
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
||||
}
|
||||
matches, err = filepath.Glob(globPattern)
|
||||
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.ContainsAny(globPattern, "*?[]") {
|
||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// collect all the imported tokens
|
||||
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
// and rewind cursor so Next() will land on first imported token
|
||||
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
|
||||
p.cursor--
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doSingleImport lexes the individual file at importFile and returns
|
||||
// its tokens or an error, if any.
|
||||
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
file, err := os.Open(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if info, err := file.Stat(); err != nil {
|
||||
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
||||
} else if info.IsDir() {
|
||||
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
||||
}
|
||||
|
||||
importedTokens, err := allTokens(file)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
||||
}
|
||||
|
||||
// Tack the file path onto these tokens so errors show the imported file's name
|
||||
// (we use full, absolute path to avoid bugs: issue #1892)
|
||||
filename, err := filepath.Abs(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
}
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].File = filename
|
||||
}
|
||||
|
||||
return importedTokens, nil
|
||||
}
|
||||
|
||||
// directive collects tokens until the directive's scope
|
||||
// closes (either end of line or end of curly brace block).
|
||||
// It expects the currently-loaded token to be a directive
|
||||
// (or } that ends a server block). The collected tokens
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := replaceEnvVars(p.Val())
|
||||
nesting := 0
|
||||
|
||||
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
|
||||
if !p.validDirective(dir) {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
nesting++
|
||||
} else if p.isNewLine() && nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
} else if p.Val() == "}" && nesting > 0 {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
} else if p.Val() == "import" && p.isNewLine() {
|
||||
if err := p.doImport(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
}
|
||||
|
||||
if nesting > 0 {
|
||||
return p.EOFErr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openCurlyBrace expects the current token to be an
|
||||
// opening curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a opening curly brace. It does NOT advance the token.
|
||||
func (p *parser) openCurlyBrace() error {
|
||||
if p.Val() != "{" {
|
||||
return p.SyntaxErr("{")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeCurlyBrace expects the current token to be
|
||||
// a closing curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a closing curly brace. It does NOT advance the token.
|
||||
func (p *parser) closeCurlyBrace() error {
|
||||
if p.Val() != "}" {
|
||||
return p.SyntaxErr("}")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validDirective returns true if dir is in p.validDirectives.
|
||||
func (p *parser) validDirective(dir string) bool {
|
||||
if p.validDirectives == nil {
|
||||
return true
|
||||
}
|
||||
for _, d := range p.validDirectives {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces environment variables that appear in the token
|
||||
// and understands both the $UNIX and %WINDOWS% syntaxes.
|
||||
func replaceEnvVars(s string) string {
|
||||
s = replaceEnvReferences(s, "{%", "%}")
|
||||
s = replaceEnvReferences(s, "{$", "}")
|
||||
return s
|
||||
}
|
||||
|
||||
// replaceEnvReferences performs the actual replacement of env variables
|
||||
// in s, given the placeholder start and placeholder end strings.
|
||||
func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
index := strings.Index(s, refStart)
|
||||
for index != -1 {
|
||||
endIndex := strings.Index(s[index:], refEnd)
|
||||
if endIndex == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
endIndex += index
|
||||
if endIndex > index+len(refStart) {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
index = strings.Index(s, refStart)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ServerBlock associates any number of keys (usually addresses
|
||||
// of some sort) with tokens (grouped by directive name).
|
||||
type ServerBlock struct {
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) isSnippet() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
|
||||
return true, strings.TrimSuffix(keys[0][1:], ")")
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// read and store everything in a block for later replay.
|
||||
func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// TODO: disallow imports in snippets for simplicity at import time
|
||||
// snippet must have curlies.
|
||||
err := p.openCurlyBrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := 1
|
||||
tokens := []Token{}
|
||||
for p.Next() {
|
||||
if p.Val() == "}" {
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.Val() == "{" {
|
||||
count++
|
||||
}
|
||||
tokens = append(tokens, p.tokens[p.cursor])
|
||||
}
|
||||
// make sure we're matched up
|
||||
if count != 0 {
|
||||
return nil, p.SyntaxErr("}")
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"unicode"
|
||||
|
||||
"github.com/flynn/go-shlex"
|
||||
)
|
||||
|
||||
var runtimeGoos = runtime.GOOS
|
||||
|
||||
// SplitCommandAndArgs takes a command string and parses it shell-style into the
|
||||
// command and its separate arguments.
|
||||
func SplitCommandAndArgs(command string) (cmd string, args []string, err error) {
|
||||
var parts []string
|
||||
|
||||
if runtimeGoos == "windows" {
|
||||
parts = parseWindowsCommand(command) // parse it Windows-style
|
||||
} else {
|
||||
parts, err = parseUnixCommand(command) // parse it Unix-style
|
||||
if err != nil {
|
||||
err = errors.New("error parsing command: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
err = errors.New("no command contained in '" + command + "'")
|
||||
return
|
||||
}
|
||||
|
||||
cmd = parts[0]
|
||||
if len(parts) > 1 {
|
||||
args = parts[1:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseUnixCommand parses a unix style command line and returns the
|
||||
// command and its arguments or an error
|
||||
func parseUnixCommand(cmd string) ([]string, error) {
|
||||
return shlex.Split(cmd)
|
||||
}
|
||||
|
||||
// parseWindowsCommand parses windows command lines and
|
||||
// returns the command and the arguments as an array. It
|
||||
// should be able to parse commonly used command lines.
|
||||
// Only basic syntax is supported:
|
||||
// - spaces in double quotes are not token delimiters
|
||||
// - double quotes are escaped by either backspace or another double quote
|
||||
// - except for the above case backspaces are path separators (not special)
|
||||
//
|
||||
// Many sources point out that escaping quotes using backslash can be unsafe.
|
||||
// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 )
|
||||
//
|
||||
// This function has to be used on Windows instead
|
||||
// of the shlex package because this function treats backslash
|
||||
// characters properly.
|
||||
func parseWindowsCommand(cmd string) []string {
|
||||
const backslash = '\\'
|
||||
const quote = '"'
|
||||
|
||||
var parts []string
|
||||
var part string
|
||||
var inQuotes bool
|
||||
var lastRune rune
|
||||
|
||||
for i, ch := range cmd {
|
||||
|
||||
if i != 0 {
|
||||
lastRune = rune(cmd[i-1])
|
||||
}
|
||||
|
||||
if ch == backslash {
|
||||
// put it in the part - for now we don't know if it's an
|
||||
// escaping char or path separator
|
||||
part += string(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == quote {
|
||||
if lastRune == backslash {
|
||||
// remove the backslash from the part and add the escaped quote instead
|
||||
part = part[:len(part)-1]
|
||||
part += string(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if lastRune == quote {
|
||||
// revert the last change of the inQuotes state
|
||||
// it was an escaping quote
|
||||
inQuotes = !inQuotes
|
||||
part += string(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
// normal escaping quotes
|
||||
inQuotes = !inQuotes
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 {
|
||||
parts = append(parts, part)
|
||||
part = ""
|
||||
continue
|
||||
}
|
||||
|
||||
part += string(ch)
|
||||
}
|
||||
|
||||
if len(part) > 0 {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/caddy/caddyfile"
|
||||
)
|
||||
|
||||
// Controller is given to the setup function of directives which
|
||||
// gives them access to be able to read tokens with which to
|
||||
// configure themselves. It also stores state for the setup
|
||||
// functions, can get the current context, and can be used to
|
||||
// identify a particular server block using the Key field.
|
||||
type Controller struct {
|
||||
caddyfile.Dispenser
|
||||
|
||||
// The instance in which the setup is occurring
|
||||
instance *Instance
|
||||
|
||||
// Key is the key from the top of the server block, usually
|
||||
// an address, hostname, or identifier of some sort.
|
||||
Key string
|
||||
|
||||
// OncePerServerBlock is a function that executes f
|
||||
// exactly once per server block, no matter how many
|
||||
// hosts are associated with it. If it is the first
|
||||
// time, the function f is executed immediately
|
||||
// (not deferred) and may return an error which is
|
||||
// returned by OncePerServerBlock.
|
||||
OncePerServerBlock func(f func() error) error
|
||||
|
||||
// ServerBlockIndex is the 0-based index of the
|
||||
// server block as it appeared in the input.
|
||||
ServerBlockIndex int
|
||||
|
||||
// ServerBlockKeyIndex is the 0-based index of this
|
||||
// key as it appeared in the input at the head of the
|
||||
// server block.
|
||||
ServerBlockKeyIndex int
|
||||
|
||||
// ServerBlockKeys is a list of keys that are
|
||||
// associated with this server block. All these
|
||||
// keys, consequently, share the same tokens.
|
||||
ServerBlockKeys []string
|
||||
|
||||
// ServerBlockStorage is used by a directive's
|
||||
// setup function to persist state between all
|
||||
// the keys on a server block.
|
||||
ServerBlockStorage interface{}
|
||||
}
|
||||
|
||||
// ServerType gets the name of the server type that is being set up.
|
||||
func (c *Controller) ServerType() string {
|
||||
return c.instance.serverType
|
||||
}
|
||||
|
||||
// OnFirstStartup adds fn to the list of callback functions to execute
|
||||
// when the server is about to be started NOT as part of a restart.
|
||||
func (c *Controller) OnFirstStartup(fn func() error) {
|
||||
c.instance.OnFirstStartup = append(c.instance.OnFirstStartup, fn)
|
||||
}
|
||||
|
||||
// OnStartup adds fn to the list of callback functions to execute
|
||||
// when the server is about to be started (including restarts).
|
||||
func (c *Controller) OnStartup(fn func() error) {
|
||||
c.instance.OnStartup = append(c.instance.OnStartup, fn)
|
||||
}
|
||||
|
||||
// OnRestart adds fn to the list of callback functions to execute
|
||||
// when the server is about to be restarted.
|
||||
func (c *Controller) OnRestart(fn func() error) {
|
||||
c.instance.OnRestart = append(c.instance.OnRestart, fn)
|
||||
}
|
||||
|
||||
// OnRestartFailed adds fn to the list of callback functions to execute
|
||||
// if the server failed to restart.
|
||||
func (c *Controller) OnRestartFailed(fn func() error) {
|
||||
c.instance.OnRestartFailed = append(c.instance.OnRestartFailed, fn)
|
||||
}
|
||||
|
||||
// OnShutdown adds fn to the list of callback functions to execute
|
||||
// when the server is about to be shut down (including restarts).
|
||||
func (c *Controller) OnShutdown(fn func() error) {
|
||||
c.instance.OnShutdown = append(c.instance.OnShutdown, fn)
|
||||
}
|
||||
|
||||
// OnFinalShutdown adds fn to the list of callback functions to execute
|
||||
// when the server is about to be shut down NOT as part of a restart.
|
||||
func (c *Controller) OnFinalShutdown(fn func() error) {
|
||||
c.instance.OnFinalShutdown = append(c.instance.OnFinalShutdown, fn)
|
||||
}
|
||||
|
||||
// Context gets the context associated with the instance associated with c.
|
||||
func (c *Controller) Context() Context {
|
||||
return c.instance.context
|
||||
}
|
||||
|
||||
// Get safely gets a value from the Instance's storage.
|
||||
func (c *Controller) Get(key interface{}) interface{} {
|
||||
c.instance.StorageMu.RLock()
|
||||
defer c.instance.StorageMu.RUnlock()
|
||||
return c.instance.Storage[key]
|
||||
}
|
||||
|
||||
// Set safely sets a value on the Instance's storage.
|
||||
func (c *Controller) Set(key, val interface{}) {
|
||||
c.instance.StorageMu.Lock()
|
||||
c.instance.Storage[key] = val
|
||||
c.instance.StorageMu.Unlock()
|
||||
}
|
||||
|
||||
// NewTestController creates a new Controller for
|
||||
// the server type and input specified. The filename
|
||||
// is "Testfile". If the server type is not empty and
|
||||
// is plugged in, a context will be created so that
|
||||
// the results of setup functions can be checked for
|
||||
// correctness.
|
||||
//
|
||||
// Used only for testing, but exported so plugins can
|
||||
// use this for convenience.
|
||||
func NewTestController(serverType, input string) *Controller {
|
||||
testInst := &Instance{serverType: serverType, Storage: make(map[interface{}]interface{})}
|
||||
if stype, err := getServerType(serverType); err == nil {
|
||||
testInst.context = stype.NewContext(testInst)
|
||||
}
|
||||
return &Controller{
|
||||
instance: testInst,
|
||||
Dispenser: caddyfile.NewDispenser("Testfile", strings.NewReader(input)),
|
||||
OncePerServerBlock: func(f func() error) error { return f() },
|
||||
}
|
||||
}
|
||||
|
|
@ -1,450 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/caddy/caddyfile"
|
||||
)
|
||||
|
||||
// These are all the registered plugins.
|
||||
var (
|
||||
// serverTypes is a map of registered server types.
|
||||
serverTypes = make(map[string]ServerType)
|
||||
|
||||
// plugins is a map of server type to map of plugin name to
|
||||
// Plugin. These are the "general" plugins that may or may
|
||||
// not be associated with a specific server type. If it's
|
||||
// applicable to multiple server types or the server type is
|
||||
// irrelevant, the key is empty string (""). But all plugins
|
||||
// must have a name.
|
||||
plugins = make(map[string]map[string]Plugin)
|
||||
|
||||
// eventHooks is a map of hook name to Hook. All hooks plugins
|
||||
// must have a name.
|
||||
eventHooks = &sync.Map{}
|
||||
|
||||
// parsingCallbacks maps server type to map of directive
|
||||
// to list of callback functions. These aren't really
|
||||
// plugins on their own, but are often registered from
|
||||
// plugins.
|
||||
parsingCallbacks = make(map[string]map[string][]ParsingCallback)
|
||||
|
||||
// caddyfileLoaders is the list of all Caddyfile loaders
|
||||
// in registration order.
|
||||
caddyfileLoaders []caddyfileLoader
|
||||
)
|
||||
|
||||
// DescribePlugins returns a string describing the registered plugins.
|
||||
func DescribePlugins() string {
|
||||
pl := ListPlugins()
|
||||
|
||||
str := ""
|
||||
for _, name := range pl["others"] {
|
||||
if len(name) > 3 {
|
||||
str += name[4:] + "\n" // drop dns. prefix caddy adds
|
||||
} else {
|
||||
str += name + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// ListPlugins makes a list of the registered plugins,
|
||||
// keyed by plugin type.
|
||||
func ListPlugins() map[string][]string {
|
||||
p := make(map[string][]string)
|
||||
|
||||
// server type plugins
|
||||
for name := range serverTypes {
|
||||
p["server_types"] = append(p["server_types"], name)
|
||||
}
|
||||
|
||||
// caddyfile loaders in registration order
|
||||
for _, loader := range caddyfileLoaders {
|
||||
p["caddyfile_loaders"] = append(p["caddyfile_loaders"], loader.name)
|
||||
}
|
||||
if defaultCaddyfileLoader.name != "" {
|
||||
p["caddyfile_loaders"] = append(p["caddyfile_loaders"], defaultCaddyfileLoader.name)
|
||||
}
|
||||
|
||||
// List the event hook plugins
|
||||
eventHooks.Range(func(k, _ interface{}) bool {
|
||||
p["event_hooks"] = append(p["event_hooks"], k.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
// alphabetize the rest of the plugins
|
||||
var others []string
|
||||
for stype, stypePlugins := range plugins {
|
||||
for name := range stypePlugins {
|
||||
var s string
|
||||
if stype != "" {
|
||||
s = stype + "."
|
||||
}
|
||||
s += name
|
||||
others = append(others, s)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(others)
|
||||
for _, name := range others {
|
||||
p["others"] = append(p["others"], name)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// ValidDirectives returns the list of all directives that are
|
||||
// recognized for the server type serverType. However, not all
|
||||
// directives may be installed. This makes it possible to give
|
||||
// more helpful error messages, like "did you mean ..." or
|
||||
// "maybe you need to plug in ...".
|
||||
func ValidDirectives(serverType string) []string {
|
||||
stype, err := getServerType(serverType)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return stype.Directives()
|
||||
}
|
||||
|
||||
// ServerListener pairs a server to its listener and/or packetconn.
|
||||
type ServerListener struct {
|
||||
server Server
|
||||
listener net.Listener
|
||||
packet net.PacketConn
|
||||
}
|
||||
|
||||
// LocalAddr returns the local network address of the packetconn. It returns
|
||||
// nil when it is not set.
|
||||
func (s ServerListener) LocalAddr() net.Addr {
|
||||
if s.packet == nil {
|
||||
return nil
|
||||
}
|
||||
return s.packet.LocalAddr()
|
||||
}
|
||||
|
||||
// Addr returns the listener's network address. It returns nil when it is
|
||||
// not set.
|
||||
func (s ServerListener) Addr() net.Addr {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
return s.listener.Addr()
|
||||
}
|
||||
|
||||
// Context is a type which carries a server type through
|
||||
// the load and setup phase; it maintains the state
|
||||
// between loading the Caddyfile, then executing its
|
||||
// directives, then making the servers for Caddy to
|
||||
// manage. Typically, such state involves configuration
|
||||
// structs, etc.
|
||||
type Context interface {
|
||||
// Called after the Caddyfile is parsed into server
|
||||
// blocks but before the directives are executed,
|
||||
// this method gives you an opportunity to inspect
|
||||
// the server blocks and prepare for the execution
|
||||
// of directives. Return the server blocks (which
|
||||
// you may modify, if desired) and an error, if any.
|
||||
// The first argument is the name or path to the
|
||||
// configuration file (Caddyfile).
|
||||
//
|
||||
// This function can be a no-op and simply return its
|
||||
// input if there is nothing to do here.
|
||||
InspectServerBlocks(string, []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error)
|
||||
|
||||
// This is what Caddy calls to make server instances.
|
||||
// By this time, all directives have been executed and,
|
||||
// presumably, the context has enough state to produce
|
||||
// server instances for Caddy to start.
|
||||
MakeServers() ([]Server, error)
|
||||
}
|
||||
|
||||
// RegisterServerType registers a server type srv by its
|
||||
// name, typeName.
|
||||
func RegisterServerType(typeName string, srv ServerType) {
|
||||
if _, ok := serverTypes[typeName]; ok {
|
||||
panic("server type already registered")
|
||||
}
|
||||
serverTypes[typeName] = srv
|
||||
}
|
||||
|
||||
// ServerType contains information about a server type.
|
||||
type ServerType struct {
|
||||
// Function that returns the list of directives, in
|
||||
// execution order, that are valid for this server
|
||||
// type. Directives should be one word if possible
|
||||
// and lower-cased.
|
||||
Directives func() []string
|
||||
|
||||
// DefaultInput returns a default config input if none
|
||||
// is otherwise loaded. This is optional, but highly
|
||||
// recommended, otherwise a blank Caddyfile will be
|
||||
// used.
|
||||
DefaultInput func() Input
|
||||
|
||||
// The function that produces a new server type context.
|
||||
// This will be called when a new Caddyfile is being
|
||||
// loaded, parsed, and executed independently of any
|
||||
// startup phases before this one. It's a way to keep
|
||||
// each set of server instances separate and to reduce
|
||||
// the amount of global state you need.
|
||||
NewContext func(inst *Instance) Context
|
||||
}
|
||||
|
||||
// Plugin is a type which holds information about a plugin.
|
||||
type Plugin struct {
|
||||
// ServerType is the type of server this plugin is for.
|
||||
// Can be empty if not applicable, or if the plugin
|
||||
// can associate with any server type.
|
||||
ServerType string
|
||||
|
||||
// Action is the plugin's setup function, if associated
|
||||
// with a directive in the Caddyfile.
|
||||
Action SetupFunc
|
||||
}
|
||||
|
||||
// RegisterPlugin plugs in plugin. All plugins should register
|
||||
// themselves, even if they do not perform an action associated
|
||||
// with a directive. It is important for the process to know
|
||||
// which plugins are available.
|
||||
//
|
||||
// The plugin MUST have a name: lower case and one word.
|
||||
// If this plugin has an action, it must be the name of
|
||||
// the directive that invokes it. A name is always required
|
||||
// and must be unique for the server type.
|
||||
func RegisterPlugin(name string, plugin Plugin) {
|
||||
if name == "" {
|
||||
panic("plugin must have a name")
|
||||
}
|
||||
if _, ok := plugins[plugin.ServerType]; !ok {
|
||||
plugins[plugin.ServerType] = make(map[string]Plugin)
|
||||
}
|
||||
if _, dup := plugins[plugin.ServerType][name]; dup {
|
||||
panic("plugin named " + name + " already registered for server type " + plugin.ServerType)
|
||||
}
|
||||
plugins[plugin.ServerType][name] = plugin
|
||||
}
|
||||
|
||||
// EventName represents the name of an event used with event hooks.
|
||||
type EventName string
|
||||
|
||||
// Define names for the various events
|
||||
const (
|
||||
StartupEvent EventName = "startup"
|
||||
ShutdownEvent = "shutdown"
|
||||
CertRenewEvent = "certrenew"
|
||||
InstanceStartupEvent = "instancestartup"
|
||||
InstanceRestartEvent = "instancerestart"
|
||||
)
|
||||
|
||||
// EventHook is a type which holds information about a startup hook plugin.
|
||||
type EventHook func(eventType EventName, eventInfo interface{}) error
|
||||
|
||||
// RegisterEventHook plugs in hook. All the hooks should register themselves
|
||||
// and they must have a name.
|
||||
func RegisterEventHook(name string, hook EventHook) {
|
||||
if name == "" {
|
||||
panic("event hook must have a name")
|
||||
}
|
||||
_, dup := eventHooks.LoadOrStore(name, hook)
|
||||
if dup {
|
||||
panic("hook named " + name + " already registered")
|
||||
}
|
||||
}
|
||||
|
||||
// EmitEvent executes the different hooks passing the EventType as an
|
||||
// argument. This is a blocking function. Hook developers should
|
||||
// use 'go' keyword if they don't want to block Caddy.
|
||||
func EmitEvent(event EventName, info interface{}) {
|
||||
eventHooks.Range(func(k, v interface{}) bool {
|
||||
err := v.(EventHook)(event, info)
|
||||
if err != nil {
|
||||
log.Printf("error on '%s' hook: %v", k.(string), err)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// cloneEventHooks return a clone of the event hooks *sync.Map
|
||||
func cloneEventHooks() *sync.Map {
|
||||
c := &sync.Map{}
|
||||
eventHooks.Range(func(k, v interface{}) bool {
|
||||
c.Store(k, v)
|
||||
return true
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
// purgeEventHooks purges all event hooks from the map
|
||||
func purgeEventHooks() {
|
||||
eventHooks.Range(func(k, _ interface{}) bool {
|
||||
eventHooks.Delete(k)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// restoreEventHooks restores eventHooks with a provided *sync.Map
|
||||
func restoreEventHooks(m *sync.Map) {
|
||||
// Purge old event hooks
|
||||
purgeEventHooks()
|
||||
|
||||
// Restore event hooks
|
||||
m.Range(func(k, v interface{}) bool {
|
||||
eventHooks.Store(k, v)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// ParsingCallback is a function that is called after
|
||||
// a directive's setup functions have been executed
|
||||
// for all the server blocks.
|
||||
type ParsingCallback func(Context) error
|
||||
|
||||
// RegisterParsingCallback registers callback to be called after
|
||||
// executing the directive afterDir for server type serverType.
|
||||
func RegisterParsingCallback(serverType, afterDir string, callback ParsingCallback) {
|
||||
if _, ok := parsingCallbacks[serverType]; !ok {
|
||||
parsingCallbacks[serverType] = make(map[string][]ParsingCallback)
|
||||
}
|
||||
parsingCallbacks[serverType][afterDir] = append(parsingCallbacks[serverType][afterDir], callback)
|
||||
}
|
||||
|
||||
// SetupFunc is used to set up a plugin, or in other words,
|
||||
// execute a directive. It will be called once per key for
|
||||
// each server block it appears in.
|
||||
type SetupFunc func(c *Controller) error
|
||||
|
||||
// DirectiveAction gets the action for directive dir of
|
||||
// server type serverType.
|
||||
func DirectiveAction(serverType, dir string) (SetupFunc, error) {
|
||||
if stypePlugins, ok := plugins[serverType]; ok {
|
||||
if plugin, ok := stypePlugins[dir]; ok {
|
||||
return plugin.Action, nil
|
||||
}
|
||||
}
|
||||
if genericPlugins, ok := plugins[""]; ok {
|
||||
if plugin, ok := genericPlugins[dir]; ok {
|
||||
return plugin.Action, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no action found for directive '%s' with server type '%s' (missing a plugin?)",
|
||||
dir, serverType)
|
||||
}
|
||||
|
||||
// Loader is a type that can load a Caddyfile.
|
||||
// It is passed the name of the server type.
|
||||
// It returns an error only if something went
|
||||
// wrong, not simply if there is no Caddyfile
|
||||
// for this loader to load.
|
||||
//
|
||||
// A Loader should only load the Caddyfile if
|
||||
// a certain condition or requirement is met,
|
||||
// as returning a non-nil Input value along with
|
||||
// another Loader will result in an error.
|
||||
// In other words, loading the Caddyfile must
|
||||
// be deliberate & deterministic, not haphazard.
|
||||
//
|
||||
// The exception is the default Caddyfile loader,
|
||||
// which will be called only if no other Caddyfile
|
||||
// loaders return a non-nil Input. The default
|
||||
// loader may always return an Input value.
|
||||
type Loader interface {
|
||||
Load(serverType string) (Input, error)
|
||||
}
|
||||
|
||||
// LoaderFunc is a convenience type similar to http.HandlerFunc
|
||||
// that allows you to use a plain function as a Load() method.
|
||||
type LoaderFunc func(serverType string) (Input, error)
|
||||
|
||||
// Load loads a Caddyfile.
|
||||
func (lf LoaderFunc) Load(serverType string) (Input, error) {
|
||||
return lf(serverType)
|
||||
}
|
||||
|
||||
// RegisterCaddyfileLoader registers loader named name.
|
||||
func RegisterCaddyfileLoader(name string, loader Loader) {
|
||||
caddyfileLoaders = append(caddyfileLoaders, caddyfileLoader{name: name, loader: loader})
|
||||
}
|
||||
|
||||
// SetDefaultCaddyfileLoader registers loader by name
|
||||
// as the default Caddyfile loader if no others produce
|
||||
// a Caddyfile. If another Caddyfile loader has already
|
||||
// been set as the default, this replaces it.
|
||||
//
|
||||
// Do not call RegisterCaddyfileLoader on the same
|
||||
// loader; that would be redundant.
|
||||
func SetDefaultCaddyfileLoader(name string, loader Loader) {
|
||||
defaultCaddyfileLoader = caddyfileLoader{name: name, loader: loader}
|
||||
}
|
||||
|
||||
// loadCaddyfileInput iterates the registered Caddyfile loaders
|
||||
// and, if needed, calls the default loader, to load a Caddyfile.
|
||||
// It is an error if any of the loaders return an error or if
|
||||
// more than one loader returns a Caddyfile.
|
||||
func loadCaddyfileInput(serverType string) (Input, error) {
|
||||
var loadedBy string
|
||||
var caddyfileToUse Input
|
||||
for _, l := range caddyfileLoaders {
|
||||
cdyfile, err := l.loader.Load(serverType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading Caddyfile via %s: %v", l.name, err)
|
||||
}
|
||||
if cdyfile != nil {
|
||||
if caddyfileToUse != nil {
|
||||
return nil, fmt.Errorf("Caddyfile loaded multiple times; first by %s, then by %s", loadedBy, l.name)
|
||||
}
|
||||
loaderUsed = l
|
||||
caddyfileToUse = cdyfile
|
||||
loadedBy = l.name
|
||||
}
|
||||
}
|
||||
if caddyfileToUse == nil && defaultCaddyfileLoader.loader != nil {
|
||||
cdyfile, err := defaultCaddyfileLoader.loader.Load(serverType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cdyfile != nil {
|
||||
loaderUsed = defaultCaddyfileLoader
|
||||
caddyfileToUse = cdyfile
|
||||
}
|
||||
}
|
||||
return caddyfileToUse, nil
|
||||
}
|
||||
|
||||
// OnProcessExit is a list of functions to run when the process
|
||||
// exits -- they are ONLY for cleanup and should not block,
|
||||
// return errors, or do anything fancy. They will be run with
|
||||
// every signal, even if "shutdown callbacks" are not executed.
|
||||
// This variable must only be modified in the main goroutine
|
||||
// from init() functions.
|
||||
var OnProcessExit []func()
|
||||
|
||||
// caddyfileLoader pairs the name of a loader to the loader.
|
||||
type caddyfileLoader struct {
|
||||
name string
|
||||
loader Loader
|
||||
}
|
||||
|
||||
var (
|
||||
defaultCaddyfileLoader caddyfileLoader // the default loader if all else fail
|
||||
loaderUsed caddyfileLoader // the loader that was used (relevant for reloads)
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build windows plan9 nacl js
|
||||
|
||||
package caddy
|
||||
|
||||
// checkFdlimit issues a warning if the OS limit for
|
||||
// max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !windows,!plan9,!nacl,!js
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// checkFdlimit issues a warning if the OS limit for
|
||||
// max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 8192
|
||||
|
||||
// Warn if ulimit is too low for production sites
|
||||
rlimit := &syscall.Rlimit{}
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimit)
|
||||
if err == nil && rlimit.Cur < min {
|
||||
fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+
|
||||
"At least %d is recommended. Fix with `ulimit -n %d`.\n", rlimit.Cur, min, min)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TrapSignals create signal handlers for all applicable signals for this
|
||||
// system. If your Go program uses signals, this is a rather invasive
|
||||
// function; best to implement them yourself in that case. Signals are not
|
||||
// required for the caddy package to function properly, but this is a
|
||||
// convenient way to allow the user to control this part of your program.
|
||||
func TrapSignals() {
|
||||
trapSignalsCrossPlatform()
|
||||
trapSignalsPosix()
|
||||
}
|
||||
|
||||
// trapSignalsCrossPlatform captures SIGINT, which triggers forceful
|
||||
// shutdown that executes shutdown callbacks first. A second interrupt
|
||||
// signal will exit the process immediately.
|
||||
func trapSignalsCrossPlatform() {
|
||||
go func() {
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt)
|
||||
|
||||
for i := 0; true; i++ {
|
||||
<-shutdown
|
||||
|
||||
if i > 0 {
|
||||
log.Println("[INFO] SIGINT: Force quit")
|
||||
for _, f := range OnProcessExit {
|
||||
f() // important cleanup actions only
|
||||
}
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
log.Println("[INFO] SIGINT: Shutting down")
|
||||
|
||||
// important cleanup actions before shutdown callbacks
|
||||
for _, f := range OnProcessExit {
|
||||
f()
|
||||
}
|
||||
|
||||
go func() {
|
||||
os.Exit(executeShutdownCallbacks("SIGINT"))
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// executeShutdownCallbacks executes the shutdown callbacks as initiated
|
||||
// by signame. It logs any errors and returns the recommended exit status.
|
||||
// This function is idempotent; subsequent invocations always return 0.
|
||||
func executeShutdownCallbacks(signame string) (exitCode int) {
|
||||
shutdownCallbacksOnce.Do(func() {
|
||||
// execute third-party shutdown hooks
|
||||
EmitEvent(ShutdownEvent, signame)
|
||||
|
||||
errs := allShutdownCallbacks()
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Printf("[ERROR] %s shutdown: %v", signame, err)
|
||||
}
|
||||
exitCode = 4
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// allShutdownCallbacks executes all the shutdown callbacks
|
||||
// for all the instances, and returns all the errors generated
|
||||
// during their execution. An error executing one shutdown
|
||||
// callback does not stop execution of others. Only one shutdown
|
||||
// callback is executed at a time.
|
||||
func allShutdownCallbacks() []error {
|
||||
var errs []error
|
||||
instancesMu.Lock()
|
||||
for _, inst := range instances {
|
||||
errs = append(errs, inst.ShutdownCallbacks()...)
|
||||
}
|
||||
instancesMu.Unlock()
|
||||
return errs
|
||||
}
|
||||
|
||||
// shutdownCallbacksOnce ensures that shutdown callbacks
|
||||
// for all instances are only executed once.
|
||||
var shutdownCallbacksOnce sync.Once
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build windows plan9 nacl js
|
||||
|
||||
package caddy
|
||||
|
||||
func trapSignalsPosix() {}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !windows,!plan9,!nacl,!js
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// trapSignalsPosix captures POSIX-only signals.
|
||||
func trapSignalsPosix() {
|
||||
go func() {
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
|
||||
|
||||
for sig := range sigchan {
|
||||
switch sig {
|
||||
case syscall.SIGQUIT:
|
||||
log.Println("[INFO] SIGQUIT: Quitting process immediately")
|
||||
for _, f := range OnProcessExit {
|
||||
f() // only perform important cleanup actions
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
case syscall.SIGTERM:
|
||||
log.Println("[INFO] SIGTERM: Shutting down servers then terminating")
|
||||
exitCode := executeShutdownCallbacks("SIGTERM")
|
||||
for _, f := range OnProcessExit {
|
||||
f() // only perform important cleanup actions
|
||||
}
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGTERM stop: %v", err)
|
||||
exitCode = 3
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
|
||||
case syscall.SIGUSR1:
|
||||
log.Println("[INFO] SIGUSR1: Reloading")
|
||||
|
||||
// Start with the existing Caddyfile
|
||||
caddyfileToUse, inst, err := getCurrentCaddyfile()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGUSR1: %v", err)
|
||||
continue
|
||||
}
|
||||
if loaderUsed.loader == nil {
|
||||
// This also should never happen
|
||||
log.Println("[ERROR] SIGUSR1: no Caddyfile loader with which to reload Caddyfile")
|
||||
continue
|
||||
}
|
||||
|
||||
// Load the updated Caddyfile
|
||||
newCaddyfile, err := loaderUsed.loader.Load(inst.serverType)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGUSR1: loading updated Caddyfile: %v", err)
|
||||
continue
|
||||
}
|
||||
if newCaddyfile != nil {
|
||||
caddyfileToUse = newCaddyfile
|
||||
}
|
||||
|
||||
// Backup old event hooks
|
||||
oldEventHooks := cloneEventHooks()
|
||||
|
||||
// Purge the old event hooks
|
||||
purgeEventHooks()
|
||||
|
||||
// Kick off the restart; our work is done
|
||||
EmitEvent(InstanceRestartEvent, nil)
|
||||
_, err = inst.Restart(caddyfileToUse)
|
||||
if err != nil {
|
||||
restoreEventHooks(oldEventHooks)
|
||||
|
||||
log.Printf("[ERROR] SIGUSR1: %v", err)
|
||||
}
|
||||
|
||||
case syscall.SIGUSR2:
|
||||
log.Println("[INFO] SIGUSR2: Upgrading")
|
||||
if err := Upgrade(); err != nil {
|
||||
log.Printf("[ERROR] SIGUSR2: upgrading: %v", err)
|
||||
}
|
||||
|
||||
case syscall.SIGHUP:
|
||||
// ignore; this signal is sometimes sent outside of the user's control
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// register CaddyfileInput with gob so it knows into
|
||||
// which concrete type to decode an Input interface
|
||||
gob.Register(CaddyfileInput{})
|
||||
}
|
||||
|
||||
// IsUpgrade returns true if this process is part of an upgrade
|
||||
// where a parent caddy process spawned this one to upgrade
|
||||
// the binary.
|
||||
func IsUpgrade() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return isUpgrade
|
||||
}
|
||||
|
||||
// Upgrade re-launches the process, preserving the listeners
|
||||
// for a graceful upgrade. It does NOT load new configuration;
|
||||
// it only starts the process anew with the current config.
|
||||
// This makes it possible to perform zero-downtime binary upgrades.
|
||||
//
|
||||
// TODO: For more information when debugging, see:
|
||||
// https://forum.golangbridge.org/t/bind-address-already-in-use-even-after-listener-closed/1510?u=matt
|
||||
// https://github.com/mholt/shared-conn
|
||||
func Upgrade() error {
|
||||
log.Println("[INFO] Upgrading")
|
||||
|
||||
// use existing Caddyfile; do not change configuration during upgrade
|
||||
currentCaddyfile, _, err := getCurrentCaddyfile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(os.Args) == 0 { // this should never happen, but...
|
||||
os.Args = []string{""}
|
||||
}
|
||||
|
||||
// tell the child that it's a restart
|
||||
env := os.Environ()
|
||||
if !IsUpgrade() {
|
||||
env = append(env, "CADDY__UPGRADE=1")
|
||||
}
|
||||
|
||||
// prepare our payload to the child process
|
||||
cdyfileGob := transferGob{
|
||||
ListenerFds: make(map[string]uintptr),
|
||||
Caddyfile: currentCaddyfile,
|
||||
}
|
||||
|
||||
// prepare a pipe to the fork's stdin so it can get the Caddyfile
|
||||
rpipe, wpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare a pipe that the child process will use to communicate
|
||||
// its success with us by sending > 0 bytes
|
||||
sigrpipe, sigwpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// pass along relevant file descriptors to child process; ordering
|
||||
// is very important since we rely on these being in certain positions.
|
||||
extraFiles := []*os.File{sigwpipe} // fd 3
|
||||
|
||||
// add file descriptors of all the sockets
|
||||
for i, j := 0, 0; ; i++ {
|
||||
instancesMu.Lock()
|
||||
if i >= len(instances) {
|
||||
instancesMu.Unlock()
|
||||
break
|
||||
}
|
||||
inst := instances[i]
|
||||
instancesMu.Unlock()
|
||||
|
||||
for _, s := range inst.servers {
|
||||
gs, gracefulOk := s.server.(GracefulServer)
|
||||
ln, lnOk := s.listener.(Listener)
|
||||
pc, pcOk := s.packet.(PacketConn)
|
||||
if gracefulOk {
|
||||
if lnOk {
|
||||
lnFile, _ := ln.File()
|
||||
extraFiles = append(extraFiles, lnFile)
|
||||
cdyfileGob.ListenerFds["tcp"+gs.Address()] = uintptr(4 + j) // 4 fds come before any of the listeners
|
||||
j++
|
||||
}
|
||||
if pcOk {
|
||||
pcFile, _ := pc.File()
|
||||
extraFiles = append(extraFiles, pcFile)
|
||||
cdyfileGob.ListenerFds["udp"+gs.Address()] = uintptr(4 + j) // 4 fds come before any of the listeners
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set up the command
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
cmd.Stdin = rpipe // fd 0
|
||||
cmd.Stdout = os.Stdout // fd 1
|
||||
cmd.Stderr = os.Stderr // fd 2
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Env = env
|
||||
|
||||
// spawn the child process
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// immediately close our dup'ed fds and the write end of our signal pipe
|
||||
for _, f := range extraFiles {
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// feed Caddyfile to the child
|
||||
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wpipe.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// determine whether child startup succeeded
|
||||
answer, readErr := ioutil.ReadAll(sigrpipe)
|
||||
if len(answer) == 0 {
|
||||
cmdErr := cmd.Wait() // get exit status
|
||||
errStr := fmt.Sprintf("child failed to initialize: %v", cmdErr)
|
||||
if readErr != nil {
|
||||
errStr += fmt.Sprintf(" - additionally, error communicating with child process: %v", readErr)
|
||||
}
|
||||
return fmt.Errorf(errStr)
|
||||
}
|
||||
|
||||
// looks like child is successful; we can exit gracefully.
|
||||
log.Println("[INFO] Upgrade finished")
|
||||
return Stop()
|
||||
}
|
||||
|
||||
// getCurrentCaddyfile gets the Caddyfile used by the
|
||||
// current (first) Instance and returns both of them.
|
||||
func getCurrentCaddyfile() (Input, *Instance, error) {
|
||||
instancesMu.Lock()
|
||||
if len(instances) == 0 {
|
||||
instancesMu.Unlock()
|
||||
return nil, nil, fmt.Errorf("no server instances are fully running")
|
||||
}
|
||||
inst := instances[0]
|
||||
instancesMu.Unlock()
|
||||
|
||||
currentCaddyfile := inst.caddyfileInput
|
||||
if currentCaddyfile == nil {
|
||||
// hmm, did spawning process forget to close stdin? Anyhow, this is unusual.
|
||||
return nil, inst, fmt.Errorf("no Caddyfile to reload (was stdin left open?)")
|
||||
}
|
||||
return currentCaddyfile, inst, nil
|
||||
}
|
||||
|
||||
// signalSuccessToParent tells the parent our status using pipe at index 3.
|
||||
// If this process is not a restart, this function does nothing.
|
||||
// Calling this function once this process has successfully initialized
|
||||
// is vital so that the parent process can unblock and kill itself.
|
||||
// This function is idempotent; it executes at most once per process.
|
||||
func signalSuccessToParent() {
|
||||
signalParentOnce.Do(func() {
|
||||
if IsUpgrade() {
|
||||
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
|
||||
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
|
||||
}
|
||||
ppipe.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// signalParentOnce is used to make sure that the parent is only
|
||||
// signaled once; doing so more than once breaks whatever socket is
|
||||
// at fd 4 (TODO: the reason for this is still unclear - to reproduce,
|
||||
// call Stop() and Start() in succession at least once after a
|
||||
// restart, then try loading first host of Caddyfile in the browser
|
||||
// - this was pre-v0.9; this code and godoc is borrowed from the
|
||||
// implementation then, but I'm not sure if it's been fixed yet, as
|
||||
// of v0.10.7). Do not use this directly; call signalSuccessToParent
|
||||
// instead.
|
||||
var signalParentOnce sync.Once
|
||||
|
||||
// transferGob is used if this is a child process as part of
|
||||
// a graceful upgrade; it is used to map listeners to their
|
||||
// index in the list of inherited file descriptors. This
|
||||
// variable is not safe for concurrent access.
|
||||
var loadedGob transferGob
|
||||
|
||||
// transferGob maps bind address to index of the file descriptor
|
||||
// in the Files array passed to the child process. It also contains
|
||||
// the Caddyfile contents and any other state needed by the new process.
|
||||
// Used only during graceful upgrades.
|
||||
type transferGob struct {
|
||||
ListenerFds map[string]uintptr
|
||||
Caddyfile Input
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2016-2020 The CoreDNS authors and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type zoneAddr struct {
|
||||
Zone string
|
||||
Port string
|
||||
Transport string // dns, tls or grpc
|
||||
Address string // used for bound zoneAddr - validation of overlapping
|
||||
}
|
||||
|
||||
// String returns the string representation of z.
|
||||
func (z zoneAddr) String() string {
|
||||
s := z.Transport + "://" + z.Zone + ":" + z.Port
|
||||
if z.Address != "" {
|
||||
s += " on " + z.Address
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SplitProtocolHostPort splits a full formed address like "dns://[::1]:53" into parts.
|
||||
func SplitProtocolHostPort(address string) (protocol string, ip string, port string, err error) {
|
||||
parts := strings.Split(address, "://")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
ip, port, err := net.SplitHostPort(parts[0])
|
||||
return "", ip, port, err
|
||||
case 2:
|
||||
ip, port, err := net.SplitHostPort(parts[1])
|
||||
return parts[0], ip, port, err
|
||||
default:
|
||||
return "", "", "", fmt.Errorf("provided value is not in an address format : %s", address)
|
||||
}
|
||||
}
|
||||
|
||||
type zoneOverlap struct {
|
||||
registeredAddr map[zoneAddr]zoneAddr // each zoneAddr is registered once by its key
|
||||
unboundOverlap map[zoneAddr]zoneAddr // the "no bind" equiv ZoneAddr is registered by its original key
|
||||
}
|
||||
|
||||
func newOverlapZone() *zoneOverlap {
|
||||
return &zoneOverlap{registeredAddr: make(map[zoneAddr]zoneAddr), unboundOverlap: make(map[zoneAddr]zoneAddr)}
|
||||
}
|
||||
|
||||
// registerAndCheck adds a new zoneAddr for validation, it returns information about existing or overlapping with already registered
|
||||
// we consider that an unbound address is overlapping all bound addresses for same zone, same port
|
||||
func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) {
|
||||
existingZone, overlappingZone = zo.check(z)
|
||||
if existingZone != nil || overlappingZone != nil {
|
||||
return existingZone, overlappingZone
|
||||
}
|
||||
// there is no overlap, keep the current zoneAddr for future checks
|
||||
zo.registeredAddr[z] = z
|
||||
zo.unboundOverlap[z.unbound()] = z
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// check validates a zoneAddr for overlap without registering it
|
||||
func (zo *zoneOverlap) check(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) {
|
||||
if exist, ok := zo.registeredAddr[z]; ok {
|
||||
// exact same zone already registered
|
||||
return &exist, nil
|
||||
}
|
||||
uz := z.unbound()
|
||||
if already, ok := zo.unboundOverlap[uz]; ok {
|
||||
if z.Address == "" {
|
||||
// current is not bound to an address, but there is already another zone with a bind address registered
|
||||
return nil, &already
|
||||
}
|
||||
if _, ok := zo.registeredAddr[uz]; ok {
|
||||
// current zone is bound to an address, but there is already an overlapping zone+port with no bind address
|
||||
return nil, &uz
|
||||
}
|
||||
}
|
||||
// there is no overlap
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// unbound returns an unbound version of the zoneAddr
|
||||
func (z zoneAddr) unbound() zoneAddr {
|
||||
return zoneAddr{Zone: z.Zone, Address: "", Port: z.Port, Transport: z.Transport}
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/request"
|
||||
)
|
||||
|
||||
// Config configuration for a single server.
|
||||
type Config struct {
|
||||
// The zone of the site.
|
||||
Zone string
|
||||
|
||||
// one or several hostnames to bind the server to.
|
||||
// defaults to a single empty string that denote the wildcard address
|
||||
ListenHosts []string
|
||||
|
||||
// The port to listen on.
|
||||
Port string
|
||||
|
||||
// The number of servers that will listen on one port.
|
||||
// By default, one server will be running.
|
||||
NumSockets int
|
||||
|
||||
// Root points to a base directory we find user defined "things".
|
||||
// First consumer is the file plugin to looks for zone files in this place.
|
||||
Root string
|
||||
|
||||
// Debug controls the panic/recover mechanism that is enabled by default.
|
||||
Debug bool
|
||||
|
||||
// Stacktrace controls including stacktrace as part of log from recover mechanism, it is disabled by default.
|
||||
Stacktrace bool
|
||||
|
||||
// The transport we implement, normally just "dns" over TCP/UDP, but could be
|
||||
// DNS-over-TLS or DNS-over-gRPC.
|
||||
Transport string
|
||||
|
||||
// If this function is not nil it will be used to inspect and validate
|
||||
// HTTP requests. Although this isn't referenced in-tree, external plugins
|
||||
// may depend on it.
|
||||
HTTPRequestValidateFunc func(*http.Request) bool
|
||||
|
||||
// FilterFuncs is used to further filter access
|
||||
// to this handler. E.g. to limit access to a reverse zone
|
||||
// on a non-octet boundary, i.e. /17
|
||||
FilterFuncs []FilterFunc
|
||||
|
||||
// ViewName is the name of the Viewer PLugin defined in the Config
|
||||
ViewName string
|
||||
|
||||
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// MaxQUICStreams defines the maximum number of concurrent QUIC streams for a QUIC server.
|
||||
// This is nil if not specified, allowing for a default to be used.
|
||||
MaxQUICStreams *int
|
||||
|
||||
// MaxQUICWorkerPoolSize defines the size of the worker pool for processing QUIC streams.
|
||||
// This is nil if not specified, allowing for a default to be used.
|
||||
MaxQUICWorkerPoolSize *int
|
||||
|
||||
// Timeouts for TCP, TLS and HTTPS servers.
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
|
||||
// TSIG secrets, [name]key.
|
||||
TsigSecret map[string]string
|
||||
|
||||
// Plugin stack.
|
||||
Plugin []plugin.Plugin
|
||||
|
||||
// Compiled plugin stack.
|
||||
pluginChain plugin.Handler
|
||||
|
||||
// Plugin interested in announcing that they exist, so other plugin can call methods
|
||||
// on them should register themselves here. The name should be the name as return by the
|
||||
// Handler's Name method.
|
||||
registry map[string]plugin.Handler
|
||||
|
||||
// firstConfigInBlock is used to reference the first config in a server block, for the
|
||||
// purpose of sharing single instance of each plugin among all zones in a server block.
|
||||
firstConfigInBlock *Config
|
||||
|
||||
// metaCollector references the first MetadataCollector plugin, if one exists
|
||||
metaCollector MetadataCollector
|
||||
}
|
||||
|
||||
// FilterFunc is a function that filters requests from the Config
|
||||
type FilterFunc func(context.Context, *request.Request) bool
|
||||
|
||||
// keyForConfig builds a key for identifying the configs during setup time
|
||||
func keyForConfig(blocIndex int, blocKeyIndex int) string {
|
||||
return fmt.Sprintf("%d:%d", blocIndex, blocKeyIndex)
|
||||
}
|
||||
|
||||
// GetConfig gets the Config that corresponds to c.
|
||||
// If none exist nil is returned.
|
||||
func GetConfig(c *caddy.Controller) *Config {
|
||||
ctx := c.Context().(*dnsContext)
|
||||
key := keyForConfig(c.ServerBlockIndex, c.ServerBlockKeyIndex)
|
||||
if cfg, ok := ctx.keysToConfigs[key]; ok {
|
||||
return cfg
|
||||
}
|
||||
// we should only get here during tests because directive
|
||||
// actions typically skip the server blocks where we make
|
||||
// the configs.
|
||||
ctx.saveConfig(key, &Config{ListenHosts: []string{""}})
|
||||
return GetConfig(c)
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DoHWriter is a dns.ResponseWriter that adds more specific LocalAddr and RemoteAddr methods.
|
||||
type DoHWriter struct {
|
||||
// raddr is the remote's address. This can be optionally set.
|
||||
raddr net.Addr
|
||||
// laddr is our address. This can be optionally set.
|
||||
laddr net.Addr
|
||||
|
||||
// request is the HTTP request we're currently handling.
|
||||
request *http.Request
|
||||
|
||||
// Msg is a response to be written to the client.
|
||||
Msg *dns.Msg
|
||||
}
|
||||
|
||||
// WriteMsg stores the message to be written to the client.
|
||||
func (d *DoHWriter) WriteMsg(m *dns.Msg) error {
|
||||
d.Msg = m
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write stores the message to be written to the client.
|
||||
func (d *DoHWriter) Write(b []byte) (int, error) {
|
||||
d.Msg = new(dns.Msg)
|
||||
return len(b), d.Msg.Unpack(b)
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote address.
|
||||
func (d *DoHWriter) RemoteAddr() net.Addr {
|
||||
return d.raddr
|
||||
}
|
||||
|
||||
// LocalAddr returns the local address.
|
||||
func (d *DoHWriter) LocalAddr() net.Addr {
|
||||
return d.laddr
|
||||
}
|
||||
|
||||
// Request returns the HTTP request.
|
||||
func (d *DoHWriter) Request() *http.Request {
|
||||
return d.request
|
||||
}
|
||||
|
||||
// Close no-op implementation.
|
||||
func (d *DoHWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TsigStatus no-op implementation.
|
||||
func (d *DoHWriter) TsigStatus() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TsigTimersOnly no-op implementation.
|
||||
func (d *DoHWriter) TsigTimersOnly(_ bool) {}
|
||||
|
||||
// Hijack no-op implementation.
|
||||
func (d *DoHWriter) Hijack() {}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
)
|
||||
|
||||
// checkZoneSyntax() checks whether the given string match 1035 Preferred Syntax or not.
|
||||
// The root zone, and all reverse zones always return true even though they technically don't meet 1035 Preferred Syntax
|
||||
func checkZoneSyntax(zone string) bool {
|
||||
if zone == "." || dnsutil.IsReverse(zone) != 0 {
|
||||
return true
|
||||
}
|
||||
regex1035PreferredSyntax, _ := regexp.MatchString(`^(([A-Za-z]([A-Za-z0-9-]*[A-Za-z0-9])?)\.)+$`, zone)
|
||||
return regex1035PreferredSyntax
|
||||
}
|
||||
|
||||
// startUpZones creates the text that we show when starting up:
|
||||
// grpc://example.com.:1055
|
||||
// example.com.:1053 on 127.0.0.1
|
||||
func startUpZones(protocol, addr string, zones map[string][]*Config) string {
|
||||
s := ""
|
||||
|
||||
keys := make([]string, len(zones))
|
||||
i := 0
|
||||
|
||||
for k := range zones {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, zone := range keys {
|
||||
if !checkZoneSyntax(zone) {
|
||||
s += fmt.Sprintf("Warning: Domain %q does not follow RFC1035 preferred syntax\n", zone)
|
||||
}
|
||||
// split addr into protocol, IP and Port
|
||||
_, ip, port, err := SplitProtocolHostPort(addr)
|
||||
|
||||
if err != nil {
|
||||
// this should not happen, but we need to take care of it anyway
|
||||
s += fmt.Sprintln(protocol + zone + ":" + addr)
|
||||
continue
|
||||
}
|
||||
if ip == "" {
|
||||
s += fmt.Sprintln(protocol + zone + ":" + port)
|
||||
continue
|
||||
}
|
||||
// if the server is listening on a specific address let's make it visible in the log,
|
||||
// so one can differentiate between all active listeners
|
||||
s += fmt.Sprintln(protocol + zone + ":" + port + " on " + ip)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
type DoQWriter struct {
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
stream quic.Stream
|
||||
Msg *dns.Msg
|
||||
}
|
||||
|
||||
func (w *DoQWriter) Write(b []byte) (int, error) {
|
||||
b = AddPrefix(b)
|
||||
return w.stream.Write(b)
|
||||
}
|
||||
|
||||
func (w *DoQWriter) WriteMsg(m *dns.Msg) error {
|
||||
bytes, err := m.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
// Close sends the STREAM FIN signal.
|
||||
// The server MUST send the response(s) on the same stream and MUST
|
||||
// indicate, after the last response, through the STREAM FIN
|
||||
// mechanism that no further data will be sent on that stream.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9250#section-4.2-7
|
||||
func (w *DoQWriter) Close() error {
|
||||
return w.stream.Close()
|
||||
}
|
||||
|
||||
// AddPrefix adds a 2-byte prefix with the DNS message length.
|
||||
func AddPrefix(b []byte) (m []byte) {
|
||||
m = make([]byte, 2+len(b))
|
||||
binary.BigEndian.PutUint16(m, uint16(len(b)))
|
||||
copy(m[2:], b)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// These methods implement the dns.ResponseWriter interface from Go DNS.
|
||||
func (w *DoQWriter) TsigStatus() error { return nil }
|
||||
func (w *DoQWriter) TsigTimersOnly(b bool) {}
|
||||
func (w *DoQWriter) Hijack() {}
|
||||
func (w *DoQWriter) LocalAddr() net.Addr { return w.localAddr }
|
||||
func (w *DoQWriter) RemoteAddr() net.Addr { return w.remoteAddr }
|
||||
|
|
@ -1,368 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/caddy/caddyfile"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/parse"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const serverType = "dns"
|
||||
|
||||
func init() {
|
||||
caddy.RegisterServerType(serverType, caddy.ServerType{
|
||||
Directives: func() []string { return Directives },
|
||||
DefaultInput: func() caddy.Input {
|
||||
return caddy.CaddyfileInput{
|
||||
Filepath: "Corefile",
|
||||
Contents: []byte(".:" + Port + " {\nwhoami\nlog\n}\n"),
|
||||
ServerTypeName: serverType,
|
||||
}
|
||||
},
|
||||
NewContext: newContext,
|
||||
})
|
||||
}
|
||||
|
||||
func newContext(i *caddy.Instance) caddy.Context {
|
||||
return &dnsContext{keysToConfigs: make(map[string]*Config)}
|
||||
}
|
||||
|
||||
type dnsContext struct {
|
||||
keysToConfigs map[string]*Config
|
||||
|
||||
// configs is the master list of all site configs.
|
||||
configs []*Config
|
||||
}
|
||||
|
||||
func (h *dnsContext) saveConfig(key string, cfg *Config) {
|
||||
h.configs = append(h.configs, cfg)
|
||||
h.keysToConfigs[key] = cfg
|
||||
}
|
||||
|
||||
// Compile-time check to ensure dnsContext implements the caddy.Context interface
|
||||
var _ caddy.Context = &dnsContext{}
|
||||
|
||||
// InspectServerBlocks make sure that everything checks out before
|
||||
// executing directives and otherwise prepares the directives to
|
||||
// be parsed and executed.
|
||||
func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
// Normalize and check all the zone names and check for duplicates
|
||||
for ib, s := range serverBlocks {
|
||||
// Walk the s.Keys and expand any reverse address in their proper DNS in-addr zones. If the expansions leads for
|
||||
// more than one reverse zone, replace the current value and add the rest to s.Keys.
|
||||
zoneAddrs := []zoneAddr{}
|
||||
for ik, k := range s.Keys {
|
||||
trans, k1 := parse.Transport(k) // get rid of any dns:// or other scheme.
|
||||
hosts, port, err := plugin.SplitHostPort(k1)
|
||||
// We need to make this a fully qualified domain name to catch all errors here and not later when
|
||||
// plugin.Normalize is called again on these strings, with the prime difference being that the domain
|
||||
// name is fully qualified. This was found by fuzzing where "ȶ" is deemed OK, but "ȶ." is not (might be a
|
||||
// bug in miekg/dns actually). But here we were checking ȶ, which is OK, and later we barf in ȶ. leading to
|
||||
// "index out of range".
|
||||
for ih := range hosts {
|
||||
_, _, err := plugin.SplitHostPort(dns.Fqdn(hosts[ih]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
switch trans {
|
||||
case transport.DNS:
|
||||
port = Port
|
||||
case transport.TLS:
|
||||
port = transport.TLSPort
|
||||
case transport.QUIC:
|
||||
port = transport.QUICPort
|
||||
case transport.GRPC:
|
||||
port = transport.GRPCPort
|
||||
case transport.HTTPS:
|
||||
port = transport.HTTPSPort
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) > 1 {
|
||||
s.Keys[ik] = hosts[0] + ":" + port // replace for the first
|
||||
for _, h := range hosts[1:] { // add the rest
|
||||
s.Keys = append(s.Keys, h+":"+port)
|
||||
}
|
||||
}
|
||||
for i := range hosts {
|
||||
zoneAddrs = append(zoneAddrs, zoneAddr{Zone: dns.Fqdn(hosts[i]), Port: port, Transport: trans})
|
||||
}
|
||||
}
|
||||
|
||||
serverBlocks[ib].Keys = s.Keys // important to save back the new keys that are potentially created here.
|
||||
|
||||
var firstConfigInBlock *Config
|
||||
|
||||
for ik := range s.Keys {
|
||||
za := zoneAddrs[ik]
|
||||
s.Keys[ik] = za.String()
|
||||
// Save the config to our master list, and key it for lookups.
|
||||
cfg := &Config{
|
||||
Zone: za.Zone,
|
||||
ListenHosts: []string{""},
|
||||
Port: za.Port,
|
||||
Transport: za.Transport,
|
||||
}
|
||||
|
||||
// Set reference to the first config in the current block.
|
||||
// This is used later by MakeServers to share a single plugin list
|
||||
// for all zones in a server block.
|
||||
if ik == 0 {
|
||||
firstConfigInBlock = cfg
|
||||
}
|
||||
cfg.firstConfigInBlock = firstConfigInBlock
|
||||
|
||||
keyConfig := keyForConfig(ib, ik)
|
||||
h.saveConfig(keyConfig, cfg)
|
||||
}
|
||||
}
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// MakeServers uses the newly-created siteConfigs to create and return a list of server instances.
|
||||
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
|
||||
// Copy parameters from first config in the block to all other config in the same block
|
||||
propagateConfigParams(h.configs)
|
||||
|
||||
// we must map (group) each config to a bind address
|
||||
groups, err := groupConfigsByListenAddr(h.configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// then we create a server for each group
|
||||
var servers []caddy.Server
|
||||
for addr, group := range groups {
|
||||
serversForGroup, err := makeServersForGroup(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, serversForGroup...)
|
||||
}
|
||||
|
||||
// For each server config, check for View Filter plugins
|
||||
for _, c := range h.configs {
|
||||
// Add filters in the plugin.cfg order for consistent filter func evaluation order.
|
||||
for _, d := range Directives {
|
||||
if vf, ok := c.registry[d].(Viewer); ok {
|
||||
if c.ViewName != "" {
|
||||
return nil, fmt.Errorf("multiple views defined in server block")
|
||||
}
|
||||
c.ViewName = vf.ViewName()
|
||||
c.FilterFuncs = append(c.FilterFuncs, vf.Filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that there is no overlap on the zones and listen addresses
|
||||
// for unfiltered server configs
|
||||
errValid := h.validateZonesAndListeningAddresses()
|
||||
if errValid != nil {
|
||||
return nil, errValid
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// AddPlugin adds a plugin to a site's plugin stack.
|
||||
func (c *Config) AddPlugin(m plugin.Plugin) {
|
||||
c.Plugin = append(c.Plugin, m)
|
||||
}
|
||||
|
||||
// registerHandler adds a handler to a site's handler registration. Handlers
|
||||
//
|
||||
// use this to announce that they exist to other plugin.
|
||||
func (c *Config) registerHandler(h plugin.Handler) {
|
||||
if c.registry == nil {
|
||||
c.registry = make(map[string]plugin.Handler)
|
||||
}
|
||||
|
||||
// Just overwrite...
|
||||
c.registry[h.Name()] = h
|
||||
}
|
||||
|
||||
// Handler returns the plugin handler that has been added to the config under its name.
|
||||
// This is useful to inspect if a certain plugin is active in this server.
|
||||
// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin
|
||||
// comes before the plugin you are checking; it will not be there (yet).
|
||||
func (c *Config) Handler(name string) plugin.Handler {
|
||||
if c.registry == nil {
|
||||
return nil
|
||||
}
|
||||
if h, ok := c.registry[name]; ok {
|
||||
return h
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handlers returns a slice of plugins that have been registered. This can be used to
|
||||
// inspect and interact with registered plugins but cannot be used to remove or add plugins.
|
||||
// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin
|
||||
// comes before the plugin you are checking; it will not be there (yet).
|
||||
func (c *Config) Handlers() []plugin.Handler {
|
||||
if c.registry == nil {
|
||||
return nil
|
||||
}
|
||||
hs := make([]plugin.Handler, 0, len(c.registry))
|
||||
for _, k := range Directives {
|
||||
registry := c.Handler(k)
|
||||
if registry != nil {
|
||||
hs = append(hs, registry)
|
||||
}
|
||||
}
|
||||
return hs
|
||||
}
|
||||
|
||||
func (h *dnsContext) validateZonesAndListeningAddresses() error {
|
||||
//Validate Zone and addresses
|
||||
checker := newOverlapZone()
|
||||
for _, conf := range h.configs {
|
||||
for _, h := range conf.ListenHosts {
|
||||
// Validate the overlapping of ZoneAddr
|
||||
akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port}
|
||||
var existZone, overlapZone *zoneAddr
|
||||
if len(conf.FilterFuncs) > 0 {
|
||||
// This config has filters. Check for overlap with other (unfiltered) configs.
|
||||
existZone, overlapZone = checker.check(akey)
|
||||
} else {
|
||||
// This config has no filters. Check for overlap with other (unfiltered) configs,
|
||||
// and register the zone to prevent subsequent zones from overlapping with it.
|
||||
existZone, overlapZone = checker.registerAndCheck(akey)
|
||||
}
|
||||
if existZone != nil {
|
||||
return fmt.Errorf("cannot serve %s - it is already defined", akey.String())
|
||||
}
|
||||
if overlapZone != nil {
|
||||
return fmt.Errorf("cannot serve %s - zone overlap listener capacity with %v", akey.String(), overlapZone.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// propagateConfigParams copies the necessary parameters from first config in the block
|
||||
// to all other config in the same block. Doing this results in zones
|
||||
// sharing the same plugin instances and settings as other zones in
|
||||
// the same block.
|
||||
func propagateConfigParams(configs []*Config) {
|
||||
for _, c := range configs {
|
||||
c.Plugin = c.firstConfigInBlock.Plugin
|
||||
c.ListenHosts = c.firstConfigInBlock.ListenHosts
|
||||
c.Debug = c.firstConfigInBlock.Debug
|
||||
c.Stacktrace = c.firstConfigInBlock.Stacktrace
|
||||
c.NumSockets = c.firstConfigInBlock.NumSockets
|
||||
|
||||
// Fork TLSConfig for each encrypted connection
|
||||
c.TLSConfig = c.firstConfigInBlock.TLSConfig.Clone()
|
||||
c.ReadTimeout = c.firstConfigInBlock.ReadTimeout
|
||||
c.WriteTimeout = c.firstConfigInBlock.WriteTimeout
|
||||
c.IdleTimeout = c.firstConfigInBlock.IdleTimeout
|
||||
c.TsigSecret = c.firstConfigInBlock.TsigSecret
|
||||
}
|
||||
}
|
||||
|
||||
// groupConfigsByListenAddr groups site configs by their listen
|
||||
// (bind) address, so sites that use the same listener can be served
|
||||
// on the same server instance. The return value maps the listen
|
||||
// address (what you pass into net.Listen) to the list of site configs.
|
||||
// This function does NOT vet the configs to ensure they are compatible.
|
||||
func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
|
||||
groups := make(map[string][]*Config)
|
||||
for _, conf := range configs {
|
||||
for _, h := range conf.ListenHosts {
|
||||
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(h, conf.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrstr := conf.Transport + "://" + addr.String()
|
||||
groups[addrstr] = append(groups[addrstr], conf)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// makeServersForGroup creates servers for a specific transport and group.
|
||||
// It creates as many servers as specified in the NumSockets configuration.
|
||||
// If the NumSockets param is not specified, one server is created by default.
|
||||
func makeServersForGroup(addr string, group []*Config) ([]caddy.Server, error) {
|
||||
// that is impossible, but better to check
|
||||
if len(group) == 0 {
|
||||
return nil, fmt.Errorf("no configs for group defined")
|
||||
}
|
||||
// create one server by default if no NumSockets specified
|
||||
numSockets := 1
|
||||
if group[0].NumSockets > 0 {
|
||||
numSockets = group[0].NumSockets
|
||||
}
|
||||
|
||||
var servers []caddy.Server
|
||||
for range numSockets {
|
||||
// switch on addr
|
||||
switch tr, _ := parse.Transport(addr); tr {
|
||||
case transport.DNS:
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case transport.TLS:
|
||||
s, err := NewServerTLS(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case transport.QUIC:
|
||||
s, err := NewServerQUIC(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case transport.GRPC:
|
||||
s, err := NewServergRPC(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
|
||||
case transport.HTTPS:
|
||||
s, err := NewServerHTTPS(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, s)
|
||||
}
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// DefaultPort is the default port.
|
||||
const DefaultPort = transport.Port
|
||||
|
||||
// These "soft defaults" are configurable by
|
||||
// command line flags, etc.
|
||||
var (
|
||||
// Port is the port we listen on by default.
|
||||
Port = DefaultPort
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
)
|
||||
|
||||
var _ caddy.GracefulServer = new(Server)
|
||||
|
|
@ -1,458 +0,0 @@
|
|||
// Package dnsserver implements all the interfaces from Caddy, so that CoreDNS can be a servertype plugin.
|
||||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
"github.com/coredns/coredns/plugin/pkg/edns"
|
||||
"github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/trace"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
ot "github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
// Server represents an instance of a server, which serves
|
||||
// DNS requests at a particular address (host and port). A
|
||||
// server is capable of serving numerous zones on
|
||||
// the same address and the listener may be stopped for
|
||||
// graceful termination (POSIX only).
|
||||
type Server struct {
|
||||
Addr string // Address we listen on
|
||||
|
||||
server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
|
||||
m sync.Mutex // protects the servers
|
||||
|
||||
zones map[string][]*Config // zones keyed by their address
|
||||
dnsWg sync.WaitGroup // used to wait on outstanding connections
|
||||
graceTimeout time.Duration // the maximum duration of a graceful shutdown
|
||||
trace trace.Trace // the trace plugin for the server
|
||||
debug bool // disable recover()
|
||||
stacktrace bool // enable stacktrace in recover error log
|
||||
classChaos bool // allow non-INET class queries
|
||||
idleTimeout time.Duration // Idle timeout for TCP
|
||||
readTimeout time.Duration // Read timeout for TCP
|
||||
writeTimeout time.Duration // Write timeout for TCP
|
||||
|
||||
tsigSecret map[string]string
|
||||
}
|
||||
|
||||
// MetadataCollector is a plugin that can retrieve metadata functions from all metadata providing plugins
|
||||
type MetadataCollector interface {
|
||||
Collect(context.Context, request.Request) context.Context
|
||||
}
|
||||
|
||||
// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class
|
||||
// queries are blocked unless queries from enableChaos are loaded.
|
||||
func NewServer(addr string, group []*Config) (*Server, error) {
|
||||
s := &Server{
|
||||
Addr: addr,
|
||||
zones: make(map[string][]*Config),
|
||||
graceTimeout: 5 * time.Second,
|
||||
idleTimeout: 10 * time.Second,
|
||||
readTimeout: 3 * time.Second,
|
||||
writeTimeout: 5 * time.Second,
|
||||
tsigSecret: make(map[string]string),
|
||||
}
|
||||
|
||||
// We have to bound our wg with one increment
|
||||
// to prevent a "race condition" that is hard-coded
|
||||
// into sync.WaitGroup.Wait() - basically, an add
|
||||
// with a positive delta must be guaranteed to
|
||||
// occur before Wait() is called on the wg.
|
||||
// In a way, this kind of acts as a safety barrier.
|
||||
s.dnsWg.Add(1)
|
||||
|
||||
for _, site := range group {
|
||||
if site.Debug {
|
||||
s.debug = true
|
||||
log.D.Set()
|
||||
}
|
||||
s.stacktrace = site.Stacktrace
|
||||
|
||||
// append the config to the zone's configs
|
||||
s.zones[site.Zone] = append(s.zones[site.Zone], site)
|
||||
|
||||
// set timeouts
|
||||
if site.ReadTimeout != 0 {
|
||||
s.readTimeout = site.ReadTimeout
|
||||
}
|
||||
if site.WriteTimeout != 0 {
|
||||
s.writeTimeout = site.WriteTimeout
|
||||
}
|
||||
if site.IdleTimeout != 0 {
|
||||
s.idleTimeout = site.IdleTimeout
|
||||
}
|
||||
|
||||
// copy tsig secrets
|
||||
for key, secret := range site.TsigSecret {
|
||||
s.tsigSecret[key] = secret
|
||||
}
|
||||
|
||||
// compile custom plugin for everything
|
||||
var stack plugin.Handler
|
||||
for i := len(site.Plugin) - 1; i >= 0; i-- {
|
||||
stack = site.Plugin[i](stack)
|
||||
|
||||
// register the *handler* also
|
||||
site.registerHandler(stack)
|
||||
|
||||
// If the current plugin is a MetadataCollector, bookmark it for later use. This loop traverses the plugin
|
||||
// list backwards, so the first MetadataCollector plugin wins.
|
||||
if mdc, ok := stack.(MetadataCollector); ok {
|
||||
site.metaCollector = mdc
|
||||
}
|
||||
|
||||
if s.trace == nil && stack.Name() == "trace" {
|
||||
// we have to stash away the plugin, not the
|
||||
// Tracer object, because the Tracer won't be initialized yet
|
||||
if t, ok := stack.(trace.Trace); ok {
|
||||
s.trace = t
|
||||
}
|
||||
}
|
||||
// Unblock CH class queries when any of these plugins are loaded.
|
||||
if _, ok := EnableChaos[stack.Name()]; ok {
|
||||
s.classChaos = true
|
||||
}
|
||||
}
|
||||
site.pluginChain = stack
|
||||
}
|
||||
|
||||
if !s.debug {
|
||||
// When reloading we need to explicitly disable debug logging if it is now disabled.
|
||||
log.D.Clear()
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Compile-time check to ensure Server implements the caddy.GracefulServer interface
|
||||
var _ caddy.GracefulServer = &Server{}
|
||||
|
||||
// Serve starts the server with an existing listener. It blocks until the server stops.
|
||||
// This implements caddy.TCPServer interface.
|
||||
func (s *Server) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
|
||||
s.server[tcp] = &dns.Server{Listener: l,
|
||||
Net: "tcp",
|
||||
TsigSecret: s.tsigSecret,
|
||||
MaxTCPQueries: tcpMaxQueries,
|
||||
ReadTimeout: s.readTimeout,
|
||||
WriteTimeout: s.writeTimeout,
|
||||
IdleTimeout: func() time.Duration {
|
||||
return s.idleTimeout
|
||||
},
|
||||
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
ctx := context.WithValue(context.Background(), Key{}, s)
|
||||
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
||||
s.ServeDNS(ctx, w, r)
|
||||
})}
|
||||
|
||||
s.m.Unlock()
|
||||
|
||||
return s.server[tcp].ActivateAndServe()
|
||||
}
|
||||
|
||||
// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
|
||||
// This implements caddy.UDPServer interface.
|
||||
func (s *Server) ServePacket(p net.PacketConn) error {
|
||||
s.m.Lock()
|
||||
s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
ctx := context.WithValue(context.Background(), Key{}, s)
|
||||
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
||||
s.ServeDNS(ctx, w, r)
|
||||
}), TsigSecret: s.tsigSecret}
|
||||
s.m.Unlock()
|
||||
|
||||
return s.server[udp].ActivateAndServe()
|
||||
}
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *Server) Listen() (net.Listener, error) {
|
||||
l, err := reuseport.Listen("tcp", s.Addr[len(transport.DNS+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// WrapListener Listen implements caddy.GracefulServer interface.
|
||||
func (s *Server) WrapListener(ln net.Listener) net.Listener {
|
||||
return ln
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *Server) ListenPacket() (net.PacketConn, error) {
|
||||
p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.DNS+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Stop stops the server. It blocks until the server is
|
||||
// totally stopped. On POSIX systems, it will wait for
|
||||
// connections to close (up to a max timeout of a few
|
||||
// seconds); on Windows it will close the listener
|
||||
// immediately.
|
||||
// This implements Caddy.Stopper interface.
|
||||
func (s *Server) Stop() (err error) {
|
||||
if runtime.GOOS != "windows" {
|
||||
// force connections to close after timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.dnsWg.Done() // decrement our initial increment used as a barrier
|
||||
s.dnsWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for remaining connections to finish or
|
||||
// force them all to close after timeout
|
||||
select {
|
||||
case <-time.After(s.graceTimeout):
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
// Close the listener now; this stops the server without delay
|
||||
s.m.Lock()
|
||||
for _, s1 := range s.server {
|
||||
// We might not have started and initialized the full set of servers
|
||||
if s1 != nil {
|
||||
err = s1.Shutdown()
|
||||
}
|
||||
}
|
||||
s.m.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Address together with Stop() implement caddy.GracefulServer.
|
||||
func (s *Server) Address() string { return s.Addr }
|
||||
|
||||
// ServeDNS is the entry point for every request to the address that
|
||||
// is bound to. It acts as a multiplexer for the requests zonename as
|
||||
// defined in the request so that the correct zone
|
||||
// (configuration and plugin stack) will handle the request.
|
||||
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
|
||||
// The default dns.Mux checks the question section size, but we have our
|
||||
// own mux here. Check if we have a question section. If not drop them here.
|
||||
if r == nil || len(r.Question) == 0 {
|
||||
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.debug {
|
||||
defer func() {
|
||||
// In case the user doesn't enable error plugin, we still
|
||||
// need to make sure that we stay alive up here
|
||||
if rec := recover(); rec != nil {
|
||||
if s.stacktrace {
|
||||
log.Errorf("Recovered from panic in server: %q %v\n%s", s.Addr, rec, string(debug.Stack()))
|
||||
} else {
|
||||
log.Errorf("Recovered from panic in server: %q %v", s.Addr, rec)
|
||||
}
|
||||
vars.Panic.Inc()
|
||||
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if !s.classChaos && r.Question[0].Qclass != dns.ClassINET {
|
||||
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
|
||||
return
|
||||
}
|
||||
|
||||
if m, err := edns.Version(r); err != nil { // Wrong EDNS version, return at once.
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
// Wrap the response writer in a ScrubWriter so we automatically make the reply fit in the client's buffer.
|
||||
w = request.NewScrubWriter(r, w)
|
||||
|
||||
q := strings.ToLower(r.Question[0].Name)
|
||||
var (
|
||||
off int
|
||||
end bool
|
||||
dshandler *Config
|
||||
)
|
||||
|
||||
for {
|
||||
if z, ok := s.zones[q[off:]]; ok {
|
||||
for _, h := range z {
|
||||
if h.pluginChain == nil { // zone defined, but has not got any plugins
|
||||
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
|
||||
return
|
||||
}
|
||||
|
||||
if h.metaCollector != nil {
|
||||
// Collect metadata now, so it can be used before we send a request down the plugin chain.
|
||||
ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w})
|
||||
}
|
||||
|
||||
// If all filter funcs pass, use this config.
|
||||
if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) {
|
||||
if h.ViewName != "" {
|
||||
// if there was a view defined for this Config, set the view name in the context
|
||||
ctx = context.WithValue(ctx, ViewKey{}, h.ViewName)
|
||||
}
|
||||
if r.Question[0].Qtype != dns.TypeDS {
|
||||
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
|
||||
if !plugin.ClientWrite(rcode) {
|
||||
errorFunc(s.Addr, w, r, rcode)
|
||||
}
|
||||
return
|
||||
}
|
||||
// The type is DS, keep the handler, but keep on searching as maybe we are serving
|
||||
// the parent as well and the DS should be routed to it - this will probably *misroute* DS
|
||||
// queries to a possibly grand parent, but there is no way for us to know at this point
|
||||
// if there is an actual delegation from grandparent -> parent -> zone.
|
||||
// In all fairness: direct DS queries should not be needed.
|
||||
dshandler = h
|
||||
}
|
||||
}
|
||||
}
|
||||
off, end = dns.NextLabel(q, off)
|
||||
if end {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil {
|
||||
// DS request, and we found a zone, use the handler for the query.
|
||||
rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r)
|
||||
if !plugin.ClientWrite(rcode) {
|
||||
errorFunc(s.Addr, w, r, rcode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Wildcard match, if we have found nothing try the root zone as a last resort.
|
||||
if z, ok := s.zones["."]; ok {
|
||||
for _, h := range z {
|
||||
if h.pluginChain == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.metaCollector != nil {
|
||||
// Collect metadata now, so it can be used before we send a request down the plugin chain.
|
||||
ctx = h.metaCollector.Collect(ctx, request.Request{Req: r, W: w})
|
||||
}
|
||||
|
||||
// If all filter funcs pass, use this config.
|
||||
if passAllFilterFuncs(ctx, h.FilterFuncs, &request.Request{Req: r, W: w}) {
|
||||
if h.ViewName != "" {
|
||||
// if there was a view defined for this Config, set the view name in the context
|
||||
ctx = context.WithValue(ctx, ViewKey{}, h.ViewName)
|
||||
}
|
||||
rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
|
||||
if !plugin.ClientWrite(rcode) {
|
||||
errorFunc(s.Addr, w, r, rcode)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Still here? Error out with REFUSED.
|
||||
errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
|
||||
}
|
||||
|
||||
// passAllFilterFuncs returns true if all filter funcs evaluate to true for the given request
|
||||
func passAllFilterFuncs(ctx context.Context, filterFuncs []FilterFunc, req *request.Request) bool {
|
||||
for _, ff := range filterFuncs {
|
||||
if !ff(ctx, req) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *Server) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
out := startUpZones("", s.Addr, s.zones)
|
||||
if out != "" {
|
||||
fmt.Print(out)
|
||||
}
|
||||
}
|
||||
|
||||
// Tracer returns the tracer in the server if defined.
|
||||
func (s *Server) Tracer() ot.Tracer {
|
||||
if s.trace == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.trace.Tracer()
|
||||
}
|
||||
|
||||
// errorFunc responds to an DNS request with an error.
|
||||
func errorFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rc)
|
||||
state.SizeAndDo(answer)
|
||||
|
||||
w.WriteMsg(answer)
|
||||
}
|
||||
|
||||
func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rc)
|
||||
state.SizeAndDo(answer)
|
||||
|
||||
vars.Report(server, state, vars.Dropped, "", rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now())
|
||||
|
||||
w.WriteMsg(answer)
|
||||
}
|
||||
|
||||
const (
|
||||
tcp = 0
|
||||
udp = 1
|
||||
|
||||
tcpMaxQueries = -1
|
||||
)
|
||||
|
||||
type (
|
||||
// Key is the context key for the current server added to the context.
|
||||
Key struct{}
|
||||
|
||||
// LoopKey is the context key to detect server wide loops.
|
||||
LoopKey struct{}
|
||||
|
||||
// ViewKey is the context key for the current view, if defined
|
||||
ViewKey struct{}
|
||||
)
|
||||
|
||||
// EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default.
|
||||
var EnableChaos = map[string]struct{}{
|
||||
"chaos": {},
|
||||
"forward": {},
|
||||
"proxy": {},
|
||||
}
|
||||
|
||||
// Quiet mode will not show any informative output on initialization.
|
||||
var Quiet bool
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/pb"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/peer"
|
||||
)
|
||||
|
||||
// ServergRPC represents an instance of a DNS-over-gRPC server.
|
||||
type ServergRPC struct {
|
||||
*Server
|
||||
*pb.UnimplementedDnsServiceServer
|
||||
grpcServer *grpc.Server
|
||||
listenAddr net.Addr
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// NewServergRPC returns a new CoreDNS GRPC server and compiles all plugin in to it.
|
||||
func NewServergRPC(addr string, group []*Config) (*ServergRPC, error) {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The *tls* plugin must make sure that multiple conflicting
|
||||
// TLS configuration returns an error: it can only be specified once.
|
||||
var tlsConfig *tls.Config
|
||||
for _, z := range s.zones {
|
||||
for _, conf := range z {
|
||||
// Should we error if some configs *don't* have TLS?
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
}
|
||||
// http/2 is required when using gRPC. We need to specify it in next protos
|
||||
// or the upgrade won't happen.
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.NextProtos = []string{"h2"}
|
||||
}
|
||||
|
||||
return &ServergRPC{Server: s, tlsConfig: tlsConfig}, nil
|
||||
}
|
||||
|
||||
// Compile-time check to ensure Server implements the caddy.GracefulServer interface
|
||||
var _ caddy.GracefulServer = &Server{}
|
||||
|
||||
// Serve implements caddy.TCPServer interface.
|
||||
func (s *ServergRPC) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
s.listenAddr = l.Addr()
|
||||
s.m.Unlock()
|
||||
|
||||
if s.Tracer() != nil {
|
||||
onlyIfParent := func(parentSpanCtx opentracing.SpanContext, method string, req, resp interface{}) bool {
|
||||
return parentSpanCtx != nil
|
||||
}
|
||||
intercept := otgrpc.OpenTracingServerInterceptor(s.Tracer(), otgrpc.IncludingSpans(onlyIfParent))
|
||||
s.grpcServer = grpc.NewServer(grpc.UnaryInterceptor(intercept))
|
||||
} else {
|
||||
s.grpcServer = grpc.NewServer()
|
||||
}
|
||||
|
||||
pb.RegisterDnsServiceServer(s.grpcServer, s)
|
||||
|
||||
if s.tlsConfig != nil {
|
||||
l = tls.NewListener(l, s.tlsConfig)
|
||||
}
|
||||
return s.grpcServer.Serve(l)
|
||||
}
|
||||
|
||||
// ServePacket implements caddy.UDPServer interface.
|
||||
func (s *ServergRPC) ServePacket(p net.PacketConn) error { return nil }
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *ServergRPC) Listen() (net.Listener, error) {
|
||||
l, err := reuseport.Listen("tcp", s.Addr[len(transport.GRPC+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *ServergRPC) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *ServergRPC) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
out := startUpZones(transport.GRPC+"://", s.Addr, s.zones)
|
||||
if out != "" {
|
||||
fmt.Print(out)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the server. It blocks until the server is
|
||||
// totally stopped.
|
||||
func (s *ServergRPC) Stop() (err error) {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.GracefulStop()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Query is the main entry-point into the gRPC server. From here we call ServeDNS like
|
||||
// any normal server. We use a custom responseWriter to pick up the bytes we need to write
|
||||
// back to the client as a protobuf.
|
||||
func (s *ServergRPC) Query(ctx context.Context, in *pb.DnsPacket) (*pb.DnsPacket, error) {
|
||||
msg := new(dns.Msg)
|
||||
err := msg.Unpack(in.GetMsg())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, ok := peer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("no peer in gRPC context")
|
||||
}
|
||||
|
||||
a, ok := p.Addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no TCP peer in gRPC context: %v", p.Addr)
|
||||
}
|
||||
|
||||
w := &gRPCresponse{localAddr: s.listenAddr, remoteAddr: a, Msg: msg}
|
||||
|
||||
dnsCtx := context.WithValue(ctx, Key{}, s.Server)
|
||||
dnsCtx = context.WithValue(dnsCtx, LoopKey{}, 0)
|
||||
s.ServeDNS(dnsCtx, w, msg)
|
||||
|
||||
packed, err := w.Msg.Pack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.DnsPacket{Msg: packed}, nil
|
||||
}
|
||||
|
||||
// Shutdown stops the server (non gracefully).
|
||||
func (s *ServergRPC) Shutdown() error {
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type gRPCresponse struct {
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
Msg *dns.Msg
|
||||
}
|
||||
|
||||
// Write is the hack that makes this work. It does not actually write the message
|
||||
// but returns the bytes we need to write in r. We can then pick this up in Query
|
||||
// and write a proper protobuf back to the client.
|
||||
func (r *gRPCresponse) Write(b []byte) (int, error) {
|
||||
r.Msg = new(dns.Msg)
|
||||
return len(b), r.Msg.Unpack(b)
|
||||
}
|
||||
|
||||
// These methods implement the dns.ResponseWriter interface from Go DNS.
|
||||
func (r *gRPCresponse) Close() error { return nil }
|
||||
func (r *gRPCresponse) TsigStatus() error { return nil }
|
||||
func (r *gRPCresponse) TsigTimersOnly(b bool) {}
|
||||
func (r *gRPCresponse) Hijack() {}
|
||||
func (r *gRPCresponse) LocalAddr() net.Addr { return r.localAddr }
|
||||
func (r *gRPCresponse) RemoteAddr() net.Addr { return r.remoteAddr }
|
||||
func (r *gRPCresponse) WriteMsg(m *dns.Msg) error { r.Msg = m; return nil }
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"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"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
)
|
||||
|
||||
// ServerHTTPS represents an instance of a DNS-over-HTTPS server.
|
||||
type ServerHTTPS struct {
|
||||
*Server
|
||||
httpsServer *http.Server
|
||||
listenAddr net.Addr
|
||||
tlsConfig *tls.Config
|
||||
validRequest func(*http.Request) bool
|
||||
}
|
||||
|
||||
// loggerAdapter is a simple adapter around CoreDNS logger made to implement io.Writer in order to log errors from HTTP server
|
||||
type loggerAdapter struct {
|
||||
}
|
||||
|
||||
func (l *loggerAdapter) Write(p []byte) (n int, err error) {
|
||||
clog.Debug(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// HTTPRequestKey is the context key for the current processed HTTP request (if current processed request was done over DOH)
|
||||
type HTTPRequestKey struct{}
|
||||
|
||||
// NewServerHTTPS returns a new CoreDNS HTTPS server and compiles all plugins in to it.
|
||||
func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The *tls* plugin must make sure that multiple conflicting
|
||||
// TLS configuration returns an error: it can only be specified once.
|
||||
var tlsConfig *tls.Config
|
||||
for _, z := range s.zones {
|
||||
for _, conf := range z {
|
||||
// Should we error if some configs *don't* have TLS?
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
}
|
||||
|
||||
// http/2 is recommended when using DoH. We need to specify it in next protos
|
||||
// or the upgrade won't happen.
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
|
||||
}
|
||||
|
||||
// Use a custom request validation func or use the standard DoH path check.
|
||||
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 }
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
ReadTimeout: s.readTimeout,
|
||||
WriteTimeout: s.writeTimeout,
|
||||
IdleTimeout: s.idleTimeout,
|
||||
ErrorLog: stdlog.New(&loggerAdapter{}, "", 0),
|
||||
}
|
||||
sh := &ServerHTTPS{
|
||||
Server: s, tlsConfig: tlsConfig, httpsServer: srv, validRequest: validator,
|
||||
}
|
||||
sh.httpsServer.Handler = sh
|
||||
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
// Compile-time check to ensure Server implements the caddy.GracefulServer interface
|
||||
var _ caddy.GracefulServer = &Server{}
|
||||
|
||||
// Serve implements caddy.TCPServer interface.
|
||||
func (s *ServerHTTPS) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
s.listenAddr = l.Addr()
|
||||
s.m.Unlock()
|
||||
|
||||
if s.tlsConfig != nil {
|
||||
l = tls.NewListener(l, s.tlsConfig)
|
||||
}
|
||||
return s.httpsServer.Serve(l)
|
||||
}
|
||||
|
||||
// ServePacket implements caddy.UDPServer interface.
|
||||
func (s *ServerHTTPS) ServePacket(p net.PacketConn) error { return nil }
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *ServerHTTPS) Listen() (net.Listener, error) {
|
||||
l, err := reuseport.Listen("tcp", s.Addr[len(transport.HTTPS+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *ServerHTTPS) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *ServerHTTPS) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
out := startUpZones(transport.HTTPS+"://", s.Addr, s.zones)
|
||||
if out != "" {
|
||||
fmt.Print(out)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the server. It blocks until the server is totally stopped.
|
||||
func (s *ServerHTTPS) Stop() error {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
if s.httpsServer != nil {
|
||||
s.httpsServer.Shutdown(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP is the handler that gets the HTTP request and converts to the dns format, calls the plugin
|
||||
// chain, converts it back and write it to the client.
|
||||
func (s *ServerHTTPS) 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
|
||||
}
|
||||
|
||||
// Create a DoHWriter with the correct addresses in it.
|
||||
h, p, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
port, _ := strconv.Atoi(p)
|
||||
dw := &DoHWriter{
|
||||
laddr: s.listenAddr,
|
||||
raddr: &net.TCPAddr{IP: net.ParseIP(h), Port: port},
|
||||
request: r,
|
||||
}
|
||||
|
||||
// We just call the normal chain handler - all error handling is done there.
|
||||
// We should expect a packet to be returned that we can send to the client.
|
||||
ctx := context.WithValue(context.Background(), Key{}, s.Server)
|
||||
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
||||
ctx = context.WithValue(ctx, HTTPRequestKey{}, r)
|
||||
s.ServeDNS(ctx, dw, msg)
|
||||
|
||||
// See section 4.2.1 of RFC 8484.
|
||||
// We are using code 500 to indicate an unexpected situation when the chain
|
||||
// handler has not provided any response message.
|
||||
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 *ServerHTTPS) countResponse(status int) {
|
||||
vars.HTTPSResponsesCount.WithLabelValues(s.Addr, strconv.Itoa(status)).Inc()
|
||||
}
|
||||
|
||||
// Shutdown stops the server (non gracefully).
|
||||
func (s *ServerHTTPS) Shutdown() error {
|
||||
if s.httpsServer != nil {
|
||||
s.httpsServer.Shutdown(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,376 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
const (
|
||||
// DoQCodeNoError is used when the connection or stream needs to be
|
||||
// closed, but there is no error to signal.
|
||||
DoQCodeNoError quic.ApplicationErrorCode = 0
|
||||
|
||||
// DoQCodeInternalError signals that the DoQ implementation encountered
|
||||
// an internal error and is incapable of pursuing the transaction or the
|
||||
// connection.
|
||||
DoQCodeInternalError quic.ApplicationErrorCode = 1
|
||||
|
||||
// DoQCodeProtocolError signals that the DoQ implementation encountered
|
||||
// a protocol error and is forcibly aborting the connection.
|
||||
DoQCodeProtocolError quic.ApplicationErrorCode = 2
|
||||
|
||||
// DefaultMaxQUICStreams is the default maximum number of concurrent QUIC streams
|
||||
// on a per-connection basis. RFC 9250 (DNS-over-QUIC) does not require a high
|
||||
// concurrent-stream limit; normal stub or recursive resolvers open only a handful
|
||||
// of streams in parallel. This default (256) is a safe upper bound.
|
||||
DefaultMaxQUICStreams = 256
|
||||
|
||||
// DefaultQUICStreamWorkers is the default number of workers for processing QUIC streams.
|
||||
DefaultQUICStreamWorkers = 1024
|
||||
)
|
||||
|
||||
// ServerQUIC represents an instance of a DNS-over-QUIC server.
|
||||
type ServerQUIC struct {
|
||||
*Server
|
||||
listenAddr net.Addr
|
||||
tlsConfig *tls.Config
|
||||
quicConfig *quic.Config
|
||||
quicListener *quic.Listener
|
||||
maxStreams int
|
||||
streamProcessPool chan struct{}
|
||||
}
|
||||
|
||||
// NewServerQUIC returns a new CoreDNS QUIC server and compiles all plugin in to it.
|
||||
func NewServerQUIC(addr string, group []*Config) (*ServerQUIC, error) {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The *tls* plugin must make sure that multiple conflicting
|
||||
// TLS configuration returns an error: it can only be specified once.
|
||||
var tlsConfig *tls.Config
|
||||
for _, z := range s.zones {
|
||||
for _, conf := range z {
|
||||
// Should we error if some configs *don't* have TLS?
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
tlsConfig.NextProtos = []string{"doq"}
|
||||
}
|
||||
|
||||
maxStreams := DefaultMaxQUICStreams
|
||||
if len(group) > 0 && group[0] != nil && group[0].MaxQUICStreams != nil {
|
||||
maxStreams = *group[0].MaxQUICStreams
|
||||
}
|
||||
|
||||
streamProcessPoolSize := DefaultQUICStreamWorkers
|
||||
if len(group) > 0 && group[0] != nil && group[0].MaxQUICWorkerPoolSize != nil {
|
||||
streamProcessPoolSize = *group[0].MaxQUICWorkerPoolSize
|
||||
}
|
||||
|
||||
var quicConfig = &quic.Config{
|
||||
MaxIdleTimeout: s.idleTimeout,
|
||||
MaxIncomingStreams: int64(maxStreams),
|
||||
MaxIncomingUniStreams: int64(maxStreams),
|
||||
// Enable 0-RTT by default for all connections on the server-side.
|
||||
Allow0RTT: true,
|
||||
}
|
||||
|
||||
return &ServerQUIC{
|
||||
Server: s,
|
||||
tlsConfig: tlsConfig,
|
||||
quicConfig: quicConfig,
|
||||
maxStreams: maxStreams,
|
||||
streamProcessPool: make(chan struct{}, streamProcessPoolSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ServePacket implements caddy.UDPServer interface.
|
||||
func (s *ServerQUIC) ServePacket(p net.PacketConn) error {
|
||||
s.m.Lock()
|
||||
s.listenAddr = s.quicListener.Addr()
|
||||
s.m.Unlock()
|
||||
|
||||
return s.ServeQUIC()
|
||||
}
|
||||
|
||||
// ServeQUIC listens for incoming QUIC packets.
|
||||
func (s *ServerQUIC) ServeQUIC() error {
|
||||
for {
|
||||
conn, err := s.quicListener.Accept(context.Background())
|
||||
if err != nil {
|
||||
if s.isExpectedErr(err) {
|
||||
s.closeQUICConn(conn, DoQCodeNoError)
|
||||
return err
|
||||
}
|
||||
|
||||
s.closeQUICConn(conn, DoQCodeInternalError)
|
||||
return err
|
||||
}
|
||||
|
||||
go s.serveQUICConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// serveQUICConnection handles a new QUIC connection. It waits for new streams
|
||||
// and passes them to serveQUICStream.
|
||||
func (s *ServerQUIC) serveQUICConnection(conn quic.Connection) {
|
||||
for {
|
||||
// In DoQ, one query consumes one stream.
|
||||
// The client MUST select the next available client-initiated bidirectional
|
||||
// stream for each subsequent query on a QUIC connection.
|
||||
stream, err := conn.AcceptStream(context.Background())
|
||||
if err != nil {
|
||||
if s.isExpectedErr(err) {
|
||||
s.closeQUICConn(conn, DoQCodeNoError)
|
||||
return
|
||||
}
|
||||
|
||||
s.closeQUICConn(conn, DoQCodeInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// Use a bounded worker pool
|
||||
s.streamProcessPool <- struct{}{} // Acquire a worker slot, may block
|
||||
go func(st quic.Stream, cn quic.Connection) {
|
||||
defer func() { <-s.streamProcessPool }() // Release worker slot
|
||||
s.serveQUICStream(st, cn)
|
||||
}(stream, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerQUIC) serveQUICStream(stream quic.Stream, conn quic.Connection) {
|
||||
buf, err := readDOQMessage(stream)
|
||||
|
||||
// io.EOF does not really mean that there's any error, it is just
|
||||
// the STREAM FIN indicating that there will be no data to read
|
||||
// anymore from this stream.
|
||||
if err != nil && err != io.EOF {
|
||||
s.closeQUICConn(conn, DoQCodeProtocolError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
req := &dns.Msg{}
|
||||
err = req.Unpack(buf)
|
||||
if err != nil {
|
||||
clog.Debugf("unpacking quic packet: %s", err)
|
||||
s.closeQUICConn(conn, DoQCodeProtocolError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !validRequest(req) {
|
||||
// If a peer encounters such an error condition, it is considered a
|
||||
// fatal error. It SHOULD forcibly abort the connection using QUIC's
|
||||
// CONNECTION_CLOSE mechanism and SHOULD use the DoQ error code
|
||||
// DOQ_PROTOCOL_ERROR.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9250#section-4.3.3-3
|
||||
s.closeQUICConn(conn, DoQCodeProtocolError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w := &DoQWriter{
|
||||
localAddr: conn.LocalAddr(),
|
||||
remoteAddr: conn.RemoteAddr(),
|
||||
stream: stream,
|
||||
Msg: req,
|
||||
}
|
||||
|
||||
dnsCtx := context.WithValue(stream.Context(), Key{}, s.Server)
|
||||
dnsCtx = context.WithValue(dnsCtx, LoopKey{}, 0)
|
||||
s.ServeDNS(dnsCtx, w, req)
|
||||
s.countResponse(DoQCodeNoError)
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *ServerQUIC) ListenPacket() (net.PacketConn, error) {
|
||||
p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.QUIC+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
|
||||
s.quicListener, err = quic.Listen(p, s.tlsConfig, s.quicConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *ServerQUIC) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
out := startUpZones(transport.QUIC+"://", s.Addr, s.zones)
|
||||
if out != "" {
|
||||
fmt.Print(out)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the server non-gracefully. It blocks until the server is totally stopped.
|
||||
func (s *ServerQUIC) Stop() error {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
|
||||
if s.quicListener != nil {
|
||||
return s.quicListener.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve implements caddy.TCPServer interface.
|
||||
func (s *ServerQUIC) Serve(l net.Listener) error { return nil }
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *ServerQUIC) Listen() (net.Listener, error) { return nil, nil }
|
||||
|
||||
// closeQUICConn quietly closes the QUIC connection.
|
||||
func (s *ServerQUIC) closeQUICConn(conn quic.Connection, code quic.ApplicationErrorCode) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
clog.Debugf("closing quic conn %s with code %d", conn.LocalAddr(), code)
|
||||
err := conn.CloseWithError(code, "")
|
||||
if err != nil {
|
||||
clog.Debugf("failed to close quic connection with code %d: %s", code, err)
|
||||
}
|
||||
|
||||
// DoQCodeNoError metrics are already registered after s.ServeDNS()
|
||||
if code != DoQCodeNoError {
|
||||
s.countResponse(code)
|
||||
}
|
||||
}
|
||||
|
||||
// validRequest checks for protocol errors in the unpacked DNS message.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9250.html#name-protocol-errors
|
||||
func validRequest(req *dns.Msg) (ok bool) {
|
||||
// 1. a client or server receives a message with a non-zero Message ID.
|
||||
if req.Id != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. an implementation receives a message containing the edns-tcp-keepalive
|
||||
// EDNS(0) Option [RFC7828].
|
||||
if opt := req.IsEdns0(); opt != nil {
|
||||
for _, option := range opt.Option {
|
||||
if option.Option() == dns.EDNS0TCPKEEPALIVE {
|
||||
clog.Debug("client sent EDNS0 TCP keepalive option")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. the client or server does not indicate the expected STREAM FIN after
|
||||
// sending requests or responses.
|
||||
//
|
||||
// This is quite problematic to validate this case since this would imply
|
||||
// we have to wait until STREAM FIN is arrived before we start processing
|
||||
// the message. So we're consciously ignoring this case in this
|
||||
// implementation.
|
||||
|
||||
// 4. a server receives a "replayable" transaction in 0-RTT data
|
||||
//
|
||||
// The information necessary to validate this is not exposed by quic-go.
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// readDOQMessage reads a DNS over QUIC (DOQ) message from the given stream
|
||||
// and returns the message bytes.
|
||||
// Drafts of the RFC9250 did not require the 2-byte prefixed message length.
|
||||
// Thus, we are only supporting the official version (DoQ v1).
|
||||
func readDOQMessage(r io.Reader) ([]byte, error) {
|
||||
// All DNS messages (queries and responses) sent over DoQ connections MUST
|
||||
// be encoded as a 2-octet length field followed by the message content as
|
||||
// specified in [RFC1035].
|
||||
// See https://www.rfc-editor.org/rfc/rfc9250.html#section-4.2-4
|
||||
sizeBuf := make([]byte, 2)
|
||||
_, err := io.ReadFull(r, sizeBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size := binary.BigEndian.Uint16(sizeBuf)
|
||||
|
||||
if size == 0 {
|
||||
return nil, fmt.Errorf("message size is 0: probably unsupported DoQ version")
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
_, err = io.ReadFull(r, buf)
|
||||
|
||||
// A client or server receives a STREAM FIN before receiving all the bytes
|
||||
// for a message indicated in the 2-octet length field.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9250#section-4.3.3-2.2
|
||||
if size != uint16(len(buf)) {
|
||||
return nil, fmt.Errorf("message size does not match 2-byte prefix")
|
||||
}
|
||||
|
||||
return buf, err
|
||||
}
|
||||
|
||||
// isExpectedErr returns true if err is an expected error, likely related to
|
||||
// the current implementation.
|
||||
func (s *ServerQUIC) isExpectedErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// This error is returned when the QUIC listener was closed by us. As
|
||||
// graceful shutdown is not implemented, the connection will be abruptly
|
||||
// closed but there is no error to signal.
|
||||
if errors.Is(err, quic.ErrServerClosed) {
|
||||
return true
|
||||
}
|
||||
|
||||
// This error happens when the connection was closed due to a DoQ
|
||||
// protocol error but there's still something to read in the closed stream.
|
||||
// For example, when the message was sent without the prefixed length.
|
||||
var qAppErr *quic.ApplicationError
|
||||
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 2 {
|
||||
return true
|
||||
}
|
||||
|
||||
// When a connection hits the idle timeout, quic.AcceptStream() returns
|
||||
// an IdleTimeoutError. In this, case, we should just drop the connection
|
||||
// with DoQCodeNoError.
|
||||
var qIdleErr *quic.IdleTimeoutError
|
||||
return errors.As(err, &qIdleErr)
|
||||
}
|
||||
|
||||
func (s *ServerQUIC) countResponse(code quic.ApplicationErrorCode) {
|
||||
switch code {
|
||||
case DoQCodeNoError:
|
||||
vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x0").Inc()
|
||||
case DoQCodeInternalError:
|
||||
vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x1").Inc()
|
||||
case DoQCodeProtocolError:
|
||||
vars.QUICResponsesCount.WithLabelValues(s.Addr, "0x2").Inc()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ServerTLS represents an instance of a TLS-over-DNS-server.
|
||||
type ServerTLS struct {
|
||||
*Server
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// NewServerTLS returns a new CoreDNS TLS server and compiles all plugin in to it.
|
||||
func NewServerTLS(addr string, group []*Config) (*ServerTLS, error) {
|
||||
s, err := NewServer(addr, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The *tls* plugin must make sure that multiple conflicting
|
||||
// TLS configuration returns an error: it can only be specified once.
|
||||
var tlsConfig *tls.Config
|
||||
for _, z := range s.zones {
|
||||
for _, conf := range z {
|
||||
// Should we error if some configs *don't* have TLS?
|
||||
tlsConfig = conf.TLSConfig
|
||||
}
|
||||
}
|
||||
|
||||
return &ServerTLS{Server: s, tlsConfig: tlsConfig}, nil
|
||||
}
|
||||
|
||||
// Compile-time check to ensure Server implements the caddy.GracefulServer interface
|
||||
var _ caddy.GracefulServer = &Server{}
|
||||
|
||||
// Serve implements caddy.TCPServer interface.
|
||||
func (s *ServerTLS) Serve(l net.Listener) error {
|
||||
s.m.Lock()
|
||||
|
||||
if s.tlsConfig != nil {
|
||||
l = tls.NewListener(l, s.tlsConfig)
|
||||
}
|
||||
|
||||
// Only fill out the TCP server for this one.
|
||||
s.server[tcp] = &dns.Server{Listener: l,
|
||||
Net: "tcp-tls",
|
||||
MaxTCPQueries: tlsMaxQueries,
|
||||
ReadTimeout: s.readTimeout,
|
||||
WriteTimeout: s.writeTimeout,
|
||||
IdleTimeout: func() time.Duration {
|
||||
return s.idleTimeout
|
||||
},
|
||||
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
ctx := context.WithValue(context.Background(), Key{}, s.Server)
|
||||
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
||||
s.ServeDNS(ctx, w, r)
|
||||
})}
|
||||
|
||||
s.m.Unlock()
|
||||
|
||||
return s.server[tcp].ActivateAndServe()
|
||||
}
|
||||
|
||||
// ServePacket implements caddy.UDPServer interface.
|
||||
func (s *ServerTLS) ServePacket(p net.PacketConn) error { return nil }
|
||||
|
||||
// Listen implements caddy.TCPServer interface.
|
||||
func (s *ServerTLS) Listen() (net.Listener, error) {
|
||||
l, err := reuseport.Listen("tcp", s.Addr[len(transport.TLS+"://"):])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ListenPacket implements caddy.UDPServer interface.
|
||||
func (s *ServerTLS) ListenPacket() (net.PacketConn, error) { return nil, nil }
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming Quiet is false.
|
||||
func (s *ServerTLS) OnStartupComplete() {
|
||||
if Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
out := startUpZones(transport.TLS+"://", s.Addr, s.zones)
|
||||
if out != "" {
|
||||
fmt.Print(out)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
tlsMaxQueries = -1
|
||||
)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package dnsserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
)
|
||||
|
||||
// Viewer - If Viewer is implemented by a plugin in a server block, its Filter()
|
||||
// is added to the server block's filter functions when starting the server. When a running server
|
||||
// serves a DNS request, it will route the request to the first Config (server block) that passes
|
||||
// all its filter functions.
|
||||
type Viewer interface {
|
||||
// Filter returns true if the server should use the server block in which the implementing plugin resides, and the
|
||||
// name of the view for metrics logging.
|
||||
Filter(ctx context.Context, req *request.Request) bool
|
||||
|
||||
// ViewName returns the name of the view
|
||||
ViewName() string
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
// generated by directives_generate.go; DO NOT EDIT
|
||||
|
||||
package dnsserver
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed.
|
||||
//
|
||||
// Ordering is VERY important. Every plugin will
|
||||
// feel the effects of all other plugin below
|
||||
// (after) them during a request, but they must not
|
||||
// care what plugin above them are doing.
|
||||
var Directives = []string{
|
||||
"root",
|
||||
"metadata",
|
||||
"geoip",
|
||||
"cancel",
|
||||
"tls",
|
||||
"quic",
|
||||
"timeouts",
|
||||
"multisocket",
|
||||
"reload",
|
||||
"nsid",
|
||||
"bufsize",
|
||||
"bind",
|
||||
"debug",
|
||||
"trace",
|
||||
"ready",
|
||||
"health",
|
||||
"pprof",
|
||||
"prometheus",
|
||||
"errors",
|
||||
"log",
|
||||
"dnstap",
|
||||
"local",
|
||||
"dns64",
|
||||
"acl",
|
||||
"any",
|
||||
"chaos",
|
||||
"loadbalance",
|
||||
"tsig",
|
||||
"cache",
|
||||
"rewrite",
|
||||
"header",
|
||||
"dnssec",
|
||||
"autopath",
|
||||
"minimal",
|
||||
"template",
|
||||
"transfer",
|
||||
"hosts",
|
||||
"route53",
|
||||
"azure",
|
||||
"clouddns",
|
||||
"k8s_external",
|
||||
"kubernetes",
|
||||
"file",
|
||||
"auto",
|
||||
"secondary",
|
||||
"etcd",
|
||||
"loop",
|
||||
"forward",
|
||||
"grpc",
|
||||
"erratic",
|
||||
"whoami",
|
||||
"on",
|
||||
"sign",
|
||||
"view",
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
// Package coremain contains the functions for starting CoreDNS.
|
||||
package coremain
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.DefaultConfigFile = "Corefile"
|
||||
caddy.Quiet = true // don't show init stuff from caddy
|
||||
setVersion()
|
||||
|
||||
flag.StringVar(&conf, "conf", "", "Corefile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&dnsserver.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
|
||||
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
|
||||
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
|
||||
|
||||
flag.StringVar(&dnsserver.Port, serverType+".port", dnsserver.DefaultPort, "Default port")
|
||||
flag.StringVar(&dnsserver.Port, "p", dnsserver.DefaultPort, "Default port")
|
||||
|
||||
caddy.AppName = CoreName
|
||||
caddy.AppVersion = CoreVersion
|
||||
}
|
||||
|
||||
// Run is CoreDNS's main() function.
|
||||
func Run() {
|
||||
caddy.TrapSignals()
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) > 0 {
|
||||
mustLogFatal(fmt.Errorf("extra command line arguments: %s", flag.Args()))
|
||||
}
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(LogFlags)
|
||||
|
||||
if version {
|
||||
showVersion()
|
||||
os.Exit(0)
|
||||
}
|
||||
if plugins {
|
||||
fmt.Println(caddy.DescribePlugins())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
_, err := maxprocs.Set(maxprocs.Logger(log.Printf))
|
||||
if err != nil {
|
||||
log.Println("[WARNING] Failed to set GOMAXPROCS:", err)
|
||||
}
|
||||
|
||||
// Get Corefile input
|
||||
corefile, err := caddy.LoadCaddyfile(serverType)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(corefile)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
if !dnsserver.Quiet {
|
||||
showVersion()
|
||||
}
|
||||
|
||||
// Twiddle your thumbs
|
||||
instance.Wait()
|
||||
}
|
||||
|
||||
// mustLogFatal wraps log.Fatal() in a way that ensures the
|
||||
// output is always printed to stderr so the user can see it
|
||||
// if the user is still there, even if the process log was not
|
||||
// enabled. If this process is an upgrade, however, and the user
|
||||
// might not be there anymore, this just logs to the process
|
||||
// log and exits.
|
||||
func mustLogFatal(args ...interface{}) {
|
||||
if !caddy.IsUpgrade() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
log.Fatal(args...)
|
||||
}
|
||||
|
||||
// confLoader loads the Caddyfile using the -conf flag.
|
||||
func confLoader(serverType string) (caddy.Input, error) {
|
||||
if conf == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(filepath.Clean(conf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// defaultLoader loads the Corefile from the current working directory.
|
||||
func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
contents, err := os.ReadFile(caddy.DefaultConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// showVersion prints the version that is starting.
|
||||
func showVersion() {
|
||||
fmt.Print(versionString())
|
||||
fmt.Print(releaseString())
|
||||
if devBuild && gitShortStat != "" {
|
||||
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
|
||||
}
|
||||
}
|
||||
|
||||
// versionString returns the CoreDNS version as a string.
|
||||
func versionString() string {
|
||||
return fmt.Sprintf("%s-%s\n", caddy.AppName, caddy.AppVersion)
|
||||
}
|
||||
|
||||
// releaseString returns the release information related to CoreDNS version:
|
||||
// <OS>/<ARCH>, <go version>, <commit>
|
||||
// e.g.,
|
||||
// linux/amd64, go1.8.3, a6d2d7b5
|
||||
func releaseString() string {
|
||||
return fmt.Sprintf("%s/%s, %s, %s\n", runtime.GOOS, runtime.GOARCH, runtime.Version(), GitCommit)
|
||||
}
|
||||
|
||||
// setVersion figures out the version information
|
||||
// based on variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)", strings.TrimPrefix(gitNearestTag, "v"), GitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
conf string
|
||||
version bool
|
||||
plugins bool
|
||||
|
||||
// LogFlags are initially set to 0 for no extra output
|
||||
LogFlags int
|
||||
)
|
||||
|
||||
// Build information obtained with the help of -ldflags
|
||||
var (
|
||||
appVersion = "(untracked dev build)" // inferred at startup
|
||||
devBuild = true // inferred at startup
|
||||
|
||||
buildDate string // date -u
|
||||
gitTag string // git describe --exact-match HEAD 2> /dev/null
|
||||
gitNearestTag string // git describe --abbrev=0 --tags HEAD
|
||||
gitShortStat string // git diff-index --shortstat
|
||||
gitFilesModified string // git diff-index --name-only HEAD
|
||||
|
||||
// Gitcommit contains the commit where we built CoreDNS from.
|
||||
GitCommit string
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package coremain
|
||||
|
||||
// Various CoreDNS constants.
|
||||
const (
|
||||
CoreVersion = "1.12.2"
|
||||
CoreName = "CoreDNS"
|
||||
serverType = "dns"
|
||||
)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Generate the Go files from the dns.proto protobuf, you need the utilities
|
||||
# from: https://github.com/golang/protobuf to make this work.
|
||||
# The generate dns.pb.go is checked into git, so for normal builds we don't need
|
||||
# to run this generation step.
|
||||
# Note: The following has been used when regenerate pb:
|
||||
# curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip
|
||||
# go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
|
||||
# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
|
||||
# export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
# rm pb/dns.pb.go pb/dns_grpc.pb.go
|
||||
# make pb
|
||||
|
||||
all: dns.pb.go
|
||||
|
||||
dns.pb.go: dns.proto
|
||||
protoc --go_out=. --go-grpc_out=. dns.proto
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm dns.pb.go
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v3.19.4
|
||||
// source: dns.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type DnsPacket struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DnsPacket) Reset() {
|
||||
*x = DnsPacket{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_dns_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *DnsPacket) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DnsPacket) ProtoMessage() {}
|
||||
|
||||
func (x *DnsPacket) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_dns_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DnsPacket.ProtoReflect.Descriptor instead.
|
||||
func (*DnsPacket) Descriptor() ([]byte, []int) {
|
||||
return file_dns_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *DnsPacket) GetMsg() []byte {
|
||||
if x != nil {
|
||||
return x.Msg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_dns_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_dns_proto_rawDesc = []byte{
|
||||
0x0a, 0x09, 0x64, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x63, 0x6f, 0x72,
|
||||
0x65, 0x64, 0x6e, 0x73, 0x2e, 0x64, 0x6e, 0x73, 0x22, 0x1d, 0x0a, 0x09, 0x44, 0x6e, 0x73, 0x50,
|
||||
0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x32, 0x45, 0x0a, 0x0a, 0x44, 0x6e, 0x73, 0x53, 0x65,
|
||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16,
|
||||
0x2e, 0x63, 0x6f, 0x72, 0x65, 0x64, 0x6e, 0x73, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6e, 0x73,
|
||||
0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x64, 0x6e, 0x73,
|
||||
0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6e, 0x73, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x42, 0x06,
|
||||
0x5a, 0x04, 0x2e, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_dns_proto_rawDescOnce sync.Once
|
||||
file_dns_proto_rawDescData = file_dns_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_dns_proto_rawDescGZIP() []byte {
|
||||
file_dns_proto_rawDescOnce.Do(func() {
|
||||
file_dns_proto_rawDescData = protoimpl.X.CompressGZIP(file_dns_proto_rawDescData)
|
||||
})
|
||||
return file_dns_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_dns_proto_goTypes = []interface{}{
|
||||
(*DnsPacket)(nil), // 0: coredns.dns.DnsPacket
|
||||
}
|
||||
var file_dns_proto_depIdxs = []int32{
|
||||
0, // 0: coredns.dns.DnsService.Query:input_type -> coredns.dns.DnsPacket
|
||||
0, // 1: coredns.dns.DnsService.Query:output_type -> coredns.dns.DnsPacket
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
0, // [0:1] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_dns_proto_init() }
|
||||
func file_dns_proto_init() {
|
||||
if File_dns_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_dns_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*DnsPacket); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_dns_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_dns_proto_goTypes,
|
||||
DependencyIndexes: file_dns_proto_depIdxs,
|
||||
MessageInfos: file_dns_proto_msgTypes,
|
||||
}.Build()
|
||||
File_dns_proto = out.File
|
||||
file_dns_proto_rawDesc = nil
|
||||
file_dns_proto_goTypes = nil
|
||||
file_dns_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package coredns.dns;
|
||||
option go_package = ".;pb";
|
||||
|
||||
message DnsPacket {
|
||||
bytes msg = 1;
|
||||
}
|
||||
|
||||
service DnsService {
|
||||
rpc Query (DnsPacket) returns (DnsPacket);
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.4
|
||||
// source: dns.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// DnsServiceClient is the client API for DnsService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type DnsServiceClient interface {
|
||||
Query(ctx context.Context, in *DnsPacket, opts ...grpc.CallOption) (*DnsPacket, error)
|
||||
}
|
||||
|
||||
type dnsServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewDnsServiceClient(cc grpc.ClientConnInterface) DnsServiceClient {
|
||||
return &dnsServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *dnsServiceClient) Query(ctx context.Context, in *DnsPacket, opts ...grpc.CallOption) (*DnsPacket, error) {
|
||||
out := new(DnsPacket)
|
||||
err := c.cc.Invoke(ctx, "/coredns.dns.DnsService/Query", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DnsServiceServer is the server API for DnsService service.
|
||||
// All implementations must embed UnimplementedDnsServiceServer
|
||||
// for forward compatibility
|
||||
type DnsServiceServer interface {
|
||||
Query(context.Context, *DnsPacket) (*DnsPacket, error)
|
||||
mustEmbedUnimplementedDnsServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedDnsServiceServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedDnsServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedDnsServiceServer) Query(context.Context, *DnsPacket) (*DnsPacket, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Query not implemented")
|
||||
}
|
||||
func (UnimplementedDnsServiceServer) mustEmbedUnimplementedDnsServiceServer() {}
|
||||
|
||||
// UnsafeDnsServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to DnsServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeDnsServiceServer interface {
|
||||
mustEmbedUnimplementedDnsServiceServer()
|
||||
}
|
||||
|
||||
func RegisterDnsServiceServer(s grpc.ServiceRegistrar, srv DnsServiceServer) {
|
||||
s.RegisterService(&DnsService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _DnsService_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DnsPacket)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DnsServiceServer).Query(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/coredns.dns.DnsService/Query",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DnsServiceServer).Query(ctx, req.(*DnsPacket))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// DnsService_ServiceDesc is the grpc.ServiceDesc for DnsService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var DnsService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "coredns.dns.DnsService",
|
||||
HandlerType: (*DnsServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Query",
|
||||
Handler: _DnsService_Query_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "dns.proto",
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ServiceBackend defines a (dynamic) backend that returns a slice of service definitions.
|
||||
type ServiceBackend interface {
|
||||
// Services communicates with the backend to retrieve the service definitions. Exact indicates
|
||||
// on exact match should be returned.
|
||||
Services(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error)
|
||||
|
||||
// Reverse communicates with the backend to retrieve service definition based on a IP address
|
||||
// instead of a name. I.e. a reverse DNS lookup.
|
||||
Reverse(ctx context.Context, state request.Request, exact bool, opt Options) ([]msg.Service, error)
|
||||
|
||||
// Lookup is used to find records else where.
|
||||
Lookup(ctx context.Context, state request.Request, name string, typ uint16) (*dns.Msg, error)
|
||||
|
||||
// Returns _all_ services that matches a certain name.
|
||||
// Note: it does not implement a specific service.
|
||||
Records(ctx context.Context, state request.Request, exact bool) ([]msg.Service, error)
|
||||
|
||||
// IsNameError returns true if err indicated a record not found condition
|
||||
IsNameError(err error) bool
|
||||
|
||||
// Serial returns a SOA serial number to construct a SOA record.
|
||||
Serial(state request.Request) uint32
|
||||
|
||||
// MinTTL returns the minimum TTL to be used in the SOA record.
|
||||
MinTTL(state request.Request) uint32
|
||||
}
|
||||
|
||||
// Options are extra options that can be specified for a lookup.
|
||||
type Options struct{}
|
||||
|
|
@ -1,562 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const maxCnameChainLength = 10
|
||||
|
||||
// A returns A records from Backend or an error.
|
||||
func A(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, truncated bool, err error) {
|
||||
services, err := checkForApex(ctx, b, zone, state, opt)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
dup := make(map[string]struct{})
|
||||
|
||||
for _, serv := range services {
|
||||
what, ip := serv.HostType()
|
||||
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) {
|
||||
// x CNAME x is a direct loop, don't add those
|
||||
// in etcd/skydns w.x CNAME x is also direct loop due to the "recursive" nature of search results
|
||||
continue
|
||||
}
|
||||
|
||||
newRecord := serv.NewCNAME(state.QName(), serv.Host)
|
||||
if len(previousRecords) > maxCnameChainLength {
|
||||
// don't add it, and just continue
|
||||
continue
|
||||
}
|
||||
if dnsutil.DuplicateCNAME(newRecord, previousRecords) {
|
||||
continue
|
||||
}
|
||||
if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) {
|
||||
state1 := state.NewWithQuestion(serv.Host, state.QType())
|
||||
state1.Zone = zone
|
||||
nextRecords, tc, err := A(ctx, b, zone, state1, append(previousRecords, newRecord), opt)
|
||||
|
||||
if err == nil {
|
||||
// Not only have we found something we should add the CNAME and the IP addresses.
|
||||
if len(nextRecords) > 0 {
|
||||
records = append(records, newRecord)
|
||||
records = append(records, nextRecords...)
|
||||
}
|
||||
}
|
||||
if tc {
|
||||
truncated = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// This means we can not complete the CNAME, try to look else where.
|
||||
target := newRecord.Target
|
||||
// Lookup
|
||||
m1, e1 := b.Lookup(ctx, state, target, state.QType())
|
||||
if e1 != nil {
|
||||
continue
|
||||
}
|
||||
if m1.Truncated {
|
||||
truncated = true
|
||||
}
|
||||
// Len(m1.Answer) > 0 here is well?
|
||||
records = append(records, newRecord)
|
||||
records = append(records, m1.Answer...)
|
||||
continue
|
||||
|
||||
case dns.TypeA:
|
||||
if _, ok := dup[serv.Host]; !ok {
|
||||
dup[serv.Host] = struct{}{}
|
||||
records = append(records, serv.NewA(state.QName(), ip))
|
||||
}
|
||||
|
||||
case dns.TypeAAAA:
|
||||
// nada
|
||||
}
|
||||
}
|
||||
return records, truncated, nil
|
||||
}
|
||||
|
||||
// AAAA returns AAAA records from Backend or an error.
|
||||
func AAAA(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, truncated bool, err error) {
|
||||
services, err := checkForApex(ctx, b, zone, state, opt)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
dup := make(map[string]struct{})
|
||||
|
||||
for _, serv := range services {
|
||||
what, ip := serv.HostType()
|
||||
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
// Try to resolve as CNAME if it's not an IP, but only if we don't create loops.
|
||||
if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) {
|
||||
// x CNAME x is a direct loop, don't add those
|
||||
// in etcd/skydns w.x CNAME x is also direct loop due to the "recursive" nature of search results
|
||||
continue
|
||||
}
|
||||
|
||||
newRecord := serv.NewCNAME(state.QName(), serv.Host)
|
||||
if len(previousRecords) > maxCnameChainLength {
|
||||
// don't add it, and just continue
|
||||
continue
|
||||
}
|
||||
if dnsutil.DuplicateCNAME(newRecord, previousRecords) {
|
||||
continue
|
||||
}
|
||||
if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) {
|
||||
state1 := state.NewWithQuestion(serv.Host, state.QType())
|
||||
state1.Zone = zone
|
||||
nextRecords, tc, err := AAAA(ctx, b, zone, state1, append(previousRecords, newRecord), opt)
|
||||
|
||||
if err == nil {
|
||||
// Not only have we found something we should add the CNAME and the IP addresses.
|
||||
if len(nextRecords) > 0 {
|
||||
records = append(records, newRecord)
|
||||
records = append(records, nextRecords...)
|
||||
}
|
||||
}
|
||||
if tc {
|
||||
truncated = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// This means we can not complete the CNAME, try to look else where.
|
||||
target := newRecord.Target
|
||||
m1, e1 := b.Lookup(ctx, state, target, state.QType())
|
||||
if e1 != nil {
|
||||
continue
|
||||
}
|
||||
if m1.Truncated {
|
||||
truncated = true
|
||||
}
|
||||
// Len(m1.Answer) > 0 here is well?
|
||||
records = append(records, newRecord)
|
||||
records = append(records, m1.Answer...)
|
||||
continue
|
||||
// both here again
|
||||
|
||||
case dns.TypeA:
|
||||
// nada
|
||||
|
||||
case dns.TypeAAAA:
|
||||
if _, ok := dup[serv.Host]; !ok {
|
||||
dup[serv.Host] = struct{}{}
|
||||
records = append(records, serv.NewAAAA(state.QName(), ip))
|
||||
}
|
||||
}
|
||||
}
|
||||
return records, truncated, nil
|
||||
}
|
||||
|
||||
// SRV returns SRV records from the Backend.
|
||||
// If the Target is not a name but an IP address, a name is created on the fly.
|
||||
func SRV(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) {
|
||||
services, err := b.Services(ctx, state, false, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dup := make(map[item]struct{})
|
||||
lookup := make(map[string]struct{})
|
||||
|
||||
// Looping twice to get the right weight vs priority. This might break because we may drop duplicate SRV records latter on.
|
||||
w := make(map[int]int)
|
||||
for _, serv := range services {
|
||||
weight := 100
|
||||
if serv.Weight != 0 {
|
||||
weight = serv.Weight
|
||||
}
|
||||
if _, ok := w[serv.Priority]; !ok {
|
||||
w[serv.Priority] = weight
|
||||
continue
|
||||
}
|
||||
w[serv.Priority] += weight
|
||||
}
|
||||
for _, serv := range services {
|
||||
// Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint
|
||||
// does not have any declared ports.
|
||||
if serv.Port == -1 {
|
||||
continue
|
||||
}
|
||||
w1 := 100.0 / float64(w[serv.Priority])
|
||||
if serv.Weight == 0 {
|
||||
w1 *= 100
|
||||
} else {
|
||||
w1 *= float64(serv.Weight)
|
||||
}
|
||||
weight := uint16(math.Floor(w1))
|
||||
// weight should be at least 1
|
||||
if weight == 0 {
|
||||
weight = 1
|
||||
}
|
||||
|
||||
what, ip := serv.HostType()
|
||||
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
srv := serv.NewSRV(state.QName(), weight)
|
||||
records = append(records, srv)
|
||||
|
||||
if _, ok := lookup[srv.Target]; ok {
|
||||
break
|
||||
}
|
||||
|
||||
lookup[srv.Target] = struct{}{}
|
||||
|
||||
if !dns.IsSubDomain(zone, srv.Target) {
|
||||
m1, e1 := b.Lookup(ctx, state, srv.Target, dns.TypeA)
|
||||
if e1 == nil {
|
||||
extra = append(extra, m1.Answer...)
|
||||
}
|
||||
|
||||
m1, e1 = b.Lookup(ctx, state, srv.Target, dns.TypeAAAA)
|
||||
if e1 == nil {
|
||||
// If we have seen CNAME's we *assume* that they are already added.
|
||||
for _, a := range m1.Answer {
|
||||
if _, ok := a.(*dns.CNAME); !ok {
|
||||
extra = append(extra, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
// Internal name, we should have some info on them, either v4 or v6
|
||||
// Clients expect a complete answer, because we are a recursor in their view.
|
||||
state1 := state.NewWithQuestion(srv.Target, dns.TypeA)
|
||||
addr, _, e1 := A(ctx, b, zone, state1, nil, opt)
|
||||
if e1 == nil {
|
||||
extra = append(extra, addr...)
|
||||
}
|
||||
// TODO(miek): AAAA as well here.
|
||||
|
||||
case dns.TypeA, dns.TypeAAAA:
|
||||
addr := serv.Host
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
srv := serv.NewSRV(state.QName(), weight)
|
||||
|
||||
if ok := isDuplicate(dup, srv.Target, "", srv.Port); !ok {
|
||||
records = append(records, srv)
|
||||
}
|
||||
|
||||
if ok := isDuplicate(dup, srv.Target, addr, 0); !ok {
|
||||
extra = append(extra, newAddress(serv, srv.Target, ip, what))
|
||||
}
|
||||
}
|
||||
}
|
||||
return records, extra, nil
|
||||
}
|
||||
|
||||
// MX returns MX records from the Backend. If the Target is not a name but an IP address, a name is created on the fly.
|
||||
func MX(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) {
|
||||
services, err := b.Services(ctx, state, false, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dup := make(map[item]struct{})
|
||||
lookup := make(map[string]struct{})
|
||||
for _, serv := range services {
|
||||
if !serv.Mail {
|
||||
continue
|
||||
}
|
||||
what, ip := serv.HostType()
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
mx := serv.NewMX(state.QName())
|
||||
records = append(records, mx)
|
||||
if _, ok := lookup[mx.Mx]; ok {
|
||||
break
|
||||
}
|
||||
|
||||
lookup[mx.Mx] = struct{}{}
|
||||
|
||||
if !dns.IsSubDomain(zone, mx.Mx) {
|
||||
m1, e1 := b.Lookup(ctx, state, mx.Mx, dns.TypeA)
|
||||
if e1 == nil {
|
||||
extra = append(extra, m1.Answer...)
|
||||
}
|
||||
|
||||
m1, e1 = b.Lookup(ctx, state, mx.Mx, dns.TypeAAAA)
|
||||
if e1 == nil {
|
||||
// If we have seen CNAME's we *assume* that they are already added.
|
||||
for _, a := range m1.Answer {
|
||||
if _, ok := a.(*dns.CNAME); !ok {
|
||||
extra = append(extra, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
// Internal name
|
||||
state1 := state.NewWithQuestion(mx.Mx, dns.TypeA)
|
||||
addr, _, e1 := A(ctx, b, zone, state1, nil, opt)
|
||||
if e1 == nil {
|
||||
extra = append(extra, addr...)
|
||||
}
|
||||
// TODO(miek): AAAA as well here.
|
||||
|
||||
case dns.TypeA, dns.TypeAAAA:
|
||||
addr := serv.Host
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
mx := serv.NewMX(state.QName())
|
||||
|
||||
if ok := isDuplicate(dup, mx.Mx, "", mx.Preference); !ok {
|
||||
records = append(records, mx)
|
||||
}
|
||||
// Fake port to be 0 for address...
|
||||
if ok := isDuplicate(dup, serv.Host, addr, 0); !ok {
|
||||
extra = append(extra, newAddress(serv, serv.Host, ip, what))
|
||||
}
|
||||
}
|
||||
}
|
||||
return records, extra, nil
|
||||
}
|
||||
|
||||
// CNAME returns CNAME records from the backend or an error.
|
||||
func CNAME(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) {
|
||||
services, err := b.Services(ctx, state, true, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(services) > 0 {
|
||||
serv := services[0]
|
||||
if ip := net.ParseIP(serv.Host); ip == nil {
|
||||
records = append(records, serv.NewCNAME(state.QName(), serv.Host))
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// TXT returns TXT records from Backend or an error.
|
||||
func TXT(ctx context.Context, b ServiceBackend, zone string, state request.Request, previousRecords []dns.RR, opt Options) (records []dns.RR, truncated bool, err error) {
|
||||
services, err := b.Services(ctx, state, false, opt)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
dup := make(map[string]struct{})
|
||||
|
||||
for _, serv := range services {
|
||||
what, _ := serv.HostType()
|
||||
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
if Name(state.Name()).Matches(dns.Fqdn(serv.Host)) {
|
||||
// x CNAME x is a direct loop, don't add those
|
||||
// in etcd/skydns w.x CNAME x is also direct loop due to the "recursive" nature of search results
|
||||
continue
|
||||
}
|
||||
|
||||
newRecord := serv.NewCNAME(state.QName(), serv.Host)
|
||||
if len(previousRecords) > maxCnameChainLength {
|
||||
// don't add it, and just continue
|
||||
continue
|
||||
}
|
||||
if dnsutil.DuplicateCNAME(newRecord, previousRecords) {
|
||||
continue
|
||||
}
|
||||
if dns.IsSubDomain(zone, dns.Fqdn(serv.Host)) {
|
||||
state1 := state.NewWithQuestion(serv.Host, state.QType())
|
||||
state1.Zone = zone
|
||||
nextRecords, tc, err := TXT(ctx, b, zone, state1, append(previousRecords, newRecord), opt)
|
||||
if tc {
|
||||
truncated = true
|
||||
}
|
||||
if err == nil {
|
||||
// Not only have we found something we should add the CNAME and the IP addresses.
|
||||
if len(nextRecords) > 0 {
|
||||
records = append(records, newRecord)
|
||||
records = append(records, nextRecords...)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
// This means we can not complete the CNAME, try to look else where.
|
||||
target := newRecord.Target
|
||||
// Lookup
|
||||
m1, e1 := b.Lookup(ctx, state, target, state.QType())
|
||||
if e1 != nil {
|
||||
continue
|
||||
}
|
||||
// Len(m1.Answer) > 0 here is well?
|
||||
records = append(records, newRecord)
|
||||
records = append(records, m1.Answer...)
|
||||
continue
|
||||
|
||||
case dns.TypeTXT:
|
||||
if _, ok := dup[serv.Text]; !ok {
|
||||
dup[serv.Text] = struct{}{}
|
||||
records = append(records, serv.NewTXT(state.QName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return records, truncated, nil
|
||||
}
|
||||
|
||||
// PTR returns the PTR records from the backend, only services that have a domain name as host are included.
|
||||
func PTR(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records []dns.RR, err error) {
|
||||
services, err := b.Reverse(ctx, state, true, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dup := make(map[string]struct{})
|
||||
|
||||
for _, serv := range services {
|
||||
if ip := net.ParseIP(serv.Host); ip == nil {
|
||||
if _, ok := dup[serv.Host]; !ok {
|
||||
dup[serv.Host] = struct{}{}
|
||||
records = append(records, serv.NewPTR(state.QName(), serv.Host))
|
||||
}
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// NS returns NS records from the backend
|
||||
func NS(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) (records, extra []dns.RR, err error) {
|
||||
// NS record for this zone lives in a special place, ns.dns.<zone>. Fake our lookup.
|
||||
// Only a tad bit fishy...
|
||||
old := state.QName()
|
||||
|
||||
state.Clear()
|
||||
state.Req.Question[0].Name = dnsutil.Join("ns.dns.", zone)
|
||||
services, err := b.Services(ctx, state, false, opt)
|
||||
// reset the query name to the original
|
||||
state.Req.Question[0].Name = old
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
|
||||
for _, serv := range services {
|
||||
what, ip := serv.HostType()
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
return nil, nil, fmt.Errorf("NS record must be an IP address: %s", serv.Host)
|
||||
|
||||
case dns.TypeA, dns.TypeAAAA:
|
||||
serv.Host = msg.Domain(serv.Key)
|
||||
ns := serv.NewNS(state.QName())
|
||||
extra = append(extra, newAddress(serv, ns.Ns, ip, what))
|
||||
if _, ok := seen[ns.Ns]; ok {
|
||||
continue
|
||||
}
|
||||
seen[ns.Ns] = true
|
||||
records = append(records, ns)
|
||||
}
|
||||
}
|
||||
return records, extra, nil
|
||||
}
|
||||
|
||||
// SOA returns a SOA record from the backend.
|
||||
func SOA(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) ([]dns.RR, error) {
|
||||
minTTL := b.MinTTL(state)
|
||||
ttl := uint32(300)
|
||||
if minTTL < ttl {
|
||||
ttl = minTTL
|
||||
}
|
||||
|
||||
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: ttl, Class: dns.ClassINET}
|
||||
|
||||
Mbox := dnsutil.Join(hostmaster, zone)
|
||||
Ns := dnsutil.Join("ns.dns", zone)
|
||||
|
||||
soa := &dns.SOA{Hdr: header,
|
||||
Mbox: Mbox,
|
||||
Ns: Ns,
|
||||
Serial: b.Serial(state),
|
||||
Refresh: 7200,
|
||||
Retry: 1800,
|
||||
Expire: 86400,
|
||||
Minttl: minTTL,
|
||||
}
|
||||
return []dns.RR{soa}, nil
|
||||
}
|
||||
|
||||
// BackendError writes an error response to the client.
|
||||
func BackendError(ctx context.Context, b ServiceBackend, zone string, rcode int, state request.Request, err error, opt Options) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(state.Req, rcode)
|
||||
m.Authoritative = true
|
||||
m.Ns, _ = SOA(ctx, b, zone, state, opt)
|
||||
|
||||
state.W.WriteMsg(m)
|
||||
// Return success as the rcode to signal we have written to the client.
|
||||
return dns.RcodeSuccess, err
|
||||
}
|
||||
|
||||
func newAddress(s msg.Service, name string, ip net.IP, what uint16) dns.RR {
|
||||
hdr := dns.RR_Header{Name: name, Rrtype: what, Class: dns.ClassINET, Ttl: s.TTL}
|
||||
|
||||
if what == dns.TypeA {
|
||||
return &dns.A{Hdr: hdr, A: ip}
|
||||
}
|
||||
// Should always be dns.TypeAAAA
|
||||
return &dns.AAAA{Hdr: hdr, AAAA: ip}
|
||||
}
|
||||
|
||||
// checkForApex checks the special apex.dns directory for records that will be returned as A or AAAA.
|
||||
func checkForApex(ctx context.Context, b ServiceBackend, zone string, state request.Request, opt Options) ([]msg.Service, error) {
|
||||
if state.Name() != zone {
|
||||
return b.Services(ctx, state, false, opt)
|
||||
}
|
||||
|
||||
// If the zone name itself is queried we fake the query to search for a special entry
|
||||
// this is equivalent to the NS search code.
|
||||
old := state.QName()
|
||||
state.Clear()
|
||||
state.Req.Question[0].Name = dnsutil.Join("apex.dns", zone)
|
||||
|
||||
services, err := b.Services(ctx, state, false, opt)
|
||||
if err == nil {
|
||||
state.Req.Question[0].Name = old
|
||||
return services, err
|
||||
}
|
||||
|
||||
state.Req.Question[0].Name = old
|
||||
return b.Services(ctx, state, false, opt)
|
||||
}
|
||||
|
||||
// item holds records.
|
||||
type item struct {
|
||||
name string // name of the record (either owner or something else unique).
|
||||
port uint16 // port of the record (used for address records, A and AAAA).
|
||||
addr string // address of the record (A and AAAA).
|
||||
}
|
||||
|
||||
// isDuplicate uses m to see if the combo (name, addr, port) already exists. If it does
|
||||
// not exist already IsDuplicate will also add the record to the map.
|
||||
func isDuplicate(m map[item]struct{}, name, addr string, port uint16) bool {
|
||||
if addr != "" {
|
||||
_, ok := m[item{name, 0, addr}]
|
||||
if !ok {
|
||||
m[item{name, 0, addr}] = struct{}{}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
_, ok := m[item{name, port, ""}]
|
||||
if !ok {
|
||||
m[item{name, port, ""}] = struct{}{}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
const hostmaster = "hostmaster"
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
# cache
|
||||
|
||||
## Name
|
||||
|
||||
*cache* - enables a frontend cache.
|
||||
|
||||
## Description
|
||||
|
||||
With *cache* enabled, all records except zone transfers and metadata records will be cached for up to
|
||||
3600s. Caching is mostly useful in a scenario when fetching data from the backend (upstream,
|
||||
database, etc.) is expensive.
|
||||
|
||||
*Cache* will pass DNSSEC (DNSSEC OK; DO) options through the plugin for upstream queries.
|
||||
|
||||
This plugin can only be used once per Server Block.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
cache [TTL] [ZONES...]
|
||||
~~~
|
||||
|
||||
* **TTL** max TTL in seconds. If not specified, the maximum TTL will be used, which is 3600 for
|
||||
NOERROR responses and 1800 for denial of existence ones.
|
||||
Setting a TTL of 300: `cache 300` would cache records up to 300 seconds.
|
||||
* **ZONES** zones it should cache for. If empty, the zones from the configuration block are used.
|
||||
|
||||
Each element in the cache is cached according to its TTL (with **TTL** as the max).
|
||||
A cache is divided into 256 shards, each holding up to 39 items by default - for a total size
|
||||
of 256 * 39 = 9984 items.
|
||||
|
||||
If you want more control:
|
||||
|
||||
~~~ txt
|
||||
cache [TTL] [ZONES...] {
|
||||
success CAPACITY [TTL] [MINTTL]
|
||||
denial CAPACITY [TTL] [MINTTL]
|
||||
prefetch AMOUNT [[DURATION] [PERCENTAGE%]]
|
||||
serve_stale [DURATION] [REFRESH_MODE]
|
||||
servfail DURATION
|
||||
disable success|denial [ZONES...]
|
||||
keepttl
|
||||
}
|
||||
~~~
|
||||
|
||||
* **TTL** and **ZONES** as above.
|
||||
* `success`, override the settings for caching successful responses. **CAPACITY** indicates the maximum
|
||||
number of packets we cache before we start evicting (*randomly*). **TTL** overrides the cache maximum TTL.
|
||||
**MINTTL** overrides the cache minimum TTL (default 5), which can be useful to limit queries to the backend.
|
||||
* `denial`, override the settings for caching denial of existence responses. **CAPACITY** indicates the maximum
|
||||
number of packets we cache before we start evicting (LRU). **TTL** overrides the cache maximum TTL.
|
||||
**MINTTL** overrides the cache minimum TTL (default 5), which can be useful to limit queries to the backend.
|
||||
There is a third category (`error`) but those responses are never cached.
|
||||
* `prefetch` will prefetch popular items when they are about to be expunged from the cache.
|
||||
Popular means **AMOUNT** queries have been seen with no gaps of **DURATION** or more between them.
|
||||
**DURATION** defaults to 1m. Prefetching will happen when the TTL drops below **PERCENTAGE**,
|
||||
which defaults to `10%`, or latest 1 second before TTL expiration. Values should be in the range `[10%, 90%]`.
|
||||
Note the percent sign is mandatory. **PERCENTAGE** is treated as an `int`.
|
||||
* `serve_stale`, when serve\_stale is set, cache will always serve an expired entry to a client if there is one
|
||||
available as long as it has not been expired for longer than **DURATION** (default 1 hour). By default, the _cache_ plugin will
|
||||
attempt to refresh the cache entry after sending the expired cache entry to the client. The
|
||||
responses have a TTL of 0. **REFRESH_MODE** controls the timing of the expired cache entry refresh.
|
||||
`verify` will first verify that an entry is still unavailable from the source before sending the expired entry to the client.
|
||||
`immediate` will immediately send the expired entry to the client before
|
||||
checking to see if the entry is available from the source. **REFRESH_MODE** defaults to `immediate`. Setting this
|
||||
value to `verify` can lead to increased latency when serving stale responses, but will prevent stale entries
|
||||
from ever being served if an updated response can be retrieved from the source.
|
||||
* `servfail` cache SERVFAIL responses for **DURATION**. Setting **DURATION** to 0 will disable caching of SERVFAIL
|
||||
responses. If this option is not set, SERVFAIL responses will be cached for 5 seconds. **DURATION** may not be
|
||||
greater than 5 minutes.
|
||||
* `disable` disable the success or denial cache for the listed **ZONES**. If no **ZONES** are given, the specified
|
||||
cache will be disabled for all zones.
|
||||
* `keepttl` do not age TTL when serving responses from cache. The entry will still be removed from cache
|
||||
when the TTL expires as normal, but until it expires responses will include the original TTL instead
|
||||
of the remaining TTL. This can be useful if CoreDNS is used as an authoritative server and you want
|
||||
to serve a consistent TTL to downstream clients. This is **NOT** recommended when CoreDNS is caching
|
||||
records it is not authoritative for because it could result in downstream clients using stale answers.
|
||||
|
||||
## Capacity and Eviction
|
||||
|
||||
If **CAPACITY** _is not_ specified, the default cache size is 9984 per cache. The minimum allowed cache size is 1024.
|
||||
If **CAPACITY** _is_ specified, the actual cache size used will be rounded down to the nearest number divisible by 256 (so all shards are equal in size).
|
||||
|
||||
Eviction is done per shard. In effect, when a shard reaches capacity, items are evicted from that shard.
|
||||
Since shards don't fill up perfectly evenly, evictions will occur before the entire cache reaches full capacity.
|
||||
Each shard capacity is equal to the total cache size / number of shards (256). Eviction is random, not TTL based.
|
||||
Entries with 0 TTL will remain in the cache until randomly evicted when the shard reaches capacity.
|
||||
|
||||
## Metrics
|
||||
|
||||
If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported:
|
||||
|
||||
* `coredns_cache_entries{server, type, zones, view}` - Total elements in the cache by cache type.
|
||||
* `coredns_cache_hits_total{server, type, zones, view}` - Counter of cache hits by cache type.
|
||||
* `coredns_cache_misses_total{server, zones, view}` - Counter of cache misses. - Deprecated, derive misses from cache hits/requests counters.
|
||||
* `coredns_cache_requests_total{server, zones, view}` - Counter of cache requests.
|
||||
* `coredns_cache_prefetch_total{server, zones, view}` - Counter of times the cache has prefetched a cached item.
|
||||
* `coredns_cache_drops_total{server, zones, view}` - Counter of responses excluded from the cache due to request/response question name mismatch.
|
||||
* `coredns_cache_served_stale_total{server, zones, view}` - Counter of requests served from stale cache entries.
|
||||
* `coredns_cache_evictions_total{server, type, zones, view}` - Counter of cache evictions.
|
||||
|
||||
Cache types are either "denial" or "success". `Server` is the server handling the request, see the
|
||||
prometheus plugin for documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
Enable caching for all zones, but cap everything to a TTL of 10 seconds:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
cache 10
|
||||
whoami
|
||||
}
|
||||
~~~
|
||||
|
||||
Proxy to Google Public DNS and only cache responses for example.org (or below).
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
forward . 8.8.8.8:53
|
||||
cache example.org
|
||||
}
|
||||
~~~
|
||||
|
||||
Enable caching for `example.org`, keep a positive cache size of 5000 and a negative cache size of 2500:
|
||||
|
||||
~~~ corefile
|
||||
example.org {
|
||||
cache {
|
||||
success 5000
|
||||
denial 2500
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
Enable caching for `example.org`, but do not cache denials in `sub.example.org`:
|
||||
|
||||
~~~ corefile
|
||||
example.org {
|
||||
cache {
|
||||
disable denial sub.example.org
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
// Package cache implements a cache.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/cache"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Cache is a plugin that looks up responses in a cache and caches replies.
|
||||
// It has a success and a denial of existence cache.
|
||||
type Cache struct {
|
||||
Next plugin.Handler
|
||||
Zones []string
|
||||
|
||||
zonesMetricLabel string
|
||||
viewMetricLabel string
|
||||
|
||||
ncache *cache.Cache
|
||||
ncap int
|
||||
nttl time.Duration
|
||||
minnttl time.Duration
|
||||
|
||||
pcache *cache.Cache
|
||||
pcap int
|
||||
pttl time.Duration
|
||||
minpttl time.Duration
|
||||
failttl time.Duration // TTL for caching SERVFAIL responses
|
||||
|
||||
// Prefetch.
|
||||
prefetch int
|
||||
duration time.Duration
|
||||
percentage int
|
||||
|
||||
// Stale serve
|
||||
staleUpTo time.Duration
|
||||
verifyStale bool
|
||||
|
||||
// Positive/negative zone exceptions
|
||||
pexcept []string
|
||||
nexcept []string
|
||||
|
||||
// Keep ttl option
|
||||
keepttl bool
|
||||
|
||||
// Testing.
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// New returns an initialized Cache with default settings. It's up to the
|
||||
// caller to set the Next handler.
|
||||
func New() *Cache {
|
||||
return &Cache{
|
||||
Zones: []string{"."},
|
||||
pcap: defaultCap,
|
||||
pcache: cache.New(defaultCap),
|
||||
pttl: maxTTL,
|
||||
minpttl: minTTL,
|
||||
ncap: defaultCap,
|
||||
ncache: cache.New(defaultCap),
|
||||
nttl: maxNTTL,
|
||||
minnttl: minNTTL,
|
||||
failttl: minNTTL,
|
||||
prefetch: 0,
|
||||
duration: 1 * time.Minute,
|
||||
percentage: 10,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// key returns key under which we store the item, -1 will be returned if we don't store the message.
|
||||
// Currently we do not cache Truncated, errors zone transfers or dynamic update messages.
|
||||
// qname holds the already lowercased qname.
|
||||
func key(qname string, m *dns.Msg, t response.Type, do, cd bool) (bool, uint64) {
|
||||
// We don't store truncated responses.
|
||||
if m.Truncated {
|
||||
return false, 0
|
||||
}
|
||||
// Nor errors or Meta or Update.
|
||||
if t == response.OtherError || t == response.Meta || t == response.Update {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
return true, hash(qname, m.Question[0].Qtype, do, cd)
|
||||
}
|
||||
|
||||
var one = []byte("1")
|
||||
var zero = []byte("0")
|
||||
|
||||
func hash(qname string, qtype uint16, do, cd bool) uint64 {
|
||||
h := fnv.New64()
|
||||
|
||||
if do {
|
||||
h.Write(one)
|
||||
} else {
|
||||
h.Write(zero)
|
||||
}
|
||||
|
||||
if cd {
|
||||
h.Write(one)
|
||||
} else {
|
||||
h.Write(zero)
|
||||
}
|
||||
|
||||
h.Write([]byte{byte(qtype >> 8)})
|
||||
h.Write([]byte{byte(qtype)})
|
||||
h.Write([]byte(qname))
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func computeTTL(msgTTL, minTTL, maxTTL time.Duration) time.Duration {
|
||||
ttl := msgTTL
|
||||
if ttl < minTTL {
|
||||
ttl = minTTL
|
||||
}
|
||||
if ttl > maxTTL {
|
||||
ttl = maxTTL
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
// ResponseWriter is a response writer that caches the reply message.
|
||||
type ResponseWriter struct {
|
||||
dns.ResponseWriter
|
||||
*Cache
|
||||
state request.Request
|
||||
server string // Server handling the request.
|
||||
|
||||
do bool // When true the original request had the DO bit set.
|
||||
cd bool // When true the original request had the CD bit set.
|
||||
ad bool // When true the original request had the AD bit set.
|
||||
prefetch bool // When true write nothing back to the client.
|
||||
remoteAddr net.Addr
|
||||
|
||||
wildcardFunc func() string // function to retrieve wildcard name that synthesized the result.
|
||||
|
||||
pexcept []string // positive zone exceptions
|
||||
nexcept []string // negative zone exceptions
|
||||
}
|
||||
|
||||
// newPrefetchResponseWriter returns a Cache ResponseWriter to be used in
|
||||
// prefetch requests. It ensures RemoteAddr() can be called even after the
|
||||
// original connection has already been closed.
|
||||
func newPrefetchResponseWriter(server string, state request.Request, c *Cache) *ResponseWriter {
|
||||
// Resolve the address now, the connection might be already closed when the
|
||||
// actual prefetch request is made.
|
||||
addr := state.W.RemoteAddr()
|
||||
// The protocol of the client triggering a cache prefetch doesn't matter.
|
||||
// The address type is used by request.Proto to determine the response size,
|
||||
// and using TCP ensures the message isn't unnecessarily truncated.
|
||||
if u, ok := addr.(*net.UDPAddr); ok {
|
||||
addr = &net.TCPAddr{IP: u.IP, Port: u.Port, Zone: u.Zone}
|
||||
}
|
||||
|
||||
return &ResponseWriter{
|
||||
ResponseWriter: state.W,
|
||||
Cache: c,
|
||||
state: state,
|
||||
server: server,
|
||||
do: state.Do(),
|
||||
cd: state.Req.CheckingDisabled,
|
||||
prefetch: true,
|
||||
remoteAddr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteAddr implements the dns.ResponseWriter interface.
|
||||
func (w *ResponseWriter) RemoteAddr() net.Addr {
|
||||
if w.remoteAddr != nil {
|
||||
return w.remoteAddr
|
||||
}
|
||||
return w.ResponseWriter.RemoteAddr()
|
||||
}
|
||||
|
||||
// WriteMsg implements the dns.ResponseWriter interface.
|
||||
func (w *ResponseWriter) WriteMsg(res *dns.Msg) error {
|
||||
mt, _ := response.Typify(res, w.now().UTC())
|
||||
|
||||
// key returns empty string for anything we don't want to cache.
|
||||
hasKey, key := key(w.state.Name(), res, mt, w.do, w.cd)
|
||||
|
||||
msgTTL := dnsutil.MinimalTTL(res, mt)
|
||||
var duration time.Duration
|
||||
switch mt {
|
||||
case response.NameError, response.NoData:
|
||||
duration = computeTTL(msgTTL, w.minnttl, w.nttl)
|
||||
case response.ServerError:
|
||||
duration = w.failttl
|
||||
default:
|
||||
duration = computeTTL(msgTTL, w.minpttl, w.pttl)
|
||||
}
|
||||
|
||||
if hasKey && duration > 0 {
|
||||
if w.state.Match(res) {
|
||||
w.set(res, key, mt, duration)
|
||||
cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.pcache.Len()))
|
||||
cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.ncache.Len()))
|
||||
} else {
|
||||
// Don't log it, but increment counter
|
||||
cacheDrops.WithLabelValues(w.server, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
if w.prefetch {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply capped TTL to this reply to avoid jarring TTL experience 1799 -> 8 (e.g.)
|
||||
ttl := uint32(duration.Seconds())
|
||||
res.Answer = filterRRSlice(res.Answer, ttl, false)
|
||||
res.Ns = filterRRSlice(res.Ns, ttl, false)
|
||||
res.Extra = filterRRSlice(res.Extra, ttl, false)
|
||||
|
||||
if !w.do && !w.ad {
|
||||
// unset AD bit if requester is not OK with DNSSEC
|
||||
// But retain AD bit if requester set the AD bit in the request, per RFC6840 5.7-5.8
|
||||
res.AuthenticatedData = false
|
||||
}
|
||||
|
||||
return w.ResponseWriter.WriteMsg(res)
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration time.Duration) {
|
||||
// duration is expected > 0
|
||||
// and key is valid
|
||||
switch mt {
|
||||
case response.NoError, response.Delegation:
|
||||
if plugin.Zones(w.pexcept).Matches(m.Question[0].Name) != "" {
|
||||
// zone is in exception list, do not cache
|
||||
return
|
||||
}
|
||||
i := newItem(m, w.now(), duration)
|
||||
if w.wildcardFunc != nil {
|
||||
i.wildcard = w.wildcardFunc()
|
||||
}
|
||||
if w.pcache.Add(key, i) {
|
||||
evictions.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
||||
}
|
||||
// when pre-fetching, remove the negative cache entry if it exists
|
||||
if w.prefetch {
|
||||
w.ncache.Remove(key)
|
||||
}
|
||||
|
||||
case response.NameError, response.NoData, response.ServerError:
|
||||
if plugin.Zones(w.nexcept).Matches(m.Question[0].Name) != "" {
|
||||
// zone is in exception list, do not cache
|
||||
return
|
||||
}
|
||||
i := newItem(m, w.now(), duration)
|
||||
if w.wildcardFunc != nil {
|
||||
i.wildcard = w.wildcardFunc()
|
||||
}
|
||||
if w.ncache.Add(key, i) {
|
||||
evictions.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
||||
}
|
||||
|
||||
case response.OtherError:
|
||||
// don't cache these
|
||||
default:
|
||||
log.Warningf("Caching called with unknown classification: %d", mt)
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements the dns.ResponseWriter interface.
|
||||
func (w *ResponseWriter) Write(buf []byte) (int, error) {
|
||||
log.Warning("Caching called with Write: not caching reply")
|
||||
if w.prefetch {
|
||||
return 0, nil
|
||||
}
|
||||
n, err := w.ResponseWriter.Write(buf)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// verifyStaleResponseWriter is a response writer that only writes messages if they should replace a
|
||||
// stale cache entry, and otherwise discards them.
|
||||
type verifyStaleResponseWriter struct {
|
||||
*ResponseWriter
|
||||
refreshed bool // set to true if the last WriteMsg wrote to ResponseWriter, false otherwise.
|
||||
}
|
||||
|
||||
// newVerifyStaleResponseWriter returns a ResponseWriter to be used when verifying stale cache
|
||||
// entries. It only forward writes if an entry was successfully refreshed according to RFC8767,
|
||||
// section 4 (response is NoError or NXDomain), and ignores any other response.
|
||||
func newVerifyStaleResponseWriter(w *ResponseWriter) *verifyStaleResponseWriter {
|
||||
return &verifyStaleResponseWriter{
|
||||
w,
|
||||
false,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteMsg implements the dns.ResponseWriter interface.
|
||||
func (w *verifyStaleResponseWriter) WriteMsg(res *dns.Msg) error {
|
||||
w.refreshed = false
|
||||
if res.Rcode == dns.RcodeSuccess || res.Rcode == dns.RcodeNameError {
|
||||
w.refreshed = true
|
||||
return w.ResponseWriter.WriteMsg(res) // stores to the cache and send to client
|
||||
}
|
||||
return nil // else discard
|
||||
}
|
||||
|
||||
const (
|
||||
maxTTL = dnsutil.MaximumDefaulTTL
|
||||
minTTL = dnsutil.MinimalDefaultTTL
|
||||
maxNTTL = dnsutil.MaximumDefaulTTL / 2
|
||||
minNTTL = dnsutil.MinimalDefaultTTL
|
||||
|
||||
defaultCap = 10000 // default capacity of the cache.
|
||||
|
||||
// Success is the class for caching positive caching.
|
||||
Success = "success"
|
||||
// Denial is the class defined for negative caching.
|
||||
Denial = "denial"
|
||||
)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package cache
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// filterRRSlice filters out OPT RRs, and sets all RR TTLs to ttl.
|
||||
// If dup is true the RRs in rrs are _copied_ into the slice that is
|
||||
// returned.
|
||||
func filterRRSlice(rrs []dns.RR, ttl uint32, dup bool) []dns.RR {
|
||||
j := 0
|
||||
rs := make([]dns.RR, len(rrs))
|
||||
for _, r := range rrs {
|
||||
if r.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
r.Header().Ttl = ttl
|
||||
if dup {
|
||||
rs[j] = dns.Copy(r)
|
||||
} else {
|
||||
rs[j] = r
|
||||
}
|
||||
j++
|
||||
}
|
||||
return rs[:j]
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// Package freq keeps track of last X seen events. The events themselves are not stored
|
||||
// here. So the Freq type should be added next to the thing it is tracking.
|
||||
package freq
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Freq tracks the frequencies of things.
|
||||
type Freq struct {
|
||||
// Last time we saw a query for this element.
|
||||
last time.Time
|
||||
// Number of this in the last time slice.
|
||||
hits int
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// New returns a new initialized Freq.
|
||||
func New(t time.Time) *Freq {
|
||||
return &Freq{last: t, hits: 0}
|
||||
}
|
||||
|
||||
// Update updates the number of hits. Last time seen will be set to now.
|
||||
// If the last time we've seen this entity is within now - d, we increment hits, otherwise
|
||||
// we reset hits to 1. It returns the number of hits.
|
||||
func (f *Freq) Update(d time.Duration, now time.Time) int {
|
||||
earliest := now.Add(-1 * d)
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
if f.last.Before(earliest) {
|
||||
f.last = now
|
||||
f.hits = 1
|
||||
return f.hits
|
||||
}
|
||||
f.last = now
|
||||
f.hits++
|
||||
return f.hits
|
||||
}
|
||||
|
||||
// Hits returns the number of hits that we have seen, according to the updates we have done to f.
|
||||
func (f *Freq) Hits() int {
|
||||
f.RLock()
|
||||
defer f.RUnlock()
|
||||
return f.hits
|
||||
}
|
||||
|
||||
// Reset resets f to time t and hits to hits.
|
||||
func (f *Freq) Reset(t time.Time, hits int) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.last = t
|
||||
f.hits = hits
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
//go:build gofuzz
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin/pkg/fuzz"
|
||||
)
|
||||
|
||||
// Fuzz fuzzes cache.
|
||||
func Fuzz(data []byte) int {
|
||||
return fuzz.Do(New(), data)
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metadata"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ServeDNS implements the plugin.Handler interface.
|
||||
func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
rc := r.Copy() // We potentially modify r, to prevent other plugins from seeing this (r is a pointer), copy r into rc.
|
||||
state := request.Request{W: w, Req: rc}
|
||||
do := state.Do()
|
||||
cd := r.CheckingDisabled
|
||||
ad := r.AuthenticatedData
|
||||
|
||||
zone := plugin.Zones(c.Zones).Matches(state.Name())
|
||||
if zone == "" {
|
||||
return plugin.NextOrFailure(c.Name(), c.Next, ctx, w, rc)
|
||||
}
|
||||
|
||||
now := c.now().UTC()
|
||||
server := metrics.WithServer(ctx)
|
||||
|
||||
// On cache refresh, we will just use the DO bit from the incoming query for the refresh since we key our cache
|
||||
// with the query DO bit. That means two separate cache items for the query DO bit true or false. In the situation
|
||||
// in which upstream doesn't support DNSSEC, the two cache items will effectively be the same. Regardless, any
|
||||
// DNSSEC RRs in the response are written to cache with the response.
|
||||
|
||||
i := c.getIgnoreTTL(now, state, server)
|
||||
if i == nil {
|
||||
crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, ad: ad, cd: cd,
|
||||
nexcept: c.nexcept, pexcept: c.pexcept, wildcardFunc: wildcardFunc(ctx)}
|
||||
return c.doRefresh(ctx, state, crr)
|
||||
}
|
||||
ttl := i.ttl(now)
|
||||
if ttl < 0 {
|
||||
// serve stale behavior
|
||||
if c.verifyStale {
|
||||
crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, cd: cd}
|
||||
cw := newVerifyStaleResponseWriter(crr)
|
||||
ret, err := c.doRefresh(ctx, state, cw)
|
||||
if cw.refreshed {
|
||||
return ret, err
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the time to get a 0 TTL in the reply built from a stale item.
|
||||
now = now.Add(time.Duration(ttl) * time.Second)
|
||||
if !c.verifyStale {
|
||||
cw := newPrefetchResponseWriter(server, state, c)
|
||||
go c.doPrefetch(ctx, state, cw, i, now)
|
||||
}
|
||||
servedStale.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
} else if c.shouldPrefetch(i, now) {
|
||||
cw := newPrefetchResponseWriter(server, state, c)
|
||||
go c.doPrefetch(ctx, state, cw, i, now)
|
||||
}
|
||||
|
||||
if i.wildcard != "" {
|
||||
// Set wildcard source record name to metadata
|
||||
metadata.SetValueFunc(ctx, "zone/wildcard", func() string {
|
||||
return i.wildcard
|
||||
})
|
||||
}
|
||||
|
||||
if c.keepttl {
|
||||
// If keepttl is enabled we fake the current time to the stored
|
||||
// one so that we always get the original TTL
|
||||
now = i.stored
|
||||
}
|
||||
resp := i.toMsg(r, now, do, ad)
|
||||
w.WriteMsg(resp)
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
func wildcardFunc(ctx context.Context) func() string {
|
||||
return func() string {
|
||||
// Get wildcard source record name from metadata
|
||||
if f := metadata.ValueFunc(ctx, "zone/wildcard"); f != nil {
|
||||
return f()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) doPrefetch(ctx context.Context, state request.Request, cw *ResponseWriter, i *item, now time.Time) {
|
||||
cachePrefetches.WithLabelValues(cw.server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
c.doRefresh(ctx, state, cw)
|
||||
|
||||
// When prefetching we loose the item i, and with it the frequency
|
||||
// that we've gathered sofar. See we copy the frequencies info back
|
||||
// into the new item that was stored in the cache.
|
||||
if i1 := c.exists(state); i1 != nil {
|
||||
i1.Reset(now, i.Hits())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) doRefresh(ctx context.Context, state request.Request, cw dns.ResponseWriter) (int, error) {
|
||||
return plugin.NextOrFailure(c.Name(), c.Next, ctx, cw, state.Req)
|
||||
}
|
||||
|
||||
func (c *Cache) shouldPrefetch(i *item, now time.Time) bool {
|
||||
if c.prefetch <= 0 {
|
||||
return false
|
||||
}
|
||||
i.Update(c.duration, now)
|
||||
threshold := int(math.Ceil(float64(c.percentage) / 100 * float64(i.origTTL)))
|
||||
return i.Hits() >= c.prefetch && i.ttl(now) <= threshold
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (c *Cache) Name() string { return "cache" }
|
||||
|
||||
// getIgnoreTTL unconditionally returns an item if it exists in the cache.
|
||||
func (c *Cache) getIgnoreTTL(now time.Time, state request.Request, server string) *item {
|
||||
k := hash(state.Name(), state.QType(), state.Do(), state.Req.CheckingDisabled)
|
||||
cacheRequests.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
|
||||
if i, ok := c.ncache.Get(k); ok {
|
||||
itm := i.(*item)
|
||||
ttl := itm.ttl(now)
|
||||
if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) {
|
||||
cacheHits.WithLabelValues(server, Denial, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
return i.(*item)
|
||||
}
|
||||
}
|
||||
if i, ok := c.pcache.Get(k); ok {
|
||||
itm := i.(*item)
|
||||
ttl := itm.ttl(now)
|
||||
if itm.matches(state) && (ttl > 0 || (c.staleUpTo > 0 && -ttl < int(c.staleUpTo.Seconds()))) {
|
||||
cacheHits.WithLabelValues(server, Success, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
return i.(*item)
|
||||
}
|
||||
}
|
||||
cacheMisses.WithLabelValues(server, c.zonesMetricLabel, c.viewMetricLabel).Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) exists(state request.Request) *item {
|
||||
k := hash(state.Name(), state.QType(), state.Do(), state.Req.CheckingDisabled)
|
||||
if i, ok := c.ncache.Get(k); ok {
|
||||
return i.(*item)
|
||||
}
|
||||
if i, ok := c.pcache.Get(k); ok {
|
||||
return i.(*item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin/cache/freq"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type item struct {
|
||||
Name string
|
||||
QType uint16
|
||||
Rcode int
|
||||
AuthenticatedData bool
|
||||
RecursionAvailable bool
|
||||
Answer []dns.RR
|
||||
Ns []dns.RR
|
||||
Extra []dns.RR
|
||||
wildcard string
|
||||
|
||||
origTTL uint32
|
||||
stored time.Time
|
||||
|
||||
*freq.Freq
|
||||
}
|
||||
|
||||
func newItem(m *dns.Msg, now time.Time, d time.Duration) *item {
|
||||
i := new(item)
|
||||
if len(m.Question) != 0 {
|
||||
i.Name = m.Question[0].Name
|
||||
i.QType = m.Question[0].Qtype
|
||||
}
|
||||
i.Rcode = m.Rcode
|
||||
i.AuthenticatedData = m.AuthenticatedData
|
||||
i.RecursionAvailable = m.RecursionAvailable
|
||||
i.Answer = m.Answer
|
||||
i.Ns = m.Ns
|
||||
i.Extra = make([]dns.RR, len(m.Extra))
|
||||
// Don't copy OPT records as these are hop-by-hop.
|
||||
j := 0
|
||||
for _, e := range m.Extra {
|
||||
if e.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
i.Extra[j] = e
|
||||
j++
|
||||
}
|
||||
i.Extra = i.Extra[:j]
|
||||
|
||||
i.origTTL = uint32(d.Seconds())
|
||||
i.stored = now.UTC()
|
||||
|
||||
i.Freq = new(freq.Freq)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// toMsg turns i into a message, it tailors the reply to m.
|
||||
// The Authoritative bit should be set to 0, but some client stub resolver implementations, most notably,
|
||||
// on some legacy systems(e.g. ubuntu 14.04 with glib version 2.20), low-level glibc function `getaddrinfo`
|
||||
// useb by Python/Ruby/etc.. will discard answers that do not have this bit set.
|
||||
// So we're forced to always set this to 1; regardless if the answer came from the cache or not.
|
||||
// On newer systems(e.g. ubuntu 16.04 with glib version 2.23), this issue is resolved.
|
||||
// So we may set this bit back to 0 in the future ?
|
||||
func (i *item) toMsg(m *dns.Msg, now time.Time, do bool, ad bool) *dns.Msg {
|
||||
m1 := new(dns.Msg)
|
||||
m1.SetReply(m)
|
||||
|
||||
// Set this to true as some DNS clients discard the *entire* packet when it's non-authoritative.
|
||||
// This is probably not according to spec, but the bit itself is not super useful as this point, so
|
||||
// just set it to true.
|
||||
m1.Authoritative = true
|
||||
m1.AuthenticatedData = i.AuthenticatedData
|
||||
if !do && !ad {
|
||||
// When DNSSEC was not wanted, it can't be authenticated data.
|
||||
// However, retain the AD bit if the requester set the AD bit, per RFC6840 5.7-5.8
|
||||
m1.AuthenticatedData = false
|
||||
}
|
||||
m1.RecursionAvailable = i.RecursionAvailable
|
||||
m1.Rcode = i.Rcode
|
||||
|
||||
m1.Answer = make([]dns.RR, len(i.Answer))
|
||||
m1.Ns = make([]dns.RR, len(i.Ns))
|
||||
m1.Extra = make([]dns.RR, len(i.Extra))
|
||||
|
||||
ttl := uint32(i.ttl(now))
|
||||
m1.Answer = filterRRSlice(i.Answer, ttl, true)
|
||||
m1.Ns = filterRRSlice(i.Ns, ttl, true)
|
||||
m1.Extra = filterRRSlice(i.Extra, ttl, true)
|
||||
|
||||
return m1
|
||||
}
|
||||
|
||||
func (i *item) ttl(now time.Time) int {
|
||||
ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds())
|
||||
return ttl
|
||||
}
|
||||
|
||||
func (i *item) matches(state request.Request) bool {
|
||||
if state.QType() == i.QType && strings.EqualFold(state.QName(), i.Name) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
// cacheSize is total elements in the cache by cache type.
|
||||
cacheSize = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "entries",
|
||||
Help: "The number of elements in the cache.",
|
||||
}, []string{"server", "type", "zones", "view"})
|
||||
// cacheRequests is a counter of all requests through the cache.
|
||||
cacheRequests = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "requests_total",
|
||||
Help: "The count of cache requests.",
|
||||
}, []string{"server", "zones", "view"})
|
||||
// cacheHits is counter of cache hits by cache type.
|
||||
cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "hits_total",
|
||||
Help: "The count of cache hits.",
|
||||
}, []string{"server", "type", "zones", "view"})
|
||||
// cacheMisses is the counter of cache misses. - Deprecated
|
||||
cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "misses_total",
|
||||
Help: "The count of cache misses. Deprecated, derive misses from cache hits/requests counters.",
|
||||
}, []string{"server", "zones", "view"})
|
||||
// cachePrefetches is the number of time the cache has prefetched a cached item.
|
||||
cachePrefetches = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "prefetch_total",
|
||||
Help: "The number of times the cache has prefetched a cached item.",
|
||||
}, []string{"server", "zones", "view"})
|
||||
// cacheDrops is the number responses that are not cached, because the reply is malformed.
|
||||
cacheDrops = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "drops_total",
|
||||
Help: "The number responses that are not cached, because the reply is malformed.",
|
||||
}, []string{"server", "zones", "view"})
|
||||
// servedStale is the number of requests served from stale cache entries.
|
||||
servedStale = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "served_stale_total",
|
||||
Help: "The number of requests served from stale cache entries.",
|
||||
}, []string{"server", "zones", "view"})
|
||||
// evictions is the counter of cache evictions.
|
||||
evictions = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "cache",
|
||||
Name: "evictions_total",
|
||||
Help: "The count of cache evictions.",
|
||||
}, []string{"server", "type", "zones", "view"})
|
||||
)
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/cache"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
var log = clog.NewWithPlugin("cache")
|
||||
|
||||
func init() { plugin.Register("cache", setup) }
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
ca, err := cacheParse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("cache", err)
|
||||
}
|
||||
|
||||
c.OnStartup(func() error {
|
||||
ca.viewMetricLabel = dnsserver.GetConfig(c).ViewName
|
||||
return nil
|
||||
})
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
ca.Next = next
|
||||
return ca
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cacheParse(c *caddy.Controller) (*Cache, error) {
|
||||
ca := New()
|
||||
|
||||
j := 0
|
||||
for c.Next() {
|
||||
if j > 0 {
|
||||
return nil, plugin.ErrOnce
|
||||
}
|
||||
j++
|
||||
|
||||
// cache [ttl] [zones..]
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 0 {
|
||||
// first args may be just a number, then it is the ttl, if not it is a zone
|
||||
ttl, err := strconv.Atoi(args[0])
|
||||
if err == nil {
|
||||
// Reserve 0 (and smaller for future things)
|
||||
if ttl <= 0 {
|
||||
return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", ttl)
|
||||
}
|
||||
ca.pttl = time.Duration(ttl) * time.Second
|
||||
ca.nttl = time.Duration(ttl) * time.Second
|
||||
args = args[1:]
|
||||
}
|
||||
}
|
||||
origins := plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys)
|
||||
|
||||
// Refinements? In an extra block.
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
// first number is cap, second is an new ttl
|
||||
case Success:
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
pcap, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.pcap = pcap
|
||||
if len(args) > 1 {
|
||||
pttl, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reserve 0 (and smaller for future things)
|
||||
if pttl <= 0 {
|
||||
return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", pttl)
|
||||
}
|
||||
ca.pttl = time.Duration(pttl) * time.Second
|
||||
if len(args) > 2 {
|
||||
minpttl, err := strconv.Atoi(args[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reserve < 0
|
||||
if minpttl < 0 {
|
||||
return nil, fmt.Errorf("cache min TTL can not be negative: %d", minpttl)
|
||||
}
|
||||
ca.minpttl = time.Duration(minpttl) * time.Second
|
||||
}
|
||||
}
|
||||
case Denial:
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ncap, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.ncap = ncap
|
||||
if len(args) > 1 {
|
||||
nttl, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reserve 0 (and smaller for future things)
|
||||
if nttl <= 0 {
|
||||
return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", nttl)
|
||||
}
|
||||
ca.nttl = time.Duration(nttl) * time.Second
|
||||
if len(args) > 2 {
|
||||
minnttl, err := strconv.Atoi(args[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reserve < 0
|
||||
if minnttl < 0 {
|
||||
return nil, fmt.Errorf("cache min TTL can not be negative: %d", minnttl)
|
||||
}
|
||||
ca.minnttl = time.Duration(minnttl) * time.Second
|
||||
}
|
||||
}
|
||||
case "prefetch":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 || len(args) > 3 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
amount, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if amount < 0 {
|
||||
return nil, fmt.Errorf("prefetch amount should be positive: %d", amount)
|
||||
}
|
||||
ca.prefetch = amount
|
||||
|
||||
if len(args) > 1 {
|
||||
dur, err := time.ParseDuration(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.duration = dur
|
||||
}
|
||||
if len(args) > 2 {
|
||||
pct := args[2]
|
||||
if x := pct[len(pct)-1]; x != '%' {
|
||||
return nil, fmt.Errorf("last character of percentage should be `%%`, but is: %q", x)
|
||||
}
|
||||
pct = pct[:len(pct)-1]
|
||||
|
||||
num, err := strconv.Atoi(pct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if num < 10 || num > 90 {
|
||||
return nil, fmt.Errorf("percentage should fall in range [10, 90]: %d", num)
|
||||
}
|
||||
ca.percentage = num
|
||||
}
|
||||
|
||||
case "serve_stale":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ca.staleUpTo = 1 * time.Hour
|
||||
if len(args) > 0 {
|
||||
d, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d < 0 {
|
||||
return nil, errors.New("invalid negative duration for serve_stale")
|
||||
}
|
||||
ca.staleUpTo = d
|
||||
}
|
||||
ca.verifyStale = false
|
||||
if len(args) > 1 {
|
||||
mode := strings.ToLower(args[1])
|
||||
if mode != "immediate" && mode != "verify" {
|
||||
return nil, fmt.Errorf("invalid value for serve_stale refresh mode: %s", mode)
|
||||
}
|
||||
ca.verifyStale = mode == "verify"
|
||||
}
|
||||
case "servfail":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
d, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d < 0 {
|
||||
return nil, errors.New("invalid negative ttl for servfail")
|
||||
}
|
||||
if d > 5*time.Minute {
|
||||
// RFC 2308 prohibits caching SERVFAIL longer than 5 minutes
|
||||
return nil, errors.New("caching SERVFAIL responses over 5 minutes is not permitted")
|
||||
}
|
||||
ca.failttl = d
|
||||
case "disable":
|
||||
// disable [success|denial] [zones]...
|
||||
args := c.RemainingArgs()
|
||||
if len(args) < 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
var zones []string
|
||||
if len(args) > 1 {
|
||||
for _, z := range args[1:] { // args[1:] define the list of zones to disable
|
||||
nz := plugin.Name(z).Normalize()
|
||||
if nz == "" {
|
||||
return nil, fmt.Errorf("invalid disabled zone: %s", z)
|
||||
}
|
||||
zones = append(zones, nz)
|
||||
}
|
||||
} else {
|
||||
// if no zones specified, default to root
|
||||
zones = []string{"."}
|
||||
}
|
||||
|
||||
switch args[0] { // args[0] defines which cache to disable
|
||||
case Denial:
|
||||
ca.nexcept = zones
|
||||
case Success:
|
||||
ca.pexcept = zones
|
||||
default:
|
||||
return nil, fmt.Errorf("cache type for disable must be %q or %q", Success, Denial)
|
||||
}
|
||||
case "keepttl":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ca.keepttl = true
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
ca.Zones = origins
|
||||
ca.zonesMetricLabel = strings.Join(origins, ",")
|
||||
ca.pcache = cache.New(ca.pcap)
|
||||
ca.ncache = cache.New(ca.ncap)
|
||||
}
|
||||
|
||||
return ca, nil
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import "context"
|
||||
|
||||
// Done is a non-blocking function that returns true if the context has been canceled.
|
||||
func Done(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package msg
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Path converts a domainname to an etcd path. If s looks like service.staging.skydns.local.,
|
||||
// the resulting key will be /skydns/local/skydns/staging/service .
|
||||
func Path(s, prefix string) string {
|
||||
l := dns.SplitDomainName(s)
|
||||
for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l...)...)
|
||||
}
|
||||
|
||||
// Domain is the opposite of Path.
|
||||
func Domain(s string) string {
|
||||
l := strings.Split(s, "/")
|
||||
if l[len(l)-1] == "" {
|
||||
l = l[:len(l)-1]
|
||||
}
|
||||
// start with 1, to strip /skydns
|
||||
for i, j := 1, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
return dnsutil.Join(l[1 : len(l)-1]...)
|
||||
}
|
||||
|
||||
// PathWithWildcard acts as Path, but if a name contains wildcards (* or any), the name will be
|
||||
// chopped of before the (first) wildcard, and we do a higher level search and
|
||||
// later find the matching names. So service.*.skydns.local, will look for all
|
||||
// services under skydns.local and will later check for names that match
|
||||
// service.*.skydns.local. If a wildcard is found the returned bool is true.
|
||||
func PathWithWildcard(s, prefix string) (string, bool) {
|
||||
l := dns.SplitDomainName(s)
|
||||
for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
for i, k := range l {
|
||||
if k == "*" || k == "any" {
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l[:i]...)...), true
|
||||
}
|
||||
}
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l...)...), false
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
// Package msg defines the Service structure which is used for service discovery.
|
||||
package msg
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Service defines a discoverable service in etcd. It is the rdata from a SRV
|
||||
// record, but with a twist. Host (Target in SRV) must be a domain name, but
|
||||
// if it looks like an IP address (4/6), we will treat it like an IP address.
|
||||
type Service struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference.
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
|
||||
// When a SRV record with a "Host: IP-address" is added, we synthesize
|
||||
// a srv.Target domain name. Normally we convert the full Key where
|
||||
// the record lives to a DNS name and use this as the srv.Target. When
|
||||
// TargetStrip > 0 we strip the left most TargetStrip labels from the
|
||||
// DNS name.
|
||||
TargetStrip int `json:"targetstrip,omitempty"`
|
||||
|
||||
// Group is used to group (or *not* to group) different services
|
||||
// together. Services with an identical Group are returned in the same
|
||||
// answer.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// Etcd key where we found this service and ignored from json un-/marshalling
|
||||
Key string `json:"-"`
|
||||
}
|
||||
|
||||
// NewSRV returns a new SRV record based on the Service.
|
||||
func (s *Service) NewSRV(name string, weight uint16) *dns.SRV {
|
||||
host := dns.Fqdn(s.Host)
|
||||
if s.TargetStrip > 0 {
|
||||
host = targetStrip(host, s.TargetStrip)
|
||||
}
|
||||
|
||||
return &dns.SRV{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: s.TTL},
|
||||
Priority: uint16(s.Priority), Weight: weight, Port: uint16(s.Port), Target: host}
|
||||
}
|
||||
|
||||
// NewMX returns a new MX record based on the Service.
|
||||
func (s *Service) NewMX(name string) *dns.MX {
|
||||
host := dns.Fqdn(s.Host)
|
||||
if s.TargetStrip > 0 {
|
||||
host = targetStrip(host, s.TargetStrip)
|
||||
}
|
||||
|
||||
return &dns.MX{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: s.TTL},
|
||||
Preference: uint16(s.Priority), Mx: host}
|
||||
}
|
||||
|
||||
// NewA returns a new A record based on the Service.
|
||||
func (s *Service) NewA(name string, ip net.IP) *dns.A {
|
||||
return &dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.TTL}, A: ip}
|
||||
}
|
||||
|
||||
// NewAAAA returns a new AAAA record based on the Service.
|
||||
func (s *Service) NewAAAA(name string, ip net.IP) *dns.AAAA {
|
||||
return &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: s.TTL}, AAAA: ip}
|
||||
}
|
||||
|
||||
// NewCNAME returns a new CNAME record based on the Service.
|
||||
func (s *Service) NewCNAME(name string, target string) *dns.CNAME {
|
||||
return &dns.CNAME{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: s.TTL}, Target: dns.Fqdn(target)}
|
||||
}
|
||||
|
||||
// NewTXT returns a new TXT record based on the Service.
|
||||
func (s *Service) NewTXT(name string) *dns.TXT {
|
||||
return &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: s.TTL}, Txt: split255(s.Text)}
|
||||
}
|
||||
|
||||
// NewPTR returns a new PTR record based on the Service.
|
||||
func (s *Service) NewPTR(name string, target string) *dns.PTR {
|
||||
return &dns.PTR{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: s.TTL}, Ptr: dns.Fqdn(target)}
|
||||
}
|
||||
|
||||
// NewNS returns a new NS record based on the Service.
|
||||
func (s *Service) NewNS(name string) *dns.NS {
|
||||
host := dns.Fqdn(s.Host)
|
||||
if s.TargetStrip > 0 {
|
||||
host = targetStrip(host, s.TargetStrip)
|
||||
}
|
||||
return &dns.NS{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: s.TTL}, Ns: host}
|
||||
}
|
||||
|
||||
// Group checks the services in sx, it looks for a Group attribute on the shortest
|
||||
// keys. If there are multiple shortest keys *and* the group attribute disagrees (and
|
||||
// is not empty), we don't consider it a group.
|
||||
// If a group is found, only services with *that* group (or no group) will be returned.
|
||||
func Group(sx []Service) []Service {
|
||||
if len(sx) == 0 {
|
||||
return sx
|
||||
}
|
||||
|
||||
// Shortest key with group attribute sets the group for this set.
|
||||
group := sx[0].Group
|
||||
slashes := strings.Count(sx[0].Key, "/")
|
||||
length := make([]int, len(sx))
|
||||
for i, s := range sx {
|
||||
x := strings.Count(s.Key, "/")
|
||||
length[i] = x
|
||||
if x < slashes {
|
||||
if s.Group == "" {
|
||||
break
|
||||
}
|
||||
slashes = x
|
||||
group = s.Group
|
||||
}
|
||||
}
|
||||
|
||||
if group == "" {
|
||||
return sx
|
||||
}
|
||||
|
||||
ret := []Service{} // with slice-tricks in sx we can prolly save this allocation (TODO)
|
||||
|
||||
for i, s := range sx {
|
||||
if s.Group == "" {
|
||||
ret = append(ret, s)
|
||||
continue
|
||||
}
|
||||
|
||||
// Disagreement on the same level
|
||||
if length[i] == slashes && s.Group != group {
|
||||
return sx
|
||||
}
|
||||
|
||||
if s.Group == group {
|
||||
ret = append(ret, s)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Split255 splits a string into 255 byte chunks.
|
||||
func split255(s string) []string {
|
||||
if len(s) < 255 {
|
||||
return []string{s}
|
||||
}
|
||||
sx := []string{}
|
||||
p, i := 0, 255
|
||||
for {
|
||||
if i > len(s) {
|
||||
sx = append(sx, s[p:])
|
||||
break
|
||||
}
|
||||
sx = append(sx, s[p:i])
|
||||
p, i = p+255, i+255
|
||||
}
|
||||
|
||||
return sx
|
||||
}
|
||||
|
||||
// targetStrip strips "targetstrip" labels from the left side of the fully qualified name.
|
||||
func targetStrip(name string, targetStrip int) string {
|
||||
offset, end := 0, false
|
||||
for range targetStrip {
|
||||
offset, end = dns.NextLabel(name, offset)
|
||||
}
|
||||
if end {
|
||||
// We overshot the name, use the original one.
|
||||
offset = 0
|
||||
}
|
||||
name = name[offset:]
|
||||
return name
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package msg
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// HostType returns the DNS type of what is encoded in the Service Host field. We're reusing
|
||||
// dns.TypeXXX to not reinvent a new set of identifiers.
|
||||
//
|
||||
// dns.TypeA: the service's Host field contains an A record.
|
||||
// dns.TypeAAAA: the service's Host field contains an AAAA record.
|
||||
// dns.TypeCNAME: the service's Host field contains a name.
|
||||
//
|
||||
// Note that a service can double/triple as a TXT record or MX record.
|
||||
func (s *Service) HostType() (what uint16, normalized net.IP) {
|
||||
ip := net.ParseIP(s.Host)
|
||||
|
||||
switch {
|
||||
case ip == nil:
|
||||
if len(s.Text) == 0 {
|
||||
return dns.TypeCNAME, nil
|
||||
}
|
||||
return dns.TypeTXT, nil
|
||||
|
||||
case ip.To4() != nil:
|
||||
return dns.TypeA, ip.To4()
|
||||
|
||||
case ip.To4() == nil:
|
||||
return dns.TypeAAAA, ip.To16()
|
||||
}
|
||||
// This should never be reached.
|
||||
return dns.TypeNone, nil
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# metadata
|
||||
|
||||
## Name
|
||||
|
||||
*metadata* - enables a metadata collector.
|
||||
|
||||
## Description
|
||||
|
||||
By enabling *metadata* any plugin that implements [metadata.Provider
|
||||
interface](https://godoc.org/github.com/coredns/coredns/plugin/metadata#Provider) will be called for
|
||||
each DNS query, at the beginning of the process for that query, in order to add its own metadata to
|
||||
context.
|
||||
|
||||
The metadata collected will be available for all plugins, via the Context parameter provided in the
|
||||
ServeDNS function. The package (code) documentation has examples on how to inspect and retrieve
|
||||
metadata a plugin might be interested in.
|
||||
|
||||
The metadata is added by setting a label with a value in the context. These labels should be named
|
||||
`plugin/NAME`, where **NAME** is something descriptive. The only hard requirement the *metadata*
|
||||
plugin enforces is that the labels contain a slash. See the documentation for
|
||||
`metadata.SetValueFunc`.
|
||||
|
||||
The value stored is a string. The empty string signals "no metadata". See the documentation for
|
||||
`metadata.ValueFunc` on how to retrieve this.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
metadata [ZONES... ]
|
||||
~~~
|
||||
|
||||
* **ZONES** zones metadata should be invoked for.
|
||||
|
||||
## Plugins
|
||||
|
||||
`metadata.Provider` interface needs to be implemented by each plugin willing to provide metadata
|
||||
information for other plugins. It will be called by metadata and gather the information from all
|
||||
plugins in context.
|
||||
|
||||
Note: this method should work quickly, because it is called for every request.
|
||||
|
||||
## Examples
|
||||
|
||||
The *rewrite* plugin uses meta data to rewrite requests.
|
||||
|
||||
## See Also
|
||||
|
||||
The [Provider interface](https://godoc.org/github.com/coredns/coredns/plugin/metadata#Provider) and
|
||||
the [package level](https://godoc.org/github.com/coredns/coredns/plugin/metadata) documentation.
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Metadata implements collecting metadata information from all plugins that
|
||||
// implement the Provider interface.
|
||||
type Metadata struct {
|
||||
Zones []string
|
||||
Providers []Provider
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (m *Metadata) Name() string { return "metadata" }
|
||||
|
||||
// ContextWithMetadata is exported for use by provider tests
|
||||
func ContextWithMetadata(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, key{}, md{})
|
||||
}
|
||||
|
||||
// ServeDNS implements the plugin.Handler interface.
|
||||
func (m *Metadata) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r)
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
// Collect will retrieve metadata functions from each metadata provider and update the context
|
||||
func (m *Metadata) Collect(ctx context.Context, state request.Request) context.Context {
|
||||
ctx = ContextWithMetadata(ctx)
|
||||
if plugin.Zones(m.Zones).Matches(state.Name()) != "" {
|
||||
// Go through all Providers and collect metadata.
|
||||
for _, p := range m.Providers {
|
||||
ctx = p.Metadata(ctx, state)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
// Package metadata provides an API that allows plugins to add metadata to the context.
|
||||
// Each metadata is stored under a label that has the form <plugin>/<name>. Each metadata
|
||||
// is returned as a Func. When Func is called the metadata is returned. If Func is expensive to
|
||||
// execute it is its responsibility to provide some form of caching. During the handling of a
|
||||
// query it is expected the metadata stays constant.
|
||||
//
|
||||
// Basic example:
|
||||
//
|
||||
// Implement the Provider interface for a plugin p:
|
||||
//
|
||||
// func (p P) Metadata(ctx context.Context, state request.Request) context.Context {
|
||||
// metadata.SetValueFunc(ctx, "test/something", func() string { return "myvalue" })
|
||||
// return ctx
|
||||
// }
|
||||
//
|
||||
// Basic example with caching:
|
||||
//
|
||||
// func (p P) Metadata(ctx context.Context, state request.Request) context.Context {
|
||||
// cached := ""
|
||||
// f := func() string {
|
||||
// if cached != "" {
|
||||
// return cached
|
||||
// }
|
||||
// cached = expensiveFunc()
|
||||
// return cached
|
||||
// }
|
||||
// metadata.SetValueFunc(ctx, "test/something", f)
|
||||
// return ctx
|
||||
// }
|
||||
//
|
||||
// If you need access to this metadata from another plugin:
|
||||
//
|
||||
// // ...
|
||||
// valueFunc := metadata.ValueFunc(ctx, "test/something")
|
||||
// value := valueFunc()
|
||||
// // use 'value'
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
)
|
||||
|
||||
// Provider interface needs to be implemented by each plugin willing to provide
|
||||
// metadata information for other plugins.
|
||||
type Provider interface {
|
||||
// Metadata adds metadata to the context and returns a (potentially) new context.
|
||||
// Note: this method should work quickly, because it is called for every request
|
||||
// from the metadata plugin.
|
||||
Metadata(ctx context.Context, state request.Request) context.Context
|
||||
}
|
||||
|
||||
// Func is the type of function in the metadata, when called they return the value of the label.
|
||||
type Func func() string
|
||||
|
||||
// IsLabel checks that the provided name is a valid label name, i.e. two or more words separated by a slash.
|
||||
func IsLabel(label string) bool {
|
||||
p := strings.Index(label, "/")
|
||||
if p <= 0 || p >= len(label)-1 {
|
||||
// cannot accept namespace empty nor label empty
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Labels returns all metadata keys stored in the context. These label names should be named
|
||||
// as: plugin/NAME, where NAME is something descriptive.
|
||||
func Labels(ctx context.Context) []string {
|
||||
if metadata := ctx.Value(key{}); metadata != nil {
|
||||
if m, ok := metadata.(md); ok {
|
||||
return keys(m)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValueFuncs returns the map[string]Func from the context, or nil if it does not exist.
|
||||
func ValueFuncs(ctx context.Context) map[string]Func {
|
||||
if metadata := ctx.Value(key{}); metadata != nil {
|
||||
if m, ok := metadata.(md); ok {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValueFunc returns the value function of label. If none can be found nil is returned. Calling the
|
||||
// function returns the value of the label.
|
||||
func ValueFunc(ctx context.Context, label string) Func {
|
||||
if metadata := ctx.Value(key{}); metadata != nil {
|
||||
if m, ok := metadata.(md); ok {
|
||||
return m[label]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetValueFunc set the metadata label to the value function. If no metadata can be found this is a noop and
|
||||
// false is returned. Any existing value is overwritten.
|
||||
func SetValueFunc(ctx context.Context, label string, f Func) bool {
|
||||
if metadata := ctx.Value(key{}); metadata != nil {
|
||||
if m, ok := metadata.(md); ok {
|
||||
m[label] = f
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// md is metadata information storage.
|
||||
type md map[string]Func
|
||||
|
||||
// key defines the type of key that is used to save metadata into the context.
|
||||
type key struct{}
|
||||
|
||||
func keys(m map[string]Func) []string {
|
||||
s := make([]string, len(m))
|
||||
i := 0
|
||||
for k := range m {
|
||||
s[i] = k
|
||||
i++
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
)
|
||||
|
||||
func init() { plugin.Register("metadata", setup) }
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
m, err := metadataParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
m.Next = next
|
||||
return m
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
plugins := dnsserver.GetConfig(c).Handlers()
|
||||
for _, p := range plugins {
|
||||
if met, ok := p.(Provider); ok {
|
||||
m.Providers = append(m.Providers, met)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func metadataParse(c *caddy.Controller) (*Metadata, error) {
|
||||
m := &Metadata{}
|
||||
c.Next()
|
||||
|
||||
m.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys)
|
||||
|
||||
if c.NextBlock() || c.Next() {
|
||||
return nil, plugin.Error("metadata", c.ArgErr())
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
# prometheus
|
||||
|
||||
## Name
|
||||
|
||||
*prometheus* - enables [Prometheus](https://prometheus.io/) metrics.
|
||||
|
||||
## Description
|
||||
|
||||
With *prometheus* you export metrics from CoreDNS and any plugin that has them.
|
||||
The default location for the metrics is `localhost:9153`. The metrics path is fixed to `/metrics`.
|
||||
|
||||
In addition to the default Go metrics exported by the [Prometheus Go client](https://prometheus.io/docs/guides/go-application/),
|
||||
the following metrics are exported:
|
||||
|
||||
* `coredns_build_info{version, revision, goversion}` - info about CoreDNS itself.
|
||||
* `coredns_panics_total{}` - total number of panics.
|
||||
* `coredns_dns_requests_total{server, zone, view, proto, family, type}` - total query count.
|
||||
* `coredns_dns_request_duration_seconds{server, zone, view, type}` - duration to process each query.
|
||||
* `coredns_dns_request_size_bytes{server, zone, view, proto}` - size of the request in bytes. Uses the original size before any plugin rewrites.
|
||||
* `coredns_dns_do_requests_total{server, view, zone}` - queries that have the DO bit set
|
||||
* `coredns_dns_response_size_bytes{server, zone, view, proto}` - response size in bytes.
|
||||
* `coredns_dns_responses_total{server, zone, view, rcode, plugin}` - response per zone, rcode and plugin.
|
||||
* `coredns_dns_https_responses_total{server, status}` - responses per server and http status code.
|
||||
* `coredns_dns_quic_responses_total{server, status}` - responses per server and QUIC application code.
|
||||
* `coredns_plugin_enabled{server, zone, view, name}` - indicates whether a plugin is enabled on per server, zone and view basis.
|
||||
|
||||
Almost each counter has a label `zone` which is the zonename used for the request/response.
|
||||
|
||||
Extra labels used are:
|
||||
|
||||
* `server` is identifying the server responsible for the request. This is a string formatted
|
||||
as the server's listening address: `<scheme>://[<bind>]:<port>`. I.e. for a "normal" DNS server
|
||||
this is `dns://:53`. If you are using the *bind* plugin an IP address is included, e.g.: `dns://127.0.0.53:53`.
|
||||
* `proto` which holds the transport of the response ("udp" or "tcp")
|
||||
* The address family (`family`) of the transport (1 = IP (IP version 4), 2 = IP6 (IP version 6)).
|
||||
* `type` which holds the query type. It holds most common types (A, AAAA, MX, SOA, CNAME, PTR, TXT,
|
||||
NS, SRV, DS, DNSKEY, RRSIG, NSEC, NSEC3, HTTPS, IXFR, AXFR and ANY) and "other" which lumps together all
|
||||
other types.
|
||||
* `status` which holds the https status code. Possible values are:
|
||||
* 200 - request is processed,
|
||||
* 404 - request has been rejected on validation,
|
||||
* 400 - request to dns message conversion failed,
|
||||
* 500 - processing ended up with no response.
|
||||
* the `plugin` label holds the name of the plugin that made the write to the client. If the server
|
||||
did the write (on error for instance), the value is empty.
|
||||
|
||||
If monitoring is enabled, queries that do not enter the plugin chain are exported under the fake
|
||||
name "dropped" (without a closing dot - this is never a valid domain name).
|
||||
|
||||
Other plugins may export additional stats when the _prometheus_ plugin is enabled. Those stats are documented in each
|
||||
plugin's README.
|
||||
|
||||
This plugin can only be used once per Server Block.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
prometheus [ADDRESS]
|
||||
~~~
|
||||
|
||||
For each zone that you want to see metrics for.
|
||||
|
||||
It optionally takes a bind address to which the metrics are exported; the default
|
||||
listens on `localhost:9153`. The metrics path is fixed to `/metrics`.
|
||||
|
||||
## Examples
|
||||
|
||||
Use an alternative listening address:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
prometheus localhost:9253
|
||||
}
|
||||
~~~
|
||||
|
||||
Or via an environment variable (this is supported throughout the Corefile): `export PORT=9253`, and
|
||||
then:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
prometheus localhost:{$PORT}
|
||||
}
|
||||
~~~
|
||||
|
||||
## Bugs
|
||||
|
||||
When reloading, the Prometheus handler is stopped before the new server instance is started.
|
||||
If that new server fails to start, then the initial server instance is still available and DNS queries still served,
|
||||
but Prometheus handler stays down.
|
||||
Prometheus will not reply HTTP request until a successful reload or a complete restart of CoreDNS.
|
||||
Only the plugins that register as Handler are visible in `coredns_plugin_enabled{server, zone, name}`. As of today the plugins reload and bind will not be reported.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
)
|
||||
|
||||
// WithServer returns the current server handling the request. It returns the
|
||||
// server listening address: <scheme>://[<bind>]:<port> Normally this is
|
||||
// something like "dns://:53", but if the bind plugin is used, i.e. "bind
|
||||
// 127.0.0.53", it will be "dns://127.0.0.53:53", etc. If not address is found
|
||||
// the empty string is returned.
|
||||
//
|
||||
// Basic usage with a metric:
|
||||
//
|
||||
// <metric>.WithLabelValues(metrics.WithServer(ctx), labels..).Add(1)
|
||||
func WithServer(ctx context.Context) string {
|
||||
srv := ctx.Value(dnsserver.Key{})
|
||||
if srv == nil {
|
||||
return ""
|
||||
}
|
||||
return srv.(*dnsserver.Server).Addr
|
||||
}
|
||||
|
||||
// WithView returns the name of the view currently handling the request, if a view is defined.
|
||||
//
|
||||
// Basic usage with a metric:
|
||||
//
|
||||
// <metric>.WithLabelValues(metrics.WithView(ctx), labels..).Add(1)
|
||||
func WithView(ctx context.Context) string {
|
||||
v := ctx.Value(dnsserver.ViewKey{})
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return v.(string)
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ServeDNS implements the Handler interface.
|
||||
func (m *Metrics) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
// Capture the original request size before any plugins modify it
|
||||
originalSize := r.Len()
|
||||
|
||||
qname := state.QName()
|
||||
zone := plugin.Zones(m.ZoneNames()).Matches(qname)
|
||||
if zone == "" {
|
||||
zone = "."
|
||||
}
|
||||
|
||||
// Record response to get status code and size of the reply.
|
||||
rw := NewRecorder(w)
|
||||
status, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, rw, r)
|
||||
|
||||
rc := rw.Rcode
|
||||
if !plugin.ClientWrite(status) {
|
||||
// when no response was written, fallback to status returned from next plugin as this status
|
||||
// is actually used as rcode of DNS response
|
||||
// see https://github.com/coredns/coredns/blob/master/core/dnsserver/server.go#L318
|
||||
rc = status
|
||||
}
|
||||
plugin := m.authoritativePlugin(rw.Caller)
|
||||
// Pass the original request size to vars.Report
|
||||
vars.Report(WithServer(ctx), state, zone, WithView(ctx), rcode.ToString(rc), plugin,
|
||||
rw.Len, rw.Start, vars.WithOriginalReqSize(originalSize))
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// Name implements the Handler interface.
|
||||
func (m *Metrics) Name() string { return "prometheus" }
|
||||
|
||||
// authoritativePlugin returns which of made the write, if none is found the empty string is returned.
|
||||
func (m *Metrics) authoritativePlugin(caller [3]string) string {
|
||||
// a b and c contain the full path of the caller, the plugin name 2nd last elements
|
||||
// .../coredns/plugin/whoami/whoami.go --> whoami
|
||||
// this is likely FS specific, so use filepath.
|
||||
for _, c := range caller {
|
||||
plug := filepath.Base(filepath.Dir(c))
|
||||
if _, ok := m.plugins[plug]; ok {
|
||||
return plug
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
// Package metrics implement a handler and plugin that provides Prometheus metrics.
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Metrics holds the prometheus configuration. The metrics' path is fixed to be /metrics .
|
||||
type Metrics struct {
|
||||
Next plugin.Handler
|
||||
Addr string
|
||||
Reg *prometheus.Registry
|
||||
|
||||
ln net.Listener
|
||||
lnSetup bool
|
||||
|
||||
mux *http.ServeMux
|
||||
srv *http.Server
|
||||
|
||||
zoneNames []string
|
||||
zoneMap map[string]struct{}
|
||||
zoneMu sync.RWMutex
|
||||
|
||||
plugins map[string]struct{} // all available plugins, used to determine which plugin made the client write
|
||||
}
|
||||
|
||||
// New returns a new instance of Metrics with the given address.
|
||||
func New(addr string) *Metrics {
|
||||
met := &Metrics{
|
||||
Addr: addr,
|
||||
Reg: prometheus.DefaultRegisterer.(*prometheus.Registry),
|
||||
zoneMap: make(map[string]struct{}),
|
||||
plugins: pluginList(caddy.ListPlugins()),
|
||||
}
|
||||
|
||||
return met
|
||||
}
|
||||
|
||||
// MustRegister wraps m.Reg.MustRegister.
|
||||
func (m *Metrics) MustRegister(c prometheus.Collector) {
|
||||
err := m.Reg.Register(c)
|
||||
if err != nil {
|
||||
// ignore any duplicate error, but fatal on any other kind of error
|
||||
if _, ok := err.(prometheus.AlreadyRegisteredError); !ok {
|
||||
log.Fatalf("Cannot register metrics collector: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddZone adds zone z to m.
|
||||
func (m *Metrics) AddZone(z string) {
|
||||
m.zoneMu.Lock()
|
||||
m.zoneMap[z] = struct{}{}
|
||||
m.zoneNames = keys(m.zoneMap)
|
||||
m.zoneMu.Unlock()
|
||||
}
|
||||
|
||||
// RemoveZone remove zone z from m.
|
||||
func (m *Metrics) RemoveZone(z string) {
|
||||
m.zoneMu.Lock()
|
||||
delete(m.zoneMap, z)
|
||||
m.zoneNames = keys(m.zoneMap)
|
||||
m.zoneMu.Unlock()
|
||||
}
|
||||
|
||||
// ZoneNames returns the zones of m.
|
||||
func (m *Metrics) ZoneNames() []string {
|
||||
m.zoneMu.RLock()
|
||||
s := m.zoneNames
|
||||
m.zoneMu.RUnlock()
|
||||
return s
|
||||
}
|
||||
|
||||
// OnStartup sets up the metrics on startup.
|
||||
func (m *Metrics) OnStartup() error {
|
||||
ln, err := reuseport.Listen("tcp", m.Addr)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to start metrics handler: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m.ln = ln
|
||||
m.lnSetup = true
|
||||
|
||||
m.mux = http.NewServeMux()
|
||||
m.mux.Handle("/metrics", promhttp.HandlerFor(m.Reg, promhttp.HandlerOpts{}))
|
||||
|
||||
// creating some helper variables to avoid data races on m.srv and m.ln
|
||||
server := &http.Server{Handler: m.mux}
|
||||
m.srv = server
|
||||
|
||||
go func() {
|
||||
server.Serve(ln)
|
||||
}()
|
||||
|
||||
ListenAddr = ln.Addr().String() // For tests.
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnRestart stops the listener on reload.
|
||||
func (m *Metrics) OnRestart() error {
|
||||
if !m.lnSetup {
|
||||
return nil
|
||||
}
|
||||
u.Unset(m.Addr)
|
||||
return m.stopServer()
|
||||
}
|
||||
|
||||
func (m *Metrics) stopServer() error {
|
||||
if !m.lnSetup {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
defer cancel()
|
||||
if err := m.srv.Shutdown(ctx); err != nil {
|
||||
log.Infof("Failed to stop prometheus http server: %s", err)
|
||||
return err
|
||||
}
|
||||
m.lnSetup = false
|
||||
m.ln.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnFinalShutdown tears down the metrics listener on shutdown and restart.
|
||||
func (m *Metrics) OnFinalShutdown() error { return m.stopServer() }
|
||||
|
||||
func keys(m map[string]struct{}) []string {
|
||||
sx := []string{}
|
||||
for k := range m {
|
||||
sx = append(sx, k)
|
||||
}
|
||||
return sx
|
||||
}
|
||||
|
||||
// pluginList iterates over the returned plugin map from caddy and removes the "dns." prefix from them.
|
||||
func pluginList(m map[string][]string) map[string]struct{} {
|
||||
pm := map[string]struct{}{}
|
||||
for _, p := range m["others"] {
|
||||
// only add 'dns.' plugins
|
||||
if len(p) > 3 {
|
||||
pm[p[4:]] = struct{}{}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return pm
|
||||
}
|
||||
|
||||
// ListenAddr is assigned the address of the prometheus listener. Its use is mainly in tests where
|
||||
// we listen on "localhost:0" and need to retrieve the actual address.
|
||||
var ListenAddr string
|
||||
|
||||
// shutdownTimeout is the maximum amount of time the metrics plugin will wait
|
||||
// before erroring when it tries to close the metrics server
|
||||
const shutdownTimeout time.Duration = time.Second * 5
|
||||
|
||||
var buildInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Name: "build_info",
|
||||
Help: "A metric with a constant '1' value labeled by version, revision, and goversion from which CoreDNS was built.",
|
||||
}, []string{"version", "revision", "goversion"})
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Recorder is a dnstest.Recorder specific to the metrics plugin.
|
||||
type Recorder struct {
|
||||
*dnstest.Recorder
|
||||
// CallerN holds the string return value of the call to runtime.Caller(N+1)
|
||||
Caller [3]string
|
||||
}
|
||||
|
||||
// NewRecorder makes and returns a new Recorder.
|
||||
func NewRecorder(w dns.ResponseWriter) *Recorder { return &Recorder{Recorder: dnstest.NewRecorder(w)} }
|
||||
|
||||
// WriteMsg records the status code and calls the
|
||||
// underlying ResponseWriter's WriteMsg method.
|
||||
func (r *Recorder) WriteMsg(res *dns.Msg) error {
|
||||
_, r.Caller[0], _, _ = runtime.Caller(1)
|
||||
_, r.Caller[1], _, _ = runtime.Caller(2)
|
||||
_, r.Caller[2], _, _ = runtime.Caller(3)
|
||||
return r.Recorder.WriteMsg(res)
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type reg struct {
|
||||
sync.RWMutex
|
||||
r map[string]*prometheus.Registry
|
||||
}
|
||||
|
||||
func newReg() *reg { return ®{r: make(map[string]*prometheus.Registry)} }
|
||||
|
||||
// update sets the registry if not already there and returns the input. Or it returns
|
||||
// a previous set value.
|
||||
func (r *reg) getOrSet(addr string, pr *prometheus.Registry) *prometheus.Registry {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if v, ok := r.r[addr]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
r.r[addr] = pr
|
||||
return pr
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/coremain"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/uniq"
|
||||
)
|
||||
|
||||
var (
|
||||
log = clog.NewWithPlugin("prometheus")
|
||||
u = uniq.New()
|
||||
registry = newReg()
|
||||
)
|
||||
|
||||
func init() { plugin.Register("prometheus", setup) }
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
m, err := parse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("prometheus", err)
|
||||
}
|
||||
m.Reg = registry.getOrSet(m.Addr, m.Reg)
|
||||
|
||||
c.OnStartup(func() error { m.Reg = registry.getOrSet(m.Addr, m.Reg); u.Set(m.Addr, m.OnStartup); return nil })
|
||||
c.OnRestartFailed(func() error { m.Reg = registry.getOrSet(m.Addr, m.Reg); u.Set(m.Addr, m.OnStartup); return nil })
|
||||
|
||||
c.OnStartup(func() error { return u.ForEach() })
|
||||
c.OnRestartFailed(func() error { return u.ForEach() })
|
||||
|
||||
c.OnStartup(func() error {
|
||||
conf := dnsserver.GetConfig(c)
|
||||
for _, h := range conf.ListenHosts {
|
||||
addrstr := conf.Transport + "://" + net.JoinHostPort(h, conf.Port)
|
||||
for _, p := range conf.Handlers() {
|
||||
vars.PluginEnabled.WithLabelValues(addrstr, conf.Zone, conf.ViewName, p.Name()).Set(1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.OnRestartFailed(func() error {
|
||||
conf := dnsserver.GetConfig(c)
|
||||
for _, h := range conf.ListenHosts {
|
||||
addrstr := conf.Transport + "://" + net.JoinHostPort(h, conf.Port)
|
||||
for _, p := range conf.Handlers() {
|
||||
vars.PluginEnabled.WithLabelValues(addrstr, conf.Zone, conf.ViewName, p.Name()).Set(1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.OnRestart(m.OnRestart)
|
||||
c.OnRestart(func() error { vars.PluginEnabled.Reset(); return nil })
|
||||
c.OnFinalShutdown(m.OnFinalShutdown)
|
||||
|
||||
// Initialize metrics.
|
||||
buildInfo.WithLabelValues(coremain.CoreVersion, coremain.GitCommit, runtime.Version()).Set(1)
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
m.Next = next
|
||||
return m
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (*Metrics, error) {
|
||||
met := New(defaultAddr)
|
||||
|
||||
i := 0
|
||||
for c.Next() {
|
||||
if i > 0 {
|
||||
return nil, plugin.ErrOnce
|
||||
}
|
||||
i++
|
||||
|
||||
zones := plugin.OriginsFromArgsOrServerBlock(nil /* args */, c.ServerBlockKeys)
|
||||
for _, z := range zones {
|
||||
met.AddZone(z)
|
||||
}
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
met.Addr = args[0]
|
||||
_, _, e := net.SplitHostPort(met.Addr)
|
||||
if e != nil {
|
||||
return met, e
|
||||
}
|
||||
default:
|
||||
return met, c.ArgErr()
|
||||
}
|
||||
}
|
||||
return met, nil
|
||||
}
|
||||
|
||||
// defaultAddr is the address the where the metrics are exported by default.
|
||||
const defaultAddr = "localhost:9153"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package vars
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var monitorType = map[uint16]struct{}{
|
||||
dns.TypeAAAA: {},
|
||||
dns.TypeA: {},
|
||||
dns.TypeCNAME: {},
|
||||
dns.TypeDNSKEY: {},
|
||||
dns.TypeDS: {},
|
||||
dns.TypeMX: {},
|
||||
dns.TypeNSEC3: {},
|
||||
dns.TypeNSEC: {},
|
||||
dns.TypeNS: {},
|
||||
dns.TypePTR: {},
|
||||
dns.TypeRRSIG: {},
|
||||
dns.TypeSOA: {},
|
||||
dns.TypeSRV: {},
|
||||
dns.TypeTXT: {},
|
||||
dns.TypeHTTPS: {},
|
||||
// Meta Qtypes
|
||||
dns.TypeIXFR: {},
|
||||
dns.TypeAXFR: {},
|
||||
dns.TypeANY: {},
|
||||
}
|
||||
|
||||
// qTypeString returns the RR type based on monitorType. It returns the text representation
|
||||
// of those types. RR types not in that list will have "other" returned.
|
||||
func qTypeString(qtype uint16) string {
|
||||
if _, known := monitorType[qtype]; known {
|
||||
return dns.Type(qtype).String()
|
||||
}
|
||||
return "other"
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package vars
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
)
|
||||
|
||||
// ReportOptions is a struct that contains available options for the Report function.
|
||||
type ReportOptions struct {
|
||||
OriginalReqSize int
|
||||
}
|
||||
|
||||
// ReportOption defines a function that modifies ReportOptions
|
||||
type ReportOption func(*ReportOptions)
|
||||
|
||||
// WithOriginalReqSize returns an option to set the original request size
|
||||
func WithOriginalReqSize(size int) ReportOption {
|
||||
return func(opts *ReportOptions) {
|
||||
opts.OriginalReqSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// Report reports the metrics data associated with request. This function is exported because it is also
|
||||
// called from core/dnsserver to report requests hitting the server that should not be handled and are thus
|
||||
// not sent down the plugin chain.
|
||||
func Report(server string, req request.Request, zone, view, rcode, plugin string,
|
||||
size int, start time.Time, opts ...ReportOption) {
|
||||
options := ReportOptions{
|
||||
OriginalReqSize: 0,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
// Proto and Family.
|
||||
net := req.Proto()
|
||||
fam := "1"
|
||||
if req.Family() == 2 {
|
||||
fam = "2"
|
||||
}
|
||||
|
||||
if req.Do() {
|
||||
RequestDo.WithLabelValues(server, zone, view).Inc()
|
||||
}
|
||||
|
||||
qType := qTypeString(req.QType())
|
||||
RequestCount.WithLabelValues(server, zone, view, net, fam, qType).Inc()
|
||||
|
||||
RequestDuration.WithLabelValues(server, zone, view).Observe(time.Since(start).Seconds())
|
||||
|
||||
ResponseSize.WithLabelValues(server, zone, view, net).Observe(float64(size))
|
||||
|
||||
reqSize := req.Len()
|
||||
if options.OriginalReqSize > 0 {
|
||||
reqSize = options.OriginalReqSize
|
||||
}
|
||||
|
||||
RequestSize.WithLabelValues(server, zone, view, net).Observe(float64(reqSize))
|
||||
|
||||
ResponseRcode.WithLabelValues(server, zone, view, rcode, plugin).Inc()
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
package vars
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
// Request* and Response* are the prometheus counters and gauges we are using for exporting metrics.
|
||||
var (
|
||||
RequestCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "requests_total",
|
||||
Help: "Counter of DNS requests made per zone, protocol and family.",
|
||||
}, []string{"server", "zone", "view", "proto", "family", "type"})
|
||||
|
||||
RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "request_duration_seconds",
|
||||
Buckets: plugin.TimeBuckets,
|
||||
NativeHistogramBucketFactor: plugin.NativeHistogramBucketFactor,
|
||||
Help: "Histogram of the time (in seconds) each request took per zone.",
|
||||
}, []string{"server", "zone", "view"})
|
||||
|
||||
RequestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "request_size_bytes",
|
||||
Help: "Size of the EDNS0 UDP buffer in bytes (64K for TCP) per zone and protocol.",
|
||||
Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
|
||||
NativeHistogramBucketFactor: plugin.NativeHistogramBucketFactor,
|
||||
}, []string{"server", "zone", "view", "proto"})
|
||||
|
||||
RequestDo = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "do_requests_total",
|
||||
Help: "Counter of DNS requests with DO bit set per zone.",
|
||||
}, []string{"server", "zone", "view"})
|
||||
|
||||
ResponseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "response_size_bytes",
|
||||
Help: "Size of the returned response in bytes.",
|
||||
Buckets: []float64{0, 100, 200, 300, 400, 511, 1023, 2047, 4095, 8291, 16e3, 32e3, 48e3, 64e3},
|
||||
NativeHistogramBucketFactor: plugin.NativeHistogramBucketFactor,
|
||||
}, []string{"server", "zone", "view", "proto"})
|
||||
|
||||
ResponseRcode = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "responses_total",
|
||||
Help: "Counter of response status codes.",
|
||||
}, []string{"server", "zone", "view", "rcode", "plugin"})
|
||||
|
||||
Panic = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Name: "panics_total",
|
||||
Help: "A metrics that counts the number of panics.",
|
||||
})
|
||||
|
||||
PluginEnabled = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Name: "plugin_enabled",
|
||||
Help: "A metric that indicates whether a plugin is enabled on per server and zone basis.",
|
||||
}, []string{"server", "zone", "view", "name"})
|
||||
|
||||
HTTPSResponsesCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "https_responses_total",
|
||||
Help: "Counter of DoH responses per server and http status code.",
|
||||
}, []string{"server", "status"})
|
||||
|
||||
QUICResponsesCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "quic_responses_total",
|
||||
Help: "Counter of DoQ responses per server and QUIC application code.",
|
||||
}, []string{"server", "status"})
|
||||
)
|
||||
|
||||
const (
|
||||
subsystem = "dns"
|
||||
|
||||
// Dropped indicates we dropped the query before any handling. It has no closing dot, so it can not be a valid zone.
|
||||
Dropped = "dropped"
|
||||
)
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/cidr"
|
||||
"github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/parse"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// See core/dnsserver/address.go - we should unify these two impls.
|
||||
|
||||
// Zones represents a lists of zone names.
|
||||
type Zones []string
|
||||
|
||||
// Matches checks if qname is a subdomain of any of the zones in z. The match
|
||||
// will return the most specific zones that matches. The empty string
|
||||
// signals a not found condition.
|
||||
func (z Zones) Matches(qname string) string {
|
||||
zone := ""
|
||||
for _, zname := range z {
|
||||
if dns.IsSubDomain(zname, qname) {
|
||||
// We want the *longest* matching zone, otherwise we may end up in a parent
|
||||
if len(zname) > len(zone) {
|
||||
zone = zname
|
||||
}
|
||||
}
|
||||
}
|
||||
return zone
|
||||
}
|
||||
|
||||
// Normalize fully qualifies all zones in z. The zones in Z must be domain names, without
|
||||
// a port or protocol prefix.
|
||||
func (z Zones) Normalize() {
|
||||
for i := range z {
|
||||
z[i] = Name(z[i]).Normalize()
|
||||
}
|
||||
}
|
||||
|
||||
// Name represents a domain name.
|
||||
type Name string
|
||||
|
||||
// Matches checks to see if other is a subdomain (or the same domain) of n.
|
||||
// This method assures that names can be easily and consistently matched.
|
||||
func (n Name) Matches(child string) bool {
|
||||
if dns.Name(n) == dns.Name(child) {
|
||||
return true
|
||||
}
|
||||
return dns.IsSubDomain(string(n), child)
|
||||
}
|
||||
|
||||
// Normalize lowercases and makes n fully qualified.
|
||||
func (n Name) Normalize() string { return strings.ToLower(dns.Fqdn(string(n))) }
|
||||
|
||||
type (
|
||||
// Host represents a host from the Corefile, may contain port.
|
||||
Host string
|
||||
)
|
||||
|
||||
// Normalize will return the host portion of host, stripping
|
||||
// of any port or transport. The host will also be fully qualified and lowercased.
|
||||
// An empty string is returned on failure
|
||||
// Deprecated: use OriginsFromArgsOrServerBlock or NormalizeExact
|
||||
func (h Host) Normalize() string {
|
||||
var caller string
|
||||
if _, file, line, ok := runtime.Caller(1); ok {
|
||||
caller = fmt.Sprintf("(%v line %d) ", file, line)
|
||||
}
|
||||
log.Warning("An external plugin " + caller + "is using the deprecated function Normalize. " +
|
||||
"This will be removed in a future versions of CoreDNS. The plugin should be updated to use " +
|
||||
"OriginsFromArgsOrServerBlock or NormalizeExact instead.")
|
||||
|
||||
s := string(h)
|
||||
_, s = parse.Transport(s)
|
||||
|
||||
// The error can be ignored here, because this function is called after the corefile has already been vetted.
|
||||
hosts, _, err := SplitHostPort(s)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return Name(hosts[0]).Normalize()
|
||||
}
|
||||
|
||||
// MustNormalize will return the host portion of host, stripping
|
||||
// of any port or transport. The host will also be fully qualified and lowercased.
|
||||
// An error is returned on error
|
||||
// Deprecated: use OriginsFromArgsOrServerBlock or NormalizeExact
|
||||
func (h Host) MustNormalize() (string, error) {
|
||||
var caller string
|
||||
if _, file, line, ok := runtime.Caller(1); ok {
|
||||
caller = fmt.Sprintf("(%v line %d) ", file, line)
|
||||
}
|
||||
log.Warning("An external plugin " + caller + "is using the deprecated function MustNormalize. " +
|
||||
"This will be removed in a future versions of CoreDNS. The plugin should be updated to use " +
|
||||
"OriginsFromArgsOrServerBlock or NormalizeExact instead.")
|
||||
|
||||
s := string(h)
|
||||
_, s = parse.Transport(s)
|
||||
|
||||
// The error can be ignored here, because this function is called after the corefile has already been vetted.
|
||||
hosts, _, err := SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return Name(hosts[0]).Normalize(), nil
|
||||
}
|
||||
|
||||
// NormalizeExact will return the host portion of host, stripping
|
||||
// of any port or transport. The host will also be fully qualified and lowercased.
|
||||
// An empty slice is returned on failure
|
||||
func (h Host) NormalizeExact() []string {
|
||||
// The error can be ignored here, because this function should only be called after the corefile has already been vetted.
|
||||
s := string(h)
|
||||
_, s = parse.Transport(s)
|
||||
|
||||
hosts, _, err := SplitHostPort(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for i := range hosts {
|
||||
hosts[i] = Name(hosts[i]).Normalize()
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
// SplitHostPort splits s up in a host(s) and port portion, taking reverse address notation into account.
|
||||
// String the string s should *not* be prefixed with any protocols, i.e. dns://. SplitHostPort can return
|
||||
// multiple hosts when a reverse notation on a non-octet boundary is given.
|
||||
func SplitHostPort(s string) (hosts []string, port string, err error) {
|
||||
// If there is: :[0-9]+ on the end we assume this is the port. This works for (ascii) domain
|
||||
// names and our reverse syntax, which always needs a /mask *before* the port.
|
||||
// So from the back, find first colon, and then check if it's a number.
|
||||
colon := strings.LastIndex(s, ":")
|
||||
if colon == len(s)-1 {
|
||||
return nil, "", fmt.Errorf("expecting data after last colon: %q", s)
|
||||
}
|
||||
if colon != -1 {
|
||||
if p, err := strconv.Atoi(s[colon+1:]); err == nil {
|
||||
port = strconv.Itoa(p)
|
||||
s = s[:colon]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(miek): this should take escaping into account.
|
||||
if len(s) > 255 {
|
||||
return nil, "", fmt.Errorf("specified zone is too long: %d > 255", len(s))
|
||||
}
|
||||
|
||||
if _, ok := dns.IsDomainName(s); !ok {
|
||||
return nil, "", fmt.Errorf("zone is not a valid domain name: %s", s)
|
||||
}
|
||||
|
||||
// Check if it parses as a reverse zone, if so we use that. Must be fully specified IP and mask.
|
||||
_, n, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return []string{s}, port, nil
|
||||
}
|
||||
|
||||
if s[0] == ':' || (s[0] == '0' && strings.Contains(s, ":")) {
|
||||
return nil, "", fmt.Errorf("invalid CIDR %s", s)
|
||||
}
|
||||
|
||||
// now check if multiple hosts must be returned.
|
||||
nets := cidr.Split(n)
|
||||
hosts = cidr.Reverse(nets)
|
||||
return hosts, port, nil
|
||||
}
|
||||
|
||||
// OriginsFromArgsOrServerBlock returns the normalized args if that slice
|
||||
// is not empty, otherwise the serverblock slice is returned (in a newly copied slice).
|
||||
func OriginsFromArgsOrServerBlock(args, serverblock []string) []string {
|
||||
if len(args) == 0 {
|
||||
s := make([]string, len(serverblock))
|
||||
copy(s, serverblock)
|
||||
for i := range s {
|
||||
s[i] = Host(s[i]).NormalizeExact()[0] // expansion of these already happened in dnsserver/register.go
|
||||
}
|
||||
return s
|
||||
}
|
||||
s := []string{}
|
||||
for i := range args {
|
||||
sx := Host(args[i]).NormalizeExact()
|
||||
if len(sx) == 0 {
|
||||
continue // silently ignores errors.
|
||||
}
|
||||
s = append(s, sx...)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
// Package cache implements a cache. The cache hold 256 shards, each shard
|
||||
// holds a cache: a map with a mutex. There is no fancy expunge algorithm, it
|
||||
// just randomly evicts elements when it gets full.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Hash returns the FNV hash of what.
|
||||
func Hash(what []byte) uint64 {
|
||||
h := fnv.New64()
|
||||
h.Write(what)
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
// Cache is cache.
|
||||
type Cache struct {
|
||||
shards [shardSize]*shard
|
||||
}
|
||||
|
||||
// shard is a cache with random eviction.
|
||||
type shard struct {
|
||||
items map[uint64]interface{}
|
||||
size int
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// New returns a new cache.
|
||||
func New(size int) *Cache {
|
||||
ssize := size / shardSize
|
||||
if ssize < 4 {
|
||||
ssize = 4
|
||||
}
|
||||
|
||||
c := &Cache{}
|
||||
|
||||
// Initialize all the shards
|
||||
for i := range shardSize {
|
||||
c.shards[i] = newShard(ssize)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Add adds a new element to the cache. If the element already exists it is overwritten.
|
||||
// Returns true if an existing element was evicted to make room for this element.
|
||||
func (c *Cache) Add(key uint64, el interface{}) bool {
|
||||
shard := key & (shardSize - 1)
|
||||
return c.shards[shard].Add(key, el)
|
||||
}
|
||||
|
||||
// Get looks up element index under key.
|
||||
func (c *Cache) Get(key uint64) (interface{}, bool) {
|
||||
shard := key & (shardSize - 1)
|
||||
return c.shards[shard].Get(key)
|
||||
}
|
||||
|
||||
// Remove removes the element indexed with key.
|
||||
func (c *Cache) Remove(key uint64) {
|
||||
shard := key & (shardSize - 1)
|
||||
c.shards[shard].Remove(key)
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the cache.
|
||||
func (c *Cache) Len() int {
|
||||
l := 0
|
||||
for _, s := range &c.shards {
|
||||
l += s.Len()
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Walk walks each shard in the cache.
|
||||
func (c *Cache) Walk(f func(map[uint64]interface{}, uint64) bool) {
|
||||
for _, s := range &c.shards {
|
||||
s.Walk(f)
|
||||
}
|
||||
}
|
||||
|
||||
// newShard returns a new shard with size.
|
||||
func newShard(size int) *shard { return &shard{items: make(map[uint64]interface{}), size: size} }
|
||||
|
||||
// Add adds element indexed by key into the cache. Any existing element is overwritten
|
||||
// Returns true if an existing element was evicted to make room for this element.
|
||||
func (s *shard) Add(key uint64, el interface{}) bool {
|
||||
eviction := false
|
||||
s.Lock()
|
||||
if len(s.items) >= s.size {
|
||||
if _, ok := s.items[key]; !ok {
|
||||
for k := range s.items {
|
||||
delete(s.items, k)
|
||||
eviction = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
s.items[key] = el
|
||||
s.Unlock()
|
||||
return eviction
|
||||
}
|
||||
|
||||
// Remove removes the element indexed by key from the cache.
|
||||
func (s *shard) Remove(key uint64) {
|
||||
s.Lock()
|
||||
delete(s.items, key)
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// Evict removes a random element from the cache.
|
||||
func (s *shard) Evict() {
|
||||
s.Lock()
|
||||
for k := range s.items {
|
||||
delete(s.items, k)
|
||||
break
|
||||
}
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
// Get looks up the element indexed under key.
|
||||
func (s *shard) Get(key uint64) (interface{}, bool) {
|
||||
s.RLock()
|
||||
el, found := s.items[key]
|
||||
s.RUnlock()
|
||||
return el, found
|
||||
}
|
||||
|
||||
// Len returns the current length of the cache.
|
||||
func (s *shard) Len() int {
|
||||
s.RLock()
|
||||
l := len(s.items)
|
||||
s.RUnlock()
|
||||
return l
|
||||
}
|
||||
|
||||
// Walk walks the shard for each element the function f is executed while holding a write lock.
|
||||
func (s *shard) Walk(f func(map[uint64]interface{}, uint64) bool) {
|
||||
s.RLock()
|
||||
items := make([]uint64, len(s.items))
|
||||
i := 0
|
||||
for k := range s.items {
|
||||
items[i] = k
|
||||
i++
|
||||
}
|
||||
s.RUnlock()
|
||||
for _, k := range items {
|
||||
s.Lock()
|
||||
ok := f(s.items, k)
|
||||
s.Unlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shardSize = 256
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
// Package cidr contains functions that deal with classless reverse zones in the DNS.
|
||||
package cidr
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/apparentlymart/go-cidr/cidr"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Split returns a slice of non-overlapping subnets that in union equal the subnet n,
|
||||
// and where each subnet falls on a reverse name segment boundary.
|
||||
// for ipv4 this is any multiple of 8 bits (/8, /16, /24 or /32)
|
||||
// for ipv6 this is any multiple of 4 bits
|
||||
func Split(n *net.IPNet) []string {
|
||||
boundary := 8
|
||||
nstr := n.String()
|
||||
if strings.Contains(nstr, ":") {
|
||||
boundary = 4
|
||||
}
|
||||
ones, _ := n.Mask.Size()
|
||||
if ones%boundary == 0 {
|
||||
return []string{n.String()}
|
||||
}
|
||||
|
||||
mask := int(math.Ceil(float64(ones)/float64(boundary))) * boundary
|
||||
networks := nets(n, mask)
|
||||
cidrs := make([]string, len(networks))
|
||||
for i := range networks {
|
||||
cidrs[i] = networks[i].String()
|
||||
}
|
||||
return cidrs
|
||||
}
|
||||
|
||||
// nets return a slice of prefixes with the desired mask subnetted from original network.
|
||||
func nets(network *net.IPNet, newPrefixLen int) []*net.IPNet {
|
||||
prefixLen, _ := network.Mask.Size()
|
||||
maxSubnets := int(math.Exp2(float64(newPrefixLen)) / math.Exp2(float64(prefixLen)))
|
||||
nets := []*net.IPNet{{IP: network.IP, Mask: net.CIDRMask(newPrefixLen, 8*len(network.IP))}}
|
||||
|
||||
for i := 1; i < maxSubnets; i++ {
|
||||
next, exceeds := cidr.NextSubnet(nets[len(nets)-1], newPrefixLen)
|
||||
nets = append(nets, next)
|
||||
if exceeds {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nets
|
||||
}
|
||||
|
||||
// Reverse return the reverse zones that are authoritative for each net in ns.
|
||||
func Reverse(nets []string) []string {
|
||||
rev := make([]string, len(nets))
|
||||
for i := range nets {
|
||||
ip, n, _ := net.ParseCIDR(nets[i])
|
||||
r, err1 := dns.ReverseAddr(ip.String())
|
||||
if err1 != nil {
|
||||
continue
|
||||
}
|
||||
ones, bits := n.Mask.Size()
|
||||
// get the size, in bits, of each portion of hostname defined in the reverse address. (8 for IPv4, 4 for IPv6)
|
||||
sizeDigit := 8
|
||||
if len(n.IP) == net.IPv6len {
|
||||
sizeDigit = 4
|
||||
}
|
||||
// Get the first lower octet boundary to see what encompassing zone we should be authoritative for.
|
||||
mod := (bits - ones) % sizeDigit
|
||||
nearest := (bits - ones) + mod
|
||||
offset := 0
|
||||
var end bool
|
||||
for range nearest / sizeDigit {
|
||||
offset, end = dns.NextLabel(r, offset)
|
||||
if end {
|
||||
break
|
||||
}
|
||||
}
|
||||
rev[i] = r[offset:]
|
||||
}
|
||||
return rev
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue