diff --git a/Makefile b/Makefile index 0784ee8e..3f93170b 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/common/sched_queue.go b/common/sched_queue.go deleted file mode 100644 index cbfc9eb3..00000000 --- a/common/sched_queue.go +++ /dev/null @@ -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 -} diff --git a/common/sched_queue_test.go b/common/sched_queue_test.go deleted file mode 100644 index d9eb6830..00000000 --- a/common/sched_queue_test.go +++ /dev/null @@ -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") -} diff --git a/nameserver/addrs.go b/nameserver/addrs.go deleted file mode 100644 index fb3deb66..00000000 --- a/nameserver/addrs.go +++ /dev/null @@ -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 -} diff --git a/nameserver/addrs_test.go b/nameserver/addrs_test.go deleted file mode 100644 index 00d1ba7d..00000000 --- a/nameserver/addrs_test.go +++ /dev/null @@ -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") -} diff --git a/nameserver/cache.go b/nameserver/cache.go deleted file mode 100644 index fd0be604..00000000 --- a/nameserver/cache.go +++ /dev/null @@ -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() -} diff --git a/nameserver/cache_test.go b/nameserver/cache_test.go deleted file mode 100644 index d6e29c25..00000000 --- a/nameserver/cache_test.go +++ /dev/null @@ -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") -} diff --git a/nameserver/dns.go b/nameserver/dns.go index c232d45b..c2868aa2 100644 --- a/nameserver/dns.go +++ b/nameserver/dns.go @@ -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 } diff --git a/nameserver/dns_test.go b/nameserver/dns_test.go index 3e001e6e..b44ce94b 100644 --- a/nameserver/dns_test.go +++ b/nameserver/dns_test.go @@ -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) + } } diff --git a/nameserver/entry.go b/nameserver/entry.go new file mode 100644 index 00000000..0ab22008 --- /dev/null +++ b/nameserver/entry.go @@ -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()} +} diff --git a/nameserver/entry_test.go b/nameserver/entry_test.go new file mode 100644 index 00000000..5e8efa03 --- /dev/null +++ b/nameserver/entry_test.go @@ -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) +} diff --git a/nameserver/http.go b/nameserver/http.go index 02e1d380..d85b9970 100644 --- a/nameserver/http.go +++ b/nameserver/http.go @@ -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) } diff --git a/nameserver/http_test.go b/nameserver/http_test.go deleted file mode 100644 index 18e26b1b..00000000 --- a/nameserver/http_test.go +++ /dev/null @@ -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 -} diff --git a/nameserver/mdns.go b/nameserver/mdns.go deleted file mode 100644 index e476fb4f..00000000 --- a/nameserver/mdns.go +++ /dev/null @@ -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) -} diff --git a/nameserver/mdns_client.go b/nameserver/mdns_client.go deleted file mode 100644 index 0be13fbd..00000000 --- a/nameserver/mdns_client.go +++ /dev/null @@ -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) - } - } -} diff --git a/nameserver/mdns_client_test.go b/nameserver/mdns_client_test.go deleted file mode 100644 index edeb1bf1..00000000 --- a/nameserver/mdns_client_test.go +++ /dev/null @@ -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) - } -} diff --git a/nameserver/mdns_lookup.go b/nameserver/mdns_lookup.go deleted file mode 100644 index db051bb4..00000000 --- a/nameserver/mdns_lookup.go +++ /dev/null @@ -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) -} diff --git a/nameserver/mdns_server.go b/nameserver/mdns_server.go deleted file mode 100644 index df51f6f8..00000000 --- a/nameserver/mdns_server.go +++ /dev/null @@ -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 -} diff --git a/nameserver/mdns_server_test.go b/nameserver/mdns_server_test.go deleted file mode 100644 index 5022af21..00000000 --- a/nameserver/mdns_server_test.go +++ /dev/null @@ -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) - } -} diff --git a/nameserver/mdns_test.go b/nameserver/mdns_test.go deleted file mode 100644 index 6acf1ac8..00000000 --- a/nameserver/mdns_test.go +++ /dev/null @@ -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) - } -} diff --git a/nameserver/mocks_test.go b/nameserver/mocks_test.go deleted file mode 100644 index 58bb5e04..00000000 --- a/nameserver/mocks_test.go +++ /dev/null @@ -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)) -} diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go new file mode 100644 index 00000000..43562ae5 --- /dev/null +++ b/nameserver/nameserver.go @@ -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...)...) +} diff --git a/nameserver/nameserver_test.go b/nameserver/nameserver_test.go new file mode 100644 index 00000000..f1b091fe --- /dev/null +++ b/nameserver/nameserver_test.go @@ -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) +} diff --git a/nameserver/server.go b/nameserver/server.go deleted file mode 100644 index 6f36aaf4..00000000 --- a/nameserver/server.go +++ /dev/null @@ -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) -} diff --git a/nameserver/server_cache_test.go b/nameserver/server_cache_test.go deleted file mode 100644 index 394710d6..00000000 --- a/nameserver/server_cache_test.go +++ /dev/null @@ -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") - -} diff --git a/nameserver/server_test.go b/nameserver/server_test.go deleted file mode 100644 index 9c4a1976..00000000 --- a/nameserver/server_test.go +++ /dev/null @@ -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)") -} diff --git a/nameserver/zone.go b/nameserver/zone.go deleted file mode 100644 index e6598932..00000000 --- a/nameserver/zone.go +++ /dev/null @@ -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 -} diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go deleted file mode 100644 index 6e515b62..00000000 --- a/nameserver/zone_lookup.go +++ /dev/null @@ -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 -} diff --git a/nameserver/zone_lookup_test.go b/nameserver/zone_lookup_test.go deleted file mode 100644 index 31789133..00000000 --- a/nameserver/zone_lookup_test.go +++ /dev/null @@ -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") -} diff --git a/nameserver/zone_test.go b/nameserver/zone_test.go deleted file mode 100644 index 2a3a1467..00000000 --- a/nameserver/zone_test.go +++ /dev/null @@ -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") -} diff --git a/net/address/address.go b/net/address/address.go index f97d44c8..f3faf138 100644 --- a/net/address/address.go +++ b/net/address/address.go @@ -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 +} diff --git a/prog/weavedns/Dockerfile b/prog/weavedns/Dockerfile deleted file mode 100644 index 883b34b8..00000000 --- a/prog/weavedns/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM scratch -MAINTAINER Weaveworks Inc -WORKDIR /home/weave -ADD ./weavedns /home/weave/ -EXPOSE 53/udp -ENTRYPOINT ["/home/weave/weavedns"] diff --git a/prog/weavedns/main.go b/prog/weavedns/main.go deleted file mode 100644 index c8217656..00000000 --- a/prog/weavedns/main.go +++ /dev/null @@ -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() -} diff --git a/prog/weaver/main.go b/prog/weaver/main.go index 30a17d96..53aa3b20 100644 --- a/prog/weaver/main.go +++ b/prog/weaver/main.go @@ -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) { diff --git a/proxy/create_container_interceptor.go b/proxy/create_container_interceptor.go index 8e3cca04..f4c3ee06 100644 --- a/proxy/create_container_interceptor.go +++ b/proxy/create_container_interceptor.go @@ -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 diff --git a/site/ipam.md b/site/ipam.md index 1c28978c..f3596f2a 100644 --- a/site/ipam.md +++ b/site/ipam.md @@ -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. diff --git a/site/proxy.md b/site/proxy.md index 453c81de..2cd04a2b 100644 --- a/site/proxy.md +++ b/site/proxy.md @@ -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 diff --git a/site/weavedns-design.md b/site/weavedns-design.md index fe70df18..9059505c 100644 --- a/site/weavedns-design.md +++ b/site/weavedns-design.md @@ -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//` 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//` -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/` Remove all records for the host-scoped identifier. -`GET /name/` +`GET /name/` -List all the records in the database. - -`GET /name/` - -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. diff --git a/site/weavedns.md b/site/weavedns.md index f41a7639..1166b00e 100644 --- a/site/weavedns.md +++ b/site/weavedns.md @@ -26,25 +26,15 @@ hosts. ## 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`. ## 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 ... ## 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. ## 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 `. -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. ## 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. -## 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 -``` ## 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. ## Configuring the domain search paths @@ -241,12 +207,12 @@ docker run -ti \ ## 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 ## 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. diff --git a/test/200_dns_test.sh b/test/200_dns_test.sh index 49fa215a..f4d8c998 100755 --- a/test/200_dns_test.sh +++ b/test/200_dns_test.sh @@ -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 diff --git a/test/210_dns_cross_hosts_test.sh b/test/210_dns_cross_hosts_test.sh new file mode 100755 index 00000000..b3836bbc --- /dev/null +++ b/test/210_dns_cross_hosts_test.sh @@ -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 diff --git a/test/210_dns_multicast_test.sh b/test/210_dns_multicast_test.sh deleted file mode 100755 index e8619703..00000000 --- a/test/210_dns_multicast_test.sh +++ /dev/null @@ -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 diff --git a/test/220_dns_custom_test.sh b/test/220_dns_custom_test.sh index ec1b1efe..bb404d32 100755 --- a/test/220_dns_custom_test.sh +++ b/test/220_dns_custom_test.sh @@ -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 diff --git a/test/230_dns_unqualified_test.sh b/test/230_dns_unqualified_test.sh index 38e30043..0283d73f 100755 --- a/test/230_dns_unqualified_test.sh +++ b/test/230_dns_unqualified_test.sh @@ -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 diff --git a/test/240_dns_add_name_test.sh b/test/240_dns_add_name_test.sh index ce76e225..87badd50 100755 --- a/test/240_dns_add_name_test.sh +++ b/test/240_dns_add_name_test.sh @@ -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 diff --git a/test/250_dns_restart_test.sh b/test/250_dns_restart_test.sh new file mode 100755 index 00000000..9c8a0e54 --- /dev/null +++ b/test/250_dns_restart_test.sh @@ -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 diff --git a/test/260_dns_removal_test.sh b/test/260_dns_removal_test.sh index 78fa9bd9..bc951d40 100755 --- a/test/260_dns_removal_test.sh +++ b/test/260_dns_removal_test.sh @@ -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 diff --git a/test/280_with_or_without_dns_test.sh b/test/280_with_or_without_dns_test.sh index 2a58be95..f38e23e9 100755 --- a/test/280_with_or_without_dns_test.sh +++ b/test/280_with_or_without_dns_test.sh @@ -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 diff --git a/test/290_dns_fallback_test.sh b/test/290_dns_fallback_test.sh index 9eee0f8f..1d897732 100755 --- a/test/290_dns_fallback_test.sh +++ b/test/290_dns_fallback_test.sh @@ -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 diff --git a/test/295_dns_large_response.sh b/test/295_dns_large_response.sh new file mode 100755 index 00000000..daf7e82e --- /dev/null +++ b/test/295_dns_large_response.sh @@ -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 diff --git a/test/500_weave_multi_cidr_test.sh b/test/500_weave_multi_cidr_test.sh index 83bc20c8..d3d09661 100755 --- a/test/500_weave_multi_cidr_test.sh +++ b/test/500_weave_multi_cidr_test.sh @@ -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) diff --git a/test/630_proxy_dns_test.sh b/test/630_proxy_dns_test.sh index d3b9ce04..8276be6c 100755 --- a/test/630_proxy_dns_test.sh +++ b/test/630_proxy_dns_test.sh @@ -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 diff --git a/test/635_proxy_dns_unqualified_test.sh b/test/635_proxy_dns_unqualified_test.sh index b4ffbd18..f81cbb3f 100755 --- a/test/635_proxy_dns_unqualified_test.sh +++ b/test/635_proxy_dns_unqualified_test.sh @@ -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 diff --git a/test/640_proxy_restart_reattaches_test.sh b/test/640_proxy_restart_reattaches_test.sh index b85e87c4..1e468def 100755 --- a/test/640_proxy_restart_reattaches_test.sh +++ b/test/640_proxy_restart_reattaches_test.sh @@ -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 diff --git a/test/config.sh b/test/config.sh index 3d136999..ffdfe652 100644 --- a/test/config.sh +++ b/test/config.sh @@ -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 +# assert_dns_a_record [] 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 diff --git a/weave b/weave index c7983f68..1bedfb61 100755 --- a/weave +++ b/weave @@ -34,7 +34,6 @@ weave launch [--password ] [--nickname ] weave launch-router [--password ] [--nickname ] [--ipalloc-range [--ipalloc-default-subnet ]] [--no-discovery] [--init-peer-count ] ... -weave launch-dns [] weave launch-proxy [-H ] [--with-dns | --without-dns] [--no-default-ipalloc] [--no-rewrite-hosts] weave connect [--replace] [ ...] @@ -55,7 +54,6 @@ weave config weave version weave stop weave stop-router -weave stop-dns weave stop-proxy weave reset weave rmpeer @@ -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