Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
48520b1
Typo in the readme
tarnfeld Sep 20, 2014
a100666
[WIP]
tarnfeld Sep 21, 2014
159702b
WIP
tarnfeld Sep 22, 2014
ffca58d
Fixed compiler errors
tarnfeld Sep 23, 2014
43abcd9
Add a boolean argument to prevent resolving CNAMEs
tarnfeld Sep 25, 2014
da29d03
Ensure there is no leading slash from nameToKey
tarnfeld Sep 29, 2014
e5e685e
Fixed type error
tarnfeld Sep 29, 2014
ddc71c8
Added helper methods for domain/rrset validation checks
tarnfeld Sep 29, 2014
1f68dca
Added more useful debug logging
tarnfeld Sep 29, 2014
683576d
Added some basic test cases for panic based locks
tarnfeld Nov 21, 2014
266b5bd
Merge branch 'master' into feature/nsupdate
tarnfeld Nov 24, 2014
6838133
Added MOTD for makefile
tarnfeld Feb 3, 2015
d67d220
Test race conditions too
tarnfeld Feb 3, 2015
a65461d
Text alignment
tarnfeld Feb 3, 2015
1b93f29
Progress with add/delete dynamic updates
tarnfeld Feb 3, 2015
bfbe460
Make sure we send a TSIG response back
tarnfeld Feb 6, 2015
d18eb08
Create new records as keys inside the type directory
tarnfeld Feb 9, 2015
eb36f88
Fix creating directories for records:
Feb 10, 2015
8a395e6
Fix 'given peers are not reachable' test errs:
Feb 9, 2015
0e36d77
Typo
Feb 6, 2015
8bcd572
Correct signature for not-yet-implementeds
Feb 6, 2015
5a8ba10
Use correct TTL etcd keyname:
Feb 9, 2015
1f9ea6b
Implement update record converters: SRV, TXT, PTR
Feb 6, 2015
5b09124
Clean up any stale locks in etcd before tests:
Feb 9, 2015
d0a67d6
Use etcd prefixes for locks:
Feb 10, 2015
9b87d5c
Add (failing) test for multiple records:
Feb 10, 2015
a3dda33
Merge remote-tracking branch 'origin/master' into feature/nsupdate
Feb 19, 2015
b7244d9
Merge branch 'master' into feature/nsupdate
Feb 20, 2015
6d6729b
Fix NameExists to use simpler flow from master
Feb 20, 2015
ac8fd0e
Fix some merge issues
Feb 20, 2015
c037341
Add CLI option for unauthenticated updates:
Feb 18, 2015
6232c76
Use consistent etcd keys for new records:
Feb 18, 2015
0bd0d05
Replace panic-based locking with blocking locks:
Feb 10, 2015
dd80139
Add simple lock test
Feb 18, 2015
5536ccc
Add conflicting lock test
Feb 18, 2015
3a396b6
Merge pull request #37 from duedil-ltd/nsupdate/consistent-keys
Feb 20, 2015
4d82fc8
Merge pull request #39 from duedil-ltd/nsupdate/allow-unauthed
Feb 20, 2015
edca961
Merge pull request #40 from duedil-ltd/nsupdate/syncronous-locking
Feb 20, 2015
c81e5fb
Annotate prereq conditions with their RFC meanings
Feb 23, 2015
36a19cc
Tests for Name is/is not in use
Feb 23, 2015
18a38b2
Fix prereq checks for name-(not-)in-use
Feb 23, 2015
383a06c
Tests for RRset exists (value independent)
Feb 23, 2015
0233539
Use the rrset-exists helper in prereq checks
Feb 23, 2015
8dc8889
Add strict-rr-match helper to resolver
Feb 23, 2015
b8fa209
Validate RRset matches in prereq checks
Feb 23, 2015
82b0506
Factor out test boilerplate via reflection
Feb 24, 2015
f145857
Make test faliures report right line
Feb 24, 2015
16786e0
Tests for RRset exists (value dependent)
Feb 24, 2015
ab7021f
Cover RRset does not exist too
Feb 24, 2015
ab3aaa9
Internal notes on fixes needed in performUpdate
Feb 24, 2015
b6c5859
Full validation of update RRS before updating
Feb 24, 2015
a99838a
Don't shadow the etcd package
Feb 24, 2015
ffb2ff6
Fix delete tests:
Feb 25, 2015
cc870c8
Check TTL on inserted record
Feb 25, 2015
fc4b785
Add (failing) test for Delete-RRset:
Feb 25, 2015
dbe9da9
WIP: rewrite performUpdate:
Feb 25, 2015
2bb281e
More refactoring of performUpdate:
Feb 25, 2015
6b0f450
Full CNAME support, warts and all
Feb 26, 2015
c750c0e
Don't call `SetRcode`, set it directly:
May 22, 2015
65cb285
Send correct response opcode
May 22, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
all:: clean build
all:: clean build test
build:: get compile

clean:
motd:
@echo
@echo ' ___ __'
@echo ' ____/ (_)_____________ ____/ /___ _____'
@echo ' / __ / / ___/ ___/ __ \/ __ / __ \/ ___/'
@echo '/ /_/ / (__ ) /__/ /_/ / /_/ / / / (__ )'
@echo '\__,_/_/____/\___/\____/\__,_/_/ /_/____/'
@echo
@echo '© Copyright DueDil 2015. Licensed under MIT.'
@echo

clean: motd
@echo "\033[34m●\033[39m Cleaning out the build folder ./build"
rm -rf build/*
@echo "\033[32m✔\033[39m Cleaned ./build"

get:
get: motd
@echo "\033[34m●\033[39m Downloading go packages"
go get github.com/tools/godep
go get -d
godep restore
@echo "\033[32m✔\033[39m Finished downloading packages"

compile:
compile: motd get
@echo "\033[34m●\033[39m Building into ./build"
mkdir -p build/bin
go build -o build/bin/discodns *.go
@echo "\033[32m✔\033[39m Successfully built into ./build"

test:
test: motd
@echo "\033[34m●\033[39m Running tests"
go test -race
go test -race ./
@echo "\033[32m✔\033[39m Tests passed"

install:
install: motd compile
@echo "\033[34m●\033[39m Installing into /usr/local/bin"
cp build/bin/discodns /usr/local/bin/
@echo "\033[32m✔\033[39m Successfully installed into /usr/local/bin/discodns"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

discodns
======
========

[![Build Status](https://travis-ci.org/duedil-ltd/discodns.png?branch=master)](https://travis-ci.org/duedil-ltd/discodns)

Expand All @@ -18,7 +18,7 @@ An authoritative DNS nameserver that queries an [etcd](http://github.com/coreos/
- Support for TTLs
- Global default on all records
- Individual TTL values for individual records
- Runtime and application metrics are captured regularly for monitoring (stdout or grahite)
- Runtime and application metrics are captured regularly for monitoring (stdout or graphite)
- Incoming query filters

#### Production Readyness
Expand Down
2 changes: 1 addition & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type NodeConversionError struct {

func (e *NodeConversionError) Error() string {
return fmt.Sprintf(
"Unable to convert etc Node into a RR of type %d ('%s'): %s. Node details: %+v",
"Unable to convert etcd Node into a RR of type %d ('%s'): %s. Node details: %+v",
e.AttemptedType,
dns.TypeToString[e.AttemptedType],
e.Message,
Expand Down
29 changes: 29 additions & 0 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,32 @@ func (f *QueryFilterer) ShouldAcceptQuery(req *dns.Msg) bool {

return accepted
}

// parseFilters will convert a string into a Query Filter structure. The accepted
// format for input is [domain]:[type,type,...]. For example...
//
// - "domain:A,AAAA" # Match all A and AAAA queries within `domain`
// - ":TXT" # Matches only TXT queries for any domain
// - "domain:" # Matches any query within `domain`
func parseFilters(filters []string) []QueryFilter {
parsedFilters := make([]QueryFilter, 0)
for _, filter := range filters {
components := strings.Split(filter, ":")
if len(components) != 2 {
logger.Printf("Expected only one colon ([domain]:[type,type...])")
continue
}

domain := dns.Fqdn(components[0])
types := strings.Split(components[1], ",")

if len(types) == 1 && len(types[0]) == 0 {
types = make([]string, 0)
}

debugMsg("Adding filter with domain '" + domain + "' and types '" + strings.Join(types, ",") + "'")
parsedFilters = append(parsedFilters, QueryFilter{domain, types})
}

return parsedFilters
}
151 changes: 151 additions & 0 deletions lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package main

import (
"code.google.com/p/go-uuid/uuid"
"errors"
"github.com/coreos/go-etcd/etcd"
"time"
)

const (
ETCD_LOCK_TTL = 10
ETCD_LOCK_HEARTBEAT = 5
)

// EtcdKeyLock represents a lock on a single key. Its semantics: once asked to
// Acquire, it will try to grab hold of the key in etcd, if it doesnt exist.
// If it does exist, then the lock waits for the other party to release it.
// Once Acquired, it will hold on to the lock indefinitely until Abandoned
type EtcdKeyLock struct {
uuid string
key string
etcdClient *etcd.Client
killChan chan bool
killed bool
}

func NewEtcdKeyLock(etcdClient *etcd.Client, key string) *EtcdKeyLock {
uuid := uuid.New()
return &EtcdKeyLock{uuid: uuid, key: key, etcdClient: etcdClient, killChan: make(chan bool)}
}

// Start the process of trying to acquire a key lock. Returns a channel that
// will be sent true when the lock is aquired then closed. Callers can use this
// as their signal to proceed. The lock will kept indefinitely until abandoned.
func (l *EtcdKeyLock) Acquire() chan bool {
inner_acq := make(chan bool)
acquired := make(chan bool)
go func() {
_, ok := <-inner_acq
if ok {
acquired <- true
go heartbeat(l, nil)
go removeWhenCancelled(l)
}
close(acquired)
}()
go tryCreate(l, inner_acq)
return acquired
}

// Abandons the lock. This just means closing the internal cancellation
// channel, causing all the child goros to do whatever they need to do.
func (l *EtcdKeyLock) Abandon() {
if !l.killed {
l.killed = true
close(l.killChan)
}
}

// Blocking version of Acquire, hiding the channels from callers who just want
// to synchronously wait
func (l *EtcdKeyLock) WaitForAcquire(timeout int) (bool, error) {
timeoutKiller := time.AfterFunc(time.Duration(timeout) * time.Second, func(){
l.Abandon()
})
acq := l.Acquire()
ok, open := <- acq
if ok && open {
stopped := timeoutKiller.Stop()
// Stopped == false means the timer already fired: this shouldn't be
// possible (we shouldn't have been able to get an OK message in that
// case). Erroring mostly out of paranoia: I'm positive this race can't
// happen (famous last words though)
if !stopped {
return false, errors.New("Acquired a lock that was also killed by a timeout: this should not be possible!")
}
return true, nil
} else {
return false, errors.New("Couldn't aqcuire lock in time")
}
}

// The internals of trying to get a lock: Try to PUT to the lock key iff it
// doesn't exist. If that suceeds, the lock is owned; signal the chan and
// return. If it fails, watch the etcd key until it changes. When it does
// change, try again. Repeat indefinitely until cancelled.
func tryCreate(l *EtcdKeyLock, acq chan bool) {
defer close(acq)
for {
select {
case _, chOpen := <-l.killChan:
if !chOpen {
return
}
default:
_, err := l.etcdClient.Create(l.key, l.uuid, ETCD_LOCK_TTL)
if err == nil {
acq <- true
return
} else {
err, cast := err.(*etcd.EtcdError)
if cast && err.ErrorCode == 105 {
// Watch until it changes. (The current index is given to
// make sure we don't miss any changes in between)
_, err := l.etcdClient.Watch(l.key, err.Index+1, false, nil, l.killChan)
if err == nil {
// Skip the sleep and attempt a retry asap
continue
}
}
// if not created and not watching, pause briefly
time.Sleep(1 * time.Second)
}
}
}
}

func heartbeat(l *EtcdKeyLock, ping chan bool) {
if ping == nil {
ping = make(chan bool)
}
defer close(ping)
for {
time.Sleep(ETCD_LOCK_HEARTBEAT * time.Second)
select {
case _, chOpen := <-l.killChan:
if !chOpen {
return
}
default:
_, err := l.etcdClient.Set(l.key, l.uuid, ETCD_LOCK_TTL)
// non-blocking write on the ping channel
select {
case ping <- (err == nil):
default:
}
}
}
}

func removeWhenCancelled(l *EtcdKeyLock) {
defer func() {
l.etcdClient.CompareAndDelete(l.key, l.uuid, 0)
}()
for {
_, chOpen := <-l.killChan
if !chOpen {
return
}
}
}
59 changes: 59 additions & 0 deletions lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"testing"
"time"
)

func TestSimpleLockUnlock(t *testing.T) {
testKey := "TestSimpleLockUnlock/.lock"
client.Delete(testKey, true)

lock := NewEtcdKeyLock(client, testKey)
locked, err := lock.WaitForAcquire(1)

if !locked || err != nil {
t.Error("Expected to acquire lock, failed")
t.Fatal()
}
_, err = client.Get(testKey, false, true)
if err != nil {
t.Error("Lock claimed to succeed but etcd record missing/broken")
t.Fatal()
}

lock.Abandon()
time.Sleep(500 * time.Millisecond)
_, err = client.Get(testKey, false, true)
if err == nil {
t.Error("Lock abandoned, but key exists")
t.Fatal()
}
}

func TestConflictingLock(t *testing.T) {
testKey := "TestConflictingLock/.lock"
client.Delete(testKey, true)

lock_a := NewEtcdKeyLock(client, testKey)
lock_a.WaitForAcquire(30)

lock_b := NewEtcdKeyLock(client, testKey)
b_locked, b_err := lock_b.WaitForAcquire(1)
if b_locked || b_err == nil {
t.Error("Expected second lock to timeout")
t.Fatal()
}

lock_c := NewEtcdKeyLock(client, testKey)
go func(){
time.Sleep(500 * time.Millisecond)
lock_a.Abandon()
}()

c_locked, c_err := lock_c.WaitForAcquire(5)
if !c_locked || c_err != nil {
t.Error("Expected third lock to succeed in time")
t.Fatal()
}
}
Loading