├── .travis.yml ├── license ├── main.go ├── main_test.go ├── patents └── readme.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | 6 | before_install: 7 | - go get -v golang.org/x/tools/cmd/vet 8 | - go get -v golang.org/x/tools/cmd/cover 9 | - go get -v github.com/golang/lint/golint 10 | 11 | install: 12 | - go install -race -v std 13 | - go get -race -t -v ./... 14 | - go install -race -v ./... 15 | 16 | script: 17 | - go vet ./... 18 | - $HOME/gopath/bin/golint . 19 | - go test -cpu=2 -race -v ./... 20 | - go test -cpu=2 -covermode=atomic ./... 21 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For boxdns software 4 | 5 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Command boxdns provides a DNS server for a subset of docker containers. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/facebookgo/dockerutil" 16 | "github.com/facebookgo/errgroup" 17 | "github.com/facebookgo/stackerr" 18 | "github.com/miekg/dns" 19 | "github.com/samalba/dockerclient" 20 | ) 21 | 22 | const ( 23 | qtypeIPv4 = 1 24 | qtypeIPv6 = 28 25 | ) 26 | 27 | // Overrides specify the custom hostname => IP address our DNS server will 28 | // override. 29 | type Overrides map[string]net.IP 30 | 31 | // DNSClient allows us to forward unhandled DNS queries. 32 | type DNSClient interface { 33 | Exchange(m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error) 34 | } 35 | 36 | // DockerClient allows us to build our overrides and monitor for container 37 | // changes. 38 | type DockerClient interface { 39 | StartMonitorEvents(dockerclient.Callback, chan error, ...interface{}) 40 | ListContainers(all bool, size bool, filters string) ([]dockerclient.Container, error) 41 | InspectContainer(id string) (*dockerclient.ContainerInfo, error) 42 | } 43 | 44 | // App is our DNS server. 45 | type App struct { 46 | Addr string 47 | Prefix string 48 | Domain string 49 | Nameservers []string 50 | Overrides atomic.Value 51 | Log *log.Logger 52 | 53 | rebuildMutex sync.Mutex 54 | docker DockerClient 55 | dnsUDPclient DNSClient 56 | dnsTCPclient DNSClient 57 | } 58 | 59 | func (a *App) run(nameservers string) error { 60 | // default to /etc/resolv.conf if explicit nameservers were not provided 61 | if nameservers == "" { 62 | cc, err := dns.ClientConfigFromFile("/etc/resolv.conf") 63 | if err != nil { 64 | return stackerr.Wrap(err) 65 | } 66 | for _, s := range cc.Servers { 67 | a.Nameservers = append(a.Nameservers, net.JoinHostPort(s, cc.Port)) 68 | } 69 | } else { 70 | a.Nameservers = strings.Split(nameservers, ",") 71 | } 72 | 73 | c, err := dockerutil.BestEffortDockerClient() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | a.docker = c 79 | a.dnsTCPclient = &dns.Client{Net: "tcp", SingleInflight: true} 80 | a.dnsUDPclient = &dns.Client{Net: "udp", SingleInflight: true} 81 | 82 | // monitor first, then rebuild to ensure we dont miss any updates 83 | a.docker.StartMonitorEvents(a.onDockerEvent, make(chan error, 1)) 84 | if err := a.rebuild(); err != nil { 85 | return err 86 | } 87 | 88 | var eg errgroup.Group 89 | eg.Add(2) 90 | go a.listenAndServe("udp", &eg) 91 | go a.listenAndServe("tcp", &eg) 92 | if err := eg.Wait(); err != nil { 93 | return err 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (a *App) listenAndServe(net string, eg *errgroup.Group) { 100 | defer eg.Done() 101 | if err := dns.ListenAndServe(a.Addr, net, a); err != nil { 102 | eg.Error(stackerr.Wrap(err)) 103 | } 104 | } 105 | 106 | func (a *App) rebuild() error { 107 | // only 1 rebuild at a time 108 | a.rebuildMutex.Lock() 109 | defer a.rebuildMutex.Unlock() 110 | 111 | containers, err := a.docker.ListContainers(false, false, "") 112 | if err != nil { 113 | return stackerr.Wrap(err) 114 | } 115 | 116 | overrides := make(Overrides) 117 | for _, container := range containers { 118 | var ci *dockerclient.ContainerInfo 119 | for _, name := range container.Names { 120 | if strings.HasPrefix(name, a.Prefix) && strings.Index(name[1:], "/") == -1 { 121 | if ci == nil { 122 | if ci, err = a.docker.InspectContainer(container.Id); err != nil { 123 | return stackerr.Wrap(err) 124 | } 125 | } 126 | ip := net.ParseIP(ci.NetworkSettings.IPAddress) 127 | if ip == nil { 128 | return stackerr.Newf( 129 | "invalid IP address from docker %q for %q", 130 | ci.NetworkSettings.IPAddress, 131 | name, 132 | ) 133 | } 134 | overrides[strings.TrimPrefix(name, a.Prefix)+a.Domain+"."] = ip 135 | } 136 | } 137 | } 138 | a.Overrides.Store(overrides) 139 | return nil 140 | } 141 | 142 | func (a *App) onDockerEvent(e *dockerclient.Event, errch chan error, args ...interface{}) { 143 | go func() { 144 | if err := a.rebuild(); err != nil { 145 | a.Log.Printf("error rebuilding overrides: %s\n", err) 146 | } 147 | }() 148 | } 149 | 150 | // ServeDNS serves DNS requests. 151 | func (a *App) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { 152 | if req.Opcode == dns.OpcodeQuery { 153 | a.handleDNSQuery(w, req) 154 | return 155 | } 156 | a.forwardDNSRequest(w, req) 157 | } 158 | 159 | func (a *App) handleDNSQuery(w dns.ResponseWriter, req *dns.Msg) { 160 | // TODO: do we need to handle 1 of many questions and forward the rest? 161 | if len(req.Question) == 1 { 162 | o := a.Overrides.Load().(Overrides) 163 | q := req.Question[0] 164 | if ip := o[q.Name]; ip != nil { 165 | switch q.Qtype { 166 | default: 167 | a.Log.Printf("unhandled Qtype %d for overridden host %q\n", q.Qtype, q.Name) 168 | case qtypeIPv4: 169 | res := new(dns.Msg) 170 | res.SetReply(req) 171 | res.RecursionAvailable = true 172 | res.Answer = []dns.RR{ 173 | &dns.A{ 174 | A: ip, 175 | Hdr: dns.RR_Header{ 176 | Name: q.Name, 177 | Rrtype: 1, 178 | Class: 1, 179 | Ttl: 100, 180 | Rdlength: 4, 181 | }, 182 | }, 183 | } 184 | w.WriteMsg(res) 185 | return 186 | case qtypeIPv6: 187 | res := new(dns.Msg) 188 | res.SetReply(req) 189 | res.RecursionAvailable = true 190 | w.WriteMsg(res) 191 | return 192 | } 193 | } 194 | } 195 | a.forwardDNSRequest(w, req) 196 | } 197 | 198 | func (a *App) forwardDNSRequest(w dns.ResponseWriter, req *dns.Msg) { 199 | // proxy using the same protocol 200 | exchange := a.dnsUDPclient.Exchange 201 | if _, ok := w.RemoteAddr().(*net.TCPAddr); ok { 202 | exchange = a.dnsTCPclient.Exchange 203 | } 204 | 205 | // try all nameservers in order 206 | for _, ns := range a.Nameservers { 207 | res, _, err := exchange(req, ns) 208 | if err == nil { 209 | res.Compress = true 210 | w.WriteMsg(res) 211 | return 212 | } 213 | a.Log.Printf("error from %q: %v", ns, err) 214 | } 215 | 216 | // failed 217 | m := new(dns.Msg) 218 | m.SetReply(req) 219 | m.SetRcode(req, dns.RcodeServerFailure) 220 | w.WriteMsg(m) 221 | } 222 | 223 | func main() { 224 | nameservers := flag.String( 225 | "nameservers", "", "nameservers to foward unhandled requests to") 226 | a := App{Log: log.New(os.Stderr, "", log.LstdFlags)} 227 | flag.StringVar(&a.Addr, "addr", ":53", "dns address to listen on") 228 | flag.StringVar(&a.Prefix, "prefix", "/", "docker container prefix to include") 229 | flag.StringVar(&a.Domain, "domain", ".local", "domain suffix for hostname") 230 | flag.Parse() 231 | 232 | if err := a.run(*nameservers); err != nil { 233 | fmt.Fprintln(os.Stderr, err) 234 | os.Exit(1) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "regexp" 9 | "sync/atomic" 10 | "testing" 11 | "time" 12 | 13 | "github.com/facebookgo/ensure" 14 | "github.com/miekg/dns" 15 | "github.com/samalba/dockerclient" 16 | ) 17 | 18 | type fDockerClient struct { 19 | startMonitorEvents func(dockerclient.Callback, chan error, ...interface{}) 20 | listContainers func(all bool, size bool, filters string) ([]dockerclient.Container, error) 21 | inspectContainer func(id string) (*dockerclient.ContainerInfo, error) 22 | } 23 | 24 | func (f fDockerClient) StartMonitorEvents(cb dockerclient.Callback, errch chan error, args ...interface{}) { 25 | f.startMonitorEvents(cb, errch, args...) 26 | } 27 | 28 | func (f fDockerClient) ListContainers(all bool, size bool, filters string) ([]dockerclient.Container, error) { 29 | return f.listContainers(all, size, filters) 30 | } 31 | 32 | func (f fDockerClient) InspectContainer(id string) (*dockerclient.ContainerInfo, error) { 33 | return f.inspectContainer(id) 34 | } 35 | 36 | type fDNSClient struct { 37 | exchange func(m *dns.Msg, a string) (*dns.Msg, time.Duration, error) 38 | } 39 | 40 | func (f fDNSClient) Exchange(m *dns.Msg, a string) (*dns.Msg, time.Duration, error) { 41 | return f.exchange(m, a) 42 | } 43 | 44 | type fDNSResponseWriter struct { 45 | dns.ResponseWriter 46 | remoteAddr net.Addr 47 | msg *dns.Msg 48 | } 49 | 50 | func (f *fDNSResponseWriter) RemoteAddr() net.Addr { 51 | return f.remoteAddr 52 | } 53 | 54 | func (f *fDNSResponseWriter) WriteMsg(m *dns.Msg) error { 55 | f.msg = m 56 | return nil 57 | } 58 | 59 | func TestRebuildPrefixDomain(t *testing.T) { 60 | t.Parallel() 61 | const ( 62 | prefix = "/p-" 63 | name = "foo" 64 | ip = "1.2.3.4" 65 | ) 66 | a := App{ 67 | Prefix: prefix, 68 | Domain: ".local", 69 | docker: fDockerClient{ 70 | listContainers: func(bool, bool, string) ([]dockerclient.Container, error) { 71 | return []dockerclient.Container{ 72 | { 73 | Id: "xyz", 74 | Names: []string{"foo", prefix + name}, 75 | }, 76 | }, nil 77 | }, 78 | inspectContainer: func(string) (*dockerclient.ContainerInfo, error) { 79 | var ci dockerclient.ContainerInfo 80 | ci.NetworkSettings.IPAddress = ip 81 | return &ci, nil 82 | }, 83 | }, 84 | } 85 | ensure.Nil(t, a.rebuild()) 86 | ensure.DeepEqual(t, a.Overrides.Load(), Overrides{ 87 | name + a.Domain + ".": net.ParseIP(ip), 88 | }) 89 | } 90 | 91 | func TestRebuildListError(t *testing.T) { 92 | t.Parallel() 93 | const errMsg = "foo" 94 | a := App{ 95 | docker: fDockerClient{ 96 | listContainers: func(bool, bool, string) ([]dockerclient.Container, error) { 97 | return nil, errors.New(errMsg) 98 | }, 99 | }, 100 | } 101 | ensure.Err(t, a.rebuild(), regexp.MustCompile(errMsg)) 102 | } 103 | 104 | func TestRebuildInspectError(t *testing.T) { 105 | t.Parallel() 106 | const errMsg = "foo" 107 | a := App{ 108 | docker: fDockerClient{ 109 | listContainers: func(bool, bool, string) ([]dockerclient.Container, error) { 110 | return []dockerclient.Container{ 111 | { 112 | Id: "xyz", 113 | Names: []string{"foo"}, 114 | }, 115 | }, nil 116 | }, 117 | inspectContainer: func(string) (*dockerclient.ContainerInfo, error) { 118 | return nil, errors.New(errMsg) 119 | }, 120 | }, 121 | } 122 | ensure.Err(t, a.rebuild(), regexp.MustCompile(errMsg)) 123 | } 124 | 125 | func TestRebuildInvalidIP(t *testing.T) { 126 | t.Parallel() 127 | a := App{ 128 | docker: fDockerClient{ 129 | listContainers: func(bool, bool, string) ([]dockerclient.Container, error) { 130 | return []dockerclient.Container{ 131 | { 132 | Id: "xyz", 133 | Names: []string{"foo"}, 134 | }, 135 | }, nil 136 | }, 137 | inspectContainer: func(string) (*dockerclient.ContainerInfo, error) { 138 | var ci dockerclient.ContainerInfo 139 | ci.NetworkSettings.IPAddress = "a" 140 | return &ci, nil 141 | }, 142 | }, 143 | } 144 | ensure.Err(t, a.rebuild(), regexp.MustCompile("invalid IP")) 145 | } 146 | 147 | func TestServeDNSForwardUDP(t *testing.T) { 148 | t.Parallel() 149 | const ns = "a" 150 | res := new(dns.Msg) 151 | a := App{ 152 | Nameservers: []string{ns}, 153 | dnsUDPclient: fDNSClient{ 154 | exchange: func(m *dns.Msg, a string) (*dns.Msg, time.Duration, error) { 155 | ensure.DeepEqual(t, a, ns) 156 | return res, time.Minute, nil 157 | }, 158 | }, 159 | } 160 | req := new(dns.Msg) 161 | req.Opcode = dns.OpcodeStatus 162 | var w fDNSResponseWriter 163 | a.ServeDNS(&w, req) 164 | ensure.DeepEqual(t, w.msg, res) 165 | } 166 | 167 | func TestServeDNSForwardTCP(t *testing.T) { 168 | t.Parallel() 169 | const ns = "a" 170 | res := new(dns.Msg) 171 | a := App{ 172 | Nameservers: []string{ns}, 173 | dnsUDPclient: fDNSClient{}, 174 | dnsTCPclient: fDNSClient{ 175 | exchange: func(m *dns.Msg, a string) (*dns.Msg, time.Duration, error) { 176 | ensure.DeepEqual(t, a, ns) 177 | return res, time.Minute, nil 178 | }, 179 | }, 180 | } 181 | req := new(dns.Msg) 182 | req.Opcode = dns.OpcodeStatus 183 | w := fDNSResponseWriter{ 184 | remoteAddr: &net.TCPAddr{}, 185 | } 186 | a.ServeDNS(&w, req) 187 | ensure.DeepEqual(t, w.msg, res) 188 | } 189 | 190 | func TestServeDNSForwardTryAllAndFail(t *testing.T) { 191 | t.Parallel() 192 | ns := []string{"a", "b"} 193 | var current int32 194 | a := App{ 195 | Log: log.New(ioutil.Discard, "", log.LstdFlags), 196 | Nameservers: ns, 197 | dnsUDPclient: fDNSClient{ 198 | exchange: func(m *dns.Msg, a string) (*dns.Msg, time.Duration, error) { 199 | ensure.DeepEqual(t, a, ns[int(atomic.AddInt32(¤t, 1)-1)]) 200 | return nil, time.Minute, errors.New("foo") 201 | }, 202 | }, 203 | } 204 | req := new(dns.Msg) 205 | req.Opcode = dns.OpcodeStatus 206 | var w fDNSResponseWriter 207 | a.ServeDNS(&w, req) 208 | ensure.DeepEqual(t, w.msg.Rcode, dns.RcodeServerFailure) 209 | ensure.DeepEqual(t, atomic.LoadInt32(¤t), int32(2)) 210 | } 211 | 212 | func TestServeDNSOverrideIPv4(t *testing.T) { 213 | t.Parallel() 214 | const hostname = "foo.com." 215 | ip := net.ParseIP("1.2.3.4") 216 | var a App 217 | a.Overrides.Store(Overrides{hostname: ip}) 218 | req := new(dns.Msg) 219 | req.Opcode = dns.OpcodeQuery 220 | req.Question = []dns.Question{{Name: hostname, Qtype: qtypeIPv4}} 221 | var w fDNSResponseWriter 222 | a.ServeDNS(&w, req) 223 | ensure.DeepEqual(t, w.msg.Answer, []dns.RR{ 224 | &dns.A{ 225 | A: ip, 226 | Hdr: dns.RR_Header{ 227 | Name: hostname, 228 | Rrtype: 1, 229 | Class: 1, 230 | Ttl: 100, 231 | Rdlength: 4, 232 | }, 233 | }, 234 | }) 235 | } 236 | 237 | func TestServeDNSOverrideIPv6(t *testing.T) { 238 | t.Parallel() 239 | const hostname = "foo.com." 240 | var a App 241 | a.Overrides.Store(Overrides{hostname: net.ParseIP("1.2.3.4")}) 242 | req := new(dns.Msg) 243 | req.Opcode = dns.OpcodeQuery 244 | req.Question = []dns.Question{{Name: hostname, Qtype: qtypeIPv6}} 245 | var w fDNSResponseWriter 246 | a.ServeDNS(&w, req) 247 | ensure.DeepEqual(t, len(w.msg.Answer), 0) 248 | } 249 | 250 | func TestServeDNSForwardNonOverrideQuery(t *testing.T) { 251 | t.Parallel() 252 | const ns = "a" 253 | res := new(dns.Msg) 254 | a := App{ 255 | Nameservers: []string{ns}, 256 | dnsUDPclient: fDNSClient{ 257 | exchange: func(m *dns.Msg, a string) (*dns.Msg, time.Duration, error) { 258 | ensure.DeepEqual(t, a, ns) 259 | return res, time.Minute, nil 260 | }, 261 | }, 262 | } 263 | a.Overrides.Store(Overrides{}) 264 | req := new(dns.Msg) 265 | req.Opcode = dns.OpcodeQuery 266 | req.Question = []dns.Question{{Name: "foo.com."}} 267 | var w fDNSResponseWriter 268 | a.ServeDNS(&w, req) 269 | ensure.DeepEqual(t, w.msg, res) 270 | } 271 | -------------------------------------------------------------------------------- /patents: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the boxdns software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook’s rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | boxdns [![Build Status](https://secure.travis-ci.org/facebookgo/boxdns.svg)](https://travis-ci.org/facebookgo/boxdns) 2 | ====== 3 | 4 | boxdns provides a DNS server suitable for use in development along with docker. 5 | It provides an alternative to linking. 6 | 7 | The typical usecase where boxdns is useful is where one spins up a number of 8 | containers representing an "instance". Imagine something like a mysql container, 9 | memcache container, and an application container. The instances use a "prefix" 10 | to allow isolation from other instances. So for example the relevant container 11 | names may be "dev-mysql", "dev-memcached", "dev-app". Within the containers 12 | we want to be able to reference them simply as "mysql.myapp.com" and 13 | "memcached.myapp.com". To achive this, we can spin up a boxdns server with 14 | 15 | ``` 16 | boxdns -prefix=/dev- -domain=.myapp.com 17 | ``` 18 | 19 | Further the application container needs to be started with its DNS servers 20 | configured to use this `boxdns` instance. 21 | 22 | TODO: provide a boxdns container. 23 | 24 | Documentation: https://godoc.org/github.com/facebookgo/boxdns 25 | --------------------------------------------------------------------------------