mirror of
https://github.com/coredns/coredns.git
synced 2026-04-05 11:45:33 -04:00
Extend the tsig plugin to require TSIG signatures based on DNS opcodes,
similar to the existing qtype-based requirement.
The new require_opcode directive accepts opcode names (QUERY, IQUERY,
STATUS, NOTIFY, UPDATE) or the special values "all" and "none".
This is useful for requiring TSIG on dynamic update (UPDATE) or zone
transfer notification (NOTIFY) requests while allowing unsigned queries.
Example:
```
tsig {
secret key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk=
require_opcode UPDATE NOTIFY
}
```
Signed-off-by: Seena Fallah <seenafallah@gmail.com>
148 lines
4.2 KiB
Go
148 lines
4.2 KiB
Go
package tsig
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"time"
|
|
|
|
"github.com/coredns/coredns/plugin"
|
|
"github.com/coredns/coredns/plugin/pkg/log"
|
|
"github.com/coredns/coredns/request"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// TSIGServer verifies tsig status and adds tsig to responses
|
|
type TSIGServer struct {
|
|
Zones []string
|
|
secrets map[string]string // [key-name]secret
|
|
types qTypes
|
|
opcodes opCodes
|
|
allTypes bool
|
|
allOpcodes bool
|
|
Next plugin.Handler
|
|
}
|
|
|
|
type qTypes map[uint16]struct{}
|
|
type opCodes map[int]struct{}
|
|
|
|
// Name implements plugin.Handler
|
|
func (t TSIGServer) Name() string { return pluginName }
|
|
|
|
// ServeDNS implements plugin.Handler
|
|
func (t *TSIGServer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
|
var err error
|
|
state := request.Request{Req: r, W: w}
|
|
if z := plugin.Zones(t.Zones).Matches(state.Name()); z == "" {
|
|
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
|
}
|
|
|
|
var tsigRR = r.IsTsig()
|
|
rcode := dns.RcodeSuccess
|
|
if !t.tsigRequired(state.QType(), r.Opcode) && tsigRR == nil {
|
|
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
|
}
|
|
|
|
if tsigRR == nil {
|
|
log.Debugf("rejecting '%s' request without TSIG\n", dns.TypeToString[state.QType()])
|
|
rcode = dns.RcodeRefused
|
|
}
|
|
|
|
// wrap the response writer so the response will be TSIG signed.
|
|
w = &restoreTsigWriter{w, r, tsigRR}
|
|
|
|
tsigStatus := w.TsigStatus()
|
|
if tsigStatus != nil {
|
|
log.Debugf("TSIG validation failed: %v %v", dns.TypeToString[state.QType()], tsigStatus)
|
|
rcode = dns.RcodeNotAuth
|
|
switch tsigStatus {
|
|
case dns.ErrSecret:
|
|
tsigRR.Error = dns.RcodeBadKey
|
|
case dns.ErrTime:
|
|
tsigRR.Error = dns.RcodeBadTime
|
|
default:
|
|
tsigRR.Error = dns.RcodeBadSig
|
|
}
|
|
resp := new(dns.Msg).SetRcode(r, rcode)
|
|
w.WriteMsg(resp)
|
|
return dns.RcodeSuccess, nil
|
|
}
|
|
|
|
// strip the TSIG RR. Next, and subsequent plugins will not see the TSIG RRs.
|
|
// This violates forwarding cases (RFC 8945 5.5). See README.md Bugs
|
|
if len(r.Extra) > 1 {
|
|
r.Extra = r.Extra[0 : len(r.Extra)-1]
|
|
} else {
|
|
r.Extra = []dns.RR{}
|
|
}
|
|
|
|
if rcode == dns.RcodeSuccess {
|
|
rcode, err = plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
|
|
if err != nil {
|
|
log.Errorf("request handler returned an error: %v\n", err)
|
|
}
|
|
}
|
|
// If the plugin chain result was not an error, restore the TSIG and write the response.
|
|
if !plugin.ClientWrite(rcode) {
|
|
resp := new(dns.Msg).SetRcode(r, rcode)
|
|
w.WriteMsg(resp)
|
|
}
|
|
return dns.RcodeSuccess, nil
|
|
}
|
|
|
|
func (t *TSIGServer) tsigRequired(qtype uint16, opcode int) bool {
|
|
typeMatches := t.allTypes
|
|
if !typeMatches {
|
|
_, typeMatches = t.types[qtype]
|
|
}
|
|
|
|
opcodeMatches := t.allOpcodes
|
|
if !opcodeMatches {
|
|
_, opcodeMatches = t.opcodes[opcode]
|
|
}
|
|
|
|
return typeMatches || opcodeMatches
|
|
}
|
|
|
|
// restoreTsigWriter Implement Response Writer, and adds a TSIG RR to a response
|
|
type restoreTsigWriter struct {
|
|
dns.ResponseWriter
|
|
req *dns.Msg // original request excluding TSIG if it has one
|
|
reqTSIG *dns.TSIG // original TSIG
|
|
}
|
|
|
|
// WriteMsg adds a TSIG RR to the response
|
|
func (r *restoreTsigWriter) WriteMsg(m *dns.Msg) error {
|
|
// Make sure the response has an EDNS OPT RR if the request had it.
|
|
// Otherwise ScrubWriter would append it *after* TSIG, making it a non-compliant DNS message.
|
|
state := request.Request{Req: r.req, W: r.ResponseWriter}
|
|
state.SizeAndDo(m)
|
|
|
|
repTSIG := m.IsTsig()
|
|
if r.reqTSIG != nil && repTSIG == nil {
|
|
repTSIG = new(dns.TSIG)
|
|
repTSIG.Hdr = dns.RR_Header{Name: r.reqTSIG.Hdr.Name, Rrtype: dns.TypeTSIG, Class: dns.ClassANY}
|
|
repTSIG.Algorithm = r.reqTSIG.Algorithm
|
|
repTSIG.OrigId = m.Id
|
|
repTSIG.Error = r.reqTSIG.Error
|
|
repTSIG.MAC = r.reqTSIG.MAC
|
|
repTSIG.MACSize = r.reqTSIG.MACSize
|
|
if repTSIG.Error == dns.RcodeBadTime {
|
|
// per RFC 8945 5.2.3. client time goes into TimeSigned, server time in OtherData, OtherLen = 6 ...
|
|
repTSIG.TimeSigned = r.reqTSIG.TimeSigned
|
|
b := make([]byte, 8)
|
|
// TimeSigned is network byte order.
|
|
binary.BigEndian.PutUint64(b, uint64(time.Now().Unix())) // #nosec G115 -- Unix time fits in uint64
|
|
// truncate to 48 least significant bits (network order 6 rightmost bytes)
|
|
repTSIG.OtherData = hex.EncodeToString(b[2:])
|
|
repTSIG.OtherLen = 6
|
|
}
|
|
m.Extra = append(m.Extra, repTSIG)
|
|
}
|
|
|
|
return r.ResponseWriter.WriteMsg(m)
|
|
}
|
|
|
|
const pluginName = "tsig"
|