Mike Ferrier

I beat code into submission.

Setting Up Wildcard DNS on Localhost Domains on OSX

Mavericks Update

As wadedorrell points out in the comments, OSX Mavericks doesn’t ship with bind anymore. An easy alternative is to use dnsmasq with an /etc/resolver entry. Here’s a great post on how to do that. As it turns out, this newer process is much easier than setting up named, so I’d recommend doing that instead.



Like many other Rails developers, I’ve made the switch to using nginx+passenger for my local development on OSX. It’s super easy to compile and install, especially if you use brew and having your nginx process serving all your development sites is a snap to configure.

The workflow for a new project generally goes like this: add your project directory, give that directory its own server{} block in nginx.conf, and then picking a local (i.e. fake) domain and pointing that domain at 127.0.0.1, or localhost. For instance, if I were to start projectx in ~/dev/projectx, I would then have to add the appropriate server{} block in nginx.conf, and then add projectx.local to my /etc/hosts file. When I hit http://projectx.local in my browser, it’ll resolve to localhost and hit my local nginx, serving me my development site.

The one place where this kind of fails is when you need wildcard subdomains. That is to say, when you need *.projectx.local to point to localhost.

While working on  4ormat, we do a lot of testing on our local machines, and since we give users their own subdomains on 4ormat.com, it comes in handy to be able to map *.4ormat.local in our web browsers to the localhost. While nginx supports wildcard hostnames in its configuration file, /etc/hosts does not.

So what’s the answer? Set up your own local nameserver. It’s easier than you might think. OSX Snow Leopard ships with ISC Bind (the de facto standard for serving DNS) and it’s pretty easy to get going. Here’s how to do it.

(big thanks to Geoff Hankerson’s post which helped kickstart this process)

1. Generate an rndc key

rndc is a utility that allows you to admin the bind server remotely, and by default the Bind config looks for a key, so start by generating one:

1
2
$ sudo rndc-confgen -a -c /etc/rndc.key
wrote key file "/etc/rndc.key"

2. Add your .local top level domain as a zone

Edit /etc/named.conf and add the following block, called a “zone”:

1
2
3
4
5
zone "local" IN {
type master;
file "local.zone";
allow-update { none; };
};

Add it on its own lines right after one of the existing “zone” blocks.

3. Add the .local zone file

The line file “local.zone” in named.conf sets the name of the file where bind will look for information on that zone. Those zone files live in /var/named so next we add the file /var/named/local.zone

Here’s what mine looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$TTL  60
$ORIGIN local.
@      1D IN SOA  localhost. root.localhost. (
          45    ; serial (d. adams)
          3H    ; refresh
          15M    ; retry
          1W    ; expiry
          1D )    ; minimum

      1D IN NS  localhost.
      1D IN A  127.0.0.1

*.local. 60 IN A 127.0.0.1
4ormat.local. 60 IN A 127.0.0.1
*.4ormat.local. 60 IN A 127.0.0.1

In a nutshell, this config says:

  1. The authority on .local domains is localhost
  2. The domains anything.local and anything.4ormat.local resolve to 127.0.0.1

The important line is the one that starts *.4ormat.local. That allows us to use wildcard domains that will all resolve to localhost which is something we can’t do with /etc/hosts.

You’ll want to replace 4ormat with the domain of your choice, and local with the suffix you want.

By the way, the trailing dots are no typo — they’re Fully Qualified Domain Names. It’s a good practice when dealing with DNS settings to use FQDNs, as they remove any ambiguity that might arise when referencing domain names.

4. Check your config and zone files

Bind comes with some handy utilities for checking your config and zone files.

1
2
3
4
sudo named-checkconf /etc/named.conf
$ sudo named-checkzone local /var/named/local.zone
zone local/IN: loaded serial 45
OK

The first command will have no output if your config is set up properly. The second should say “OK”.

5. Start bind

An easy way to start up bind is to add it to the Launch Daemons of the root user:

1
sudo launchctl load -w /System/Library/LaunchDaemons/org.isc.named.plist

After running this command, launchd should have started up named automatically:

1
2
$ ps aux | grep named
root 2467 0.0 0.2 2441740 8380 ?? Ss 6:43pm 0:05.48 /usr/sbin/named -f

As an added bonus, this command makes sure named is automatically started on boot, too.

6. Verify things are working

dig is the swiss army knife of DNS tools, and is distributed with Bind, so it’s already around on OSX. Asking localhost to resolve a few domains will verify things are working:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
$ dig @localhost foo.local

; <<>> DiG 9.6.0-APPLE-P2 <<>> @localhost foo.local
; (3 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53309
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; QUESTION SECTION:
;foo.local. IN  A

;; ANSWER SECTION:
foo.local.    60  IN  A  127.0.0.1

;; AUTHORITY SECTION:
local.   86400  IN  NS  localhost.

;; ADDITIONAL SECTION:
localhost.    86400  IN  A  127.0.0.1

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Mon Apr  4 17:13:51 2011
;; MSG SIZE  rcvd: 82


$ dig @localhost 4ormat.local

; <<>> DiG 9.6.0-APPLE-P2 <<>> @localhost 4ormat.local
; (3 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1431
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; QUESTION SECTION:
;4ormat.local.      IN  A

;; ANSWER SECTION:
4ormat.local.    60  IN  A  127.0.0.1

;; AUTHORITY SECTION:
local.      86400  IN  NS  localhost.

;; ADDITIONAL SECTION:
localhost.    86400  IN  A  127.0.0.1

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Mon Apr  4 17:13:54 2011
;; MSG SIZE  rcvd: 85


$ dig @localhost foo.4ormat.local

; <<>> DiG 9.6.0-APPLE-P2 <<>> @localhost foo.4ormat.local
; (3 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43030
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; QUESTION SECTION:
;foo.4ormat.local.    IN  A

;; ANSWER SECTION:
foo.4ormat.local.  60  IN  A  127.0.0.1

;; AUTHORITY SECTION:
local.      86400  IN  NS  localhost.

;; ADDITIONAL SECTION:
localhost.    86400  IN  A  127.0.0.1

;; Query time: 0 msec

;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Mon Apr  4 17:13:57 2011
;; MSG SIZE  rcvd: 89

The important part of each of those three queries is the IN A response in the ANSWER SECTION. In each, we see that the domains resolve to 127.0.0.1, which is exactly what we want.

7. Add 127.0.0.1 to your list of DNS servers

Open System Preferences and open the Network pane. On the left side, select the network adapter you use (most likely your wireless adapter), and then click Advanced. Under the DNS tab, click the + button under the DNS Servers list, and add 127.0.0.1. Click OK, then click Apply.

That’s it, you’re done.

Reloading Bind and flushing DNS

If you ever change anything in your config or zone files, you can signal Bind to reload its config using:

1
sudo rndc -p 54 reload

It’s also a good practice to increment the serial field in your zone file any time anything changes. This lets any DNS client know to invalidate its DNS cache for that zone. So in our example, our serial is set to 45. So if you made any change, you’d change it to 46 before saving the file and reloading Bind.

Some people also reported that they needed to flush their local DNS cache after doing this (I didn’t.) If you feel you need to, you can by issuing the command:

1
dscacheutil -flushcache

Comments