├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── build.sh ├── circle.yml ├── config └── systemd │ └── network │ └── 99-resolvable.network ├── docker_test.go ├── dockerpool ├── daemon.go ├── nested.go └── pool.go ├── main.go ├── modules.go ├── resolver ├── extpoints.go ├── resolvconf.go ├── resolvconf_test.go ├── resolver.go ├── resolver_test.go └── types.go └── systemd └── systemd.go /.dockerignore: -------------------------------------------------------------------------------- 1 | release 2 | build 3 | Makefile 4 | .git 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | release/ 2 | build/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gliderlabs/alpine:3.3 2 | ENTRYPOINT ["/bin/resolvable"] 3 | 4 | RUN apk add --no-cache -t build-deps go git mercurial 5 | COPY ./config /config 6 | COPY . /src 7 | RUN cd /src && ./build.sh "$(cat VERSION)" 8 | 9 | ONBUILD COPY ./modules.go /src/modules.go 10 | ONBUILD RUN cd /src && ./build.sh "$(cat VERSION)-custom" 11 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM gliderlabs/alpine:3.1 2 | 3 | ENV GOPATH /go 4 | RUN apk-install go git mercurial 5 | COPY . /go/src/github.com/gliderlabs/resolvable 6 | WORKDIR /go/src/github.com/gliderlabs/resolvable 7 | RUN go get 8 | CMD go get \ 9 | && go build -ldflags "-X main.Version dev" -o /bin/resolvable \ 10 | && exec /bin/resolvable 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matthew Good 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=resolvable 2 | VERSION=$(shell cat VERSION) 3 | 4 | dev: 5 | @docker history $(NAME):dev &> /dev/null \ 6 | || docker build -f Dockerfile.dev -t $(NAME):dev . 7 | @docker run --rm \ 8 | --hostname $(NAME) \ 9 | -v $(PWD):/go/src/github.com/gliderlabs/resolvable \ 10 | -v $(PWD)/config:/config \ 11 | -v /var/run/docker.sock:/tmp/docker.sock \ 12 | -v /etc/resolv.conf:/tmp/resolv.conf \ 13 | $(NAME):dev 14 | 15 | build: 16 | mkdir -p build 17 | docker build -t $(NAME):$(VERSION) . 18 | docker save $(NAME):$(VERSION) | gzip -9 > build/$(NAME)_$(VERSION).tgz 19 | 20 | test: 21 | GOMAXPROCS=4 go test -v ./... -race 22 | 23 | release: 24 | rm -rf release && mkdir release 25 | go get github.com/progrium/gh-release/... 26 | cp build/* release 27 | gh-release create gliderlabs/$(NAME) $(VERSION) \ 28 | $(shell git rev-parse --abbrev-ref HEAD) $(VERSION) 29 | 30 | circleci: 31 | rm ~/.gitconfig 32 | ifneq ($(CIRCLE_BRANCH), release) 33 | echo build-$$CIRCLE_BUILD_NUM > VERSION 34 | endif 35 | 36 | .PHONY: build release 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resolvable - Docker DNS resolver 2 | 3 | A simple DNS server to resolve names of local Docker containers. 4 | 5 | `resolvable` is intended to run in a Docker container: 6 | 7 | docker run -d \ 8 | --hostname resolvable \ 9 | -v /var/run/docker.sock:/tmp/docker.sock \ 10 | -v /etc/resolv.conf:/tmp/resolv.conf \ 11 | mgood/resolvable 12 | 13 | The `docker.sock` is mounted to allow `resolvable` to listen for Docker events and automatically register containers. 14 | 15 | `resolvable` can insert itself into the host's `/etc/resolv.conf` file by mounting this file to `/tmp/resolv.conf` in the container. When starting, it will insert itself as the first `nameserver` in the file, and remove itself when shutting down. 16 | 17 | ## Systemd integration 18 | 19 | On systems using systemd, `resolvable` can integrate with the systemd DNS configuration. Instead of mounting `/etc/resolv.conf`, mount the systemd configuration path `/run/systemd` and the DBUS socket as follows: 20 | 21 | docker run -d \ 22 | --hostname resolvable \ 23 | -v /var/run/docker.sock:/tmp/docker.sock \ 24 | -v /run/systemd:/tmp/systemd \ 25 | -v /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket \ 26 | mgood/resolvable 27 | 28 | `resolvable` will generate a systemd network config, and then use the DBUS socket to reload `systemd-networkd` to regenerate the host's `/etc/resolv.conf`. 29 | 30 | ## Container Registration 31 | 32 | `resolvable` provides DNS entries `` and `.docker` for each container. Containers are automatically registered when they start, and removed when they die. 33 | 34 | For example, the following container would be available via DNS as `myhost` and `myname.docker`: 35 | 36 | docker run -d \ 37 | --hostname myhost \ 38 | --name myname \ 39 | mycontainer 40 | 41 | ## DNS Forwarding 42 | 43 | `resolvable` also supports forwarding DNS queries to other containers providing DNS servers. This integrates well with tools like Consul or SkyDNS that offer a DNS endpoint for service discovery. 44 | 45 | Containers configured with the `DNS_RESOLVES` environment variable are registered in `resolvable` to forward DNS queries for any domains listed. 46 | 47 | To run an example `consul` container, supporting DNS queries for the `.consul` domain on port `8600`: 48 | 49 | docker run -d \ 50 | -e DNS_RESOLVES=consul \ 51 | -e DNS_PORT=8600 \ 52 | -p 8600/udp \ 53 | consul 54 | 55 | `DNS_RESOLVES` must contain least one domain to forward to this container. Multiple values can be provided as a comma-separated list. 56 | 57 | `DNS_PORT` is optional, and defaults to `53`. 58 | 59 | ## Interface Addresses 60 | 61 | `resolvable` also provides a DNS entry for the Docker bridge interface address, usually `docker0`. This can be used to communicate with services with a known port bound to the Docker bridge. 62 | 63 | See this article on [Docker network configuration](https://docs.docker.com/articles/networking/) for additional details on the Docker bridge interface. 64 | 65 | 66 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2 -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | mkdir -p /go/src/github.com/gliderlabs 4 | cp -r /src /go/src/github.com/gliderlabs/resolvable 5 | cd /go/src/github.com/gliderlabs/resolvable 6 | export GOPATH=/go 7 | go get 8 | go build -ldflags "-X main.Version $1" -o /bin/resolvable 9 | apk del --purge build-deps 10 | rm -rf /go 11 | rm -rf /var/cache/apk/* 12 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | DESTROY_NATIVE_CONTAINERS: 1 4 | services: 5 | - docker -------------------------------------------------------------------------------- /config/systemd/network/99-resolvable.network: -------------------------------------------------------------------------------- 1 | [Match] 2 | 3 | [Network] 4 | DNS = {{.Address}} 5 | -------------------------------------------------------------------------------- /docker_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "regexp" 11 | "runtime" 12 | "testing" 13 | "time" 14 | 15 | "github.com/gliderlabs/resolvable/dockerpool" 16 | 17 | dockerapi "github.com/fsouza/go-dockerclient" 18 | ) 19 | 20 | var useNativeContainers = os.Getenv("DESTROY_NATIVE_CONTAINERS") != "" 21 | 22 | var DaemonPool dockerpool.Pool 23 | 24 | func TestMain(m *testing.M) { 25 | os.Exit(runMain(m)) 26 | } 27 | 28 | func runMain(m *testing.M) int { 29 | if err := setup(); err != nil { 30 | log.Fatal(err) 31 | } 32 | defer DaemonPool.Close() 33 | 34 | return m.Run() 35 | } 36 | 37 | func setup() error { 38 | var pool dockerpool.Pool 39 | var err error 40 | 41 | if useNativeContainers { 42 | pool, err = dockerpool.NewNativePool("gliderlabs/alpine:latest") 43 | } else { 44 | pool, err = dockerpool.NewDockerInDockerPool("gliderlabs/alpine:latest") 45 | } 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | DaemonPool = pool 52 | return nil 53 | } 54 | 55 | func TestStartupShutdown(t *testing.T) { 56 | if useNativeContainers { 57 | t.Skip("not supported with native containers, cannot shutdown the native Docker daemon") 58 | } 59 | t.Parallel() 60 | 61 | daemon, err := dockerpool.NewDockerInDockerDaemon() 62 | ok(t, err) 63 | defer daemon.Close() 64 | 65 | dns := RunDebugResolver(daemon.Client) 66 | 67 | assertNext(t, "listen", dns.ch, 10*time.Second) 68 | 69 | ok(t, daemon.Close()) 70 | assertNext(t, "close", dns.ch, 20*time.Second) 71 | } 72 | 73 | func TestAddContainerBeforeStarted(t *testing.T) { 74 | t.Parallel() 75 | 76 | daemon, err := DaemonPool.Borrow() 77 | ok(t, err) 78 | defer DaemonPool.Return(daemon) 79 | 80 | containerId, err := daemon.RunSimple("sleep", "30") 81 | ok(t, err) 82 | 83 | dns := RunDebugResolver(daemon.Client) 84 | defer dns.Cleanup() 85 | 86 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 87 | assertNextMatch(t, "add: bridge:docker0 .*", dns.ch, time.Second) 88 | assertNext(t, "listen", dns.ch, 10*time.Second) 89 | } 90 | 91 | func TestAddRemoveWhileRunning(t *testing.T) { 92 | t.Parallel() 93 | 94 | daemon, err := DaemonPool.Borrow() 95 | ok(t, err) 96 | defer DaemonPool.Return(daemon) 97 | 98 | dns := RunDebugResolver(daemon.Client) 99 | defer dns.Cleanup() 100 | 101 | assertNext(t, "listen", dns.ch, 10*time.Second) 102 | 103 | containerId, err := daemon.RunSimple("sleep", "30") 104 | ok(t, err) 105 | 106 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 107 | assertNextMatch(t, "add: bridge:docker0 .*", dns.ch, time.Second) 108 | 109 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 110 | ID: containerId, 111 | })) 112 | 113 | assertNext(t, "remove: "+containerId, dns.ch, time.Second) 114 | } 115 | 116 | func TestAddUpstreamDefaultPort(t *testing.T) { 117 | t.Parallel() 118 | 119 | daemon, err := DaemonPool.Borrow() 120 | ok(t, err) 121 | defer DaemonPool.Return(daemon) 122 | 123 | dns := RunDebugResolver(daemon.Client) 124 | defer dns.Cleanup() 125 | 126 | assertNext(t, "listen", dns.ch, 10*time.Second) 127 | 128 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 129 | Config: &dockerapi.Config{ 130 | Image: "gliderlabs/alpine", 131 | Cmd: []string{"sleep", "30"}, 132 | Env: []string{"DNS_RESOLVES=domain"}, 133 | }, 134 | }, nil) 135 | ok(t, err) 136 | 137 | container, err := daemon.Client.InspectContainer(containerId) 138 | ok(t, err) 139 | 140 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 141 | assertNext(t, 142 | fmt.Sprintf("add upstream: %v %v %v [domain]", containerId, container.NetworkSettings.IPAddress, 53), 143 | dns.ch, time.Second, 144 | ) 145 | assertNextMatch(t, "add: bridge:docker0 .*", dns.ch, time.Second) 146 | 147 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 148 | ID: containerId, 149 | })) 150 | 151 | assertNext(t, "remove: "+containerId, dns.ch, time.Second) 152 | assertNext(t, "remove upstream: "+containerId, dns.ch, time.Second) 153 | } 154 | 155 | func TestAddUpstreamEmptyDomains(t *testing.T) { 156 | t.Parallel() 157 | 158 | daemon, err := DaemonPool.Borrow() 159 | ok(t, err) 160 | defer DaemonPool.Return(daemon) 161 | 162 | dns := RunDebugResolver(daemon.Client) 163 | defer dns.Cleanup() 164 | 165 | assertNext(t, "listen", dns.ch, 10*time.Second) 166 | 167 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 168 | Config: &dockerapi.Config{ 169 | Image: "gliderlabs/alpine", 170 | Cmd: []string{"sleep", "30"}, 171 | Env: []string{"DNS_RESOLVES="}, 172 | }, 173 | }, nil) 174 | ok(t, err) 175 | 176 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 177 | select { 178 | case msg := <-dns.ch: 179 | t.Fatalf("expected no more results, got: %v", msg) 180 | case <-time.After(time.Second): 181 | } 182 | } 183 | 184 | func TestAddUpstreamEmptyPort(t *testing.T) { 185 | t.Parallel() 186 | 187 | daemon, err := DaemonPool.Borrow() 188 | ok(t, err) 189 | defer DaemonPool.Return(daemon) 190 | 191 | dns := RunDebugResolver(daemon.Client) 192 | defer dns.Cleanup() 193 | 194 | assertNext(t, "listen", dns.ch, 10*time.Second) 195 | 196 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 197 | Config: &dockerapi.Config{ 198 | Image: "gliderlabs/alpine", 199 | Cmd: []string{"sleep", "30"}, 200 | Env: []string{ 201 | "DNS_RESOLVES=domain", 202 | "DNS_PORT=", 203 | }, 204 | }, 205 | }, nil) 206 | ok(t, err) 207 | 208 | container, err := daemon.Client.InspectContainer(containerId) 209 | ok(t, err) 210 | 211 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 212 | assertNext(t, 213 | fmt.Sprintf("add upstream: %v %v %v [domain]", containerId, container.NetworkSettings.IPAddress, 53), 214 | dns.ch, time.Second, 215 | ) 216 | } 217 | 218 | func TestAddUpstreamAlternatePort(t *testing.T) { 219 | t.Parallel() 220 | 221 | daemon, err := DaemonPool.Borrow() 222 | ok(t, err) 223 | defer DaemonPool.Return(daemon) 224 | 225 | dns := RunDebugResolver(daemon.Client) 226 | defer dns.Cleanup() 227 | 228 | assertNext(t, "listen", dns.ch, 10*time.Second) 229 | 230 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 231 | Config: &dockerapi.Config{ 232 | Image: "gliderlabs/alpine", 233 | Cmd: []string{"sleep", "30"}, 234 | Env: []string{ 235 | "DNS_RESOLVES=domain", 236 | "DNS_PORT=5353", 237 | }, 238 | }, 239 | }, nil) 240 | ok(t, err) 241 | 242 | container, err := daemon.Client.InspectContainer(containerId) 243 | ok(t, err) 244 | 245 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 246 | assertNext(t, 247 | fmt.Sprintf("add upstream: %v %v %v [domain]", containerId, container.NetworkSettings.IPAddress, 5353), 248 | dns.ch, time.Second, 249 | ) 250 | } 251 | 252 | func TestAddUpstreamInvalidPort(t *testing.T) { 253 | t.Parallel() 254 | 255 | daemon, err := DaemonPool.Borrow() 256 | ok(t, err) 257 | defer DaemonPool.Return(daemon) 258 | 259 | dns := RunDebugResolver(daemon.Client) 260 | defer dns.Cleanup() 261 | 262 | assertNext(t, "listen", dns.ch, 10*time.Second) 263 | 264 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 265 | Config: &dockerapi.Config{ 266 | Image: "gliderlabs/alpine", 267 | Cmd: []string{"sleep", "30"}, 268 | Env: []string{ 269 | "DNS_RESOLVES=domain", 270 | "DNS_PORT=invalid", 271 | }, 272 | }, 273 | }, nil) 274 | ok(t, err) 275 | 276 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 277 | // XXX should it still attempt to add the bridge if there is another error? 278 | // assertNext(t, "add: bridge:docker0", dns.ch, time.Second) 279 | 280 | select { 281 | case msg := <-dns.ch: 282 | t.Fatalf("expected no more results, got: %v", msg) 283 | default: 284 | } 285 | } 286 | 287 | func TestAddUpstreamDomains(t *testing.T) { 288 | t.Parallel() 289 | 290 | daemon, err := DaemonPool.Borrow() 291 | ok(t, err) 292 | defer DaemonPool.Return(daemon) 293 | 294 | dns := RunDebugResolver(daemon.Client) 295 | defer dns.Cleanup() 296 | 297 | assertNext(t, "listen", dns.ch, 10*time.Second) 298 | 299 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 300 | Config: &dockerapi.Config{ 301 | Image: "gliderlabs/alpine", 302 | Cmd: []string{"sleep", "30"}, 303 | Env: []string{ 304 | "DNS_RESOLVES=domain,another.domain", 305 | "DNS_PORT=5353", 306 | }, 307 | }, 308 | }, nil) 309 | ok(t, err) 310 | 311 | container, err := daemon.Client.InspectContainer(containerId) 312 | ok(t, err) 313 | 314 | assertNextAdd(t, daemon.Client, containerId, dns.ch, time.Second) 315 | assertNext(t, 316 | fmt.Sprintf("add upstream: %v %v %v [domain another.domain]", containerId, container.NetworkSettings.IPAddress, 5353), 317 | dns.ch, time.Second, 318 | ) 319 | } 320 | 321 | func TestAddNetHostMode_IgnoredByDefault(t *testing.T) { 322 | t.Parallel() 323 | 324 | daemon, err := DaemonPool.Borrow() 325 | ok(t, err) 326 | defer DaemonPool.Return(daemon) 327 | 328 | dns := RunDebugResolver(daemon.Client) 329 | defer dns.Cleanup() 330 | 331 | assertNext(t, "listen", dns.ch, 10*time.Second) 332 | 333 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 334 | Config: &dockerapi.Config{ 335 | Image: "gliderlabs/alpine", 336 | Cmd: []string{"sleep", "30"}, 337 | }, 338 | HostConfig: &dockerapi.HostConfig{ 339 | NetworkMode: "host", 340 | }, 341 | }, nil) 342 | ok(t, err) 343 | 344 | select { 345 | case msg := <-dns.ch: 346 | t.Fatalf("expected no more results, got: %v", msg) 347 | case <-time.After(time.Second): 348 | } 349 | 350 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 351 | ID: containerId, 352 | })) 353 | 354 | assertNext(t, "remove: "+containerId, dns.ch, time.Second) 355 | } 356 | 357 | func TestAddNetHostMode_HostIPSet(t *testing.T) { 358 | t.Parallel() 359 | 360 | daemon, err := DaemonPool.Borrow() 361 | ok(t, err) 362 | defer DaemonPool.Return(daemon) 363 | 364 | hostIP := net.ParseIP("192.168.42.42") 365 | 366 | dns := NewDebugResolver(daemon.Client) 367 | dns.hostIP = hostIP 368 | go dns.Run() 369 | defer dns.Cleanup() 370 | 371 | assertNext(t, "listen", dns.ch, 10*time.Second) 372 | 373 | containerId, err := daemon.Run(dockerapi.CreateContainerOptions{ 374 | Config: &dockerapi.Config{ 375 | Image: "gliderlabs/alpine", 376 | Cmd: []string{"sleep", "30"}, 377 | }, 378 | HostConfig: &dockerapi.HostConfig{ 379 | NetworkMode: "host", 380 | }, 381 | }, nil) 382 | ok(t, err) 383 | 384 | assertNext(t, fmt.Sprintf("add: %v 192.168.42.42", containerId), dns.ch, time.Second) 385 | 386 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 387 | ID: containerId, 388 | })) 389 | 390 | assertNext(t, "remove: "+containerId, dns.ch, time.Second) 391 | } 392 | 393 | func TestAddNetContainerMode(t *testing.T) { 394 | t.Parallel() 395 | 396 | daemon, err := DaemonPool.Borrow() 397 | ok(t, err) 398 | defer DaemonPool.Return(daemon) 399 | 400 | dns := RunDebugResolver(daemon.Client) 401 | defer dns.Cleanup() 402 | 403 | assertNext(t, "listen", dns.ch, 10*time.Second) 404 | 405 | containerId1, err := daemon.RunSimple("sleep", "30") 406 | ok(t, err) 407 | 408 | addr, err := containerAddress(daemon.Client, containerId1) 409 | assertNext(t, fmt.Sprintf("add: %v %v", containerId1, addr), dns.ch, time.Second) 410 | assertNextMatch(t, "add: bridge:docker0 .*", dns.ch, time.Second) 411 | 412 | // reference container by id 413 | containerId2, err := daemon.Run(dockerapi.CreateContainerOptions{ 414 | Config: &dockerapi.Config{ 415 | Image: "gliderlabs/alpine", 416 | Cmd: []string{"sleep", "30"}, 417 | }, 418 | HostConfig: &dockerapi.HostConfig{ 419 | NetworkMode: "container:" + containerId1, 420 | }, 421 | }, nil) 422 | 423 | assertNext(t, fmt.Sprintf("add: %v %v", containerId2, addr), dns.ch, time.Second) 424 | 425 | container2, err := daemon.Client.InspectContainer(containerId2) 426 | ok(t, err) 427 | 428 | // recursive reference 429 | // reference via container name 430 | containerId3, err := daemon.Run(dockerapi.CreateContainerOptions{ 431 | Config: &dockerapi.Config{ 432 | Image: "gliderlabs/alpine", 433 | Cmd: []string{"sleep", "30"}, 434 | }, 435 | HostConfig: &dockerapi.HostConfig{ 436 | NetworkMode: "container:" + container2.Name, 437 | }, 438 | }, nil) 439 | 440 | assertNext(t, fmt.Sprintf("add: %v %v", containerId3, addr), dns.ch, time.Second) 441 | 442 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 443 | ID: containerId1, 444 | })) 445 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 446 | ID: containerId2, 447 | })) 448 | ok(t, daemon.Client.KillContainer(dockerapi.KillContainerOptions{ 449 | ID: containerId3, 450 | })) 451 | 452 | assertNext(t, "remove: "+containerId1, dns.ch, time.Second) 453 | assertNext(t, "remove: "+containerId2, dns.ch, time.Second) 454 | assertNext(t, "remove: "+containerId3, dns.ch, time.Second) 455 | } 456 | 457 | func containerAddress(client *dockerapi.Client, containerId string) (string, error) { 458 | container, err := client.InspectContainer(containerId) 459 | if err != nil { 460 | return "", err 461 | } 462 | return container.NetworkSettings.IPAddress, nil 463 | } 464 | 465 | func assertNextAdd(tb testing.TB, client *dockerapi.Client, containerId string, ch chan string, timeout time.Duration) { 466 | addr, err := containerAddress(client, containerId) 467 | ok(tb, err) 468 | 469 | assertNextMatch(tb, regexp.QuoteMeta(fmt.Sprintf("add: %v %v", containerId, addr)), ch, time.Second) 470 | } 471 | 472 | func assertNext(tb testing.TB, exp string, ch chan string, timeout time.Duration) { 473 | select { 474 | case act := <-ch: 475 | equals(tb, exp, act) 476 | case <-time.After(timeout): 477 | _, file, line, _ := runtime.Caller(1) 478 | fmt.Printf("\033[31m%s:%d: timed out after %v, exp: %s\033[39m\n\n", filepath.Base(file), line, timeout, exp) 479 | tb.FailNow() 480 | } 481 | } 482 | 483 | func assertNextMatch(tb testing.TB, exp string, ch chan string, timeout time.Duration) { 484 | select { 485 | case act := <-ch: 486 | matches, err := regexp.MatchString("^"+exp+"$", act) 487 | ok(tb, err) 488 | 489 | if matches { 490 | return 491 | } 492 | 493 | _, file, line, _ := runtime.Caller(1) 494 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 495 | tb.FailNow() 496 | 497 | case <-time.After(timeout): 498 | _, file, line, _ := runtime.Caller(1) 499 | fmt.Printf("\033[31m%s:%d: timed out after %v, exp: %s\033[39m\n\n", filepath.Base(file), line, timeout, exp) 500 | tb.FailNow() 501 | } 502 | } 503 | 504 | // TODO add a test for when the container doesn't start up right, 505 | // the IP will be nil, since the container aborted, so we shouldn't try to add it at all 506 | 507 | //////////////////////////////////////////////////////////////////////////////// 508 | // Helpers 509 | //////////////////////////////////////////////////////////////////////////////// 510 | 511 | // ok fails the test if an err is not nil. 512 | func ok(tb testing.TB, err error) { 513 | if err != nil { 514 | _, file, line, _ := runtime.Caller(1) 515 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 516 | tb.FailNow() 517 | } 518 | } 519 | 520 | // equals fails the test if exp is not equal to act. 521 | func equals(tb testing.TB, exp, act interface{}) { 522 | if !reflect.DeepEqual(exp, act) { 523 | _, file, line, _ := runtime.Caller(1) 524 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 525 | tb.FailNow() 526 | } 527 | } 528 | 529 | type DebugResolver struct { 530 | ch chan string 531 | client *dockerapi.Client 532 | events chan *dockerapi.APIEvents 533 | hostIP net.IP 534 | } 535 | 536 | func RunDebugResolver(client *dockerapi.Client) *DebugResolver { 537 | dns := NewDebugResolver(client) 538 | go dns.Run() 539 | return dns 540 | } 541 | 542 | func NewDebugResolver(client *dockerapi.Client) *DebugResolver { 543 | events := make(chan *dockerapi.APIEvents) 544 | return &DebugResolver{make(chan string), client, events, nil} 545 | } 546 | 547 | func (r *DebugResolver) Run() { 548 | registerContainers(r.client, r.events, r, "docker", r.hostIP) 549 | } 550 | 551 | func (r *DebugResolver) Cleanup() { 552 | r.client.RemoveEventListener(r.events) 553 | close(r.events) 554 | } 555 | 556 | func (r *DebugResolver) AddHost(id string, addr net.IP, name string, aliases ...string) error { 557 | // r.ch <- fmt.Sprintf("add: %v %v %v %v", id, addr, name, aliases) 558 | r.ch <- fmt.Sprintf("add: %v %v", id, addr) 559 | return nil 560 | } 561 | 562 | func (r *DebugResolver) RemoveHost(id string) error { 563 | r.ch <- fmt.Sprintf("remove: %v", id) 564 | return nil 565 | } 566 | 567 | func (r *DebugResolver) AddUpstream(id string, addr net.IP, port int, domains ...string) error { 568 | r.ch <- fmt.Sprintf("add upstream: %v %v %v %v", id, addr, port, domains) 569 | return nil 570 | } 571 | 572 | func (r *DebugResolver) RemoveUpstream(id string) error { 573 | r.ch <- fmt.Sprintf("remove upstream: %v", id) 574 | return nil 575 | } 576 | 577 | func (r *DebugResolver) Listen() error { 578 | r.ch <- "listen" 579 | return nil 580 | } 581 | 582 | func (r *DebugResolver) Close() { 583 | r.ch <- "close" 584 | } 585 | -------------------------------------------------------------------------------- /dockerpool/daemon.go: -------------------------------------------------------------------------------- 1 | package dockerpool 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/joeshaw/multierror" 9 | 10 | dockerapi "github.com/fsouza/go-dockerclient" 11 | ) 12 | 13 | type DockerDaemon struct { 14 | Client *dockerapi.Client 15 | Close func() error 16 | } 17 | 18 | func getopt(name, def string) string { 19 | if env := os.Getenv(name); env != "" { 20 | return env 21 | } 22 | return def 23 | } 24 | 25 | func clientFromEnv() (client *dockerapi.Client, endpointUrl *url.URL, err error) { 26 | endpoint := getopt("DOCKER_HOST", "unix:///var/run/docker.sock") 27 | endpointUrl, err = url.Parse(endpoint) 28 | if err != nil { 29 | return 30 | } 31 | 32 | if os.Getenv("DOCKER_TLS_VERIFY") == "" { 33 | client, err = dockerapi.NewClient(endpoint) 34 | } else { 35 | certPath := os.Getenv("DOCKER_CERT_PATH") 36 | client, err = dockerapi.NewTLSClient(endpoint, 37 | filepath.Join(certPath, "cert.pem"), 38 | filepath.Join(certPath, "key.pem"), 39 | filepath.Join(certPath, "ca.pem"), 40 | ) 41 | } 42 | 43 | return 44 | } 45 | 46 | func NewNativeDockerDaemon() (*DockerDaemon, error) { 47 | client, _, err := clientFromEnv() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | daemon := &DockerDaemon{Client: client} 53 | daemon.Close = daemon.KillAllContainers 54 | return daemon, nil 55 | } 56 | 57 | func (d *DockerDaemon) RunSimple(cmd ...string) (string, error) { 58 | return d.Run(dockerapi.CreateContainerOptions{ 59 | Config: &dockerapi.Config{ 60 | Image: "gliderlabs/alpine", 61 | Cmd: cmd, 62 | }, 63 | }, nil) 64 | } 65 | 66 | func (d *DockerDaemon) Run(createOpts dockerapi.CreateContainerOptions, startConfig *dockerapi.HostConfig) (string, error) { 67 | return runContainer(d.Client, createOpts, startConfig) 68 | } 69 | 70 | func (d *DockerDaemon) KillAllContainers() error { 71 | containers, err := d.Client.ListContainers(dockerapi.ListContainersOptions{}) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | var errs multierror.Errors 77 | 78 | for _, container := range containers { 79 | err = d.Client.KillContainer(dockerapi.KillContainerOptions{ 80 | ID: container.ID, 81 | }) 82 | if err != nil { 83 | errs = append(errs, err) 84 | } 85 | } 86 | 87 | return errs.Err() 88 | } 89 | 90 | func runContainer(client *dockerapi.Client, createOpts dockerapi.CreateContainerOptions, startConfig *dockerapi.HostConfig) (string, error) { 91 | container, err := client.CreateContainer(createOpts) 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | err = client.StartContainer(container.ID, startConfig) 97 | // return container ID even if there is an error, so caller can clean up container if desired 98 | return container.ID, err 99 | } 100 | -------------------------------------------------------------------------------- /dockerpool/nested.go: -------------------------------------------------------------------------------- 1 | package dockerpool 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/cenkalti/backoff" 10 | dockerapi "github.com/fsouza/go-dockerclient" 11 | ) 12 | 13 | type DockerInDocker struct { 14 | client *dockerapi.Client 15 | containerId string 16 | } 17 | 18 | type ClientInit func(*dockerapi.Client) error 19 | 20 | func NewDockerInDockerDaemon() (*DockerDaemon, error) { 21 | rootClient, endpoint, err := clientFromEnv() 22 | if err != nil { 23 | return nil, err 24 | } 25 | return newDockerInDockerDaemon(rootClient, endpoint, pingClient) 26 | } 27 | 28 | var pingClient ClientInit = func(client *dockerapi.Client) error { 29 | return client.Ping() 30 | } 31 | 32 | func newDockerInDockerDaemon(rootClient *dockerapi.Client, endpoint *url.URL, clientInit ClientInit) (*DockerDaemon, error) { 33 | var err error 34 | 35 | dockerInDocker := &DockerInDocker{ 36 | client: rootClient, 37 | } 38 | daemon := &DockerDaemon{ 39 | Close: dockerInDocker.Close, 40 | } 41 | defer func() { 42 | // if there is an error, client will not be set, so clean up 43 | if daemon.Client == nil { 44 | daemon.Close() 45 | } 46 | }() 47 | 48 | port := dockerapi.Port("4444/tcp") 49 | 50 | dockerInDocker.containerId, err = runContainer(rootClient, 51 | dockerapi.CreateContainerOptions{ 52 | Config: &dockerapi.Config{ 53 | Image: "jpetazzo/dind", 54 | Env: []string{"PORT=" + port.Port()}, 55 | ExposedPorts: map[dockerapi.Port]struct{}{port: {}}, 56 | }, 57 | }, &dockerapi.HostConfig{ 58 | Privileged: true, 59 | PublishAllPorts: true, 60 | }, 61 | ) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | container, err := rootClient.InspectContainer(dockerInDocker.containerId) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | var hostAddr, hostPort string 72 | 73 | if endpoint.Scheme == "unix" { 74 | hostAddr = container.NetworkSettings.IPAddress 75 | hostPort = port.Port() 76 | } else { 77 | portBinding := container.NetworkSettings.Ports[port][0] 78 | hostAddr, _, err = net.SplitHostPort(endpoint.Host) 79 | if err != nil { 80 | return nil, err 81 | } 82 | hostPort = portBinding.HostPort 83 | } 84 | 85 | dindEndpoint := fmt.Sprintf("tcp://%v:%v", hostAddr, hostPort) 86 | client, err := dockerapi.NewClient(dindEndpoint) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | b := backoff.NewExponentialBackOff() 92 | // retry a bit faster than the defaults 93 | b.InitialInterval = time.Second / 10 94 | b.Multiplier = 1.1 95 | b.RandomizationFactor = 0.2 96 | // don't need to wait a full minute to timeout 97 | b.MaxElapsedTime = 30 * time.Second 98 | 99 | if err = backoff.Retry(func() error { return clientInit(client) }, b); err != nil { 100 | return nil, err 101 | } 102 | 103 | daemon.Client = client 104 | return daemon, err 105 | } 106 | 107 | func (d *DockerInDocker) Close() error { 108 | if d.containerId == "" { 109 | return nil 110 | } 111 | return d.client.RemoveContainer(dockerapi.RemoveContainerOptions{ 112 | ID: d.containerId, 113 | RemoveVolumes: true, 114 | Force: true, 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /dockerpool/pool.go: -------------------------------------------------------------------------------- 1 | package dockerpool 2 | 3 | import ( 4 | "bytes" 5 | "net/url" 6 | 7 | dockerapi "github.com/fsouza/go-dockerclient" 8 | 9 | "runtime" 10 | ) 11 | 12 | type Pool interface { 13 | Borrow() (*DockerDaemon, error) 14 | Return(*DockerDaemon) 15 | Close() 16 | } 17 | 18 | type BasePool struct { 19 | pool chan *DockerDaemon 20 | } 21 | 22 | func (p *BasePool) Return(d *DockerDaemon) { 23 | select { 24 | case p.pool <- d: 25 | default: 26 | d.Close() 27 | } 28 | } 29 | 30 | func (p *BasePool) Close() { 31 | close(p.pool) 32 | for d := range p.pool { 33 | d.Close() 34 | } 35 | } 36 | 37 | type NativePool struct { 38 | BasePool 39 | } 40 | 41 | func NewNativePool(preloadImages ...string) (*NativePool, error) { 42 | daemon, err := NewNativeDockerDaemon() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if err = pullImages(daemon.Client, preloadImages); err != nil { 48 | return nil, err 49 | } 50 | 51 | pool := &NativePool{BasePool{make(chan *DockerDaemon, 1)}} 52 | pool.Return(daemon) 53 | return pool, nil 54 | } 55 | 56 | func (p *NativePool) Borrow() (*DockerDaemon, error) { 57 | d := <-p.pool 58 | d.KillAllContainers() 59 | return d, nil 60 | } 61 | 62 | type DockerInDockerPool struct { 63 | BasePool 64 | client *dockerapi.Client 65 | endpoint *url.URL 66 | preloadImages [][]byte 67 | } 68 | 69 | func NewDockerInDockerPool(preloadImages ...string) (*DockerInDockerPool, error) { 70 | client, endpoint, err := clientFromEnv() 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if err = pullImages(client, append(preloadImages, "jpetazzo/dind")); err != nil { 76 | return nil, err 77 | } 78 | 79 | imageData := make([][]byte, 0, len(preloadImages)) 80 | var buf bytes.Buffer 81 | 82 | for _, image := range preloadImages { 83 | err = client.ExportImage(dockerapi.ExportImageOptions{ 84 | Name: image, 85 | OutputStream: &buf, 86 | }) 87 | if err != nil { 88 | return nil, err 89 | } 90 | imageData = append(imageData, buf.Bytes()) 91 | buf.Reset() 92 | } 93 | 94 | pool := make(chan *DockerDaemon, runtime.GOMAXPROCS(-1)) 95 | return &DockerInDockerPool{BasePool{pool}, client, endpoint, imageData}, nil 96 | } 97 | 98 | func (p *DockerInDockerPool) clientInit(client *dockerapi.Client) error { 99 | if len(p.preloadImages) == 0 { 100 | return client.Ping() 101 | } 102 | 103 | for _, imageData := range p.preloadImages { 104 | err := client.LoadImage(dockerapi.LoadImageOptions{ 105 | InputStream: bytes.NewReader(imageData), 106 | }) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | func (p *DockerInDockerPool) Borrow() (*DockerDaemon, error) { 115 | select { 116 | case d := <-p.pool: 117 | d.KillAllContainers() 118 | return d, nil 119 | default: 120 | return newDockerInDockerDaemon(p.client, p.endpoint, p.clientInit) 121 | } 122 | } 123 | 124 | func pullImages(client *dockerapi.Client, images []string) error { 125 | for _, image := range images { 126 | err := client.PullImage(dockerapi.PullImageOptions{ 127 | Repository: image, 128 | }, dockerapi.AuthConfiguration{}) 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/miekg/dns" 15 | 16 | "github.com/gliderlabs/resolvable/resolver" 17 | 18 | dockerapi "github.com/fsouza/go-dockerclient" 19 | ) 20 | 21 | var Version string 22 | 23 | func getopt(name, def string) string { 24 | if env := os.Getenv(name); env != "" { 25 | return env 26 | } 27 | return def 28 | } 29 | 30 | func ipAddress() (string, error) { 31 | addrs, err := net.InterfaceAddrs() 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | for _, address := range addrs { 37 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsMulticast() { 38 | if ipv4 := ipnet.IP.To4(); ipv4 != nil { 39 | return ipv4.String(), nil 40 | } 41 | } 42 | } 43 | 44 | return "", errors.New("no addresses found") 45 | } 46 | 47 | func parseContainerEnv(containerEnv []string, prefix string) map[string]string { 48 | parsed := make(map[string]string) 49 | 50 | for _, env := range containerEnv { 51 | if !strings.HasPrefix(env, prefix) { 52 | continue 53 | } 54 | keyVal := strings.SplitN(env, "=", 2) 55 | if len(keyVal) > 1 { 56 | parsed[keyVal[0]] = keyVal[1] 57 | } else { 58 | parsed[keyVal[0]] = "" 59 | } 60 | } 61 | 62 | return parsed 63 | } 64 | 65 | func registerContainers(docker *dockerapi.Client, events chan *dockerapi.APIEvents, dns resolver.Resolver, containerDomain string, hostIP net.IP) error { 66 | // TODO add an options struct instead of passing all as parameters 67 | // though passing the events channel from an options struct was triggering 68 | // data race warnings within AddEventListener, so needs more investigation 69 | 70 | if events == nil { 71 | events = make(chan *dockerapi.APIEvents) 72 | } 73 | if err := docker.AddEventListener(events); err != nil { 74 | return err 75 | } 76 | 77 | if !strings.HasPrefix(containerDomain, ".") { 78 | containerDomain = "." + containerDomain 79 | } 80 | 81 | getAddress := func(container *dockerapi.Container) (net.IP, error) { 82 | for { 83 | if container.NetworkSettings.IPAddress != "" { 84 | return net.ParseIP(container.NetworkSettings.IPAddress), nil 85 | } 86 | 87 | if container.HostConfig.NetworkMode == "host" { 88 | if hostIP == nil { 89 | return nil, errors.New("IP not available with network mode \"host\"") 90 | } else { 91 | return hostIP, nil 92 | } 93 | } 94 | 95 | if strings.HasPrefix(container.HostConfig.NetworkMode, "container:") { 96 | otherId := container.HostConfig.NetworkMode[len("container:"):] 97 | var err error 98 | container, err = docker.InspectContainer(otherId) 99 | if err != nil { 100 | return nil, err 101 | } 102 | continue 103 | } 104 | 105 | return nil, fmt.Errorf("unknown network mode", container.HostConfig.NetworkMode) 106 | } 107 | } 108 | 109 | addContainer := func(containerId string) error { 110 | container, err := docker.InspectContainer(containerId) 111 | if err != nil { 112 | return err 113 | } 114 | addr, err := getAddress(container) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | err = dns.AddHost(containerId, addr, container.Config.Hostname, container.Name[1:]+containerDomain) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | env := parseContainerEnv(container.Config.Env, "DNS_") 125 | if dnsDomains, ok := env["DNS_RESOLVES"]; ok { 126 | if dnsDomains == "" { 127 | return errors.New("empty DNS_RESOLVES, should contain a comma-separated list with at least one domain") 128 | } 129 | 130 | port := 53 131 | if portString := env["DNS_PORT"]; portString != "" { 132 | port, err = strconv.Atoi(portString) 133 | if err != nil { 134 | return errors.New("invalid DNS_PORT \"" + portString + "\", should contain a number") 135 | } 136 | } 137 | 138 | domains := strings.Split(dnsDomains, ",") 139 | err = dns.AddUpstream(containerId, addr, port, domains...) 140 | if err != nil { 141 | return err 142 | } 143 | } 144 | 145 | if bridge := container.NetworkSettings.Bridge; bridge != "" { 146 | bridgeAddr := net.ParseIP(container.NetworkSettings.Gateway) 147 | err = dns.AddHost("bridge:"+bridge, bridgeAddr, bridge) 148 | if err != nil { 149 | return err 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | 156 | containers, err := docker.ListContainers(dockerapi.ListContainersOptions{}) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | for _, listing := range containers { 162 | if err := addContainer(listing.ID); err != nil { 163 | log.Printf("error adding container %s: %s\n", listing.ID[:12], err) 164 | } 165 | } 166 | 167 | if err = dns.Listen(); err != nil { 168 | return err 169 | } 170 | defer dns.Close() 171 | 172 | for msg := range events { 173 | go func(msg *dockerapi.APIEvents) { 174 | switch msg.Status { 175 | case "start": 176 | if err := addContainer(msg.ID); err != nil { 177 | log.Printf("error adding container %s: %s\n", msg.ID[:12], err) 178 | } 179 | case "die": 180 | dns.RemoveHost(msg.ID) 181 | dns.RemoveUpstream(msg.ID) 182 | } 183 | }(msg) 184 | } 185 | 186 | return errors.New("docker event loop closed") 187 | } 188 | 189 | func run() error { 190 | // set up the signal handler first to ensure cleanup is handled if a signal is 191 | // caught while initializing 192 | exitReason := make(chan error) 193 | go func() { 194 | c := make(chan os.Signal, 1) 195 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 196 | sig := <-c 197 | log.Println("exit requested by signal:", sig) 198 | exitReason <- nil 199 | }() 200 | 201 | docker, err := dockerapi.NewClient(getopt("DOCKER_HOST", "unix:///tmp/docker.sock")) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | address, err := ipAddress() 207 | if err != nil { 208 | return err 209 | } 210 | log.Println("got local address:", address) 211 | 212 | for name, conf := range resolver.HostResolverConfigs.All() { 213 | err := conf.StoreAddress(address) 214 | if err != nil { 215 | log.Printf("[ERROR] error in %s: %s", name, err) 216 | } 217 | defer conf.Clean() 218 | } 219 | 220 | var hostIP net.IP 221 | if envHostIP := os.Getenv("HOST_IP"); envHostIP != "" { 222 | hostIP = net.ParseIP(envHostIP) 223 | log.Println("using address for --net=host:", hostIP) 224 | } 225 | 226 | dnsResolver, err := resolver.NewResolver() 227 | if err != nil { 228 | return err 229 | } 230 | defer dnsResolver.Close() 231 | 232 | localDomain := "docker" 233 | dnsResolver.AddUpstream(localDomain, nil, 0, localDomain) 234 | 235 | resolvConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf") 236 | if err != nil { 237 | return err 238 | } 239 | resolvConfigPort, err := strconv.Atoi(resolvConfig.Port) 240 | if err != nil { 241 | return err 242 | } 243 | for _, server := range resolvConfig.Servers { 244 | if server != address { 245 | dnsResolver.AddUpstream("resolv.conf:"+server, net.ParseIP(server), resolvConfigPort) 246 | } 247 | } 248 | 249 | go func() { 250 | dnsResolver.Wait() 251 | exitReason <- errors.New("dns resolver exited") 252 | }() 253 | go func() { 254 | exitReason <- registerContainers(docker, nil, dnsResolver, localDomain, hostIP) 255 | }() 256 | 257 | return <-exitReason 258 | } 259 | 260 | func main() { 261 | if len(os.Args) == 2 && os.Args[1] == "--version" { 262 | fmt.Println(Version) 263 | os.Exit(0) 264 | } 265 | log.Printf("Starting resolvable %s ...", Version) 266 | 267 | err := run() 268 | if err != nil { 269 | log.Fatal("resolvable: ", err) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /modules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/gliderlabs/resolvable/resolver" 5 | _ "github.com/gliderlabs/resolvable/systemd" 6 | ) 7 | -------------------------------------------------------------------------------- /resolver/extpoints.go: -------------------------------------------------------------------------------- 1 | // generated by go-extpoints -- DO NOT EDIT 2 | package resolver 3 | 4 | import ( 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | var extRegistry = ®istryType{m: make(map[string]*extensionPoint)} 12 | 13 | type registryType struct { 14 | sync.Mutex 15 | m map[string]*extensionPoint 16 | } 17 | 18 | // Top level registration 19 | 20 | func extensionTypes(extension interface{}) []string { 21 | var ifaces []string 22 | typ := reflect.TypeOf(extension) 23 | for name, ep := range extRegistry.m { 24 | if ep.iface.Kind() == reflect.Func && typ.AssignableTo(ep.iface) { 25 | ifaces = append(ifaces, name) 26 | } 27 | if ep.iface.Kind() != reflect.Func && typ.Implements(ep.iface) { 28 | ifaces = append(ifaces, name) 29 | } 30 | } 31 | return ifaces 32 | } 33 | 34 | func RegisterExtension(extension interface{}, name string) []string { 35 | extRegistry.Lock() 36 | defer extRegistry.Unlock() 37 | var ifaces []string 38 | for _, iface := range extensionTypes(extension) { 39 | if extRegistry.m[iface].register(extension, name) { 40 | ifaces = append(ifaces, iface) 41 | } 42 | } 43 | return ifaces 44 | } 45 | 46 | func UnregisterExtension(name string) []string { 47 | extRegistry.Lock() 48 | defer extRegistry.Unlock() 49 | var ifaces []string 50 | for iface, extpoint := range extRegistry.m { 51 | if extpoint.unregister(name) { 52 | ifaces = append(ifaces, iface) 53 | } 54 | } 55 | return ifaces 56 | } 57 | 58 | 59 | // Base extension point 60 | 61 | type extensionPoint struct { 62 | sync.Mutex 63 | iface reflect.Type 64 | extensions map[string]interface{} 65 | } 66 | 67 | func newExtensionPoint(iface interface{}) *extensionPoint { 68 | ep := &extensionPoint{ 69 | iface: reflect.TypeOf(iface).Elem(), 70 | extensions: make(map[string]interface{}), 71 | } 72 | extRegistry.Lock() 73 | extRegistry.m[ep.iface.Name()] = ep 74 | extRegistry.Unlock() 75 | return ep 76 | } 77 | 78 | func (ep *extensionPoint) lookup(name string) interface{} { 79 | ep.Lock() 80 | defer ep.Unlock() 81 | ext, ok := ep.extensions[name] 82 | if !ok { 83 | return nil 84 | } 85 | return ext 86 | } 87 | 88 | func (ep *extensionPoint) all() map[string]interface{} { 89 | ep.Lock() 90 | defer ep.Unlock() 91 | all := make(map[string]interface{}) 92 | for k, v := range ep.extensions { 93 | all[k] = v 94 | } 95 | return all 96 | } 97 | 98 | func (ep *extensionPoint) register(extension interface{}, name string) bool { 99 | ep.Lock() 100 | defer ep.Unlock() 101 | if name == "" { 102 | typ := reflect.TypeOf(extension) 103 | if typ.Kind() == reflect.Func { 104 | nameParts := strings.Split(runtime.FuncForPC( 105 | reflect.ValueOf(extension).Pointer()).Name(), ".") 106 | name = nameParts[len(nameParts)-1] 107 | } else { 108 | name = typ.Elem().Name() 109 | } 110 | } 111 | _, exists := ep.extensions[name] 112 | if exists { 113 | return false 114 | } 115 | ep.extensions[name] = extension 116 | return true 117 | } 118 | 119 | func (ep *extensionPoint) unregister(name string) bool { 120 | ep.Lock() 121 | defer ep.Unlock() 122 | _, exists := ep.extensions[name] 123 | if !exists { 124 | return false 125 | } 126 | delete(ep.extensions, name) 127 | return true 128 | } 129 | 130 | // HostResolverConfig 131 | 132 | var HostResolverConfigs = &hostResolverConfigExt{ 133 | newExtensionPoint(new(HostResolverConfig)), 134 | } 135 | 136 | type hostResolverConfigExt struct { 137 | *extensionPoint 138 | } 139 | 140 | func (ep *hostResolverConfigExt) Unregister(name string) bool { 141 | return ep.unregister(name) 142 | } 143 | 144 | func (ep *hostResolverConfigExt) Register(extension HostResolverConfig, name string) bool { 145 | return ep.register(extension, name) 146 | } 147 | 148 | func (ep *hostResolverConfigExt) Lookup(name string) HostResolverConfig { 149 | ext := ep.lookup(name) 150 | if ext == nil { 151 | return nil 152 | } 153 | return ext.(HostResolverConfig) 154 | } 155 | 156 | func (ep *hostResolverConfigExt) Select(names []string) []HostResolverConfig { 157 | var selected []HostResolverConfig 158 | for _, name := range names { 159 | selected = append(selected, ep.Lookup(name)) 160 | } 161 | return selected 162 | } 163 | 164 | func (ep *hostResolverConfigExt) All() map[string]HostResolverConfig { 165 | all := make(map[string]HostResolverConfig) 166 | for k, v := range ep.all() { 167 | all[k] = v.(HostResolverConfig) 168 | } 169 | return all 170 | } 171 | 172 | func (ep *hostResolverConfigExt) Names() []string { 173 | var names []string 174 | for k := range ep.all() { 175 | names = append(names, k) 176 | } 177 | return names 178 | } 179 | 180 | 181 | -------------------------------------------------------------------------------- /resolver/resolvconf.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | const RESOLVCONF_COMMENT = "# added by resolvable" 13 | 14 | var resolvConfPattern = regexp.MustCompile("(?m:^.*" + regexp.QuoteMeta(RESOLVCONF_COMMENT) + ")(?:$|\n)") 15 | 16 | type ResolvConf struct { 17 | path string 18 | } 19 | 20 | func init() { 21 | resolveConf := getopt("RESOLV_CONF", "/tmp/resolv.conf") 22 | HostResolverConfigs.Register(&ResolvConf{resolveConf}, "resolvconf") 23 | } 24 | 25 | func (r *ResolvConf) StoreAddress(address string) error { 26 | resolveConfEntry := fmt.Sprintf("nameserver %s %s\n", address, RESOLVCONF_COMMENT) 27 | return updateResolvConf(resolveConfEntry, r.path) 28 | } 29 | 30 | func (r *ResolvConf) Clean() { 31 | updateResolvConf("", r.path) 32 | } 33 | 34 | func getopt(name, def string) string { 35 | if env := os.Getenv(name); env != "" { 36 | return env 37 | } 38 | return def 39 | } 40 | 41 | func updateResolvConf(insert, path string) error { 42 | log.Println("updating resolv.conf:", path) 43 | 44 | f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) 45 | if err != nil { 46 | return err 47 | } 48 | defer f.Close() 49 | 50 | orig, err := ioutil.ReadAll(f) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | orig = resolvConfPattern.ReplaceAllLiteral(orig, []byte{}) 56 | 57 | if _, err = f.Seek(0, os.SEEK_SET); err != nil { 58 | return err 59 | } 60 | 61 | if _, err = f.WriteString(insert); err != nil { 62 | return err 63 | } 64 | 65 | lines := strings.SplitAfter(string(orig), "\n") 66 | for _, line := range lines { 67 | // if file ends in a newline, skip empty string from splitting 68 | if line == "" { 69 | continue 70 | } 71 | if insert == "" { 72 | line = strings.TrimLeft(line, "# ") 73 | } else { 74 | line = "# " + line 75 | } 76 | if _, err = f.WriteString(line); err != nil { 77 | return err 78 | } 79 | } 80 | 81 | // contents may have been shortened, so truncate where we are 82 | pos, err := f.Seek(0, os.SEEK_CUR) 83 | if err != nil { 84 | return err 85 | } 86 | return f.Truncate(pos) 87 | } 88 | -------------------------------------------------------------------------------- /resolver/resolvconf_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func tempdir(t *testing.T) string { 12 | dir, err := ioutil.TempDir("", "") 13 | if err != nil { 14 | t.Fatal("could not create temp dir:", err) 15 | } 16 | return dir 17 | } 18 | 19 | func assertFileContains(t *testing.T, path, expected string) { 20 | got, err := ioutil.ReadFile(path) 21 | if err != nil { 22 | t.Fatalf("could not read '%v': %v", path, err) 23 | } 24 | 25 | if string(got) != expected { 26 | t.Errorf("expected file %v to be:\n%v\n\nbut got:\n%v", path, expected, string(got)) 27 | } 28 | } 29 | 30 | func checkInsertLine(t *testing.T, path, line, orig string) { 31 | err := updateResolvConf(line+"\n", path) 32 | if err != nil { 33 | t.Fatal("could not insert line:", err) 34 | } 35 | 36 | lines := strings.SplitAfter(orig, "\n") 37 | for i, line := range lines { 38 | if line != "" { 39 | lines[i] = "# " + line 40 | } 41 | } 42 | commented := strings.Join(lines, "") 43 | 44 | assertFileContains(t, path, line+"\n"+commented) 45 | } 46 | 47 | func TestInsertLineNewFile(t *testing.T) { 48 | dir := tempdir(t) 49 | defer os.RemoveAll(dir) 50 | 51 | path := filepath.Join(dir, "test.txt") 52 | checkInsertLine(t, path, "hello world", "") 53 | } 54 | 55 | func TestInsertLineEmptyFile(t *testing.T) { 56 | dir := tempdir(t) 57 | defer os.RemoveAll(dir) 58 | 59 | path := filepath.Join(dir, "test.txt") 60 | err := ioutil.WriteFile(path, []byte{}, 0666) 61 | if err != nil { 62 | t.Fatal("could not create file:", err) 63 | } 64 | checkInsertLine(t, path, "hello world", "") 65 | } 66 | 67 | func TestInsertLineExistingFile(t *testing.T) { 68 | dir := tempdir(t) 69 | defer os.RemoveAll(dir) 70 | 71 | path := filepath.Join(dir, "test.txt") 72 | orig := "existing text\nanother line\n" 73 | err := ioutil.WriteFile(path, []byte(orig), 0666) 74 | if err != nil { 75 | t.Fatal("could not create file:", err) 76 | } 77 | checkInsertLine(t, path, "hello world", orig) 78 | } 79 | 80 | func TestInsertLineExistingFileWithComments(t *testing.T) { 81 | dir := tempdir(t) 82 | defer os.RemoveAll(dir) 83 | 84 | path := filepath.Join(dir, "test.txt") 85 | 86 | expected := "existing text\nanother line\n" 87 | orig := expected + "comment line " + RESOLVCONF_COMMENT 88 | 89 | err := ioutil.WriteFile(path, []byte(orig), 0666) 90 | if err != nil { 91 | t.Fatal("could not create file:", err) 92 | } 93 | checkInsertLine(t, path, "hello world", expected) 94 | } 95 | 96 | func checkRemoveLine(t *testing.T, path, expected string) { 97 | err := updateResolvConf("", path) 98 | if err != nil { 99 | t.Fatal("could not remove line:", err) 100 | } 101 | 102 | assertFileContains(t, path, expected) 103 | } 104 | 105 | func TestRemoveLineMissingFile(t *testing.T) { 106 | dir := tempdir(t) 107 | defer os.RemoveAll(dir) 108 | 109 | err := updateResolvConf("", filepath.Join(dir, "test.txt")) 110 | if err != nil { 111 | t.Fatal("could not remove line:", err) 112 | } 113 | } 114 | 115 | func TestRemoveLineBeginning(t *testing.T) { 116 | dir := tempdir(t) 117 | defer os.RemoveAll(dir) 118 | 119 | path := filepath.Join(dir, "test.txt") 120 | line := "hello world " + RESOLVCONF_COMMENT 121 | rest := "some more\ntext after\n" 122 | 123 | err := ioutil.WriteFile(path, []byte(line+"\n"+rest), 0666) 124 | if err != nil { 125 | t.Fatal("could not create file:", err) 126 | } 127 | 128 | checkRemoveLine(t, path, rest) 129 | } 130 | 131 | func TestRemoveLineMiddle(t *testing.T) { 132 | dir := tempdir(t) 133 | defer os.RemoveAll(dir) 134 | 135 | path := filepath.Join(dir, "test.txt") 136 | line := "hello world " + RESOLVCONF_COMMENT 137 | pre := "some\nbefore\n" 138 | post := "more\nafter\n" 139 | 140 | err := ioutil.WriteFile(path, []byte(pre+line+"\n"+post), 0666) 141 | if err != nil { 142 | t.Fatal("could not create file:", err) 143 | } 144 | 145 | checkRemoveLine(t, path, pre+post) 146 | } 147 | 148 | func TestRemoveLineEnd(t *testing.T) { 149 | dir := tempdir(t) 150 | defer os.RemoveAll(dir) 151 | 152 | path := filepath.Join(dir, "test.txt") 153 | line := "hello world " + RESOLVCONF_COMMENT 154 | pre := "some\nbefore\n" 155 | 156 | err := ioutil.WriteFile(path, []byte(pre+line), 0666) 157 | if err != nil { 158 | t.Fatal("could not create file:", err) 159 | } 160 | 161 | checkRemoveLine(t, path, pre) 162 | } 163 | 164 | func TestRemoveLineMulti(t *testing.T) { 165 | dir := tempdir(t) 166 | defer os.RemoveAll(dir) 167 | 168 | path := filepath.Join(dir, "test.txt") 169 | pre := "some\nbefore\n" 170 | line1 := "hello world " + RESOLVCONF_COMMENT 171 | mid := "and\nbetween\n" 172 | line2 := "something else " + RESOLVCONF_COMMENT 173 | post := "more\nafter\n" 174 | 175 | origText := pre + line1 + "\n" + mid + line2 + "\n" + post 176 | expected := pre + mid + post 177 | 178 | err := ioutil.WriteFile(path, []byte(origText), 0666) 179 | if err != nil { 180 | t.Fatal("could not create file:", err) 181 | } 182 | 183 | checkRemoveLine(t, path, expected) 184 | } 185 | -------------------------------------------------------------------------------- /resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | type Resolver interface { 14 | AddHost(id string, addr net.IP, name string, aliases ...string) error 15 | RemoveHost(id string) error 16 | 17 | AddUpstream(id string, addr net.IP, port int, domain ...string) error 18 | RemoveUpstream(id string) error 19 | 20 | Listen() error 21 | Close() 22 | } 23 | 24 | type hostsEntry struct { 25 | Address net.IP 26 | Names []string 27 | } 28 | 29 | type serversEntry struct { 30 | Address net.IP 31 | Port int 32 | Domains []string 33 | } 34 | 35 | type dnsResolver struct { 36 | hostMutex sync.RWMutex 37 | upstreamMutex sync.RWMutex 38 | 39 | Port int 40 | hosts map[string]*hostsEntry 41 | upstream map[string]*serversEntry 42 | server *dns.Server 43 | stopped chan struct{} 44 | } 45 | 46 | func NewResolver() (*dnsResolver, error) { 47 | return &dnsResolver{ 48 | Port: 53, 49 | hosts: make(map[string]*hostsEntry), 50 | upstream: make(map[string]*serversEntry), 51 | stopped: make(chan struct{}), 52 | }, nil 53 | } 54 | 55 | func (r *dnsResolver) AddHost(id string, addr net.IP, name string, aliases ...string) error { 56 | r.hostMutex.Lock() 57 | defer r.hostMutex.Unlock() 58 | 59 | r.hosts[id] = &hostsEntry{Address: addr, Names: append([]string{name}, aliases...)} 60 | return nil 61 | } 62 | 63 | func (r *dnsResolver) RemoveHost(id string) error { 64 | r.hostMutex.Lock() 65 | defer r.hostMutex.Unlock() 66 | 67 | delete(r.hosts, id) 68 | return nil 69 | } 70 | 71 | func (r *dnsResolver) AddUpstream(id string, addr net.IP, port int, domains ...string) error { 72 | r.upstreamMutex.Lock() 73 | defer r.upstreamMutex.Unlock() 74 | 75 | r.upstream[id] = &serversEntry{Address: addr, Port: port, Domains: domains} 76 | return nil 77 | } 78 | 79 | func (r *dnsResolver) RemoveUpstream(id string) error { 80 | r.upstreamMutex.Lock() 81 | defer r.upstreamMutex.Unlock() 82 | 83 | delete(r.upstream, id) 84 | return nil 85 | } 86 | 87 | func (r *dnsResolver) Listen() error { 88 | addr := fmt.Sprintf(":%d", r.Port) 89 | 90 | listenAddr, err := net.ResolveUDPAddr("udp4", addr) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | conn, err := net.ListenUDP("udp4", listenAddr) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | r.Port = conn.LocalAddr().(*net.UDPAddr).Port 101 | 102 | startupError := make(chan error) 103 | r.server = &dns.Server{Handler: r, PacketConn: conn, NotifyStartedFunc: func() { 104 | startupError <- nil 105 | }} 106 | 107 | go func() { 108 | select { 109 | case startupError <- r.run(): 110 | default: 111 | } 112 | }() 113 | 114 | return <-startupError 115 | } 116 | 117 | func (r *dnsResolver) run() error { 118 | defer close(r.stopped) 119 | return r.server.ActivateAndServe() 120 | } 121 | 122 | func (r *dnsResolver) Wait() error { 123 | <-r.stopped 124 | return nil 125 | } 126 | 127 | func (r *dnsResolver) Close() { 128 | if r.server != nil { 129 | r.server.Shutdown() 130 | } 131 | } 132 | 133 | func (r *dnsResolver) ServeDNS(w dns.ResponseWriter, query *dns.Msg) { 134 | response, err := r.responseForQuery(query) 135 | if err != nil { 136 | log.Printf("response error: %T %s", err, err) 137 | return 138 | } 139 | if response == nil { 140 | return 141 | } 142 | 143 | err = w.WriteMsg(response) 144 | if err != nil { 145 | log.Println("write error:", err) 146 | } 147 | } 148 | 149 | func (r *dnsResolver) responseForQuery(query *dns.Msg) (*dns.Msg, error) { 150 | // TODO multiple queries? 151 | name := query.Question[0].Name 152 | 153 | if query.Question[0].Qtype == dns.TypeA { 154 | if addrs := r.findHost(name); len(addrs) > 0 { 155 | return dnsAddressRecord(query, name, addrs), nil 156 | } 157 | } else if query.Question[0].Qtype == dns.TypePTR { 158 | if hosts := r.findReverse(name); len(hosts) > 0 { 159 | return dnsPtrRecord(query, name, hosts), nil 160 | } 161 | } 162 | 163 | // What if RecursionDesired = false? 164 | if resp, err := r.findUpstream(name, query); resp != nil || err != nil { 165 | return resp, err 166 | } 167 | 168 | return dnsNotFound(query), nil 169 | } 170 | 171 | func (r *dnsResolver) upstreamForHost(name string) (matchedUpstream *serversEntry) { 172 | r.upstreamMutex.RLock() 173 | defer r.upstreamMutex.RUnlock() 174 | 175 | matchedDomain := "" 176 | 177 | for _, upstream := range r.upstream { 178 | if len(upstream.Domains) == 0 && matchedDomain == "" { 179 | matchedUpstream = upstream 180 | } 181 | 182 | for _, domain := range upstream.Domains { 183 | domain = dns.Fqdn(domain) 184 | if len(domain) > len(matchedDomain) && (domain == name || strings.HasSuffix(name, "."+domain)) { 185 | matchedDomain = domain 186 | matchedUpstream = upstream 187 | } 188 | } 189 | } 190 | 191 | return 192 | } 193 | 194 | func (r *dnsResolver) findUpstream(name string, msg *dns.Msg) (*dns.Msg, error) { 195 | upstream := r.upstreamForHost(name) 196 | if upstream == nil || upstream.Address == nil { 197 | return nil, nil 198 | } 199 | 200 | c := &dns.Client{Net: "udp"} 201 | addr := fmt.Sprintf("%s:%d", upstream.Address.String(), upstream.Port) 202 | resp, _, err := c.Exchange(msg, addr) 203 | return resp, err 204 | } 205 | 206 | func (r *dnsResolver) findHost(name string) (addrs []net.IP) { 207 | r.hostMutex.RLock() 208 | defer r.hostMutex.RUnlock() 209 | 210 | for _, hosts := range r.hosts { 211 | for _, hostName := range hosts.Names { 212 | if dns.Fqdn(hostName) == name { 213 | addrs = append(addrs, hosts.Address) 214 | } 215 | } 216 | } 217 | return 218 | } 219 | 220 | func (r *dnsResolver) findReverse(address string) (hosts []string) { 221 | r.hostMutex.RLock() 222 | defer r.hostMutex.RUnlock() 223 | 224 | address = strings.ToLower(dns.Fqdn(address)) 225 | 226 | for _, entry := range r.hosts { 227 | if r, _ := dns.ReverseAddr(entry.Address.String()); address == r && len(entry.Names) > 0 { 228 | hosts = append(hosts, dns.Fqdn(entry.Names[0])) 229 | } 230 | } 231 | return 232 | } 233 | 234 | func dnsAddressRecord(query *dns.Msg, name string, addrs []net.IP) *dns.Msg { 235 | resp := new(dns.Msg) 236 | resp.SetReply(query) 237 | for _, addr := range addrs { 238 | rr := new(dns.A) 239 | rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0} 240 | rr.A = addr 241 | 242 | resp.Answer = append(resp.Answer, rr) 243 | } 244 | return resp 245 | } 246 | 247 | func dnsPtrRecord(query *dns.Msg, name string, hosts []string) *dns.Msg { 248 | resp := new(dns.Msg) 249 | resp.SetReply(query) 250 | for _, host := range hosts { 251 | rr := new(dns.PTR) 252 | rr.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0} 253 | rr.Ptr = host 254 | 255 | resp.Answer = append(resp.Answer, rr) 256 | } 257 | return resp 258 | } 259 | 260 | func dnsNotFound(query *dns.Msg) *dns.Msg { 261 | resp := new(dns.Msg) 262 | resp.SetReply(query) 263 | resp.SetRcode(query, dns.RcodeNameError) 264 | return resp 265 | } 266 | -------------------------------------------------------------------------------- /resolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "path/filepath" 7 | "reflect" 8 | "runtime" 9 | "sort" 10 | "testing" 11 | "time" 12 | 13 | "github.com/tonnerre/golang-dns" 14 | ) 15 | 16 | func TestResolver(t *testing.T) { 17 | hostname := "foobar" 18 | address := net.ParseIP("1.2.3.4") 19 | 20 | resolver, err := NewResolver() 21 | ok(t, err) 22 | 23 | resolver.AddHost(address.String(), address, hostname) 24 | 25 | ok(t, startResolver(resolver)) 26 | defer resolver.Close() 27 | 28 | assertResolvesTo(t, []net.IP{address}, hostname, resolver.Port) 29 | 30 | resolver.RemoveHost(address.String()) 31 | assertDoesNotResolve(t, hostname, resolver.Port) 32 | } 33 | 34 | func TestMultipleAddresses(t *testing.T) { 35 | hostname := "foobar" 36 | addr1 := net.ParseIP("1.2.3.4") 37 | addr2 := net.ParseIP("5.6.7.8") 38 | 39 | resolver, err := runResolver() 40 | ok(t, err) 41 | defer resolver.Close() 42 | 43 | resolver.AddHost(addr1.String(), addr1, hostname) 44 | resolver.AddHost(addr2.String(), addr2, hostname) 45 | 46 | assertResolvesTo(t, []net.IP{addr1, addr2}, hostname, resolver.Port) 47 | } 48 | 49 | func TestUpstreamResolver(t *testing.T) { 50 | hostname := "foobar" 51 | address := net.ParseIP("1.2.3.4") 52 | 53 | resolver, err := runResolver() 54 | ok(t, err) 55 | defer resolver.Close() 56 | 57 | upstream, err := runResolver() 58 | ok(t, err) 59 | defer upstream.Close() 60 | upstream.AddHost("foobar", address, hostname) 61 | 62 | assertDoesNotResolve(t, hostname, resolver.Port) 63 | 64 | resolver.AddUpstream("upstream", net.ParseIP("127.0.0.1"), upstream.Port) 65 | 66 | assertResolvesTo(t, []net.IP{address}, hostname, resolver.Port) 67 | } 68 | 69 | func TestUpstreamResolverDomains(t *testing.T) { 70 | shouldResolve := net.ParseIP("1.0.0.1") 71 | shouldAlsoResolve := net.ParseIP("2.0.0.1") 72 | shouldNotResolve := net.ParseIP("3.0.0.1") 73 | 74 | resolver, err := runResolver() 75 | ok(t, err) 76 | defer resolver.Close() 77 | 78 | upstream, err := runResolver() 79 | ok(t, err) 80 | defer upstream.Close() 81 | upstream.AddHost("should-resolve", shouldResolve, "domain.should-resolve") 82 | upstream.AddHost("should-also-resolve", shouldAlsoResolve, "domain.should-also-resolve") 83 | upstream.AddHost("should-not-resolve", shouldNotResolve, "domain.should-not-resolve") 84 | 85 | resolver.AddUpstream("upstream", net.ParseIP("127.0.0.1"), upstream.Port, "should-resolve", "should-also-resolve") 86 | 87 | assertDoesNotResolve(t, "domain.should-not-resolve", resolver.Port) 88 | assertResolvesTo(t, []net.IP{shouldResolve}, "domain.should-resolve", resolver.Port) 89 | assertResolvesTo(t, []net.IP{shouldAlsoResolve}, "domain.should-also-resolve", resolver.Port) 90 | } 91 | 92 | func TestUpstreamResolverSubDomains(t *testing.T) { 93 | addr := net.ParseIP("1.0.0.1") 94 | 95 | resolver, err := runResolver() 96 | ok(t, err) 97 | defer resolver.Close() 98 | 99 | upstream1, err := runResolver() 100 | ok(t, err) 101 | defer upstream1.Close() 102 | 103 | upstream1.AddHost("should-resolve", addr, "name.top") 104 | 105 | upstream2, err := runResolver() 106 | ok(t, err) 107 | defer upstream2.Close() 108 | 109 | upstream2.AddHost("should-also-resolve", addr, "name.sub.top") 110 | 111 | resolver.AddUpstream("upstream1", net.ParseIP("127.0.0.1"), upstream1.Port, "top") 112 | resolver.AddUpstream("upstream2", net.ParseIP("127.0.0.1"), upstream2.Port, "sub.top") 113 | 114 | assertResolvesTo(t, []net.IP{addr}, "name.top", resolver.Port) 115 | assertResolvesTo(t, []net.IP{addr}, "name.sub.top", resolver.Port) 116 | } 117 | 118 | // queries within the "local domain" should not be forwarded to upstream servers 119 | func TestLocalDomain(t *testing.T) { 120 | shouldResolve := net.ParseIP("1.0.0.1") 121 | shouldNotResolve := net.ParseIP("3.0.0.1") 122 | 123 | resolver, err := NewResolver() 124 | ok(t, err) 125 | 126 | // upstream with a "nil" address should not be forwarded 127 | resolver.AddUpstream("docker", nil, 0, "docker") 128 | 129 | ok(t, startResolver(resolver)) 130 | defer resolver.Close() 131 | 132 | upstream, err := runResolver() 133 | ok(t, err) 134 | defer upstream.Close() 135 | 136 | resolver.AddUpstream("upstream", net.ParseIP("127.0.0.1"), upstream.Port) 137 | resolver.AddHost("should-resolve", shouldResolve, "should-resolve.docker") 138 | upstream.AddHost("should-not-resolve", shouldNotResolve, "should-not-resolve.docker") 139 | 140 | assertDoesNotResolve(t, "should-not-resolve.docker", resolver.Port) 141 | assertResolvesTo(t, []net.IP{shouldResolve}, "should-resolve.docker", resolver.Port) 142 | } 143 | 144 | func TestReverseLookup(t *testing.T) { 145 | addr := net.ParseIP("1.2.3.4") 146 | 147 | resolver, err := runResolver() 148 | ok(t, err) 149 | defer resolver.Close() 150 | 151 | resolver.AddHost("foo", addr, "primary.domain", "secondary.domain") 152 | 153 | m := new(dns.Msg) 154 | m.SetQuestion("4.3.2.1.in-addr.arpa.", dns.TypePTR) 155 | 156 | c := new(dns.Client) 157 | r, _, err := c.Exchange(m, fmt.Sprintf("127.0.0.1:%d", resolver.Port)) 158 | ok(t, err) 159 | 160 | hosts := make([]string, 0, len(r.Answer)) 161 | for _, answer := range r.Answer { 162 | if record, ok := answer.(*dns.PTR); ok { 163 | hosts = append(hosts, record.Ptr) 164 | } 165 | } 166 | 167 | equals(t, []string{"primary.domain."}, hosts) 168 | } 169 | 170 | func TestWait(t *testing.T) { 171 | resolver, err := runResolver() 172 | ok(t, err) 173 | defer resolver.Close() 174 | 175 | done := make(chan struct{}) 176 | go func() { 177 | resolver.Wait() 178 | close(done) 179 | }() 180 | 181 | select { 182 | case <-done: 183 | t.Fatal("wait should not return before process has exited") 184 | case <-time.After(time.Second / 2): 185 | } 186 | 187 | resolver.Close() 188 | 189 | select { 190 | case <-done: 191 | case <-time.After(10 * time.Second): 192 | t.Fatal("wait should return after process has exited") 193 | } 194 | } 195 | 196 | func TestWaitBeforeListen(t *testing.T) { 197 | resolver, err := NewResolver() 198 | ok(t, err) 199 | defer resolver.Close() 200 | 201 | done := make(chan struct{}) 202 | go func() { 203 | resolver.Wait() 204 | close(done) 205 | }() 206 | 207 | select { 208 | case <-done: 209 | t.Fatal("wait should not return before process has exited") 210 | case <-time.After(time.Second / 2): 211 | } 212 | 213 | ok(t, startResolver(resolver)) 214 | 215 | select { 216 | case <-done: 217 | t.Fatal("wait should not return before process has exited") 218 | case <-time.After(time.Second / 2): 219 | } 220 | 221 | resolver.Close() 222 | 223 | select { 224 | case <-done: 225 | case <-time.After(10 * time.Second): 226 | t.Fatal("wait should return after process has exited") 227 | } 228 | } 229 | 230 | //////////////////////////////////////////////////////////////////////////////// 231 | 232 | func startResolver(resolver *dnsResolver) error { 233 | resolver.Port = 0 234 | if err := resolver.Listen(); err != nil { 235 | return err 236 | } 237 | return nil 238 | } 239 | 240 | func runResolver() (*dnsResolver, error) { 241 | resolver, err := NewResolver() 242 | if err == nil { 243 | err = startResolver(resolver) 244 | } 245 | return resolver, err 246 | } 247 | 248 | func lookupHost(host, server string) ([]net.IP, error) { 249 | m := new(dns.Msg) 250 | m.SetQuestion(dns.Fqdn(host), dns.TypeA) 251 | 252 | c := new(dns.Client) 253 | r, _, err := c.Exchange(m, server) 254 | 255 | if err != nil { 256 | return nil, err 257 | } 258 | 259 | addrs := make([]net.IP, 0, len(r.Answer)) 260 | 261 | for _, answer := range r.Answer { 262 | if record, ok := answer.(*dns.A); ok { 263 | addrs = append(addrs, record.A) 264 | } 265 | } 266 | 267 | return addrs, nil 268 | } 269 | 270 | func assertResolvesTo(tb testing.TB, expected []net.IP, hostname string, dnsPort int) { 271 | addrs, err := lookupHost(hostname, fmt.Sprintf("127.0.0.1:%d", dnsPort)) 272 | ok(tb, err) 273 | equals(tb, sortIPs(expected), sortIPs(addrs)) 274 | } 275 | 276 | func sortIPs(ips []net.IP) []string { 277 | vals := make([]string, len(ips)) 278 | for i, ip := range ips { 279 | vals[i] = ip.String() 280 | } 281 | sort.Strings(vals) 282 | return vals 283 | } 284 | 285 | func assertDoesNotResolve(tb testing.TB, hostname string, dnsPort int) { 286 | assertResolvesTo(tb, []net.IP{}, hostname, dnsPort) 287 | } 288 | 289 | // ok fails the test if an err is not nil. 290 | func ok(tb testing.TB, err error) { 291 | if err != nil { 292 | _, file, line, _ := runtime.Caller(1) 293 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 294 | tb.FailNow() 295 | } 296 | } 297 | 298 | // equals fails the test if exp is not equal to act. 299 | func equals(tb testing.TB, exp, act interface{}) { 300 | if !reflect.DeepEqual(exp, act) { 301 | _, file, line, _ := runtime.Caller(1) 302 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 303 | tb.FailNow() 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /resolver/types.go: -------------------------------------------------------------------------------- 1 | //go:generate go-extpoints . HostResolverConfig 2 | 3 | package resolver 4 | 5 | type HostResolverConfig interface { 6 | StoreAddress(address string) error 7 | Clean() 8 | } 9 | -------------------------------------------------------------------------------- /systemd/systemd.go: -------------------------------------------------------------------------------- 1 | package systemd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "text/template" 9 | 10 | "github.com/gliderlabs/resolvable/resolver" 11 | 12 | "github.com/coreos/go-systemd/daemon" 13 | "github.com/coreos/go-systemd/dbus" 14 | ) 15 | 16 | type service struct { 17 | name string 18 | dir string 19 | filepattern string 20 | } 21 | 22 | var defaultServices []service = []service{ 23 | service{"systemd-resolved.service", "resolved.conf.d", "*.conf"}, 24 | service{"systemd-networkd.service", "network", "*.network"}, 25 | } 26 | 27 | type SystemdConfig struct { 28 | templatePath string 29 | destPath string 30 | services []service 31 | written map[string][]string 32 | } 33 | 34 | type templateArgs struct { 35 | Address string 36 | } 37 | 38 | func getopt(name, def string) string { 39 | if env := os.Getenv(name); env != "" { 40 | return env 41 | } 42 | return def 43 | } 44 | 45 | func init() { 46 | systemdConf := getopt("SYSTEMD_CONF_PATH", "/tmp/systemd") 47 | if _, err := os.Stat(systemdConf); err != nil { 48 | log.Printf("systemd: disabled, cannot read %s: %s", systemdConf, err) 49 | return 50 | } 51 | resolver.HostResolverConfigs.Register(&SystemdConfig{ 52 | templatePath: "/config/systemd", 53 | destPath: systemdConf, 54 | services: defaultServices, 55 | written: make(map[string][]string), 56 | }, "systemd") 57 | } 58 | 59 | func (r *SystemdConfig) StoreAddress(address string) error { 60 | data := templateArgs{address} 61 | 62 | for _, s := range r.services { 63 | pattern := filepath.Join(r.templatePath, s.dir, s.filepattern) 64 | 65 | log.Printf("systemd: %s: loading config from %s", s.name, pattern) 66 | 67 | templates, err := template.ParseGlob(pattern) 68 | if err != nil { 69 | log.Println("systemd:", err) 70 | continue 71 | } 72 | 73 | var written []string 74 | 75 | for _, t := range templates.Templates() { 76 | dest := filepath.Join(r.destPath, s.dir, t.Name()) 77 | log.Println("systemd: generating", dest) 78 | fp, err := os.Create(dest) 79 | if err != nil { 80 | log.Println("systemd:", err) 81 | continue 82 | } 83 | written = append(written, dest) 84 | t.Execute(fp, data) 85 | fp.Close() 86 | } 87 | 88 | if written != nil { 89 | r.written[s.name] = written 90 | reload(s.name) 91 | } else { 92 | log.Println("systemd: %s: no configs written, skipping reload", s.name) 93 | } 94 | } 95 | 96 | daemon.SdNotify("READY=1") 97 | return nil 98 | } 99 | 100 | func (r *SystemdConfig) Clean() { 101 | daemon.SdNotify("STOPPING=1") 102 | 103 | for service, filenames := range r.written { 104 | log.Printf("systemd: %s: removing configs...", service) 105 | for _, filename := range filenames { 106 | os.Remove(filename) 107 | } 108 | reload(service) 109 | } 110 | } 111 | 112 | func reload(name string) error { 113 | conn, err := dbus.New() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | log.Printf("systemd: %s: starting reload...", name) 119 | 120 | statusCh := make(chan string) 121 | _, err = conn.ReloadOrRestartUnit(name, "replace", statusCh) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | status := <-statusCh 127 | log.Printf("systemd: %s: %s", name, status) 128 | if status != "done" { 129 | return fmt.Errorf("error reloading %s: %s", name, status) 130 | } 131 | return nil 132 | } 133 | --------------------------------------------------------------------------------