瀏覽代碼

Merge branch 'master' into edns-subnet

Ask Bjørn Hansen 13 年之前
父節點
當前提交
81d8fb55ed
共有 10 個文件被更改,包括 265 次插入22 次删除
  1. 93 8
      README.md
  2. 20 2
      config.go
  3. 8 2
      config_test.go
  4. 7 1
      dns/example.com.json
  5. 48 0
      dns/test.example.com.json
  6. 1 1
      geodns.go
  7. 7 5
      serve.go
  8. 52 0
      serve_test.go
  9. 17 3
      service/run
  10. 12 0
      zone_test.go

+ 93 - 8
README.md

@@ -5,26 +5,111 @@ This is a (so far) experimental Golang implementation of the
 Pool](http://www.pool.ntp.org/) system.
 Pool](http://www.pool.ntp.org/) system.
 [![Build Status](https://secure.travis-ci.org/abh/geodns.png)](http://travis-ci.org/abh/geodns)
 [![Build Status](https://secure.travis-ci.org/abh/geodns.png)](http://travis-ci.org/abh/geodns)
 
 
-
 ## Installation
 ## Installation
 
 
-Run `go get` to install the Go dependencies. You will also need the
-GeoIP C library.
+If you already have go installed, just run `go get` to install the Go dependencies.
+
+You will also need the GeoIP C library, on RedHat derived systems
+that's `yum install geoip-devel`.
+
+If you don't have Go installed the easiest way to build geodns from source is to
+download Go from http://code.google.com/p/go/downloads/list and untar'ing it in
+`/usr/local/go` and then run the following from a regular user account:
+
+```sh
+export PATH=$PATH:/usr/local/go/bin
+export GOPATH=~/go
+go get github.com/abh/geodns
+cd ~/go/src/github.com/abh/geodns
+go test
+go build
+```
 
 
 ## Sample configuration
 ## Sample configuration
 
 
-Download some sample configuration files:
+There's a sample configuration file in `dns/example.com.json`. This is currently
+derived from the `test.example.com` data used for unit tests and not an example
+of a "best practices" configuration.
+
+For testing there's also a bigger test file at:
 
 
 ```sh
 ```sh
-mkdir dns
-curl -o dns/ntppool.org.json http://tmp.askask.com/2012/08/dns/ntppool.org.json.big
-curl -o dns/example.com.json http://tmp.askask.com/2012/08/dns/example.com.json
+mkdir -p dns
+curl -o dns/test.ntppool.org.json http://tmp.askask.com/2012/08/dns/ntppool.org.json.big
 ```
 ```
 
 
 ## Run it
 ## Run it
 
 
 `go run *.go -log -interface 127.1 -port 5053`
 `go run *.go -log -interface 127.1 -port 5053`
 
 
+or if you already built geodns, then `./geodns ...`.
+
 To test the responses run
 To test the responses run
 
 
-`dig -t a ntppool.org  @127.1 -p 5053`
+`dig -t a test.example.com @127.1 -p 5053`
+
+## WebSocket interface
+
+geodns runs a WebSocket server on port 8053 that outputs various performance
+metrics, see `monitor.go` for details.
+
+## Country and continent lookups
+
+## Weighted records
+
+Except for NS records all records can have a 'weight' assigned. If any records
+of a particular type for a particular name have a weight, the system will return
+the "max_hosts" records (default 2)
+
+## Configuration format
+
+In the configuration file the whole zone is a big hash (associative array). At the
+top level you can (optionally) set some options with the keys serial, ttl and max_hosts.
+
+The actual zone data (dns records) is in a hash under the key "data". The keys
+in the hash are hostnames and the value for each hostname is yet another hash
+where the keys are record types (lowercase) and the values an array of records.
+
+For example to setup an MX record at the zone apex and then have a different
+A record for users in Europe than anywhere else, use:
+
+    {
+        "serial": 1,
+        "data": {
+            "": { "mx": { "mx": "mail.example.com", "preference": 10 } },
+            "mail": { "a": [ ["192.168.0.1", 100], ["192.168.10.1", 50] ] },
+            "mail.europe": { "a": [ ["192.168.255.1", 0] ] },
+            "smtp": { "alias": "mail" }
+        }
+    }
+
+The configuration files are automatically reloaded when they're updated. If a file
+can't be read (invalid JSON, for example) the previous configuration for that zone
+will be kept.
+
+## Supported record types
+
+### A
+
+Each record has the format of a short array with the first element being the
+IP address and the second the weight.
+
+   { "a": [ [ "192.168.0.1", 10], ["192.168.2.1", 5] ] }
+
+### AAAA
+
+Same format as A records (except the record type is "aaaa").
+
+### CNAME
+
+### NS
+
+### MX
+
+   { "mx": "foo.example.com" }
+
+### Alias
+
+Internally resolved cname, of sorts. Only works internally in a zone.
+
+   { "alias": "foo" }

+ 20 - 2
config.go

@@ -147,6 +147,7 @@ func setupZoneData(data map[string]interface{}, Zone *Zone) {
 		"aaaa":  dns.TypeAAAA,
 		"aaaa":  dns.TypeAAAA,
 		"ns":    dns.TypeNS,
 		"ns":    dns.TypeNS,
 		"cname": dns.TypeCNAME,
 		"cname": dns.TypeCNAME,
+		"mx":    dns.TypeMX,
 		"alias": dns.TypeMF,
 		"alias": dns.TypeMF,
 	}
 	}
 
 
@@ -237,10 +238,8 @@ func setupZoneData(data map[string]interface{}, Zone *Zone) {
 						if err != nil {
 						if err != nil {
 							panic("Error converting weight to integer")
 							panic("Error converting weight to integer")
 						}
 						}
-						label.Weight[dnsType] += record.Weight
 					case float64:
 					case float64:
 						record.Weight = int(rec[1].(float64))
 						record.Weight = int(rec[1].(float64))
-						label.Weight[dnsType] += record.Weight
 					}
 					}
 					switch dnsType {
 					switch dnsType {
 					case dns.TypeA:
 					case dns.TypeA:
@@ -257,6 +256,24 @@ func setupZoneData(data map[string]interface{}, Zone *Zone) {
 						panic("Bad AAAA record")
 						panic("Bad AAAA record")
 					}
 					}
 
 
+				case dns.TypeMX:
+					rec := records[rType][i].(map[string]interface{})
+					pref := uint16(0)
+					mx := rec["mx"].(string)
+					if !strings.HasSuffix(mx, ".") {
+						mx = mx + "."
+					}
+					if rec["weight"] != nil {
+						record.Weight = valueToInt(rec["weight"])
+					}
+					if rec["preference"] != nil {
+						pref = uint16(valueToInt(rec["preference"]))
+					}
+					record.RR = &dns.RR_MX{
+						Hdr:        h,
+						Mx:         mx,
+						Preference: pref}
+
 				case dns.TypeCNAME:
 				case dns.TypeCNAME:
 					rec := records[rType][i]
 					rec := records[rType][i]
 					record.RR = &dns.RR_CNAME{Hdr: h, Target: dns.Fqdn(rec.(string))}
 					record.RR = &dns.RR_CNAME{Hdr: h, Target: dns.Fqdn(rec.(string))}
@@ -301,6 +318,7 @@ func setupZoneData(data map[string]interface{}, Zone *Zone) {
 					panic("record.RR is nil")
 					panic("record.RR is nil")
 				}
 				}
 
 
+				label.Weight[dnsType] += record.Weight
 				label.Records[dnsType][i] = *record
 				label.Records[dnsType][i] = *record
 			}
 			}
 			if label.Weight[dnsType] > 0 {
 			if label.Weight[dnsType] > 0 {

+ 8 - 2
config_test.go

@@ -17,7 +17,13 @@ var _ = Suite(&ConfigSuite{})
 func (s *ConfigSuite) TestReadConfigs(c *C) {
 func (s *ConfigSuite) TestReadConfigs(c *C) {
 	s.zones = make(Zones)
 	s.zones = make(Zones)
 	configReadDir("dns", s.zones)
 	configReadDir("dns", s.zones)
+
+	// Just check that example.com loaded, too.
 	c.Check(s.zones["example.com"].Origin, Equals, "example.com")
 	c.Check(s.zones["example.com"].Origin, Equals, "example.com")
-	c.Check(s.zones["example.com"].Options.MaxHosts, Equals, 2)
-	c.Check(s.zones["example.com"].Labels["weight"].MaxHosts, Equals, 1)
+
+	// The real tests are in test.example.com so we have a place
+	// to make nutty configuration entries
+	c.Check(s.zones["test.example.com"].Origin, Equals, "test.example.com")
+	c.Check(s.zones["test.example.com"].Options.MaxHosts, Equals, 2)
+	c.Check(s.zones["test.example.com"].Labels["weight"].MaxHosts, Equals, 1)
 }
 }

+ 7 - 1
dns/example.com.json

@@ -2,7 +2,13 @@
   "ttl":    600,
   "ttl":    600,
   "max_hosts": 2,
   "max_hosts": 2,
   "data" : {
   "data" : {
-    "":  { "ns": { "ns1.example.net.": null, "ns2.example.net.": null } },
+    "":  {
+      "ns": { "ns1.example.net.": null, "ns2.example.net.": null },
+      "mx": [ { "preference": 20, "mx": "mx2.example.net", "weight": 0 },
+              { "preference": 10, "mx": "mx.example.net.", "weight": 1 }
+            ]
+    },
+    "europe": { "mx": [ { "mx": "mx-eu.example.net" }]},
     "foo": { 
     "foo": { 
       "a": [ [ "192.168.1.2", 10 ], [ "192.168.1.3", 10 ], [ "192.168.1.4", 10 ] ],
       "a": [ [ "192.168.1.2", 10 ], [ "192.168.1.3", 10 ], [ "192.168.1.4", 10 ] ],
       "aaaa": [ ["fd06:c1d3:e902::2", 10], ["fd06:c1d3:e902:202:a5ff:fecd:13a6:a", 10], ["fd06:c1d3:e902::4", 10] ]
       "aaaa": [ ["fd06:c1d3:e902::2", 10], ["fd06:c1d3:e902:202:a5ff:fecd:13a6:a", 10], ["fd06:c1d3:e902::4", 10] ]

+ 48 - 0
dns/test.example.com.json

@@ -0,0 +1,48 @@
+{ "serial": 3,
+  "ttl":    600,
+  "max_hosts": 2,
+  "data" : {
+    "":  {
+      "ns": { "ns1.example.net.": null, "ns2.example.net.": null },
+      "mx": [ { "preference": 20, "mx": "mx2.example.net", "weight": 0 },
+              { "preference": 10, "mx": "mx.example.net.", "weight": 1 }
+            ]
+    },
+    "europe": { "mx": [ { "mx": "mx-eu.example.net" }]},
+    "foo": { 
+      "a": [ [ "192.168.1.2", 10 ], [ "192.168.1.3", 10 ], [ "192.168.1.4", 10 ] ],
+      "aaaa": [ ["fd06:c1d3:e902::2", 10], ["fd06:c1d3:e902:202:a5ff:fecd:13a6:a", 10], ["fd06:c1d3:e902::4", 10] ]
+    },
+    "weight": { 
+      "a": [ [ "192.168.1.2", 100 ], [ "192.168.1.3", 50 ], [ "192.168.1.4", 25 ] ],
+      "max_hosts": "1"
+    },
+    "bar": { 
+      "a": [ [ "192.168.1.2", 10 ] ],
+      "ttl": "601"
+    },
+    "bar.no": { "a": [] },
+    "0": {
+      "a": [ [ "192.168.0.1", 10 ] ]
+    },
+    "0-alias": {
+      "alias": "0"
+    },
+    "bar-alias": {
+      "alias": "bar"
+    },
+    "www-alias": {
+      "alias": "www"
+    },
+    "www": {
+      "cname": "geo.bitnames.com."
+    },
+    "cname-long-ttl": {
+      "cname": "geo.bitnames.com.",
+      "ttl": 86400
+    },
+    "cname-internal-referal": {
+      "cname": "bar"      
+    }
+  }
+}

+ 1 - 1
geodns.go

@@ -11,7 +11,7 @@ import (
 	"time"
 	"time"
 )
 )
 
 
-var VERSION string = "2.1.1"
+var VERSION string = "2.1.2"
 var gitVersion string
 var gitVersion string
 var serverId string
 var serverId string
 
 

+ 7 - 5
serve.go

@@ -25,7 +25,7 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 	qtype := req.Question[0].Qtype
 	qtype := req.Question[0].Qtype
 
 
 	logPrintf("[zone %s] incoming %s %s %d from %s\n", z.Origin, req.Question[0].Name,
 	logPrintf("[zone %s] incoming %s %s %d from %s\n", z.Origin, req.Question[0].Name,
-		dns.Rr_str[qtype], req.MsgHdr.Id, w.RemoteAddr())
+		dns.TypeToString[qtype], req.MsgHdr.Id, w.RemoteAddr())
 
 
 	// is this safe/atomic or does it need to go through a channel?
 	// is this safe/atomic or does it need to go through a channel?
 	qCounter++
 	qCounter++
@@ -94,7 +94,7 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 		if label == "_status" && (qtype == dns.TypeANY || qtype == dns.TypeTXT) {
 		if label == "_status" && (qtype == dns.TypeANY || qtype == dns.TypeTXT) {
 			m.Answer = statusRR(z)
 			m.Answer = statusRR(z)
 			m.Authoritative = true
 			m.Authoritative = true
-			w.Write(m)
+			w.WriteMsg(m)
 			return
 			return
 		}
 		}
 
 
@@ -112,7 +112,7 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 			}}
 			}}
 
 
 			m.Authoritative = true
 			m.Authoritative = true
-			w.Write(m)
+			w.WriteMsg(m)
 			return
 			return
 		}
 		}
 
 
@@ -122,7 +122,7 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 
 
 		m.Ns = []dns.RR{z.SoaRR()}
 		m.Ns = []dns.RR{z.SoaRR()}
 
 
-		w.Write(m)
+		w.WriteMsg(m)
 		return
 		return
 	}
 	}
 
 
@@ -141,6 +141,8 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 			if _, ok := labels.Records[dns.TypeCNAME]; ok {
 			if _, ok := labels.Records[dns.TypeCNAME]; ok {
 				cname := labels.firstRR(dns.TypeCNAME)
 				cname := labels.firstRR(dns.TypeCNAME)
 				m.Answer = append(m.Answer, cname)
 				m.Answer = append(m.Answer, cname)
+			} else {
+				m.Ns = append(m.Ns, z.SoaRR())
 			}
 			}
 		} else {
 		} else {
 			m.Ns = append(m.Ns, z.SoaRR())
 			m.Ns = append(m.Ns, z.SoaRR())
@@ -149,7 +151,7 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 
 
 	logPrintln(m)
 	logPrintln(m)
 
 
-	err := w.Write(m)
+	err := w.WriteMsg(m)
 	if err != nil {
 	if err != nil {
 		// if Pack'ing fails the Write fails. Return SERVFAIL.
 		// if Pack'ing fails the Write fails. Return SERVFAIL.
 		log.Println("Error writing packet", m)
 		log.Println("Error writing packet", m)

+ 52 - 0
serve_test.go

@@ -0,0 +1,52 @@
+package main
+
+import (
+	"github.com/miekg/dns"
+	. "launchpad.net/gocheck"
+	"strings"
+	"time"
+)
+
+func (s *ConfigSuite) TestServing(c *C) {
+
+	Zones := make(Zones)
+	setupPgeodnsZone(Zones)
+	go configReader("dns", Zones)
+	go listenAndServe(":8853", &Zones)
+
+	time.Sleep(100 * time.Millisecond)
+
+	r := exchange(c, "_status.pgeodns.", dns.TypeTXT)
+	txt := r.Answer[0].(*dns.RR_TXT).Txt[0]
+	if !strings.HasPrefix(txt, "{") {
+		c.Log("Unexpected result for _status.pgeodns", txt)
+		c.Fail()
+	}
+
+	r = exchange(c, "bar.test.example.com.", dns.TypeA)
+	ip := r.Answer[0].(*dns.RR_A).A
+	c.Check(ip.String(), Equals, "192.168.1.2")
+
+	r = exchange(c, "test.example.com.", dns.TypeSOA)
+	soa := r.Answer[0].(*dns.RR_SOA)
+	serial := soa.Serial
+	c.Check(int(serial), Equals, 3)
+
+	// no AAAA records for 'bar', so check we get a soa record back
+	r = exchange(c, "test.example.com.", dns.TypeAAAA)
+	soa2 := r.Ns[0].(*dns.RR_SOA)
+	c.Check(soa, DeepEquals, soa2)
+}
+
+func exchange(c *C, name string, dnstype uint16) *dns.Msg {
+	msg := new(dns.Msg)
+	cli := new(dns.Client)
+
+	msg.SetQuestion(name, dnstype)
+	r, _, err := cli.Exchange(msg, "127.0.0.1:8853")
+	if err != nil {
+		c.Log("err", err)
+		c.Fail()
+	}
+	return r
+}

+ 17 - 3
service/run

@@ -1,6 +1,20 @@
 #!/bin/sh
 #!/bin/sh
 exec 2>&1
 exec 2>&1
+sleep 1 # just in case we spin for some reason
+
 cd /opt/geodns
 cd /opt/geodns
-IP=`head -1 env/IP`
-CONFIG=`head -1 env/CONFIG`
-exec softlimit -d500000000 ./geodns --interface="$IP" --config="$CONFIG"
+
+INTERFACE=""
+if [ -e env/IP ]; then
+  IP=`head -1 env/IP`
+  if [ ! -z "$IP" ]; then
+    INTERFACE="--interface=$IP"
+  fi
+fi
+
+CONFIG=dns
+if [ -e env/CONFIG ]; then
+  CONFIG=`head -1 env/CONFIG`
+fi
+
+exec softlimit -d500000000 ./geodns $INTERFACE --config="$CONFIG"

+ 12 - 0
zone_test.go

@@ -12,4 +12,16 @@ func (s *ConfigSuite) TestZone(c *C) {
 	// Make sure that the empty "no.bar" zone gets skipped and "bar" is used
 	// Make sure that the empty "no.bar" zone gets skipped and "bar" is used
 	label := ex.findLabels("bar", "no", dns.TypeA)
 	label := ex.findLabels("bar", "no", dns.TypeA)
 	c.Check(label.Records[dns.TypeA], HasLen, 1)
 	c.Check(label.Records[dns.TypeA], HasLen, 1)
+	c.Check(label.Records[dns.TypeA][0].RR.(*dns.RR_A).A.String(), Equals, "192.168.1.2")
+
+	label = ex.findLabels("", "", dns.TypeMX)
+	Mxs := label.Records[dns.TypeMX]
+	c.Check(Mxs, HasLen, 2)
+	c.Check(Mxs[0].RR.(*dns.RR_MX).Mx, Equals, "mx.example.net.")
+	c.Check(Mxs[1].RR.(*dns.RR_MX).Mx, Equals, "mx2.example.net.")
+
+	Mxs = ex.findLabels("", "dk", dns.TypeMX).Records[dns.TypeMX]
+	c.Check(Mxs, HasLen, 1)
+	c.Check(Mxs[0].RR.(*dns.RR_MX).Mx, Equals, "mx-eu.example.net.")
+
 }
 }