├── .gitignore ├── Dockerfile ├── cache.go ├── dns.go ├── main.go ├── readme.md └── spy.go /.gitignore: -------------------------------------------------------------------------------- 1 | docker-spy 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | ADD ./docker-spy /bin/docker-spy 4 | 5 | ENV DNS_RECURSOR 8.8.8.8 6 | 7 | EXPOSE 53 8 | EXPOSE 53/udp 9 | 10 | ENTRYPOINT ["/bin/docker-spy"] 11 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type Record struct { 8 | ip string 9 | arpa string 10 | fqdn string 11 | } 12 | 13 | type Cache struct { 14 | records map[string]*Record 15 | } 16 | 17 | func (c Cache) Set(id string, r *Record) { 18 | c.records[id] = r 19 | } 20 | 21 | // Provides lookups based on fqdn or ip 22 | func (c Cache) Get(id string) (*Record, bool) { 23 | 24 | var reverseLookup = regexp.MustCompile("^.*\\.in-addr\\.arpa\\.$") 25 | 26 | if reverseLookup.MatchString(id) { 27 | for _, record := range c.records { 28 | if record.arpa == id { 29 | return record, true 30 | } 31 | } 32 | } 33 | 34 | if record, ok := c.records[id]; ok { 35 | return record, true 36 | } 37 | 38 | return nil, false 39 | } 40 | 41 | func (c Cache) Remove(id string) { 42 | delete(c.records, id) 43 | } 44 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "log" 6 | "net" 7 | "strconv" 8 | ) 9 | 10 | type DNS struct { 11 | bind string 12 | port int 13 | domain string 14 | recursors []string 15 | cache Cache 16 | } 17 | 18 | func (s *DNS) Run() { 19 | 20 | s.cache.records = make(map[string]*Record) 21 | 22 | mux := dns.NewServeMux() 23 | bind := s.bind + ":" + strconv.Itoa(s.port) 24 | 25 | srvUDP := &dns.Server{ 26 | Addr: bind, 27 | Net: "udp", 28 | Handler: mux, 29 | } 30 | 31 | srvTCP := &dns.Server{ 32 | Addr: bind, 33 | Net: "tcp", 34 | Handler: mux, 35 | } 36 | 37 | mux.HandleFunc(s.domain, s.handleDNSInternal) 38 | mux.HandleFunc("in-addr.arpa.", s.handleReverseDNSLookup) 39 | mux.HandleFunc(".", s.handleDNSExternal) 40 | 41 | go func() { 42 | log.Printf("Binding UDP listener to %s", bind) 43 | 44 | err := srvUDP.ListenAndServe() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | }() 49 | 50 | go func() { 51 | log.Printf("Binding TCP listener to %s", bind) 52 | 53 | err := srvTCP.ListenAndServe() 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | }() 58 | } 59 | 60 | func (s *DNS) handleDNSInternal(w dns.ResponseWriter, req *dns.Msg) { 61 | 62 | q := req.Question[0] 63 | 64 | if q.Qtype == dns.TypeA && q.Qclass == dns.ClassINET { 65 | if record, ok := s.cache.Get(q.Name); ok { 66 | 67 | log.Printf("Found internal record for %s", q.Name) 68 | 69 | m := new(dns.Msg) 70 | m.SetReply(req) 71 | rr_header := dns.RR_Header{ 72 | Name: q.Name, 73 | Rrtype: dns.TypeA, 74 | Class: dns.ClassINET, 75 | Ttl: 0, 76 | } 77 | a := &dns.A{rr_header, net.ParseIP(record.ip)} 78 | m.Answer = append(m.Answer, a) 79 | w.WriteMsg(m) 80 | 81 | return 82 | } 83 | 84 | log.Printf("No internal record found for %s", q.Name) 85 | dns.HandleFailed(w, req) 86 | } 87 | 88 | log.Printf("Only handling type A requests, skipping") 89 | dns.HandleFailed(w, req) 90 | } 91 | 92 | func (s *DNS) handleDNSExternal(w dns.ResponseWriter, req *dns.Msg) { 93 | 94 | network := "udp" 95 | if _, ok := w.RemoteAddr().(*net.TCPAddr); ok { 96 | network = "tcp" 97 | } 98 | 99 | c := &dns.Client{Net: network} 100 | var r *dns.Msg 101 | var err error 102 | for _, recursor := range s.recursors { 103 | 104 | if recursor == "" { 105 | log.Printf("Found empty recursor") 106 | continue 107 | } 108 | 109 | log.Printf("Forwarding request to external recursor for: %s", req.Question[0].Name) 110 | 111 | r, _, err = c.Exchange(req, recursor) 112 | if err == nil { 113 | if err := w.WriteMsg(r); err != nil { 114 | log.Printf("DNS lookup failed %v", err) 115 | } 116 | return 117 | } 118 | } 119 | 120 | dns.HandleFailed(w, req) 121 | } 122 | 123 | func (s *DNS) handleReverseDNSLookup(w dns.ResponseWriter, req *dns.Msg) { 124 | 125 | q := req.Question[0] 126 | 127 | if q.Qtype == dns.TypePTR && q.Qclass == dns.ClassINET { 128 | 129 | if record, ok := s.cache.Get(q.Name); ok { 130 | 131 | log.Printf("Found internal record for %s", q.Name) 132 | 133 | m := new(dns.Msg) 134 | m.SetReply(req) 135 | rr_header := dns.RR_Header{ 136 | Name: q.Name, 137 | Rrtype: dns.TypePTR, 138 | Class: dns.ClassINET, 139 | Ttl: 0, 140 | } 141 | 142 | a := &dns.PTR{rr_header, record.fqdn} 143 | m.Answer = append(m.Answer, a) 144 | w.WriteMsg(m) 145 | 146 | return 147 | 148 | } 149 | 150 | log.Printf("Forwarding request to external recursor for: %s", q.Name) 151 | 152 | // Forward the request 153 | s.handleDNSExternal(w, req) 154 | } 155 | 156 | dns.HandleFailed(w, req) 157 | } 158 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | dockerApi "github.com/fsouza/go-dockerclient" 11 | ) 12 | 13 | var dnsBind = flag.String("dns-bind", getopt("DNS_BIND", "0.0.0.0"), "Bind address for the DNS server") 14 | var dnsPort = flag.String("dns-port", getopt("DNS_PORT", "53"), "Port for the DNS server") 15 | var dnsRecursor = flag.String("dns-recursor", getopt("DNS_RECURSOR", ""), "DNS recursor for non-local addresses") 16 | var dnsDomain = flag.String("dns-domain", getopt("DNS_DOMAIN", "localdomain"), "The domain that Docker-spy should consider local") 17 | var dockerHost = flag.String("docker-host", getopt("DOCKER_HOST", "unix:///var/run/docker.sock"), "Address for the Docker daemon") 18 | 19 | func getopt(name, def string) string { 20 | if env := os.Getenv(name); env != "" { 21 | return env 22 | } 23 | return def 24 | } 25 | 26 | func main() { 27 | 28 | flag.Parse() 29 | 30 | log.Println("Starting DNS server...") 31 | 32 | port, err := strconv.Atoi(*dnsPort) 33 | if err != nil { 34 | log.Fatalf("Could not convert %s to numeric type", *dnsPort) 35 | } 36 | 37 | server := &DNS{ 38 | bind: *dnsBind, 39 | port: port, 40 | recursors: []string{*dnsRecursor + ":53"}, 41 | domain: *dnsDomain + ".", 42 | } 43 | 44 | server.Run() 45 | 46 | log.Println("Listening for container events...") 47 | 48 | docker, err := dockerApi.NewClient(*dockerHost) 49 | 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | spy := &Spy{ 55 | docker: docker, 56 | dns: server, 57 | } 58 | 59 | spy.Watch() 60 | 61 | sig := make(chan os.Signal) 62 | signal.Notify(sig, os.Interrupt) 63 | 64 | forever: 65 | for { 66 | select { 67 | case <-sig: 68 | log.Println("signal received, stopping") 69 | break forever 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Docker-spy provides a DNS service based on Docker container events. It keeps an in-memory database of records that map container hostnames to ip addresses. When containers are start/stopped/destroyed it keeps track of their location. 4 | 5 | It is specifically targeted at small local development environments where you want an easy way to connect with your containers. Originally developed as part of my [blog series](http://www.ivoverberk.nl/docker-tutorial-puppet-and-the-foreman/) on running a local Puppet dev stack with Docker. 6 | 7 | # Usage 8 | 9 | The easiest way to run docker-spy is through Docker. The [image](https://registry.hub.docker.com/u/iverberk/docker-spy/) is based on the scratch image (basically a zero sized image) and contains only the compiled Go executable. 10 | 11 | ### Configuration 12 | 13 | Docker-spy can be configured through a number of environment variables: 14 | 15 | * DNS_BIND: the address that the DNS server will bind to in the container. Defaults to '0.0.0.0'. 16 | * DNS_PORT: the port on which the DNS server will be reachable (tcp/udp). Defaults to '53' 17 | * DNS_RECURSOR: the recursor to use when DNS requests are made to non-local addresses. Defaults to '8.8.8.8' from the Dockerfile 18 | * DNS_DOMAIN: the domain that docker-spy should consider local and keep records for. Defaults to 'localdomain' 19 | * DOCKER_HOST: the location of the Docker daemon. Defaults to the DOCKER_HOST environment variable or, if the DOCKER_HOST environment variable is not set, unix:///var/run/docker.sock. Setting this explicitly allows you to override the location. 20 | 21 | ### DNS Forwarding 22 | 23 | Docker-spy will consider all DNS requests that end with the above configured DNS_DOMAIN to be internal requests that should be mapped to a container. All other DNS requests are forwarded to the recursor, so the DNS server should be relatively transparent. 24 | 25 | ### Prerequisites 26 | 27 | Before starting docker-spy you should know the following things about your system: 28 | 29 | 1. The IP address of the Docker bridge. Issue an ```ifconfig``` and look for the Docker0 bridge entry. OSX users should first ssh into the boot2docker virtual machine with ```boot2docker ssh``` 30 | 2. (OSX Only) The IP address of the boot2docker virtual machine. Run ```boot2docker ip``` to find out what it is. 31 | 32 | ### Running Docker-spy 33 | To run docker-spy you can issue the following command: 34 | 35 | ``` 36 | docker run --name docker_spy -p 53:53/udp -p 53:53 -v /var/run/docker.sock:/var/run/docker.sock iverberk/docker-spy 37 | ``` 38 | 39 | This maps the Docker socket as a volume in the container so that events may be tracked and it publishes port 53 on udp/tcp to the host. Add a ```-d``` parameter to run the container in the background. You may then inspect the logs with ```docker logs docker_spy``` 40 | 41 | ### OSX (boot2docker) 42 | 43 | To have seamless DNS resolution and access to your containers you should perform the following steps: 44 | 45 | 1. Create an /etc/resolver/$DOMAIN file (create the /etc/resolver directory first). $DOMAIN should be substituted with the domain that you use for local development (e.g. for 'localdomain' create a /etc/resolver/localdomain file). Add the following contents to this file: ```nameserver x.x.x.x``` (x.x.x.x should be replaced with the Docker bridge IP address that you looked up earlier) 46 | 2. Create a route so that the container ip range is accessible from the osx host system through the boot2docker host adapter:

```sudo route -n add -net 172.17.0.0 192.168.59.104```

**important:** substitute 192.168.59.104 with the ip address of **your** boot2docker vm (run boot2docker ip to find out what it is). Substitute 172.17.0.0 with the appropriate range (if you have the Docker bridge IP just chop of the last two digits and replace them with zeros, e.g. if the Docker bridge IP is 10.0.42.1 then you should use 10.0.0.0 as **your** -net parameter in the above command) 47 | 3. You should now be able to ping your containers directly and run DNS queries against the Docker bridge IP.

**important**: check for any firewall rules on the host that may be blocking the udp/tcp traffic on port 53! 48 | 49 | ### Linux 50 | 51 | To have automatic DNS resolution of your containers you should update your /etc/resolv.conf and add the Docker bridge IP address as a resolver (usually 172.17.42.1 but check it with ifconfig to be certain). 52 | 53 | # Issues and Contributing 54 | 55 | Docker-spy is really young and has a lot of rough edges. I wanted to have a basic, working solution before adding nice-to-haves. It is also my first Go program so there will probably be less then idiomatic constructs in the program. Fixes and enhancements are gladly accepted. Issues may be filed against the github repository. 56 | 57 | ### Building 58 | 59 | To build docker-spy just install the go build environment and run ```go build -o docker-spy *.go``` in the directory. 60 | -------------------------------------------------------------------------------- /spy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | dockerApi "github.com/fsouza/go-dockerclient" 5 | "github.com/miekg/dns" 6 | "log" 7 | "regexp" 8 | ) 9 | 10 | type Spy struct { 11 | docker *dockerApi.Client 12 | dns *DNS 13 | } 14 | 15 | func (s *Spy) Watch() { 16 | 17 | s.registerRunningContainers() 18 | 19 | events := make(chan *dockerApi.APIEvents) 20 | s.docker.AddEventListener(events) 21 | 22 | go s.readEventStream(events) 23 | } 24 | 25 | func (s *Spy) registerRunningContainers() { 26 | containers, err := s.docker.ListContainers(dockerApi.ListContainersOptions{}) 27 | if err != nil { 28 | log.Fatalf("Unable to register running containers: %v", err) 29 | } 30 | for _, listing := range containers { 31 | s.mutateContainerInCache(listing.ID, listing.Status) 32 | } 33 | } 34 | 35 | func (s *Spy) readEventStream(events chan *dockerApi.APIEvents) { 36 | for msg := range events { 37 | s.mutateContainerInCache(msg.ID, msg.Status) 38 | } 39 | } 40 | 41 | func (s *Spy) mutateContainerInCache(id string, status string) { 42 | 43 | container, err := s.docker.InspectContainer(id) 44 | if err != nil { 45 | log.Printf("Unable to inspect container %s, skipping", id) 46 | return 47 | } 48 | 49 | name := container.Config.Hostname + "." + container.Config.Domainname + "." 50 | 51 | var running = regexp.MustCompile("start|^Up.*$") 52 | var stopping = regexp.MustCompile("die") 53 | 54 | switch { 55 | case running.MatchString(status): 56 | log.Printf("Adding record for %v", name) 57 | arpa, err := dns.ReverseAddr(container.NetworkSettings.IPAddress) 58 | if err != nil { 59 | log.Printf("Unable to create ARPA address. Reverse DNS lookup will be unavailable for this container.") 60 | } 61 | s.dns.cache.Set(name, &Record{ 62 | container.NetworkSettings.IPAddress, 63 | arpa, 64 | name, 65 | }) 66 | case stopping.MatchString(status): 67 | log.Printf("Removing record for %v", name) 68 | s.dns.cache.Remove(name) 69 | } 70 | } 71 | --------------------------------------------------------------------------------