Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for swarm-mode (1.12) #20

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/bin/
/pkg/
/rawdns*
.vscode
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,64 @@ PING dns.docker (172.18.0.30) 56(84) bytes of data.
rtt min/avg/max/mdev = 0.025/0.038/0.049/0.011 ms
```

## swarm support
## swarm mode (Docker 1.12) support

`rawdns` can be used with swarm mode by creating a configuration with the `swarmmode` set to true. The `swarmnode` option enables `rawdns` to query by service name instead of container name it will return the assigned virtual ip for the service. You can at the same time filter vip, which belong to a given network (use the `networkId` for this).

Example swarm configuration:

```json
{
"swarm.": {
"type": "containers",
"socket": "unix:///var/run/docker.sock",
"swarmmode": true,
"networkId": "2zcqib9vlz6fa0gotr525dgcm",
"tlsverify": true,
"tlscacert": "/var/lib/docker/swarm/certificates/swarm-root-ca.crt",
"tlscert": "/var/lib/docker/swarm/certificates/swarm-node.crt",
"tlskey": "/var/lib/docker/swarm/certificates/swarm-node.key"
},
"example.tld.": {
"type": "containers",
"socket": "unix:///var/run/docker.sock",
"swarmmode": true,
"networkId": "2zcqib9vlz6fa0gotr525dgcm",
"tlsverify": true,
"tlscacert": "/var/lib/docker/swarm/certificates/swarm-root-ca.crt",
"tlscert": "/var/lib/docker/swarm/certificates/swarm-node.crt",
"tlskey": "/var/lib/docker/swarm/certificates/swarm-node.key"
},
".": {
"type": "forwarding",
"nameservers": [ "8.8.8.8", "8.8.4.4" ]
}
}
```

Example usage:

```shell
$ docker service create --name dns \
--publish 53:53/udp \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--mount type=bind,source=/var/lib/docker/swarm/certificates,target=/var/lib/docker/swarm/certificates \
--mount type=bind,source=/etc/rawdns/config.json,target=/etc/rawdns/config.json \
tianon/rawdns rawdns /etc/rawdns/config.json

2015/09/14 21:50:49 rawdns v1.2 (go1.4.2 on linux/amd64; gc)
2015/09/14 21:50:49 listening on domain: .
2015/09/14 21:50:49 listening on domain: swarm.
2015/09/14 21:50:49 listening on domain: example.tld.
```

> NOTE: You need to create the config.json on every swarm member or use `--constraint` to only run on machines with the configuration.

You can now retrieve the vip of the `dns` service (`docker service inspect dns`) and use it like this:

`docker service create --name service-using-dns --dns <vip> --dns-search example.tld myaccount/myservice`

## swarm (legacy) support

`rawdns` can be used with swarm by creating a configuration that provides the socket details using the `tcp://` scheme. You will also need to enable `swarmnode` by setting it to true. The `swarmnode` option enables `rawdns` to look at the `Node` section of the inspect API response for the external/host IP address.

Expand Down Expand Up @@ -147,3 +204,13 @@ root@69967c3e5179:/# ping dns.docker
PING dns.docker (172.17.0.85): 56 data bytes
64 bytes from 172.17.0.85: icmp_seq=0 ttl=64 time=0.076 ms
```


## Development / Contributing

To build run `./build-cross.sh` (on git bash when using windows).

To create a container for testing run
`docker build -t myaccount/rawdns .`
and push with
`docker push myaccount/rawdns:latest`
77 changes: 75 additions & 2 deletions src/cmd/rawdns/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (

var (
dockerApiVersions = []string{
// swarm mode was added in 1.24.
"v1.24",

// libnetwork doesn't provide "Networks" until at least API version 1.21
"v1.21",

Expand Down Expand Up @@ -56,13 +59,57 @@ type dockerContainer struct {
}
}

func dockerGetIpList(dockerHost, containerName string, tlsConfig *tls.Config, swarmNode bool) ([]net.IP, error) {
type dockerService struct {
ID string
Version struct {
Index int
}
CreatedAt string
UpdatedAt string
Spec struct {
Name string
}
Endpoint struct {
VirtualIps []struct {
NetworkID string
Addr string
}
}
}

func dockerGetIpList(dockerHost, domainPrefixOrContainerName string, tlsConfig *tls.Config, swarmNode bool, swarmMode bool, networkID string) ([]net.IP, error) {
var (
container *dockerContainer
service *dockerService
err error
)

for _, apiVersion := range dockerApiVersions {
container, err = dockerInspectContainer(dockerHost, containerName, tlsConfig, apiVersion)
if swarmMode {
service, err = dockerInspectService(dockerHost, domainPrefixOrContainerName, tlsConfig, apiVersion)
if err != nil {
continue
}

ips := []net.IP{}
for _, vip := range service.Endpoint.VirtualIps {
if networkID != "" && vip.NetworkID != networkID {
continue
}

ip, _, err := net.ParseCIDR(vip.Addr)
if err != nil {
return nil, err
}

ips = append(ips, ip)

// TODO: Maybe discover tasks and add their ips?
}

return ips, nil
}
container, err = dockerInspectContainer(dockerHost, domainPrefixOrContainerName, tlsConfig, apiVersion)
if err != nil {
continue
}
Expand Down Expand Up @@ -125,6 +172,32 @@ func dockerInspectContainer(dockerHost, containerName string, tlsConfig *tls.Con
return &ret, nil
}

func dockerInspectService(dockerHost, containerName string, tlsConfig *tls.Config, apiVersion string) (*dockerService, error) {
u, err := url.Parse(dockerHost)
if err != nil {
return nil, fmt.Errorf("failed parsing URL '%s': %v", dockerHost, err)
}
client := httpClient(u, tlsConfig)
req, err := http.NewRequest("GET", u.String()+"/"+apiVersion+"/services/"+containerName, nil)
if err != nil {
return nil, fmt.Errorf("failed creating request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed HTTP request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("not '200 OK': %v", resp.Status)
}
ret := dockerService{}
err = json.NewDecoder(resp.Body).Decode(&ret)
if err != nil {
return nil, fmt.Errorf("failed decoding JSON response: %v", err)
}
return &ret, nil
}

func httpClient(u *url.URL, tlsConfig *tls.Config) *http.Client {
transport := &http.Transport{}
transport.DisableKeepAlives = true
Expand Down
22 changes: 14 additions & 8 deletions src/cmd/rawdns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ type DomainConfig struct {
TLSKey string `json:"tlskey"`

// IP address strategy
SwarmNode bool `json:"swarmnode"`
SwarmNode bool `json:"swarmnode"`
NetworkID string `json:"networkId"` // When using swarmmode this will filter vips for a network
SwarmMode bool `json:"swarmmode"`

// "type": "forwarding"
Nameservers []string `json:"nameservers"` // [ "8.8.8.8", "8.8.4.4" ]
Expand Down Expand Up @@ -86,6 +88,10 @@ func main() {
}
}

if config[domain].SwarmMode && config[domain].SwarmNode {
log.Fatalf("invalid configuration: cannot be swarmNode and swarmMode at the same time, ignoring swarmNode")
}

dCopy := domain
dns.HandleFunc(dCopy, func(w dns.ResponseWriter, r *dns.Msg) {
handleDockerRequest(dCopy, tlsConfig, w, r)
Expand Down Expand Up @@ -190,26 +196,26 @@ func handleDockerRequest(domain string, tlsConfig *tls.Config, w dns.ResponseWri
log.Printf("error: request for unknown domain %q (in %q)\n", name, domain)
return
}
containerName := name[:len(name)-len(domainSuffix)]
domainPrefix := name[:len(name)-len(domainSuffix)]

ips, err := dockerGetIpList(config[domain].Socket, containerName, tlsConfig, config[domain].SwarmNode)
if err != nil && strings.Contains(containerName, ".") {
ips, err := dockerGetIpList(config[domain].Socket, domainPrefix, tlsConfig, config[domain].SwarmNode, config[domain].SwarmMode, config[domain].NetworkID)
if err != nil && strings.Contains(domainPrefix, ".") {
// we have something like "db.app", so let's try looking up a "app/db" container (linking!)
parts := strings.Split(containerName, ".")
parts := strings.Split(domainPrefix, ".")
var linkedContainerName string
for i := range parts {
linkedContainerName += "/" + parts[len(parts)-i-1]
}
ips, err = dockerGetIpList(config[domain].Socket, linkedContainerName, tlsConfig, config[domain].SwarmNode)
ips, err = dockerGetIpList(config[domain].Socket, linkedContainerName, tlsConfig, config[domain].SwarmNode, config[domain].SwarmMode, config[domain].NetworkID)
}
if err != nil {
m.SetRcode(r, dns.RcodeNameError)
log.Printf("error: failed to lookup container %q: %v\n", containerName, err)
log.Printf("error: failed to lookup domain prefix %q: %v\n", domainPrefix, err)
return
}

if len(ips) == 0 {
log.Printf("error: container %q is IP-less\n", containerName)
log.Printf("error: domain prefix %q is IP-less\n", domainPrefix)
return
}

Expand Down