Documentation
¶
Overview ¶
Package gatewaydns is an embeddable DNS engine: a caching, filtering, forwarding resolver you construct inside your own program.
engine, err := gatewaydns.New(gatewaydns.Options{
Upstreams: []string{"tls://1.1.1.1:853", "tls://9.9.9.9:853"},
})
if err != nil {
return err
}
defer engine.Close()
if err := engine.AddBlocklistFile("/etc/blocklist.txt"); err != nil {
return err
}
if err := engine.ListenAndServe("127.0.0.1:5353"); err != nil {
return err
}
What this package is ¶
It is a facade, and only a facade. Every decision it makes is a default over the packages beneath it, and every one of those packages is usable on its own: github.com/daboss2003/dns/dnsmsg for the wire format, github.com/daboss2003/dns/cache for caching, and so on down the list in the architecture documentation. Nothing here is required to use any of them, and nothing there depends on anything here.
That matters because a facade's defaults are opinions, and opinions age. When one of these stops fitting — a different failover strategy, a policy engine of your own, a transport this project has not implemented — the answer is to assemble the pieces directly rather than to wait for an option to be added. The facade exists to make the common case one call, not to be the only door.
What it will not do for you ¶
It does not open a privileged port on your behalf, drop privileges, daemonise, write a PID file, install a service, or reach for a configuration file it was not given. Those belong to whatever is embedding this, and a library that did them would be making decisions about a process it does not own.
Engine.Serve takes listeners the caller opened, precisely so a program that needs port 53 can bind it while it still has the privilege to do so and hand the sockets over afterwards. Engine.ListenAndServe is the convenience form for everything else.
Concurrency ¶
An Engine is safe for concurrent use once constructed. Blocklists and rules may be replaced while it is serving: the swap is atomic, so a reload of a million entries never blocks a query.
Example ¶
A filtering forwarder on port 53, shut down cleanly on a signal.
The sockets are opened here rather than inside the engine because port 53 is privileged: a process that starts as root binds them, hands them over, and can then drop to an unprivileged user before the first query arrives.
engine, err := gatewaydns.New(gatewaydns.Options{
Upstreams: []string{"tls://1.1.1.1", "tls://9.9.9.9"},
ServeStale: 24 * time.Hour,
})
if err != nil {
log.Fatal(err)
}
defer engine.Close()
if _, err := engine.AddBlocklistFile("/etc/gatewaydns/blocklist.txt"); err != nil {
log.Fatal(err)
}
pc, err := net.ListenPacket("udp", ":53")
if err != nil {
log.Fatal(err)
}
l, err := net.Listen("tcp", ":53")
if err != nil {
log.Fatal(err)
}
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown stops accepting and lets the queries already in flight
// finish. Close, which the defer above runs, is the hard stop.
_ = engine.Shutdown(ctx)
}()
if err := engine.Serve(pc, l); err != nil {
log.Fatal(err)
}
Index ¶
- type Engine
- func (e *Engine) AddAllowlist(r io.Reader, source string) (policy.ListStats, error)
- func (e *Engine) AddAllowlistFile(path string) (policy.ListStats, error)
- func (e *Engine) AddBlocklist(r io.Reader, source string) (policy.ListStats, error)
- func (e *Engine) AddBlocklistFile(path string) (policy.ListStats, error)
- func (e *Engine) Allow(name string) error
- func (e *Engine) AllowExact(name string) error
- func (e *Engine) Block(name string) error
- func (e *Engine) BlockExact(name string) error
- func (e *Engine) BlockRegex(pattern string) error
- func (e *Engine) ClearCache()
- func (e *Engine) ClearCacheFor(name string) (int, error)
- func (e *Engine) ClearRules()
- func (e *Engine) Close() error
- func (e *Engine) Events() *events.Bus
- func (e *Engine) Handler() resolver.Handler
- func (e *Engine) ListenAndServe(addr string) error
- func (e *Engine) QueryLog() storage.Store
- func (e *Engine) Resolve(ctx context.Context, q *dnsmsg.Message) (*dnsmsg.Message, error)
- func (e *Engine) Serve(pc net.PacketConn, l net.Listener) error
- func (e *Engine) Shutdown(ctx context.Context) error
- func (e *Engine) Stats() Stats
- type Options
- type Stats
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is a caching, filtering, forwarding DNS resolver.
func New ¶
New builds an engine from opts. It opens no sockets; see Engine.Serve and Engine.ListenAndServe.
func (*Engine) AddAllowlist ¶
AddAllowlist loads an allowlist from r.
func (*Engine) AddAllowlistFile ¶
AddAllowlistFile loads an allowlist and installs it.
An allow rule beats a block rule whatever their specificity, which is what makes a large list usable: a list of a million names cannot be audited, so the way anyone recovers from a false positive is to allow the name here rather than to edit a list somebody else maintains.
func (*Engine) AddBlocklist ¶
AddBlocklist loads a blocklist from r. source names it in logs and in the reason a blocked client is given.
func (*Engine) AddBlocklistFile ¶
AddBlocklistFile loads a blocklist and installs it.
The format is detected: a hosts file, one name per line, or the DNS-expressible subset of Adblock syntax. A single unparseable line never fails the load — real lists contain stray markup and half-edited entries, and discarding a million good rules over one bad one is the wrong trade — so the count of rejected lines comes back in the policy.ListStats rather than as an error.
Calling it several times accumulates. Rules take effect on the next query: the swap is atomic, so loading a million entries never blocks one.
func (*Engine) Allow ¶
Allow adds one rule allowing name and everything under it, overriding any block.
func (*Engine) AllowExact ¶
AllowExact adds one rule allowing name and nothing under it.
func (*Engine) Block ¶
Block adds one rule blocking name and everything under it.
Example ¶
Rules can be added and removed while the engine is serving; every change applies to the next query, including one whose answer is already cached.
engine, err := gatewaydns.New(gatewaydns.Options{Upstreams: []string{"1.1.1.1"}})
if err != nil {
log.Fatal(err)
}
defer engine.Close()
// A name and everything under it.
_ = engine.Block("doubleclick.net")
// That name only, leaving its subdomains alone.
_ = engine.BlockExact("example.com")
// A pattern, held to RE2 so it cannot backtrack into a stall.
_ = engine.BlockRegex(`^ads?[0-9]*\.`)
// An exception, which beats every block above it.
_ = engine.Allow("cdn.doubleclick.net")
fmt.Println(engine.Stats().BlockRules)
Output: 3
func (*Engine) BlockExact ¶
BlockExact adds one rule blocking name and nothing under it.
func (*Engine) BlockRegex ¶
BlockRegex adds one rule blocking names matching a regular expression.
Patterns are matched against the canonical (lower-cased) name and are case-insensitive. They are checked only after every literal rule has failed to match, because a literal rule is cheaper and easier to reason about — and they are safe to accept at all only because Go's regexp is RE2: linear in the input with no backtracking, so a pathological pattern costs more than a simple one but cannot be made to cost unboundedly more.
func (*Engine) ClearCacheFor ¶
ClearCacheFor drops name and everything under it, reporting how many entries went.
It is the instrument to reach for when one name's records have changed: flushing everything to fix one name throws away every other answer the resolver has learned, which on a busy network is a self-inflicted latency spike.
func (*Engine) ClearRules ¶
func (e *Engine) ClearRules()
ClearRules discards every rule, leaving the engine resolving everything.
func (*Engine) Close ¶
Close shuts the engine down and releases everything it opened. It is idempotent.
func (*Engine) Events ¶
Events returns the engine's event bus, on which a caller can subscribe to queries, blocks, upstream failures and configuration reloads.
Events are not logs and not metrics: they are the stream a user interface tails and an integration reacts to. Publication is non-blocking and bounded, so a subscriber that stops reading loses events rather than adding latency to the queries it is watching.
func (*Engine) Handler ¶
Handler returns the resolver, for a caller that has an identity to supply.
Engine.Resolve is the simple form and attributes the query to nobody. This is the form a front end uses when it knows which client is asking, so that Options.Identify and Options.Devices can do their work — and so the query is logged as coming from a device rather than from the process itself.
func (*Engine) ListenAndServe ¶
ListenAndServe opens a UDP socket and a TCP listener on addr and serves both.
func (*Engine) QueryLog ¶
QueryLog returns the store the engine writes to, or nil when none was configured. It is how an embedder reads back what was resolved.
func (*Engine) Resolve ¶
Resolve answers one query directly, without a socket.
It is what an embedder uses when the queries are already in hand — a DoH front end, a test, a program resolving on its own behalf — and it is the same path the listeners take, so it exercises the cache, the policy and the upstreams identically. It answers on behalf of nobody in particular, so per-device policy does not apply to it. A caller that knows whose query it is — a DoH front end holding a client address, a test — should use Engine.Handler and pass the client.
Example ¶
An engine can resolve without listening on anything, which is what a program embedding one for its own lookups wants.
engine, err := gatewaydns.New(gatewaydns.Options{
Upstreams: []string{"https://dns.quad9.net/dns-query"},
})
if err != nil {
log.Fatal(err)
}
defer engine.Close()
q := new(dnsmsg.Message)
q.SetQuestion(dnsmsg.MustParseName("example.com."), dnsmsg.TypeA, dnsmsg.ClassINET)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
reply, err := engine.Resolve(ctx, q)
if err != nil {
log.Fatal(err)
}
for _, rr := range reply.Answers {
if a, ok := rr.Data.(*dnsmsg.A); ok {
fmt.Println(a.Addr)
}
}
func (*Engine) Serve ¶
Serve answers queries on the given sockets until Engine.Close. Either may be nil, and the sockets remain the caller's to close.
This is the primary entry point rather than Engine.ListenAndServe because port 53 is privileged: a program that needs it binds the socket while it still has the privilege and hands it over here.
type Options ¶
type Options struct {
// Upstreams are endpoints in preference order, each "scheme://address"
// where scheme is udp, tcp, tls or https. A bare address is treated as udp.
//
// A udp:// upstream is automatically paired with tcp:// to the same
// address, because a truncated UDP answer is an instruction to ask again
// over a stream and a resolver that could not would fail every large answer.
Upstreams []string
// Strategy picks between upstreams. Nil selects sequential failover, which
// is the right default: it is deterministic, it costs one query, and it
// respects the order the operator wrote them in. [providers.Race] is faster
// at the tail and leaks every name to every provider raced, which is a
// privacy decision rather than a tuning one.
Strategy providers.Strategy
// Randomize enables DNS-0x20 case randomisation against upstreams. It costs
// nothing and adds roughly a bit of entropy per letter against off-path
// forgery, and is off by default only because a small number of servers
// normalise case in replies and break outright when it is on.
Randomize bool
// CacheEntries bounds the cache. Zero selects a default; negative disables
// caching entirely, which is a supported configuration for an engine
// sitting in front of another resolver.
CacheEntries int
// MinTTL and MaxTTL clamp how long answers are held.
MinTTL, MaxTTL time.Duration
// ServeStale answers from expired entries for this long when no upstream
// can be reached (RFC 8767). Zero disables it. It is a deliberate
// correctness trade and so is off unless asked for.
ServeStale time.Duration
// BlockMode is what a filtered client is told. See [policy.BlockMode]; none
// of the choices is simply correct.
BlockMode policy.BlockMode
// QueryTimeout bounds one resolution end to end.
QueryTimeout time.Duration
// Timeout bounds one upstream exchange.
Timeout time.Duration
// Dialer opens upstream connections. Nil uses the standard library's.
Dialer transport.Dialer
// QueryLog records resolved queries. Nil keeps no record, which is the most
// private configuration available and costs nothing to choose.
QueryLog storage.Store
// Middleware wraps resolution, outermost first, and runs BEFORE the
// engine's own policy middleware.
//
// Before, because that ordering is what makes the seam useful. The thing a
// consumer most often needs here is to identify the client — to turn the
// address a query arrived from into a device, and put it in
// [resolver.Client.ID] — and policy scoped to a device has to run after
// something has decided which device it is. A middleware that ran
// afterwards could observe the decision but not inform it.
//
// [resolver.Client] is passed by value, so a middleware sets a field on its
// own copy and hands that to the next handler; nothing it does can reach
// back into the server.
Middleware []resolver.Middleware
// Devices maps a client to the rules governing it, which is how per-device
// policy is applied. Nil gives every client the same rules, which is the
// default.
//
// It is called on every query and must be cheap and safe for concurrent
// use. Pair it with [Options.Identify]: that assigns the identity, this
// decides what the identity means.
Devices policy.Resolver
// Identify turns the address a query arrived from into a device identity,
// before anything else runs. Nil leaves every query anonymous, which is the
// default and the most private configuration.
//
// It is separate from [Options.Middleware] because it must inform the
// metrics and the query log as well as the policy decision, and a
// middleware can only inform what runs after it. See
// [resolver.Options.Identify].
Identify func(resolver.Client) resolver.Client
Clock clock.Clock
Logger *slog.Logger
}
Options configure an Engine. The zero value is not usable; at least one upstream is required.
type Stats ¶
type Stats struct {
Queries metrics.Snapshot `json:"queries"`
Cache *cache.Stats `json:"cache,omitempty"`
Server server.Stats `json:"server"`
// Rules is how many block and allow rules are installed.
BlockRules int `json:"block_rules"`
AllowRules int `json:"allow_rules"`
// Evaluated and Blocked count policy decisions.
Evaluated uint64 `json:"evaluated"`
Blocked uint64 `json:"blocked"`
}
Stats is a snapshot of what the engine has done.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package api exposes a resolver's statistics and controls over HTTP.
|
Package api exposes a resolver's statistics and controls over HTTP. |
|
Package cache stores DNS responses and serves them until they expire.
|
Package cache stores DNS responses and serves them until they expire. |
|
Package clock abstracts the passage of time so that code depending on it can be tested without waiting.
|
Package clock abstracts the passage of time so that code depending on it can be tested without waiting. |
|
Package config composes the configuration of a GatewayDNS engine into one document, loads it, validates it, and hands it out to a running server.
|
Package config composes the configuration of a GatewayDNS engine into one document, loads it, validates it, and hands it out to a running server. |
|
Package dnsmsg implements the DNS message format defined by RFC 1035 and its many successors.
|
Package dnsmsg implements the DNS message format defined by RFC 1035 and its many successors. |
|
Package events delivers individual occurrences inside the engine to programmatic subscribers, without ever blocking the code that reports them.
|
Package events delivers individual occurrences inside the engine to programmatic subscribers, without ever blocking the code that reports them. |
|
examples
|
|
|
customtype
command
Command customtype implements a resource record type that dnsmsg has never heard of, and round-trips it through the wire codec.
|
Command customtype implements a resource record type that dnsmsg has never heard of, and round-trips it through the wire codec. |
|
inspect
command
Command inspect decodes a DNS message and prints everything in it.
|
Command inspect decodes a DNS message and prints everything in it. |
|
query
command
Command query resolves a single name against a recursive resolver over UDP and prints the response.
|
Command query resolves a single name against a recursive resolver over UDP and prints the response. |
|
Package logging supplies the parts of structured logging that the standard library leaves to the application, and nothing else.
|
Package logging supplies the parts of structured logging that the standard library leaves to the application, and nothing else. |
|
Package metrics aggregates what a DNS engine did, so that an operator can answer "is it healthy?" without reading a log.
|
Package metrics aggregates what a DNS engine did, so that an operator can answer "is it healthy?" without reading a log. |
|
Package policy decides what to do with a query before it is resolved.
|
Package policy decides what to do with a query before it is resolved. |
|
Package providers turns a set of upstream resolvers into one upstream that keeps working when some of them do not.
|
Package providers turns a set of upstream resolvers into one upstream that keeps working when some of them do not. |
|
Package resolver answers a DNS query using a cache and a set of upstreams.
|
Package resolver answers a DNS query using a cache and a set of upstreams. |
|
Package server accepts DNS queries from the network and answers them.
|
Package server accepts DNS queries from the network and answers them. |
|
Package storage persists what a resolver learns: the query log, per-device statistics, and whatever else a deployment wants to keep across a restart.
|
Package storage persists what a resolver learns: the query log, per-device statistics, and whatever else a deployment wants to keep across a restart. |
|
Package transport carries a DNS query to an upstream resolver and brings back the reply.
|
Package transport carries a DNS query to an upstream resolver and brings back the reply. |