From db0f0b6bbba652eaff6226abafa1434c8fe6af90 Mon Sep 17 00:00:00 2001 From: Hugo Landau Date: Wed, 22 Oct 2014 16:14:16 +0100 Subject: [PATCH] major refactoring --- abstract/abstract.go | 16 -- backend/backend.go | 31 ++- main.go | 17 ++ namecoin/namecoin.go | 4 +- ncdns.go | 618 ------------------------------------------- ncerr/ncerr.go | 57 ---- server/server.go | 153 +++++++++++ txutil.go | 191 ------------- 8 files changed, 187 insertions(+), 900 deletions(-) delete mode 100644 abstract/abstract.go create mode 100644 main.go delete mode 100644 ncdns.go delete mode 100644 ncerr/ncerr.go create mode 100644 server/server.go delete mode 100644 txutil.go diff --git a/abstract/abstract.go b/abstract/abstract.go deleted file mode 100644 index 8730c5d..0000000 --- a/abstract/abstract.go +++ /dev/null @@ -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) -} diff --git a/backend/backend.go b/backend/backend.go index 0709fd3..17b6638 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -9,11 +9,10 @@ import "fmt" import "strings" import "net" import "github.com/hlandau/ncdns/namecoin" -import "github.com/hlandau/ncdns/ncerr" +import "github.com/hlandau/madns/merr" import "github.com/hlandau/ncdns/util" -import "github.com/hlandau/ncdns/abstract" -type ncBackend struct { +type Backend struct { //s *Server nc namecoin.NamecoinConn cache lru.Cache // items are of type *Domain @@ -46,8 +45,8 @@ type Config struct { } // Creates a new Namecoin backend. -func New(cfg *Config) (backend abstract.Backend, err error) { - b := &ncBackend{} +func New(cfg *Config) (backend *Backend, err error) { + b := &Backend{} b.cfg = *cfg b.nc.Username = cfg.RPCUsername @@ -85,7 +84,7 @@ func toNamecoinName(basename string) (string, error) { return "d/" + basename, nil } -func (b *ncBackend) getNamecoinEntry(name string) (*domain, error) { +func (b *Backend) getNamecoinEntry(name string) (*domain, error) { if dd, ok := b.cache.Get(name); ok { d := dd.(*domain) return d, nil @@ -100,7 +99,7 @@ func (b *ncBackend) getNamecoinEntry(name string) (*domain, error) { return d, nil } -func (b *ncBackend) getNamecoinEntryLL(name string) (*domain, error) { +func (b *Backend) getNamecoinEntryLL(name string) (*domain, error) { v, err := b.nc.Query(name) if err != nil { log.Infoe(err, "namecoin query failed: ", err) @@ -118,7 +117,7 @@ func (b *ncBackend) getNamecoinEntryLL(name string) (*domain, error) { return d, nil } -func (b *ncBackend) jsonToDomain(v string) (dd *domain, err error) { +func (b *Backend) jsonToDomain(v string) (dd *domain, err error) { d := &domain{} ncv := &ncValue{} @@ -135,7 +134,7 @@ func (b *ncBackend) jsonToDomain(v string) (dd *domain, err error) { } type btx struct { - b *ncBackend + b *Backend qname string subname, basename, rootname string @@ -147,7 +146,7 @@ func (tx *btx) determineDomain() (subname, basename, rootname string, err error) parts := strings.Split(qname, ".") if len(parts) < 2 { if parts[0] != "bit" { - err = ncerr.ErrNotInZone + err = merr.ErrNotInZone return } @@ -172,7 +171,7 @@ func (tx *btx) determineDomain() (subname, basename, rootname string, err error) } } - err = ncerr.ErrNotInZone + err = merr.ErrNotInZone return } @@ -187,7 +186,7 @@ func (tx *btx) Do() (rrs []dns.RR, err error) { if tx.rootname == "" { // REFUSED - return nil, ncerr.ErrNotInZone + return nil, merr.ErrNotInZone } if tx.subname == "" && tx.basename == "" { @@ -294,7 +293,7 @@ func (tx *btx) doUserDomain() (rrs []dns.RR, err error) { func (tx *btx) doUnderDomain(d *domain) (rrs []dns.RR, err error) { rrs, err = tx.addAnswersUnderNCValue(d.ncv, tx.subname) - if err == ncerr.ErrNoResults { + if err == merr.ErrNoResults { err = nil } @@ -337,14 +336,14 @@ func (tx *btx) _findNCValue(ncv *ncValue, isubname, subname string, depth int, if !ok { sub, ok = ncv.Map["*"] if !ok { - return nil, "", ncerr.ErrNoSuchDomain + return nil, "", merr.ErrNoSuchDomain } } return tx._findNCValue(sub, rest, head + "." + subname, depth+1, shortCircuitFunc) } if shortCircuitFunc != nil { - return nil, subname, ncerr.ErrNoSuchDomain + return nil, subname, merr.ErrNoSuchDomain } return ncv, subname, nil @@ -571,7 +570,7 @@ func (ncv *ncValue) GetDSs() (dss []dns.DS, err error) { // f[a]("", "a.b.c.d.e.f.g.zzz.bit") // Do low-level queries against an abstract zone file. -func (b *ncBackend) Lookup(qname string) (rrs []dns.RR, err error) { +func (b *Backend) Lookup(qname string) (rrs []dns.RR, err error) { btx := &btx{} btx.b = b btx.qname = qname diff --git a/main.go b/main.go new file mode 100644 index 0000000..670b23f --- /dev/null +++ b/main.go @@ -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() +} diff --git a/namecoin/namecoin.go b/namecoin/namecoin.go index ea4e593..7a3dce9 100644 --- a/namecoin/namecoin.go +++ b/namecoin/namecoin.go @@ -2,7 +2,7 @@ package namecoin // btcjson had to be modified a bit to get correct error reporting. import "github.com/hlandauf/btcjson" -import "github.com/hlandau/ncdns/ncerr" +import "github.com/hlandau/madns/merr" import "github.com/hlandau/ncdns/namecoin/extratypes" import "sync/atomic" @@ -58,7 +58,7 @@ func (nc *NamecoinConn) Query(name string) (v string, err error) { if r.Error != nil { //log.Info("RPC error: ", r.Error) if r.Error.Code == -4 { - return "", ncerr.ErrNoSuchDomain + return "", merr.ErrNoSuchDomain } return "", r.Error } diff --git a/ncdns.go b/ncdns.go deleted file mode 100644 index 6f36f15..0000000 --- a/ncdns.go +++ /dev/null @@ -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 GPLv3 or later diff --git a/ncerr/ncerr.go b/ncerr/ncerr.go deleted file mode 100644 index 6fb30dd..0000000 --- a/ncerr/ncerr.go +++ /dev/null @@ -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") diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..5121b8e --- /dev/null +++ b/server/server.go @@ -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 +} diff --git a/txutil.go b/txutil.go deleted file mode 100644 index 75cc250..0000000 --- a/txutil.go +++ /dev/null @@ -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] }