mirror of https://github.com/namecoin/ncdns
major refactoring
parent
12b30fa15c
commit
db0f0b6bbb
@ -1,16 +0,0 @@
|
|||||||
package abstract
|
|
||||||
import "github.com/miekg/dns"
|
|
||||||
|
|
||||||
type Backend interface {
|
|
||||||
// Lookup all resource records having a given fully-qualified owner name,
|
|
||||||
// regardless of type or class. Returns a slice of all those resource records
|
|
||||||
// or an error.
|
|
||||||
//
|
|
||||||
// The returned slice may contain both authoritative and non-authoritative records
|
|
||||||
// (for example, NS records for delegations and glue records.)
|
|
||||||
//
|
|
||||||
// The existence of wildcard records will be determined by doing a lookup for a name
|
|
||||||
// like "*.example.com", so there is no need to process the wildcard logic other than
|
|
||||||
// to make sure such a lookup functions correctly.
|
|
||||||
Lookup(qname string) (rrs []dns.RR, err error)
|
|
||||||
}
|
|
@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
import "github.com/hlandau/degoutils/config"
|
||||||
|
import "github.com/hlandau/degoutils/log"
|
||||||
|
import "github.com/hlandau/ncdns/server"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := server.ServerConfig{}
|
||||||
|
config := config.Configurator{
|
||||||
|
ProgramName: "ncdns",
|
||||||
|
ConfigFilePaths: []string { "etc/ncdns.conf", "/etc/ncdns/ncdns.conf", },
|
||||||
|
}
|
||||||
|
config.ParseFatal(&cfg)
|
||||||
|
s, err := server.NewServer(&cfg)
|
||||||
|
log.Fatale(err)
|
||||||
|
|
||||||
|
s.Run()
|
||||||
|
}
|
@ -1,618 +0,0 @@
|
|||||||
package main
|
|
||||||
import "github.com/miekg/dns"
|
|
||||||
import "github.com/hlandau/degoutils/log"
|
|
||||||
import "os/signal"
|
|
||||||
import "os"
|
|
||||||
import "syscall"
|
|
||||||
import "fmt"
|
|
||||||
import "strings"
|
|
||||||
import "sort"
|
|
||||||
import "github.com/hlandau/degoutils/config"
|
|
||||||
import "github.com/hlandau/ncdns/ncerr"
|
|
||||||
import "github.com/hlandau/ncdns/abstract"
|
|
||||||
import "github.com/hlandau/ncdns/backend"
|
|
||||||
|
|
||||||
// A Go daemon to serve Namecoin domain records via DNS.
|
|
||||||
// This daemon is intended to be used in one of the following situations:
|
|
||||||
//
|
|
||||||
// 1. It is desired to mirror a domain name suffix (bit.suffix) to the .bit TLD.
|
|
||||||
// Accordingly, bit.suffix is delegated to one or more servers each running this daemon.
|
|
||||||
//
|
|
||||||
// 2. It is desired to act as an authoritative server for the .bit TLD directly.
|
|
||||||
// For example, a recursive DNS resolver is configured to override the root zone and use
|
|
||||||
// a server running this daemon for .bit. Or .bit is added to the root zone (when pigs fly).
|
|
||||||
//
|
|
||||||
// If the Unbound recursive DNS resolver were used:
|
|
||||||
// unbound.conf:
|
|
||||||
// server:
|
|
||||||
// stub-zone:
|
|
||||||
// name: bit
|
|
||||||
// stub-addr: 127.0.0.1@1153
|
|
||||||
//
|
|
||||||
// This daemon currently requires namecoind or a compatible daemon running with JSON-RPC interface.
|
|
||||||
// The name_* API calls are used to obtain .bit domain information.
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cfg := ServerConfig {}
|
|
||||||
config := config.Configurator{
|
|
||||||
ProgramName: "ncdns",
|
|
||||||
ConfigFilePaths: []string { "etc/ncdns.conf", "/etc/ncdns/ncdns.conf", },
|
|
||||||
}
|
|
||||||
config.ParseFatal(&cfg)
|
|
||||||
s := NewServer(&cfg)
|
|
||||||
s.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(cfg *ServerConfig) *Server {
|
|
||||||
s := &Server{}
|
|
||||||
s.cfg = *cfg
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) loadKey(fn, privateFn string) (k *dns.DNSKEY, privatek dns.PrivateKey, err error) {
|
|
||||||
f, err := os.Open(fn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rr, err := dns.ReadRR(f, fn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
k, ok := rr.(*dns.DNSKEY)
|
|
||||||
if !ok {
|
|
||||||
err = fmt.Errorf("Loaded record from key file, but it wasn't a DNSKEY")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
privatef, err := os.Open(privateFn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
privatek, err = k.ReadPrivateKey(privatef, privateFn)
|
|
||||||
log.Fatale(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Run() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
s.mux = dns.NewServeMux()
|
|
||||||
s.mux.HandleFunc(".", s.handle)
|
|
||||||
|
|
||||||
// key setup
|
|
||||||
s.ksk, s.kskPrivate, err = s.loadKey(s.cfg.PublicKey, s.cfg.PrivateKey)
|
|
||||||
log.Fatale(err, "error reading KSK key")
|
|
||||||
|
|
||||||
if s.cfg.ZonePublicKey != "" {
|
|
||||||
s.zsk, s.zskPrivate, err = s.loadKey(s.cfg.ZonePublicKey, s.cfg.ZonePrivateKey)
|
|
||||||
log.Fatale(err, "error reading ZSK key")
|
|
||||||
} else {
|
|
||||||
s.zsk = &dns.DNSKEY{}
|
|
||||||
s.zsk.Hdr.Rrtype = dns.TypeDNSKEY
|
|
||||||
s.zsk.Hdr.Class = dns.ClassINET
|
|
||||||
s.zsk.Hdr.Ttl = 3600
|
|
||||||
s.zsk.Algorithm = dns.RSASHA256
|
|
||||||
s.zsk.Protocol = 3
|
|
||||||
s.zsk.Flags = dns.ZONE
|
|
||||||
|
|
||||||
s.zskPrivate, err = s.zsk.Generate(2048)
|
|
||||||
log.Fatale(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bcfg := &backend.Config {
|
|
||||||
RPCUsername: s.cfg.NamecoinRPCUsername,
|
|
||||||
RPCPassword: s.cfg.NamecoinRPCPassword,
|
|
||||||
RPCAddress: s.cfg.NamecoinRPCAddress,
|
|
||||||
CacheMaxEntries: s.cfg.CacheMaxEntries,
|
|
||||||
SelfName: s.cfg.SelfName,
|
|
||||||
SelfIP: s.cfg.SelfIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.b, err = backend.New(bcfg)
|
|
||||||
log.Fatale(err)
|
|
||||||
|
|
||||||
// run
|
|
||||||
s.udpListener = s.runListener("udp")
|
|
||||||
s.tcpListener = s.runListener("tcp")
|
|
||||||
|
|
||||||
log.Info("Ready.")
|
|
||||||
|
|
||||||
// wait
|
|
||||||
sig := make(chan os.Signal)
|
|
||||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
for {
|
|
||||||
s := <-sig
|
|
||||||
fmt.Printf("Signal %v received, stopping.", s)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
mux *dns.ServeMux
|
|
||||||
udpListener *dns.Server
|
|
||||||
tcpListener *dns.Server
|
|
||||||
ksk *dns.DNSKEY
|
|
||||||
kskPrivate dns.PrivateKey
|
|
||||||
zsk *dns.DNSKEY
|
|
||||||
zskPrivate dns.PrivateKey
|
|
||||||
cfg ServerConfig
|
|
||||||
b abstract.Backend
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerConfig struct {
|
|
||||||
Bind string `default:":53" usage:"Address to bind to (e.g. 0.0.0.0:53)"`
|
|
||||||
PublicKey string `default:"ncdns.key" usage:"Path to the DNSKEY KSK public key file"`
|
|
||||||
PrivateKey string `default:"ncdns.private" usage:"Path to the KSK's corresponding private key file"`
|
|
||||||
ZonePublicKey string `default:"" usage:"Path to the DNSKEY ZSK public key file; if one is not specified, a temporary one is generated on startup and used only for the duration of that process"`
|
|
||||||
ZonePrivateKey string `default:"" usage:"Path to the ZSK's corresponding private key file"`
|
|
||||||
|
|
||||||
NamecoinRPCUsername string `default:"" usage:"Namecoin RPC username"`
|
|
||||||
NamecoinRPCPassword string `default:"" usage:"Namecoin RPC password"`
|
|
||||||
NamecoinRPCAddress string `default:"localhost:8336" usage:"Namecoin RPC server address"`
|
|
||||||
CacheMaxEntries int `default:"1000" usage:"Maximum name cache entries"`
|
|
||||||
SelfIP string `default:"127.127.127.127" usage:"The canonical IP address for this service"`
|
|
||||||
SelfName string `default:"" usage:"Canonical name for this nameserver (default: autogenerated psuedo-hostname resolving to SelfIP; SelfIP is not used if this is set)"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) doRunListener(ds *dns.Server) {
|
|
||||||
err := ds.ListenAndServe()
|
|
||||||
log.Fatale(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) runListener(net string) *dns.Server {
|
|
||||||
ds := &dns.Server {
|
|
||||||
Addr: s.cfg.Bind,
|
|
||||||
Net: net,
|
|
||||||
Handler: s.mux,
|
|
||||||
}
|
|
||||||
go s.doRunListener(ds)
|
|
||||||
return ds
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tx struct {
|
|
||||||
req *dns.Msg
|
|
||||||
res *dns.Msg
|
|
||||||
qname string
|
|
||||||
qtype uint16
|
|
||||||
qclass uint16
|
|
||||||
s *Server
|
|
||||||
rcode int
|
|
||||||
|
|
||||||
typesAtQname map[uint16]struct{}
|
|
||||||
additionalQueue map[string]struct{}
|
|
||||||
soa *dns.SOA
|
|
||||||
delegationPoint string // domain name at which the selected delegation was found
|
|
||||||
|
|
||||||
// The query was made for the selected delegation's name.
|
|
||||||
// i.e., if a lookup a.b.c.d has been made, and b.c.d has been chosen as the
|
|
||||||
// closest available delegation to serve, this is false. Whereas if b.c.d is
|
|
||||||
// queried, this is true.
|
|
||||||
queryIsAtDelegationPoint bool
|
|
||||||
|
|
||||||
// Add a 'consolation SOA' to the Authority section?
|
|
||||||
// Usually set when there are no results. This has to be done later, because
|
|
||||||
// we add DNSKEYs (if requested) at a later time and need to be able to quash
|
|
||||||
// this at that time in case adding DNSKEYs means an answer has stopped being
|
|
||||||
// empty of results.
|
|
||||||
consolationSOA bool
|
|
||||||
|
|
||||||
// Don't NSEC for having no answers. Used for qtype==DS.
|
|
||||||
suppressNSEC bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handle(rw dns.ResponseWriter, reqMsg *dns.Msg) {
|
|
||||||
tx := Tx{}
|
|
||||||
tx.req = reqMsg
|
|
||||||
tx.res = &dns.Msg{}
|
|
||||||
tx.res.SetReply(tx.req)
|
|
||||||
tx.res.Authoritative = true
|
|
||||||
tx.res.Compress = true
|
|
||||||
tx.s = s
|
|
||||||
tx.rcode = 0
|
|
||||||
tx.typesAtQname = map[uint16]struct{}{}
|
|
||||||
tx.additionalQueue = map[string]struct{}{}
|
|
||||||
|
|
||||||
opt := tx.req.IsEdns0()
|
|
||||||
if opt != nil {
|
|
||||||
tx.res.Extra = append(tx.res.Extra, opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, q := range tx.req.Question {
|
|
||||||
tx.qname = strings.ToLower(q.Name)
|
|
||||||
tx.qtype = q.Qtype
|
|
||||||
tx.qclass = q.Qclass
|
|
||||||
|
|
||||||
if q.Qclass != dns.ClassINET && q.Qclass != dns.ClassANY {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tx.addAnswers()
|
|
||||||
if err != nil {
|
|
||||||
if err == ncerr.ErrNoResults {
|
|
||||||
tx.rcode = 0
|
|
||||||
} else if err == ncerr.ErrNoSuchDomain {
|
|
||||||
tx.rcode = dns.RcodeNameError
|
|
||||||
} else if tx.rcode == 0 {
|
|
||||||
log.Infoe(err, "Handler error, doing SERVFAIL")
|
|
||||||
tx.rcode = dns.RcodeServerFailure
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.res.SetRcode(tx.req, tx.rcode)
|
|
||||||
|
|
||||||
//log.Info("response: ", res.String())
|
|
||||||
err := rw.WriteMsg(tx.res)
|
|
||||||
log.Infoe(err, "Couldn't write response: " + tx.res.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) blookup(qname string) (rrs []dns.RR, err error) {
|
|
||||||
log.Info("blookup: ", qname)
|
|
||||||
rrs, err = tx.s.b.Lookup(qname)
|
|
||||||
if err == nil && len(rrs) == 0 {
|
|
||||||
err = ncerr.ErrNoResults
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func rrsetHasType(rrs []dns.RR, t uint16) dns.RR {
|
|
||||||
for i := range rrs {
|
|
||||||
if rrs[i].Header().Rrtype == t {
|
|
||||||
return rrs[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAnswers() error {
|
|
||||||
err := tx.addAnswersMain()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are at the zone apex...
|
|
||||||
if _, ok := tx.typesAtQname[dns.TypeSOA]; tx.soa != nil && ok {
|
|
||||||
// Add DNSKEYs.
|
|
||||||
if tx.istype(dns.TypeDNSKEY) {
|
|
||||||
tx.s.ksk.Hdr.Name = tx.soa.Hdr.Name
|
|
||||||
tx.s.zsk.Hdr.Name = tx.s.ksk.Hdr.Name
|
|
||||||
|
|
||||||
tx.res.Answer = append(tx.res.Answer, tx.s.ksk)
|
|
||||||
tx.res.Answer = append(tx.res.Answer, tx.s.zsk)
|
|
||||||
|
|
||||||
// cancel sending a consolation SOA since we're giving DNSKEY answers
|
|
||||||
tx.consolationSOA = false
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.typesAtQname[dns.TypeDNSKEY] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
if tx.consolationSOA && tx.soa != nil {
|
|
||||||
tx.res.Ns = append(tx.res.Ns, tx.soa)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.addNSEC()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.addAdditional()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.signResponse()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAnswersMain() error {
|
|
||||||
var soa *dns.SOA
|
|
||||||
var origq []dns.RR
|
|
||||||
var origerr error
|
|
||||||
var firsterr error
|
|
||||||
var nss []dns.RR
|
|
||||||
firstNSAtLen := -1
|
|
||||||
firstSOAAtLen := -1
|
|
||||||
|
|
||||||
// We have to find out the zone root by trying to find SOA for progressively shorter domain names.
|
|
||||||
norig := strings.TrimRight(tx.qname, ".")
|
|
||||||
n := norig
|
|
||||||
|
|
||||||
A:
|
|
||||||
for len(n) > 0 {
|
|
||||||
rrs, err := tx.blookup(n)
|
|
||||||
if len(n) == len(norig) { // keep track of the results for the original qname
|
|
||||||
origq = rrs
|
|
||||||
origerr = err
|
|
||||||
}
|
|
||||||
if err == nil { // success
|
|
||||||
for i := range rrs {
|
|
||||||
t := rrs[i].Header().Rrtype
|
|
||||||
switch t {
|
|
||||||
case dns.TypeSOA:
|
|
||||||
// found the apex of the closest zone for which we are authoritative
|
|
||||||
// We haven't found any nameservers at this point, so we can serve without worrying about delegations.
|
|
||||||
if soa == nil {
|
|
||||||
soa = rrs[i].(*dns.SOA)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have found a SOA record at this level. This is preferred over everything
|
|
||||||
// so we can break now.
|
|
||||||
if firstSOAAtLen < 0 {
|
|
||||||
firstSOAAtLen = len(n)
|
|
||||||
}
|
|
||||||
break A
|
|
||||||
|
|
||||||
case dns.TypeNS:
|
|
||||||
// found an NS on the path; we are not authoritative for this owner or anything under it
|
|
||||||
// We need to return Authority data regardless of the nature of the query.
|
|
||||||
nss = rrs
|
|
||||||
|
|
||||||
// There could also be a SOA record at this level that we haven't reached yet.
|
|
||||||
if firstNSAtLen < 0 {
|
|
||||||
firstNSAtLen = len(n)
|
|
||||||
|
|
||||||
tx.delegationPoint = dns.Fqdn(n)
|
|
||||||
log.Info("DELEGATION POINT: ", tx.delegationPoint)
|
|
||||||
|
|
||||||
if n == norig {
|
|
||||||
tx.queryIsAtDelegationPoint = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if firsterr == nil {
|
|
||||||
firsterr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
nidx := strings.Index(n, ".")
|
|
||||||
if nidx < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
n = n[nidx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if soa == nil {
|
|
||||||
// If we didn't even get a SOA at any point, we don't have any appropriate zone for this query.
|
|
||||||
return ncerr.ErrNotInZone
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.soa = soa
|
|
||||||
|
|
||||||
if firstSOAAtLen >= firstNSAtLen {
|
|
||||||
// We got a SOA and zero or more NSes at the same level; we're not a delegation.
|
|
||||||
return tx.addAnswersAuthoritative(origq, origerr)
|
|
||||||
} else {
|
|
||||||
// We have a delegation.
|
|
||||||
return tx.addAnswersDelegation(nss)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAnswersAuthoritative(rrs []dns.RR, origerr error) error {
|
|
||||||
log.Info("AUTHORITATIVE")
|
|
||||||
|
|
||||||
// A call to blookup either succeeds or fails.
|
|
||||||
//
|
|
||||||
// If it fails:
|
|
||||||
// ErrNotInZone -- you're looking fundamentally in the wrong place; if there is no other
|
|
||||||
// appropriate zone, fail with REFUSED
|
|
||||||
// ErrNoSuchDomain -- there are no records at this name of ANY type, nor are there at any
|
|
||||||
// direct or indirect descendant domain; fail with NXDOMAIN
|
|
||||||
// ErrNoResults -- There are no records of the given type of class. However, there are
|
|
||||||
// other records at the given domain and/or records at a direct or
|
|
||||||
// indirect descendant domain; NOERROR
|
|
||||||
// any other error -- SERVFAIL
|
|
||||||
//
|
|
||||||
// If it succeeds:
|
|
||||||
// If there are zero records, treat the response as ErrNoResults above. Otherwise, each record
|
|
||||||
// can be classified into one of the following categories:
|
|
||||||
//
|
|
||||||
// - A NS record not at the zone apex and thus not authoritative (handled in addAnswersDelegation)
|
|
||||||
//
|
|
||||||
// - A record not within the zone and thus not authoritative (glue records)
|
|
||||||
//
|
|
||||||
// - A CNAME record (must not be glue) (TODO: DNAME)
|
|
||||||
//
|
|
||||||
// - Any other record
|
|
||||||
if origerr != nil {
|
|
||||||
return origerr
|
|
||||||
}
|
|
||||||
|
|
||||||
cn := rrsetHasType(rrs, dns.TypeCNAME)
|
|
||||||
if cn != nil && !tx.istype(dns.TypeCNAME) {
|
|
||||||
// We have an alias.
|
|
||||||
// TODO: check that the CNAME record is actually in the zone and not some bizarro CNAME glue record
|
|
||||||
return tx.addAnswersCNAME(cn.(*dns.CNAME))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add every record which was requested.
|
|
||||||
for i := range rrs {
|
|
||||||
t := rrs[i].Header().Rrtype
|
|
||||||
if tx.istype(t) {
|
|
||||||
tx.res.Answer = append(tx.res.Answer, rrs[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep track of the types that really do exist here in case we have to NSEC.
|
|
||||||
tx.typesAtQname[t] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tx.res.Answer) == 0 {
|
|
||||||
// no matching records, hand out the SOA (done later, might be quashed)
|
|
||||||
tx.consolationSOA = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAnswersCNAME(cn *dns.CNAME) error {
|
|
||||||
tx.res.Answer = append(tx.res.Answer, cn)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAnswersDelegation(nss []dns.RR) error {
|
|
||||||
log.Info("DELEGATION")
|
|
||||||
|
|
||||||
if tx.qtype == dns.TypeDS /* don't use istype, must not match ANY */ &&
|
|
||||||
tx.queryIsAtDelegationPoint {
|
|
||||||
// If type DS was requested specifically (not ANY), we have to act like
|
|
||||||
// we're handling things authoritatively and hand out a consolation SOA
|
|
||||||
// record and NOT hand out NS records. These still go in the Authority
|
|
||||||
// section though.
|
|
||||||
//
|
|
||||||
// If a DS record exists, it's given; if one doesn't, an NSEC record is
|
|
||||||
// given.
|
|
||||||
added := false
|
|
||||||
for _, ns := range nss {
|
|
||||||
t := ns.Header().Rrtype
|
|
||||||
if t == dns.TypeDS {
|
|
||||||
added = true
|
|
||||||
tx.res.Answer = append(tx.res.Answer, ns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if added {
|
|
||||||
tx.suppressNSEC = true
|
|
||||||
} else {
|
|
||||||
tx.consolationSOA = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tx.res.Authoritative = false
|
|
||||||
|
|
||||||
// Note that this is not authoritative data and thus does not get signed.
|
|
||||||
for _, ns := range nss {
|
|
||||||
t := ns.Header().Rrtype
|
|
||||||
if t == dns.TypeNS || t == dns.TypeDS {
|
|
||||||
tx.res.Ns = append(tx.res.Ns, ns)
|
|
||||||
}
|
|
||||||
if t == dns.TypeNS {
|
|
||||||
ns_ := ns.(*dns.NS)
|
|
||||||
tx.queueAdditional(ns_.Ns)
|
|
||||||
}
|
|
||||||
if t == dns.TypeDS {
|
|
||||||
tx.suppressNSEC = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nonauthoritative NS records are still included in the NSEC extant types list
|
|
||||||
tx.typesAtQname[dns.TypeNS] = struct{}{}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) queueAdditional(name string) {
|
|
||||||
tx.additionalQueue[name] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addNSEC() error {
|
|
||||||
if !tx.useDNSSEC() || tx.suppressNSEC {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NSEC replies should be given in the following circumstances:
|
|
||||||
//
|
|
||||||
// - No ANSWER SECTION responses for type requested, qtype != DS
|
|
||||||
// - No ANSWER SECTION responses for type requested, qtype == DS
|
|
||||||
// - Wildcard, no data responses
|
|
||||||
// - Wildcard, data response
|
|
||||||
// - Name error response
|
|
||||||
// - Direct NSEC request
|
|
||||||
|
|
||||||
if len(tx.res.Answer) == 0 {
|
|
||||||
log.Info("adding NSEC3")
|
|
||||||
err := tx.addNSEC3RR()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addNSEC3RR() error {
|
|
||||||
// deny the name
|
|
||||||
err := tx.addNSEC3RRActual(tx.qname, tx.typesAtQname)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEVEVER.BIT.
|
|
||||||
// deny DEVEVER.BIT. (DS)
|
|
||||||
// deny *.BIT.
|
|
||||||
|
|
||||||
// deny the existence of a wildcard that could have served the name
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addNSEC3RRActual(name string, tset map[uint16]struct{}) error {
|
|
||||||
tbm := []uint16{}
|
|
||||||
for t, _ := range tset {
|
|
||||||
tbm = append(tbm, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(uint16Slice(tbm))
|
|
||||||
|
|
||||||
nsr1n := dns.HashName(tx.qname, dns.SHA1, 1, "8F")
|
|
||||||
nsr1nn := stepName(nsr1n)
|
|
||||||
nsr1 := &dns.NSEC3 {
|
|
||||||
Hdr: dns.RR_Header {
|
|
||||||
Name: dns.Fqdn(nsr1n + "." + tx.soa.Hdr.Name),
|
|
||||||
Rrtype: dns.TypeNSEC3,
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
Ttl: 600,
|
|
||||||
},
|
|
||||||
Hash: dns.SHA1,
|
|
||||||
Flags: 0,
|
|
||||||
Iterations: 1,
|
|
||||||
SaltLength: 1,
|
|
||||||
Salt: "8F",
|
|
||||||
HashLength: uint8(len(nsr1nn)),
|
|
||||||
NextDomain: nsr1nn,
|
|
||||||
TypeBitMap: tbm,
|
|
||||||
}
|
|
||||||
tx.res.Ns = append(tx.res.Ns, nsr1)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAdditional() error {
|
|
||||||
for aname := range tx.additionalQueue {
|
|
||||||
err := tx.addAdditionalItem(aname)
|
|
||||||
if err != nil {
|
|
||||||
// eat the error
|
|
||||||
//return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *Tx) addAdditionalItem(aname string) error {
|
|
||||||
log.Info("ADDITIONAL: ", aname)
|
|
||||||
rrs, err := tx.blookup(aname)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, rr := range rrs {
|
|
||||||
t := rr.Header().Rrtype
|
|
||||||
if t == dns.TypeA || t == dns.TypeAAAA {
|
|
||||||
tx.res.Extra = append(tx.res.Extra, rr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// © 2014 Hugo Landau <hlandau@devever.net> GPLv3 or later
|
|
@ -1,57 +0,0 @@
|
|||||||
// Error types for processing DNS requests.
|
|
||||||
package ncerr
|
|
||||||
|
|
||||||
import "github.com/miekg/dns"
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// An Error interface which allows an associated rcode to be queried.
|
|
||||||
type Error interface {
|
|
||||||
error
|
|
||||||
|
|
||||||
// Returns the rcode which this error should be represented as in the DNS protocol.
|
|
||||||
Rcode() int
|
|
||||||
}
|
|
||||||
|
|
||||||
type rerr struct {
|
|
||||||
error
|
|
||||||
e error
|
|
||||||
rcode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (re *rerr) Error() string {
|
|
||||||
return re.e.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (re *rerr) Rcode() int {
|
|
||||||
return re.rcode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to generate an Error which has a particular rcode. Otherwise like fmt.Errorf.
|
|
||||||
func Rerrorf(rcode int, fmts string, args ...interface{}) Error {
|
|
||||||
re := &rerr{}
|
|
||||||
re.e = fmt.Errorf(fmts, args...)
|
|
||||||
re.rcode = rcode
|
|
||||||
return re
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard errors.
|
|
||||||
|
|
||||||
// Represents NXDOMAIN. Used when the name requested lies within a zone for
|
|
||||||
// which this server is authoritative, but does not exist.
|
|
||||||
//
|
|
||||||
// Note that a name is considered to exist if there exist any records of any
|
|
||||||
// type at a name, even if those records were not requested or sent. A name is
|
|
||||||
// also considered to exist if there are any names under it.
|
|
||||||
//
|
|
||||||
// In other words, b.c should return NOERROR even if it has no records of any
|
|
||||||
// type if there is a record at a.b.c, or so on.
|
|
||||||
var ErrNoSuchDomain = Rerrorf(dns.RcodeNameError, "no such domain")
|
|
||||||
|
|
||||||
// Represents REFUSED, which we use when a request is received for a zone for
|
|
||||||
// which the server is not authoritative.
|
|
||||||
var ErrNotInZone = Rerrorf(dns.RcodeRefused, "domain not in zone")
|
|
||||||
|
|
||||||
// Represents NOERROR. This error is used when NXDOMAIN is not an appropriate
|
|
||||||
// response code, but no results were returned. (DNS also uses NOERROR when results
|
|
||||||
// are returned, but we return nil in that case.)
|
|
||||||
var ErrNoResults = Rerrorf(0, "no results")
|
|
@ -0,0 +1,153 @@
|
|||||||
|
package server
|
||||||
|
import "github.com/hlandau/madns"
|
||||||
|
import "github.com/hlandau/degoutils/log"
|
||||||
|
import "github.com/hlandau/ncdns/backend"
|
||||||
|
import "github.com/miekg/dns"
|
||||||
|
import "os"
|
||||||
|
import "fmt"
|
||||||
|
import "os/signal"
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
cfg ServerConfig
|
||||||
|
|
||||||
|
engine madns.Engine
|
||||||
|
|
||||||
|
mux *dns.ServeMux
|
||||||
|
udpListener *dns.Server
|
||||||
|
tcpListener *dns.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Bind string `default:":53" usage:"Address to bind to (e.g. 0.0.0.0:53)"`
|
||||||
|
PublicKey string `default:"ncdns.key" usage:"Path to the DNSKEY KSK public key file"`
|
||||||
|
PrivateKey string `default:"ncdns.private" usage:"Path to the KSK's corresponding private key file"`
|
||||||
|
ZonePublicKey string `default:"" usage:"Path to the DNSKEY ZSK public key file; if one is not specified, a temporary one is generated on startup and used only for the duration of that process"`
|
||||||
|
ZonePrivateKey string `default:"" usage:"Path to the ZSK's corresponding private key file"`
|
||||||
|
|
||||||
|
NamecoinRPCUsername string `default:"" usage:"Namecoin RPC username"`
|
||||||
|
NamecoinRPCPassword string `default:"" usage:"Namecoin RPC password"`
|
||||||
|
NamecoinRPCAddress string `default:"localhost:8336" usage:"Namecoin RPC server address"`
|
||||||
|
CacheMaxEntries int `default:"1000" usage:"Maximum name cache entries"`
|
||||||
|
SelfName string `default:"" usage:"Canonical name for this nameserver (default: autogenerated psuedo-hostname resolving to SelfIP; SelfIP is not used if this is set)"`
|
||||||
|
SelfIP string `default:"127.127.127.127" usage:"The canonical IP address for this service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(cfg *ServerConfig) (s *Server, err error) {
|
||||||
|
s = &Server{}
|
||||||
|
s.cfg = *cfg
|
||||||
|
|
||||||
|
bcfg := &backend.Config {
|
||||||
|
RPCUsername: cfg.NamecoinRPCUsername,
|
||||||
|
RPCPassword: cfg.NamecoinRPCPassword,
|
||||||
|
CacheMaxEntries: cfg.CacheMaxEntries,
|
||||||
|
SelfName: cfg.SelfName,
|
||||||
|
SelfIP: cfg.SelfIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := backend.New(bcfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// key setup
|
||||||
|
ksk, kskPrivate, err := s.loadKey(cfg.PublicKey, cfg.PrivateKey)
|
||||||
|
log.Fatale(err, "error reading KSK key")
|
||||||
|
|
||||||
|
var zsk *dns.DNSKEY
|
||||||
|
var zskPrivate dns.PrivateKey
|
||||||
|
|
||||||
|
if cfg.ZonePublicKey != "" {
|
||||||
|
zsk, zskPrivate, err = s.loadKey(cfg.ZonePublicKey, cfg.ZonePrivateKey)
|
||||||
|
log.Fatale(err, "error reading ZSK key")
|
||||||
|
} else {
|
||||||
|
zsk = &dns.DNSKEY{}
|
||||||
|
zsk.Hdr.Rrtype = dns.TypeDNSKEY
|
||||||
|
zsk.Hdr.Class = dns.ClassINET
|
||||||
|
zsk.Hdr.Ttl = 3600
|
||||||
|
zsk.Algorithm = dns.RSASHA256
|
||||||
|
zsk.Protocol = 3
|
||||||
|
zsk.Flags = dns.ZONE
|
||||||
|
|
||||||
|
zskPrivate, err = zsk.Generate(2048)
|
||||||
|
log.Fatale(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ecfg := &madns.EngineConfig {
|
||||||
|
Backend: b,
|
||||||
|
KSK: ksk,
|
||||||
|
KSKPrivate: kskPrivate,
|
||||||
|
ZSK: zsk,
|
||||||
|
ZSKPrivate: zskPrivate,
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := madns.NewEngine(ecfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.engine = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) loadKey(fn, privateFn string) (k *dns.DNSKEY, privatek dns.PrivateKey, err error) {
|
||||||
|
f, err := os.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rr, err := dns.ReadRR(f, fn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
k, ok := rr.(*dns.DNSKEY)
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("Loaded record from key file, but it wasn't a DNSKEY")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privatef, err := os.Open(privateFn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privatek, err = k.ReadPrivateKey(privatef, privateFn)
|
||||||
|
log.Fatale(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run() {
|
||||||
|
s.mux = dns.NewServeMux()
|
||||||
|
s.mux.Handle(".", s.engine)
|
||||||
|
|
||||||
|
s.udpListener = s.runListener("udp")
|
||||||
|
s.tcpListener = s.runListener("tcp")
|
||||||
|
|
||||||
|
log.Info("Ready.")
|
||||||
|
|
||||||
|
// wait
|
||||||
|
sig := make(chan os.Signal)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
for {
|
||||||
|
s := <-sig
|
||||||
|
fmt.Printf("Signal %v received, stopping.", s)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) doRunListener(ds *dns.Server) {
|
||||||
|
err := ds.ListenAndServe()
|
||||||
|
log.Fatale(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) runListener(net string) *dns.Server {
|
||||||
|
ds := &dns.Server {
|
||||||
|
Addr: s.cfg.Bind,
|
||||||
|
Net: net,
|
||||||
|
Handler: s.mux,
|
||||||
|
}
|
||||||
|
go s.doRunListener(ds)
|
||||||
|
return ds
|
||||||
|
}
|
@ -1,191 +0,0 @@
|
|||||||
package main
|
|
||||||
import "encoding/base32"
|
|
||||||
import "fmt"
|
|
||||||
import "github.com/miekg/dns"
|
|
||||||
import "github.com/hlandau/degoutils/log"
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Determines if a transaction should be considered to have the given query type.
|
|
||||||
// Returns true iff the query type was qtype or ANY.
|
|
||||||
func (tx *Tx) istype(qtype uint16) bool {
|
|
||||||
return tx.qtype == qtype || tx.qtype == dns.TypeANY
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is used in NSEC3 hash generation. A hash like ...decafbad has one added
|
|
||||||
// to it so that it becomes ...decafbae. This is needed because NSEC3's hashes
|
|
||||||
// are inclusive-exclusive (i.e. "[,)"), and we want a hash that covers only the
|
|
||||||
// name specified.
|
|
||||||
//
|
|
||||||
// Takes a hash in base32hex form.
|
|
||||||
func stepName(hashB32Hex string) string {
|
|
||||||
if len(hashB32Hex) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := base32.HexEncoding.DecodeString(hashB32Hex)
|
|
||||||
log.Panice(err, hashB32Hex)
|
|
||||||
|
|
||||||
for i := len(b)-1; i>=0; i-- {
|
|
||||||
b[i] += 1
|
|
||||||
if b[i] != 0 { // didn't rollover, don't need to continue
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base32.HexEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true iff a type should be covered by a RRSIG.
|
|
||||||
func shouldSignType(t uint16, isAuthoritySection bool) bool {
|
|
||||||
switch t {
|
|
||||||
case dns.TypeOPT:
|
|
||||||
return false
|
|
||||||
case dns.TypeNS:
|
|
||||||
return !isAuthoritySection
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true iff a client requested DNSSEC.
|
|
||||||
func (tx *Tx) useDNSSEC() bool {
|
|
||||||
opt := tx.req.IsEdns0()
|
|
||||||
if opt == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return opt.Do()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets an rcode for the response if there is no error rcode currently set for
|
|
||||||
// the response. The idea is to return the rcode corresponding to the first
|
|
||||||
// error which occurs.
|
|
||||||
func (tx *Tx) setRcode(x int) {
|
|
||||||
if tx.rcode == 0 {
|
|
||||||
tx.rcode = x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Determines the maximum TTL for a slice of resource records.
|
|
||||||
// Returns 0 if the slice is empty.
|
|
||||||
func rraMaxTTL(rra []dns.RR) uint32 {
|
|
||||||
x := uint32(0)
|
|
||||||
for _, rr := range rra {
|
|
||||||
ttl := rr.Header().Ttl
|
|
||||||
if ttl > x {
|
|
||||||
x = ttl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used by signResponseSection.
|
|
||||||
func (tx *Tx) signRRs(rra []dns.RR, useKSK bool) (dns.RR, error) {
|
|
||||||
if len(rra) == 0 {
|
|
||||||
return nil, fmt.Errorf("no RRs to such")
|
|
||||||
}
|
|
||||||
|
|
||||||
maxttl := rraMaxTTL(rra)
|
|
||||||
exp := time.Duration(maxttl)*time.Second + time.Duration(10)*time.Minute
|
|
||||||
|
|
||||||
log.Info("maxttl: ", maxttl, " expiration: ", exp)
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
rrsig := &dns.RRSIG {
|
|
||||||
Hdr: dns.RR_Header { Ttl: maxttl, },
|
|
||||||
Algorithm: dns.RSASHA256,
|
|
||||||
Expiration: uint32(now.Add(exp).Unix()),
|
|
||||||
Inception: uint32(now.Add(time.Duration(-10)*time.Minute).Unix()),
|
|
||||||
SignerName: dns.Fqdn(tx.soa.Hdr.Name),
|
|
||||||
}
|
|
||||||
pk := tx.s.zskPrivate
|
|
||||||
if useKSK {
|
|
||||||
pk = tx.s.kskPrivate
|
|
||||||
rrsig.KeyTag = tx.s.ksk.KeyTag()
|
|
||||||
} else {
|
|
||||||
rrsig.KeyTag = tx.s.zsk.KeyTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
err := rrsig.Sign(pk, rra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rrsig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used by signResponse.
|
|
||||||
func (tx *Tx) signResponseSection(rra *[]dns.RR) error {
|
|
||||||
if len(*rra) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
//log.Info("sign section: ", *rra)
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
a := []dns.RR{}
|
|
||||||
pt := (*rra)[0].Header().Rrtype
|
|
||||||
t := uint16(0)
|
|
||||||
|
|
||||||
origrra := *rra
|
|
||||||
|
|
||||||
for i < len(origrra) {
|
|
||||||
for i < len(origrra) {
|
|
||||||
t = (*rra)[i].Header().Rrtype
|
|
||||||
if t != pt {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
a = append(a, origrra[i])
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldSignType(pt, (rra == &tx.res.Ns) ) {
|
|
||||||
useKSK := (pt == dns.TypeDNSKEY)
|
|
||||||
if useKSK {
|
|
||||||
srr, err := tx.signRRs(a, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*rra = append(*rra, srr)
|
|
||||||
}
|
|
||||||
|
|
||||||
srr, err := tx.signRRs(a, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*rra = append(*rra, srr)
|
|
||||||
}
|
|
||||||
|
|
||||||
pt = t
|
|
||||||
a = []dns.RR{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is called to append RRSIGs to the response based on the current records in the Answer and
|
|
||||||
// Authority sections of the response. Records in the Additional section are not signed.
|
|
||||||
func (tx *Tx) signResponse() error {
|
|
||||||
if !tx.useDNSSEC() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range []*[]dns.RR { &tx.res.Answer, &tx.res.Ns, /*&tx.res.Extra*/ } {
|
|
||||||
err := tx.signResponseSection(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Infoe(err, "fail signResponse")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("done signResponse")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for sorting RRTYPE lists for encoding into type bit maps.
|
|
||||||
type uint16Slice []uint16
|
|
||||||
func (p uint16Slice) Len() int { return len(p) }
|
|
||||||
func (p uint16Slice) Less(i, j int) bool { return p[i] < p[j] }
|
|
||||||
func (p uint16Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
Loading…
Reference in New Issue