mirror of https://github.com/namecoin/ncdns
basic functionality complete
commit
fff64d57a6
@ -0,0 +1,102 @@
|
||||
package main
|
||||
import "github.com/conformal/btcjson"
|
||||
import "encoding/json"
|
||||
import "sync/atomic"
|
||||
import "fmt"
|
||||
|
||||
var idCounter int32 = 0
|
||||
|
||||
func newID() int32 {
|
||||
return atomic.AddInt32(&idCounter, 1)
|
||||
}
|
||||
|
||||
type NameShowCmd struct {
|
||||
id interface{}
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewNameShowCmd(id interface{}, name string) (*NameShowCmd, error) {
|
||||
return &NameShowCmd {
|
||||
id: id,
|
||||
Name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *NameShowCmd) Id() interface{} {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *NameShowCmd) Method() string {
|
||||
return "name_show"
|
||||
}
|
||||
|
||||
func (c *NameShowCmd) MarshalJSON() ([]byte, error) {
|
||||
params := []interface{}{
|
||||
c.Name,
|
||||
}
|
||||
|
||||
raw, err := btcjson.NewRawCmd(c.id, c.Method(), params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
func (c *NameShowCmd) UnmarshalJSON(b []byte) error {
|
||||
// We don't need to implement this as we are only ever the client.
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
type NameShowReply struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
func replyParser(m json.RawMessage) (interface{}, error) {
|
||||
nsr := &NameShowReply{}
|
||||
err := json.Unmarshal(m, nsr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nsr, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
btcjson.RegisterCustomCmd("name_show", nil, replyParser, "name_show <name>")
|
||||
}
|
||||
|
||||
type NamecoinConn struct {
|
||||
Username string
|
||||
Password string
|
||||
Server string
|
||||
}
|
||||
|
||||
func (nc *NamecoinConn) Query(name string) (v string, err error) {
|
||||
cmd, err := NewNameShowCmd(newID(), name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
r, err := btcjson.RpcSend(nc.Username, nc.Password, nc.Server, cmd)
|
||||
if err != nil {
|
||||
if e, ok := err.(*btcjson.Error); ok {
|
||||
if e.Code == -4 {
|
||||
return "", ErrNoSuchDomain
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if r.Result == nil {
|
||||
return "", fmt.Errorf("got nil result")
|
||||
}
|
||||
|
||||
if nsr, ok := r.Result.(*NameShowReply); ok {
|
||||
return nsr.Value, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("bad reply")
|
||||
}
|
||||
}
|
@ -0,0 +1,484 @@
|
||||
package main
|
||||
import "github.com/miekg/dns"
|
||||
import "github.com/hlandau/degoutils/log"
|
||||
import "os/signal"
|
||||
import "os"
|
||||
import "syscall"
|
||||
import "fmt"
|
||||
import "github.com/golang/groupcache/lru"
|
||||
import "encoding/json"
|
||||
import "strings"
|
||||
import "net"
|
||||
import "time"
|
||||
|
||||
//import "crypto/rsa"
|
||||
//import "crypto/rand"
|
||||
//import "math/big"
|
||||
|
||||
// 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.
|
||||
|
||||
type Server struct {
|
||||
mux *dns.ServeMux
|
||||
udpListener *dns.Server
|
||||
tcpListener *dns.Server
|
||||
nc NamecoinConn
|
||||
cache lru.Cache // items are of type *Domain
|
||||
ksk *dns.DNSKEY
|
||||
kskPrivate dns.PrivateKey
|
||||
zsk dns.DNSKEY
|
||||
zskPrivate dns.PrivateKey
|
||||
}
|
||||
|
||||
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: ":1153",
|
||||
Net: net,
|
||||
Handler: s.mux,
|
||||
}
|
||||
go s.doRunListener(ds)
|
||||
return ds
|
||||
}
|
||||
|
||||
// Keep domains in DNS format.
|
||||
type Domain struct {
|
||||
ncv *ncValue
|
||||
}
|
||||
|
||||
// Root of a domain JSON structure
|
||||
type ncValue struct {
|
||||
IP interface{} `json:"ip"`
|
||||
IP6 interface{} `json:"ip6"`
|
||||
Service [][]interface{} `json:"service"`
|
||||
Alias string `json:"alias"`
|
||||
NS interface{} `json:"ns"`
|
||||
Map map[string]*ncValue `json:"map"` // may contain "" and "*"
|
||||
}
|
||||
|
||||
func (s *Server) getNamecoinEntry(name string) (*Domain, error) {
|
||||
if dd, ok := s.cache.Get(name); ok {
|
||||
d := dd.(*Domain)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
d, err := s.getNamecoinEntryLL(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.cache.Add(name, d)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (s *Server) getNamecoinEntryLL(name string) (*Domain, error) {
|
||||
v, err := s.nc.Query(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d, err := s.jsonToDomain(v)
|
||||
if err != nil {
|
||||
log.Infoe(err, "cannot convert JSON to domain")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (s *Server) jsonToDomain(v string) (dd *Domain, err error) {
|
||||
d := &Domain{}
|
||||
ncv := &ncValue{}
|
||||
|
||||
err = json.Unmarshal([]byte(v), ncv)
|
||||
if err != nil {
|
||||
log.Infoe(err, fmt.Sprintf("cannot unmarshal JSON: %+v", v))
|
||||
return
|
||||
}
|
||||
|
||||
d.ncv = ncv
|
||||
|
||||
dd = d
|
||||
return
|
||||
}
|
||||
|
||||
func toNamecoinName(basename string) (string, error) {
|
||||
return "d/" + basename, nil
|
||||
}
|
||||
|
||||
func splitDomainHead(name string) (head string, rest string, err error) {
|
||||
parts := strings.Split(name, ".")
|
||||
|
||||
head = parts[len(parts)-1]
|
||||
|
||||
if len(parts) >= 2 {
|
||||
rest = strings.Join(parts[0:len(parts)-1], ".")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ncv *ncValue) getArray(a interface{}) (ips []string, err error) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ipa, ok := a.([]interface{})
|
||||
if ok {
|
||||
for _, v := range ipa {
|
||||
s, ok := v.(string)
|
||||
if ok {
|
||||
ips = append(ips, s)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s, ok := ncv.IP.(string)
|
||||
if ok {
|
||||
ips = []string{s}
|
||||
} else {
|
||||
err = fmt.Errorf("malformed IP value")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ncv *ncValue) GetIPs() (ips []string, err error) {
|
||||
return ncv.getArray(ncv.IP)
|
||||
}
|
||||
|
||||
func (ncv *ncValue) GetIP6s() (ips []string, err error) {
|
||||
return ncv.getArray(ncv.IP6)
|
||||
}
|
||||
|
||||
func (ncv *ncValue) GetNSs() (nss []string, err error) {
|
||||
return ncv.getArray(ncv.NS)
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
var ErrNoSuchDomain = fmt.Errorf("no such domain")
|
||||
|
||||
func (s *Server) addAnswersUnderNCValue(ncv *ncValue, subname, basename, rootname string, qtype uint16, res *dns.Msg) error {
|
||||
if subname != "" {
|
||||
head, rest, err := splitDomainHead(subname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sub, ok := ncv.Map[head]
|
||||
if !ok {
|
||||
sub, ok = ncv.Map["*"]
|
||||
if !ok {
|
||||
return ErrNoSuchDomain
|
||||
}
|
||||
}
|
||||
return s.addAnswersUnderNCValue(sub, rest, head + "." + basename, rootname, qtype, res)
|
||||
}
|
||||
|
||||
toAdd := []dns.RR{}
|
||||
|
||||
switch qtype {
|
||||
case dns.TypeA:
|
||||
ips, err := ncv.GetIPs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
pip := net.ParseIP(ip)
|
||||
if pip == nil || pip.To4() == nil {
|
||||
continue
|
||||
}
|
||||
toAdd = append(toAdd, &dns.A {
|
||||
Hdr: dns.RR_Header { Name: basename, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60, },
|
||||
A: pip })
|
||||
}
|
||||
|
||||
case dns.TypeAAAA:
|
||||
ips, err := ncv.GetIP6s()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
pip := net.ParseIP(ip)
|
||||
if pip == nil || pip.To4() != nil {
|
||||
continue
|
||||
}
|
||||
toAdd = append(toAdd, &dns.AAAA {
|
||||
Hdr: dns.RR_Header { Name: basename, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 60, },
|
||||
AAAA: pip })
|
||||
}
|
||||
|
||||
case dns.TypeNS:
|
||||
nss, err := ncv.GetNSs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ns := range nss {
|
||||
ns = strings.TrimRight(ns, ".") + "."
|
||||
toAdd = append(toAdd, &dns.NS {
|
||||
Hdr: dns.RR_Header { Name: basename, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 60, },
|
||||
Ns: ns })
|
||||
}
|
||||
|
||||
case dns.TypeTXT:
|
||||
// TODO
|
||||
case dns.TypeMX:
|
||||
// TODO
|
||||
case dns.TypeSRV:
|
||||
// TODO
|
||||
default:
|
||||
// ...
|
||||
}
|
||||
|
||||
if len(toAdd) == 0 {
|
||||
// we didn't get anything, so try the "" entry in the map
|
||||
if m, ok := ncv.Map[""]; ok {
|
||||
return s.addAnswersUnderNCValue(m, "", basename, rootname, qtype, res)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range toAdd {
|
||||
res.Answer = append(res.Answer, toAdd[i])
|
||||
}
|
||||
|
||||
if len(res.Answer) > 0 {
|
||||
now := time.Now()
|
||||
rrsig := &dns.RRSIG {
|
||||
Algorithm: dns.RSASHA256,
|
||||
Expiration: uint32(now.Add(time.Duration(5)*time.Minute).Unix()),
|
||||
Inception: uint32(now.Unix()),
|
||||
KeyTag: s.zsk.KeyTag(),
|
||||
SignerName: rootname + ".",
|
||||
}
|
||||
err := rrsig.Sign(s.zskPrivate, res.Answer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.Answer = append(res.Answer, rrsig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addAnswersUnderDomain(d *Domain, subname, basename, rootname string, qtype uint16, res *dns.Msg) error {
|
||||
return s.addAnswersUnderNCValue(d.ncv, subname, basename, rootname, qtype, res)
|
||||
}
|
||||
|
||||
func (s *Server) determineDomain(qname string) (subname, basename, rootname string, err error) {
|
||||
qname = strings.TrimRight(qname, ".")
|
||||
parts := strings.Split(qname, ".")
|
||||
if len(parts) < 2 {
|
||||
rootname = parts[0]
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(parts)-1; i >= 0; i-- {
|
||||
v := parts[i]
|
||||
|
||||
// scanning for rootname
|
||||
if v == "bit" {
|
||||
rootname = strings.Join(parts[i:len(parts)], ".")
|
||||
basename = parts[i-1]
|
||||
subname = strings.Join(parts[0:i-1], ".")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("not a namecoin domain")
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) addRootAnswers(rootname string, qtype uint16, res *dns.Msg) error {
|
||||
useKSK := false
|
||||
|
||||
s.zsk.Hdr.Name = rootname + "."
|
||||
|
||||
switch qtype {
|
||||
case dns.TypeDNSKEY:
|
||||
res.Answer = append(res.Answer, s.ksk)
|
||||
res.Answer = append(res.Answer, &s.zsk)
|
||||
useKSK = true
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
if len(res.Answer) > 0 {
|
||||
now := time.Now()
|
||||
rrsig := &dns.RRSIG {
|
||||
Algorithm: dns.RSASHA256,
|
||||
Expiration: uint32(now.Add(time.Duration(5)*time.Minute).Unix()),
|
||||
Inception: uint32(now.Unix()),
|
||||
KeyTag: s.zsk.KeyTag(),
|
||||
SignerName: rootname + ".",
|
||||
}
|
||||
pk := s.zskPrivate
|
||||
if useKSK {
|
||||
pk = s.kskPrivate
|
||||
rrsig.KeyTag = s.ksk.KeyTag()
|
||||
}
|
||||
|
||||
err := rrsig.Sign(pk, res.Answer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.Answer = append(res.Answer, rrsig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addAnswers(qname string, qtype uint16, res *dns.Msg) error {
|
||||
subname, basename, rootname, err := s.determineDomain(qname)
|
||||
if err != nil {
|
||||
log.Infoe(err, "cannot determine domain name")
|
||||
return err
|
||||
}
|
||||
//log.Info("DD: sub=", subname, " base=", basename, " root=", rootname)
|
||||
|
||||
if rootname == "" {
|
||||
return fmt.Errorf("invalid domain name, no root")
|
||||
}
|
||||
|
||||
if subname == "" && basename == "" {
|
||||
return s.addRootAnswers(rootname, qtype, res)
|
||||
}
|
||||
|
||||
ncname, err := toNamecoinName(basename)
|
||||
if err != nil {
|
||||
log.Infoe(err, "cannot determine namecoin name")
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := s.getNamecoinEntry(ncname)
|
||||
if err != nil {
|
||||
log.Infoe(err, "cannot get namecoin entry")
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.addAnswersUnderDomain(d, subname, basename + "." + rootname + ".", rootname, qtype, res)
|
||||
if err != nil {
|
||||
log.Infoe(err, "cannot add answers")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handle(rw dns.ResponseWriter, req *dns.Msg) {
|
||||
res := dns.Msg{}
|
||||
res.SetReply(req)
|
||||
res.Authoritative = true
|
||||
res.Compress = true
|
||||
|
||||
for _, q := range req.Question {
|
||||
if q.Qclass != dns.ClassINET && q.Qclass != dns.ClassANY {
|
||||
continue
|
||||
}
|
||||
|
||||
err := s.addAnswers(q.Name, q.Qtype, &res)
|
||||
if err == ErrNoSuchDomain {
|
||||
res.SetRcode(req, dns.RcodeNameError)
|
||||
break
|
||||
} else if err != nil {
|
||||
// TODO
|
||||
log.Infoe(err, "Could not determine answer for query, doing SERVFAIL.")
|
||||
res.SetRcode(req, dns.RcodeServerFailure)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err := rw.WriteMsg(&res)
|
||||
log.Infoe(err, "Couldn't write response: " + res.String())
|
||||
}
|
||||
|
||||
func (s *Server) Run() {
|
||||
s.mux = dns.NewServeMux()
|
||||
s.mux.HandleFunc(".", s.handle)
|
||||
|
||||
// key setup
|
||||
kskf, err := os.Open("Kbit.+008+04050.key")
|
||||
log.Fatale(err)
|
||||
|
||||
kskRR, err := dns.ReadRR(kskf, "Kbit.+008+04050.key")
|
||||
log.Fatale(err)
|
||||
|
||||
ksk, ok := kskRR.(*dns.DNSKEY)
|
||||
if !ok {
|
||||
log.Fatal("loaded record from key file, but it wasn't a DNSKEY")
|
||||
return
|
||||
}
|
||||
|
||||
s.ksk = ksk
|
||||
|
||||
kskPrivatef, err := os.Open("Kbit.+008+04050.private")
|
||||
log.Fatale(err)
|
||||
|
||||
s.kskPrivate, err = s.ksk.ReadPrivateKey(kskPrivatef, "Kbit.+008+04050.private")
|
||||
log.Fatale(err)
|
||||
|
||||
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)
|
||||
|
||||
// run
|
||||
s.udpListener = s.runListener("udp")
|
||||
s.tcpListener = s.runListener("tcp")
|
||||
s.nc.Username = "user"
|
||||
s.nc.Password = "password"
|
||||
s.nc.Server = "127.0.0.1:8336"
|
||||
s.cache.MaxEntries = 1000
|
||||
|
||||
// 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 main() {
|
||||
s := Server{}
|
||||
s.Run()
|
||||
}
|
Loading…
Reference in New Issue