move upstream project (https://github.com/itsxaos/stub) into subdir
This commit is contained in:
parent
e36782c04b
commit
99a5e07224
11 changed files with 0 additions and 0 deletions
3
caddyapp/.gitignore
vendored
Normal file
3
caddyapp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/caddy
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
22
caddyapp/LICENSE
Normal file
22
caddyapp/LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2023 xaos
|
||||
|
||||
|
||||
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.
|
||||
183
caddyapp/README.md
Normal file
183
caddyapp/README.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
Stub DNS module for Caddy
|
||||
=========================
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> This is alpha-quality, at best. It is not ready for production use. Use at your own risk!
|
||||
|
||||
The easiest way to get a wildcard certificate with Caddy: almost as *magical* as the automatic [HTTP challenge](https://caddyserver.com/docs/automatic-https#http-challenge)!
|
||||
|
||||
This module **does not require any DNS API**; it simply serves the DNS itself.
|
||||
You just need to set up your DNS *once* to delegate all queries for the `_acme-challenge` zone to the IP where Caddy is running.
|
||||
From then on, Caddy can fulfill all DNS challenges itself by opening a temporary DNS server.
|
||||
|
||||
Basically, by redirecting the DNS challenge queries to the host itself, the DNS challenge can be solved just like the [HTTP challenge](https://caddyserver.com/docs/automatic-https#http-challenge).
|
||||
|
||||
Pros:
|
||||
- no DNS API required
|
||||
- no credentials stored on the device
|
||||
- no need to connect to another server to complete the challenge
|
||||
- no propagation delays
|
||||
- no separate DNS server process to manage
|
||||
- server only listens for DNS queries when it has records to serve
|
||||
- can also serve arbitrary DNS records defined in the configuration
|
||||
|
||||
Cons:
|
||||
- requires one additional DNS record in the requested zone
|
||||
- UDP port 53 needs to be exposed & externally accessible (or port 53 on another host forwarded to it)
|
||||
- ACME CA (i.e. Let's Encrypt) needs to connect to your server (like the [HTTP](https://caddyserver.com/docs/automatic-https#http-challenge) & [TLS-ALPN](https://caddyserver.com/docs/automatic-https#tls-alpn-challenge) challenge)
|
||||
- can't have another public DNS server running on the same IP (see [below](#already-running-a-dns-server))
|
||||
- can't have multiple Caddies running on different hosts authenticate for the same domain
|
||||
|
||||
## Limitations & Bugs
|
||||
|
||||
- non-`IN` class records are not supported
|
||||
- no DNSSEC or EDNS
|
||||
- UDP-only
|
||||
- currently, only one DNS server can be defined, and it can only listen on a single address
|
||||
- not optimized
|
||||
- reloading / changing the configuration while attempting to solve the DNS challenge will probably cause it to fail
|
||||
|
||||
Currently, solving the DNS challenge seems to require disabling the propagation checks.
|
||||
This may or may not be a bug with the implementation.
|
||||
In any case, there is no reason to wait & check for propagation, since the server will start listening immediately.
|
||||
|
||||
A potentially serious issue exists with the way records are parsed from the configuration (Caddyfile & JSON): because it is possible to `$INCLUDE` a *file*, it may be possible for an attacker *who has control over the configuration* (and perhaps even a rogue ACME CA, though this is seems unlikely) to get Caddy to read files it shouldn't.
|
||||
Parts of those files could then be exposed in the Caddy logs and, if they happen to look like zone files, get served over DNS.
|
||||
|
||||
## Required DNS Record for the ACME challenge
|
||||
|
||||
To solve the ACME DNS challenge, the DNS zone needs to be set up to direct all DNS queries for the `_acme-challenge` subdomain of the zone you're trying to authenticate to the server that's running Caddy.
|
||||
This should be pretty simple, but you still need to be careful and make sure **only** queries for the `_acme-challenge` subdomain get sent to your server.
|
||||
Otherwise, you would need to configure the DNS app to serve all the records for your zone.
|
||||
|
||||
So, let's say you have this record (could be an `AAAA` record too) wherever you bought your domain/host your DNS:
|
||||
```
|
||||
example.com. A 192.0.2.123
|
||||
```
|
||||
|
||||
Simply create a record like:
|
||||
```
|
||||
_acme-challenge.example.com. NS example.com.
|
||||
```
|
||||
And you're done!
|
||||
|
||||
Since DNS can be a little confusing, here's a quick recap of what this means.
|
||||
|
||||
The first record means "if you want to *connect to* `example.com`, go to `192.0.2.123`".
|
||||
You need a record like this for clients (i.e. a web browser) to connect to your website, and you've probably set this up already.
|
||||
The second record, which you need to add for this module to work, means "if you want to *look up a domain* in `_acme-challenge.example.com.`, then you need to ask the nameserver running at `example.com`".
|
||||
This will cause the client (e.g. Let's Encrypt or another ACME CA) to look up the first record as well, since it now knows it has to *connect to* `example.com` (though not to make an HTTP request like your browser would) to complete the query, and then it will get the `TXT` record for the challenge directly from your server.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
The provider does not require any configuration value, but it might be necessary to configure the server with the (local) IP address and port to serve the DNS on.
|
||||
If no address is configured, the server will default to `:53`.
|
||||
In this case, when the IP is not specified, DNS will be served on *all* of the addresses assigned to the machine, and this may work for you.
|
||||
However, many systems already have a DNS server running for local use: for instance, [systemd-resolved](https://wiki.archlinux.org/title/Systemd-resolved) listens on `127.0.0.53` by default.
|
||||
In this case, it will not be possible to bind to the wildcard address, since it would overlap with systemd-resolved, and the provider will fail with `bind: address already in use`.
|
||||
To avoid this, specify the (externally accessible) IP address you want to use.
|
||||
|
||||
For the port, you'll need to use `53` since that is the DNS port, and that's where Let's Encrypt (or whatever ACME CA you use) will query for the challenge.
|
||||
Still, this isn't hard-coded to allow for more complicated setups and forwarding.
|
||||
|
||||
Note: It is technically possible to specify a protocol before the address (as in `udp/127.0.0.1:53`).
|
||||
Do not do this.
|
||||
Only UDP is supported, specifying other protocols will either cause an error, or worse, get silently ignored.
|
||||
|
||||
### Already running a DNS server?
|
||||
|
||||
If you're already hosting a DNS server on the machine that's running Caddy (and you don't want to make Caddy serve all its records), you'll need to do some additional configuration.
|
||||
|
||||
The issue essentially boils down to not having enough IP addresses to host DNS on (you can create as many subdomains as you like, but they all have to point to an IP address in the end), and it can be resolved in two ways.
|
||||
|
||||
The first and arguably *cleaner* solution is to figure out a way to get your DNS server to forward / recurse queries for the `_acme-challenge` subdomain to some internal address & port and have this module listen on that.
|
||||
Note though that DNS has two kinds of "forwarding": one where the server will tell the client where to go to make their query ("iterative") and one where the server will do it on behalf of the client ("recursive").
|
||||
Obviously, if you use an internal address that the client (e.g. LE) can't reach itself, you'll need to get your server to do the second kind.
|
||||
If you manage to get this to work, please let people know how!
|
||||
|
||||
The second solution is to just use IPv6 since you probably have tons of IPv6 addresses you can use anyway, and Let's Encrypt has supported it for many years.
|
||||
This is (arguably, again) less clean than the first because you'll need to set up another `NS` record and also an `AAAA` record to point it to, but it may be easier if you already have a reasonable setup for IPv6 (e.g. firewall rules).
|
||||
|
||||
## Caddy module names
|
||||
|
||||
App:
|
||||
```
|
||||
dns
|
||||
```
|
||||
|
||||
Provider:
|
||||
```
|
||||
dns.providers.internal
|
||||
```
|
||||
|
||||
## Config examples
|
||||
|
||||
### ACME DNS Provider
|
||||
|
||||
To use this module for the ACME DNS challenge, [configure the ACME issuer in your Caddy JSON](https://caddyserver.com/docs/json/apps/tls/automation/policies/issuer/acme/) like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "acme",
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"provider": {
|
||||
"name": "internal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or with the Caddyfile:
|
||||
|
||||
```
|
||||
# one site
|
||||
tls {
|
||||
dns internal
|
||||
# disable propagation checks, may be necessary
|
||||
propagation_timeout -1
|
||||
}
|
||||
```
|
||||
|
||||
Unlike other providers, global configuration with `acme_dns` does *not* work!
|
||||
|
||||
### DNS Server
|
||||
|
||||
The server can be configured with an address to bind to, and records to serve.
|
||||
Both are optional.
|
||||
|
||||
```json
|
||||
{
|
||||
"apps": {
|
||||
"dns": {
|
||||
"address": "192.0.2.123:53",
|
||||
"records": [
|
||||
"example.com.\t3600\tIN\tA\t192.0.2.123"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or, in the [global options](https://caddyserver.com/docs/caddyfile/options) block at the beginning of the Caddyfile:
|
||||
```
|
||||
{
|
||||
dns 192.0.2.123:53
|
||||
}
|
||||
```
|
||||
|
||||
And to define a record to serve:
|
||||
```
|
||||
{
|
||||
dns 192.0.2.123:53 {
|
||||
record "example.com. A 192.0.2.123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The syntax for defining records is pretty straight-forward, at least for simple record types.
|
||||
It uses the [`miekg/dns.NewRR()`](https://pkg.go.dev/github.com/miekg/dns#NewRR) function to parse the definitions.
|
||||
The linked page has (some) more information: "full [zone file](https://en.wikipedia.org/wiki/Zone_file) syntax" is supported.
|
||||
188
caddyapp/app.go
Normal file
188
caddyapp/app.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
// the address & port on which to serve DNS for the challenge
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
// Statically configured set of records to serve
|
||||
Records []string `json:"records,omitempty"`
|
||||
|
||||
ctx *caddy.Context // set in Provision()
|
||||
logger *zap.Logger // set in Provision()
|
||||
|
||||
requests chan request // set in Provision()
|
||||
shutdown chan struct{} // set in Provision()
|
||||
}
|
||||
|
||||
func (App) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "dns",
|
||||
New: func() caddy.Module { return &App{} },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the module. Implements caddy.Provisioner.
|
||||
func (a *App) Provision(ctx caddy.Context) error {
|
||||
a.logger = ctx.Logger()
|
||||
if a.requests == nil {
|
||||
a.requests = make(chan request)
|
||||
}
|
||||
if a.Records == nil {
|
||||
a.Records = []string{}
|
||||
}
|
||||
if a.shutdown == nil {
|
||||
a.shutdown = make(chan struct{})
|
||||
}
|
||||
if a.Address == "" {
|
||||
a.Address = ":53"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Start() error {
|
||||
parsed, err := caddy.ParseNetworkAddress(a.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed.Network = "udp"
|
||||
a.logger.Debug("starting app", zap.Stringer("address", parsed))
|
||||
srv := Server{
|
||||
Address: parsed,
|
||||
logger: a.logger,
|
||||
shutdown: a.shutdown,
|
||||
ctx: a.ctx,
|
||||
requests: a.requests,
|
||||
Records: make(map[key][]dns.RR),
|
||||
}
|
||||
for _, record_string := range a.Records {
|
||||
record, err := dns.NewRR(record_string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.insert_record(record)
|
||||
}
|
||||
if len(a.Records) > 0 {
|
||||
a.logger.Debug("loaded records", zap.Int("count", len(a.Records)))
|
||||
} else {
|
||||
a.logger.Debug("no records loaded")
|
||||
}
|
||||
|
||||
err = srv.start_stop_server()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go srv.main()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Stop() error {
|
||||
a.logger.Debug("stopping app")
|
||||
close(a.shutdown)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the DNS provider from Caddyfile tokens. Syntax:
|
||||
//
|
||||
// dns [address] {
|
||||
// bind <address>
|
||||
// [record "<record>"]
|
||||
// }
|
||||
func (a *App) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
if d.NextArg() {
|
||||
a.Address = d.Val()
|
||||
_, err := caddy.ParseNetworkAddress(a.Address)
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "bind":
|
||||
if a.Address != "" {
|
||||
return d.Err("Bind address already set")
|
||||
}
|
||||
if d.NextArg() {
|
||||
a.Address = d.Val()
|
||||
_, err := caddy.ParseNetworkAddress(a.Address)
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
case "record":
|
||||
if d.NextArg() {
|
||||
rr, err := dns.NewRR(d.Val())
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
if rr == nil {
|
||||
return d.Err("invalid empty record")
|
||||
}
|
||||
a.Records = append(a.Records, rr.String())
|
||||
} else {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
default:
|
||||
return d.Errf("unrecognized subdirective '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
if a.Address == "" {
|
||||
a.Address = ":53"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseApp configures the "dns" global option from Caddyfile.
|
||||
// Syntax:
|
||||
//
|
||||
// dns [address] {
|
||||
// bind <address>
|
||||
// [record <record>]
|
||||
// }
|
||||
func parseApp(d *caddyfile.Dispenser, prev interface{}) (interface{}, error) {
|
||||
var a App
|
||||
var warnings []caddyconfig.Warning
|
||||
if prev != nil {
|
||||
return nil, fmt.Errorf("multiple DNS servers are not supported!")
|
||||
}
|
||||
|
||||
err := a.UnmarshalCaddyfile(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tell Caddyfile adapter that this is the JSON for an app
|
||||
return httpcaddyfile.App{
|
||||
Name: "dns",
|
||||
Value: caddyconfig.JSON(a, &warnings),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.App = (*App)(nil)
|
||||
_ caddy.Provisioner = (*App)(nil)
|
||||
)
|
||||
137
caddyapp/go.mod
Normal file
137
caddyapp/go.mod
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
module github.com/itsxaos/stub
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.6.4
|
||||
github.com/libdns/libdns v0.2.1
|
||||
github.com/miekg/dns v1.1.50
|
||||
go.uber.org/zap v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.5.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.17.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.13.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/klauspost/compress v1.15.15 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez v1.1.0 // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
|
||||
github.com/quic-go/quic-go v0.32.0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/smallstep/certificates v0.23.2 // indirect
|
||||
github.com/smallstep/nosql v0.5.0 // indirect
|
||||
github.com/smallstep/truststore v0.12.1 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2 // indirect
|
||||
github.com/urfave/cli v1.22.12 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yuin/goldmark v1.5.4 // indirect
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.13.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
go.step.sm/cli-utils v0.7.5 // indirect
|
||||
go.step.sm/crypto v0.23.2 // indirect
|
||||
go.step.sm/linkedca v0.19.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 // indirect
|
||||
google.golang.org/grpc v1.52.3 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
1168
caddyapp/go.sum
Normal file
1168
caddyapp/go.sum
Normal file
File diff suppressed because it is too large
Load diff
249
caddyapp/log.go
Normal file
249
caddyapp/log.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Wrapper for logging (relevant parts of) dns.Msg
|
||||
type LoggableDNSMsg struct{ *dns.Msg }
|
||||
|
||||
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||
func (m LoggableDNSMsg) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||
// adapted version of MsgHdr.String() from github.com/miekg/dns
|
||||
enc.AddUint16("id", m.Id)
|
||||
enc.AddString("opcode", dns.OpcodeToString[m.Opcode])
|
||||
enc.AddString("status", dns.RcodeToString[m.Rcode])
|
||||
|
||||
flag_array := func(arr zapcore.ArrayEncoder) error {
|
||||
if m.Response {
|
||||
arr.AppendString("qr")
|
||||
}
|
||||
if m.Authoritative {
|
||||
arr.AppendString("aa")
|
||||
}
|
||||
if m.Truncated {
|
||||
arr.AppendString("tc")
|
||||
}
|
||||
if m.RecursionDesired {
|
||||
arr.AppendString("rd")
|
||||
}
|
||||
if m.RecursionAvailable {
|
||||
arr.AppendString("ra")
|
||||
}
|
||||
if m.Zero {
|
||||
arr.AppendString("z")
|
||||
}
|
||||
if m.AuthenticatedData {
|
||||
arr.AppendString("ad")
|
||||
}
|
||||
if m.CheckingDisabled {
|
||||
arr.AppendString("cd")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
enc.AddArray("flags", zapcore.ArrayMarshalerFunc(flag_array))
|
||||
|
||||
log_questions(enc, &m.Question)
|
||||
log_answers(enc, &m.Answer)
|
||||
// not logged:
|
||||
// - EDNS0 "OPT pseudosection" from m.IsEdns0()
|
||||
// - "authority section" in m.Ns
|
||||
// - "additional section" in m.Extra
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func log_answers(enc zapcore.ObjectEncoder, answers *[]dns.RR) {
|
||||
if len(*answers) > 0 {
|
||||
array := func(arr zapcore.ArrayEncoder) error {
|
||||
for _, r := range *answers {
|
||||
object := func(obj zapcore.ObjectEncoder) error {
|
||||
log_RR(obj, r)
|
||||
return nil
|
||||
}
|
||||
arr.AppendObject(zapcore.ObjectMarshalerFunc(object))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
enc.AddArray("answer", zapcore.ArrayMarshalerFunc(array))
|
||||
}
|
||||
}
|
||||
|
||||
func log_questions(enc zapcore.ObjectEncoder, questions *[]dns.Question) {
|
||||
if len(*questions) > 0 {
|
||||
array := func(arr zapcore.ArrayEncoder) error {
|
||||
for _, q := range *questions {
|
||||
object := func(obj zapcore.ObjectEncoder) error {
|
||||
obj.AddString("name", q.Name)
|
||||
obj.AddString("class", dns.ClassToString[q.Qclass])
|
||||
obj.AddString("type", dns.TypeToString[q.Qtype])
|
||||
return nil
|
||||
}
|
||||
arr.AppendObject(zapcore.ObjectMarshalerFunc(object))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
enc.AddArray("question", zapcore.ArrayMarshalerFunc(array))
|
||||
}
|
||||
}
|
||||
|
||||
// Only logs the "content"/values of the RR for common types
|
||||
func log_RR(enc zapcore.ObjectEncoder, rr dns.RR) {
|
||||
hdr := rr.Header()
|
||||
enc.AddString("name", hdr.Name)
|
||||
enc.AddString("class", dns.ClassToString[hdr.Class])
|
||||
enc.AddString("type", dns.TypeToString[hdr.Rrtype])
|
||||
enc.AddUint32("TTL", hdr.Ttl)
|
||||
switch r := rr.(type) {
|
||||
case *dns.A:
|
||||
enc.AddString("A", r.A.String())
|
||||
case *dns.AAAA:
|
||||
enc.AddString("AAAA", r.AAAA.String())
|
||||
case *dns.AFSDB:
|
||||
// case *dns.AMTRELAY:
|
||||
case *dns.ANY: // empty
|
||||
case *dns.APL:
|
||||
case *dns.AVC:
|
||||
case *dns.CAA:
|
||||
enc.AddUint8("flag", r.Flag)
|
||||
enc.AddString("tag", r.Tag)
|
||||
enc.AddString("value", r.Value)
|
||||
case *dns.CDNSKEY:
|
||||
case *dns.CDS:
|
||||
case *dns.CERT:
|
||||
case *dns.CNAME:
|
||||
enc.AddString("target", r.Target)
|
||||
case *dns.CSYNC:
|
||||
case *dns.DHCID:
|
||||
case *dns.DLV:
|
||||
case *dns.DNAME:
|
||||
enc.AddString("target", r.Target)
|
||||
case *dns.DNSKEY:
|
||||
case *dns.DS:
|
||||
case *dns.EID:
|
||||
case *dns.EUI48:
|
||||
case *dns.EUI64:
|
||||
case *dns.GID:
|
||||
case *dns.GPOS:
|
||||
case *dns.HINFO:
|
||||
case *dns.HIP:
|
||||
case *dns.HTTPS:
|
||||
// case *dns.IPSECKEY:
|
||||
case *dns.KEY:
|
||||
case *dns.KX:
|
||||
case *dns.L32:
|
||||
case *dns.L64:
|
||||
case *dns.LOC:
|
||||
case *dns.LP:
|
||||
case *dns.MB:
|
||||
case *dns.MD:
|
||||
case *dns.MF:
|
||||
case *dns.MG:
|
||||
case *dns.MINFO:
|
||||
case *dns.MR:
|
||||
case *dns.MX:
|
||||
enc.AddString("MX", r.Mx)
|
||||
enc.AddUint16("preference", r.Preference)
|
||||
case *dns.NAPTR:
|
||||
case *dns.NID:
|
||||
case *dns.NIMLOC:
|
||||
case *dns.NINFO:
|
||||
case *dns.NS:
|
||||
enc.AddString("NS", r.Ns)
|
||||
case *dns.NSAPPTR:
|
||||
case *dns.NSEC:
|
||||
case *dns.NSEC3:
|
||||
case *dns.NSEC3PARAM:
|
||||
case *dns.NULL:
|
||||
case *dns.OPENPGPKEY:
|
||||
enc.AddString("public_key", r.PublicKey)
|
||||
case *dns.OPT:
|
||||
case *dns.PTR:
|
||||
enc.AddString("PTR", r.Ptr)
|
||||
case *dns.PX:
|
||||
case *dns.RKEY:
|
||||
case *dns.RP:
|
||||
case *dns.RRSIG:
|
||||
case *dns.RT:
|
||||
case *dns.SIG:
|
||||
case *dns.SMIMEA:
|
||||
case *dns.SOA:
|
||||
enc.AddString("NS", r.Ns)
|
||||
enc.AddString("mbox", r.Mbox)
|
||||
enc.AddUint32("serial", r.Serial)
|
||||
enc.AddUint32("retry", r.Retry)
|
||||
enc.AddUint32("refresh", r.Refresh)
|
||||
enc.AddUint32("expire", r.Expire)
|
||||
enc.AddUint32("minttl", r.Minttl)
|
||||
case *dns.SPF:
|
||||
zap.Strings("TXT", r.Txt).AddTo(enc)
|
||||
case *dns.SRV:
|
||||
enc.AddUint16("priority", r.Priority)
|
||||
enc.AddUint16("weight", r.Weight)
|
||||
enc.AddUint16("port", r.Port)
|
||||
enc.AddString("target", r.Target)
|
||||
case *dns.SSHFP:
|
||||
enc.AddUint8("algorithm", r.Algorithm)
|
||||
enc.AddUint8("type", r.Type)
|
||||
enc.AddString("fingerprint", r.FingerPrint)
|
||||
case *dns.SVCB:
|
||||
case *dns.TA:
|
||||
case *dns.TALINK:
|
||||
case *dns.TKEY:
|
||||
case *dns.TLSA:
|
||||
case *dns.TSIG:
|
||||
case *dns.TXT:
|
||||
zap.Strings("TXT", r.Txt).AddTo(enc)
|
||||
case *dns.UID:
|
||||
case *dns.UINFO:
|
||||
case *dns.URI:
|
||||
case *dns.X25:
|
||||
case *dns.ZONEMD:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func log_libdns_record(record *libdns.Record) zapcore.ObjectMarshaler {
|
||||
f := func(enc zapcore.ObjectEncoder) error {
|
||||
enc.AddString("ID", record.ID)
|
||||
enc.AddString("type", record.Type)
|
||||
enc.AddString("name", record.Name)
|
||||
enc.AddString("value", record.Value)
|
||||
enc.AddString("TTL", record.TTL.String())
|
||||
if record.Priority != 0 {
|
||||
enc.AddInt("priority", record.Priority)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return zapcore.ObjectMarshalerFunc(f)
|
||||
}
|
||||
|
||||
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||
func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||
enc.AddString("zone", r.zone)
|
||||
if r.append {
|
||||
enc.AddString("type", "append")
|
||||
} else {
|
||||
enc.AddString("type", "delete")
|
||||
}
|
||||
|
||||
if len(r.records) > 0 {
|
||||
array := func(arr zapcore.ArrayEncoder) error {
|
||||
for _, r := range r.records {
|
||||
object := func(obj zapcore.ObjectEncoder) error {
|
||||
log_RR(obj, r)
|
||||
return nil
|
||||
}
|
||||
arr.AppendObject(zapcore.ObjectMarshalerFunc(object))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
enc.AddArray("records", zapcore.ArrayMarshalerFunc(array))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
144
caddyapp/provider.go
Normal file
144
caddyapp/provider.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
app_channel chan request
|
||||
logger *zap.Logger // set in Provision()
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Provider) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "dns.providers.internal",
|
||||
New: func() caddy.Module { return &Provider{} },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the module. Implements caddy.Provisioner.
|
||||
func (p *Provider) Provision(ctx caddy.Context) error {
|
||||
p.logger = ctx.Logger()
|
||||
if !ctx.AppIsConfigured("dns") {
|
||||
p.logger.Warn("DNS app not yet configured")
|
||||
}
|
||||
app, err := ctx.App("dns")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app == nil {
|
||||
return fmt.Errorf("failed to load DNS app")
|
||||
}
|
||||
dns_app, ok := app.(*App)
|
||||
if !ok {
|
||||
return fmt.Errorf("received invalid app")
|
||||
}
|
||||
p.app_channel = dns_app.requests
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile sets up the DNS provider from Caddyfile tokens. Syntax:
|
||||
//
|
||||
// dns internal
|
||||
func (p *Provider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
default:
|
||||
return d.Errf("unrecognized subdirective '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) convert(zone string, set []libdns.Record) ([]dns.RR, error) {
|
||||
converted := []dns.RR{}
|
||||
|
||||
for _, r := range set {
|
||||
rr, err := record_to_rr(zone, r)
|
||||
if err != nil {
|
||||
p.logger.Error(
|
||||
"failed to convert",
|
||||
zap.Error(err),
|
||||
zap.String("zone", zone),
|
||||
zap.Object("record", log_libdns_record(&r)),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
converted = append(converted, rr)
|
||||
}
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
func (p *Provider) make_request(
|
||||
ctx context.Context,
|
||||
zone string,
|
||||
append bool,
|
||||
recs []libdns.Record,
|
||||
) ([]libdns.Record, error) {
|
||||
resp := make(chan error)
|
||||
|
||||
records, err := p.convert(zone, recs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := request{
|
||||
append: append,
|
||||
zone: zone,
|
||||
records: records,
|
||||
responder: resp,
|
||||
}
|
||||
|
||||
p.app_channel <- req
|
||||
p.logger.Debug("sent request", zap.Object("request", req))
|
||||
|
||||
select {
|
||||
case err = <-resp:
|
||||
if err != nil {
|
||||
p.logger.Debug("request failed", zap.Error(err))
|
||||
return nil, err
|
||||
} else {
|
||||
p.logger.Debug("request succeeded")
|
||||
return recs, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) AppendRecords(
|
||||
ctx context.Context,
|
||||
zone string,
|
||||
recs []libdns.Record,
|
||||
) ([]libdns.Record, error) {
|
||||
return p.make_request(ctx, zone, true, recs)
|
||||
}
|
||||
|
||||
func (p *Provider) DeleteRecords(
|
||||
ctx context.Context,
|
||||
zone string,
|
||||
recs []libdns.Record,
|
||||
) ([]libdns.Record, error) {
|
||||
return p.make_request(ctx, zone, false, recs)
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Provider)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Provider)(nil)
|
||||
_ libdns.RecordAppender = (*Provider)(nil)
|
||||
_ libdns.RecordDeleter = (*Provider)(nil)
|
||||
)
|
||||
260
caddyapp/server.go
Normal file
260
caddyapp/server.go
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/miekg/dns"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// A DNS Query coming in from outside
|
||||
type query struct {
|
||||
w dns.ResponseWriter
|
||||
r *dns.Msg
|
||||
}
|
||||
|
||||
type key struct {
|
||||
Type dns.Type
|
||||
Name string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
// the address & port on which to serve DNS for the challenge
|
||||
Address caddy.NetworkAddress `json:"address,omitempty"`
|
||||
|
||||
// Statically configured records to serve
|
||||
Records map[key][]dns.RR `json:"records,omitempty"`
|
||||
|
||||
logger *zap.Logger // set by App.start()
|
||||
ctx *caddy.Context // set by App.start()
|
||||
shutdown chan struct{} // set by App.start()
|
||||
requests chan request // set by App.start()
|
||||
|
||||
dns_server *dns.Server // set by start_stop_server()
|
||||
queries chan query // set by start_stop_server()
|
||||
|
||||
}
|
||||
|
||||
func rr_key(record dns.RR) key {
|
||||
return key{
|
||||
Type: dns.Type(record.Header().Rrtype),
|
||||
Name: strings.ToLower(record.Header().Name),
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) insert_record(record dns.RR) {
|
||||
key := rr_key(record)
|
||||
current, exists := srv.Records[key]
|
||||
if exists {
|
||||
// TODO: de-duplicate?
|
||||
srv.Records[key] = append(current, record)
|
||||
} else {
|
||||
srv.Records[key] = []dns.RR{record}
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) delete_record(record dns.RR) bool {
|
||||
key := rr_key(record)
|
||||
current, exists := srv.Records[key]
|
||||
if exists {
|
||||
filtered := []dns.RR{}
|
||||
as_string := record.String()
|
||||
for _, rec := range current {
|
||||
// TODO: see if there is a more efficient way to compare these
|
||||
// just rec != record does not seem to work, might be doing ptr eq
|
||||
if rec.String() != as_string {
|
||||
filtered = append(filtered, rec)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
delete(srv.Records, key)
|
||||
} else {
|
||||
srv.Records[key] = filtered
|
||||
}
|
||||
return len(filtered) < len(current)
|
||||
} else {
|
||||
// doesn't exist, nothing to delete
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// This is the "main loop" of the DNS server
|
||||
// To avoid having to synchronize access to the records map, it is owned
|
||||
// exclusively by this loop, and the methods it calls.
|
||||
// All DNS queries coming from outside, as well as all requests to create
|
||||
// or delete DNS records coming from within the process are serialized by
|
||||
// the select statement.
|
||||
func (srv *Server) main() {
|
||||
srv.logger.Debug(
|
||||
"main loop running",
|
||||
zap.Int("record_count", len(srv.Records)),
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case r := <-srv.requests:
|
||||
srv.handle_request(r)
|
||||
case q := <-srv.queries:
|
||||
srv.handle_query(q)
|
||||
case <-srv.shutdown:
|
||||
srv.logger.Debug("stopping main loop")
|
||||
if srv.dns_server != nil {
|
||||
srv.dns_server.Shutdown()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) handle_request(r request) {
|
||||
var count_field zap.Field
|
||||
if r.append {
|
||||
for _, record := range r.records {
|
||||
srv.insert_record(record)
|
||||
}
|
||||
count_field = zap.Int("appended_records", len(r.records))
|
||||
} else {
|
||||
count := 0
|
||||
for _, record := range r.records {
|
||||
if srv.delete_record(record) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
count_field = zap.Int("deleted_records", count)
|
||||
}
|
||||
|
||||
srv.logger.Debug("handled", zap.Object("request", r), count_field)
|
||||
|
||||
r.responder <- srv.start_stop_server()
|
||||
}
|
||||
|
||||
func (srv *Server) start_stop_server() error {
|
||||
if srv.queries == nil {
|
||||
srv.queries = make(chan query)
|
||||
}
|
||||
if len(srv.Records) == 0 {
|
||||
if srv.dns_server != nil {
|
||||
srv.logger.Debug("no more records to serve, shutting down server")
|
||||
err := srv.dns_server.Shutdown()
|
||||
srv.dns_server = nil
|
||||
return err
|
||||
}
|
||||
srv.logger.Debug("no records to serve")
|
||||
return nil
|
||||
} else {
|
||||
if srv.dns_server == nil {
|
||||
conn, err := srv.bind()
|
||||
if err != nil {
|
||||
srv.logger.Error(
|
||||
"failed to bind",
|
||||
zap.Stringer("address", srv.Address),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// spawn the server
|
||||
handler := make_proxy(srv.queries)
|
||||
server := &dns.Server{
|
||||
PacketConn: conn,
|
||||
Net: "udp",
|
||||
Handler: handler,
|
||||
TsigSecret: nil,
|
||||
}
|
||||
srv.logger.Debug(
|
||||
"starting server",
|
||||
zap.Int("record_count", len(srv.Records)),
|
||||
)
|
||||
go srv.serve(server)
|
||||
|
||||
// store the server for shutdown later
|
||||
srv.dns_server = server
|
||||
return nil
|
||||
}
|
||||
srv.logger.Debug(
|
||||
"server already running",
|
||||
zap.Int("record_count", len(srv.Records)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) bind() (net.PacketConn, error) {
|
||||
conn, err := srv.Address.Listen(srv.ctx, 0, net.ListenConfig{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkt_conn := conn.(net.PacketConn)
|
||||
if pkt_conn == nil {
|
||||
return nil, errors.New("invalid address")
|
||||
}
|
||||
srv.logger.Debug("bound to socket", zap.Stringer("address", srv.Address))
|
||||
return pkt_conn, nil
|
||||
}
|
||||
|
||||
func (srv *Server) handle_query(q query) {
|
||||
// dns.DefaultMsgAcceptFunc already checks that the query is fairly
|
||||
// reasonable.
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q.r)
|
||||
|
||||
reject_and_log := func(code int, reason string) {
|
||||
m.Rcode = code
|
||||
m.Answer = []dns.RR{}
|
||||
srv.logger.Debug(
|
||||
"rejecting query",
|
||||
zap.Stringer("address", q.w.RemoteAddr()),
|
||||
zap.String("reason", reason),
|
||||
zap.Object("response", LoggableDNSMsg{m}),
|
||||
)
|
||||
q.w.WriteMsg(m)
|
||||
}
|
||||
|
||||
qstn := q.r.Question[0]
|
||||
if !(qstn.Qclass == dns.ClassINET || qstn.Qclass == dns.ClassANY) {
|
||||
// TODO: consider just not worrying about this
|
||||
reject_and_log(dns.RcodeNotImplemented, "invalid class")
|
||||
return
|
||||
}
|
||||
// queries may be wAcKY casE
|
||||
// https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00
|
||||
key := key{
|
||||
Type: dns.Type(qstn.Qtype),
|
||||
Name: strings.ToLower(qstn.Name),
|
||||
}
|
||||
records, exists := srv.Records[key]
|
||||
if !exists {
|
||||
reject_and_log(dns.RcodeNameError, "no such record")
|
||||
return
|
||||
}
|
||||
|
||||
m.Authoritative = true
|
||||
m.Answer = records
|
||||
|
||||
srv.logger.Debug(
|
||||
"answering query",
|
||||
zap.Stringer("address", q.w.RemoteAddr()),
|
||||
zap.Object("response", LoggableDNSMsg{m}),
|
||||
)
|
||||
q.w.WriteMsg(m)
|
||||
}
|
||||
|
||||
func (srv *Server) serve(server *dns.Server) {
|
||||
err := server.ActivateAndServe()
|
||||
if err != nil {
|
||||
srv.logger.Error("dns.ActivateAndServe failed", zap.Error(err))
|
||||
} else {
|
||||
srv.logger.Debug("server terminated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// dns.HandlerFunc that forwards every query into a channel
|
||||
func make_proxy(sink chan query) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
q := query{w, r}
|
||||
sink <- q
|
||||
}
|
||||
}
|
||||
352
caddyapp/server_test.go
Normal file
352
caddyapp/server_test.go
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const dns_address string = "127.0.0.1:53535"
|
||||
|
||||
const dns_only string = `{
|
||||
admin localhost:2999
|
||||
debug
|
||||
dns 127.0.0.1:53535 {
|
||||
record "sub123.example.com. A 127.0.0.1"
|
||||
record "ABC123. AAAA ::"
|
||||
record "example.com. CAA 0 issue ca.example.net"
|
||||
record "example.com. CNAME test123"
|
||||
record "example.com. 3333 IN NS ns1.example.com."
|
||||
record "example.com. MX 42 mx.example.com."
|
||||
record "_caddy._tcp.example.com. SRV 3 33 2999 test123."
|
||||
record "txt123.example.com. TXT Test123"
|
||||
record "whitespace. TXT Test 123 ABC XYZ"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const dns_only_json string = `{
|
||||
"admin": {
|
||||
"listen": "localhost:2999"
|
||||
},
|
||||
"logging": {
|
||||
"logs": {
|
||||
"default": {
|
||||
"level": "DEBUG"
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"dns": {
|
||||
"address": "127.0.0.1:53535",
|
||||
"records": [
|
||||
"sub123.example.com.\t3600\tIN\tA\t127.0.0.1",
|
||||
"ABC123.\t3600\tIN\tAAAA\t::",
|
||||
"example.com.\t3600\tIN\tCAA\t0 issue \"ca.example.net\"",
|
||||
"example.com.\t3600\tIN\tCNAME\ttest123.",
|
||||
"example.com.\t3333\tIN\tNS\tns1.example.com.",
|
||||
"example.com.\t3600\tIN\tMX\t42 mx.example.com.",
|
||||
"_caddy._tcp.example.com.\t3600\tIN\tSRV\t3 33 2999 test123.",
|
||||
"txt123.example.com.\t3600\tIN\tTXT\t\"Test123\"",
|
||||
"whitespace.\t3600\tIN\tTXT\t\"Test\" \"123\" \"ABC\" \"XYZ\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const dns_but_empty string = `{
|
||||
admin localhost:2999
|
||||
debug
|
||||
dns 127.0.0.1:53535
|
||||
}
|
||||
`
|
||||
|
||||
const dns_but_empty_json string = `{
|
||||
"admin": {
|
||||
"listen": "localhost:2999"
|
||||
},
|
||||
"logging": {
|
||||
"logs": {
|
||||
"default": {
|
||||
"level": "debug"
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"dns": {
|
||||
"address": "127.0.0.1:53535"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
func TestDNSConfig(t *testing.T) {
|
||||
caddytest.AssertAdapt(t, dns_only, "caddyfile", dns_only_json)
|
||||
}
|
||||
|
||||
func check_exists(t *testing.T, record string) {
|
||||
rr, err := dns.NewRR(record)
|
||||
if err != nil {
|
||||
t.Fatal("invalid record; ", record, "\nerror:", err)
|
||||
}
|
||||
in := query_dns(t, rr.Header().Name, rr.Header().Rrtype)
|
||||
|
||||
if in.MsgHdr.Rcode != dns.RcodeSuccess {
|
||||
t.Fatal("DNS error: ", dns.RcodeToString[in.MsgHdr.Rcode])
|
||||
}
|
||||
|
||||
expected := rr.String()
|
||||
received := in.Answer[0].String()
|
||||
|
||||
if expected != received {
|
||||
t.Fatal(
|
||||
"record mismatch!",
|
||||
"\nexpected: ",
|
||||
expected,
|
||||
"\nreceived: ",
|
||||
received,
|
||||
)
|
||||
}
|
||||
|
||||
// Gives different results than the string comparison. unclear why
|
||||
//if !reflect.DeepEqual(in.Answer[0], rr) {
|
||||
// t.Fatal("answer section mismatch!")
|
||||
//}
|
||||
}
|
||||
|
||||
func query_dns(t *testing.T, name string, qtype uint16) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(name, qtype)
|
||||
|
||||
c := new(dns.Client)
|
||||
c.DialTimeout = 1 * time.Second
|
||||
in, rtt, err := c.Exchange(m, dns_address)
|
||||
_ = rtt
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(in.Question, m.Question) {
|
||||
t.Fatal("question section mismatch!")
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func wAcKY_casE(input string) string {
|
||||
INPUT := strings.ToUpper(input)
|
||||
InPuT := ""
|
||||
|
||||
for i := range input {
|
||||
if rand.Intn(2) == 1 {
|
||||
InPuT = InPuT + INPUT[i:i]
|
||||
} else {
|
||||
InPuT = InPuT + input[i:i]
|
||||
}
|
||||
}
|
||||
|
||||
return InPuT
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00
|
||||
func qUeRY_dNs(t *testing.T, name string, qtype uint16) *dns.Msg {
|
||||
|
||||
runes := []rune{}
|
||||
for _, c := range name {
|
||||
if rand.Intn(2) == 1 {
|
||||
runes = append(runes, unicode.ToUpper(c))
|
||||
} else {
|
||||
runes = append(runes, c)
|
||||
}
|
||||
}
|
||||
nAmE := string(runes)
|
||||
println("before: ", name, "\nafter: ", nAmE)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(nAmE, qtype)
|
||||
|
||||
c := new(dns.Client)
|
||||
c.DialTimeout = 1 * time.Second
|
||||
in, rtt, err := c.Exchange(m, dns_address)
|
||||
_ = rtt
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(in.Question, m.Question) {
|
||||
t.Fatal(
|
||||
"question section mismatch!",
|
||||
"\nsent: ",
|
||||
m.Question,
|
||||
"\nreceived: ",
|
||||
in.Question,
|
||||
)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func cHEcK_eXiSTs(t *testing.T, record string) {
|
||||
rr, err := dns.NewRR(record)
|
||||
if err != nil {
|
||||
t.Fatal("invalid record; ", record, "\nerror:", err)
|
||||
}
|
||||
in := qUeRY_dNs(t, rr.Header().Name, rr.Header().Rrtype)
|
||||
|
||||
if in.MsgHdr.Rcode != dns.RcodeSuccess {
|
||||
t.Fatal("DNS error: ", dns.RcodeToString[in.MsgHdr.Rcode])
|
||||
}
|
||||
|
||||
expected := rr.String()
|
||||
received := in.Answer[0].String()
|
||||
|
||||
if expected != received {
|
||||
t.Fatal(
|
||||
"record mismatch!",
|
||||
"\nexpected: ",
|
||||
expected,
|
||||
"\nreceived: ",
|
||||
received,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func check_errors(t *testing.T, m *dns.Msg, rcode int) {
|
||||
c := new(dns.Client)
|
||||
c.DialTimeout = 100 * time.Millisecond
|
||||
in, rtt, err := c.Exchange(m, dns_address)
|
||||
_ = rtt
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err, "\n", m)
|
||||
}
|
||||
/*
|
||||
if !reflect.DeepEqual(in.Question, m.Question) {
|
||||
t.Fatal(
|
||||
"question section mismatch!",
|
||||
"\nsent: ",
|
||||
m.Question,
|
||||
"\nreceived: ",
|
||||
in.Question,
|
||||
)
|
||||
}
|
||||
*/
|
||||
if in.Rcode != rcode {
|
||||
t.Fatal(
|
||||
"rcode mismatch:",
|
||||
"\nexpected: ",
|
||||
rcode,
|
||||
"\nreceived: ",
|
||||
in.Rcode,
|
||||
"\nquery:\n",
|
||||
m,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func check_fails(t *testing.T, m *dns.Msg, error_contains string) {
|
||||
c := new(dns.Client)
|
||||
c.DialTimeout = 100 * time.Millisecond
|
||||
in, rtt, err := c.Exchange(m, dns_address)
|
||||
_ = rtt
|
||||
|
||||
if in != nil {
|
||||
t.Fatal(
|
||||
"query was expected to fail with: \"",
|
||||
error_contains,
|
||||
"\", but returned response!\n",
|
||||
in.String(),
|
||||
)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal(
|
||||
"query was expected to fail with: \"",
|
||||
error_contains,
|
||||
"\", but did not return an error!",
|
||||
)
|
||||
}
|
||||
if !strings.Contains(err.Error(), error_contains) {
|
||||
// TODO: this might be flaky
|
||||
t.Fatal(
|
||||
"query was expected to fail with: \"",
|
||||
error_contains,
|
||||
"\", but returned error: ",
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
// I guess I have to seed my own RNG like a caveman
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
caddytest.Default.TestRequestTimeout = 1 * time.Second
|
||||
caddytest.Default.LoadRequestTimeout = 1 * time.Second
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(dns_only, "caddyfile")
|
||||
|
||||
records := []string{
|
||||
"sub123.example.com. A 127.0.0.1",
|
||||
"ABC123 AAAA ::",
|
||||
"example.com. CAA 0 issue ca.example.net",
|
||||
"example.com CNAME test123",
|
||||
"example.com. 3333 IN NS ns1.example.com.",
|
||||
"example.com. MX 42 mx.example.com.",
|
||||
"_caddy._tcp.example.com. SRV 3 33 2999 test123.",
|
||||
"txt123.example.com. TXT Test123",
|
||||
"whitespace. TXT Test 123 ABC XYZ",
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
check_exists(t, record)
|
||||
/* WACKY-CASE QUERIES */
|
||||
cHEcK_eXiSTs(t, record)
|
||||
}
|
||||
|
||||
empty := new(dns.Msg)
|
||||
check_errors(t, empty, dns.RcodeFormatError)
|
||||
|
||||
non_existent := new(dns.Msg)
|
||||
non_existent.SetQuestion("does.not.exist.example.com.", dns.TypeA)
|
||||
check_errors(t, non_existent, dns.RcodeNameError)
|
||||
|
||||
chaos := new(dns.Msg)
|
||||
chaos.SetQuestion("sub123.example.com.", dns.TypeA)
|
||||
chaos.Question[0].Qclass = dns.ClassCHAOS
|
||||
check_errors(t, chaos, dns.RcodeNotImplemented)
|
||||
|
||||
/* IGNORE NON-QUESTIONS */
|
||||
question := new(dns.Msg)
|
||||
question.SetQuestion("sub123.example.com.", dns.TypeA)
|
||||
not_a_question := new(dns.Msg)
|
||||
not_a_question.SetReply(not_a_question)
|
||||
answer, _ := dns.NewRR("sub123.example.com. A 127.0.0.1")
|
||||
not_a_question.Answer = []dns.RR{answer}
|
||||
check_fails(t, not_a_question, "timeout")
|
||||
|
||||
/* REFUSE MULTI-QUESTIONS */
|
||||
multi := new(dns.Msg)
|
||||
multi.SetQuestion("example.com.", dns.TypeCNAME)
|
||||
q1 := multi.Question[0]
|
||||
multi.SetQuestion("sub123.example.com.", dns.TypeA)
|
||||
multi.Question = append(multi.Question, q1)
|
||||
check_errors(t, multi, dns.RcodeFormatError)
|
||||
|
||||
}
|
||||
|
||||
// The server only listens for DNS queries if there are records to serve
|
||||
func TestEmptyServer(t *testing.T) {
|
||||
caddytest.Default.TestRequestTimeout = 1 * time.Second
|
||||
caddytest.Default.LoadRequestTimeout = 1 * time.Second
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(dns_but_empty, "caddyfile")
|
||||
|
||||
question := new(dns.Msg)
|
||||
question.SetQuestion("sub123.example.com.", dns.TypeA)
|
||||
check_fails(t, question, "refused")
|
||||
}
|
||||
47
caddyapp/stub.go
Normal file
47
caddyapp/stub.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// An in-process request to create or delete a DNS record
|
||||
type request struct {
|
||||
append bool
|
||||
zone string
|
||||
records []dns.RR
|
||||
responder chan error
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(App{})
|
||||
caddy.RegisterModule(Provider{})
|
||||
|
||||
httpcaddyfile.RegisterGlobalOption("dns", parseApp)
|
||||
}
|
||||
|
||||
func record_to_rr(zone string, record libdns.Record) (dns.RR, error) {
|
||||
maybe_priority := ""
|
||||
if record.Priority != 0 {
|
||||
maybe_priority += strconv.FormatInt(int64(record.Priority), 10)
|
||||
maybe_priority += " "
|
||||
}
|
||||
//TODO: consider fixing this with dns.StringToType & dns.TypeToRR
|
||||
// Problem is putting the value in, since it will be a different field
|
||||
// for every type.
|
||||
// Also, will probably require parsing the value anyway (e.g. to net.IP)
|
||||
//TODO: does the value need to be escaped?!
|
||||
return dns.NewRR(
|
||||
dns.Fqdn(record.Name+"."+zone) +
|
||||
" " +
|
||||
strconv.FormatInt(int64(record.TTL.Seconds()), 10) +
|
||||
" IN " +
|
||||
record.Type +
|
||||
" " +
|
||||
maybe_priority +
|
||||
record.Value)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue