Skip to content

Instantly share code, notes, and snippets.

@tianon
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tianon/063c8083c215be29b83a to your computer and use it in GitHub Desktop.
Save tianon/063c8083c215be29b83a to your computer and use it in GitHub Desktop.
first pass at a raw DNS interface to Docker
github.com/tianon/rawdns
FROM golang:onbuild
{
"docker.": {
"type": "containers",
"socket": "unix:///var/run/docker.sock"
},
".": {
"type": "forwarding",
"nameservers": [ "8.8.8.8:53", "8.8.4.4:53" ]
}
}
// borrowed (and then modified) from https://github.com/skynetservices/skydns/blob/0301ef16a80107b84a9e25fb4afa7fb777c3f7a8/forwarding.go
// Copyright (c) 2014 The SkyDNS Authors. All rights reserved.
// Use of this source code is governed by The MIT License (MIT) that can be
// found in the LICENSE file.
package main
import (
"log"
"net"
"time"
"github.com/miekg/dns"
)
// ServeDNSForward forwards a request to a nameservers and returns the response.
func handleForwarding(nameservers []string, w dns.ResponseWriter, req *dns.Msg) {
if len(nameservers) == 0 {
log.Printf("no nameservers defined, can not forward\n")
m := new(dns.Msg)
m.SetReply(req)
m.SetRcode(req, dns.RcodeServerFailure)
m.Authoritative = false // no matter what set to false
m.RecursionAvailable = true // and this is still true
w.WriteMsg(m)
return
}
tcp := false
if _, ok := w.RemoteAddr().(*net.TCPAddr); ok {
tcp = true
}
var (
r *dns.Msg
err error
try int
)
// Use request Id for "random" nameserver selection.
nsid := int(req.Id) % len(nameservers)
dnsClient := &dns.Client{Net: "udp", ReadTimeout: 4 * time.Second, WriteTimeout: 4 * time.Second, SingleInflight: true}
if tcp {
dnsClient.Net = "tcp"
}
Redo:
switch tcp {
case false:
r, _, err = dnsClient.Exchange(req, nameservers[nsid])
case true:
r, _, err = dnsClient.Exchange(req, nameservers[nsid])
}
if err == nil {
r.Compress = true
w.WriteMsg(r)
return
}
// Seen an error, this can only mean, "server not reached", try again
// but only if we have not exausted our nameservers.
if try < len(nameservers) {
try++
nsid = (nsid + 1) % len(nameservers)
goto Redo
}
log.Printf("failure to forward request %q\n", err)
m := new(dns.Msg)
m.SetReply(req)
m.SetRcode(req, dns.RcodeServerFailure)
w.WriteMsg(m)
}
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"strings"
"github.com/miekg/dns"
"github.com/samalba/dockerclient"
)
type Config map[string]DomainConfig // { "docker.": { ... }, ".": { ... } }
type DomainConfig struct {
Type string `json:"type"` // "containers" or "forwarding"
// "type": "containers"
Socket string `json:"socket"` // "unix:///var/run/docker.sock"
// "type": "forwarding"
Nameservers []string `json:"nameservers"` // [ "8.8.8.8:53", "8.8.4.4:53" ]
}
var config Config
func main() {
configFile := "example-config.json" // TODO lol
configData, err := ioutil.ReadFile(configFile)
if err != nil {
log.Fatalf("error: unable to read config file %s: %v\n", configFile, err)
}
err = json.Unmarshal(configData, &config)
if err != nil {
log.Fatalf("error: unable to process config file data from %s: %v\n", configFile, err)
}
for domain := range config {
switch config[domain].Type {
case "containers":
// TODO there must be a better way to pass "domain" along without an anonymous function AND copied variable
dCopy := domain
dns.HandleFunc(dCopy, func(w dns.ResponseWriter, r *dns.Msg) {
handleDockerRequest(dCopy, w, r)
})
case "forwarding":
// TODO there must be a better way to pass "domain" along without an anonymous function AND copied variable
nameservers := config[domain].Nameservers
dns.HandleFunc(domain, func(w dns.ResponseWriter, r *dns.Msg) {
handleForwarding(nameservers, w, r)
})
default:
log.Printf("error: unknown domain type on %s: %s\n", domain, config[domain].Type)
continue
}
log.Printf("listening on domain: %s\n", domain)
}
go serve("tcp", ":53")
go serve("udp", ":53")
sig := make(chan os.Signal)
signal.Notify(sig)
for {
select {
case s := <-sig:
log.Fatalf("fatal: signal %s received\n", s)
}
}
}
func serve(net, addr string) {
server := &dns.Server{Addr: addr, Net: net, TsigSecret: nil}
err := server.ListenAndServe()
if err != nil {
log.Fatalf("Failed to setup the %s server: %v\n", net, err)
}
}
func handleDockerRequest(domain string, w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
defer w.WriteMsg(m)
dockerHost := config[domain].Socket
if dockerHost == "" {
dockerHost = os.Getenv("DOCKER_HOST")
}
if dockerHost == "" {
dockerHost = "unix:///var/run/docker.sock"
}
docker, err := dockerclient.NewDockerClient(dockerHost, nil)
if err != nil {
log.Printf("error: initializing Docker client: %v\n", err)
}
name := r.Question[0].Name
domainSuffix := "." + dns.Fqdn(domain)
if !strings.HasSuffix(name, domainSuffix) {
log.Printf("error: request for unknown domain %s (in %s)\n", name, domain)
return
}
containerName := name[:len(name)-len(domainSuffix)]
container, err := docker.InspectContainer(containerName)
if err != nil && strings.Contains(containerName, ".") {
// we have something like "db.app", so let's try looking up a "app/db" container (linking!)
parts := strings.Split(containerName, ".")
var linkedContainerName string
for i := range parts {
linkedContainerName += "/" + parts[len(parts)-i-1]
}
container, err = docker.InspectContainer(linkedContainerName)
}
if err != nil {
log.Printf("error: failed to lookup container %s: %v\n", containerName, err)
return
}
containerIp := container.NetworkSettings.IpAddress
if containerIp == "" {
log.Printf("error: container %s is IP-less\n", containerName)
return
}
aRecord := new(dns.A)
aRecord.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
aRecord.A = net.ParseIP(containerIp)
switch r.Question[0].Qtype {
case dns.TypeA:
m.Answer = append(m.Answer, aRecord)
case dns.TypeAAAA:
m.Extra = append(m.Extra, aRecord)
//rr := new(dns.AAAA)
//rr.Hdr = dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
//rr.AAAA = container.NetworkSettings.Ipv6AddressesAsMultipleAnswerEntries
// TODO IPv6 support (when Docker itself has such a thing...)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment