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