From 8a509168ee7fa95652fee7a3d22907fef8747951 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 14:08:43 +0200 Subject: [PATCH] New zone database --- nameserver/addrs.go | 2 +- nameserver/http_test.go | 6 +- nameserver/server.go | 1 - nameserver/server_test.go | 21 +- nameserver/zone.go | 694 ++++++++++++++++++++++++++++++++------ nameserver/zone_lookup.go | 55 +++ nameserver/zone_test.go | 130 +++++-- weavedns/main.go | 30 +- 8 files changed, 791 insertions(+), 148 deletions(-) create mode 100644 nameserver/zone_lookup.go diff --git a/nameserver/addrs.go b/nameserver/addrs.go index 0bdb0dca..1e315a91 100644 --- a/nameserver/addrs.go +++ b/nameserver/addrs.go @@ -25,7 +25,7 @@ type ZoneLookup interface { LookupInaddr(inaddr string) ([]ZoneRecord, error) } -type ZoneObserver interface { +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 diff --git a/nameserver/http_test.go b/nameserver/http_test.go index 5979d683..8b2b778a 100644 --- a/nameserver/http_test.go +++ b/nameserver/http_test.go @@ -27,7 +27,11 @@ func TestHttp(t *testing.T) { dockerIP = "9.8.7.6" ) - zone := NewZoneDb(DefaultLocalDomain) + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() port := rand.Intn(10000) + 32768 fmt.Println("Http test on port", port) diff --git a/nameserver/server.go b/nameserver/server.go index 3077c8f4..0d3ddc94 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -10,7 +10,6 @@ import ( ) const ( - DefaultLocalDomain = "weave.local." // The default name used for the local domain DefaultServerPort = 53 // The default server port DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load DefaultUDPBuflen = 4096 // bigger than the default 512 diff --git a/nameserver/server_test.go b/nameserver/server_test.go index 7e893c5b..2ae5203f 100644 --- a/nameserver/server_test.go +++ b/nameserver/server_test.go @@ -44,8 +44,15 @@ func TestUDPDNSServer(t *testing.T) { ) testCIDR1 := testAddr1 + "/24" - InitDefaultLogging(true) - var zone = NewZoneDb(DefaultLocalDomain) + InitDefaultLogging(testing.Verbose()) + Info.Println("TestUDPDNSServer starting") + + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + ip, _, _ := net.ParseCIDR(testCIDR1) zone.AddRecord(containerID, successTestName, ip) @@ -125,9 +132,15 @@ func TestTCPDNSServer(t *testing.T) { ) dnsAddr := fmt.Sprintf("localhost:%d", testPort) - InitDefaultLogging(true) - var zone = NewZoneDb(DefaultLocalDomain) + InitDefaultLogging(testing.Verbose()) + Info.Println("TestTCPDNSServer starting") + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + // generate a list of `numAnswers` IP addresses var addrs []ZoneRecord bs := make([]byte, 4) diff --git a/nameserver/zone.go b/nameserver/zone.go index ca23dab1..b35f2a46 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -3,41 +3,27 @@ package nameserver import ( "bytes" "fmt" + "github.com/benbjohnson/clock" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" "net" "sync" + "time" ) const ( RDNSDomain = "in-addr.arpa." ) +const ( + DefaultLocalDomain = "weave.local." // The default name used for the local domain + + defaultRemoteIdent = "weave:remote" // Ident used for info obtained from mDNS +) + // +1 to also exclude a dot var rdnsDomainLen = len(RDNSDomain) + 1 -type Zone interface { - AddRecord(ident string, name string, ip net.IP) error - DeleteRecord(ident string, ip net.IP) error - DeleteRecordsFor(ident string) error - Domain() string - ZoneLookup -} - -type dbRecord struct { - Ident string - Name string - IP net.IP -} - -// Very simple data structure for now, with linear searching. -// TODO: make more sophisticated to improve performance. -type ZoneDb struct { - mx sync.RWMutex - recs []dbRecord - domain string -} - type LookupError string func (ops LookupError) Error() string { @@ -51,103 +37,609 @@ func (dup DuplicateError) Error() string { return "Tried to add a duplicate entry" } -func NewZoneDb(domain string) *ZoneDb { - return &ZoneDb{ - domain: domain, +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 a record in the local database + DeleteRecord(ident string, ip net.IP) error + // Delete all records for an ident in the local database + DeleteRecordsFor(ident string) error + // Return a status string + Status() string +} + +/////////////////////////////////////////////////////////////////// +// +// Zone database: +// +// 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 (zone *ZoneDb) Domain() string { - return zone.domain +func (i *recordEntry) addIPObserver(zro ZoneRecordObserver) { + i.observers = append(i.observers, zro) } -func (zone *ZoneDb) indexOf(match func(dbRecord) bool) int { - for i, r := range zone.recs { - if match(r) { - return i +// 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 { + Debug.Printf("[zonedb] Notifying %d observers of '%s'", numObservers, re.ip) + for _, observer := range re.observers { + observer() } + re.observers = make([]ZoneRecordObserver, 0) } - return -1 } -func (zone *ZoneDb) String() string { - zone.mx.RLock() - defer zone.mx.RUnlock() +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(localTTL) + } + 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 - for _, r := range zone.recs { - fmt.Fprintf(&buf, "%.12s %s %v\n", r.Ident, r.IP, r.Name) + fmt.Fprintf(&buf, "%s", re.Record) + lobs := len(re.observers) + if lobs > 0 { + fmt.Fprintf(&buf, "/OBS:%d", lobs) } return buf.String() } -func (zone *ZoneDb) LookupName(name string) ([]ZoneRecord, error) { +/////////////////////////////////////////////////////////////////// + +// 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 + 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 an IP for this name +func (n *name) deleteIP(ip IPv4) bool { + if ipRecord, found := n.ipv4[ip]; found { + ipRecord.notifyIPObservers() + n.notifyNameObservers() + delete(n.ipv4, ip) + return true + } + return false +} + +// 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.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 { + Debug.Printf("[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 namesSet struct { + names map[string]*name +} + +func newNamesSet() *namesSet { return &namesSet{names: make(map[string]*name)} } +func (ns namesSet) len() int { return len(ns.names) } +func (ns namesSet) empty() bool { return ns.len() == 0 } +func (ns namesSet) hasName(name string) bool { + if n, found := ns.names[name]; !found { + return false + } else { + // 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 *namesSet) getName(name string, create bool) (n *name) { + if n, found := ns.names[name]; !found && create { + newName := newName(name) + ns.names[name] = newName + return newName + } else { + return n + } +} + +// Get all the entries for a name +func (ns *namesSet) 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 *namesSet) 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 *namesSet) 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 a name in the names set +func (ns *namesSet) deleteName(n string) error { + if name, found := ns.names[n]; found { + for _, ipRecord := range name.getAllEntries() { + ipRecord.notifyIPObservers() + } + name.notifyNameObservers() + delete(ns.names, n) + return nil + } else { + return LookupError(fmt.Sprintf("%+v", name)) + } +} + +// Delete an IPv4 from all names +func (ns *namesSet) deleteIP(ip IPv4) error { + cnt := 0 + for nkey, n := range ns.names { + if n.deleteIP(ip) { + cnt++ + } + if n.empty() { + delete(ns.names, nkey) + } + } + + // we must return an error if no records have been found and deleted + if cnt == 0 { + return LookupError(fmt.Sprintf("%+v", ip)) + } + return nil +} + +// Get the name query time +func (ns namesSet) 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 *namesSet) touchName(name string, now time.Time) { + Debug.Printf("[zonedb] Touching name %s", name) + n := ns.getName(name, false) + if n != nil { + n.lastAccessTime = now + } +} + +func (ns namesSet) 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() +} + +/////////////////////////////////////////////////////////////////// + +// Names sets, by ident name +type identRecordsSet map[string]*namesSet + +func (irs identRecordsSet) String() string { + var buf bytes.Buffer + for ident, nameset := range irs { + fmt.Fprintf(&buf, "%.12s: %s\n", ident, nameset) + } + return buf.String() +} + +/////////////////////////////////////////////////////////////////// + +type ZoneConfig struct { + // The local domain + Domain string + // The interface where mDNS operates + Iface *net.Interface + // 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 identRecordsSet + + mdnsCli ZoneMDNSClient + mdnsSrv ZoneMDNSServer + iface *net.Interface + domain string // the local domain + clock clock.Clock +} + +// Create a new zone database +func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { + zone = &zoneDb{ + domain: config.Domain, + idents: make(identRecordsSet), + mdnsCli: config.MDNSClient, + mdnsSrv: config.MDNSServer, + iface: config.Iface, + clock: config.Clock, + } + + // fix the default configuration parameters + if zone.clock == nil { + zone.clock = clock.New() + } + if len(zone.domain) == 0 { + zone.domain = DefaultLocalDomain + } + + // Create the mDNS client and server + if zone.mdnsCli == nil { + if zone.mdnsCli, err = NewMDNSClient(); err != nil { + return + } + } + if zone.mdnsSrv == nil { + if zone.mdnsSrv, err = NewMDNSServer(zone); err != nil { + return + } + } + + return +} + +// Start the zone database +func (zone *zoneDb) Start() (err error) { + if zone.iface != nil { + Info.Printf("[zonedb] Using mDNS on %s", zone.iface) + } else { + Info.Printf("[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 + } + return +} + +// Perform a graceful shutdown of the zone database +func (zone *zoneDb) Stop() error { + Debug.Printf("[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() + Debug.Printf("[zonedb] Adding record: '%s'/'%s'[%s]", ident, name, ip) + record := Record{dns.Fqdn(name), ip, 0, 0, 0} + _, err = zone.getNamesSet(ident).addIPToName(record, zone.clock.Now()) + return +} + +// Delete all records for an ident +func (zone *zoneDb) DeleteRecordsFor(ident string) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + Debug.Printf("[zonedb] Deleting records for ident '%s'", ident) + zone.deleteIdent(ident) + return +} + +// Delete an IP in a ident +func (zone *zoneDb) DeleteRecord(ident string, ip net.IP) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + + Debug.Printf("[zonedb] Deleting record '%s'/[%s]", ident, ip) + if _, found := zone.idents[ident]; found { + err = zone.idents[ident].deleteIP(ipToIPv4(ip)) + if zone.idents[ident].empty() { + zone.deleteIdent(ident) + } + } else { + err = LookupError(ident) + } + return +} + +// 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 += 1 + } + } + 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 += 1 + } + } + 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() - for _, r := range zone.recs { - if r.Name == name { - return []ZoneRecord{Record{r.Name, r.IP, 0, 0, 0}}, nil - } - } - return nil, LookupError(name) + return zone.idents.String() } -func (zone *ZoneDb) LookupInaddr(inaddr string) ([]ZoneRecord, error) { - if revIP := net.ParseIP(inaddr[:len(inaddr)-rdnsDomainLen]); revIP != nil { - revIP4 := revIP.To4() - ip := []byte{revIP4[3], revIP4[2], revIP4[1], revIP4[0]} - Debug.Printf("[zonedb] Looking for address: %+v", ip) - zone.mx.RLock() - defer zone.mx.RUnlock() - for _, r := range zone.recs { - if r.IP.Equal(ip) { - return []ZoneRecord{Record{r.Name, r.IP, 0, 0, 0}}, nil - } - } - return nil, LookupError(inaddr) - } - Warning.Printf("[zonedb] Asked to reverse lookup %s", inaddr) - return nil, LookupError(inaddr) -} - -func (zone *ZoneDb) AddRecord(ident string, name string, ip net.IP) error { - zone.mx.Lock() - defer zone.mx.Unlock() - fqdn := dns.Fqdn(name) - pred := func(r dbRecord) bool { return r.Name == fqdn && r.IP.Equal(ip) && r.Ident == ident } - if index := zone.indexOf(pred); index != -1 { - return DuplicateError{} - } - zone.recs = append(zone.recs, dbRecord{ident, fqdn, ip}) - return nil -} - -func (zone *ZoneDb) DeleteRecord(ident string, ip net.IP) error { - zone.mx.Lock() - defer zone.mx.Unlock() - pred := func(r dbRecord) bool { return r.Ident == ident && r.IP.Equal(ip) } - if index := zone.indexOf(pred); index != -1 { - zone.recs = append(zone.recs[:index], zone.recs[index+1:]...) - return nil - } - return LookupError(ident) -} - -func (zone *ZoneDb) DeleteRecordsFor(ident string) error { - zone.mx.Lock() - defer zone.mx.Unlock() - w := 0 // write index - - for _, r := range zone.recs { - if r.Ident != ident { - zone.recs[w] = r - w++ - } - } - zone.recs = zone.recs[:w] - return nil -} - -func (zone *ZoneDb) ContainerDied(ident string) error { +// Notify that a container has died +func (zone *zoneDb) ContainerDied(ident string) error { Info.Printf("[zonedb] Container %s down. Removing records", ident) return zone.DeleteRecordsFor(ident) } + +// 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() + + remoteNamesSet := zone.getNamesSet(defaultRemoteIdent) + if remoteNamesSet != nil { + return remoteNamesSet.hasName(n) + } + return false +} + +// Get or create a names set +func (zone *zoneDb) getNamesSet(ident string) *namesSet { + ns, found := zone.idents[ident] + if !found { + ns = newNamesSet() + zone.idents[ident] = ns + } + return ns +} + +// Delete a ident +func (zone *zoneDb) deleteIdent(ident string) { + Info.Printf("[zonedb] Removing everything for ident '%s'", ident) + if ns, found := zone.idents[ident]; found { + for _, n := range ns.names { + for _, entry := range n.getAllEntries() { + entry.notifyIPObservers() + } + n.notifyNameObservers() + } + delete(zone.idents, ident) + } +} diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go new file mode 100644 index 00000000..985f84ae --- /dev/null +++ b/nameserver/zone_lookup.go @@ -0,0 +1,55 @@ +package nameserver + +import ( + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" +) + +// 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) + Debug.Printf("[zonedb] Looking for name '%s' in local database", name) + for identName, nameset := range zone.idents { + if identName != defaultRemoteIdent { + for _, ze := range nameset.getEntriesForName(name) { + res = append(res, ze) + } + } + } + + if len(res) == 0 { + err = LookupError(name) + } + return +} + +// 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) + } + Debug.Printf("[zonedb] Looking for address in local database: '%s' (%s)", revIPv4, inaddr) + for identName, nameset := range zone.idents { + if identName != defaultRemoteIdent { + for _, ze := range nameset.getEntriesForIP(revIPv4) { + res = append(res, ZoneRecord(ze)) + } + } + } + if len(res) == 0 { + err = LookupError(inaddr) + } + return +} diff --git a/nameserver/zone_test.go b/nameserver/zone_test.go index a807464f..968b4b7d 100644 --- a/nameserver/zone_test.go +++ b/nameserver/zone_test.go @@ -1,6 +1,8 @@ package nameserver import ( + "fmt" + . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" "net" "testing" @@ -8,60 +10,118 @@ import ( func TestZone(t *testing.T) { var ( - containerID = "deadbeef" - otherContainerID = "cowjuice" - successTestName = "test1.weave." - testAddr1 = "10.2.2.1/24" + 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." ) - var zone = NewZoneDb(DefaultLocalDomain) + InitDefaultLogging(testing.Verbose()) - ip, _, _ := net.ParseCIDR(testAddr1) - err := zone.AddRecord(containerID, successTestName, ip) + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(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) wt.AssertNoErr(t, err) // Add a few more records to make the job harder - err = zone.AddRecord("abcdef0123", "adummy.weave.", net.ParseIP("10.2.0.1")) + t.Logf("Adding 'adummy.weave.' to 'abcdef0123'") + err = zone.AddRecord("abcdef0123", "adummy.weave.", net.ParseIP("10.9.0.1")) wt.AssertNoErr(t, err) - err = zone.AddRecord("0123abcdef", "zdummy.weave.", net.ParseIP("10.2.0.2")) + t.Logf("Adding 'zdummy.weave.' to '0123abcdef'") + err = zone.AddRecord("0123abcdef", "zdummy.weave.", net.ParseIP("10.9.0.2")) + wt.AssertNoErr(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) wt.AssertNoErr(t, err) - // Check that the address is now there. - foundIP, err := zone.LookupName(successTestName) - wt.AssertNoErr(t, err) - - if !foundIP[0].IP().Equal(ip) { - t.Fatal("Unexpected result for", successTestName, foundIP) + if !foundIPs[0].IP().Equal(ip1) { + t.Fatal("Unexpected result for", name1, foundIPs) } - // See if we can find the address by IP. - foundName, err := zone.LookupInaddr("1.2.2.10.in-addr.arpa.") + t.Logf("Checking if we cannot find some silly name like 'something.wrong'") + foundIPs, err = zone.LookupName("something.wrong") + wt.AssertErrorType(t, err, (*LookupError)(nil), 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) wt.AssertNoErr(t, err) - if foundName[0].Name() != successTestName { - t.Fatal("Unexpected result for", ip, foundName) + t.Logf("Checking if we can find both the old IP and the new IP for '%s'", name1) + foundIPs, err = zone.LookupName(name1) + wt.AssertNoErr(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) } - err = zone.AddRecord(containerID, successTestName, ip) + 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.") + wt.AssertNoErr(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, err, (*LookupError)(nil), 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, err, (*DuplicateError)(nil), "duplicate add") - err = zone.AddRecord(otherContainerID, successTestName, ip) - // Delete the record for the original container - err = zone.DeleteRecord(containerID, ip) + t.Logf("Adding '%s'/%s in %s too", name1, ip1, container2) + err = zone.AddRecord(container2, name1, ip1) wt.AssertNoErr(t, err) - _, err = zone.LookupName(successTestName) + name1Removed := 0 + err = zone.ObserveName(name1, func() { t.Logf("Observer #1 for '%s' notified.", name1); name1Removed++ }) + wt.AssertNoErr(t, err) + err = zone.ObserveName(name1, func() { t.Logf("Observer #2 for '%s' notified.", name1); name1Removed++ }) + wt.AssertNoErr(t, err) + err = zone.ObserveInaddr(revName1Addr1, func() { t.Logf("Observer #1 for '%s' notified.", revName1Addr1); name1Removed++ }) wt.AssertNoErr(t, err) - err = zone.DeleteRecord(otherContainerID, ip) + t.Logf("Zone database:\n%s", zone) + t.Logf("Deleting the %s in %s", ip1, container1) + err = zone.DeleteRecord(container1, ip1) + wt.AssertNoErr(t, err) + 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) wt.AssertNoErr(t, err) - // Check that the address is not there now. - _, err = zone.LookupName(successTestName) + t.Logf("Checking %s is not found after removing %s it in %s and %s in %s", + name1, ip1, container2, ip2, container1) + err = zone.DeleteRecord(container1, ip2) + err = zone.DeleteRecord(container2, ip1) + wt.AssertNoErr(t, err) + t.Logf("Zone database:\n%s", zone) + _, err = zone.LookupName(name1) wt.AssertErrorType(t, err, (*LookupError)(nil), "after deleting record") - // Delete a record that isn't there - err = zone.DeleteRecord(containerID, net.ParseIP("0.0.0.0")) + t.Logf("Checking if removing an unknown record results in an error") + err = zone.DeleteRecord(container1, net.ParseIP("0.0.0.0")) wt.AssertErrorType(t, err, (*LookupError)(nil), "when deleting record that doesn't exist") } @@ -72,14 +132,22 @@ func TestDeleteFor(t *testing.T) { addr1 = "10.2.2.3/24" addr2 = "10.2.7.8/24" ) - zone := NewZoneDb(DefaultLocalDomain) + + InitDefaultLogging(testing.Verbose()) + + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + for _, addr := range []string{addr1, addr2} { ip, _, _ := net.ParseCIDR(addr) err := zone.AddRecord(id, name, ip) wt.AssertNoErr(t, err) } - _, err := zone.LookupName(name) + _, err = zone.LookupName(name) wt.AssertNoErr(t, err) err = zone.DeleteRecordsFor(id) diff --git a/weavedns/main.go b/weavedns/main.go index 6a888896..a944f7c3 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -52,15 +52,6 @@ func main() { InitDefaultLogging(debug) Info.Printf("WeaveDNS version %s\n", version) // first thing in log: the version - var zone = weavedns.NewZoneDb(domain) - - if watch { - err := updater.Start(apiPath, zone) - if err != nil { - Error.Fatal("Unable to start watcher", err) - } - } - var iface *net.Interface if ifaceName != "" { var err error @@ -73,6 +64,27 @@ func main() { } } + zoneConfig := weavedns.ZoneConfig{ + Domain: domain, + Iface: iface, + } + zone, err := weavedns.NewZoneDb(zoneConfig) + if err != nil { + Error.Fatal("[main] Unable to initialize the Zone database", err) + } + err = zone.Start() + if err != nil { + Error.Fatal("[main] Unable to start the Zone database", err) + } + defer zone.Stop() + + if watch { + err := updater.Start(apiPath, zone) + if err != nil { + Error.Fatal("[main] Unable to start watcher", err) + } + } + srvConfig := weavedns.DNSServerConfig{ Port: dnsPort, CacheLen: cacheLen,