Merge pull request #1065 from weaveworks/gossipdns

Gossip-based DNS embedded in the router.
This commit is contained in:
Alvaro committed 2015-07-14 18:30:16 +02:00
commit a1ac0313e5
56 files changed
+1470 -5758

No files matched your search

+6 -14
View File
@@ -1,4 +1,4 @@
PUBLISH=publish_weave publish_weavedns publish_weaveexec
PUBLISH=publish_weave publish_weaveexec
.DEFAULT: all
.PHONY: all update tests publish $(PUBLISH) clean prerequisites build travis run-smoketests
@@ -10,26 +10,23 @@ DOCKERHUB_USER=weaveworks
WEAVE_VERSION=git-$(shell git rev-parse --short=12 HEAD)
WEAVER_EXE=prog/weaver/weaver
WEAVEDNS_EXE=prog/weavedns/weavedns
WEAVEPROXY_EXE=prog/weaveproxy/weaveproxy
SIGPROXY_EXE=prog/sigproxy/sigproxy
WEAVEWAIT_EXE=prog/weavewait/weavewait
NETCHECK_EXE=prog/netcheck/netcheck
COVER_EXE=testing/cover/cover
EXES=$(WEAVER_EXE) $(WEAVEDNS_EXE) $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) $(NETCHECK_EXE) $(COVER_EXE)
EXES=$(WEAVER_EXE) $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) $(NETCHECK_EXE) $(COVER_EXE)
WEAVER_UPTODATE=.weaver.uptodate
WEAVEDNS_UPTODATE=.weavedns.uptodate
WEAVEEXEC_UPTODATE=.weaveexec.uptodate
IMAGES_UPTODATE=$(WEAVER_UPTODATE) $(WEAVEDNS_UPTODATE) $(WEAVEEXEC_UPTODATE)
IMAGES_UPTODATE=$(WEAVER_UPTODATE) $(WEAVEEXEC_UPTODATE)
WEAVER_IMAGE=$(DOCKERHUB_USER)/weave
WEAVEDNS_IMAGE=$(DOCKERHUB_USER)/weavedns
WEAVEEXEC_IMAGE=$(DOCKERHUB_USER)/weaveexec
IMAGES=$(WEAVER_IMAGE) $(WEAVEDNS_IMAGE) $(WEAVEEXEC_IMAGE)
IMAGES=$(WEAVER_IMAGE) $(WEAVEEXEC_IMAGE)
WEAVE_EXPORT=weave.tar
@@ -64,13 +61,12 @@ else
endif
$(NETGO_CHECK)
$(WEAVEDNS_EXE) $(WEAVEPROXY_EXE) $(NETCHECK_EXE): common/*.go common/*/*.go net/*.go
$(WEAVEPROXY_EXE) $(NETCHECK_EXE): common/*.go common/*/*.go net/*.go
go get -tags netgo ./$(@D)
go build -ldflags "-extldflags \"-static\" -X main.version $(WEAVE_VERSION)" -tags netgo -o $@ ./$(@D)
$(NETGO_CHECK)
$(WEAVER_EXE): router/*.go ipam/*.go ipam/*/*.go prog/weaver/main.go
$(WEAVEDNS_EXE): nameserver/*.go prog/weavedns/main.go
$(WEAVER_EXE): router/*.go ipam/*.go ipam/*/*.go nameserver/*.go prog/weaver/main.go
$(WEAVEPROXY_EXE): proxy/*.go prog/weaveproxy/main.go
$(NETCHECK_EXE): prog/netcheck/netcheck.go
@@ -88,10 +84,6 @@ $(WEAVER_UPTODATE): prog/weaver/Dockerfile $(WEAVER_EXE)
$(SUDO) docker build -t $(WEAVER_IMAGE) prog/weaver
touch $@
$(WEAVEDNS_UPTODATE): prog/weavedns/Dockerfile $(WEAVEDNS_EXE)
$(SUDO) docker build -t $(WEAVEDNS_IMAGE) prog/weavedns
touch $@
$(WEAVEEXEC_UPTODATE): prog/weaveexec/Dockerfile $(DOCKER_DISTRIB) weave $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) $(NETCHECK_EXE)
cp weave prog/weaveexec/weave
cp $(SIGPROXY_EXE) prog/weaveexec/sigproxy
-155
View File
@@ -1,155 +0,0 @@
package common
import (
"container/heap"
"math"
"time"
"sync/atomic"
"github.com/benbjohnson/clock"
)
// A callable is a function that is called and return an (optional) next scheduled time
type callable func() time.Time
type schedCall struct {
t time.Time
c callable
}
func (sc *schedCall) FromNow(now time.Time) time.Duration {
if sc.t.After(now) {
return time.Duration(sc.t.Sub(now).Nanoseconds())
}
return time.Duration(0)
}
////////////////////////////////////////////////////////////////////////////////
// A min-heap of scheduled callables
type schedCallsHeap []*schedCall
func (ch *schedCallsHeap) Len() int { return len(*ch) }
func (ch *schedCallsHeap) Less(i, j int) bool { return (*ch)[i].t.Before((*ch)[j].t) }
func (ch *schedCallsHeap) Swap(i, j int) {
(*ch)[i], (*ch)[j] = (*ch)[j], (*ch)[i]
}
func (ch *schedCallsHeap) Push(x interface{}) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
entry := x.(*schedCall)
*ch = append(*ch, entry)
}
func (ch *schedCallsHeap) Pop() interface{} {
old := *ch
n := len(old)
item := old[n-1]
*ch = old[0 : n-1]
return item
}
////////////////////////////////////////////////////////////////////////////////
// SchedQueue is queue of scheduled callables
type SchedQueue struct {
clock clock.Clock
callablesH schedCallsHeap
schedChan chan *schedCall
closeChan chan bool
counter uint64 // number of calls invoked so far (used for stats). Note: it will wrap.
}
// NewSchedQueue creates a new scheduled queue
func NewSchedQueue(clock clock.Clock) *SchedQueue {
cq := SchedQueue{
clock: clock,
schedChan: make(chan *schedCall),
closeChan: make(chan bool),
}
heap.Init(&cq.callablesH)
return &cq
}
// Start starts the scheduled queue
func (cq *SchedQueue) Start() {
go func() {
defer func() { Log.Debugf("[sched-q] Quitting (%d calls pending)", len(cq.callablesH)) }()
var now time.Time
timer := cq.clock.Timer(time.Duration(math.MaxInt64))
for {
// Wait until an insertion, until the next callable or until we are Stop()ed
select {
case ce := <-cq.schedChan:
heap.Push(&cq.callablesH, ce)
now = cq.clock.Now()
case <-cq.closeChan:
return
case now = <-timer.C:
}
timer.Stop()
durationUntilNext := cq.durationUntilNext(now)
for durationUntilNext == 0 {
sched := heap.Pop(&cq.callablesH).(*schedCall)
atomic.AddUint64(&cq.counter, 1)
schedNextTime := sched.c()
if !schedNextTime.IsZero() {
sched.t = schedNextTime
heap.Push(&cq.callablesH, sched) // TODO: use a Fix() instead of Pop()&Push()
}
now = cq.clock.Now()
durationUntilNext = cq.durationUntilNext(now)
}
timer = cq.clock.Timer(durationUntilNext)
}
}()
}
// Stop stops the scheduled queue
func (cq *SchedQueue) Stop() {
Log.Debugf("[sched-q] Stopping...")
cq.closeChan <- true
}
// Add schedules a call.
// The callable should not modify the scheduled queue in any way.
func (cq *SchedQueue) Add(c callable, t time.Time) {
Log.Debugf("[sched-q] Adding call at %s", t)
ce := schedCall{c: c, t: t}
cq.schedChan <- &ce
}
// Counter returns the number of calls invoked in the queued
// Note: the result will wrap over time.
func (cq *SchedQueue) Count() uint64 {
return atomic.LoadUint64(&cq.counter)
}
func (cq *SchedQueue) durationUntilNext(now time.Time) time.Duration {
if len(cq.callablesH) > 0 {
firstSched := cq.callablesH[0]
return firstSched.FromNow(now)
}
return time.Duration(math.MaxInt64)
}
// ensure that the background goroutine has executed any pending callbacks
func (cq *SchedQueue) Flush() {
ch := make(chan struct{})
flushFunc := func() time.Time {
atomic.AddUint64(&cq.counter, ^uint64(0)) // decrement so nobody sees this
close(ch)
return time.Time{}
}
cq.schedChan <- &schedCall{c: flushFunc, t: time.Time{}}
<-ch
}
-92
View File
@@ -1,92 +0,0 @@
package common
import (
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/stretchr/testify/require"
)
// Ensure we can add new calls while forwarding the clock
func TestSchedCallsBasic(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestSchedCallsBasic starting")
const testSecs = 1000
clk := clock.NewMock()
schedQueue := NewSchedQueue(clk)
schedQueue.Start()
defer schedQueue.Stop()
c := func() time.Time {
return clk.Now().Add(time.Second)
}
schedQueue.Add(c, clk.Now().Add(time.Second))
schedQueue.Flush()
for i := 0; i < testSecs; i++ {
clk.Add(time.Second)
schedQueue.Flush()
}
t.Logf("Now: %s - calls: %d", clk.Now(), schedQueue.Count())
require.Equal(t, testSecs, (int)(schedQueue.Count()), "Number of calls")
}
// Ensure we can create a 100 seconds gap in the middle of the time travel
func TestSchedCallsGap(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestSchedCallsGap starting")
const testSecs = 1000
clk := clock.NewMock()
schedQueue := NewSchedQueue(clk)
schedQueue.Start()
defer schedQueue.Stop()
c2 := func() time.Time {
if schedQueue.Count() == testSecs/2 {
return clk.Now().Add(time.Duration(100) * time.Second)
}
return clk.Now().Add(time.Second)
}
schedQueue.Add(c2, clk.Now().Add(time.Second))
schedQueue.Flush()
for i := 0; i < testSecs; i++ {
clk.Add(time.Second)
schedQueue.Flush()
}
t.Logf("Now: %s - calls: %d", clk.Now(), schedQueue.Count())
require.Equal(t, testSecs-100+1, (int)(schedQueue.Count()), "Number of calls")
}
func TestSchedCallsStop(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestSchedCallsStop starting")
const testSecs = 1000
clk := clock.NewMock()
schedQueue := NewSchedQueue(clk)
schedQueue.Start()
defer schedQueue.Stop()
c2 := func() time.Time {
if schedQueue.Count() == testSecs/2 {
return time.Time{}
}
return clk.Now().Add(time.Second)
}
schedQueue.Add(c2, clk.Now().Add(time.Second))
schedQueue.Flush()
for i := 0; i < testSecs; i++ {
clk.Add(time.Second)
schedQueue.Flush()
}
t.Logf("Now: %s - calls: %d", clk.Now(), schedQueue.Count())
require.Equal(t, testSecs/2, (int)(schedQueue.Count()), "Number of calls")
}
-130
View File
@@ -1,130 +0,0 @@
package nameserver
import (
"bytes"
"fmt"
"net"
)
// A zone record
//
// Note: all the zone records information is concentrated here. Maybe
// not all queries will use things like priorities or weights, but we
// do not want to create a hierarchy for dealing with all possible
// queries...
type ZoneRecord interface {
Name() string // The name for this IP
IP() net.IP // The IP (v4) address
Priority() int // The priority
Weight() int // The weight
TTL() int // TTL
}
type ZoneLookup interface {
// Lookup for a name
LookupName(name string) ([]ZoneRecord, error)
// Lookup for an address
LookupInaddr(inaddr string) ([]ZoneRecord, error)
}
type ZoneLookupFunc func(target string) ([]ZoneRecord, error)
type ZoneObservable interface {
// Observe anything that affects a particular name in the zone
ObserveName(name string, observer ZoneRecordObserver) error
// Observe anything that affects a particular IP in the zone
ObserveInaddr(inaddr string, observer ZoneRecordObserver) error
}
type ZoneObserverFunc func(string, ZoneRecordObserver) error
type ZoneRecordObserver func()
/////////////////////////////////////////////////////////////
// A basic record struct (satisfying the ZoneRecord interface)
type Record struct {
name string
ip net.IP
priority int
weight int
ttl int
}
func (r Record) Name() string { return r.name }
func (r Record) IP() net.IP { return r.ip }
func (r Record) Priority() int { return r.priority }
func (r Record) Weight() int { return r.weight }
func (r Record) TTL() int { return r.ttl }
func (r Record) String() string {
var buf bytes.Buffer
if len(r.Name()) > 0 {
fmt.Fprintf(&buf, "%s", r.Name())
}
if !r.IP().IsUnspecified() {
fmt.Fprintf(&buf, "[%s]", r.IP())
}
if r.Priority() > 0 {
fmt.Fprintf(&buf, "/P:%d", r.Priority())
}
if r.Weight() > 0 {
fmt.Fprintf(&buf, "/W:%d", r.Weight())
}
if r.TTL() > 0 {
fmt.Fprintf(&buf, "/TTL:%d", r.TTL())
}
return buf.String()
}
/////////////////////////////////////////////////////////////
func newParseError(reason string, addr string) *net.ParseError {
return &net.ParseError{Type: reason, Text: addr}
}
// simple IPv4 type that can be used as a key in a map (in contrast with net.IP), used for sets of IPs, etc...
type IPv4 [4]byte
func (ip IPv4) toNetIP() net.IP { return net.IPv4(ip[0], ip[1], ip[2], ip[3]) }
func (ip IPv4) String() string { return ip.toNetIP().String() }
func ipToIPv4(nip net.IP) IPv4 { ip := nip.To4(); return IPv4([4]byte{ip[0], ip[1], ip[2], ip[3]}) }
// Parse an address (eg, "10.13.12.11") and return the corresponding IPv4 (eg, "10.13.12.11")
func addrToIPv4(addr string) (IPv4, error) {
ip := net.ParseIP(addr)
if ip == nil {
return IPv4{}, newParseError("IP address", addr)
}
return ipToIPv4(ip), nil
}
// Parse a reverse address (eg, "11.12.13.10.in-addr.arpa.") and return the corresponding IPv4 (eg, "10.13.12.11")
func raddrToIPv4(addr string) (IPv4, error) {
l := len(addr)
if l < rdnsDomainLen+1 {
return IPv4{}, newParseError("too short reverse IP address", addr)
}
suffixLen := l - rdnsDomainLen
suffix := addr[suffixLen+1:]
if suffix != RDNSDomain {
return IPv4{}, newParseError("suffix of reverse IP address", addr)
}
ipStr := addr[:suffixLen]
revIP := net.ParseIP(ipStr)
if revIP == nil {
return IPv4{}, newParseError("reverse IP address", addr)
}
revIP4 := revIP.To4()
return IPv4([4]byte{revIP4[3], revIP4[2], revIP4[1], revIP4[0]}), nil
}
func raddrToIP(addr string) (net.IP, error) {
r, err := raddrToIPv4(addr)
if err != nil {
return net.IP{}, err
}
return r.toNetIP(), nil
}
-32
View File
@@ -1,32 +0,0 @@
package nameserver
import (
"net"
"testing"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
func TestAddrs(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestAddrs starting")
ip, err := addrToIPv4("10.13.12.11")
require.NoError(t, err)
require.True(t, net.ParseIP("10.13.12.11").Equal(ip.toNetIP()), "IP")
ip, err = raddrToIPv4("11.12.13.10.in-addr.arpa.")
require.NoError(t, err)
require.True(t, net.ParseIP("10.13.12.11").Equal(ip.toNetIP()), "IP")
// some malformed addresses
ip, err = addrToIPv4("10.13.12")
require.True(t, err != nil, "when parsing malformed address")
ip, err = addrToIPv4("10.13.AA.12")
require.True(t, err != nil, "when parsing malformed address")
ip, err = raddrToIPv4("11.12.13.10.in-axxx.arpa.")
require.True(t, err != nil, "when parsing malformed address")
ip, err = raddrToIPv4("11.12.13.10.in-addr")
require.True(t, err != nil, "when parsing malformed address")
}
-382
View File
@@ -1,382 +0,0 @@
package nameserver
import (
"bytes"
"container/heap"
"errors"
"fmt"
"math"
"sync"
"time"
"github.com/benbjohnson/clock"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
)
var (
errInvalidCapacity = errors.New("Invalid cache capacity")
errCouldNotResolve = errors.New("Could not resolve")
errTimeout = errors.New("Timeout while waiting for resolution")
errNoLocalReplies = errors.New("No local replies")
)
const (
defPendingTimeout int = 5 // timeout for a resolution
)
const nullTTL = 0 // a null TTL
type entryStatus uint8
const (
stPending entryStatus = iota // someone is waiting for the resolution
stResolved entryStatus = iota // resolved
)
var statusToString = map[entryStatus]string{
stPending: "pending",
stResolved: "resolved",
}
const (
CacheNoLocalReplies uint8 = 1 << iota // not found in local network (stored in the cache so we skip another local lookup or some time)
)
// a cache entry
type cacheEntry struct {
Status entryStatus // status of the entry
Flags uint8 // some extra flags
ReplyLen int
question dns.Question
protocol dnsProtocol
reply dns.Msg
validUntil time.Time // obtained from the reply and stored here for convenience/speed
putTime time.Time
index int // for fast lookups in the heap
}
func newCacheEntry(question *dns.Question, now time.Time) *cacheEntry {
e := &cacheEntry{
Status: stPending,
validUntil: now.Add(time.Second * time.Duration(defPendingTimeout)),
question: *question,
index: -1,
}
return e
}
// Get a copy of the reply stored in the entry, but with some values adjusted like the TTL
func (e *cacheEntry) getReply(request *dns.Msg, maxLen int, now time.Time) (*dns.Msg, error) {
if e.Status != stResolved {
return nil, nil
}
// if the reply has expired or is invalid, force the caller to start a new resolution
if e.hasExpired(now) {
return nil, nil
}
if e.Flags&CacheNoLocalReplies != 0 {
return nil, errNoLocalReplies
}
if e.ReplyLen >= maxLen {
Log.Debugf("[cache msgid %d] returning truncated reponse: %d > %d", request.MsgHdr.Id, e.ReplyLen, maxLen)
return makeTruncatedReply(request), nil
}
// create a copy of the reply, with values for this particular query
reply := e.reply.Copy()
reply.SetReply(request)
// adjust the TTLs
passedSecs := uint32(now.Sub(e.putTime).Seconds())
for _, rr := range reply.Answer {
hdr := rr.Header()
ttl := hdr.Ttl
if passedSecs < ttl {
hdr.Ttl = ttl - passedSecs
} else {
return nil, nil // it is expired: do not spend more time and return nil...
}
}
reply.Rcode = e.reply.Rcode
reply.Authoritative = true
return reply, nil
}
func (e cacheEntry) hasExpired(now time.Time) bool {
return e.validUntil.Before(now) || e.validUntil == now
}
// set the reply for the entry
// returns True if the entry has changed the validUntil time
func (e *cacheEntry) setReply(reply *dns.Msg, ttl int, flags uint8, now time.Time) bool {
var prevValidUntil time.Time
if e.Status == stResolved {
if reply != nil {
Log.Debugf("[cache msgid %d] replacing response in cache", reply.MsgHdr.Id)
}
prevValidUntil = e.validUntil
}
// make sure we do not overwrite noLocalReplies entries
if flags&CacheNoLocalReplies != 0 {
if e.Flags&CacheNoLocalReplies != 0 {
return false
}
}
if ttl != nullTTL {
e.validUntil = now.Add(time.Second * time.Duration(ttl))
} else if reply != nil {
// calculate the validUntil from the reply TTL
var minTTL uint32 = math.MaxUint32
for _, rr := range reply.Answer {
ttl := rr.Header().Ttl
if ttl < minTTL {
minTTL = ttl // TODO: improve the minTTL calculation (maybe we should skip some RRs)
}
}
e.validUntil = now.Add(time.Second * time.Duration(minTTL))
} else {
Log.Warningf("[cache] no valid TTL could be calculated")
}
e.Status = stResolved
e.Flags = flags
e.putTime = now
if reply != nil {
e.reply = *reply.Copy()
e.ReplyLen = reply.Len()
}
return (prevValidUntil != e.validUntil)
}
func (e *cacheEntry) String() string {
var buf bytes.Buffer
q := e.question
fmt.Fprintf(&buf, "'%s'[%s]: ", q.Name, dns.TypeToString[q.Qtype])
if e.Flags&CacheNoLocalReplies != 0 {
fmt.Fprintf(&buf, "neg-local")
} else {
fmt.Fprintf(&buf, "%s", statusToString[e.Status])
}
fmt.Fprintf(&buf, "(%d bytes)", e.ReplyLen)
return buf.String()
}
//////////////////////////////////////////////////////////////////////////////////////
// An entriesPtrHeap is a min-heap of cache entries.
type entriesPtrsHeap []*cacheEntry
func (h entriesPtrsHeap) Len() int { return len(h) }
func (h entriesPtrsHeap) Less(i, j int) bool { return h[i].validUntil.Before(h[j].validUntil) }
func (h entriesPtrsHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
h[i].index = i
h[j].index = j
}
func (h *entriesPtrsHeap) Push(x interface{}) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
n := len(*h)
entry := x.(*cacheEntry)
entry.index = n
*h = append(*h, entry)
}
func (h *entriesPtrsHeap) Pop() interface{} {
old := *h
n := len(old)
item := old[n-1]
item.index = -1 // for safety
*h = old[0 : n-1]
return item
}
//////////////////////////////////////////////////////////////////////////////////////
// The cache interface
type ZoneCache interface {
// Get from the cache
Get(request *dns.Msg, maxLen int) (reply *dns.Msg, err error)
// Put in the cache
Put(request *dns.Msg, reply *dns.Msg, ttl int, flags uint8) int
// Remove from the cache
Remove(question *dns.Question)
// Purge all expired entries from the cache
Purge()
// Remove all entries
Clear()
// Return the cache length
Len() int
// Return the max capacity
Capacity() int
}
type cacheKey dns.Question
type entries map[cacheKey]*cacheEntry
// Cache is a thread-safe fixed capacity LRU cache.
type Cache struct {
capacity int
entries entries
entriesH entriesPtrsHeap // len(entriesH) <= len(entries), as pending entries can be in entries but not in entriesH
clock clock.Clock
lock sync.RWMutex
}
// NewCache creates a cache of the given capacity
func NewCache(capacity int, clk clock.Clock) (*Cache, error) {
if capacity <= 0 {
return nil, errInvalidCapacity
}
c := &Cache{
capacity: capacity,
entries: make(entries, capacity),
clock: clk,
}
if c.clock == nil {
c.clock = clock.New()
}
heap.Init(&c.entriesH)
return c, nil
}
// Clear removes all the entries in the cache
func (c *Cache) Clear() {
c.lock.Lock()
defer c.lock.Unlock()
c.entries = make(entries, c.capacity)
heap.Init(&c.entriesH)
}
// Purge removes the old elements in the cache
func (c *Cache) Purge() {
c.lock.Lock()
defer c.lock.Unlock()
now := c.clock.Now()
for i, entry := range c.entriesH {
if entry.hasExpired(now) {
heap.Remove(&c.entriesH, i)
delete(c.entries, cacheKey(entry.question))
} else {
return // all remaining entries must be still valid...
}
}
}
// Add adds a reply to the cache.
// When `ttl` is equal to `nullTTL`, the cache entry will be valid until the closest TTL in the `reply`
func (c *Cache) Put(request *dns.Msg, reply *dns.Msg, ttl int, flags uint8) int {
c.lock.Lock()
defer c.lock.Unlock()
now := c.clock.Now()
question := request.Question[0]
key := cacheKey(question)
ent, found := c.entries[key]
if found {
updated := ent.setReply(reply, ttl, flags, now)
if updated {
heap.Fix(&c.entriesH, ent.index)
}
} else {
// If we will add a new item and the capacity has been exceeded, make some room...
if len(c.entriesH) >= c.capacity {
lowestEntry := heap.Pop(&c.entriesH).(*cacheEntry)
delete(c.entries, cacheKey(lowestEntry.question))
}
ent = newCacheEntry(&question, now)
ent.setReply(reply, ttl, flags, now)
heap.Push(&c.entriesH, ent)
c.entries[key] = ent
}
return ent.ReplyLen
}
// Look up for a question's reply from the cache.
// If no reply is stored in the cache, it returns a `nil` reply and no error.
func (c *Cache) Get(request *dns.Msg, maxLen int) (reply *dns.Msg, err error) {
c.lock.Lock()
defer c.lock.Unlock()
now := c.clock.Now()
question := request.Question[0]
key := cacheKey(question)
if ent, found := c.entries[key]; found {
reply, err = ent.getReply(request, maxLen, now)
if ent.hasExpired(now) {
Log.Debugf("[cache msgid %d] expired: removing", request.MsgHdr.Id)
if ent.index > 0 {
heap.Remove(&c.entriesH, ent.index)
}
delete(c.entries, key)
reply = nil
}
} else {
// we are the first asking for this name: create an entry with no reply... the caller must wait
Log.Debugf("[cache msgid %d] addind in pending state", request.MsgHdr.Id)
c.entries[key] = newCacheEntry(&question, now)
}
return
}
// Remove removes the provided question from the cache.
func (c *Cache) Remove(question *dns.Question) {
c.lock.Lock()
defer c.lock.Unlock()
key := cacheKey(*question)
if entry, found := c.entries[key]; found {
Log.Debugf("[cache] removing %s-response for '%s'", dns.TypeToString[question.Qtype], question.Name)
if entry.index > 0 {
heap.Remove(&c.entriesH, entry.index)
}
delete(c.entries, key)
}
}
// Len returns the number of entries in the cache.
func (c *Cache) Len() int {
c.lock.RLock()
defer c.lock.RUnlock()
return len(c.entries)
}
// Return the max capacity
func (c *Cache) Capacity() int {
c.lock.RLock()
defer c.lock.RUnlock()
return c.capacity
}
// Return the max capacity
func (c *Cache) String() string {
c.lock.RLock()
defer c.lock.RUnlock()
var buf bytes.Buffer
for _, entry := range c.entries {
fmt.Fprintf(&buf, "%s\n", entry)
}
return buf.String()
}
-230
View File
@@ -1,230 +0,0 @@
package nameserver
import (
"fmt"
"net"
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
// Check that the cache keeps its intended capacity constant
func TestCacheLength(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestCacheLength starting")
const cacheLen = 128
l, err := NewCache(cacheLen, nil)
require.NoError(t, err)
insTime := time.Now()
t.Logf("Inserting 256 questions in the cache at '%s', with TTL from 0 to 255", insTime)
for i := 0; i < cacheLen*2; i++ {
name := fmt.Sprintf("name%d", i)
questionMsg := new(dns.Msg)
questionMsg.SetQuestion(name, dns.TypeA)
questionMsg.RecursionDesired = true
question := &questionMsg.Question[0]
ip := net.ParseIP(fmt.Sprintf("10.0.1.%d", i))
records := []ZoneRecord{Record{name, ip, 0, 0, 0}}
reply := makeAddressReply(questionMsg, question, records, DefaultLocalTTL)
reply.Answer[0].Header().Ttl = uint32(i)
l.Put(questionMsg, reply, 0, 0)
}
require.Equal(t, cacheLen, l.Len(), "cache length")
minExpectedTime := insTime.Add(time.Duration(cacheLen) * time.Second)
t.Logf("Checking all remaining entries expire after insert_time + %d secs='%s'", cacheLen, minExpectedTime)
for _, entry := range l.entries {
if entry.validUntil.Before(minExpectedTime) {
t.Fatalf("Entry valid until %s", entry.validUntil)
}
}
}
// Check that the cache entries are ok
func TestCacheEntries(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestCacheEntries starting")
Log.Infoln("Checking cache consistency")
const cacheLen = 128
clk := clock.NewMock()
l, err := NewCache(cacheLen, clk)
require.NoError(t, err)
questionMsg := new(dns.Msg)
questionMsg.SetQuestion("some.name", dns.TypeA)
questionMsg.RecursionDesired = true
question := &questionMsg.Question[0]
t.Logf("Trying to get a name")
resp, err := l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
if resp != nil {
t.Logf("Got\n%s", resp)
t.Fatalf("ERROR: Did not expect a reponse from Get() yet")
}
t.Logf("Trying to get it again")
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
if resp != nil {
t.Logf("Got\n%s", resp)
t.Fatalf("ERROR: Did not expect a reponse from Get() yet")
}
t.Logf("Inserting the reply")
records := []ZoneRecord{Record{"some.name", net.ParseIP("10.0.1.1"), 0, 0, 0}}
reply1 := makeAddressReply(questionMsg, question, records, DefaultLocalTTL)
l.Put(questionMsg, reply1, nullTTL, 0)
t.Logf("Checking we can Get() the reply now")
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
require.True(t, resp != nil, "reponse from Get()")
t.Logf("Received '%s'", resp.Answer[0])
require.IsType(t, (*dns.A)(nil), resp.Answer[0], "DNS record")
ttlGet1 := resp.Answer[0].Header().Ttl
clk.Add(time.Duration(1) * time.Second)
t.Logf("Checking that a second Get(), after 1 second, gets the same result, but with reduced TTL")
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
require.True(t, resp != nil, "reponse from a second Get()")
t.Logf("Received '%s'", resp.Answer[0])
require.IsType(t, (*dns.A)(nil), resp.Answer[0], "DNS record")
ttlGet2 := resp.Answer[0].Header().Ttl
require.Equal(t, 1, int(ttlGet1-ttlGet2), "TTL difference")
clk.Add(time.Duration(DefaultLocalTTL) * time.Second)
t.Logf("Checking that a third Get(), after %d second, gets no result", DefaultLocalTTL)
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
if resp != nil {
t.Logf("Got\n%s", resp)
t.Fatalf("ERROR: Did NOT expect a reponse from the second Get()")
}
t.Logf("Checking that an Remove() results in Get() returning nothing")
records = []ZoneRecord{Record{"some.name", net.ParseIP("10.0.9.9"), 0, 0, 0}}
replyTemp := makeAddressReply(questionMsg, question, records, DefaultLocalTTL)
l.Put(questionMsg, replyTemp, nullTTL, 0)
lenBefore := l.Len()
l.Remove(question)
require.Equal(t, lenBefore-1, l.Len(), "cache length")
l.Remove(question) // do it again: should have no effect...
require.Equal(t, lenBefore-1, l.Len(), "cache length")
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
require.True(t, resp == nil, "reponse from the Get() after a Remove()")
t.Logf("Inserting a two replies for the same query")
records = []ZoneRecord{Record{"some.name", net.ParseIP("10.0.1.2"), 0, 0, 0}}
reply2 := makeAddressReply(questionMsg, question, records, DefaultLocalTTL)
l.Put(questionMsg, reply2, nullTTL, 0)
clk.Add(time.Duration(1) * time.Second)
records = []ZoneRecord{Record{"some.name", net.ParseIP("10.0.1.3"), 0, 0, 0}}
reply3 := makeAddressReply(questionMsg, question, records, DefaultLocalTTL)
l.Put(questionMsg, reply3, nullTTL, 0)
t.Logf("Checking we get the last one...")
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
require.True(t, resp != nil, "reponse from the Get()")
t.Logf("Received '%s'", resp.Answer[0])
require.IsType(t, (*dns.A)(nil), resp.Answer[0], "DNS record")
require.Equal(t, "10.0.1.3", resp.Answer[0].(*dns.A).A.String(), "IP address")
require.Equal(t, int(DefaultLocalTTL), int(resp.Answer[0].Header().Ttl), "TTL")
clk.Add(time.Duration(DefaultLocalTTL-1) * time.Second)
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
require.True(t, resp != nil, "reponse from the Get()")
t.Logf("Received '%s'", resp.Answer[0])
require.IsType(t, (*dns.A)(nil), resp.Answer[0], "DNS record")
require.Equal(t, "10.0.1.3", resp.Answer[0].(*dns.A).A.String(), "IP address")
require.Equal(t, 1, int(resp.Answer[0].Header().Ttl), "TTL")
t.Logf("Checking we get empty replies when they are expired...")
lenBefore = l.Len()
clk.Add(time.Duration(DefaultLocalTTL) * time.Second)
resp, err = l.Get(questionMsg, minUDPSize)
require.NoError(t, err)
if resp != nil {
t.Logf("Got\n%s", resp.Answer[0])
t.Fatalf("ERROR: Did NOT expect a reponse from the Get()")
}
require.Equal(t, lenBefore-1, l.Len(), "cache length (after getting an expired entry)")
questionMsg2 := new(dns.Msg)
questionMsg2.SetQuestion("some.other.name", dns.TypeA)
questionMsg2.RecursionDesired = true
question2 := &questionMsg2.Question[0]
t.Logf("Trying to Get() a name")
resp, err = l.Get(questionMsg2, minUDPSize)
require.NoError(t, err)
require.Nil(t, resp, "reponse from Get() yet")
t.Logf("Checking that an Remove() between Get() and Put() does not break things")
records = []ZoneRecord{Record{"some.name", net.ParseIP("10.0.9.9"), 0, 0, 0}}
replyTemp2 := makeAddressReply(questionMsg2, question2, records, DefaultLocalTTL)
l.Remove(question2)
l.Put(questionMsg2, replyTemp2, nullTTL, 0)
resp, err = l.Get(questionMsg2, minUDPSize)
require.NoError(t, err)
require.NotNil(t, resp, "reponse from Get()")
questionMsg3 := new(dns.Msg)
questionMsg3.SetQuestion("some.other.name", dns.TypeA)
questionMsg3.RecursionDesired = true
question3 := &questionMsg3.Question[0]
t.Logf("Checking that a entry with CacheNoLocalReplies return an error")
l.Put(questionMsg3, nil, DefaultLocalTTL, CacheNoLocalReplies)
resp, err = l.Get(questionMsg3, minUDPSize)
require.Nil(t, resp, "Get() response with CacheNoLocalReplies")
require.True(t, err == errNoLocalReplies, "Get() error with CacheNoLocalReplies")
t.Logf("Checking that more Put's do not make the negative TTL longer")
clk.Add(time.Duration(DefaultLocalTTL/2) * time.Second)
l.Put(questionMsg3, nil, DefaultLocalTTL, CacheNoLocalReplies)
clk.Add(time.Duration(1) * time.Second)
l.Put(questionMsg3, nil, DefaultLocalTTL, CacheNoLocalReplies)
clk.Add(time.Duration(1) * time.Second)
l.Put(questionMsg3, nil, DefaultLocalTTL, CacheNoLocalReplies)
resp, err = l.Get(questionMsg3, minUDPSize)
require.Nil(t, resp, "Get() response with CacheNoLocalReplies")
require.True(t, err == errNoLocalReplies, "Get() error with CacheNoLocalReplies")
clk.Add(time.Duration(DefaultLocalTTL/2) * time.Second)
t.Logf("Checking that we get an expired response after %d seconds", DefaultLocalTTL)
clk.Add(time.Second * time.Duration(DefaultLocalTTL+1))
t.Logf("Checking that we get an expired response after %d seconds", DefaultLocalTTL)
resp, err = l.Get(questionMsg3, minUDPSize)
require.Nil(t, resp, "expired Get() response with CacheNoLocalReplies")
require.Nil(t, err, "expired Get() error with CacheNoLocalReplies")
l.Remove(question3)
t.Logf("Checking that Put&Get with CacheNoLocalReplies with a Remove in the middle returns nothing")
l.Put(questionMsg3, nil, nullTTL, CacheNoLocalReplies)
l.Remove(question3)
resp, err = l.Get(questionMsg3, minUDPSize)
require.Nil(t, resp, "Get() reponse with CacheNoLocalReplies")
require.Nil(t, err, "Get() error with CacheNoLocalReplies")
}
+252 -80
View File
@@ -1,117 +1,289 @@
package nameserver
import (
"bytes"
"fmt"
"math/rand"
"net"
"sort"
"strings"
"time"
"github.com/miekg/dns"
"github.com/weaveworks/weave/net/address"
)
const (
minUDPSize = 512
maxUDPSize = 65535
topDomain = "."
reverseDNSdomain = "in-addr.arpa."
etcResolvConf = "/etc/resolv.conf"
udpBuffSize = uint16(4096)
minUDPSize = 512
DefaultPort = 53
DefaultTTL = 1
DefaultClientTimeout = 5 * time.Second
)
func makeHeader(r *dns.Msg, q *dns.Question, ttl int) *dns.RR_Header {
return &dns.RR_Header{
Name: q.Name,
Rrtype: q.Qtype,
Class: dns.ClassINET,
Ttl: uint32(ttl),
type DNSServer struct {
ns *Nameserver
domain string
ttl uint32
port int
servers []*dns.Server
upstream *dns.ClientConfig
tcpClient *dns.Client
udpClient *dns.Client
}
func NewDNSServer(ns *Nameserver, domain string, port int, ttl uint32,
clientTimeout time.Duration) (*DNSServer, error) {
s := &DNSServer{
ns: ns,
domain: dns.Fqdn(domain),
ttl: ttl,
port: port,
tcpClient: &dns.Client{Net: "tcp", ReadTimeout: clientTimeout},
udpClient: &dns.Client{Net: "udp", ReadTimeout: clientTimeout, UDPSize: udpBuffSize},
}
var err error
if s.upstream, err = dns.ClientConfigFromFile(etcResolvConf); err != nil {
return nil, err
}
err = s.listen(port)
return s, err
}
func (d *DNSServer) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "WeaveDNS (%s)\n", d.ns.ourName)
fmt.Fprintf(&buf, " listening on port %d, for domain %s\n", d.port, d.domain)
fmt.Fprintf(&buf, " response ttl %d\n", d.ttl)
return buf.String()
}
func (d *DNSServer) listen(port int) error {
udpListener, err := net.ListenPacket("udp", fmt.Sprintf(":%d", port))
if err != nil {
return err
}
udpServer := &dns.Server{PacketConn: udpListener, Handler: d.createMux(d.udpClient, minUDPSize)}
tcpListener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
udpServer.Shutdown()
return err
}
tcpServer := &dns.Server{Listener: tcpListener, Handler: d.createMux(d.tcpClient, -1)}
d.servers = []*dns.Server{udpServer, tcpServer}
return nil
}
func (d *DNSServer) ActivateAndServe() {
for _, server := range d.servers {
go func(server *dns.Server) {
server.ActivateAndServe()
}(server)
}
}
func makeReply(r *dns.Msg, as []dns.RR) *dns.Msg {
m := new(dns.Msg)
func (d *DNSServer) Stop() error {
for _, server := range d.servers {
if err := server.Shutdown(); err != nil {
return err
}
}
return nil
}
func (d *DNSServer) errorResponse(r *dns.Msg, code int, w dns.ResponseWriter) {
m := dns.Msg{}
m.SetReply(r)
m.RecursionAvailable = true
m.Answer = as
m.Rcode = code
d.ns.debugf("error response: %+v", m)
if err := w.WriteMsg(&m); err != nil {
d.ns.infof("error responding: %v", err)
}
}
func (d *DNSServer) createMux(client *dns.Client, defaultMaxResponseSize int) *dns.ServeMux {
m := dns.NewServeMux()
m.HandleFunc(d.domain, d.handleLocal(defaultMaxResponseSize))
m.HandleFunc(reverseDNSdomain, d.handleReverse(client, defaultMaxResponseSize))
m.HandleFunc(topDomain, d.handleRecursive(client, defaultMaxResponseSize))
return m
}
func makeTruncatedReply(r *dns.Msg) *dns.Msg {
// for truncated response, we create a minimal reply with the Truncated bit set
reply := new(dns.Msg)
reply.SetReply(r)
reply.Truncated = true
return reply
}
func (d *DNSServer) handleLocal(defaultMaxResponseSize int) func(dns.ResponseWriter, *dns.Msg) {
return func(w dns.ResponseWriter, req *dns.Msg) {
d.ns.debugf("local request: %+v", *req)
if len(req.Question) != 1 || req.Question[0].Qtype != dns.TypeA {
d.errorResponse(req, dns.RcodeNameError, w)
return
}
type DNSResponseBuilder func(r *dns.Msg, q *dns.Question, addrs []ZoneRecord, ttl int) *dns.Msg
hostname := dns.Fqdn(req.Question[0].Name)
if strings.Count(hostname, ".") == 1 {
hostname = hostname + d.domain
}
func makeAddressReply(r *dns.Msg, q *dns.Question, addrs []ZoneRecord, ttl int) *dns.Msg {
answers := make([]dns.RR, len(addrs))
header := makeHeader(r, q, ttl)
count := 0
for _, addr := range addrs {
ip := addr.IP()
ip4 := ip.To4()
addrs := d.ns.Lookup(hostname)
switch q.Qtype {
case dns.TypeA:
if ip4 != nil {
answers[count] = &dns.A{Hdr: *header, A: ip}
count++
}
case dns.TypeAAAA:
if ip4 == nil {
answers[count] = &dns.AAAA{Hdr: *header, AAAA: ip}
count++
}
response := dns.Msg{}
response.RecursionAvailable = true
response.Authoritative = true
response.SetReply(req)
response.Answer = make([]dns.RR, len(addrs))
header := dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: d.ttl,
}
for i, addr := range addrs {
ip := addr.IP4()
response.Answer[i] = &dns.A{Hdr: header, A: ip}
}
shuffleAnswers(&response.Answer)
maxResponseSize := getMaxResponseSize(req, defaultMaxResponseSize)
truncateResponse(&response, maxResponseSize)
d.ns.debugf("response: %+v", response)
if err := w.WriteMsg(&response); err != nil {
d.ns.infof("error responding: %v", err)
}
}
return makeReply(r, answers[:count])
}
func makePTRReply(r *dns.Msg, q *dns.Question, names []ZoneRecord, ttl int) *dns.Msg {
answers := make([]dns.RR, len(names))
header := makeHeader(r, q, ttl)
for i, name := range names {
answers[i] = &dns.PTR{Hdr: *header, Ptr: name.Name()}
}
return makeReply(r, answers)
}
func (d *DNSServer) handleReverse(client *dns.Client, defaultMaxResponseSize int) func(dns.ResponseWriter, *dns.Msg) {
return func(w dns.ResponseWriter, req *dns.Msg) {
d.ns.debugf("reverse request: %+v", *req)
if len(req.Question) != 1 || req.Question[0].Qtype != dns.TypePTR {
d.errorResponse(req, dns.RcodeNameError, w)
return
}
func makeDNSFailResponse(r *dns.Msg) *dns.Msg {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Rcode = dns.RcodeNameError
return m
}
ipStr := strings.TrimSuffix(req.Question[0].Name, "."+reverseDNSdomain)
ip, err := address.ParseIP(ipStr)
if err != nil {
d.errorResponse(req, dns.RcodeNameError, w)
return
}
// get the maximum UDP-reply length
func getMaxReplyLen(r *dns.Msg, proto dnsProtocol) int {
maxLen := minUDPSize
if proto == protTCP {
maxLen = maxUDPSize
} else if opt := r.IsEdns0(); opt != nil {
maxLen = int(opt.UDPSize())
}
return maxLen
}
hostname, err := d.ns.ReverseLookup(ip.Reverse())
if err != nil {
d.handleRecursive(client, defaultMaxResponseSize)(w, req)
return
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
response := dns.Msg{}
response.RecursionAvailable = true
response.Authoritative = true
response.SetReply(req)
// shuffleAnswers reorders answers for very basic load balancing
func shuffleAnswers(answers []dns.RR) []dns.RR {
if len(answers) > 1 {
for i := range answers {
j := rand.Intn(i + 1)
answers[i], answers[j] = answers[j], answers[i]
header := dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: d.ttl,
}
response.Answer = []dns.RR{&dns.PTR{
Hdr: header,
Ptr: hostname,
}}
maxResponseSize := getMaxResponseSize(req, defaultMaxResponseSize)
truncateResponse(&response, maxResponseSize)
d.ns.debugf("response: %+v", response)
if err := w.WriteMsg(&response); err != nil {
d.ns.infof("error responding: %v", err)
}
}
return answers
}
// only take the first `num` answers
func pruneAnswers(answers []dns.RR, num int) []dns.RR {
if num > 0 && len(answers) > num {
// TODO: we should have some prefer locally-introduced answers, etc...
return answers[:num]
func (d *DNSServer) handleRecursive(client *dns.Client, defaultMaxResponseSize int) func(dns.ResponseWriter, *dns.Msg) {
return func(w dns.ResponseWriter, req *dns.Msg) {
d.ns.debugf("recursive request: %+v", *req)
// Resolve unqualified names locally
if len(req.Question) == 1 && req.Question[0].Qtype == dns.TypeA {
hostname := dns.Fqdn(req.Question[0].Name)
if strings.Count(hostname, ".") == 1 {
d.handleLocal(defaultMaxResponseSize)(w, req)
return
}
}
for _, server := range d.upstream.Servers {
response, _, err := client.Exchange(req, fmt.Sprintf("%s:%s", server, d.upstream.Port))
if err != nil || response == nil {
d.ns.debugf("error trying %s: %v", server, err)
continue
}
if response.Rcode != dns.RcodeSuccess && !response.Authoritative {
d.ns.debugf("non-authoritative error trying %s: %v", server, response.Rcode)
continue
}
d.ns.debugf("response: %+v", response)
if err := w.WriteMsg(response); err != nil {
d.ns.infof("error responding: %v", err)
}
return
}
d.errorResponse(req, dns.RcodeServerFailure, w)
}
return answers
}
func shuffleAnswers(answers *[]dns.RR) {
if len(*answers) <= 1 {
return
}
for i := range *answers {
j := rand.Intn(i + 1)
(*answers)[i], (*answers)[j] = (*answers)[j], (*answers)[i]
}
}
func truncateResponse(response *dns.Msg, maxSize int) {
if len(response.Answer) <= 1 || maxSize <= 0 {
return
}
// take a copy of answers, as we're going to mutate response
answers := response.Answer
// search for smallest i that is too big
i := sort.Search(len(response.Answer), func(i int) bool {
// return true if too big
response.Answer = answers[:i+1]
return response.Len() > maxSize
})
if i == len(answers) {
response.Answer = answers
return
}
response.Answer = answers[:i]
response.Truncated = true
}
func getMaxResponseSize(req *dns.Msg, defaultMaxResponseSize int) int {
if opt := req.IsEdns0(); opt != nil {
return int(opt.UDPSize())
}
return defaultMaxResponseSize
}
+78 -22
View File
@@ -1,39 +1,95 @@
package nameserver
import (
"fmt"
"math/rand"
"net"
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/net/address"
"github.com/weaveworks/weave/router"
)
// Check that we can prune an answer
func TestPrune(t *testing.T) {
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestPrune starting")
func TestTruncation(t *testing.T) {
//common.SetLogLevel("debug")
questionMsg := new(dns.Msg)
questionMsg.SetQuestion("name", dns.TypeA)
questionMsg.RecursionDesired = true
question := &questionMsg.Question[0]
records := []ZoneRecord{
Record{"name", net.ParseIP("10.0.1.1"), 0, 0, 0},
Record{"name", net.ParseIP("10.0.1.2"), 0, 0, 0},
Record{"name", net.ParseIP("10.0.1.3"), 0, 0, 0},
Record{"name", net.ParseIP("10.0.1.4"), 0, 0, 0},
peername, err := router.PeerNameFromString("00:00:00:02:00:00")
require.Nil(t, err)
nameserver := New(peername, nil, nil, "")
dnsserver, err := NewDNSServer(nameserver, "weave.local.", 0, 30, 5*time.Second)
require.Nil(t, err)
udpPort := dnsserver.servers[0].PacketConn.LocalAddr().(*net.UDPAddr).Port
tcpPort := dnsserver.servers[1].Listener.Addr().(*net.TCPAddr).Port
go dnsserver.ActivateAndServe()
defer dnsserver.Stop()
// Add 100 mappings to nameserver
addrs := []address.Address{}
for i := address.Address(0); i < 100; i++ {
addrs = append(addrs, i)
nameserver.AddEntry("foo.weave.local.", "", peername, i)
}
reply := makeAddressReply(questionMsg, question, records, DefaultLocalTTL)
reply.Answer[0].Header().Ttl = DefaultLocalTTL
doRequest := func(client *dns.Client, request *dns.Msg, port int) *dns.Msg {
request.SetQuestion("foo.weave.local.", dns.TypeA)
response, _, err := client.Exchange(request, fmt.Sprintf("127.0.0.1:%d", port))
require.Nil(t, err)
return response
}
pruned := pruneAnswers(reply.Answer, 1)
require.Equal(t, 1, len(pruned), "wrong number of answers")
// do a udp query, ensure we get a truncated response
{
udpClient := dns.Client{Net: "udp", UDPSize: minUDPSize}
response := doRequest(&udpClient, &dns.Msg{}, udpPort)
require.Nil(t, err)
require.True(t, response.MsgHdr.Truncated)
require.True(t, len(response.Answer) < 100)
}
pruned = pruneAnswers(reply.Answer, 2)
require.Equal(t, 2, len(pruned), "wrong number of answers")
// do a udp query with big size, ensure we don't get a truncated response
{
udpClient := dns.Client{Net: "udp", UDPSize: 65535}
request := &dns.Msg{}
request.SetEdns0(65535, false)
response := doRequest(&udpClient, request, udpPort)
require.False(t, response.MsgHdr.Truncated)
require.Equal(t, len(response.Answer), 100)
}
pruned = pruneAnswers(reply.Answer, 0)
require.Equal(t, len(records), len(pruned), "wrong number of answers")
// do a tcp query, ensure we don't get a truncated response
{
tcpClient := dns.Client{Net: "tcp"}
response := doRequest(&tcpClient, &dns.Msg{}, tcpPort)
require.False(t, response.MsgHdr.Truncated)
require.Equal(t, len(response.Answer), 100)
}
}
func TestTruncateResponse(t *testing.T) {
header := dns.RR_Header{
Name: "host.domain.com",
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: 30,
}
for i := 0; i < 10000; i++ {
// generate a random msg
response := &dns.Msg{}
numAnswers := 40 + rand.Intn(200)
response.Answer = make([]dns.RR, numAnswers)
for j := 0; j < numAnswers; j++ {
response.Answer[j] = &dns.A{Hdr: header, A: address.Address(j).IP4()}
}
// pick a random max size, truncate response to that, check it
maxSize := 512 + rand.Intn(response.Len()-512)
truncateResponse(response, maxSize)
require.True(t, response.Len() <= maxSize)
}
}
+184
View File
@@ -0,0 +1,184 @@
package nameserver
import (
"bytes"
"encoding/gob"
"fmt"
"sort"
"time"
"github.com/weaveworks/weave/net/address"
"github.com/weaveworks/weave/router"
)
var now = func() int64 { return time.Now().Unix() }
type Entry struct {
ContainerID string
Origin router.PeerName
Addr address.Address
Hostname string
Version int
Tombstone int64 // timestamp of when it was deleted
}
type Entries []Entry
func (e1 *Entry) equal(e2 *Entry) bool {
return e1.ContainerID == e2.ContainerID &&
e1.Origin == e2.Origin &&
e1.Addr == e2.Addr &&
e1.Hostname == e2.Hostname
}
func (e1 *Entry) less(e2 *Entry) bool {
// Entries are kept sorted by Hostname, Origin, ContainerID then address
switch {
case e1.Hostname != e2.Hostname:
return e1.Hostname < e2.Hostname
case e1.Origin != e2.Origin:
return e1.Origin < e2.Origin
case e1.ContainerID != e2.ContainerID:
return e1.ContainerID < e2.ContainerID
default:
return e1.Addr < e2.Addr
}
}
// returns true to indicate a change
func (e1 *Entry) merge(e2 *Entry) bool {
// we know container id, origin, add and hostname are equal
if e2.Version > e1.Version {
e1.Version = e2.Version
e1.Tombstone = e2.Tombstone
}
return e2.Version != e1.Version
}
func (e1 *Entry) String() string {
return fmt.Sprintf("%s -> %s", e1.Hostname, e1.Addr.String())
}
func (es Entries) Len() int { return len(es) }
func (es Entries) Swap(i, j int) { panic("Swap") }
func (es Entries) Less(i, j int) bool { return es[i].less(&es[j]) }
func (es *Entries) check() error {
if !sort.IsSorted(es) {
return fmt.Errorf("Not sorted!")
}
return nil
}
func (es *Entries) checkAndPanic() {
if err := es.check(); err != nil {
panic(err)
}
}
func (es *Entries) add(hostname, containerid string, origin router.PeerName, addr address.Address) Entry {
defer es.checkAndPanic()
entry := Entry{Hostname: hostname, Origin: origin, ContainerID: containerid, Addr: addr}
i := sort.Search(len(*es), func(i int) bool {
return !(*es)[i].less(&entry)
})
if i < len(*es) && (*es)[i].equal(&entry) {
(*es)[i].Tombstone = 0
(*es)[i].Version++
} else {
*es = append(*es, Entry{})
copy((*es)[i+1:], (*es)[i:])
(*es)[i] = entry
}
return (*es)[i]
}
func (es *Entries) merge(incoming Entries) Entries {
defer es.checkAndPanic()
var (
newEntries Entries
i = 0
)
for _, entry := range incoming {
for i < len(*es) && (*es)[i].less(&entry) {
i++
}
if i < len(*es) && (*es)[i].equal(&entry) {
if !(*es)[i].merge(&entry) {
continue
}
}
*es = append(*es, Entry{})
copy((*es)[i+1:], (*es)[i:])
(*es)[i] = entry
newEntries = append(newEntries, entry)
}
return newEntries
}
func (es *Entries) tombstone(ourname router.PeerName, f func(*Entry) bool) *Entries {
tombstoned := Entries{}
for i, e := range *es {
if f(&e) && e.Origin == ourname {
e.Version++
e.Tombstone = now()
(*es)[i] = e
tombstoned = append(tombstoned, e)
}
}
return &tombstoned
}
func (es *Entries) filter(f func(*Entry) bool) {
i := 0
for _, e := range *es {
if !f(&e) {
continue
}
(*es)[i] = e
i++
}
*es = (*es)[:i]
}
func (es Entries) lookup(hostname string) Entries {
i := sort.Search(len(es), func(i int) bool {
return es[i].Hostname >= hostname
})
if i >= len(es) || es[i].Hostname != hostname {
return Entries{}
}
j := sort.Search(len(es)-i, func(j int) bool {
return es[i+j].Hostname > hostname
})
return es[i : i+j]
}
func (es *Entries) first(f func(*Entry) bool) (*Entry, error) {
for _, e := range *es {
if f(&e) {
return &e, nil
}
}
return nil, fmt.Errorf("Not found")
}
func (es *Entries) Merge(other router.GossipData) {
es.merge(*other.(*Entries))
}
func (es *Entries) Encode() [][]byte {
buf := &bytes.Buffer{}
if err := gob.NewEncoder(buf).Encode(es); err != nil {
panic(err)
}
return [][]byte{buf.Bytes()}
}
+113
View File
@@ -0,0 +1,113 @@
package nameserver
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/weaveworks/weave/net/address"
"github.com/weaveworks/weave/router"
)
func TestAdd(t *testing.T) {
oldNow := now
defer func() { now = oldNow }()
now = func() int64 { return 1234 }
entries := Entries{}
entries.add("A", "", router.UnknownPeerName, address.Address(0))
expected := Entries{
Entry{Hostname: "A", Origin: router.UnknownPeerName, Addr: address.Address(0)},
}
require.Equal(t, entries, expected)
entries.tombstone(router.UnknownPeerName, func(e *Entry) bool { return e.Hostname == "A" })
expected = Entries{
Entry{Hostname: "A", Origin: router.UnknownPeerName, Addr: address.Address(0), Version: 1, Tombstone: 1234},
}
require.Equal(t, entries, expected)
entries.add("A", "", router.UnknownPeerName, address.Address(0))
expected = Entries{
Entry{Hostname: "A", Origin: router.UnknownPeerName, Addr: address.Address(0), Version: 2},
}
require.Equal(t, entries, expected)
}
func TestMerge(t *testing.T) {
e1 := Entries{
Entry{Hostname: "A"},
Entry{Hostname: "C"},
Entry{Hostname: "D"},
Entry{Hostname: "F"},
}
e2 := Entries{
Entry{Hostname: "B"},
Entry{Hostname: "E"},
Entry{Hostname: "F"},
}
e1.merge(e2)
expected := Entries{
Entry{Hostname: "A"},
Entry{Hostname: "B"},
Entry{Hostname: "C"},
Entry{Hostname: "D"},
Entry{Hostname: "E"},
Entry{Hostname: "F"},
}
require.Equal(t, e1, expected)
}
func TestTombstone(t *testing.T) {
oldNow := now
defer func() { now = oldNow }()
now = func() int64 { return 1234 }
es := Entries{
Entry{Hostname: "A"},
Entry{Hostname: "B"},
}
es.tombstone(router.UnknownPeerName, func(e *Entry) bool {
return e.Hostname == "B"
})
expected := Entries{
Entry{Hostname: "A"},
Entry{Hostname: "B", Version: 1, Tombstone: 1234},
}
require.Equal(t, es, expected)
}
func TestDelete(t *testing.T) {
es := Entries{
Entry{Hostname: "A"},
Entry{Hostname: "B"},
}
es.filter(func(e *Entry) bool {
return e.Hostname != "A"
})
expected := Entries{
Entry{Hostname: "B"},
}
require.Equal(t, es, expected)
}
func TestLookup(t *testing.T) {
es := Entries{
Entry{Hostname: "A"},
Entry{Hostname: "B", ContainerID: "foo"},
Entry{Hostname: "B", ContainerID: "bar"},
Entry{Hostname: "C"},
}
have := es.lookup("B")
want := Entries{
Entry{Hostname: "B", ContainerID: "foo"},
Entry{Hostname: "B", ContainerID: "bar"},
}
require.Equal(t, have, want)
}
+53 -92
View File
@@ -2,123 +2,84 @@ package nameserver
import (
"fmt"
"net"
"net/http"
"github.com/gorilla/mux"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/common/docker"
"github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/net/address"
)
func httpErrorAndLog(w http.ResponseWriter, msg string,
status int, logmsg string, logargs ...interface{}) {
http.Error(w, msg, status)
Log.Warningf("[http] "+logmsg, logargs...)
func badRequest(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
common.Log.Warningf("[gossipdns]: %v", err)
}
func extractFQDN(r *http.Request) (string, string) {
if fqdnStr := r.FormValue("fqdn"); fqdnStr != "" {
return fqdnStr, fqdnStr
}
return "*", ""
}
func ServeHTTP(listener net.Listener, version string, server *DNSServer, dockerCli *docker.Client) {
muxRouter := mux.NewRouter()
muxRouter.Methods("GET").Path("/status").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "weave DNS", version)
fmt.Fprintln(w, server.Status())
fmt.Fprintln(w, server.Zone.Status())
func (n *Nameserver) HandleHTTP(router *mux.Router) {
router.Methods("GET").Path("/domain").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, n.domain)
})
muxRouter.Methods("GET").Path("/domain").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, server.Zone.Domain())
})
muxRouter.Methods("PUT").Path("/name/{id:.+}/{ip:.+}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqError := func(msg string, logmsg string, logargs ...interface{}) {
httpErrorAndLog(w, msg, http.StatusBadRequest, logmsg, logargs...)
}
vars := mux.Vars(r)
idStr := vars["id"]
ipStr := vars["ip"]
name := r.FormValue("fqdn")
if name == "" {
reqError("Invalid FQDN", "Invalid FQDN in request: %s, %s", r.URL, r.Form)
router.Methods("PUT").Path("/name/{container}/{ip}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
vars = mux.Vars(r)
container = vars["container"]
ipStr = vars["ip"]
hostname = r.FormValue("fqdn")
ip, err = address.ParseIP(ipStr)
)
if err != nil {
badRequest(w, err)
return
}
ip := net.ParseIP(ipStr)
if ip == nil {
reqError("Invalid IP", "Invalid IP in request: %s", ipStr)
if err := n.AddEntry(dns.Fqdn(hostname), container, n.ourName, ip); err != nil {
badRequest(w, fmt.Errorf("Unable to add entry: %v", err))
return
}
domain := server.Zone.Domain()
if !dns.IsSubDomain(domain, name) {
Log.Infof("[http] Ignoring name %s, not in %s", name, domain)
return
}
Log.Infof("[http] Adding %s -> %s", name, ipStr)
if err := server.Zone.AddRecord(idStr, name, ip); err != nil {
if _, ok := err.(DuplicateError); !ok {
httpErrorAndLog(
w, "Internal error", http.StatusInternalServerError,
"Unexpected error from DB: %s", err)
return
} // oh, I already know this. whatever.
if r.FormValue("check-alive") == "true" && n.docker.IsContainerNotRunning(container) {
common.Log.Infof("[gossipdns] '%s' is not running: removing", container)
if err := n.Delete(hostname, container, ipStr, ip); err != nil {
common.Log.Infof("[gossipdns] failed to remove: %v", err)
}
}
if r.FormValue("check-alive") == "true" && dockerCli != nil && dockerCli.IsContainerNotRunning(idStr) {
Log.Infof("[http] '%s' is not running: removing", idStr)
server.Zone.DeleteRecords(idStr, name, ip)
}
w.WriteHeader(204)
})
muxRouter.Methods("DELETE").Path("/name/{id:.+}/{ip:.+}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
deleteHandler := func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
ipStr := vars["ip"]
ip := net.ParseIP(ipStr)
if ip == nil {
httpErrorAndLog(
w, "Invalid IP in request", http.StatusBadRequest,
"Invalid IP in request: %s", ipStr)
return
hostname := r.FormValue("fqdn")
if hostname == "" {
hostname = "*"
} else {
hostname = dns.Fqdn(hostname)
}
fqdnStr, fqdn := extractFQDN(r)
Log.Infof("[http] Deleting ID %s, IP %s, FQDN %s", idStr, ipStr, fqdnStr)
server.Zone.DeleteRecords(idStr, fqdn, ip)
})
muxRouter.Methods("DELETE").Path("/name/{id:.+}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
fqdnStr, fqdn := extractFQDN(r)
Log.Infof("[http] Deleting ID %s, IP *, FQDN %s", idStr, fqdnStr)
server.Zone.DeleteRecords(idStr, fqdn, net.IP{})
})
muxRouter.Methods("DELETE").Path("/name").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fqdnStr, fqdn := extractFQDN(r)
Log.Infof("[http] Deleting ID *, IP *, FQDN %s", fqdnStr)
server.Zone.DeleteRecords("", fqdn, net.IP{})
})
container, ok := vars["container"]
if !ok {
container = "*"
}
http.Handle("/", muxRouter)
ipStr, ok := vars["ip"]
ip, err := address.ParseIP(ipStr)
if ok && err != nil {
badRequest(w, err)
return
} else if !ok {
ipStr = "*"
}
if err := http.Serve(listener, nil); err != nil {
Log.Fatal("[http] Unable to serve http: ", err)
if err := n.Delete(hostname, container, ipStr, ip); err != nil {
badRequest(w, fmt.Errorf("Unable to delete entries: %v", err))
return
}
w.WriteHeader(204)
}
router.Methods("DELETE").Path("/name/{container}/{ip}").HandlerFunc(deleteHandler)
router.Methods("DELETE").Path("/name/{container}").HandlerFunc(deleteHandler)
router.Methods("DELETE").Path("/name").HandlerFunc(deleteHandler)
}
-131
View File
@@ -1,131 +0,0 @@
package nameserver
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
wt "github.com/weaveworks/weave/testing"
)
func genForm(method string, url string, data url.Values) (resp *http.Response, err error) {
req, err := http.NewRequest(method, url, strings.NewReader(data.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return http.DefaultClient.Do(req)
}
func TestHttp(t *testing.T) {
var (
containerID = "deadbeef"
testDomain = "weave.local."
successTestName = "test1." + testDomain
testAddr1 = "10.2.2.1/24"
testAddr2 = "10.2.2.2/24"
dockerIP = "9.8.7.6"
)
zone, err := NewZoneDb(ZoneConfig{Domain: testDomain})
require.NoError(t, err)
err = zone.Start()
require.NoError(t, err)
defer zone.Stop()
httpListener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal("Unable to create http listener: ", err)
}
// Create a useless server... it will not even be started
srv, _ := NewDNSServer(DNSServerConfig{Zone: zone})
port := httpListener.Addr().(*net.TCPAddr).Port
go ServeHTTP(httpListener, "", srv, nil)
// Ask the http server to add our test address into the database
addrParts := strings.Split(testAddr1, "/")
addrURL := fmt.Sprintf("http://localhost:%d/name/%s/%s", port, containerID, addrParts[0])
resp, err := genForm("PUT", addrURL,
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts[1]}})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http response")
// Check that the address is now there.
ip, _, _ := net.ParseCIDR(testAddr1)
foundIP, err := zone.LookupName(successTestName)
require.NoError(t, err)
if !foundIP[0].IP().Equal(ip) {
t.Fatalf("Unexpected result for %s: received %s, expected %s", successTestName, foundIP, ip)
}
// Adding exactly the same address should be OK
resp, err = genForm("PUT", addrURL,
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts[1]}})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http success response for duplicate add")
// Now try adding the same address again with a different ident --
// again should be fine
otherURL := fmt.Sprintf("http://localhost:%d/name/%s/%s", port, "other", addrParts[0])
resp, err = genForm("PUT", otherURL,
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts[1]}})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http response")
// Adding a new IP for the same name should be OK
addrParts2 := strings.Split(testAddr2, "/")
addrURL2 := fmt.Sprintf("http://localhost:%d/name/%s/%s", port, containerID, addrParts2[0])
resp, err = genForm("PUT", addrURL2,
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts2[1]}})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http success response for second IP")
// Check that we can get two IPs for that name
ip2, _, _ := net.ParseCIDR(testAddr2)
foundIP, err = zone.LookupName(successTestName)
require.NoError(t, err)
if len(foundIP) != 2 {
t.Logf("IPs found: %s", foundIP)
t.Fatalf("Unexpected result length: received %d responses", len(foundIP))
}
if !(foundIP[0].IP().Equal(ip) || foundIP[0].IP().Equal(ip2)) {
t.Logf("IPs found: %s", foundIP)
t.Fatalf("Unexpected result for %s: received %s, expected %s", successTestName, foundIP, ip)
}
if !(foundIP[1].IP().Equal(ip) || foundIP[1].IP().Equal(ip2)) {
t.Logf("IPs found: %s", foundIP)
t.Fatalf("Unexpected result for %s: received %s, expected %s", successTestName, foundIP, ip)
}
// Delete the address
resp, err = genForm("DELETE", addrURL, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http response")
// Check that the address is still resolvable.
x, err := zone.LookupName(successTestName)
t.Logf("Got %s", x)
require.NoError(t, err)
// Delete the address record mentioning the other container
resp, err = genForm("DELETE", otherURL, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http response")
// Delete the second IP
resp, err = genForm("DELETE", addrURL2, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "http response")
// Check that the address is gone
x, err = zone.LookupName(successTestName)
t.Logf("Got %s", x)
wt.AssertErrorType(t, (*LookupError)(nil), err, "fully-removed address")
// Would like to shut down the http server at the end of this test
// but it's complicated.
// See https://groups.google.com/forum/#!topic/golang-nuts/vLHWa5sHnCE
}
-28
View File
@@ -1,28 +0,0 @@
package nameserver
import "net"
// A basic mDNS service
type ZoneMDNS interface {
// Start the service
Start(*net.Interface) error
// Stop the service
Stop() error
}
// A mDNS server
type ZoneMDNSServer interface {
ZoneMDNS
// Return the Zone database used by the server
Zone() Zone
}
// A mDNS client interface
type ZoneMDNSClient interface {
ZoneMDNS
ZoneLookup
// Perform an insistent lookup for a name
InsistentLookupName(name string) ([]ZoneRecord, error)
// Perform an insistent lookup for a reverse address
InsistentLookupInaddr(inaddr string) ([]ZoneRecord, error)
}
-290
View File
@@ -1,290 +0,0 @@
package nameserver
import (
"bytes"
"fmt"
"math"
"net"
"time"
"github.com/miekg/dns"
)
// Portions of this code taken from github.com/armon/mdns
const (
ipv4mdns = "224.0.0.251" // link-local multicast address
mdnsPort = 5353 // mDNS assigned port
// We wait this long to hear a response from other mDNS servers on
// the network.
mDNSTimeout = 500 * time.Millisecond
MaxDuration = time.Duration(math.MaxInt64)
MailboxSize = 16
)
var (
ipv4Addr = &net.UDPAddr{
IP: net.ParseIP(ipv4mdns),
Port: mdnsPort,
}
)
type Response struct {
name string
addr net.IP
ttl int
err error
}
func (r Response) Name() string { return r.name }
func (r Response) IP() net.IP { return r.addr }
func (r Response) Priority() int { return 0 }
func (r Response) Weight() int { return 0 }
func (r Response) TTL() int { return r.ttl }
func (r Response) Equal(r2 *Response) bool {
if r.name != r2.name {
return false
}
if !r.addr.Equal(r2.addr) {
return false
}
if r.err != r2.err {
return false
}
return true
}
func (r Response) String() string {
var buf bytes.Buffer
if r.err != nil {
fmt.Fprintf(&buf, "%s", r.err)
} else {
if len(r.Name()) > 0 {
fmt.Fprintf(&buf, "%s", r.Name())
}
if !r.IP().IsUnspecified() {
fmt.Fprintf(&buf, "[%s]", r.IP())
}
if r.ttl > 0 {
fmt.Fprintf(&buf, "(TTL:%d)", r.TTL())
}
}
return buf.String()
}
type responseInfo struct {
timeout time.Time // if no answer by this time, give up
insistent bool // insistent queries are not removed on the first reply
ch chan<- *Response
}
// Represents one query that we have sent for one name.
// If we, internally, get several requests for the same name while we have
// a query in flight, then we don't want to send more queries out.
// Invariant on responseInfos: they are in ascending order of timeout.
type inflightQuery struct {
name string
id uint16 // the DNS message ID
responseInfos []*responseInfo
}
type MDNSClient struct {
// note unorthodox use of 'Server' class in client logic - using it to
// listen on a multicast socket and call callback whenever a message comes in.
listener *dns.Server
conn *net.UDPConn
addr *net.UDPAddr
inflight map[string]*inflightQuery
actionChan chan<- MDNSAction
}
type mDNSQueryInfo struct {
name string
querytype uint16
responseCh chan<- *Response
}
func NewMDNSClient() (*MDNSClient, error) {
return &MDNSClient{
addr: ipv4Addr,
inflight: make(map[string]*inflightQuery)}, nil
}
func (c *MDNSClient) Start(ifi *net.Interface) (err error) {
if c.conn, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}); err != nil {
return err
}
multicast, err := LinkLocalMulticastListener(ifi)
if err != nil {
return err
}
handleMDNS := func(w dns.ResponseWriter, r *dns.Msg) {
// Don't want to handle queries here, so filter anything out that isn't a response
if len(r.Answer) > 0 {
c.ResponseCallback(r)
}
}
actionChan := make(chan MDNSAction, MailboxSize)
c.actionChan = actionChan
go c.actorLoop(actionChan)
c.listener = &dns.Server{Unsafe: true, PacketConn: multicast, Handler: dns.HandlerFunc(handleMDNS)}
go c.listener.ActivateAndServe()
return nil
}
func (c *MDNSClient) Stop() error {
c.actionChan <- nil
return nil
}
func LinkLocalMulticastListener(ifi *net.Interface) (net.PacketConn, error) {
return net.ListenMulticastUDP("udp", ifi, ipv4Addr)
}
// ACTOR client API
type MDNSAction func()
// Async
func (c *MDNSClient) SendQuery(name string, querytype uint16, insistent bool, responseCh chan<- *Response) {
c.actionChan <- func() {
query, found := c.inflight[name]
if !found {
m := new(dns.Msg)
m.SetQuestion(name, querytype)
m.RecursionDesired = false
buf, err := m.Pack()
if err != nil {
responseCh <- &Response{err: err}
close(responseCh)
return
}
query = &inflightQuery{
name: name,
id: m.Id,
}
if _, err = c.conn.WriteTo(buf, c.addr); err != nil {
responseCh <- &Response{err: err}
close(responseCh)
return
}
c.inflight[name] = query
}
info := &responseInfo{
ch: responseCh,
timeout: time.Now().Add(mDNSTimeout),
insistent: insistent,
}
// Invariant on responseInfos: they are in ascending order of
// timeout. Since we use a fixed interval from Now(), this
// must be after all existing timeouts.
query.responseInfos = append(query.responseInfos, info)
}
}
// Async - called from dns library multiplexer
func (c *MDNSClient) ResponseCallback(r *dns.Msg) {
c.actionChan <- func() {
for _, answer := range r.Answer {
var name string
var res *Response
switch rr := answer.(type) {
case *dns.A:
name = rr.Hdr.Name
res = &Response{name: name, addr: rr.A, ttl: int(rr.Hdr.Ttl)}
case *dns.PTR:
name = rr.Hdr.Name
raddr, _ := raddrToIP(name)
res = &Response{name: rr.Ptr, addr: raddr, ttl: int(rr.Hdr.Ttl)}
default:
return
}
if query, found := c.inflight[name]; found {
var newResponseInfos []*responseInfo
for _, resp := range query.responseInfos {
resp.ch <- res
// insistent queries are not removed on the first reply, but on the timeout
if resp.insistent {
newResponseInfos = append(newResponseInfos, resp)
} else {
close(resp.ch)
}
}
if len(newResponseInfos) == 0 {
delete(c.inflight, name)
} else {
query.responseInfos = newResponseInfos
}
} else {
// We've received a response that didn't match a query
// Do we want to cache it?
}
}
}
}
// ACTOR server
// Check all in-flight queries, close all that have already timed out,
// and return the duration until the next timeout
func (c *MDNSClient) checkInFlightQueries() time.Duration {
now := time.Now()
after := MaxDuration
for name, query := range c.inflight {
// Invariant on responseInfos: they are in ascending order of timeout.
numClosed := 0
for _, item := range query.responseInfos {
duration := item.timeout.Sub(now)
if duration <= 0 { // timed out
close(item.ch)
numClosed++
} else {
if duration < after {
after = duration
}
break // don't need to look at any more for this query
}
}
// Remove timed-out items from the slice
query.responseInfos = query.responseInfos[numClosed:]
if len(query.responseInfos) == 0 {
delete(c.inflight, name)
}
}
return after
}
func (c *MDNSClient) actorLoop(actionChan <-chan MDNSAction) {
timer := time.NewTimer(MaxDuration)
run := func() { timer.Reset(c.checkInFlightQueries()) }
loop:
for {
select {
case action := <-actionChan:
if action == nil {
c.listener.Shutdown()
break loop
} else {
action()
run()
}
case <-timer.C:
run()
}
}
// Close all response channels at termination
for _, query := range c.inflight {
for _, item := range query.responseInfos {
close(item.ch)
}
}
}
-199
View File
@@ -1,199 +0,0 @@
package nameserver
import (
"log"
"net"
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
wt "github.com/weaveworks/weave/testing"
)
var (
successTestName = "test1.weave."
failTestName = "test2.weave."
testAddr = net.ParseIP("9.8.7.6")
testInAddr = "6.7.8.9.in-addr.arpa."
)
func minimalServer(w dns.ResponseWriter, req *dns.Msg) {
sendAnswer := func(m *dns.Msg) {
buf, err := m.Pack()
CheckFatal(err)
if buf == nil {
log.Fatal("Nil buffer")
}
//log.Println("minimalServer sending:", buf)
// This is a bit of a kludge - per the RFC we should send responses from 5353, but that doesn't seem to work
sendconn, err := net.DialUDP("udp", nil, ipv4Addr)
CheckFatal(err)
_, err = sendconn.Write(buf)
sendconn.Close()
CheckFatal(err)
}
//log.Println("minimalServer received:", req)
if len(req.Answer) > 0 {
return // Only interested in questions.
}
if len(req.Question) != 1 {
return // We only handle single-question messages
}
if req.Question[0].Qtype == dns.TypeA && req.Question[0].Name == successTestName {
m := new(dns.Msg)
m.SetReply(req)
hdr := dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeA,
Class: dns.ClassINET, Ttl: 3600}
a := &dns.A{Hdr: hdr, A: testAddr}
m.Answer = append(m.Answer, a)
sendAnswer(m)
} else if req.Question[0].Qtype == dns.TypePTR && req.Question[0].Name == testInAddr {
m := new(dns.Msg)
m.SetReply(req)
hdr := dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypePTR,
Class: dns.ClassINET, Ttl: 3600}
a := &dns.PTR{Hdr: hdr, Ptr: successTestName}
m.Answer = append(m.Answer, a)
sendAnswer(m)
}
}
func RunLocalMulticastServer() (*dns.Server, error) {
multicast, err := LinkLocalMulticastListener(nil)
if err != nil {
return nil, err
}
server := &dns.Server{
PacketConn: multicast,
Handler: dns.HandlerFunc(minimalServer),
ReadTimeout: 100 * time.Millisecond}
go server.ActivateAndServe()
return server, nil
}
func setup(t *testing.T) (*MDNSClient, *dns.Server, error) {
EnableDebugLogging(testing.Verbose())
server, err := RunLocalMulticastServer()
if err != nil {
t.Fatalf("Unable to run test server: %s. No default multicast interface?", err)
}
mdnsClient, err := NewMDNSClient()
require.NoError(t, err)
err = mdnsClient.Start(nil)
require.NoError(t, err)
return mdnsClient, server, err
}
func (c *MDNSClient) Shutdown() {
c.actionChan <- nil
}
type testContext struct {
receivedAddr net.IP
receivedCount int
channel chan *Response
}
func newTestContext() *testContext {
return &testContext{channel: make(chan *Response, 4)}
}
func (c *testContext) checkResponse(t *testing.T, channelOk bool, resp *Response) {
if !channelOk {
c.channel = nil
return
}
require.NoError(t, resp.err)
log.Printf("Got address response %s addr %s", resp.Name(), resp.IP())
c.receivedAddr = resp.IP()
c.receivedCount++
}
func TestSimpleQuery(t *testing.T) {
log.Println("TestSimpleQuery starting")
mdnsClient, server, _ := setup(t)
defer mdnsClient.Shutdown()
defer server.Shutdown()
context := newTestContext()
// First, a test we expect to succeed
mdnsClient.SendQuery(successTestName, dns.TypeA, false, context.channel)
for resp := range context.channel {
context.checkResponse(t, true, resp)
}
if !context.receivedAddr.Equal(testAddr) || context.receivedCount != 1 {
t.Fatal("Unexpected result for", successTestName, context.receivedAddr, context.receivedCount)
}
// Now, a test we expect to time out with no responses
context = newTestContext()
mdnsClient.SendQuery("test2.weave.", dns.TypeA, false, context.channel)
for resp := range context.channel {
context.checkResponse(t, true, resp)
}
if context.receivedCount > 0 {
t.Fatal("Unexpected result for test2.weave", context.receivedAddr)
}
}
func TestParallelQuery(t *testing.T) {
log.Println("TestParallelQuery starting")
mdnsClient, server, _ := setup(t)
defer mdnsClient.Shutdown()
defer server.Shutdown()
context1 := newTestContext()
context2 := newTestContext()
go mdnsClient.SendQuery(successTestName, dns.TypeA, false, context1.channel)
go mdnsClient.SendQuery(successTestName, dns.TypeA, false, context2.channel)
timeout := time.After(2 * time.Second)
outerloop:
for context1.channel != nil || context2.channel != nil {
select {
case resp, ok := <-context1.channel:
context1.checkResponse(t, ok, resp)
case resp, ok := <-context2.channel:
context2.checkResponse(t, ok, resp)
case <-timeout:
break outerloop
}
}
if !context1.receivedAddr.Equal(testAddr) || !context2.receivedAddr.Equal(testAddr) || context1.receivedCount != 1 || context2.receivedCount != 1 {
t.Fatal("Unexpected result for", successTestName, context1.receivedAddr, context2.receivedAddr, context1.receivedCount, context2.receivedCount)
}
}
func TestAsLookup(t *testing.T) {
mdnsClient, server, _ := setup(t)
defer mdnsClient.Shutdown()
defer server.Shutdown()
ips, err := mdnsClient.LookupName(successTestName)
require.NoError(t, err)
if !testAddr.Equal(ips[0].IP()) {
t.Fatalf("Returned address incorrect %s", ips)
}
ips, err = mdnsClient.LookupName("foo.example.com.")
wt.AssertErrorType(t, (*LookupError)(nil), err, "unknown hostname")
names, err := mdnsClient.LookupInaddr(testInAddr)
require.NoError(t, err)
if !(successTestName == names[0].Name()) {
t.Fatalf("Expected name %s, got %s", successTestName, names)
}
}
-71
View File
@@ -1,71 +0,0 @@
package nameserver
import (
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
)
func mdnsLookup(client *MDNSClient, name string, qtype uint16, insistent bool) ([]ZoneRecord, error) {
var responses []*Response
channel := make(chan *Response)
client.SendQuery(name, qtype, insistent, channel)
for resp := range channel {
if err := resp.err; err != nil {
Log.Debugf("[mdns] Error for query type %s name %s: %s", dns.TypeToString[qtype], name, err)
return nil, err
}
Log.Debugf("[mdns] Got response name for %s-query about name '%s': name '%s', addr '%s'",
dns.TypeToString[qtype], name, resp.name, resp.addr)
if !insistent {
return []ZoneRecord{resp}, nil // non-background queries return the first response...
}
// add the response, avoiding duplicates
// TODO: replace this linear search by something better... (not a big deal as we don't expect long responses)
duplicate := false
for _, r := range responses {
if r.Equal(resp) {
duplicate = true
break
}
}
if !duplicate {
responses = append(responses, resp)
}
}
if len(responses) == 0 {
return nil, LookupError(name)
}
// convert []Response to []ZoneRecord
res := make([]ZoneRecord, len(responses))
for i, rr := range responses {
res[i] = ZoneRecord(rr)
}
return res, nil
}
// Perform a lookup for a name, returning either an IP address or an error
func (client *MDNSClient) LookupName(name string) ([]ZoneRecord, error) {
return mdnsLookup(client, name, dns.TypeA, false)
}
// Perform a lookup for an IP address, returning either a name or an error
func (client *MDNSClient) LookupInaddr(inaddr string) ([]ZoneRecord, error) {
return mdnsLookup(client, inaddr, dns.TypePTR, false)
}
// Perform lookup for a name, returning either a list of IP addresses or an error
// Insistent queries will wait up to `mDNSTimeout` milliseconds for responses
func (client *MDNSClient) InsistentLookupName(name string) ([]ZoneRecord, error) {
return mdnsLookup(client, name, dns.TypeA, true)
}
// Perform an insistent lookup for an IP address, returning either a list of names or an error
// Insistent queries will wait up to `mDNSTimeout` milliseconds for responses
func (client *MDNSClient) InsistentLookupInaddr(inaddr string) ([]ZoneRecord, error) {
return mdnsLookup(client, inaddr, dns.TypePTR, true)
}
-148
View File
@@ -1,148 +0,0 @@
package nameserver
import (
"net"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
)
type MDNSServer struct {
localAddrs []net.Addr
sendconn *net.UDPConn
srv *dns.Server
zone Zone
allowLocal bool
ttl int
}
// Create a new mDNS server
// Nothing will be done (including port bindings) until you `Start()` the server
func NewMDNSServer(zone Zone, local bool, ttl int) (*MDNSServer, error) {
return &MDNSServer{
zone: zone,
allowLocal: local,
ttl: ttl,
}, nil
}
// Return true if testaddr is a UDP address with IP matching my local i/f
func (s *MDNSServer) addrIsLocal(testaddr net.Addr) bool {
if udpaddr, ok := testaddr.(*net.UDPAddr); ok {
for _, localaddr := range s.localAddrs {
if ipnetlocal, ok := localaddr.(*net.IPNet); ok {
if ipnetlocal.IP.Equal(udpaddr.IP) {
return true
}
}
}
}
return false
}
// Start the mDNS server
func (s *MDNSServer) Start(ifi *net.Interface) (err error) {
// This is a bit of a kludge - per the RFC we should send responses from 5353, but that doesn't seem to work
s.sendconn, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return err
}
conn, err := LinkLocalMulticastListener(ifi)
if err != nil {
return err
}
if ifi == nil {
s.localAddrs, err = net.InterfaceAddrs()
} else {
s.localAddrs, err = ifi.Addrs()
}
if err != nil {
return err
}
handleLocal := s.makeHandler(dns.TypeA,
func(zone ZoneLookup, r *dns.Msg, q *dns.Question) *dns.Msg {
if ips, err := zone.LookupName(q.Name); err == nil {
return makeAddressReply(r, q, ips, s.ttl)
}
return nil
})
handleReverse := s.makeHandler(dns.TypePTR,
func(zone ZoneLookup, r *dns.Msg, q *dns.Question) *dns.Msg {
if names, err := zone.LookupInaddr(q.Name); err == nil {
return makePTRReply(r, q, names, s.ttl)
}
return nil
})
mux := dns.NewServeMux()
mux.HandleFunc(s.zone.Domain(), handleLocal)
mux.HandleFunc(RDNSDomain, handleReverse)
s.srv = &dns.Server{
Listener: nil,
PacketConn: conn,
Handler: mux,
}
go s.srv.ActivateAndServe()
return err
}
// Stop the mDNS server
func (s *MDNSServer) Stop() error {
return s.srv.Shutdown()
}
func (s *MDNSServer) Zone() Zone {
return s.zone
}
type LookupFunc func(ZoneLookup, *dns.Msg, *dns.Question) *dns.Msg
func (s *MDNSServer) makeHandler(qtype uint16, lookup LookupFunc) dns.HandlerFunc {
return func(rw dns.ResponseWriter, r *dns.Msg) {
// Handle only questions, ignore answers. We might also ignore
// questions that arise locally (i.e., that come from an IP we
// think is local), but in the interest of avoiding
// complication, and easier testing, this is elided on the
// assumption that the client wouldn't ask if it already knew
// the answer, and if it does ask, it'll be happy to get an
// answer.
remoteAddr := rw.RemoteAddr()
if s.addrIsLocal(remoteAddr) && !s.allowLocal {
Log.Debugf("[mdns] srv: mDNS query from local host ('%s'): ignored", remoteAddr)
return
}
// TODO: use any Answer in the query for adding records in the Zone database...
if len(r.Answer) == 0 && len(r.Question) > 0 {
q := &r.Question[0]
if q.Qtype == qtype {
Log.Debugf("[mdns msgid %d] srv: trying to answer to mDNS query '%s'", r.MsgHdr.Id, q.Name)
if m := lookup(s.zone, r, q); m != nil {
Log.Debugf("[mdns msgid %d] srv: found local answer to mDNS query '%s'", r.MsgHdr.Id, q.Name)
if err := s.sendResponse(m); err != nil {
Log.Warningf("[mdns msgid %d] srv: error writing mDNS response to %v", r.MsgHdr.Id, s.sendconn)
} else {
Log.Debugf("[mdns msgid %d] srv: response sent: %d answers", r.MsgHdr.Id, len(m.Answer))
}
} else {
Log.Debugf("[mdns msgid %d] srv: no local answer for answering mDNS query '%s'",
r.MsgHdr.Id, q.Name)
}
}
}
}
}
func (s *MDNSServer) sendResponse(m *dns.Msg) error {
buf, err := m.Pack()
if err != nil {
return err
}
_, err = s.sendconn.WriteTo(buf, ipv4Addr)
return err
}
-124
View File
@@ -1,124 +0,0 @@
package nameserver
import (
"net"
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
func TestServerSimpleQuery(t *testing.T) {
var (
testRecord1 = Record{"test.weave.local.", net.ParseIP("10.20.20.10"), 0, 0, 0}
testRecord2 = Record{"test.weave.local.", net.ParseIP("10.20.20.20"), 0, 0, 0}
testInAddr1 = "10.20.20.10.in-addr.arpa."
)
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestServerSimpleQuery starting")
mzone := newMockedZoneWithRecords([]ZoneRecord{testRecord1, testRecord2})
mdnsServer, err := NewMDNSServer(mzone, true, DefaultLocalTTL)
require.NoError(t, err)
err = mdnsServer.Start(nil)
require.NoError(t, err)
defer mdnsServer.Stop()
var receivedAddrs []net.IP
receivedName := ""
recvChan := make(chan interface{})
receivedCount := 0
// Implement a minimal listener for responses
multicast, err := LinkLocalMulticastListener(nil)
require.NoError(t, err)
handleMDNS := func(w dns.ResponseWriter, r *dns.Msg) {
// Only handle responses here
if len(r.Answer) > 0 {
t.Logf("Received %d answer(s)", len(r.Answer))
for _, answer := range r.Answer {
recvChan <- answer
}
recvChan <- "ok"
}
}
sendQuery := func(name string, querytype uint16) {
receivedAddrs = make([]net.IP, 0)
receivedName = ""
receivedCount = 0
m := new(dns.Msg)
m.SetQuestion(name, querytype)
m.RecursionDesired = false
buf, err := m.Pack()
require.NoError(t, err)
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
require.NoError(t, err)
Log.Debugf("Sending UDP packet to %s", ipv4Addr)
_, err = conn.WriteTo(buf, ipv4Addr)
require.NoError(t, err)
Log.Debugf("Waiting for response")
for {
select {
case x := <-recvChan:
switch rr := x.(type) {
case *dns.A:
t.Logf("... A:\n%+v", rr)
receivedAddrs = append(receivedAddrs, rr.A)
receivedCount++
case *dns.PTR:
t.Logf("... PTR:\n%+v", rr)
receivedName = rr.Ptr
receivedCount++
case string:
return
}
case <-time.After(100 * time.Millisecond):
Log.Debugf("Timeout while waiting for response")
return
}
}
}
listener := &dns.Server{
Unsafe: true,
PacketConn: multicast,
Handler: dns.HandlerFunc(handleMDNS),
ReadTimeout: 100 * time.Millisecond}
go listener.ActivateAndServe()
defer listener.Shutdown()
time.Sleep(100 * time.Millisecond) // Allow for server to get going
Log.Debugf("Checking that we get 2 IPs fo name '%s' [A]", testRecord1.Name())
sendQuery(testRecord1.Name(), dns.TypeA)
if receivedCount != 2 {
t.Fatalf("Unexpected result count %d for %s", receivedCount, testRecord1.Name())
}
if !(receivedAddrs[0].Equal(testRecord1.IP()) || receivedAddrs[0].Equal(testRecord2.IP())) {
t.Fatalf("Unexpected result %s for %s", receivedAddrs, testRecord1.Name())
}
if !(receivedAddrs[1].Equal(testRecord1.IP()) || receivedAddrs[1].Equal(testRecord2.IP())) {
t.Fatalf("Unexpected result %s for %s", receivedAddrs, testRecord1.Name())
}
Log.Debugf("Checking that 'testfail.weave.' [A] gets no answers")
sendQuery("testfail.weave.", dns.TypeA)
if receivedCount != 0 {
t.Fatalf("Unexpected result count %d for testfail.weave", receivedCount)
}
Log.Debugf("Checking that '%s' [PTR] gets one name", testInAddr1)
sendQuery(testInAddr1, dns.TypePTR)
if receivedCount != 1 {
t.Fatalf("Expected an answer to %s, got %d answers", testInAddr1, receivedCount)
} else if !(testRecord1.Name() == receivedName) {
t.Fatalf("Expected answer %s to query for %s, got %s", testRecord1.Name(), testInAddr1, receivedName)
}
}
-170
View File
@@ -1,170 +0,0 @@
package nameserver
import (
"net"
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
// Check that we can use a regular mDNS server with a regular mDNS client
func TestClientServerSimpleQuery(t *testing.T) {
EnableDebugLogging(testing.Verbose())
testRecord1 := Record{"test.weave.local.", net.ParseIP("10.2.2.1"), 0, 0, 0}
testInAddr1 := "1.2.2.10.in-addr.arpa."
mzone := newMockedZoneWithRecords([]ZoneRecord{testRecord1})
mdnsServer, err := NewMDNSServer(mzone, true, DefaultLocalTTL)
require.NoError(t, err)
err = mdnsServer.Start(nil)
require.NoError(t, err)
defer mdnsServer.Stop()
var receivedAddr net.IP
var receivedName string
receivedCount := 0
mdnsCli, err := NewMDNSClient()
require.NoError(t, err)
err = mdnsCli.Start(nil)
require.NoError(t, err)
sendQuery := func(name string, querytype uint16) {
receivedAddr = nil
receivedName = ""
receivedCount = 0
Log.Debugf("Sending query...")
switch querytype {
case dns.TypeA:
r, err := mdnsCli.LookupName(name)
if len(r) > 0 {
receivedAddr = r[0].IP()
if err == nil {
receivedCount++
}
}
case dns.TypePTR:
r, err := mdnsCli.LookupInaddr(name)
if len(r) > 0 {
receivedName = r[0].Name()
if err == nil {
receivedCount++
}
}
}
}
time.Sleep(100 * time.Millisecond) // Allow for server to get going
Log.Debugf("Query: %s dns.TypeA", testRecord1.Name())
sendQuery(testRecord1.Name(), dns.TypeA)
if receivedCount != 1 {
t.Fatalf("Unexpected result count %d for %s", receivedCount, testRecord1.Name())
}
if !receivedAddr.Equal(testRecord1.IP()) {
t.Fatalf("Unexpected result %s for %s", receivedAddr, testRecord1.Name())
}
Log.Debugf("Query: testfail.weave. dns.TypeA")
sendQuery("testfail.weave.", dns.TypeA)
if receivedCount != 0 {
t.Fatalf("Unexpected result count %d for testfail.weave", receivedCount)
}
Log.Debugf("Query: %s dns.TypePTR", testInAddr1)
sendQuery(testInAddr1, dns.TypePTR)
if receivedCount != 1 {
t.Fatalf("Expected an answer to %s, got %d answers", testInAddr1, receivedCount)
} else if !(testRecord1.Name() == receivedName) {
t.Fatalf("Expected answer %s to query for %s, got %s", testRecord1.Name(), testInAddr1, receivedName)
}
}
// Check that we can use a use "insistent" queries
func TestClientServerInsistentQuery(t *testing.T) {
EnableDebugLogging(testing.Verbose())
testRecord1 := Record{"test.weave.local.", net.ParseIP("10.2.2.1"), 0, 0, 0}
testInAddr1 := "1.2.2.10.in-addr.arpa."
testRecord2 := Record{"test.weave.local.", net.ParseIP("10.2.2.2"), 0, 0, 0}
testInAddr2 := "2.2.2.10.in-addr.arpa."
mzone1 := newMockedZoneWithRecords([]ZoneRecord{testRecord1})
mdnsServer1, err := NewMDNSServer(mzone1, true, DefaultLocalTTL)
require.NoError(t, err)
err = mdnsServer1.Start(nil)
require.NoError(t, err)
defer mdnsServer1.Stop()
mzone2 := newMockedZoneWithRecords([]ZoneRecord{testRecord2})
mdnsServer2, err := NewMDNSServer(mzone2, true, DefaultLocalTTL)
require.NoError(t, err)
err = mdnsServer2.Start(nil)
require.NoError(t, err)
defer mdnsServer2.Stop()
// create a third server with exactly the same info as the second server (so we can test duplicates removals)
mdnsServer3, err := NewMDNSServer(mzone2, true, DefaultLocalTTL)
require.NoError(t, err)
err = mdnsServer3.Start(nil)
require.NoError(t, err)
defer mdnsServer3.Stop()
var receivedAddrs []ZoneRecord
var receivedNames []ZoneRecord
receivedCount := 0
mdnsCli, err := NewMDNSClient()
require.NoError(t, err)
err = mdnsCli.Start(nil)
require.NoError(t, err)
sendQuery := func(name string, querytype uint16) {
receivedAddrs = nil
receivedNames = nil
receivedCount = 0
Log.Debugf("Sending query...")
switch querytype {
case dns.TypeA:
receivedAddrs, err = mdnsCli.InsistentLookupName(name)
if err == nil {
receivedCount = len(receivedAddrs)
}
case dns.TypePTR:
receivedNames, err = mdnsCli.InsistentLookupInaddr(name)
if err == nil {
receivedCount = len(receivedNames)
}
}
}
time.Sleep(100 * time.Millisecond) // Allow for server to get going
Log.Debugf("Query: %s dns.TypeA", testRecord1.Name())
sendQuery(testRecord1.Name(), dns.TypeA)
if receivedCount != 2 {
t.Fatalf("Unexpected result count %d for %s", receivedCount, testRecord1.Name())
}
Log.Debugf("Query: testfail.weave. dns.TypeA")
sendQuery("testfail.weave.", dns.TypeA)
if receivedCount != 0 {
t.Fatalf("Unexpected result count %d for testfail.weave", receivedCount)
}
Log.Debugf("Query: %s dns.TypePTR", testInAddr1)
sendQuery(testInAddr1, dns.TypePTR)
if receivedCount != 1 {
t.Fatalf("Expected an answer to %s, got %d answers", testInAddr1, receivedCount)
}
Log.Debugf("Query: %s dns.TypePTR", testInAddr2)
sendQuery(testInAddr2, dns.TypePTR)
if receivedCount != 1 {
t.Fatalf("Expected an answer to %s, got %d answers", testInAddr2, receivedCount)
}
}
-507
View File
@@ -1,507 +0,0 @@
package nameserver
import (
"fmt"
"net"
"strconv"
"sync"
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
const (
testSocketTimeout = 100 // in millisecs
)
// Warn about some methods that some day should be implemented...
func notImplWarn() { Log.Warningf("Mocked method. Not implemented.") }
// A mocked Zone that always returns the same records
// * it does not send/receive any mDNS query
type mockedZoneWithRecords struct {
sync.RWMutex
records []ZoneRecord
// Statistics
NumLookupsName int
NumLookupsInaddr int
}
func newMockedZoneWithRecords(zr []ZoneRecord) *mockedZoneWithRecords {
return &mockedZoneWithRecords{records: zr}
}
func (mz *mockedZoneWithRecords) Domain() string { return DefaultLocalDomain }
func (mz *mockedZoneWithRecords) LookupName(name string) ([]ZoneRecord, error) {
Log.Debugf("[mocked zone]: LookupName: returning records %s", mz.records)
mz.Lock()
defer mz.Unlock()
mz.NumLookupsName++
var res []ZoneRecord
for _, r := range mz.records {
if r.Name() == name {
res = append(res, r)
}
}
return res, nil
}
func (mz *mockedZoneWithRecords) LookupInaddr(inaddr string) ([]ZoneRecord, error) {
Log.Debugf("[mocked zone]: LookupInaddr: returning records %s", mz.records)
mz.Lock()
defer mz.Unlock()
mz.NumLookupsInaddr++
var res []ZoneRecord
for _, r := range mz.records {
revIP, err := raddrToIP(inaddr)
if err != nil {
return nil, newParseError("lookup address", inaddr)
}
if r.IP().Equal(revIP) {
res = append(res, r)
}
}
return res, nil
}
func (mz *mockedZoneWithRecords) DomainLookupName(name string) ([]ZoneRecord, error) {
return mz.LookupName(name)
}
func (mz *mockedZoneWithRecords) DomainLookupInaddr(inaddr string) ([]ZoneRecord, error) {
return mz.LookupInaddr(inaddr)
}
// the following methods are not currently needed...
func (mz *mockedZoneWithRecords) AddRecord(ident string, name string, ip net.IP) error {
notImplWarn()
return nil
}
func (mz *mockedZoneWithRecords) DeleteRecords(ident string, name string, ip net.IP) int {
notImplWarn()
return 0
}
func (mz *mockedZoneWithRecords) Status() string { notImplWarn(); return "nothing" }
func (mz *mockedZoneWithRecords) ObserveName(name string, observer ZoneRecordObserver) error {
notImplWarn()
return nil
}
func (mz *mockedZoneWithRecords) ObserveInaddr(inaddr string, observer ZoneRecordObserver) error {
notImplWarn()
return nil
}
//////////////////////////////////////////////////////////////////
// A mocked mDNS server
type mockedMDNSServer struct {
zone Zone
}
func newMockedMDNSServer(z Zone) *mockedMDNSServer { return &mockedMDNSServer{zone: z} }
func (ms *mockedMDNSServer) Start(_ *net.Interface) error { return nil }
func (ms *mockedMDNSServer) Stop() error { return nil }
func (ms *mockedMDNSServer) Zone() Zone { return ms.zone }
// a useful mix of NewMockedZone and NewMockedMDNSServer
func newMockedMDNSServerWithRecord(zr ZoneRecord) *mockedMDNSServer {
return newMockedMDNSServer(newMockedZoneWithRecords([]ZoneRecord{zr}))
}
func newMockedMDNSServerWithRecords(zrs []ZoneRecord) *mockedMDNSServer {
return newMockedMDNSServer(newMockedZoneWithRecords(zrs))
}
//////////////////////////////////////////////////////////////////
// A mocked mDNS client.
// This mock asks a group of (potentially mocked) mDNS servers
type mockedMDNSClient struct {
sync.RWMutex
servers map[ZoneMDNSServer]ZoneMDNSServer
// Statistics
NumLookupsName int
NumLookupsInaddr int
NumInsLookupsName int
NumInsLookupsInaddr int
}
func newMockedMDNSClient(srvrs []*mockedMDNSServer) *mockedMDNSClient {
r := mockedMDNSClient{
servers: make(map[ZoneMDNSServer]ZoneMDNSServer),
}
if srvrs != nil {
for _, server := range srvrs {
r.servers[server] = server
}
}
return &r
}
func (mc *mockedMDNSClient) Start(ifi *net.Interface) error { return nil }
func (mc *mockedMDNSClient) Stop() error { return nil }
func (mc *mockedMDNSClient) Domain() string { return DefaultLocalDomain }
func (mc *mockedMDNSClient) LookupName(name string) ([]ZoneRecord, error) {
mc.Lock()
defer mc.Unlock()
// get a random result
mc.NumLookupsName++
for _, server := range mc.servers {
r, err := server.Zone().LookupName(name)
if err == nil {
return r, nil // return the first answer
}
}
return nil, LookupError(name)
}
func (mc *mockedMDNSClient) LookupInaddr(inaddr string) ([]ZoneRecord, error) {
mc.Lock()
defer mc.Unlock()
// get a random result
mc.NumLookupsInaddr++
for _, server := range mc.servers {
r, err := server.Zone().LookupInaddr(inaddr)
if err == nil {
return r, nil // return the first answer
}
}
return nil, LookupError(inaddr)
}
func (mc *mockedMDNSClient) InsistentLookupName(name string) ([]ZoneRecord, error) {
mc.Lock()
defer mc.Unlock()
var res []ZoneRecord
mc.NumInsLookupsName++
for _, server := range mc.servers {
r, err := server.Zone().LookupName(name)
if err == nil {
res = append(res, r...)
}
}
if len(res) == 0 {
return nil, LookupError(name)
}
return res, nil
}
func (mc *mockedMDNSClient) InsistentLookupInaddr(inaddr string) ([]ZoneRecord, error) {
mc.Lock()
defer mc.Unlock()
var res []ZoneRecord
mc.NumInsLookupsInaddr++
for _, server := range mc.servers {
r, err := server.Zone().LookupInaddr(inaddr)
if err == nil {
res = append(res, r...)
}
}
if len(res) == 0 {
return nil, LookupError(inaddr)
}
return res, nil
}
func (mc *mockedMDNSClient) AddServer(srv ZoneMDNSServer) {
mc.Lock()
defer mc.Unlock()
mc.servers[srv] = srv
}
func (mc *mockedMDNSClient) RemoveServer(srv ZoneMDNSServer) {
mc.Lock()
defer mc.Unlock()
delete(mc.servers, srv)
}
//////////////////////////////////////////////////////////////////
type zbmEntry struct {
Server *mockedMDNSServer
Client *mockedMDNSClient
Zone *ZoneDb
}
type zoneDbsWithMockedMDns []*zbmEntry
// Creates a group of (real) zone databases linked through mocked mDNS servers and clients
// This effectively creates a cluster of WeaveDNS peers
func newZoneDbsWithMockedMDns(num int, config ZoneConfig) zoneDbsWithMockedMDns {
res := make(zoneDbsWithMockedMDns, num)
for i := 0; i < num; i++ {
res[i] = new(zbmEntry)
Log.Debugf("[test] Creating mocked mDNS server #%d", i)
res[i].Server = newMockedMDNSServer(nil)
Log.Debugf("[test] Creating mocked mDNS client #%d", i)
res[i].Client = newMockedMDNSClient([]*mockedMDNSServer{})
cfg := config
cfg.MDNSClient = res[i].Client
cfg.MDNSServer = res[i].Server
Log.Debugf("[test] Creating ZoneDb #%d", i)
res[i].Zone, _ = NewZoneDb(cfg)
// link the mDNS server to its zone
// ZoneDbs are connected to mDNS in two directions: they use their mDNS
// clients for asking other peers about names/IPs, and export their records
// through a mDNS server.
res[i].Server.zone = res[i].Zone
}
// link all the clients to all the servers
for i := 0; i < num; i++ {
for j := 0; j < num; j++ {
// with this check, the local client will not ask to the local server
// this is not what we currently do, but can be convenient in some cases
// for finding some bugs...
if i != j {
Log.Debugf("[test] Linking mocked mDNS client #%d to server #%d", i, j)
res[i].Client.AddServer(res[j].Server)
}
}
}
return res
}
func (zbs zoneDbsWithMockedMDns) Start() {
for _, entry := range zbs {
entry.Zone.Start()
}
}
func (zbs zoneDbsWithMockedMDns) Stop() {
for _, entry := range zbs {
entry.Zone.Stop()
}
}
func (zbs zoneDbsWithMockedMDns) Flush() {
for _, entry := range zbs {
entry.Zone.refreshScheds.Flush()
}
}
//////////////////////////////////////////////////////////////////
// A mocked cache where we never find a single thing... ;)
type mockedCache struct {
NumGets int
NumPuts int
NumRemovals int
}
func newMockedCache() *mockedCache { return &mockedCache{} }
func (c *mockedCache) Get(request *dns.Msg, maxLen int) (reply *dns.Msg, err error) {
c.NumGets++
return nil, nil
}
func (c *mockedCache) Put(request *dns.Msg, reply *dns.Msg, ttl int, flags uint8) int {
c.NumPuts++
return 0
}
func (c *mockedCache) Remove(question *dns.Question) { c.NumRemovals++ }
func (c *mockedCache) Purge() {}
func (c *mockedCache) Clear() {}
func (c *mockedCache) Len() int { return 0 }
func (c *mockedCache) Capacity() int { return DefaultCacheLen }
//////////////////////////////////////////////////////////////////
// A mocked fallback server
type mockedFallback struct {
CliConfig *dns.ClientConfig
Addr string
Port int
udpSrv *dns.Server
tcpSrv *dns.Server
}
// Create a mocked fallback server
// You can use the server's `CliConfig` as the `UpstreamCfg` of a `DNSServer`...
func newMockedFallback(udpH dns.HandlerFunc, tcpH dns.HandlerFunc) (*mockedFallback, error) {
udpSrv, fallbackAddr, err := runLocalUDPServer("127.0.0.1:0", udpH)
if err != nil {
return nil, err
}
_, fallbackPort, err := net.SplitHostPort(fallbackAddr)
if err != nil {
return nil, err
}
fallbackPortI, _ := strconv.Atoi(fallbackPort)
res := mockedFallback{
udpSrv: udpSrv,
Addr: fallbackAddr,
Port: fallbackPortI,
CliConfig: &dns.ClientConfig{
Servers: []string{"127.0.0.1"},
Port: fallbackPort,
},
}
if tcpH != nil {
fallbackTCPAddr := fmt.Sprintf("127.0.0.1:%s", fallbackPort)
res.tcpSrv, _, err = runLocalTCPServer(fallbackTCPAddr, tcpH)
if err != nil {
return nil, err
}
}
return &res, nil
}
// Start the fallback server
func (mf *mockedFallback) Start() error {
return nil
}
// Stop the fallback server
func (mf *mockedFallback) Stop() error {
if mf.tcpSrv != nil {
mf.udpSrv.Shutdown()
mf.udpSrv = nil
}
if mf.tcpSrv != nil {
mf.tcpSrv.Shutdown()
mf.udpSrv = nil
}
return nil
}
// Run a UDP fallback server
func runLocalUDPServer(laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) {
Log.Debugf("[mocked fallback] Starting fallback UDP server at %s", laddr)
pc, err := net.ListenPacket("udp", laddr)
if err != nil {
return nil, "", err
}
server := &dns.Server{PacketConn: pc, Handler: handler, ReadTimeout: testSocketTimeout * time.Millisecond}
go func() {
server.ActivateAndServe()
pc.Close()
}()
Log.Debugf("[mocked fallback] Fallback UDP server listening at %s", pc.LocalAddr())
return server, pc.LocalAddr().String(), nil
}
// Run a TCP fallback server
func runLocalTCPServer(laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) {
Log.Debugf("[mocked fallback] Starting fallback TCP server at %s", laddr)
laddrTCP, err := net.ResolveTCPAddr("tcp", laddr)
if err != nil {
return nil, "", err
}
l, err := net.ListenTCP("tcp", laddrTCP)
if err != nil {
return nil, "", err
}
server := &dns.Server{Listener: l, Handler: handler, ReadTimeout: testSocketTimeout * time.Millisecond}
go func() {
server.ActivateAndServe()
l.Close()
}()
Log.Debugf("[mocked fallback] Fallback TCP server listening at %s", l.Addr().String())
return server, l.Addr().String(), nil
}
//////////////////////////////////////////////////////////////////
type mockedClock struct {
*clock.Mock
}
func newMockedClock() *mockedClock {
return &mockedClock{clock.NewMock()}
}
// Note: moving the clock forward does not take into account the data that could
// be waiting in channels. So your code should not depend on the channels
// for creating new timers. Otherwise, time travelling will not be reliable...
func (clk *mockedClock) Forward(secs int) {
Log.Debugf(">>>>>>> Moving clock forward %d seconds - Time traveling >>>>>>>", secs)
clk.Add(time.Duration(secs) * time.Second)
Log.Debugf("<<<<<<< Time travel finished! We are at %s <<<<<<<", clk.Now())
}
//////////////////////////////////////////////////////////////////
// Perform a DNS query and assert the reply code, number or answers, etc
func assertExchange(t *testing.T, z string, ty uint16, port int, minAnswers int, maxAnswers int, expErr int) (*dns.Msg, *dns.Msg) {
require.NotEqual(t, 0, port, "invalid DNS server port")
c := &dns.Client{
UDPSize: testUDPBufSize,
}
m := new(dns.Msg)
m.RecursionDesired = true
m.SetQuestion(z, ty)
m.SetEdns0(testUDPBufSize, false) // we don't want to play with truncation here...
lstAddr := fmt.Sprintf("127.0.0.1:%d", port)
r, _, err := c.Exchange(m, lstAddr)
t.Logf("Response from '%s':\n%+v\n", lstAddr, r)
if err != nil {
t.Errorf("Error when querying DNS server at %s: %s", lstAddr, err)
}
require.NoError(t, err)
if minAnswers == 0 && maxAnswers == 0 {
require.Equal(t, expErr, r.Rcode, "DNS response code")
} else {
require.Equal(t, dns.RcodeSuccess, r.Rcode, "DNS response code")
}
answers := len(r.Answer)
if minAnswers >= 0 && answers < minAnswers {
require.FailNow(t, fmt.Sprintf("Number of answers >= %d", minAnswers))
}
if maxAnswers >= 0 && answers > maxAnswers {
require.FailNow(t, fmt.Sprintf("Number of answers <= %d", maxAnswers))
}
return m, r
}
// Assert that we have a response in the cache for a query `q`
func assertInCache(t *testing.T, cache ZoneCache, q *dns.Msg, desc string) {
r, err := cache.Get(q, maxUDPSize)
require.NoError(t, err)
require.NotNil(t, r, fmt.Sprintf("value in the cache: %s", desc))
}
// Assert that we have a response in the cache for a query `q`
func assertNotLocalInCache(t *testing.T, cache ZoneCache, q *dns.Msg, desc string) {
r, err := cache.Get(q, maxUDPSize)
if !(r == nil && err == errNoLocalReplies) {
t.Fatalf("Cache does not return a noLocalReplies error for query %s", q)
}
}
// Assert that we do not have a response in the cache for a query `q`
func assertNotInCache(t *testing.T, cache ZoneCache, q *dns.Msg, desc string) {
r, err := cache.Get(q, maxUDPSize)
require.NoError(t, err)
require.Nil(t, r, fmt.Sprintf("value in the cache: %s\n%s", desc, r))
}
+251
View File
@@ -0,0 +1,251 @@
package nameserver
import (
"bytes"
"encoding/gob"
"fmt"
"sync"
"time"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/common/docker"
"github.com/weaveworks/weave/net/address"
"github.com/weaveworks/weave/router"
)
const (
// Tombstones do not need to survive long periods of peer disconnection, as
// we delete entries for disconnected peers. Therefore they just need to hang
// around to account for propagation delay through gossip. 10 minutes sounds
// long enough.
tombstoneTimeout = time.Minute * 10
// Used by prog/weaver/main.go and proxy/create_container_interceptor.go
DefaultDomain = "weave.local."
)
// Nameserver: gossip-based, in memory nameserver.
// - Holds a sorted list of (hostname, peer, container id, ip) tuples for the whole cluster.
// - This list is gossiped & merged around the cluser.
// - Lookup-by-hostname are O(nlogn), and return a (copy of a) slice of the entries
// - Update is O(n) for now
type Nameserver struct {
sync.RWMutex
ourName router.PeerName
domain string
gossip router.Gossip
docker *docker.Client
entries Entries
peers *router.Peers
quit chan struct{}
}
func New(ourName router.PeerName, peers *router.Peers, docker *docker.Client, domain string) *Nameserver {
ns := &Nameserver{
ourName: ourName,
domain: dns.Fqdn(domain),
peers: peers,
docker: docker,
quit: make(chan struct{}),
}
if peers != nil {
peers.OnGC(ns.PeerGone)
}
return ns
}
func (n *Nameserver) SetGossip(gossip router.Gossip) {
n.gossip = gossip
}
func (n *Nameserver) Start() {
go func() {
ticker := time.Tick(tombstoneTimeout)
for {
select {
case <-n.quit:
return
case <-ticker:
n.deleteTombstones()
}
}
}()
}
func (n *Nameserver) Stop() {
n.quit <- struct{}{}
}
func (n *Nameserver) AddEntry(hostname, containerid string, origin router.PeerName, addr address.Address) error {
n.infof("adding entry %s -> %s", hostname, addr.String())
n.Lock()
entry := n.entries.add(hostname, containerid, origin, addr)
n.Unlock()
if n.gossip != nil {
return n.gossip.GossipBroadcast(&Entries{entry})
}
return nil
}
func (n *Nameserver) Lookup(hostname string) []address.Address {
n.RLock()
defer n.RUnlock()
entries := n.entries.lookup(hostname)
result := []address.Address{}
for _, e := range entries {
if e.Tombstone > 0 {
continue
}
result = append(result, e.Addr)
}
n.debugf("lookup %s -> %s", hostname, &result)
return result
}
func (n *Nameserver) ReverseLookup(ip address.Address) (string, error) {
n.RLock()
defer n.RUnlock()
match, err := n.entries.first(func(e *Entry) bool {
return e.Addr == ip
})
if err != nil {
return "", err
}
n.debugf("reverse lookup %s -> %s", ip, match.Hostname)
return match.Hostname, nil
}
func (n *Nameserver) ContainerDied(ident string) error {
n.Lock()
entries := n.entries.tombstone(n.ourName, func(e *Entry) bool {
if e.ContainerID == ident {
n.infof("container %s died, tombstoning entry %s", ident, e.String())
return true
}
return false
})
n.Unlock()
if n.gossip != nil {
return n.gossip.GossipBroadcast(entries)
}
return nil
}
func (n *Nameserver) PeerGone(peer *router.Peer) {
n.Lock()
defer n.Unlock()
n.entries.filter(func(e *Entry) bool {
return e.Origin != peer.Name
})
}
func (n *Nameserver) Delete(hostname, containerid, ipStr string, ip address.Address) error {
n.Lock()
n.infof("tombstoning hostname=%s, containerid=%s, ip=%s", hostname, containerid, ipStr)
entries := n.entries.tombstone(n.ourName, func(e *Entry) bool {
if hostname != "*" && e.Hostname != hostname {
return false
}
if containerid != "*" && e.ContainerID != containerid {
return false
}
if ipStr != "*" && e.Addr != ip {
return false
}
return true
})
n.Unlock()
if n.gossip != nil {
return n.gossip.GossipBroadcast(entries)
}
return nil
}
func (n *Nameserver) deleteTombstones() {
n.Lock()
defer n.Unlock()
now := time.Now().Unix()
n.entries.filter(func(e *Entry) bool {
return now-e.Tombstone <= int64(tombstoneTimeout/time.Second)
})
}
func (n *Nameserver) String() string {
n.RLock()
defer n.RUnlock()
var buf bytes.Buffer
for _, entry := range n.entries {
if entry.Tombstone > 0 {
continue
}
containerid := entry.ContainerID
if len(containerid) > 12 {
containerid = containerid[:12]
}
fmt.Fprintf(&buf, "%s: %s [%s]\n", containerid, entry.Hostname, entry.Addr.String())
}
return buf.String()
}
func (n *Nameserver) Gossip() router.GossipData {
n.RLock()
defer n.RUnlock()
result := make(Entries, len(n.entries))
copy(result, n.entries)
return &result
}
func (n *Nameserver) OnGossipUnicast(sender router.PeerName, msg []byte) error {
return nil
}
func (n *Nameserver) receiveGossip(msg []byte) (router.GossipData, router.GossipData, error) {
var entries Entries
if err := gob.NewDecoder(bytes.NewReader(msg)).Decode(&entries); err != nil {
return nil, nil, err
}
if err := entries.check(); err != nil {
return nil, nil, err
}
n.Lock()
defer n.Unlock()
if n.peers != nil {
entries.filter(func(e *Entry) bool {
return n.peers.Fetch(e.Origin) != nil
})
}
newEntries := n.entries.merge(entries)
return &newEntries, &entries, nil
}
// merge received data into state and return "everything new I've
// just learnt", or nil if nothing in the received data was new
func (n *Nameserver) OnGossip(msg []byte) (router.GossipData, error) {
newEntries, _, err := n.receiveGossip(msg)
return newEntries, err
}
// merge received data into state and return a representation of
// the received data, for further propagation
func (n *Nameserver) OnGossipBroadcast(msg []byte) (router.GossipData, error) {
_, entries, err := n.receiveGossip(msg)
return entries, err
}
func (n *Nameserver) infof(fmt string, args ...interface{}) {
Log.Infof("[nameserver %s] "+fmt, append([]interface{}{n.ourName}, args...)...)
}
func (n *Nameserver) debugf(fmt string, args ...interface{}) {
Log.Debugf("[nameserver %s] "+fmt, append([]interface{}{n.ourName}, args...)...)
}
+248
View File
@@ -0,0 +1,248 @@
package nameserver
import (
"fmt"
"math/rand"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/weaveworks/weave/net/address"
"github.com/weaveworks/weave/router"
"github.com/weaveworks/weave/testing/gossip"
)
func makeNetwork(size int) ([]*Nameserver, *gossip.TestRouter) {
gossipRouter := gossip.NewTestRouter(0.0)
nameservers := make([]*Nameserver, size)
for i := 0; i < size; i++ {
name, _ := router.PeerNameFromString(fmt.Sprintf("%02d:00:00:02:00:00", i))
nameserver := New(name, nil, nil, "")
nameserver.SetGossip(gossipRouter.Connect(nameserver.ourName, nameserver))
nameserver.Start()
nameservers[i] = nameserver
}
return nameservers, gossipRouter
}
func stopNetwork(nameservers []*Nameserver) {
for _, nameserver := range nameservers {
nameserver.Stop()
}
}
type pair struct {
origin router.PeerName
addr address.Address
}
type mapping struct {
hostname string
addrs []pair
}
type addrs []address.Address
func (a addrs) Len() int { return len(a) }
func (a addrs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a addrs) Less(i, j int) bool { return a[i] < a[j] }
func (a addrs) String() string {
ss := []string{}
for _, addr := range a {
ss = append(ss, addr.String())
}
return strings.Join(ss, " ")
}
func (m mapping) Addrs() []address.Address {
want := addrs{}
for _, p := range m.addrs {
want = append(want, p.addr)
}
sort.Sort(want)
return want
}
func TestNameservers(t *testing.T) {
//common.SetLogLevel("debug")
lookupTimeout := 20 // ms
nameservers, grouter := makeNetwork(50)
defer stopNetwork(nameservers)
nameserversByName := map[router.PeerName]*Nameserver{}
for _, n := range nameservers {
nameserversByName[n.ourName] = n
}
mappings := []mapping{}
check := func(nameserver *Nameserver, expected mapping) {
have := []address.Address{}
for i := 0; i < lookupTimeout; i++ {
have = nameserver.Lookup(expected.hostname)
sort.Sort(addrs(have))
if reflect.DeepEqual(have, expected.Addrs()) {
return
}
time.Sleep(1 * time.Millisecond)
}
want := expected.Addrs()
require.Equal(t, addrs(want).String(), addrs(have).String())
}
addMapping := func() {
nameserver := nameservers[rand.Intn(len(nameservers))]
addr := address.Address(rand.Int31())
hostname := fmt.Sprintf("hostname%d", rand.Int63())
mapping := mapping{hostname, []pair{{nameserver.ourName, addr}}}
mappings = append(mappings, mapping)
require.Nil(t, nameserver.AddEntry(hostname, "", nameserver.ourName, addr))
check(nameserver, mapping)
}
addExtraMapping := func() {
if len(mappings) <= 0 {
return
}
nameserver := nameservers[rand.Intn(len(nameservers))]
i := rand.Intn(len(mappings))
mapping := mappings[i]
addr := address.Address(rand.Int31())
mapping.addrs = append(mapping.addrs, pair{nameserver.ourName, addr})
mappings[i] = mapping
require.Nil(t, nameserver.AddEntry(mapping.hostname, "", nameserver.ourName, addr))
check(nameserver, mapping)
}
deleteMapping := func() {
if len(mappings) <= 0 {
return
}
i := rand.Intn(len(mappings))
mapping := mappings[i]
if len(mapping.addrs) <= 0 {
return
}
j := rand.Intn(len(mapping.addrs))
pair := mapping.addrs[j]
mapping.addrs = append(mapping.addrs[:j], mapping.addrs[j+1:]...)
mappings[i] = mapping
nameserver := nameserversByName[pair.origin]
require.Nil(t, nameserver.Delete(mapping.hostname, "*", pair.addr.String(), pair.addr))
check(nameserver, mapping)
}
doLookup := func() {
if len(mappings) <= 0 {
return
}
mapping := mappings[rand.Intn(len(mappings))]
nameserver := nameservers[rand.Intn(len(nameservers))]
check(nameserver, mapping)
}
doReverseLookup := func() {
if len(mappings) <= 0 {
return
}
mapping := mappings[rand.Intn(len(mappings))]
if len(mapping.addrs) <= 0 {
return
}
nameserver := nameservers[rand.Intn(len(nameservers))]
hostname := ""
var err error
for i := 0; i < lookupTimeout; i++ {
hostname, err = nameserver.ReverseLookup(mapping.addrs[0].addr)
if err != nil && mapping.hostname == hostname {
return
}
time.Sleep(1 * time.Millisecond)
}
require.Nil(t, err)
require.Equal(t, mapping.hostname, hostname)
}
for i := 0; i < 1000; i++ {
if i%10 == 0 {
grouter.Flush()
}
r := rand.Float32()
switch {
case r < 0.1:
addMapping()
case 0.1 <= r && r < 0.2:
addExtraMapping()
case 0.2 <= r && r < 0.3:
deleteMapping()
case 0.3 <= r && r < 0.9:
doLookup()
case 0.9 <= r:
doReverseLookup()
}
}
}
func TestContainerAndPeerDeath(t *testing.T) {
peername, err := router.PeerNameFromString("00:00:00:02:00:00")
require.Nil(t, err)
nameserver := New(peername, nil, nil, "")
err = nameserver.AddEntry("hostname", "containerid", peername, address.Address(0))
require.Nil(t, err)
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
err = nameserver.ContainerDied("containerid")
require.Nil(t, err)
require.Equal(t, []address.Address{}, nameserver.Lookup("hostname"))
err = nameserver.AddEntry("hostname", "containerid", peername, address.Address(0))
require.Nil(t, err)
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
nameserver.PeerGone(&router.Peer{Name: peername})
require.Equal(t, []address.Address{}, nameserver.Lookup("hostname"))
}
func TestTombstoneDeletion(t *testing.T) {
oldNow := now
defer func() { now = oldNow }()
now = func() int64 { return 1234 }
peername, err := router.PeerNameFromString("00:00:00:02:00:00")
require.Nil(t, err)
nameserver := New(peername, nil, nil, "")
err = nameserver.AddEntry("hostname", "containerid", peername, address.Address(0))
require.Nil(t, err)
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
err = nameserver.Delete("hostname", "containerid", "", address.Address(0))
require.Nil(t, err)
require.Equal(t, []address.Address{}, nameserver.Lookup("hostname"))
require.Equal(t, Entries{{
ContainerID: "containerid",
Origin: peername,
Addr: address.Address(0),
Hostname: "hostname",
Version: 1,
Tombstone: 1234,
}}, nameserver.entries)
now = func() int64 { return 1234 + int64(tombstoneTimeout/time.Second) + 1 }
nameserver.deleteTombstones()
require.Equal(t, Entries{}, nameserver.entries)
}
-414
View File
@@ -1,414 +0,0 @@
package nameserver
import (
"bytes"
"fmt"
"net"
"strconv"
"sync"
"time"
"github.com/benbjohnson/clock"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
)
const (
DefaultServerPort = 53 // The default server port
DefaultHTTPPort = 6785 // The default http port
DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load
DefaultLocalTTL = 30 // default TTL for responses for local domain queries
DefaultUDPBuflen = 4096 // bigger than the default 512
DefaultCacheLen = 8192 // default cache capacity
DefaultTimeout = 5000 // default timeout for DNS resolutions (millisecs)
DefaultMaxAnswers = 1 // default number of answers provided to users
)
type DNSServerConfig struct {
// The zone
Zone Zone
// (Optional) client config file for resolving upstream servers
UpstreamCfgFile string
// (Optional) DNS client config for the fallback server(s)
UpstreamCfg *dns.ClientConfig
// (Optional) port number (for TCP and UDP)
Port int
// (Optional) cache size
CacheLen int
// (Optional) disable the cache
CacheDisabled bool
// (Optional) timeout for DNS queries
Timeout int
// (Optional) UDP buffer length
UDPBufLen int
// (Optional) force a specific cache
Cache ZoneCache
// (Optional) TTL for local domain responses
LocalTTL int
// (Optional) TTL for negative results in the local domain (defaults to LocalTTL)
CacheNegLocalTTL int
// (Optional) for a specific clock provider
Clock clock.Clock
// (Optional) Listening socket read timeout (in milliseconds)
ListenReadTimeout int
// Maximum number of answers provided to users
MaxAnswers int
}
type dnsProtocol uint8
const (
protUDP dnsProtocol = iota // UDP protocol
protTCP dnsProtocol = iota // TCP protocol
)
func (proto dnsProtocol) String() string {
switch proto {
case protUDP:
return "UDP"
case protTCP:
return "TCP"
}
return "unknown"
}
// get a new dns.Client for a protocol
func (proto dnsProtocol) GetNewClient(bufsize int, timeout time.Duration) *dns.Client {
Log.Debugf("[dns] Creating %s DNS client with %s timeout", proto, timeout)
switch proto {
case protTCP:
return &dns.Client{Net: "tcp", ReadTimeout: timeout}
case protUDP:
return &dns.Client{Net: "udp", ReadTimeout: timeout, UDPSize: uint16(bufsize)}
}
return nil
}
// a DNS server
type DNSServer struct {
Zone Zone
Upstream *dns.ClientConfig
Domain string // the local domain
ListenAddr string // the address the server is listening at
udpSrv *dns.Server
tcpSrv *dns.Server
pc net.PacketConn
lst net.Listener
cache ZoneCache
cacheDisabled bool
maxAnswers int
localTTL int
negLocalTTL int
timeout time.Duration
readTimeout time.Duration
udpBuf int
listenersWg *sync.WaitGroup
clock clock.Clock
}
// Creates a new DNS server
func NewDNSServer(config DNSServerConfig) (s *DNSServer, err error) {
s = &DNSServer{
Zone: config.Zone,
Domain: DefaultLocalDomain,
ListenAddr: fmt.Sprintf(":%d", config.Port),
listenersWg: new(sync.WaitGroup),
timeout: DefaultTimeout * time.Millisecond,
readTimeout: DefaultTimeout * time.Millisecond,
cacheDisabled: false,
maxAnswers: DefaultMaxAnswers,
localTTL: DefaultLocalTTL,
clock: config.Clock,
}
// check some basic parameters are valid
if s.Zone == nil {
return nil, fmt.Errorf("No valid Zone provided in server initialization")
}
if len(s.Domain) == 0 {
return nil, fmt.Errorf("No valid Domain provided in server initialization")
}
if s.clock == nil {
s.clock = clock.New()
}
// fill empty parameters with defaults...
if config.UpstreamCfg != nil {
s.Upstream = config.UpstreamCfg
} else {
cfgFile := DefaultCLICfgFile
if len(config.UpstreamCfgFile) > 0 {
cfgFile = config.UpstreamCfgFile
}
if s.Upstream, err = dns.ClientConfigFromFile(cfgFile); err != nil {
return nil, err
}
}
if config.Timeout > 0 {
s.timeout = time.Duration(config.Timeout) * time.Millisecond
}
if config.ListenReadTimeout > 0 {
s.readTimeout = time.Duration(config.ListenReadTimeout) * time.Millisecond
}
if config.UDPBufLen > 0 {
s.udpBuf = config.UDPBufLen
}
if config.MaxAnswers > 0 {
s.maxAnswers = config.MaxAnswers
}
if config.LocalTTL > 0 {
s.localTTL = config.LocalTTL
}
if config.CacheNegLocalTTL > 0 {
s.negLocalTTL = config.CacheNegLocalTTL
} else {
s.negLocalTTL = s.localTTL
}
if config.CacheDisabled {
s.cacheDisabled = true
}
if !s.cacheDisabled {
if config.Cache != nil {
s.cache = config.Cache
} else {
cacheLen := DefaultCacheLen
if config.CacheLen > 0 {
cacheLen = config.CacheLen
}
if s.cache, err = NewCache(cacheLen, s.clock); err != nil {
return
}
}
}
return
}
// Start the DNS server
func (s *DNSServer) Start() error {
Log.Infof("[dns] Upstream server(s): %+v", s.Upstream)
if s.cacheDisabled {
Log.Infof("[dns] Cache: disabled")
} else {
Log.Infof("[dns] Cache: %d entries", s.cache.Capacity())
}
pc, err := net.ListenPacket("udp", s.ListenAddr)
if err != nil {
return err
}
s.pc = pc
_, port, err := net.SplitHostPort(pc.LocalAddr().String())
if err != nil {
return err
}
s.ListenAddr = fmt.Sprintf(":%s", port)
s.udpSrv = &dns.Server{PacketConn: s.pc, Handler: s.createMux(protUDP), ReadTimeout: s.readTimeout}
// Bind the TCP socket at the same port, aborting otherwise
l, err := net.Listen("tcp", s.ListenAddr)
if err != nil {
s.Stop()
return err
}
s.lst = l
s.tcpSrv = &dns.Server{Listener: l, Handler: s.createMux(protTCP), ReadTimeout: s.readTimeout}
s.listenersWg.Add(2)
return nil
}
func (s *DNSServer) ActivateAndServe() {
go func() {
defer s.listenersWg.Done()
Log.Infof("[dns] Listening for DNS on %s (UDP)", s.ListenAddr)
err := s.udpSrv.ActivateAndServe()
CheckFatal(err)
Log.Debugf("[dns] DNS UDP server exiting...")
}()
go func() {
defer s.listenersWg.Done()
Log.Infof("[dns] Listening for DNS on %s (TCP)", s.ListenAddr)
err := s.tcpSrv.ActivateAndServe()
CheckFatal(err)
Log.Debugf("[dns] DNS TCP server exiting...")
}()
// Waiting for all goroutines to finish (otherwise they die as main routine dies)
s.listenersWg.Wait()
Log.Infof("[dns] Server exiting...")
}
// Return status string
func (s *DNSServer) Status() string {
var buf bytes.Buffer
fmt.Fprintln(&buf, "Listen address", s.ListenAddr)
fmt.Fprintln(&buf, "Fallback DNS config", s.Upstream)
return buf.String()
}
// Perform a graceful shutdown
func (s *DNSServer) Stop() error {
// Stop the listeners/handlers
if s.tcpSrv != nil {
if err := s.tcpSrv.Shutdown(); err != nil {
return err
}
s.lst.Close()
s.tcpSrv = nil
}
if s.udpSrv != nil {
if err := s.udpSrv.Shutdown(); err != nil {
return err
}
s.pc.Close()
s.udpSrv = nil
}
s.listenersWg.Wait()
return nil
}
// Create a multiplexer for requests
// We must create two DNS request multiplexers, depending on the protocol used by
// clients (as we use the same protocol for asking upstream servers)
func (s *DNSServer) createMux(proto dnsProtocol) *dns.ServeMux {
failFallback := func(w dns.ResponseWriter, r *dns.Msg) {
w.WriteMsg(makeDNSFailResponse(r))
}
notUsHandler := s.notUsHandler(proto)
notUsFallback := func(w dns.ResponseWriter, r *dns.Msg) {
Log.Infof("[dns msgid %d] -> sending to fallback server", r.MsgHdr.Id)
notUsHandler(w, r)
}
// create the multiplexer
m := dns.NewServeMux()
m.HandleFunc(s.Zone.Domain(), s.localHandler(proto, "Query", dns.TypeA,
s.Zone.DomainLookupName, makeAddressReply, s.Zone.ObserveName, failFallback))
m.HandleFunc(RDNSDomain, s.localHandler(proto, "Reverse query", dns.TypePTR,
s.Zone.DomainLookupInaddr, makePTRReply, s.Zone.ObserveInaddr, notUsFallback))
m.HandleFunc(".", s.notUsHandler(proto))
return m
}
// Process a request for a name/address in the '.weave.local.' zone
func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16,
lookup func(inaddr string) ([]ZoneRecord, error),
msgBuilder DNSResponseBuilder, observer ZoneObserverFunc, fallback dns.HandlerFunc) dns.HandlerFunc {
return func(w dns.ResponseWriter, r *dns.Msg) {
q := r.Question[0]
Log.Debugf("[dns msgid %d] %s: %+v", r.MsgHdr.Id, kind, q)
maxLen := getMaxReplyLen(r, proto)
// cache a response if the cache is enabled, and observe the name/IP
maybeCache := func(m *dns.Msg, ttl int, flags uint8) {
if !s.cacheDisabled {
s.cache.Put(r, m, ttl, flags)
// any change in the Zone database for this IP/name will lead to this
// cache entry being removed...
// TODO: this closure results in unnecessary `Remove`s and some wasted
// mem... but we can live with that.
observer(q.Name, func() { s.cache.Remove(&q) })
}
}
if !s.cacheDisabled {
reply, err := s.cache.Get(r, maxLen)
if err != nil {
if err == errNoLocalReplies {
Log.Debugf("[dns msgid %d] Cached no-local-replies", r.MsgHdr.Id)
fallback(w, r)
} else {
Log.Debugf("[dns msgid %d] Error from cache: %s", r.MsgHdr.Id, err)
w.WriteMsg(makeDNSFailResponse(r))
}
return
}
if reply != nil {
reply.Answer = pruneAnswers(shuffleAnswers(reply.Answer), s.maxAnswers)
Log.Debugf("[dns msgid %d] Returning reply from cache: %s/%d answers",
r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer))
w.WriteMsg(reply)
return
}
}
// catch unsupported queries
if q.Qtype != qtype {
Log.Debugf("[dns msgid %d] Unsupported query type %s", r.MsgHdr.Id, dns.TypeToString[q.Qtype])
m := makeDNSFailResponse(r)
maybeCache(m, s.negLocalTTL, 0)
w.WriteMsg(m)
return
}
if answers, err := lookup(q.Name); err != nil {
Log.Infof("[dns msgid %d] No results for type %s query for '%s' [caching no-local]",
r.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name)
maybeCache(nil, s.negLocalTTL, CacheNoLocalReplies)
fallback(w, r)
} else {
m := msgBuilder(r, &q, answers, s.localTTL)
m.Authoritative = true
maybeCache(m, nullTTL, 0)
m.Answer = pruneAnswers(shuffleAnswers(m.Answer), s.maxAnswers)
Log.Debugf("[dns msgid %d] Sending response: %s/%d answers [code:%s]",
m.MsgHdr.Id, dns.TypeToString[q.Qtype], len(m.Answer), dns.RcodeToString[m.Rcode])
w.WriteMsg(m)
}
}
}
// When we receive a request for a name outside of our '.weave.local.'
// domain, ask the configured DNS server as a fallback.
func (s *DNSServer) notUsHandler(proto dnsProtocol) dns.HandlerFunc {
dnsClient := proto.GetNewClient(DefaultUDPBuflen, s.timeout)
return func(w dns.ResponseWriter, r *dns.Msg) {
q := r.Question[0]
// announce our max payload size as the max payload our client supports
maxLen := getMaxReplyLen(r, proto)
rcopy := r
rcopy.SetEdns0(uint16(maxLen), false)
Log.Debugf("[dns msgid %d] Fallback query: %+v [%s, max:%d bytes]", rcopy.MsgHdr.Id, q, proto, maxLen)
for _, server := range s.Upstream.Servers {
reply, _, err := dnsClient.Exchange(rcopy, fmt.Sprintf("%s:%s", server, s.Upstream.Port))
if err != nil {
Log.Debugf("[dns msgid %d] Network error trying %s (%s)",
r.MsgHdr.Id, server, err)
continue
}
if reply != nil && reply.Rcode != dns.RcodeSuccess {
Log.Debugf("[dns msgid %d] Failure reported by %s for query %s",
r.MsgHdr.Id, server, q.Name)
continue
}
Log.Debugf("[dns msgid %d] Given answer by %s for query %s",
r.MsgHdr.Id, server, q.Name)
w.WriteMsg(reply)
return
}
Log.Warningf("[dns msgid %d] Failed lookup for external name %s", r.MsgHdr.Id, q.Name)
w.WriteMsg(makeDNSFailResponse(r))
}
}
// Get the listen port
func (s *DNSServer) GetPort() (int, error) {
_, portS, err := net.SplitHostPort(s.ListenAddr)
if err != nil {
return 0, err
}
return strconv.Atoi(portS)
}
-351
View File
@@ -1,351 +0,0 @@
package nameserver
import (
"fmt"
"net"
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
// Check that AddRecord/DeleteRecord/... in the Zone database lead to cache invalidations
func TestServerDbCacheInvalidation(t *testing.T) {
const (
containerID = "somecontainer"
testName1 = "first.weave.local."
testName2 = "second.weave.local."
)
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestServerDbCacheInvalidation starting")
clk := newMockedClock()
Log.Debugf("Creating mocked mDNS client and server")
mdnsServer1 := newMockedMDNSServerWithRecord(Record{testName1, net.ParseIP("10.2.2.9"), 0, 0, 0})
mdnsCli1 := newMockedMDNSClient([]*mockedMDNSServer{mdnsServer1})
Log.Debugf("Creating zone database with the mocked mDNS client and server")
zoneConfig := ZoneConfig{
MDNSServer: mdnsServer1,
MDNSClient: mdnsCli1,
Clock: clk,
}
zone, err := NewZoneDb(zoneConfig)
require.NoError(t, err)
err = zone.Start()
require.NoError(t, err)
defer zone.Stop()
Log.Debugf("Creating a cache")
cache, err := NewCache(1024, clk)
require.NoError(t, err)
fallbackHandler := func(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
if len(req.Question) == 1 {
m.Rcode = dns.RcodeNameError
}
w.WriteMsg(m)
}
// Run another DNS server for fallback
fallback, err := newMockedFallback(fallbackHandler, nil)
require.NoError(t, err)
fallback.Start()
defer fallback.Stop()
Log.Debugf("Creating a real DNS server with a mocked cache")
srv, err := NewDNSServer(DNSServerConfig{
Zone: zone,
Cache: cache,
Clock: clk,
ListenReadTimeout: testSocketTimeout,
UpstreamCfg: fallback.CliConfig,
MaxAnswers: 4,
})
require.NoError(t, err)
err = srv.Start()
require.NoError(t, err)
go srv.ActivateAndServe()
defer srv.Stop()
time.Sleep(100 * time.Millisecond) // Allow server goroutine to start
testPort, err := srv.GetPort()
require.NoError(t, err)
require.NotEqual(t, 0, testPort, "invalid listen port")
Log.Debugf("Adding two IPs to %s", testName1)
zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.1"))
zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.2"))
q, _ := assertExchange(t, testName1, dns.TypeA, testPort, 2, 2, 0)
assertInCache(t, cache, q, fmt.Sprintf("after asking for %s", testName1))
// Zone database at this point:
// first.weave.local = 10.2.2.1 10.2.2.2
zone.AddRecord(containerID, testName2, net.ParseIP("10.9.9.1"))
assertInCache(t, cache, q, fmt.Sprintf("after adding a new IP for %s", testName2))
// we should have an entry in the cache for this query
// if we add another IP, that cache entry should be removed
Log.Debugf("Adding a new IP to %s: the cache entry should be removed", testName1)
zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.3"))
assertNotInCache(t, cache, q, fmt.Sprintf("after adding a new IP for %s", testName1))
// Zone database at this point:
// first.weave.local = 10.2.2.1 10.2.2.2 10.2.2.3
// second.weave.local = 10.9.9.1
Log.Debugf("Querying again (so a cache entry will be created)")
q, _ = assertExchange(t, testName1, dns.TypeA, testPort, 3, 4, 0)
assertInCache(t, cache, q, "after asking about the name")
Log.Debugf("... and removing one of the IP addresses")
zone.DeleteRecords(containerID, "", net.ParseIP("10.2.2.2"))
assertNotInCache(t, cache, q, "after deleting IP for 10.2.2.2")
// Zone database at this point:
// first.weave.local = 10.2.2.1 10.2.2.3
// second.weave.local = 10.9.9.1
// generate cache responses
Log.Debugf("Querying for a raddr")
qname, _ := assertExchange(t, testName1, dns.TypeA, testPort, 2, 2, 0)
qptr, _ := assertExchange(t, "1.2.2.10.in-addr.arpa.", dns.TypePTR, testPort, 1, 1, 0)
qotherName, _ := assertExchange(t, testName2, dns.TypeA, testPort, 1, 1, 0)
qotherPtr, _ := assertExchange(t, "1.9.9.10.in-addr.arpa.", dns.TypePTR, testPort, 1, 1, 0)
qwrongName, _ := assertExchange(t, "wrong.weave.local.", dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
assertInCache(t, cache, qname, "after asking for name")
assertInCache(t, cache, qptr, "after asking for address")
assertInCache(t, cache, qotherName, "after asking for second name")
assertInCache(t, cache, qotherPtr, "after asking for second address")
assertNotLocalInCache(t, cache, qwrongName, "after asking for a wrong name")
// now we will check if a removal affects all the responses
Log.Debugf("... and removing an IP should invalidate both the cached responses for name and raddr")
zone.DeleteRecords(containerID, "", net.ParseIP("10.2.2.1"))
assertNotInCache(t, cache, qptr, "after deleting record")
assertNotInCache(t, cache, qname, "after deleting record")
assertInCache(t, cache, qotherName, "after deleting record")
// Zone database at this point:
// first.weave.local = 10.2.2.3
// second.weave.local = 10.9.9.1
// generate cache responses
Log.Debugf("Querying for a raddr")
qptr, _ = assertExchange(t, "3.2.2.10.in-addr.arpa.", dns.TypePTR, testPort, 1, 1, 0)
qname, _ = assertExchange(t, testName1, dns.TypeA, testPort, 1, 1, 0)
qotherName, _ = assertExchange(t, testName2, dns.TypeA, testPort, 1, 1, 0)
qotherPtr, _ = assertExchange(t, "1.9.9.10.in-addr.arpa.", dns.TypePTR, testPort, 1, 1, 0)
assertInCache(t, cache, qname, "after asking for name")
assertInCache(t, cache, qptr, "after asking for PTR")
assertInCache(t, cache, qotherName, "after asking for second name")
assertInCache(t, cache, qotherPtr, "after asking for second address")
// let's repeat this, but adding an IP
Log.Debugf("... and adding a new IP should invalidate both the cached responses for the name")
zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.7"))
assertNotInCache(t, cache, qname, "after adding a new IP")
assertInCache(t, cache, qotherName, "after adding a new IP")
assertInCache(t, cache, qotherPtr, "after adding a new IP")
// check that after some time, the cache entry is expired
clk.Forward(int(DefaultLocalTTL) + 1)
assertNotInCache(t, cache, qotherName, "after passing some time")
assertNotInCache(t, cache, qwrongName, "after passing some time")
// Zone database at this point:
// first.weave.local = 10.2.2.3 10.2.2.7
// second.weave.local = 10.9.9.1
zone.DeleteRecords(containerID, "", net.IP{})
assertNotInCache(t, cache, qotherName, "after removing container")
assertNotInCache(t, cache, qotherPtr, "after removing container")
}
// Check if the entries in the cache expire
func TestServerCacheExpiration(t *testing.T) {
const (
containerID = "somecontainer"
testName1 = "first.weave.local."
testName2 = "second.weave.local."
negativeLocalTTL = 10
)
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestServerCacheExpiration starting")
clk := newMockedClock()
Log.Debugf("Creating 2 zone databases")
zoneConfig := ZoneConfig{
RefreshInterval: 0, // no name updates
Clock: clk,
}
dbs := newZoneDbsWithMockedMDns(2, zoneConfig)
dbs.Start()
defer dbs.Stop()
Log.Debugf("Creating a cache")
cache, err := NewCache(1024, clk)
require.NoError(t, err)
Log.Debugf("Creating a real DNS server for the first zone database and with the cache")
srv, err := NewDNSServer(DNSServerConfig{
Zone: dbs[0].Zone,
Cache: cache,
Clock: clk,
ListenReadTimeout: testSocketTimeout,
CacheNegLocalTTL: negativeLocalTTL,
})
require.NoError(t, err)
err = srv.Start()
require.NoError(t, err)
go srv.ActivateAndServe()
defer srv.Stop()
time.Sleep(100 * time.Millisecond) // Allow server goroutine to start
testPort, err := srv.GetPort()
require.NoError(t, err)
require.NotEqual(t, 0, testPort, "invalid listen port")
// Check that the DNS server knows nothing about testName1
qName1, _ := assertExchange(t, testName1, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
// there should be a negative reply cached during [0, negativeLocalTTL]
assertNotLocalInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
clk.Forward(negativeLocalTTL - 1)
assertNotLocalInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
// after negativeLocalTTL, there should be absolutely nothing in the cache
clk.Forward(2)
assertNotInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
// We add the IP in the second peer right after
Log.Debugf("Adding an IPs to %s in database #2", testName1)
dbs[1].Zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.1"))
t.Logf("Zone database #2:\n%s", dbs[1].Zone)
// Check that the DNS server returns updated information, and it will be
// cached during [0, DefaultLocalTTL]
qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 1, 1, 0)
assertInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
clk.Forward(int(DefaultLocalTTL) - 1)
assertInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
// and it will expire after DefaultLocalTTL
clk.Forward(2)
assertNotInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
// We delete the IP in the second peer right after
Log.Debugf("Removing the IPs from %s in database #2", testName1)
dbs[1].Zone.DeleteRecords(containerID, "", net.ParseIP("10.2.2.1"))
// Check that we return to the initial state: a neg-local entry in the cache
qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
assertNotLocalInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
clk.Forward(negativeLocalTTL + 1)
// Check that all entries should be outdated
cache.Purge()
if cache.Len() != 0 {
t.Fatalf("Some entries still valid in cache:\n%s", cache)
}
}
// Check if the names updates lead to cache invalidations
func TestServerCacheRefresh(t *testing.T) {
const (
containerID = "somecontainer"
testName1 = "first.weave.local."
testName2 = "second.weave.local."
refreshInterval = int(DefaultLocalTTL) / 3
)
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestServerCacheRefresh starting")
clk := newMockedClock()
Log.Debugf("Creating 2 zone databases")
zoneConfig := ZoneConfig{
RefreshInterval: refreshInterval,
Clock: clk,
}
dbs := newZoneDbsWithMockedMDns(2, zoneConfig)
dbs.Start()
defer dbs.Stop()
Log.Debugf("Creating a cache")
cache, err := NewCache(1024, clk)
require.NoError(t, err)
Log.Debugf("Creating a real DNS server for the first zone database and with the cache")
srv, err := NewDNSServer(DNSServerConfig{
Zone: dbs[0].Zone,
Cache: cache,
Clock: clk,
ListenReadTimeout: testSocketTimeout,
MaxAnswers: 4,
})
require.NoError(t, err)
err = srv.Start()
require.NoError(t, err)
go srv.ActivateAndServe()
defer srv.Stop()
time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start
testPort, err := srv.GetPort()
require.NoError(t, err)
require.NotEqual(t, 0, testPort, "listen port")
Log.Debugf("Adding an IPs to %s", testName1)
dbs[1].Zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.1"))
dbs.Flush() // ensure the immediate refresh is done
// Zone database #2 at this point:
// first.weave.local = 10.2.2.1
// testName1 and testName2 should have no IPs yet
qName1, _ := assertExchange(t, testName1, dns.TypeA, testPort, 1, 1, 0)
qName2, _ := assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
assertInCache(t, cache, qName1, "after asking for first name")
assertNotLocalInCache(t, cache, qName2, "after asking for second name")
clk.Forward(refreshInterval / 2)
dbs.Flush()
Log.Debugf("Adding an IP to %s and to %s", testName1, testName2)
dbs[1].Zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.2"))
dbs[1].Zone.AddRecord(containerID, testName2, net.ParseIP("10.9.9.2"))
// Zone database #2 at this point:
// first.weave.local = 10.2.2.1 10.2.2.2
// second.weave.local = 10.9.9.2
clk.Forward(refreshInterval/2 + 2)
dbs.Flush()
// at this point, the testName1 should have been refreshed
// so it should have two IPs, and the cache entry should have been invalidated
assertNotInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
assertNotLocalInCache(t, cache, qName2, fmt.Sprintf("after asking for %s", testName2))
qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 2, 2, 0)
qName2, _ = assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
assertInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1))
assertNotLocalInCache(t, cache, qName2, "after asking for a unknown name")
// delete the IPs, and some time passes by so the cache should be purged...
dbs[1].Zone.DeleteRecords(containerID, "", net.ParseIP("10.2.2.1"))
dbs[1].Zone.DeleteRecords(containerID, "", net.ParseIP("10.2.2.2"))
clk.Forward(refreshInterval + 1)
dbs.Flush()
qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
qName2, _ = assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
assertNotLocalInCache(t, cache, qName1, "after asking for a unknown name")
assertNotLocalInCache(t, cache, qName2, "after asking for a unknown name")
}
-258
View File
@@ -1,258 +0,0 @@
package nameserver
import (
"encoding/binary"
"fmt"
"net"
"testing"
"time"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
const (
testRDNSsuccess = "1.2.2.10.in-addr.arpa."
testRDNSfail = "4.3.2.1.in-addr.arpa."
testRDNSnonlocal = "8.8.8.8.in-addr.arpa."
testUDPBufSize = 16384
)
func setupForTest(t *testing.T) {
// fail early if we cannot find a default multicast interface
multicast, err := LinkLocalMulticastListener(nil)
if err != nil {
t.Fatalf("Unable to create multicast listener: %s. No default multicast interface?", err)
}
multicast.Close()
}
func TestUDPDNSServer(t *testing.T) {
setupForTest(t)
const (
successTestName = "test1.weave.local."
failTestName = "fail.weave.local."
nonLocalName = "weave.works."
testAddr1 = "10.2.2.1"
containerID = "somecontainer"
)
testCIDR1 := testAddr1 + "/24"
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestUDPDNSServer starting")
zone, err := NewZoneDb(ZoneConfig{})
require.NoError(t, err)
err = zone.Start()
require.NoError(t, err)
defer zone.Stop()
ip, _, _ := net.ParseCIDR(testCIDR1)
zone.AddRecord(containerID, successTestName, ip)
fallbackHandler := func(w dns.ResponseWriter, req *dns.Msg) {
if len(req.Question) == 0 {
return // ignore empty queries (sent when shutting down the server)
}
m := new(dns.Msg)
m.SetReply(req)
q := req.Question[0]
if q.Name == nonLocalName && q.Qtype == dns.TypeMX {
m.Answer = make([]dns.RR, 1)
m.Answer[0] = &dns.MX{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 0}, Mx: "mail." + nonLocalName}
} else if q.Name == nonLocalName && q.Qtype == dns.TypeANY {
m.Answer = make([]dns.RR, 512/len("mailn."+nonLocalName)+1)
for i := range m.Answer {
m.Answer[i] = &dns.MX{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 0}, Mx: fmt.Sprintf("mail%d.%s", i, nonLocalName)}
}
} else if q.Name == testRDNSnonlocal && q.Qtype == dns.TypePTR {
m.Answer = make([]dns.RR, 1)
m.Answer[0] = &dns.PTR{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, Ptr: "ns1.google.com."}
} else if q.Name == testRDNSfail && q.Qtype == dns.TypePTR {
m.Rcode = dns.RcodeNameError
}
w.WriteMsg(m)
}
// Run another DNS server for fallback
fallback, err := newMockedFallback(fallbackHandler, nil)
require.NoError(t, err)
fallback.Start()
defer fallback.Stop()
srv, err := NewDNSServer(DNSServerConfig{
Zone: zone,
UpstreamCfg: fallback.CliConfig,
CacheDisabled: true,
ListenReadTimeout: testSocketTimeout,
})
require.NoError(t, err)
err = srv.Start()
require.NoError(t, err)
go srv.ActivateAndServe()
defer srv.Stop()
time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start
var r *dns.Msg
testPort, err := srv.GetPort()
require.NoError(t, err)
require.NotEqual(t, 0, testPort, "invalid listen port")
_, r = assertExchange(t, successTestName, dns.TypeA, testPort, 1, 1, 0)
require.IsType(t, (*dns.A)(nil), r.Answer[0], "DNS record")
require.Equal(t, testAddr1, r.Answer[0].(*dns.A).A.String(), "IP address")
assertExchange(t, failTestName, dns.TypeA, testPort, 0, 0, dns.RcodeNameError)
_, r = assertExchange(t, testRDNSsuccess, dns.TypePTR, testPort, 1, 1, 0)
require.IsType(t, (*dns.PTR)(nil), r.Answer[0], "DNS record")
require.Equal(t, successTestName, r.Answer[0].(*dns.PTR).Ptr, "IP address")
assertExchange(t, testRDNSfail, dns.TypePTR, testPort, 0, 0, dns.RcodeNameError)
// This should fail because we don't handle MX records
assertExchange(t, successTestName, dns.TypeMX, testPort, 0, 0, dns.RcodeNameError)
// This non-local query for an MX record should succeed by being
// passed on to the fallback server
assertExchange(t, nonLocalName, dns.TypeMX, testPort, 1, -1, 0)
// Now ask a query that we expect to return a lot of data.
assertExchange(t, nonLocalName, dns.TypeANY, testPort, 5, -1, 0)
assertExchange(t, testRDNSnonlocal, dns.TypePTR, testPort, 1, -1, 0)
// Not testing MDNS functionality of server here (yet), since it
// needs two servers, each listening on its own address
}
func TestTCPDNSServer(t *testing.T) {
setupForTest(t)
const (
numAnswers = 512
nonLocalName = "weave.works."
)
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestTCPDNSServer starting")
zone, err := NewZoneDb(ZoneConfig{})
require.NoError(t, err)
err = zone.Start()
require.NoError(t, err)
defer zone.Stop()
// generate a list of `numAnswers` IP addresses
var addrs []ZoneRecord
bs := make([]byte, 4)
for i := 0; i < numAnswers; i++ {
binary.LittleEndian.PutUint32(bs, uint32(i))
ip := net.IPv4(bs[0], bs[1], bs[2], bs[3])
addrs = append(addrs, ZoneRecord(Record{"", ip, 0, 0, 0}))
}
// handler for the fallback server: it will just return a very long response
fallbackUDPHandler := func(w dns.ResponseWriter, req *dns.Msg) {
if len(req.Question) == 0 {
return // ignore empty queries (sent when shutting down the server)
}
maxLen := getMaxReplyLen(req, protUDP)
t.Logf("Fallback UDP server got asked: returning %d answers", numAnswers)
q := req.Question[0]
m := makeAddressReply(req, &q, addrs, DefaultLocalTTL)
mLen := m.Len()
m.SetEdns0(uint16(maxLen), false)
if mLen > maxLen {
t.Logf("... truncated response (%d > %d)", mLen, maxLen)
m.Truncated = true
}
w.WriteMsg(m)
}
fallbackTCPHandler := func(w dns.ResponseWriter, req *dns.Msg) {
if len(req.Question) == 0 {
return // ignore empty queries (sent when shutting down the server)
}
t.Logf("Fallback TCP server got asked: returning %d answers", numAnswers)
q := req.Question[0]
m := makeAddressReply(req, &q, addrs, DefaultLocalTTL)
w.WriteMsg(m)
}
t.Logf("Running a DNS fallback server with UDP")
fallback, err := newMockedFallback(fallbackUDPHandler, fallbackTCPHandler)
require.NoError(t, err)
fallback.Start()
defer fallback.Stop()
t.Logf("Creating a WeaveDNS server instance, falling back to 127.0.0.1:%d", fallback.Port)
srv, err := NewDNSServer(DNSServerConfig{
Zone: zone,
UpstreamCfg: fallback.CliConfig,
CacheDisabled: true,
ListenReadTimeout: testSocketTimeout,
})
require.NoError(t, err)
err = srv.Start()
require.NoError(t, err)
go srv.ActivateAndServe()
defer srv.Stop()
time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start
testPort, err := srv.GetPort()
require.NoError(t, err)
require.NotEqual(t, 0, testPort, "listen port")
dnsAddr := fmt.Sprintf("127.0.0.1:%d", testPort)
t.Logf("Creating a UDP and a TCP client")
uc := new(dns.Client)
uc.UDPSize = minUDPSize
tc := new(dns.Client)
tc.Net = "tcp"
t.Logf("Creating DNS query message")
m := new(dns.Msg)
m.RecursionDesired = true
m.SetQuestion(nonLocalName, dns.TypeA)
t.Logf("Checking the fallback server at %s returns a truncated response with UDP", fallback.Addr)
r, _, err := uc.Exchange(m, fallback.Addr)
t.Logf("Got response from fallback server (UDP) with %d answers", len(r.Answer))
t.Logf("Response:\n%+v\n", r)
require.NoError(t, err)
require.True(t, r.MsgHdr.Truncated, "DNS truncated reponse flag")
require.NotEqual(t, numAnswers, len(r.Answer), "number of answers (UDP)")
t.Logf("Checking the WeaveDNS server at %s returns a truncated reponse with UDP", dnsAddr)
r, _, err = uc.Exchange(m, dnsAddr)
t.Logf("UDP Response:\n%+v\n", r)
require.NoError(t, err)
require.NotNil(t, r, "response")
t.Logf("%d answers", len(r.Answer))
require.True(t, r.MsgHdr.Truncated, "DNS truncated reponse flag")
require.NotEqual(t, numAnswers, len(r.Answer), "number of answers (UDP)")
t.Logf("Checking the WeaveDNS server at %s does not return a truncated reponse with TCP", dnsAddr)
r, _, err = tc.Exchange(m, dnsAddr)
t.Logf("TCP Response:\n%+v\n", r)
require.NoError(t, err)
require.NotNil(t, r, "response")
t.Logf("%d answers", len(r.Answer))
require.False(t, r.MsgHdr.Truncated, "DNS truncated response flag")
require.Equal(t, numAnswers, len(r.Answer), "number of answers (TCP)")
t.Logf("Checking the WeaveDNS server at %s does not return a truncated reponse with UDP with a bigger buffer", dnsAddr)
m.SetEdns0(testUDPBufSize, false)
r, _, err = uc.Exchange(m, dnsAddr)
t.Logf("UDP-large Response:\n%+v\n", r)
require.NoError(t, err)
require.NotNil(t, r, "response")
t.Logf("%d answers", len(r.Answer))
require.NoError(t, err)
require.False(t, r.MsgHdr.Truncated, "DNS truncated response flag")
require.Equal(t, numAnswers, len(r.Answer), "number of answers (UDP-long)")
}
-739
View File
@@ -1,739 +0,0 @@
package nameserver
import (
"bytes"
"fmt"
"net"
"sync"
"time"
"github.com/benbjohnson/clock"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
)
const (
RDNSDomain = "in-addr.arpa."
)
const (
DefaultLocalDomain = "weave.local." // The default name used for the local domain
DefaultRefreshInterval = int(DefaultLocalTTL) // Period for background updates with mDNS
DefaultRelevantTime = 60 // When to forget info about remote info if nobody asks...
defaultRemoteIdent = "weave:remote" // Ident used for info obtained from mDNS
)
// +1 to also exclude a dot
var rdnsDomainLen = len(RDNSDomain) + 1
type LookupError string
func (ops LookupError) Error() string {
return "Unable to find " + string(ops)
}
type DuplicateError struct {
}
func (dup DuplicateError) Error() string {
return "Tried to add a duplicate entry"
}
type Zone interface {
ZoneObservable
ZoneLookup
// The domain where we operate (eg, "weave.local.")
Domain() string
// Add a record in the local database
AddRecord(ident string, name string, ip net.IP) error
// Delete matching records (uninitialised values act as wildcards)
DeleteRecords(ident string, name string, ip net.IP) (count int)
// Lookup for a name in the whole domain
DomainLookupName(name string) ([]ZoneRecord, error)
// Lookup for an address in the whole domain
DomainLookupInaddr(inaddr string) ([]ZoneRecord, error)
// Return a status string
Status() string
}
///////////////////////////////////////////////////////////////////
//
// Zone database overview:
//
// idents names entry
// *-------* *------* *------*
// zone -> | ident | --*--> | name | --*-> | IPv4 |
// *-------* | *------* | *------*
// | ident | *--> | name | *-> | IPv4 |
// *-------* *------* *------*
// ... ...
//
// The zone database keeps an ident per container, but also a special
// ident for names/IPs obtained from remote peers.
//
// Each name can have multiple IPs (so far, we only consider IPv4s), and
// each IP is stored in a `recordEntry` in the database. Entries store
// some additional information like priority and weight of the IP (for
// example, for future use with SRV records)
//
// An entry in the zone database
type recordEntry struct {
Record
observers []ZoneRecordObserver // the observers for this record
insTime time.Time // time when this record was inserted
}
func newRecordEntry(zr ZoneRecord, now time.Time) *recordEntry {
return &recordEntry{
Record: Record{
name: zr.Name(),
ip: zr.IP(),
priority: zr.Priority(),
weight: zr.Weight(),
ttl: zr.TTL(),
},
insTime: now,
observers: make([]ZoneRecordObserver, 0),
}
}
func (re *recordEntry) addIPObserver(zro ZoneRecordObserver) {
re.observers = append(re.observers, zro)
}
// Notify all the IP observers and then remove them all
// IP address observers are notified when
// - the IP is removed
// - the name is removed
func (re *recordEntry) notifyIPObservers() {
numObservers := len(re.observers)
if numObservers > 0 {
Log.Debugf("[zonedb] Notifying %d observers of '%s'", numObservers, re.ip)
for _, observer := range re.observers {
observer()
}
re.observers = make([]ZoneRecordObserver, 0)
}
}
func (re *recordEntry) TTL() int {
// if we never set the TTL (eg, when using AddRecord()), return the standard value...
if re.ttl == 0 {
return int(DefaultLocalTTL)
}
return re.ttl
}
// Check if the info in this record is still valid according to the TTL
func (re *recordEntry) hasExpired(now time.Time) bool {
return re.ttl > 0 && now.Sub(re.insTime).Seconds() > float64(re.ttl)
}
func (re *recordEntry) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s", re.Record)
lobs := len(re.observers)
if lobs > 0 {
fmt.Fprintf(&buf, "/OBS:%d", lobs)
}
return buf.String()
}
///////////////////////////////////////////////////////////////////
// a name, with group of records by IPv4
type name struct {
name string
ipv4 map[IPv4]*recordEntry // all the IPv4 records for this name
observers []ZoneRecordObserver // the observers for this name
lastRefreshTime time.Time // last time this name was updated by broadcasting a query
lastAccessTime time.Time // last time a Lookup* used info from this name
}
func newName(n string) *name {
return &name{
name: n,
ipv4: make(map[IPv4]*recordEntry),
observers: make([]ZoneRecordObserver, 0),
}
}
func (n *name) len() int { return len(n.ipv4) }
func (n *name) empty() bool { return n.len() == 0 }
func (n *name) hasIP(ip net.IP) bool { return n.hasIPv4(ipToIPv4(ip)) }
func (n *name) hasIPv4(ip IPv4) bool { _, found := n.ipv4[ip]; return found }
// Get all the entries for this name
func (n *name) getAllEntries() (res []*recordEntry) {
for _, entry := range n.ipv4 {
res = append(res, entry)
}
return
}
// Get the entry for an IP in this name
func (n *name) getEntryForIP(ip IPv4) (res *recordEntry) {
if entry, found := n.ipv4[ip]; found {
res = entry
}
return
}
// Add a new IPv4 to this name
func (n *name) addIP(zr ZoneRecord, now time.Time) (*recordEntry, error) {
ne := newRecordEntry(zr, now)
ipv4 := ipToIPv4(zr.IP())
n.ipv4[ipv4] = ne
n.notifyNameObservers()
return ne, nil
}
// Delete by IP. Pass uninitialized value for wildcard
func (n *name) deleteIP(ip net.IP) int {
count := 0
if !ip.Equal(net.IP{}) {
ipv4 := ipToIPv4(ip)
if ipv4V, found := n.ipv4[ipv4]; found {
ipv4V.notifyIPObservers()
delete(n.ipv4, ipv4)
count++
}
} else {
for ipv4K, ipv4V := range n.ipv4 {
ipv4V.notifyIPObservers()
delete(n.ipv4, ipv4K)
count++
}
}
if count > 0 {
n.notifyNameObservers()
}
return count
}
// Update the list of IPs for this name, adding new ones and removing old IPs...
func (n *name) updateIPs(records []ZoneRecord, now time.Time) (added int, removed int) {
ipsMap := make(map[IPv4]bool)
// add the new IPs
for _, record := range records {
ipv4 := ipToIPv4(record.IP())
ipsMap[ipv4] = true
if !n.hasIPv4(ipv4) {
n.addIP(record, now)
added++
}
}
// remove the old IPs
for ipv4, ipRecord := range n.ipv4 {
if _, isNew := ipsMap[ipv4]; !isNew {
ipRecord.notifyIPObservers() // we must notify the observers before removing the IP record...
delete(n.ipv4, ipv4)
removed++
}
}
// notify observers and update times
if added > 0 || removed > 0 {
n.lastRefreshTime = now
n.notifyNameObservers()
}
return
}
// Add a observer for this name
func (n *name) addNameObserver(observer ZoneRecordObserver) {
n.observers = append(n.observers, observer)
}
// Notify and flush all the observers
// Name observers are notified when
// - an IP is added
// - an IP is removed
// - the name is removed
func (n *name) notifyNameObservers() {
numObservers := len(n.observers)
if numObservers > 0 {
Log.Debugf("[zonedb] Notifying %d observers of '%s'", numObservers, n.name)
for _, observer := range n.observers {
observer()
}
n.observers = make([]ZoneRecordObserver, 0)
}
}
// Convert a set of IP records to a comma-separated string
func (n *name) String() string {
var buf bytes.Buffer
for _, ip := range n.ipv4 {
fmt.Fprintf(&buf, "%s, ", ip)
}
l := buf.Len()
if l > 2 {
buf.Truncate(l - 2)
}
return buf.String()
}
///////////////////////////////////////////////////////////////////
// all the names in a ident
type nameSet struct {
names map[string]*name
}
func newNameSet() *nameSet { return &nameSet{names: make(map[string]*name)} }
func (ns *nameSet) len() int { return len(ns.names) }
func (ns *nameSet) empty() bool { return ns.len() == 0 }
func (ns *nameSet) hasName(name string) bool {
n, found := ns.names[name]
if !found {
return false
}
// the name must have some valid IPs in order to be considered as "existent"
return len(n.ipv4) > 0
}
// Get the zone entries for a name
// If the name does not exist and `create` is true, a new name will be created
func (ns *nameSet) getName(name string, create bool) (n *name) {
n, found := ns.names[name]
if !found && create {
newName := newName(name)
ns.names[name] = newName
return newName
}
return n
}
// Get all the entries for a name
func (ns *nameSet) getEntriesForName(name string) (res []*recordEntry) {
n := ns.getName(name, false)
if n != nil {
res = n.getAllEntries()
}
return
}
// Get all the entries for an IP, for all names
func (ns *nameSet) getEntriesForIP(ip IPv4) (res []*recordEntry) {
for _, name := range ns.names {
entry := name.getEntryForIP(ip)
if entry != nil {
res = append(res, entry)
}
}
return
}
// Add a new IPv4 to a name
func (ns *nameSet) addIPToName(zr ZoneRecord, now time.Time) (*recordEntry, error) {
n := ns.getName(zr.Name(), true)
ipv4 := ipToIPv4(zr.IP())
if n.hasIPv4(ipv4) {
return n.getEntryForIP(ipv4), DuplicateError{}
}
ns.touchName(zr.Name(), now)
return n.addIP(zr, now)
}
// Delete by name and IP. Pass uninitialized values for wildcard
func (ns nameSet) deleteNameIP(name string, ip net.IP) int {
count := 0
if name != "" {
if nameV, found := ns.names[name]; found {
count += nameV.deleteIP(ip)
if nameV.empty() {
delete(ns.names, name)
}
}
} else {
for nameK, nameV := range ns.names {
count += nameV.deleteIP(ip)
if nameV.empty() {
delete(ns.names, nameK)
}
}
}
return count
}
// Get the name query time
func (ns *nameSet) getNameLastAccess(n string) time.Time {
if name, found := ns.names[n]; found {
return name.lastAccessTime
}
return time.Time{}
}
// Touch the last access time for a name
// The access time is saved only for locally-introduced records (otherwise it could
// be lost when names are irrelevant...)
func (ns *nameSet) touchName(name string, now time.Time) {
Log.Debugf("[zonedb] Touching name %s", name)
n := ns.getName(name, false)
if n != nil {
n.lastAccessTime = now
}
}
func (ns *nameSet) String() string {
var buf bytes.Buffer
for _, name := range ns.names {
fmt.Fprintf(&buf, "%s, ", name)
}
l := buf.Len()
if l > 2 {
buf.Truncate(l - 2)
}
return buf.String()
}
///////////////////////////////////////////////////////////////////
// Name sets, by ident name
type identRecordSet map[string]*nameSet
func (irs identRecordSet) String() string {
var buf bytes.Buffer
for ident, nameset := range irs {
fmt.Fprintf(&buf, "%.12s: %s\n", ident, nameset)
}
return buf.String()
}
///////////////////////////////////////////////////////////////////
// Invariant on updateRequests: they are in ascending order of time.
type refreshRequest struct {
name string
time time.Time
}
type ZoneConfig struct {
// The local domain
Domain string
// The interface where mDNS operates
Iface *net.Interface
// Refresh interval for local names in the zone database (disabled by default) (in seconds)
RefreshInterval int
// Forget about remote info if nobody asks for it in this time (in seconds)
RelevantTime int
// TTL returnined in local asnwers (with mDNS)
LocalTTL int
// Force a specific mDNS client
MDNSClient ZoneMDNSClient
// Force a specific mDNS server
MDNSServer ZoneMDNSServer
// For a specific clock provider
Clock clock.Clock
}
// Zone database
// The zone database contains the locally known information about the zone.
// The zone database uses some other mechanisms (eg, mDNS) for finding unknown
// information or for keeping current information up to date.
type ZoneDb struct {
mx sync.RWMutex
idents identRecordSet
mdnsCli ZoneMDNSClient
mdnsSrv ZoneMDNSServer
iface *net.Interface
domain string // the local domain
relevantLimit time.Duration // if no one asks for something in this time, it is not relevant a anymore...
clock clock.Clock
refreshScheds *SchedQueue
refreshCloseChan chan bool
refreshInterval time.Duration
}
// Create a new zone database
func NewZoneDb(config ZoneConfig) (zone *ZoneDb, err error) {
zone = &ZoneDb{
domain: config.Domain,
idents: make(identRecordSet),
mdnsCli: config.MDNSClient,
mdnsSrv: config.MDNSServer,
iface: config.Iface,
clock: config.Clock,
relevantLimit: time.Duration(DefaultRelevantTime) * time.Second,
refreshCloseChan: make(chan bool),
}
// fix the default configuration parameters
if zone.clock == nil {
zone.clock = clock.New()
}
if len(zone.domain) == 0 {
zone.domain = DefaultLocalDomain
}
if config.RefreshInterval > 0 {
zone.refreshInterval = time.Duration(config.RefreshInterval) * time.Second
}
if config.RelevantTime > 0 {
zone.relevantLimit = time.Duration(config.RelevantTime) * time.Second
}
if zone.refreshInterval > 0 {
zone.refreshScheds = NewSchedQueue(zone.clock)
}
// Create the mDNS client and server
if zone.mdnsCli == nil {
if zone.mdnsCli, err = NewMDNSClient(); err != nil {
return
}
}
if zone.mdnsSrv == nil {
mdnsTTL := DefaultLocalTTL
if config.LocalTTL > 0 {
mdnsTTL = config.LocalTTL
}
if zone.mdnsSrv, err = NewMDNSServer(zone, false, mdnsTTL); err != nil {
return
}
}
return
}
// Start the zone database
func (zone *ZoneDb) Start() (err error) {
if zone.iface != nil {
Log.Infof("[zonedb] Using mDNS on %+v", zone.iface)
} else {
Log.Infof("[zonedb] Using mDNS on all interfaces")
}
if err = zone.mdnsCli.Start(zone.iface); err != nil {
return
}
if err = zone.mdnsSrv.Start(zone.iface); err != nil {
return
}
if zone.refreshInterval > 0 {
zone.refreshScheds.Start()
}
return
}
// Perform a graceful shutdown of the zone database
func (zone *ZoneDb) Stop() error {
if zone.refreshInterval > 0 {
Log.Debugf("[zonedb] Closing background updaters...")
zone.refreshScheds.Stop()
}
Log.Debugf("[zonedb] Exiting mDNS client and server...")
zone.mdnsCli.Stop()
zone.mdnsSrv.Stop()
return nil
}
// Obtain the domain where this database keeps information for
func (zone *ZoneDb) Domain() string {
return zone.domain
}
// Return the status string
func (zone *ZoneDb) Status() string {
var buf bytes.Buffer
fmt.Fprintln(&buf, "Local domain", zone.domain)
fmt.Fprintln(&buf, "Interface", zone.iface)
fmt.Fprintf(&buf, "Zone database:\n%s", zone)
return buf.String()
}
// Add a record with `name` pointing to `ip`
func (zone *ZoneDb) AddRecord(ident string, name string, ip net.IP) (err error) {
zone.mx.Lock()
defer zone.mx.Unlock()
Log.Debugf("[zonedb] Adding record: '%s'/'%s'[%s]", ident, name, ip)
record := Record{dns.Fqdn(name), ip, 0, 0, 0}
_, err = zone.getNameSet(ident).addIPToName(record, zone.clock.Now())
return
}
// Delete matching records (uninitialised values act as wildcards)
func (zone *ZoneDb) DeleteRecords(ident string, name string, ip net.IP) int {
if name != "" {
name = dns.Fqdn(name)
}
zone.mx.Lock()
defer zone.mx.Unlock()
return zone.deleteIdentNameIP(ident, name, ip)
}
// Observe a name.
// The name must have at least one valid IP address.
// Name observers are notified when
// - an IP is added
// - an IP is removed
// - the name is removed
// The observer will be invoked on any change/removal that affects this name.
// Each observer will be invoked at least once (but possibly more). After that, they will be removed.
// The observer should not try to lock the ZoneDB (you will get a deadlock)
func (zone *ZoneDb) ObserveName(name string, observer ZoneRecordObserver) (err error) {
zone.mx.Lock()
defer zone.mx.Unlock()
cnt := 0
name = dns.Fqdn(name)
for _, nameset := range zone.idents {
if n := nameset.getName(name, false); n != nil {
n.addNameObserver(observer)
cnt++
}
}
if cnt == 0 {
err = LookupError(fmt.Sprintf("%s", name))
}
return
}
// Observe a IP address.
// The IP address must be exit in the database.
// IP address observers are notified when
// - the IP is removed
// - the name is removed
// The observer will be invoked on any change/removal that affects this IP.
// Each observer will be invoked at least once (but possibly more). After that, they will be removed.
// The observer should not try to lock the ZoneDB (you will get a deadlock)
func (zone *ZoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (err error) {
zone.mx.Lock()
defer zone.mx.Unlock()
cnt := 0
revIP, err := raddrToIPv4(inaddr)
if err != nil {
return newParseError("lookup address", inaddr)
}
for _, nameset := range zone.idents {
for _, entry := range nameset.getEntriesForIP(revIP) {
entry.addIPObserver(observer)
cnt++
}
}
if cnt == 0 {
err = LookupError(fmt.Sprintf("%+v/%+v", inaddr, revIP))
}
return
}
// Get the string representation of a zone
func (zone *ZoneDb) String() string {
zone.mx.RLock()
defer zone.mx.RUnlock()
return zone.idents.String()
}
// Notify that a container has died
func (zone *ZoneDb) ContainerDied(ident string) error {
Log.Infof("[zonedb] Container %s down. Removing records", ident)
zone.DeleteRecords(ident, "", net.IP{})
return nil
}
// Return true if a remote name is still relevant
func (zone *ZoneDb) IsNameRelevant(n string) bool {
zone.mx.Lock()
defer zone.mx.Unlock()
lastAccess := time.Time{}
for _, nameset := range zone.idents {
t := nameset.getNameLastAccess(n)
if !t.IsZero() && (lastAccess.IsZero() || t.After(lastAccess)) {
lastAccess = t
}
}
return !lastAccess.IsZero() && zone.clock.Now().Sub(lastAccess) <= zone.relevantLimit
}
// Returns true if the info we have for a remote name has expired. Local names are never expired
// Returns false if we do not have info for this name from remote peers.
func (zone *ZoneDb) IsNameExpired(n string) bool {
zone.mx.Lock()
defer zone.mx.Unlock()
now := zone.clock.Now()
remoteNameSet := zone.getNameSet(defaultRemoteIdent)
entries := remoteNameSet.getEntriesForName(n)
if entries != nil {
for _, entry := range entries {
if entry.hasExpired(now) {
return true
}
}
}
return false
}
func (zone *ZoneDb) HasNameLocalInfo(n string) bool {
zone.mx.Lock()
defer zone.mx.Unlock()
remoteNameSet := zone.getNameSet(defaultRemoteIdent)
for _, nameset := range zone.idents {
if nameset != remoteNameSet {
if nameset.hasName(n) {
return true
}
}
}
return false
}
// Return true if the we have obtained some information for a name from remote peers
// We can have both local (eg, introduced through the HTTP API) and remote (eg, mDNS)
// information for a name. This method only checks if there is remote information...
func (zone *ZoneDb) HasNameRemoteInfo(n string) bool {
zone.mx.Lock()
defer zone.mx.Unlock()
remoteNameSet := zone.getNameSet(defaultRemoteIdent)
if remoteNameSet != nil {
return remoteNameSet.hasName(n)
}
return false
}
// Get or create a name set
func (zone *ZoneDb) getNameSet(ident string) *nameSet {
ns, found := zone.idents[ident]
if !found {
ns = newNameSet()
zone.idents[ident] = ns
}
return ns
}
// Delete by ident, name and IP. Pass uninitialized value for wildcard
func (zone *ZoneDb) deleteIdentNameIP(ident string, name string, ip net.IP) int {
count := 0
if ident != "" {
if identV, found := zone.idents[ident]; found {
count += identV.deleteNameIP(name, ip)
if identV.empty() {
delete(zone.idents, ident)
}
}
} else {
for identK, identV := range zone.idents {
count += identV.deleteNameIP(name, ip)
if identV.empty() {
delete(zone.idents, identK)
}
}
}
return count
}
-291
View File
@@ -1,291 +0,0 @@
package nameserver
import (
"net"
"time"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
)
type uniqZoneRecordKey struct {
name string
ipv4 IPv4
}
// A group of ZoneRecords where there are no duplicates (according to the name & IPv4)
type uniqZoneRecords map[uniqZoneRecordKey]ZoneRecord
func newUniqZoneRecords() uniqZoneRecords {
return make(uniqZoneRecords, 0)
}
// Add a new ZoneRecord to the group
func (uzr *uniqZoneRecords) add(zr ZoneRecord) {
key := uniqZoneRecordKey{zr.Name(), ipToIPv4(zr.IP())}
(*uzr)[key] = zr
}
// Return the group as an slice
func (uzr *uniqZoneRecords) toSlice() []ZoneRecord {
res := make([]ZoneRecord, len(*uzr))
i := 0
for _, r := range *uzr {
res[i] = r
i++
}
return res
}
//////////////////////////////////////////////////////////////////////////////
// Lookup in the database for locally-introduced information
func (zone *ZoneDb) lookup(target string, lfun func(ns *nameSet) []*recordEntry) (res []ZoneRecord, err error) {
uniq := newUniqZoneRecords()
for identName, nameset := range zone.idents {
if identName != defaultRemoteIdent {
for _, ze := range lfun(nameset) {
uniq.add(ze)
}
}
}
if len(uniq) == 0 {
return nil, LookupError(target)
}
return uniq.toSlice(), nil
}
// Perform a lookup for a name in the zone
// The name can be resolved locally with the local database
func (zone *ZoneDb) LookupName(name string) (res []ZoneRecord, err error) {
zone.mx.RLock()
defer zone.mx.RUnlock()
// note: LookupName() is usually called from the mDNS server, so we do not touch the name
name = dns.Fqdn(name)
Log.Debugf("[zonedb] Looking for name '%s' in local database", name)
return zone.lookup(name, func(ns *nameSet) []*recordEntry { return ns.getEntriesForName(name) })
}
// Perform a lookup for a IP address in the zone
// The address can be resolved locally with the local database
func (zone *ZoneDb) LookupInaddr(inaddr string) (res []ZoneRecord, err error) {
zone.mx.RLock()
defer zone.mx.RUnlock()
// note: LookupInaddr() is usually called from the mDNS server, so we do not touch the name
revIPv4, err := raddrToIPv4(inaddr)
if err != nil {
return nil, newParseError("lookup address", inaddr)
}
Log.Debugf("[zonedb] Looking for address in local database: '%s' (%s)", revIPv4, inaddr)
return zone.lookup(inaddr, func(ns *nameSet) []*recordEntry { return ns.getEntriesForIP(revIPv4) })
}
// Perform a domain lookup with mDNS
func (zone *ZoneDb) domainLookup(target string, lfun ZoneLookupFunc) (res []ZoneRecord, err error) {
// no local results have been obtained in the local database: try with a mDNS query
Log.Debugf("[zonedb] '%s' not in local database... trying with mDNS", target)
lanswers, err := lfun(target)
if err != nil {
Log.Debugf("[zonedb] mDNS lookup error for '%s': %s", target, err)
return nil, err
}
// if the request has been successful, save the IP in the local database and return the corresponding ZoneRecord
// (we do not get the remote ident in the mDNS reply, so we save it in a "remote" ident)
Log.Debugf("[zonedb] adding '%s' (obtained with mDNS) to '%s'", lanswers, target)
res = make([]ZoneRecord, len(lanswers))
zone.mx.Lock()
now := zone.clock.Now()
uniq := newUniqZoneRecords()
remoteIdent := zone.getNameSet(defaultRemoteIdent)
for _, answer := range lanswers {
r, err := remoteIdent.addIPToName(answer, now)
if err != nil {
zone.mx.Unlock()
Log.Warningf("[zonedb] '%s' insertion for %s failed: %s", answer, target, err)
return nil, err
}
uniq.add(r)
}
zone.mx.Unlock()
return uniq.toSlice(), nil
}
// Perform a lookup for a name in the zone
// The name can be resolved locally with the local database or with some other resolution method (eg, a mDNS query)
func (zone *ZoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) {
name = dns.Fqdn(name)
Log.Debugf("[zonedb] Looking for name '%s' in local(&remote) database", name)
zone.mx.Lock()
now := zone.clock.Now()
uniq := newUniqZoneRecords()
for identName, nameset := range zone.idents {
for _, ze := range nameset.getEntriesForName(name) {
// filter the entries with expired TTL
// locally introduced entries are never expired: they always have TTL=0
if ze.hasExpired(now) {
Log.Debugf("[zonedb] '%s': expired entry '%s' ignored: removing", name, ze)
nameset.deleteNameIP(name, net.IP{})
} else {
uniq.add(ze)
}
}
if identName != defaultRemoteIdent {
nameset.touchName(name, now)
}
}
zone.mx.Unlock()
if len(uniq) > 0 {
Log.Debugf("[zonedb] '%s' resolved in local database", name)
res = uniq.toSlice()
} else {
res, err = zone.domainLookup(name, zone.mdnsCli.LookupName)
}
if len(res) > 0 {
zone.startUpdatingName(name)
return res, nil
}
return nil, LookupError(name)
}
// Perform a lookup for a IP address in the zone
// The address can be resolved either with the local database or
// with some other resolution method (eg, a mDNS query)
func (zone *ZoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err error) {
revIPv4, err := raddrToIPv4(inaddr)
if err != nil {
return nil, newParseError("lookup address", inaddr)
}
Log.Debugf("[zonedb] Looking for address in local(&remote) database: '%s' (%s)", revIPv4, inaddr)
zone.mx.Lock()
now := zone.clock.Now()
uniq := newUniqZoneRecords()
for identName, nameset := range zone.idents {
for _, ze := range nameset.getEntriesForIP(revIPv4) {
// filter the entries with expired TTL
// locally introduced entries are never expired: they always have TTL=0
if ze.hasExpired(now) {
Log.Debugf("[zonedb] '%s': expired entry '%s' ignored: removing", revIPv4, ze)
nameset.deleteNameIP("", revIPv4.toNetIP())
} else {
uniq.add(ze)
if identName != defaultRemoteIdent {
nameset.touchName(ze.Name(), now)
}
}
}
}
zone.mx.Unlock()
if len(uniq) > 0 {
Log.Debugf("[zonedb] '%s' resolved in local database", inaddr)
res = uniq.toSlice()
} else {
res, err = zone.domainLookup(inaddr, zone.mdnsCli.LookupInaddr)
}
if len(res) > 0 {
// note: even for reverse addresses, we perform the background updates in the name, not in the IP
// this simplifies the process and produces basically the same results...
// note: we do not spend time trying to update names that did not return an initial response...
for _, r := range res {
zone.startUpdatingName(r.Name())
}
return res, nil
}
return nil, LookupError(inaddr)
}
//////////////////////////////////////////////////////////////////////////////
// Names updates try to find all the IPs for a given name with a mDNS query
//
// There are two types of names updates:
//
// - immediate updates.
// After a `DomainLookup*()` for a name not in the database we will return the
// first IP we can get with mDNS from other peers. Waiting for more responses would
// mean more latency in the response to the client, so we send only one answer BUT
// we also trigger an immediate update request for that name in order to get all
// the other IPs we didn't wait for...
//
// - periodic updates
// once we have obtained the first group of IPs for a name, we schedule a periodic
// refresh for that name, so we keep the list of IPs for that name up to date.
//
// These names updates are repeated until either
//
// a) there is no interest in the name, determined by a global 'relevant time'
// and the last time some local client asked about the name,
// or
// b) no peers answer one of our refresh requests (because the name has probably
// disappeared from the network)
//
// Check if we must start updating a name and, in that case, trigger a immediate update
func (zone *ZoneDb) startUpdatingName(name string) {
if zone.refreshInterval > 0 {
zone.mx.Lock()
// check if we should enqueue a refresh request for this name
n := zone.getNameSet(defaultRemoteIdent).getName(name, true)
if n.lastRefreshTime.IsZero() {
now := zone.clock.Now()
n.lastRefreshTime = now
zone.mx.Unlock()
Log.Debugf("[zonedb] Creating new immediate refresh request for '%s'", name)
zone.refreshScheds.Add(func() time.Time { return zone.updater(name) }, now)
} else {
zone.mx.Unlock()
}
}
}
// Update the IPs we have for a name
func (zone *ZoneDb) updater(name string) (nextTime time.Time) {
deleteRemoteInfo := func() {
zone.mx.Lock()
zone.getNameSet(defaultRemoteIdent).deleteNameIP(name, net.IP{})
zone.mx.Unlock()
}
// if nobody has asked for this name for long time, just forget about it...
if !zone.IsNameRelevant(name) || zone.IsNameExpired(name) {
Log.Debugf("[zonedb] '%s' seem to be irrelevant now: removing any remote information", name)
deleteRemoteInfo()
return
}
// perform the refresh for this name
fullName := dns.Fqdn(name)
startTime := zone.clock.Now()
Log.Debugf("[zonedb] Refreshing name '%s' with mDNS...", fullName)
res, _ := zone.mdnsCli.InsistentLookupName(fullName)
if res != nil && len(res) > 0 {
numIps := len(res)
zone.mx.Lock()
now := zone.clock.Now()
added, removed := zone.getNameSet(defaultRemoteIdent).getName(name, true).updateIPs(res, now)
zone.mx.Unlock()
Log.Debugf("[zonedb] Obtained %d IPs for name '%s' with mDNS: %d added, %d removed",
numIps, name, added, removed)
// once the name has been updated, we re-schedule the update
nextTime = startTime.Add(zone.refreshInterval)
Log.Debugf("[zonedb] Rescheduling update for '%s' in %s", name, nextTime.Sub(zone.clock.Now()))
} else {
Log.Debugf("[zonedb] nobody knows about '%s'... removing", name)
deleteRemoteInfo()
}
return
}
-116
View File
@@ -1,116 +0,0 @@
package nameserver
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
)
// Check that the refreshing mechanism works as expected
func TestZoneRefresh(t *testing.T) {
const (
refreshInterval = 5
relevantTime = 30
)
var (
name = "mysql.weave.local."
addr1 = "10.2.8.4"
addr2 = "10.2.8.5"
addr3 = "10.2.8.6"
addr4 = "10.2.8.7"
)
EnableDebugLogging(testing.Verbose())
Log.Infoln("TestZoneRefresh starting")
clk := newMockedClock()
// Create multiple zone databases, linked through mocked mDNS connections
zc := ZoneConfig{
RefreshInterval: refreshInterval,
RelevantTime: relevantTime,
Clock: clk,
}
dbs := newZoneDbsWithMockedMDns(2, zc)
dbs.Start()
defer dbs.Stop()
time.Sleep(100 * time.Millisecond) // allow for server to get going
Log.Debugf("Adding '%s' to Db #1", name)
dbs[0].Zone.AddRecord("someident", name, net.ParseIP(addr1))
Log.Debugf("Checking that the name %s is relevant (as it has been locally inserted) and not remote", name)
require.True(t, dbs[0].Zone.IsNameRelevant(name), "name relevant")
require.True(t, dbs[0].Zone.HasNameLocalInfo(name), "local name info")
require.False(t, dbs[0].Zone.HasNameRemoteInfo(name), "remote name info")
Log.Debugf("Asking for '%s' to Db #1: should get 1 IP from the local database...", name)
res, err := dbs[0].Zone.DomainLookupName(name)
require.NoError(t, err)
Log.Debugf("Got: %s", res)
t.Logf("Db #1 after the lookup:\n%s", dbs[0].Zone)
require.Equal(t, 1, len(res), "lookup result")
clk.Forward(refreshInterval / 2)
dbs.Flush()
Log.Debugf("A couple of seconds later, we should still have one IP for that name")
res, err = dbs[0].Zone.DomainLookupName(name)
require.NoError(t, err)
require.Equal(t, 1, len(res), "lookup result")
Log.Debugf("And then we add 2 IPs for that name at ZoneDb 2")
clk.Forward(1)
dbs.Flush()
Log.Debugf("Adding 2 IPs to '%s' in Db #2", name)
dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr2))
dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr3))
Log.Debugf("Perform a lookup, to ensure the name will be updated in the background...")
dbs[0].Zone.DomainLookupName(name)
Log.Debugf("Wait for a while, until a refresh is performed...")
clk.Forward(refreshInterval + 1)
dbs.Flush()
Log.Debugf("A refresh should have been scheduled now: we should have 3 IPs:")
Log.Debugf("the first (local) IP and the others obtained from zone2 with a mDNS query")
Log.Debugf("Asking for '%s' again... we should have 3 IPs now", name)
res, err = dbs[0].Zone.DomainLookupName(name)
Log.Debugf("Got: %s", res)
t.Logf("Db #1 after the second lookup:\n%s", dbs[0].Zone)
require.Equal(t, 3, len(res), "lookup result length")
Log.Debugf("We will not ask for `name` for a while, so it will become irrelevant and will be removed...")
clk.Forward(refreshInterval + relevantTime + 1)
dbs.Flush()
// the name should be irrelevant now, and all remote info should have been
// removed from the zone database
Log.Debugf("Name '%s' should not be in the remote database in ZoneDb 1", name)
require.False(t, dbs[0].Zone.IsNameRelevant(name), "name still relevant after some inactivity time")
require.True(t, dbs[0].Zone.HasNameLocalInfo(name), "local name info")
require.False(t, dbs[0].Zone.HasNameRemoteInfo(name), "remote name info")
Log.Debugf("There is no remote info about this name at zone 1: a new IP appears remotely meanwhile...")
clk.Forward(1)
dbs.Flush()
Log.Debugf("Adding '%s' to Db #2", name)
dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr4))
Log.Debugf("When we ask about this name again, we get 4 IPs (1 local, 3 remote)")
Log.Debugf("Asking for '%s' again... the first lookup will return only the local results", name)
res, err = dbs[0].Zone.DomainLookupName(name)
require.Equal(t, 1, len(res), "lookup result length")
Log.Debugf("... but a second lookup should return all the results in the network")
clk.Forward(refreshInterval + 1)
dbs.Flush()
res, err = dbs[0].Zone.DomainLookupName(name)
Log.Debugf("Got: %s", res)
require.Equal(t, 4, len(res), "lookup result length")
}
-160
View File
@@ -1,160 +0,0 @@
package nameserver
import (
"fmt"
"net"
"testing"
"github.com/stretchr/testify/require"
. "github.com/weaveworks/weave/common"
wt "github.com/weaveworks/weave/testing"
)
func TestZone(t *testing.T) {
var (
container1 = "deadbeef"
container2 = "cowjuice"
name1 = "test1.weave."
name1Addr1 = "10.9.2.1/24"
name1Addr2 = "10.9.2.2/24"
revName1Addr1 = "1.2.9.10.in-addr.arpa."
)
EnableDebugLogging(testing.Verbose())
zone, err := NewZoneDb(ZoneConfig{})
require.NoError(t, err)
err = zone.Start()
require.NoError(t, err)
defer zone.Stop()
ip1, _, _ := net.ParseCIDR(name1Addr1)
t.Logf("Adding '%s'/%s to '%s'", name1, ip1, container1)
err = zone.AddRecord(container1, name1, ip1)
require.NoError(t, err)
// Add a few more records to make the job harder
t.Logf("Adding 'adummy.weave.' to 'abcdef0123'")
err = zone.AddRecord("abcdef0123", "adummy.weave.", net.ParseIP("10.9.0.1"))
require.NoError(t, err)
t.Logf("Adding 'zdummy.weave.' to '0123abcdef'")
err = zone.AddRecord("0123abcdef", "zdummy.weave.", net.ParseIP("10.9.0.2"))
require.NoError(t, err)
t.Logf("Zone database:\n%s", zone)
t.Logf("Checking if we can find the name '%s'", name1)
foundIPs, err := zone.LookupName(name1)
require.NoError(t, err)
if !foundIPs[0].IP().Equal(ip1) {
t.Fatal("Unexpected result for", name1, foundIPs)
}
t.Logf("Checking if we cannot find some silly name like 'something.wrong'")
foundIPs, err = zone.LookupName("something.wrong")
wt.AssertErrorType(t, (*LookupError)(nil), err, fmt.Sprintf("unknown name: %+v", foundIPs))
ip2, _, _ := net.ParseCIDR(name1Addr2)
t.Logf("Adding a second IP for '%s'/%s to '%s'", name1, ip2, container1)
err = zone.AddRecord(container1, name1, ip2)
require.NoError(t, err)
t.Logf("Checking if we can find both the old IP and the new IP for '%s'", name1)
foundIPs, err = zone.LookupName(name1)
require.NoError(t, err)
if !(foundIPs[0].IP().Equal(ip1) || foundIPs[1].IP().Equal(ip1)) {
t.Fatal("Unexpected result for", name1, foundIPs)
}
if !(foundIPs[0].IP().Equal(ip2) || foundIPs[1].IP().Equal(ip2)) {
t.Fatal("Unexpected result for", name1, foundIPs)
}
t.Logf("Checking if we can find the address by IP '1.2.9.10.in-addr.arpa.'")
foundNames, err := zone.LookupInaddr("1.2.9.10.in-addr.arpa.")
require.NoError(t, err)
if foundNames[0].Name() != name1 {
t.Fatal("Unexpected result for", ip1, foundNames)
}
t.Logf("Checking we can not find an unknown address '30.20.10.1.in-addr.arpa.'")
foundNames, err = zone.LookupInaddr("30.20.10.1.in-addr.arpa.")
wt.AssertErrorType(t, (*LookupError)(nil), err, fmt.Sprintf("unknown IP: %+v", foundNames))
t.Logf("Checking if adding again '%s'/%s in %s results in an error", name1, ip1, container1)
err = zone.AddRecord(container1, name1, ip1)
wt.AssertErrorType(t, (*DuplicateError)(nil), err, "duplicate add")
t.Logf("Adding '%s'/%s in %s too", name1, ip1, container2)
err = zone.AddRecord(container2, name1, ip1)
require.NoError(t, err)
name1Removed := 0
err = zone.ObserveName(name1, func() { t.Logf("Observer #1 for '%s' notified.", name1); name1Removed++ })
require.NoError(t, err)
err = zone.ObserveName(name1, func() { t.Logf("Observer #2 for '%s' notified.", name1); name1Removed++ })
require.NoError(t, err)
err = zone.ObserveInaddr(revName1Addr1, func() { t.Logf("Observer #1 for '%s' notified.", revName1Addr1); name1Removed++ })
require.NoError(t, err)
t.Logf("Zone database:\n%s", zone)
t.Logf("Deleting the %s in %s", ip1, container1)
count := zone.DeleteRecords(container1, "", ip1)
require.Equal(t, 1, count, "delete failed")
t.Logf("Zone database:\n%s", zone)
t.Logf("Checking %s's observers have been notified on removal", name1)
if name1Removed < 3 {
t.Logf("Zone database:\n%s", zone)
t.Fatalf("Unexpected number (%d) of calls to observers", name1Removed)
}
t.Logf("Checking %s can be found", name1)
_, err = zone.LookupName(name1)
require.NoError(t, err)
t.Logf("Checking %s is not found after removing %s it in %s and %s in %s",
name1, ip1, container2, ip2, container1)
count = zone.DeleteRecords(container1, "", ip2)
require.Equal(t, 1, count, "delete failed")
count = zone.DeleteRecords(container2, "", ip1)
require.Equal(t, 1, count, "delete failed")
t.Logf("Zone database:\n%s", zone)
_, err = zone.LookupName(name1)
wt.AssertErrorType(t, (*LookupError)(nil), err, "after deleting record")
t.Logf("Checking if removing an unknown record results in an error")
count = zone.DeleteRecords(container1, "", net.ParseIP("0.0.0.0"))
require.Equal(t, 0, count, "delete failed")
}
func TestDeleteRecords(t *testing.T) {
var (
id = "foobar"
name = "foo.weave."
addr1 = "10.2.2.3/24"
addr2 = "10.2.7.8/24"
)
EnableDebugLogging(testing.Verbose())
zone, err := NewZoneDb(ZoneConfig{})
require.NoError(t, err)
err = zone.Start()
require.NoError(t, err)
defer zone.Stop()
for _, addr := range []string{addr1, addr2} {
ip, _, _ := net.ParseCIDR(addr)
err := zone.AddRecord(id, name, ip)
require.NoError(t, err)
}
_, err = zone.LookupName(name)
require.NoError(t, err)
count := zone.DeleteRecords(id, "", net.IP{})
require.Equal(t, 2, count, "wildcard delete failed")
_, err = zone.LookupName(name)
wt.AssertErrorType(t, (*LookupError)(nil), err, "after deleting records for ident")
}
+7
View File
@@ -98,3 +98,10 @@ func Min(a, b Offset) Offset {
}
return a
}
func (addr Address) Reverse() Address {
return ((addr >> 24) & 0xff) | // move byte 3 to byte 0
((addr << 8) & 0xff0000) | // move byte 1 to byte 2
((addr >> 8) & 0xff00) | // move byte 2 to byte 1
((addr << 24) & 0xff000000) // byte 0 to byte 3
}
-6
View File
@@ -1,6 +0,0 @@
FROM scratch
MAINTAINER Weaveworks Inc <help@weave.works>
WORKDIR /home/weave
ADD ./weavedns /home/weave/
EXPOSE 53/udp
ENTRYPOINT ["/home/weave/weavedns"]
-186
View File
@@ -1,186 +0,0 @@
package main
import (
"fmt"
"net"
"os"
"strconv"
"github.com/docker/docker/pkg/mflag"
"github.com/miekg/dns"
. "github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/common/docker"
weavedns "github.com/weaveworks/weave/nameserver"
weavenet "github.com/weaveworks/weave/net"
)
var version = "(unreleased version)"
func main() {
var (
justVersion bool
dockerCli *docker.Client
ifaceName string
apiPath string
domain string
dnsPort int
httpIfaceName string
httpPort int
wait int
ttl int
negTTL int
timeout int
udpbuf int
fallback string
refreshInterval int
relevantTime int
maxAnswers int
cacheLen int
cacheDisabled bool
watch bool
logLevel string
err error
)
mflag.BoolVar(&justVersion, []string{"#version", "-version"}, false, "print version and exit")
mflag.StringVar(&ifaceName, []string{"#iface", "-iface"}, "", "name of interface to use for multicast")
mflag.StringVar(&apiPath, []string{"#api", "-api"}, "unix:///var/run/docker.sock", "path to Docker API socket")
mflag.StringVar(&domain, []string{"#domain", "-domain"}, weavedns.DefaultLocalDomain, "local domain (ie, 'weave.local.')")
mflag.IntVar(&wait, []string{"#wait", "-wait"}, -1, "number of seconds to wait for interfaces to come up (0=don't wait, -1=wait forever)")
mflag.IntVar(&dnsPort, []string{"#dnsport", "#-dnsport", "-dns-port"}, weavedns.DefaultServerPort, "port to listen to DNS requests")
mflag.StringVar(&httpIfaceName, []string{"#httpiface", "#-httpiface", "-http-iface"}, "", "interface on which to listen for HTTP requests (empty string means listen on all interfaces)")
mflag.IntVar(&httpPort, []string{"#httpport", "#-httpport", "-http-port"}, weavedns.DefaultHTTPPort, "port to listen to HTTP requests")
mflag.IntVar(&cacheLen, []string{"#cache", "-cache"}, weavedns.DefaultCacheLen, "cache length")
mflag.IntVar(&ttl, []string{"#ttl", "-ttl"}, weavedns.DefaultLocalTTL, "TTL (in secs) for responses for local names")
mflag.BoolVar(&watch, []string{"#watch", "-watch"}, true, "watch the docker socket for container events")
mflag.StringVar(&logLevel, []string{"-log-level"}, "info", "logging level (debug, info, warning, error)")
// advanced options
mflag.IntVar(&negTTL, []string{"#neg-ttl", "-neg-ttl"}, 0, "negative TTL (in secs) for unanswered queries for local names (0=same value as --ttl")
mflag.IntVar(&refreshInterval, []string{"#refresh", "-refresh"}, weavedns.DefaultRefreshInterval, "refresh interval (in secs) for local names (0=disable)")
mflag.IntVar(&maxAnswers, []string{"#max-answers", "#-max-answers", "-dns-max-answers"}, weavedns.DefaultMaxAnswers, "maximum number of answers returned to clients (0=unlimited)")
mflag.IntVar(&relevantTime, []string{"#relevant", "-relevant"}, weavedns.DefaultRelevantTime, "life time for info in the absence of queries (in secs)")
mflag.IntVar(&udpbuf, []string{"#udpbuf", "#-udpbuf", "-dns-udpbuf"}, weavedns.DefaultUDPBuflen, "UDP buffer length for DNS")
mflag.IntVar(&timeout, []string{"#timeout", "#-timeout", "-dns-timeout"}, weavedns.DefaultTimeout, "timeout for resolutions (in millisecs)")
mflag.BoolVar(&cacheDisabled, []string{"#no-cache", "-no-cache"}, false, "disable the cache")
mflag.StringVar(&fallback, []string{"#fallback", "#-fallback", "-dns-fallback"}, "", "force a fallback server (ie, '8.8.8.8:53') (instead of /etc/resolv.conf values)")
mflag.Parse()
if justVersion {
fmt.Printf("weave DNS %s\n", version)
os.Exit(0)
}
SetLogLevel(logLevel)
Log.Infof("[main] WeaveDNS version %s", version) // first thing in log: the version
var iface *net.Interface
if ifaceName != "" {
var err error
Log.Infoln("[main] Waiting for mDNS interface", ifaceName, "to come up")
iface, err = weavenet.EnsureInterface(ifaceName, wait)
if err != nil {
Log.Fatal(err)
} else {
Log.Infoln("[main] Interface", ifaceName, "is up")
}
}
var httpIP string
if httpIfaceName == "" {
httpIP = "0.0.0.0"
} else {
Log.Infoln("[main] Waiting for HTTP interface", httpIfaceName, "to come up")
httpIface, err := weavenet.EnsureInterface(httpIfaceName, wait)
if err != nil {
Log.Fatal(err)
}
Log.Infoln("[main] Interface", httpIfaceName, "is up")
addrs, err := httpIface.Addrs()
if err != nil {
Log.Fatal(err)
}
if len(addrs) == 0 {
Log.Fatal("[main] No addresses on HTTP interface")
}
ip, _, err := net.ParseCIDR(addrs[0].String())
if err != nil {
Log.Fatal(err)
}
httpIP = ip.String()
}
httpAddr := net.JoinHostPort(httpIP, strconv.Itoa(httpPort))
zoneConfig := weavedns.ZoneConfig{
Domain: domain,
Iface: iface,
LocalTTL: ttl,
RefreshInterval: refreshInterval,
RelevantTime: relevantTime,
}
zone, err := weavedns.NewZoneDb(zoneConfig)
if err != nil {
Log.Fatal("[main] Unable to initialize the Zone database", err)
}
if err := zone.Start(); err != nil {
Log.Fatal("[main] Unable to start the Zone database", err)
}
defer zone.Stop()
dockerCli, err = docker.NewClient(apiPath)
if err != nil {
Log.Fatal("[main] Unable to start docker client: ", err)
}
if watch {
err := dockerCli.AddObserver(zone)
if err != nil {
Log.Fatal("[main] Unable to start watcher", err)
}
}
srvConfig := weavedns.DNSServerConfig{
Zone: zone,
Port: dnsPort,
CacheLen: cacheLen,
LocalTTL: ttl,
CacheNegLocalTTL: negTTL,
MaxAnswers: maxAnswers,
Timeout: timeout,
UDPBufLen: udpbuf,
CacheDisabled: cacheDisabled,
}
if len(fallback) > 0 {
fallbackHost, fallbackPort, err := net.SplitHostPort(fallback)
if err != nil {
Log.Fatal("[main] Could not parse fallback host and port", err)
}
srvConfig.UpstreamCfg = &dns.ClientConfig{Servers: []string{fallbackHost}, Port: fallbackPort}
Log.Debugf("[main] DNS fallback at %s:%s", fallbackHost, fallbackPort)
}
srv, err := weavedns.NewDNSServer(srvConfig)
if err != nil {
Log.Fatal("[main] Failed to initialize the WeaveDNS server", err)
}
httpListener, err := net.Listen("tcp", httpAddr)
if err != nil {
Log.Fatal("[main] Unable to create http listener: ", err)
}
Log.Infoln("[main] HTTP API listening on", httpAddr)
go SignalHandlerLoop(srv)
go weavedns.ServeHTTP(httpListener, version, srv, dockerCli)
err = srv.Start()
if err != nil {
Log.Fatal("[main] Failed to start the WeaveDNS server: ", err)
}
srv.ActivateAndServe()
}
+39 -7
View File
@@ -8,6 +8,7 @@ import (
"os"
"runtime"
"strings"
"time"
"github.com/davecheney/profile"
"github.com/docker/docker/pkg/mflag"
@@ -16,6 +17,7 @@ import (
. "github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/common/docker"
"github.com/weaveworks/weave/ipam"
"github.com/weaveworks/weave/nameserver"
weavenet "github.com/weaveworks/weave/net"
"github.com/weaveworks/weave/net/address"
weave "github.com/weaveworks/weave/router"
@@ -52,6 +54,10 @@ func main() {
peerCount int
apiPath string
peers []string
dnsDomain string
dnsPort int
dnsTTL int
dnsClientTimeout time.Duration
)
mflag.BoolVar(&justVersion, []string{"#version", "-version"}, false, "print version and exit")
@@ -73,6 +79,11 @@ func main() {
mflag.StringVar(&ipsubnetCIDR, []string{"#ipsubnet", "#-ipsubnet", "-ipalloc-default-subnet"}, "", "subnet to allocate within by default, in CIDR notation")
mflag.IntVar(&peerCount, []string{"#initpeercount", "#-initpeercount", "-init-peer-count"}, 0, "number of peers in network (for IP address allocation)")
mflag.StringVar(&apiPath, []string{"#api", "-api"}, "unix:///var/run/docker.sock", "Path to Docker API socket")
mflag.StringVar(&dnsDomain, []string{"-dns-domain"}, nameserver.DefaultDomain, "local domain to server requests for")
mflag.IntVar(&dnsPort, []string{"-dns-port"}, nameserver.DefaultPort, "port to listen on for DNS requests")
mflag.IntVar(&dnsTTL, []string{"-dns-ttl"}, nameserver.DefaultTTL, "TTL for DNS request from our domain")
mflag.DurationVar(&dnsClientTimeout, []string{"-dns-fallback-timeout"}, nameserver.DefaultClientTimeout, "timeout for fallback DNS requests")
mflag.Parse()
peers = mflag.Args()
@@ -141,15 +152,15 @@ func main() {
router := weave.NewRouter(config, name, nickName)
Log.Println("Our name is", router.Ourself)
dockerCli, err := docker.NewClient(apiPath)
if err != nil {
Log.Fatal("Unable to start docker client: ", err)
}
var allocator *ipam.Allocator
var defaultSubnet address.CIDR
var dockerCli *docker.Client
if iprangeCIDR != "" {
allocator, defaultSubnet = createAllocator(router, iprangeCIDR, ipsubnetCIDR, determineQuorum(peerCount, peers))
dockerCli, err = docker.NewClient(apiPath)
if err != nil {
Log.Fatal("Unable to start docker client: ", err)
}
if err = dockerCli.AddObserver(allocator); err != nil {
Log.Fatal("Unable to start watcher", err)
}
@@ -157,6 +168,22 @@ func main() {
Log.Fatal("--init-peer-count flag specified without --ipalloc-range")
}
ns := nameserver.New(router.Ourself.Peer.Name, router.Peers, dockerCli, dnsDomain)
ns.SetGossip(router.NewGossip("nameserver", ns))
if err = dockerCli.AddObserver(ns); err != nil {
Log.Fatal("Unable to start watcher", err)
}
ns.Start()
defer ns.Stop()
dnsserver, err := nameserver.NewDNSServer(ns, dnsDomain, dnsPort,
uint32(dnsTTL), dnsClientTimeout)
if err != nil {
Log.Fatal("Unable to start dns server: ", err)
}
dnsserver.ActivateAndServe()
defer dnsserver.Stop()
router.Start()
if errors := router.ConnectionMaker.InitiateConnections(peers, false); len(errors) > 0 {
Log.Fatal(errorMessages(errors))
@@ -166,7 +193,7 @@ func main() {
// so there is no point in doing "weave launch --http-addr ''".
// This is here to support stand-alone use of weaver.
if httpAddr != "" {
go handleHTTP(router, httpAddr, allocator, defaultSubnet, dockerCli)
go handleHTTP(router, httpAddr, allocator, defaultSubnet, dockerCli, ns, dnsserver)
}
SignalHandlerLoop(router)
@@ -270,13 +297,15 @@ func determineQuorum(initPeerCountFlag int, peers []string) uint {
return quorum
}
func handleHTTP(router *weave.Router, httpAddr string, allocator *ipam.Allocator, defaultSubnet address.CIDR, docker *docker.Client) {
func handleHTTP(router *weave.Router, httpAddr string, allocator *ipam.Allocator, defaultSubnet address.CIDR, docker *docker.Client, ns *nameserver.Nameserver, dnsserver *nameserver.DNSServer) {
muxRouter := mux.NewRouter()
if allocator != nil {
allocator.HandleHTTP(muxRouter, defaultSubnet, docker)
}
ns.HandleHTTP(muxRouter)
muxRouter.Methods("GET").Path("/status").Headers("Accept", "application/json").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json, _ := router.StatusJSON(version)
w.Header().Set("Content-Type", "application/json")
@@ -290,6 +319,9 @@ func handleHTTP(router *weave.Router, httpAddr string, allocator *ipam.Allocator
fmt.Fprintln(w, allocator.String())
fmt.Fprintln(w, "Allocator default subnet:", defaultSubnet)
}
fmt.Fprintln(w, "")
fmt.Fprintln(w, dnsserver.String())
fmt.Fprintln(w, ns.String())
})
muxRouter.Methods("POST").Path("/connect").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+7 -5
View File
@@ -9,8 +9,10 @@ import (
"strings"
"github.com/fsouza/go-dockerclient"
. "github.com/weaveworks/weave/common"
"github.com/weaveworks/weave/nameserver"
"github.com/weaveworks/weave/router"
)
const MaxDockerHostname = 64
@@ -137,15 +139,15 @@ func (i *createContainerInterceptor) setWeaveDNS(container *createContainerReque
}
func (i *createContainerInterceptor) getDNSDomain() (domain string, running bool) {
domain = nameserver.DefaultLocalDomain
dnsContainer, err := i.proxy.client.InspectContainer("weavedns")
domain = nameserver.DefaultDomain
weaveContainer, err := i.proxy.client.InspectContainer("weave")
if err != nil ||
dnsContainer.NetworkSettings == nil ||
dnsContainer.NetworkSettings.IPAddress == "" {
weaveContainer.NetworkSettings == nil ||
weaveContainer.NetworkSettings.IPAddress == "" {
return
}
url := fmt.Sprintf("http://%s:%d/domain", dnsContainer.NetworkSettings.IPAddress, nameserver.DefaultHTTPPort)
url := fmt.Sprintf("http://%s:%d/domain", weaveContainer.NetworkSettings.IPAddress, router.HTTPPort)
resp, err := http.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
return
+1 -1
View File
@@ -8,7 +8,7 @@ layout: default
Weave automatically assigns containers an IP address that is unique
across the network, and releases that address when a container
exits. This happens for all invocations of the `run`, `start`,
`attach`, `detach`, `expose`, `hide` and `launch-dns` commands, unless
`attach`, `detach`, `expose`, and `hide` commands, unless
the user explictly specified an address. Weave can also assign
addresses in multiple subnets.
+1 -1
View File
@@ -30,7 +30,7 @@ start it simultaneously with the router and weaveDNS via `launch`:
or independently via `launch-proxy`:
host1$ weave launch-router && weave launch-dns && weave launch-proxy
host1$ weave launch-router && weave launch-proxy
The first form is more convenient, however you can only pass proxy
related configuration arguments to `launch-proxy` so if you need to
+35 -67
View File
@@ -6,15 +6,36 @@ layout: default
# WeaveDNS (service discovery) Design Notes
The model is that each host has a service that is notified of
hostnames and weave addresses for containers on the host. It binds to
the host bridge to answer DNS queries from local containers; anything
it can't answer, it asks with mDNS on the weave network.
hostnames and weave addresses for containers on the host. Like IPAM,
this service is embedded within the router. It binds to
the host bridge to answer DNS queries from local containers; for
anything it can't answer, it uses the infomation in the host's
/etc/resolv.conf to query an 'fallback' server.
The service is comprised of a DNS server, which answers all DNS queries
from containers, and a in-memory database of hostnames and IPs. The
database on each node contains a complete copy of the hostnames and IPs
for every containers in the cluster.
For hostname queries in the local domain (default weave.local), the DNS
server will consult the in-memory database. For reverse queries, we
first consult the local database, and if not found we query the
upstream server. For all other queries, we consult the upstream
server.
Updates to the in-memory database are broadcast to other DNS servers
within the cluster. The in-memory database only contains entries from
connected DNS servers; if a DNS server becomes partitioned from the
cluster, entries belonging to that server are removed from each node in
the cluster. When the partitioned DNS server reconnects, the entries
are re-broadcast around the cluster.
The DNS server also listens to the Docker event stream, and removes
entries for containers when they die. Entries removed in this way are
tombstoned, and the tombstone lazily broadcast around the cluster.
After a short timeout the tombstones are independantly removed from
each host.
The service is divided into two components: the first is the DNS
server, which serves the records it is told about, and otherwise asks
via mDNS on the weave network. The other component is the DNS updater,
specific to Docker, which monitors the running containers so to add
and remove records from the DNS server.
## DNS server API
@@ -24,60 +45,21 @@ and methods:
`PUT /name/<identifier>/<ip-address>`
Put a record for an IP, bound to a host-scoped identifier (e.g., a
container ID), in the DNS database.
container ID), in the DNS database. The request body must contain
a `fqdn=foo.weave.local` key pair.
`DELETE /name/<identifier>/<ip-address>`
Remove a specific record for an IP and host-scoped identifier.
Remove a specific record for an IP and host-scoped identifier. The request
body can optionally contain a `fqdn=foo.weave.local` key pair.
`DELETE /name/<identifier>`
Remove all records for the host-scoped identifier.
`GET /name/`
`GET /name/<fqdn>`
List all the records in the database.
`GET /name/<identifier>`
List all the records for a host-scoped identifier.
`GET /status`
Give the server status.
### Record structure
The record structure is *either* form fields (useful for scripting
PUTs), or a JSON object with the same fields. The fields are:
```js
{
"fqdn": string,
"routing_prefix": number,
"local_ip": string
}
```
> Also TTL?
## DNS server behaviour
The DNS server listens for the HTTP requests as above, and maintains a
database of host-local names accordingly.
It also listens on the host-local network for DNS queries (port
53). Anything that is in its database it responds to immediately. For
other requests, it asks using multicast DNS on the weave network.
Meanwhile, the DNS server is listening for mDNS queries, and responds
if it the queried name is in its database.
### Caching mDNS answers
The DNS server will hear the answers to its own and other servers'
mDNS queries sent on the weave network. It can cache these, to respond
to local DNS requests; the cache is consulted before using mDNS.
List of all IPs (in JSON format) for givne FQDN.
## DNS updater
@@ -99,17 +81,3 @@ network attachment of the container, and updates the DNS server.
> Perhaps it could put containers on a watch list when it's noticed
> them.
## Container startup
> TBD
## Grouping and load balancing
> TBD
## Extensions
* An updater that checks health more specifically, e.g., by testing
that a particular port is being listened to, or that it can get a
200 OK. These could be encoded in the container environment
variables, as per registrator.
+51 -94
View File
@@ -26,25 +26,15 @@ hosts.
## <a name="usage"></a>Using weaveDNS
WeaveDNS is deployed as a set of containers that communicate with each
other over the weave network. One such container needs to be started
on every weave host, either simultaneously with the Weave router and
Weave Docker API proxy via `launch`:
WeaveDNS is deployed as an embedded service within the Weave router.
The service is automatically started when the router is launched:
```bash
host1$ weave launch
host1$ eval $(weave env)
```
or independently via `launch-dns`:
```bash
host1$ weave launch-router && weave launch-dns && weave launch-proxy
host1$ eval $(weave env)
```
The first form is more convenient, however you can only pass weaveDNS
related configuration arguments to `launch-dns` so if you need to
modify the default behaviour you will have to use the latter.
WeaveDNS related configuration arguments can be passed to `launch`.
Application containers will use weaveDNS automatically if it is
running at the point when they are started. They will use it for name
@@ -61,53 +51,40 @@ root@ubuntu:/# ping pingme
> **Please note** if both hostname and container name are specified at
> the same time the hostname takes precedence; in this circumstance if
> the hostname is not in the weaveDNS domain the container will *not* be
> registered, but will still use weaveDNS for resolution.
> the hostname is not in the weaveDNS domain the container will *not*
> be registered, but will still use weaveDNS for resolution.
It is also possible to force or forbid an application container's use
of weaveDNS with the `--with-dns` and `--without-dns` options to
`weave run` and `weave launch-proxy`; these override the runtime
detection of the weaveDNS container.
Each weaveDNS container started with `launch-dns` needs its own unique
IP address in a subnet that is common to all weaveDNS containers. In
the example above we did not specify such an address, so one was
allocated automatically from the default subnet; you can however
specify an address in CIDR format manually. In this case you are
responsible for ensuring that the IP addresses specified are uniquely
allocated and not in use by any other container.
Finally, weaveDNS can be stopped independently with
host1$ weave stop-dns
or in conjunction with the router and proxy via `stop`.
`weave run` and `weave launch-proxy`.
## <a name="how-it-works"></a>How it works
The weaveDNS container running on every host acts as the nameserver
for containers on that host. It learns about hostnames for local
containers from the Weave Docker API proxy and from the `weave run`
command. If a hostname is in the `.weave.local` domain then weaveDNS
records the association of that name with the container's weave IP
address(es).
The weaveDNS service running on every host acts as the nameserver for
containers on that host. It learns about hostnames for local containers
from the proxy and from the `weave run` command. If a hostname is in
the `.weave.local` domain then weaveDNS records the association of that
name with the container's weave IP address(es) in its in-memory
database, and broadcasts the association to other weave peers in the
cluster.
When weaveDNS is queried for a name in the `.weave.local` domain, it
first checks its own records. If the name is not found there, it asks
the weaveDNS servers on the other hosts in the weave network.
looks up the hostname its in memory database and responds with the IPs
of all containers for that hostname across the entire cluster, in a
random order.
When weaveDNS is queried for a name in a domain other than
`.weave.local`, it queries the host's configured nameserver,
which is the standard behaviour for Docker containers.
`.weave.local`, it queries the host's configured nameserver, which is
the standard behaviour for Docker containers.
So that containers can connect to a stable and always routable IP
address, weaveDNS publishes its port 53 to the Docker bridge device,
which is assumed to be `docker0`. Some configurations may use a
different Docker bridge device. To supply a different bridge device,
use the environment variable `DOCKER_BRIDGE`, e.g.,
address, weaveDNS listens on port 53 to the Docker bridge device, which
is assumed to be `docker0`. Some configurations may use a different
Docker bridge device. To supply a different bridge device, use the
environment variable `DOCKER_BRIDGE`, e.g.,
```bash
$ sudo DOCKER_BRIDGE=someother weave launch-dns
$ sudo DOCKER_BRIDGE=someother weave launch
```
In the event that weaveDNS is launched in this way, it's important that
@@ -120,8 +97,8 @@ $ sudo DOCKER_BRIDGE=someother weave run --with-dns ...
## <a name="load-balancing"></a>Load balancing
It is permissible to register multiple containers with the same name:
weaveDNS picks one address at random on each request. This provides a
basic load balancing capability.
weaveDNS returns all addresses, in a random order, for each request.
This provides a basic load balancing capability.
Returning to our earlier example, let us start an additional `pingme`
container, this time on the 2nd host, and then run some ping tests...
@@ -152,8 +129,9 @@ Notice how the ping reaches different addresses.
WeaveDNS removes the addresses of any container that dies. This offers
a simple way to implement redundancy. E.g. if in our example we stop
one of the `pingme` containers and re-run the ping tests, eventually
(within ~30s at most, since that is the weaveDNS [cache expiry time](#ttl)) we
will only be hitting the address of the container that is still alive.
(within ~30s at most, since that is the weaveDNS
[cache expiry time](#ttl)) we will only be hitting the address of the
container that is still alive.
## <a name="add-remove"></a>Adding and removing extra DNS entries
@@ -169,14 +147,15 @@ $ weave dns-add 10.2.1.27 $C -h pingme2.weave.local
You can also use `dns-add` to add the container's configured hostname
and domain, simply by omitting `-h <fqdn>`.
The inverse operation can be carried out using the `dns-remove` command:
The inverse operation can be carried out using the `dns-remove`
command:
```bash
$ weave dns-remove 10.2.1.27 $C
```
When queried about a name with multiple IPs, weaveDNS returns a random
result from the set of IPs available.
When queried about a name with multiple IPs, weaveDNS returns all IPs
available in a random order.
## <a name="hot-swapping"></a>Hot-swapping service containers
@@ -187,33 +166,20 @@ server container and then [remove](#add-remove) the entry for the old
server container. Later, when all connections to the old server have
terminated, stop the container as normal.
## <a name="retain-stopped"></a>Retaining DNS entries when containers stop
By default, weaveDNS watches docker events and removes entries for any
containers that die. You can tell it not to, by adding `--watch=false`
to the container args:
```bash
$ weave launch-dns --watch=false
```
## <a name="ttl"></a>Configuring a custom TTL
By default, weaveDNS specifies a TTL of 30 seconds in any reply sent to
another peer. Peers will honor the TTL received and cache the answer
until it is considered invalid.
However, you can force a different TTL value by launching weaveDNS with
the `--ttl` argument:
By default, weaveDNS specifies a TTL of 30 seconds in responses to DNS
requests. However, you can force a different TTL value by launching
weave with the `--dns-ttl` argument:
```bash
$ weave launch-dns --ttl=10
$ weave launch --dns-ttl=10
```
This will shorten the lifespan of answers sent to other peers,
so you will be effectively reducing the probability of them having stale
information, but you will also be increasing their resolution times (as
their cache hit rate will be reduced) and the number of request this
This will shorten the lifespan of answers sent to clients, so you will
be effectively reducing the probability of them having stale
information, but you will also be increasing the number of request this
weaveDNS instance will receive.
## <a name="domain-search-path"></a>Configuring the domain search paths
@@ -241,12 +207,12 @@ docker run -ti \
## <a name="local-domain"></a>Using a different local domain
By default, weaveDNS uses `weave.local.` as the domain for names on the
Weave network. In general users do not need to change this domain, but you
can force weaveDNS to use a different domain by launching it
with the `--domain` argument. For example,
Weave network. In general users do not need to change this domain, but
you can force weaveDNS to use a different domain by launching it with
the `--dns-domain` argument. For example,
```bash
$ weave launch-dns --domain="mycompany.local."
$ weave launch --dns-domain="mycompany.local."
```
The local domain should end with `local.`, since these names are
@@ -265,16 +231,11 @@ DNS:
````
...
weave DNS 1.0.0
Listen address :53
Fallback DNS config &{[10.0.2.3] [] 53 1 5 2}
WeaveDNS (d6:1b:df:42:af:7d)
listening on port 53, for domain weave.local.
response ttl 30
Local domain weave.local.
Interface &{74 65535 ethwe 82:0c:92:84:0e:88 up|broadcast|multicast}
Zone database:
144b75a9b873: pingme.weave.local.[10.40.0.1]/OBS:1
74a5510a91ad: ubuntu.weave.local.[10.40.0.2]
weave:remote: pingme.weave.local.[10.32.0.2]/TTL:30
1d836dea2c56: foo.weave.local. [10.2.2.1]
...
````
@@ -284,17 +245,16 @@ guide](troubleshooting.html#status-report) for more detail.
The second section is pertinent to weaveDNS, and includes:
* Port on which the DNS server is listening
* The local domain suffix which is being served
* The address on which the DNS server is listening
* The interface being used for multicast DNS
* The fallback DNS which will be used to resolve non local names
* The names known to the local weaveDNS server. Each entry comprises
* The response ttl
* The names known to the weaveDNS server. Each entry comprises
the container ID, IP address and its fully qualified domain name
Information on the processing of queries, and the general operation of
weaveDNS, can be obtained from the container logs with
docker logs weavedns
docker logs weave
## <a name="limitations"></a>Present limitations
@@ -304,6 +264,3 @@ weaveDNS, can be obtained from the container logs with
* The server may give unreachable IPs as answers, since it doesn't
try to filter by reachability. If you use subnets, align your
hostnames with the subnets.
* We use UDP multicast to find out about remote names (from weaveDNS
servers on other hosts); this likely won't scale well beyond a
certain point T.B.D.
+1 -1
View File
@@ -8,7 +8,7 @@ NAME=seetwo.weave.local
start_suite "Resolve names on a single host"
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch
start_container $HOST1 $C2/24 --name=c2 -h $NAME
start_container_with_dns $HOST1 $C1/24 --name=c1
+35
View File
@@ -0,0 +1,35 @@
#! /bin/bash
. ./config.sh
C1=10.2.3.78
C2=10.2.3.34
C2a=10.2.3.35
C2b=10.2.3.36
UNIVERSE=10.2.4.0/24
NAME2=seetwo.weave.local
NAME4=seefour.weave.local
start_suite "Resolve names across hosts"
weave_on $HOST1 launch --ipalloc-range $UNIVERSE
weave_on $HOST2 launch --ipalloc-range $UNIVERSE $HOST1
start_container $HOST2 $C2/24 --name=c2 -h $NAME2
start_container_with_dns $HOST1 $C1/24 --name=c1
assert_dns_record $HOST1 c1 $NAME2 $C2
# resolution for names mapped to multiple addresses
weave_on $HOST2 dns-add $C2a c2 -h $NAME2
weave_on $HOST2 dns-add $C2b c2 -h $NAME2
assert_dns_record $HOST1 c1 $NAME2 $C2 $C2a $C2b
# resolution when containers addresses come from IPAM
start_container $HOST2 --name=c4 -h $NAME4
start_container_with_dns $HOST1 --name=c3
C4=$(container_ip $HOST2 c4)
assert_dns_record $HOST1 c3 $NAME4 $C4
assert_raises "exec_on $HOST1 c3 getent hosts 8.8.8.8 | grep google"
end_suite
-61
View File
@@ -1,61 +0,0 @@
#! /bin/bash
. ./config.sh
C1=10.2.3.78
C2=10.2.3.34
C2a=10.2.3.35
C2b=10.2.3.36
UNIVERSE=10.2.4.0/24
NAME2=seetwo.weave.local
NAME4=seefour.weave.local
start() {
launch_dns_on $HOST1 $1 >/dev/null
launch_dns_on $HOST2 $2 >/dev/null
}
stop() {
weave_on $HOST1 stop-dns
weave_on $HOST2 stop-dns
}
check() {
start $1 $2
assert_dns_record $HOST1 c1 $NAME2 $C2
assert_raises "exec_on $HOST1 c1 getent hosts 8.8.8.8 | grep google"
stop
}
start_suite "Resolve names across hosts (with and without IPAM), and repopulate on restart"
weave_on $HOST1 launch-router --ipalloc-range $UNIVERSE
weave_on $HOST2 launch-router --ipalloc-range $UNIVERSE $HOST1
start_container $HOST2 $C2/24 --name=c2 -h $NAME2
start_container_with_dns $HOST1 $C1/24 --name=c1
# resolution when DNS is launched with various ways of obtaining an IP
# NB: this also tests re-population on DNS restart
check ip:10.2.254.1/24 ip:10.2.254.2/24
check net:10.2.4.128/25 net:10.2.4.128/25
check net:default net:default
check
start 10.2.254.1/24 10.2.254.2/24
# resolution for names mapped to multiple addresses
weave_on $HOST2 dns-add $C2a c2 -h $NAME2
weave_on $HOST2 dns-add $C2b c2 -h $NAME2
assert_dns_record $HOST1 c1 $NAME2 $C2 $C2a $C2b
# resolution when containers addresses come from IPAM
start_container $HOST2 --name=c4 -h $NAME4
start_container_with_dns $HOST1 --name=c3
C4=$(container_ip $HOST2 c4)
assert_dns_record $HOST1 c3 $NAME4 $C4
assert_raises "exec_on $HOST1 c3 getent hosts 8.8.8.8 | grep google"
stop
end_suite
+14 -1
View File
@@ -9,7 +9,20 @@ NAME=seetwo.$DOMAIN
start_suite "Resolve names in custom domain"
launch_dns_on $HOST1 10.2.254.1/24 --domain $DOMAIN.
# Check with a trailing dot on domain
weave_on $HOST1 launch --dns-domain $DOMAIN.
start_container $HOST1 $C2/24 --name=c2 -h $NAME
start_container_with_dns $HOST1 $C1/24 --name=c1
assert_dns_record $HOST1 c1 $NAME $C2
rm_containers $HOST1 c1
rm_containers $HOST1 c2
weave_on $HOST1 stop
# Check without a trailing dot on domain
weave_on $HOST1 launch --dns-domain $DOMAIN
start_container $HOST1 $C2/24 --name=c2 -h $NAME
start_container_with_dns $HOST1 $C1/24 --name=c1
+7 -7
View File
@@ -11,19 +11,19 @@ NAME=seeone.$DOMAIN
start_suite "Resolve unqualified names"
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch
start_container $HOST1 $C1/24 --name=c1 -h $NAME
start_container_with_dns $HOST1 $C2/24 --name=c2 -h seetwo.$DOMAIN
start_container_with_dns $HOST1 $C3/24 --name=c3 --dns-search=$DOMAIN
container=$(start_container_with_dns $HOST1 $C4/24)
check() {
assert "exec_on $HOST1 $1 getent hosts seeone | tr -s ' '" "$C1 $NAME"
}
assert_dns_a_record $HOST1 c2 seeone $C1 $NAME
assert_dns_a_record $HOST1 c3 seeone $C1 $NAME
assert_dns_a_record $HOST1 "$container" seeone $C1 $NAME
check c2
check c3
check "$container"
# check that unqualified names are automatically qualified for broken resolvers
weave_on $HOST1 dns-add $C1 c1 -h mysql.weave.local
assert_dns_a_record $HOST1 c1 mysql $C1
end_suite
+1 -1
View File
@@ -10,7 +10,7 @@ NAME3=seethree.weave.local
start_suite "Add and remove names on a single host"
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch
start_container $HOST1 --without-dns $C2/24 --name=c2
start_container_with_dns $HOST1 $C1/24 --name=c1
+25
View File
@@ -0,0 +1,25 @@
#! /bin/bash
. ./config.sh
C1=10.2.0.78
C2=10.2.0.34
NAME=seetwo.weave.local
start_suite "Restart Weave, check for repopulation"
weave_on $HOST1 launch
start_container $HOST1 $C2/24 --name=c2 -h $NAME
start_container_with_dns $HOST1 $C1/24 --name=c1
assert_dns_record $HOST1 c1 $NAME $C2
assert_raises "exec_on $HOST1 c1 dig MX $NAME | grep -q 'status: NXDOMAIN'"
weave_on $HOST1 stop
weave_on $HOST1 launch
assert_dns_record $HOST1 c1 $NAME $C2
assert_raises "exec_on $HOST1 c1 dig MX $NAME | grep -q 'status: NXDOMAIN'"
end_suite
+1 -1
View File
@@ -16,7 +16,7 @@ check() {
start_suite "Automatic DNS record removal on container death"
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch
start_container_with_dns $HOST1 $C1/24 --name=c1
start_container $HOST1 $C2/24 $DOPTS
+2 -9
View File
@@ -20,19 +20,12 @@ assert_resolution() {
start_suite "With or without DNS test"
# Assert behaviour without weaveDNS running
weave_on $HOST1 launch-router
start_container $HOST1 $TARGET_IP/24 --name c2 -h $TARGET
DNS_IP=$(weave_on $HOST1 docker-bridge-ip)
# Assert behaviour without weaveDNS running
assert_no_resolution --without-dns
assert_no_resolution
assert_no_resolution --with-dns
weave_on $HOST1 launch-dns 10.2.254.1/24 $WEAVEDNS_ARGS
start_container $HOST1 $TARGET_IP/24 --name c2 -h $TARGET
# Assert behaviour with weaveDNS running
assert_no_resolution --without-dns
assert_resolution
assert_resolution --with-dns
+1 -1
View File
@@ -4,7 +4,7 @@
start_suite "Resolve a non-weave address"
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch-router
start_container_with_dns $HOST1 10.2.1.5/24 --name=c1
+23
View File
@@ -0,0 +1,23 @@
#! /bin/bash
. ./config.sh
NAME=seetwo.weave.local
N=50
start_suite "Add $N dns entries and check we get the right response."
weave_on $HOST1 launch
CID=$(start_container_with_dns $HOST1 10.2.1.0/24 --name=c0)
IPS=""
for i in $(seq $N); do
IPS="$IPS 10.2.1.$i"
done
weave_on $HOST1 dns-add $IPS $CID -h $NAME
assert_dns_record $HOST1 c0 $NAME $IPS
assert_raises "exec_on $HOST1 c0 dig MX $NAME | grep -q 'status: NXDOMAIN'"
end_suite
-1
View File
@@ -74,7 +74,6 @@ start_suite "Weave run/start/attach/detach/expose/hide with multiple cidr argume
# lowest available address in the subnet
weave_on $HOST1 launch-router --ipalloc-range 10.2.3.0/24
launch_dns_on $HOST1 10.254.254.254/24
# Run container with three cidrs
CID=$(start_container $HOST1 10.2.1.1/24 ip:10.2.2.1/24 net:10.2.3.0/24 --name=multicidr -h $NAME | cut -b 1-12)
+1 -1
View File
@@ -37,7 +37,7 @@ weave_on $HOST1 launch-proxy
assert_no_resolution
# Assert behaviour with weaveDNS running
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch-router
boot_containers
assert_dns_record $HOST1 c1 $C2_NAME $C2
assert_dns_record $HOST1 c2 $C1_NAME $C1
+1 -2
View File
@@ -15,8 +15,7 @@ start_container() {
start_suite "Resolve unqualified names"
weave_on $HOST1 launch-dns 10.2.254.1/24
weave_on $HOST1 launch-proxy
weave_on $HOST1 launch
start_container -e WEAVE_CIDR=$C1/24 --name=c1 -h $NAME
start_container -e WEAVE_CIDR=$C2/24 --name=c2 -h seetwo.$DOMAIN
+1 -2
View File
@@ -8,8 +8,7 @@ NAME=seetwo.weave.local
start_suite "Proxy restart reattaches networking to containers"
launch_dns_on $HOST1 10.2.254.1/24
weave_on $HOST1 launch-proxy --with-dns
weave_on $HOST1 launch
proxy docker_on $HOST1 run -e WEAVE_CIDR=$C2/24 -dt --name=c2 -h $NAME $SMALL_IMAGE /bin/sh
proxy docker_on $HOST1 run -e WEAVE_CIDR=$C1/24 -dt --name=c1 $DNS_IMAGE /bin/sh
+3 -12
View File
@@ -43,10 +43,6 @@ CHECK_ETHWE_UP="grep ^1$ /sys/class/net/ethwe/carrier"
DOCKER_PORT=2375
WEAVEDNS_ARGS="--no-cache"
[ -n "$DEBUG" ] && WEAVEDNS_ARGS="$WEAVEDNS_ARGS --log-level=debug"
upload_executable() {
host=$1
file=$2
@@ -133,12 +129,6 @@ exec_on() {
docker -H tcp://$host:$DOCKER_PORT exec $container "$@"
}
launch_dns_on() {
host=$1
shift 1
weave_on $host launch-dns $@ $WEAVEDNS_ARGS
}
start_container() {
host=$1
shift 1
@@ -188,9 +178,10 @@ assert_no_dns_record() {
assert_raises "exec_on $host $container getent hosts $name" 2
}
# assert_dns_a_record <host> <container> <name> <ip>
# assert_dns_a_record <host> <container> <name> <ip> [<expected_name>]
assert_dns_a_record() {
assert "exec_on $1 $2 getent hosts $3 | tr -s ' '" "$4 $3"
exp_name=${5:-$3}
assert "exec_on $1 $2 getent hosts $3 | tr -s ' ' | cut -d ' ' -f 1,2" "$4 $exp_name"
}
# assert_dns_ptr_record <host> <container> <name> <ip>
+28 -65
View File
@@ -34,7 +34,6 @@ weave launch [--password <password>] [--nickname <nickname>]
weave launch-router [--password <password>] [--nickname <nickname>]
[--ipalloc-range <cidr> [--ipalloc-default-subnet <cidr>]]
[--no-discovery] [--init-peer-count <count>] <peer> ...
weave launch-dns [<addr>]
weave launch-proxy [-H <endpoint>] [--with-dns | --without-dns]
[--no-default-ipalloc] [--no-rewrite-hosts]
weave connect [--replace] [<peer> ...]
@@ -55,7 +54,6 @@ weave config
weave version
weave stop
weave stop-router
weave stop-dns
weave stop-proxy
weave reset
weave rmpeer <peer_id>
@@ -78,7 +76,6 @@ exec_remote() {
-e VERSION \
-e WEAVE_DEBUG \
-e WEAVE_DOCKER_ARGS \
-e WEAVEDNS_DOCKER_ARGS \
-e WEAVEPROXY_DOCKER_ARGS \
-e WEAVE_PASSWORD \
-e WEAVE_PORT \
@@ -164,21 +161,16 @@ fi
shift 1
BASE_IMAGE=$DOCKERHUB_USER/weave
BASE_DNS_IMAGE=$DOCKERHUB_USER/weavedns
IMAGE=$BASE_IMAGE:$IMAGE_VERSION
DNS_IMAGE=$BASE_DNS_IMAGE:$IMAGE_VERSION
PROCFS=${PROCFS:-/proc}
DOCKER_BRIDGE=${DOCKER_BRIDGE:-docker0}
CONTAINER_NAME=${WEAVE_CONTAINER_NAME:-weave}
DNS_CONTAINER_NAME=weavedns
BRIDGE=weave
CONTAINER_IFNAME=ethwe
MTU=65535
PORT=${WEAVE_PORT:-6783}
HTTP_PORT=6784
DNS_HTTP_PORT=6785
DNS_HTTP_IFNAME=eth0
PROXY_PORT=12375
PROXY_CONTAINER_NAME=weaveproxy
COVERAGE_ARGS=""
@@ -598,10 +590,6 @@ call_weave() {
http_call $CONTAINER_NAME $HTTP_PORT "$@"
}
call_dns() {
http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT "$@"
}
death_msg() {
echo "The $1 container has died. Consult the logs with 'docker logs $1' for further details."
}
@@ -672,7 +660,7 @@ peer_args() {
# Memoized function to query the weaveDNS server for our dns domain.
dns_domain() {
DNS_DOMAIN=${DNS_DOMAIN:-$(call_dns GET /domain 2>/dev/null || true)}
DNS_DOMAIN=${DNS_DOMAIN:-$(call_weave GET /domain 2>/dev/null || true)}
DNS_DOMAIN=${DNS_DOMAIN:-weave.local.}
echo $DNS_DOMAIN
}
@@ -719,9 +707,6 @@ dns_args() {
shift
done
[ -n "$WITHOUT_DNS" ] && return 0
if [ -z "$WITH_DNS" ] ; then
check_running $DNS_CONTAINER_NAME 2>/dev/null || return 0
fi
DNS_ARGS="--dns $DOCKER_BRIDGE_IP"
if [ -n "$NAME_ARG" -a -z "$HOSTNAME_SPECIFIED" ] ; then
DOMAIN=$(dns_domain)
@@ -754,9 +739,9 @@ check_running() {
fi
}
# Execute $@ only if the weaveDNS container is running
when_dns_running() {
if check_running $DNS_CONTAINER_NAME 2>/dev/null; then
# Execute $@ only if the weave container is running
when_weave_running() {
if check_running $CONTAINER_NAME 2>/dev/null; then
"$@"
fi
}
@@ -788,7 +773,7 @@ put_dns_fqdn() {
shift 2
for ADDR in "$@" ; do
call_dns PUT /name/$CONTAINER_ID/${ADDR%/*} --data-urlencode fqdn=$FQDN $CHECK_ALIVE || true
call_weave PUT /name/$CONTAINER_ID/${ADDR%/*} --data-urlencode fqdn=$FQDN $CHECK_ALIVE || true
done
}
@@ -798,7 +783,7 @@ delete_dns() {
shift 1
for ADDR in "$@" ; do
call_dns DELETE /name/$CONTAINER_ID/${ADDR%/*} || true
call_weave DELETE /name/$CONTAINER_ID/${ADDR%/*} || true
done
}
@@ -809,13 +794,13 @@ delete_dns_fqdn() {
shift 2
for ADDR in "$@" ; do
call_dns DELETE /name/$CONTAINER_ID/${ADDR%/*}?fqdn=$FQDN || true
call_weave DELETE /name/$CONTAINER_ID/${ADDR%/*}?fqdn=$FQDN || true
done
}
# Tell the newly-started weaveDNS about existing weave IPs
populate_dns() {
if ! wait_for_status $DNS_CONTAINER_NAME $DNS_HTTP_PORT ; then
if ! wait_for_status $CONTAINER_NAME $HTTP_PORT ; then
echo "If running, it will not be pre-populated." >&2
return 0
fi
@@ -981,6 +966,7 @@ common_launch_args() {
launch_router() {
create_bridge
docker_bridge_ip
# We set the router name to the bridge mac since that is
# stable across re-creations of the containers.
#
@@ -1050,6 +1036,7 @@ launch_router() {
# when launching the weave container.
CONTAINER=$(docker run --privileged -d --name=$CONTAINER_NAME \
-p $PORT:$CONTAINER_PORT/tcp -p $PORT:$CONTAINER_PORT/udp \
-p $DOCKER_BRIDGE_IP:53:53/udp -p $DOCKER_BRIDGE_IP:53:53/tcp \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WEAVE_PASSWORD \
-e WEAVE_CIDR=none \
@@ -1060,24 +1047,6 @@ launch_router() {
# Tell the newly-started weave IP allocator about existing weave IPs
with_container_addresses ipam_claim weave:expose $(docker ps -q --no-trunc)
fi
}
launch_dns() {
collect_cidr_args "$@"
[ $CIDR_ARG_COUNT -lt 2 ] || usage
shift $CIDR_ARG_COUNT
create_bridge
docker_bridge_ip
# Set WEAVEDNS_DOCKER_ARGS in the environment in order to supply
# additional parameters, such as resource limits, to docker
# when launching the weave container.
DNS_CONTAINER=$(docker run --privileged -d --name=$DNS_CONTAINER_NAME \
-p $DOCKER_BRIDGE_IP:53:53/udp \
-e WEAVE_CIDR=none \
-v /var/run/docker.sock:/var/run/docker.sock \
$WEAVEDNS_DOCKER_ARGS $DNS_IMAGE --iface $CONTAINER_IFNAME --http-iface "$DNS_HTTP_IFNAME" "$@")
ipam_cidrs_or_die $DNS_CONTAINER $CIDR_ARGS
with_container_netns_or_die $DNS_CONTAINER attach $ALL_CIDRS
populate_dns
}
@@ -1200,7 +1169,7 @@ shift 1
case "$COMMAND" in
setup)
for img in $IMAGE $DNS_IMAGE $EXEC_IMAGE ; do
for img in $IMAGE $EXEC_IMAGE ; do
docker pull $img
done
;;
@@ -1212,11 +1181,9 @@ case "$COMMAND" in
launch)
deprecation_warnings "$@"
check_not_running $CONTAINER_NAME $BASE_IMAGE
check_not_running $DNS_CONTAINER_NAME $BASE_DNS_IMAGE
check_not_running $PROXY_CONTAINER_NAME $EXEC_IMAGE
COMMON_ARGS=$(common_launch_args "$@")
launch_router "$@"
launch_dns $COMMON_ARGS
launch_proxy $COMMON_ARGS
;;
launch-router)
@@ -1225,11 +1192,6 @@ case "$COMMAND" in
launch_router "$@"
echo $CONTAINER
;;
launch-dns)
check_not_running $DNS_CONTAINER_NAME $BASE_DNS_IMAGE
launch_dns "$@"
echo $DNS_CONTAINER
;;
launch-proxy)
deprecation_warnings "$@"
check_not_running $PROXY_CONTAINER_NAME $EXEC_IMAGE
@@ -1260,7 +1222,6 @@ case "$COMMAND" in
res=0
call_weave GET /status || res=1
echo
call_dns GET /status 2>/dev/null || true
if check_running $PROXY_CONTAINER_NAME 2>/dev/null ; then
echo
echo "weave proxy is running"
@@ -1275,7 +1236,6 @@ case "$COMMAND" in
[ $# -eq 0 ] || usage
echo weave script $SCRIPT_VERSION
ask_version $CONTAINER_NAME $IMAGE || true
ask_version $DNS_CONTAINER_NAME $DNS_IMAGE || true
if ! EXEC_IMAGE_ID=$(docker inspect --format='{{.Id}}' $EXEC_IMAGE 2>/dev/null) ; then
echo "Unable to find $EXEC_IMAGE image." >&2
else
@@ -1296,7 +1256,7 @@ case "$COMMAND" in
create_bridge
ipam_cidrs_or_die $CONTAINER $CIDR_ARGS
with_container_netns_or_die $CONTAINER attach $ALL_CIDRS
when_dns_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS
when_weave_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS
echo $CONTAINER
;;
dns-args)
@@ -1316,7 +1276,7 @@ case "$COMMAND" in
create_bridge
ipam_cidrs_or_die $CONTAINER $CIDR_ARGS
with_container_netns_or_die $CONTAINER attach $ALL_CIDRS
when_dns_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS
when_weave_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS
echo $RES
;;
attach)
@@ -1331,7 +1291,7 @@ case "$COMMAND" in
create_bridge
ipam_cidrs$ATTACH_TYPE $CONTAINER $CIDR_ARGS
with_container_netns$ATTACH_TYPE $CONTAINER attach $ALL_CIDRS >/dev/null
when_dns_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS
when_weave_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS
show_addrs $ALL_CIDRS
;;
detach)
@@ -1341,7 +1301,7 @@ case "$COMMAND" in
CONTAINER=$(container_id $1)
ipam_cidrs $CONTAINER $CIDR_ARGS
with_container_netns $CONTAINER detach $ALL_CIDRS >/dev/null
when_dns_running with_container_fqdn $CONTAINER delete_dns_fqdn $ALL_CIDRS
when_weave_running with_container_fqdn $CONTAINER delete_dns_fqdn $ALL_CIDRS
for CIDR in $IPAM_CIDRS ; do
call_weave DELETE /ip/$CONTAINER/${CIDR%/*}
done
@@ -1351,7 +1311,7 @@ case "$COMMAND" in
collect_ip_args "$@"
shift $IP_COUNT
[ $IP_COUNT -gt 0 -a \( $# -eq 1 -o \( $# -eq 3 -a "$2" = "-h" \) \) ] || usage
check_running $DNS_CONTAINER_NAME
check_running $CONTAINER_NAME
CONTAINER=$(container_id $1)
if [ $# -eq 1 ] ; then
with_container_fqdn $CONTAINER put_dns_fqdn $IP_ARGS
@@ -1363,7 +1323,7 @@ case "$COMMAND" in
collect_ip_args "$@"
shift $IP_COUNT
[ $IP_COUNT -gt 0 -a \( $# -eq 1 -o \( $# -eq 3 -a "$2" = "-h" \) \) ] || usage
check_running $DNS_CONTAINER_NAME
check_running $CONTAINER_NAME
CONTAINER=$(container_id $1)
if [ $# -eq 1 ] ; then
delete_dns $CONTAINER $IP_ARGS
@@ -1389,7 +1349,7 @@ case "$COMMAND" in
add_iptables_rule nat WEAVE -d $CIDR ! -s $CIDR -j MASQUERADE
add_iptables_rule nat WEAVE -s $CIDR ! -d $CIDR -j MASQUERADE
if [ -n "$FQDN" ] ; then
when_dns_running put_dns_fqdn --no-check-alive weave:expose $FQDN $CIDR
when_weave_running put_dns_fqdn --no-check-alive weave:expose $FQDN $CIDR
fi
fi
done
@@ -1405,7 +1365,7 @@ case "$COMMAND" in
ip addr del dev $BRIDGE $CIDR
delete_iptables_rule nat WEAVE -d $CIDR ! -s $CIDR -j MASQUERADE
delete_iptables_rule nat WEAVE -s $CIDR ! -d $CIDR -j MASQUERADE
when_dns_running delete_dns weave:expose $CIDR
when_weave_running delete_dns weave:expose $CIDR
fi
done
for CIDR in $IPAM_CIDRS ; do
@@ -1416,7 +1376,6 @@ case "$COMMAND" in
stop)
[ $# -eq 0 ] || usage
stop $PROXY_CONTAINER_NAME "Proxy"
stop $DNS_CONTAINER_NAME "WeaveDNS"
stop $CONTAINER_NAME "Weave"
conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true
;;
@@ -1425,10 +1384,6 @@ case "$COMMAND" in
stop $CONTAINER_NAME "Weave"
conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true
;;
stop-dns)
[ $# -eq 0 ] || usage
stop $DNS_CONTAINER_NAME "WeaveDNS"
;;
stop-proxy)
[ $# -eq 0 ] || usage
stop $PROXY_CONTAINER_NAME "Proxy"
@@ -1436,7 +1391,7 @@ case "$COMMAND" in
reset)
[ $# -eq 0 ] || usage
call_weave DELETE /peer >/dev/null 2>&1 || true
for NAME in $CONTAINER_NAME $DNS_CONTAINER_NAME $PROXY_CONTAINER_NAME ; do
for NAME in $CONTAINER_NAME $PROXY_CONTAINER_NAME ; do
docker stop $NAME >/dev/null 2>&1 || true
docker rm -f $NAME >/dev/null 2>&1 || true
done
@@ -1451,6 +1406,14 @@ case "$COMMAND" in
PEER=$1
call_weave DELETE /peer/$PEER
;;
launch-dns)
echo "The 'launch-dns' command has been removed; DNS is launched as part of 'launch' and 'launch-router'." >&2
exit 0
;;
stop-dns)
echo "The 'stop-dns command has been removed; DNS is stopped as part of 'stop' and 'stop-router'." >&2
exit 0
;;
*)
echo "Unknown weave command '$COMMAND'" >&2
usage