package backend import "github.com/miekg/dns" import "github.com/golang/groupcache/lru" import "gopkg.in/hlandau/madns.v1/merr" import "github.com/hlandau/ncdns/namecoin" import "github.com/hlandau/ncdns/util" import "github.com/hlandau/ncdns/ncdomain" import "github.com/hlandau/xlog" import "sync" import "fmt" import "net" import "net/mail" import "strings" import "time" // Provides an abstract zone file for the Namecoin .bit TLD. type Backend struct { //s *Server nc namecoin.Conn cache lru.Cache // items are of type *Domain cacheMutex sync.Mutex cfg Config } const defaultMaxEntries = 100 var log, Log = xlog.New("ncdns.backend") // Backend configuration. type Config struct { NamecoinConn namecoin.Conn // Username and password to use for connecting to the Namecoin JSON-RPC interface. //RPCUsername string //RPCPassword string // hostname:port to use for connecting to the Namecoin JSON-RPC interface. //RPCAddress string // Maximum entries to permit in name cache. If zero, a default value is used. CacheMaxEntries int // Nameservers to advertise at zone apex. The first is considered the primary. // If empty, a psuedo-hostname resolvable to SelfIP is used. CanonicalNameservers []string // Vanity IPs to place at the zone apex. VanityIPs []net.IP // Used only if CanonicalNameservers is left blank. An IP which the internal // psuedo-hostname should resolve to. This should be the public IP of the // nameserver serving the zone expressed by this backend. SelfIP string // Hostmaster in e. mail form (e.g. "hostmaster@example.com"). Hostmaster string // Map names (like "d/example") to strings containing JSON values. Used to provide // fake names for testing purposes. You don't need to use this. FakeNames map[string]string } // Creates a new Namecoin backend. func New(cfg *Config) (backend *Backend, err error) { b := &Backend{} b.cfg = *cfg b.nc = b.cfg.NamecoinConn //b.nc.Username = cfg.RPCUsername //b.nc.Password = cfg.RPCPassword //b.nc.Server = cfg.RPCAddress b.cache.MaxEntries = cfg.CacheMaxEntries if b.cache.MaxEntries == 0 { b.cache.MaxEntries = defaultMaxEntries } hostmaster, err := convertEmail(b.cfg.Hostmaster) if err != nil { return } b.cfg.Hostmaster = hostmaster backend = b return } func convertEmail(email string) (string, error) { if email == "" { return ".", nil } if util.ValidateHostName(email) { return email, nil } addr, err := mail.ParseAddress(email) if err != nil { return "", err } email = addr.Address parts := strings.SplitN(email, "@", 2) if len(parts) < 2 { return "", fmt.Errorf("invalid e. mail address specified") } return dns.Fqdn(parts[0] + "." + parts[1]), nil } // Do low-level queries against an abstract zone file. This is the per-query // entrypoint from madns. func (b *Backend) Lookup(qname string) (rrs []dns.RR, err error) { btx := &btx{} btx.b = b btx.qname = qname return btx.Do() } // Things to keep track of while processing a query. type btx struct { b *Backend qname string subname, basename, rootname string } func (tx *btx) Do() (rrs []dns.RR, err error) { // Split the domain up. 'rootname' is the TLD and everything after it, // basename is the name directly before that, and subname is every name // before that. So "a.b.example.bit.suffix.xyz." would have a subname // of "a.b", a basename of "example" and a rootname of "bit.suffix.xyz". tx.subname, tx.basename, tx.rootname, err = tx.determineDomain() if err != nil { // We get an error if '.bit.' does not appear anywhere. In that case // we're not authoritative for the query in question and error out. return } if tx.rootname == "" { // REFUSED return nil, merr.ErrNotInZone } // If subname and basename are "", this means the query was for a root name // such as "bit." or "bit.suffix.xyz." directly. Serve SOA, NS records, etc. // as requested. if tx.subname == "" && tx.basename == "" { return tx.doRootDomain() } // Where ncdns has not been configured with a hostname to identify itself by, // it generates one under a special meta domain "x--nmc". This domain is not // a valid Namecoin domain name, so it does not confict with the Namecoin // domain name namespace. if tx.basename == "x--nmc" && len(tx.b.cfg.CanonicalNameservers) == 0 { return tx.doMetaDomain() } // If we have reached this point the query must be a normal user query. rrs, err = tx.doUserDomain() return } func (tx *btx) determineDomain() (subname, basename, rootname string, err error) { return util.SplitDomainByFloatingAnchor(tx.qname, "bit") } func (tx *btx) doRootDomain() (rrs []dns.RR, err error) { nss := tx.b.cfg.CanonicalNameservers if len(tx.b.cfg.CanonicalNameservers) == 0 { nss = []string{dns.Fqdn("this.x--nmc." + tx.rootname)} } soa := &dns.SOA{ Hdr: dns.RR_Header{ Name: dns.Fqdn(tx.rootname), Ttl: 86400, Class: dns.ClassINET, Rrtype: dns.TypeSOA, }, Ns: nss[0], Mbox: tx.b.cfg.Hostmaster, Serial: 1, Refresh: 600, Retry: 600, Expire: 7200, Minttl: 600, } rrs = make([]dns.RR, 0, 1+len(nss)+len(tx.b.cfg.VanityIPs)) rrs = append(rrs, soa) for _, cn := range nss { ns := &dns.NS{ Hdr: dns.RR_Header{ Name: dns.Fqdn(tx.rootname), Ttl: 86400, Class: dns.ClassINET, Rrtype: dns.TypeNS, }, Ns: dns.Fqdn(cn), } rrs = append(rrs, ns) } for _, ip := range tx.b.cfg.VanityIPs { if ip.To4() != nil { a := &dns.A{ Hdr: dns.RR_Header{ Name: dns.Fqdn(tx.rootname), Ttl: 86400, Class: dns.ClassINET, Rrtype: dns.TypeA, }, A: ip, } rrs = append(rrs, a) } else { a := &dns.AAAA{ Hdr: dns.RR_Header{ Name: dns.Fqdn(tx.rootname), Ttl: 86400, Class: dns.ClassINET, Rrtype: dns.TypeAAAA, }, AAAA: ip, } rrs = append(rrs, a) } } return } func (tx *btx) doMetaDomain() (rrs []dns.RR, err error) { ip := net.ParseIP(tx.b.cfg.SelfIP) if ip == nil || ip.To4() == nil { return nil, fmt.Errorf("invalid value specified for SelfIP") } switch tx.subname { case "this": rrs = []dns.RR{ &dns.A{ Hdr: dns.RR_Header{ Name: dns.Fqdn("this." + tx.basename + "." + tx.rootname), Ttl: 86400, Class: dns.ClassINET, Rrtype: dns.TypeA, }, A: ip, }, } default: } return } func (tx *btx) doUserDomain() (rrs []dns.RR, err error) { ncname, err := util.BasenameToNamecoinKey(tx.basename) if err != nil { return } d, err := tx.b.getNamecoinEntry(ncname) if err != nil { return nil, err } rrs, err = tx.doUnderDomain(d) if err != nil { return nil, err } return rrs, nil } // Keep domains in parsed format. type domain struct { ncv *ncdomain.Value } func (b *Backend) getNamecoinEntry(name string) (*domain, error) { d := b.getNamecoinEntryCache(name) if d != nil { return d, nil } d, err := b.getNamecoinEntryLL(name) if err != nil { return nil, err } b.addNamecoinEntryToCache(name, d) return d, nil } func (b *Backend) getNamecoinEntryCache(name string) *domain { b.cacheMutex.Lock() defer b.cacheMutex.Unlock() if dd, ok := b.cache.Get(name); ok { d := dd.(*domain) return d } return nil } func (b *Backend) addNamecoinEntryToCache(name string, d *domain) { b.cacheMutex.Lock() defer b.cacheMutex.Unlock() b.cache.Add(name, d) } func (b *Backend) getNamecoinEntryLL(name string) (*domain, error) { v, err := b.resolveName(name) if err != nil { return nil, err } d, err := b.jsonToDomain(name, v) if err != nil { return nil, err } return d, nil } func (b *Backend) resolveName(name string) (jsonValue string, err error) { if fv, ok := b.cfg.FakeNames[name]; ok { if fv == "NX" { return "", merr.ErrNoSuchDomain } return fv, nil } // The btcjson package has quite a long timeout, far in excess of standard // DNS timeouts. We need to return an error response rapidly if we can't // query the backend. Be generous with the timeout as responses from the // Namecoin JSON-RPC seem sluggish sometimes. result := make(chan struct{}, 1) go func() { jsonValue, err = b.nc.Query(name) log.Errore(err, "failed to query namecoin") result <- struct{}{} }() select { case <-result: return case <-time.After(1500 * time.Millisecond): return "", fmt.Errorf("timeout") } } func (b *Backend) jsonToDomain(name, jsonValue string) (*domain, error) { d := &domain{} v := ncdomain.ParseValue(name, jsonValue, b.resolveExtraName, nil) if v == nil { return nil, fmt.Errorf("couldn't parse value") } d.ncv = v return d, nil } func (b *Backend) resolveExtraName(name string) (jsonValue string, err error) { return b.resolveName(name) } func (tx *btx) doUnderDomain(d *domain) (rrs []dns.RR, err error) { rrs, err = tx.addAnswersUnderNCValue(d.ncv, tx.subname) if err == merr.ErrNoResults { err = nil } return } func (tx *btx) addAnswersUnderNCValue(rncv *ncdomain.Value, subname string) (rrs []dns.RR, err error) { ncv, sn, err := tx.findNCValue(rncv, subname, nil /*hasNS*/) if err != nil { return } return tx.addAnswersUnderNCValueActual(ncv, sn) } func (tx *btx) findNCValue(ncv *ncdomain.Value, subname string, shortCircuitFunc func(curNCV *ncdomain.Value) bool) (xncv *ncdomain.Value, sn string, err error) { return tx._findNCValue(ncv, subname, "", 0, shortCircuitFunc) } func (tx *btx) _findNCValue(ncv *ncdomain.Value, isubname, subname string, depth int, shortCircuitFunc func(curNCV *ncdomain.Value) bool) (xncv *ncdomain.Value, sn string, err error) { if shortCircuitFunc != nil && shortCircuitFunc(ncv) { return ncv, subname, nil } if isubname != "" { head, rest := util.SplitDomainHead(isubname) sub, ok := ncv.Map[head] if !ok { sub, ok = ncv.Map["*"] if !ok { return nil, "", merr.ErrNoSuchDomain } } return tx._findNCValue(sub, rest, head+"."+subname, depth+1, shortCircuitFunc) } if shortCircuitFunc != nil { return nil, subname, merr.ErrNoSuchDomain } return ncv, subname, nil } func (tx *btx) addAnswersUnderNCValueActual(ncv *ncdomain.Value, sn string) (rrs []dns.RR, err error) { rrs, err = ncv.RRs(nil, dns.Fqdn(tx.qname), dns.Fqdn(tx.basename+"."+tx.rootname)) return } // a.b.c.d.e.f.g.zzz.bit // f("a.b.c.d.e.f.g", "zzz.bit") // f[g]("a.b.c.d.e.f", "g.zzz.bit") // f[f]("a.b.c.d.e", "f.g.zzz.bit") // f[e]("a.b.c.d", "e.f.g.zzz.bit") // f[d]("a.b.c", "d.e.f.g.zzz.bit") // f[c]("a.b", "c.d.e.f.g.zzz.bit") // f[b]("a", "b.c.d.e.f.g.zzz.bit") // f[a]("", "a.b.c.d.e.f.g.zzz.bit") // © 2014 Hugo Landau GPLv3 or later