DaveSouth.org

Email RSS Twitter Delicious

How to set up dnsmasq on Snow Leopard for local wildcard domains

This method is out-of-date. For a far better way please read Rails subdomains, CNAMEs, and crzy.me

We had a working solution to use wildcard domains on the localhost. Using a proxy.pac file and a tiny rack application we could pass any domain into the development environment. It was working great, until we tried switching to Rails3 beta. Whenever we specified a non-standard local domain, the Rails router would strip the query string off the URL.

If we passed in a URL like:
http://www.example.com.dev/search?q=find&a&term

We would get:
http://www.example.com.dev/search

This is not helpful.

So after some more digging around the internet we found yet another solution to create a wildcard domain WITHOUT setting up a full blown DNS server — use dnsmasq.

Dnsmasq is a DNS forwarder typically used on firewalls for DHCP service and provide a DNS resolver for clients without implementing a full DNS server. It’s lightweight, easy to configure and can act as a pseudo DNS server for a few “domains”.

Setting up dnsmasq

These instructions are for intermediate to advanced users to install dnsmasq using MacPorts. It works well on Snow Leopard and should work on Leopard as well. We create a local “domain” called “dev” which routes to the local host.

# Add dev as localhost to /etc/hosts
127.0.0.1  dev

# Install dnsmasq
sudo port install dnsmasq
 
# Add line to the top of /opt/local/etc/dnsmasq.conf
address=/.dev/127.0.0.1

# Start dnsmasq and set to launch at startup
sudo port load dnsmasq

# Open Mac Network Preferences
# Set DNS server as 127.0.0.1 + existing server
# If existing DNS is 10.0.0.1
127.0.0.1, 10.0.0.1

# Check that it's working
dig google.com

# Should return results from
# ;; SERVER: 127.0.0.01

# Test dev domain
ping dev

# Should return something like
# 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.031 ms

# Also test dev subdomains
ping www.anywhere.com.dev

# Should return similar results

VMWare Fusion

We use Windows on VMWare Fusion to test our sites in IE and other Windows browsers. We can point Windows to use the Mac as the DNS server which will resolve “dev” to 127.0.0.1. That doesn’t help since 127.0.0.1 is the Windows host and not the Rails app running on the Mac host. We have to set up a second “domain” for windows to use.

Set up Windows on Fusion as a shared address, not bridged. It already uses the Mac as the DNS resolver. All we have to do is add a pseudo domain to the hosts file in Windows. Find the “gateway” IP which is the route to the local Mac. Point the “win” domain there and it’ll work.

# Find the 'gateway' address (eg. 192.168.40.2)
ipconfig

# Edit hosts in Windows client
c:\windows\system32\drivers\etc\hosts

# Add "win" as link to gateway address
192.168.40.2    win

# Add gateway to /opt/local/etc/dnsmasq.conf on the Mac
address=/.win/192.168.40.2

# Test in Windows
ping www.anywhere.com.wdev

# Should return local pings

Local domain

In our last story we discussed the LocalDomain rack middleware we use. It’s a simple program to strip “.dev” or “.win” off any domain longer than two terms.

www.example.com.dev => www.example.com

By passing into the rails application the fully-qualified domain without “.dev” you can develop applications that work with any domain and not just subdomains.

# Local Domain
# RAILS_ROOT/lib/local_domain.rb
#
# Subdomains are preserved
# local => local
# example.test => example.test
#
# Longer domains are truncated
# example.com.local => example.com
# www.example.com.dev => www.example.com
# slc.utah.example.com.test => slc.utah.example.com
#
# Only load in development environment

class LocalDomain

  def initialize(app)
    @app = app
  end
  
  def call(env)
    host = env['SERVER_NAME']
    parts = host.split('.')

    if parts.length > 2
      env['HTTP_X_FORWARDED_HOST'] = [host, domain(parts)].join(', ')
    end

    @app.call(env)
  end

  def domain(parts)
    parts[0..-2].join('.')
  end
end

This is a really simplistic rack application. It passes all domains with only two terms. If there are three or more, it strips off the last term no matter what it is.

Add the test file, local_domain_test.rb, to test/unit.

# RAILS_ROOT/test/unit/local_domain_test.rb
require 'local_domain'
require 'rack/test'

class LocalDomainTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    mock_app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Good Morning!"] }
    app = LocalDomain.new(mock_app)
  end

  def test_local_pass_through
    get 'http://local:3000/'
    assert_nil last_request.env['HTTP_X_FORWARDED_HOST']
    assert_equal 'local', last_request.env['SERVER_NAME']
  end

  def test_subdomain_pass_through
    get 'http://example.local:3000/'
    assert_nil last_request.env['HTTP_X_FORWARDED_HOST']
    assert_equal 'example.local', last_request.env['SERVER_NAME']
  end

  def test_strips_off_last_domain_term
    get 'http://www.example.com.local/'
    assert_equal 'www.example.com.local, www.example.com', last_request.env['HTTP_X_FORWARDED_HOST']
    assert_equal 'www.example.com.local', last_request.env['SERVER_NAME']
  end
end

To load LocalDomain create a loader in config/initializers with:
require 'local_domain'

Then add middleware to the development.rb file in config/environments:
config.middleware.use "LocalDomain"

Notes

That should do it. Unlike the proxy.pac solution, we now have to specify the port used by Rails:
http://dev:3000/ or http://www.example.com.dev:3000/

Sometimes you need to flush the DNS cache to force the Mac or Windows to see the new DNS entries. The process is simple.

To flush the cache on the Mac:
dscacheutil -flushcache

On Windows:
ipconfig /flushdns

More information