diff --git a/Network/DNS/Common.hs b/Network/DNS/Common.hs
new file mode 100644
--- /dev/null
+++ b/Network/DNS/Common.hs
@@ -0,0 +1,257 @@
+{-# LANGUAGE ScopedTypeVariables, ForeignFunctionInterface #-}
+
+module Network.DNS.Common where
+
+import Control.Monad (when)
+
+import Data.Word
+import Data.Bits
+import Data.List (intersperse)
+
+import qualified Data.ByteString as B
+import Data.ByteString.Internal (c2w, w2c)
+import qualified Data.Binary.Put as P
+import qualified Data.Binary.Strict.Get as G
+import qualified Data.Binary.Strict.BitGet as BG
+
+import Network.DNS.Types
+
+foreign import ccall unsafe "htonl" htonl :: Word32 -> Word32
+
+-- | Types of DNS queries. RFC 1035, 4.1.1.
+data QueryType = QUERY | IQUERY | SERVERSTATUS deriving (Show, Eq, Enum, Bounded)
+
+-- | Parse an enum from a Word8 in a monad and fail if the value is out of range.
+--   It's assumed that the enum is defined at every value between the min and max
+--   bound
+parseEnum :: forall m a. (Enum a, Bounded a, Monad m) => Word8 -> m a
+parseEnum x' = r where
+  r = if x < low || x > high
+         then fail "Enum out of bounds"
+         else return $ toEnum x
+  low = fromEnum (minBound :: a)
+  high = fromEnum (maxBound :: a)
+  x = fromIntegral x'
+
+-- | A DNS protocol header. RFC 1035, 4.1.1.
+data Header = Header { headId :: Word16
+                     , headIsResponse :: Bool
+                     , headOpCode :: QueryType
+                     , headIsAuthoritative :: Bool
+                     , headIsTruncated :: Bool
+                     , headRecursionDesired :: Bool
+                     , headRecursionAvailible :: Bool
+                     , headResponseCode :: ResponseCode
+                     , headQuestionCount :: Int
+                     , headAnswerCount :: Int
+                     , headNSCount :: Int
+                     , headAdditionalCount :: Int }
+                     deriving (Show, Eq)
+
+parseHeader :: G.Get Header
+parseHeader = do
+  id <- G.getWord16be
+  flags <- G.getByteString 2
+  qdcount <- G.getWord16be >>= return . fromIntegral
+  ancount <- G.getWord16be >>= return . fromIntegral
+  nscount <- G.getWord16be >>= return . fromIntegral
+  arcount <- G.getWord16be >>= return . fromIntegral
+
+  let r = BG.runBitGet flags (do
+            isquery <- BG.getBit
+            opcode <- BG.getAsWord8 4 >>= parseEnum
+            aa <- BG.getBit
+            tc <- BG.getBit
+            rd <- BG.getBit
+            ra <- BG.getBit
+
+            BG.getAsWord8 3
+            rcode <- BG.getAsWord8 4 >>= parseEnum
+
+            return $ Header id isquery opcode aa tc rd ra rcode qdcount ancount nscount arcount)
+
+  case r of
+       Left error -> fail error
+       Right x -> return x
+
+serialiseHeader :: Header -> P.Put
+serialiseHeader header = do
+  P.putWord16be $ headId header
+
+  let flags1 = (v (headIsResponse header) `shiftL` 7) .|.
+               (fromEnum (headOpCode header) `shiftL` 3) .|.
+               (v (headIsAuthoritative header) `shiftL` 2) .|.
+               (v (headIsTruncated header) `shiftL` 1) .|.
+               (v (headRecursionDesired header))
+      flags2 = (v (headRecursionAvailible header) `shiftL` 7) .|.
+               (fromEnum (headResponseCode header))
+      v True = 1
+      v False = 0
+
+  P.putWord8 $ fromIntegral flags1
+  P.putWord8 $ fromIntegral flags2
+  P.putWord16be $ fromIntegral $ headQuestionCount header
+  P.putWord16be $ fromIntegral $ headAnswerCount header
+  P.putWord16be $ fromIntegral $ headNSCount header
+  P.putWord16be $ fromIntegral $ headAdditionalCount header
+
+-- | Break a DNS name (e.g. www.google.com) into a list of labels
+splitDNSName :: String -> [String]
+splitDNSName = filter (not . null) . split '.' where
+  split c xs = head : tail where
+    (head, rest) = span (/= c) xs
+    tail = case rest of
+                [] -> []
+                (_:xs) -> split c xs
+
+-- | Convert a split name into the length-prefixed DNS wire format.
+--   FIXME: should work with the IDNA system. Returns Nothing if the
+--   name couldn't be serialised.
+--   FIXME: catch invalid charactors and > 255 parts
+serialiseDNSName :: [String] -> Maybe B.ByteString
+serialiseDNSName x
+  | length x > 255 = fail ""
+  | otherwise = mapM f x >>= return . (flip B.snoc) 0 . B.concat where
+      f x
+        | length x > 63 = fail ""
+        | otherwise = return $ B.cons lengthByte s where
+            lengthByte = fromIntegral $ length x
+            s = B.pack $ map c2w x
+
+-- | Convert a list of labels to a normal string by interspersing periods
+fromDNSName :: [String] -> String
+fromDNSName = concat . intersperse "."
+
+parseDNSName :: B.ByteString -> G.Get [String]
+parseDNSName packet = do
+  let getLabel 16 = fail "Pointer loop in DNS name"
+      getLabel depth = do
+        b <- G.getWord8
+        -- if it's a pointer we need to decode it
+        if b .&. 0xc0 == 0xc0
+           then do b2 <- G.getWord8
+                   let offset = ((fromIntegral $ b .&. 0x3f) `shiftL` 8) .|. fromIntegral b2
+                   if offset >= B.length packet
+                      then fail "Invalid DNS label pointer"
+                      else case G.runGet (getLabel (depth + 1)) $ B.drop offset packet of
+                                (Left error, _) -> fail error
+                                (Right l, _) -> return l
+           else if b == 0
+                   then return []
+                   else do l <- G.getByteString $ fromIntegral b
+                           rest <- getLabel depth
+                           return $ (map w2c $ B.unpack l) : rest
+  getLabel (0 :: Int)
+
+serialiseQuestion :: B.ByteString  -- ^ the encoded name (see @serialiseDNSName@)
+                  -> DNSType  -- ^ the type of the question
+                  -> P.Put
+serialiseQuestion s ty = do
+  P.putByteString s
+  P.putWord16be $ fromIntegral $ fromEnum ty
+  P.putWord16be 1
+
+parseQuestion :: B.ByteString -> G.Get (String, DNSType)
+parseQuestion packet = do
+  name <- parseDNSName packet
+  ty <- parseDNSType
+  G.getWord16be
+
+  return (fromDNSName name, ty)
+
+parseDNSType :: G.Get DNSType
+parseDNSType = do
+  ty <- G.getWord16be >>= return . toEnum . fromIntegral
+  case ty of
+       UnknownDNSType -> fail "Unknown DNS type in question"
+       _ -> return ty
+
+deserialiseQuestion :: B.ByteString -> G.Get ([String], DNSType)
+deserialiseQuestion packet = do
+  name <- parseDNSName packet
+  ty <- parseDNSType
+  G.getWord16be
+
+  return (name, ty)
+
+parseGenericRR :: B.ByteString -> G.Get ([String], DNSType, Word32, B.ByteString)
+parseGenericRR packet = do
+  name <- parseDNSName packet
+  ty <- parseDNSType
+  clas <- G.getWord16be
+  when (clas /= 1) $ fail "Bad class in RR"
+  ttl <- G.getWord32be
+  rlen <- G.getWord16be
+  bytes <- G.getByteString $ fromIntegral rlen
+
+  return (name, ty, ttl, bytes)
+
+type Entry = ([String], Word32, RR)
+
+parseRR :: B.ByteString -> G.Get Entry
+parseRR packet = do
+  (name, ty, ttl, bytes) <- parseGenericRR packet
+  let parseMany :: G.Get a -> G.Get [a]
+      parseMany parser = do
+        emptyp <- G.isEmpty
+        if emptyp
+           then return []
+           else do v <- parser
+                   rest <- parseMany parser
+                   return $ v : rest
+      parseIP = G.getWord32be >>= return . htonl
+      parseA = parseMany parseIP
+      parseAAAA = parseMany $ do
+        a <- parseIP
+        b <- parseIP
+        c <- parseIP
+        d <- parseIP
+        return (a, b, c, d)
+
+      parseName = parseDNSName packet
+      parseMX = parseMany $ do
+        pref <- G.getWord16be
+        name <- parseDNSName packet
+        return (fromIntegral pref, name)
+
+      parseSOA = do
+        name <- parseDNSName packet
+        rname <- parseDNSName packet
+        serial <- G.getWord32be
+        refresh <- G.getWord32be
+        retry <- G.getWord32be
+        expire <- G.getWord32be
+        minimum <- G.getWord32be
+
+        return $ RRSOA name rname serial refresh retry expire minimum
+
+      parseTXT = do
+        length <- G.getWord8
+        G.getByteString (fromIntegral length) >>= return
+
+  let parse = case ty of
+                   A -> parseA >>= return . RRA
+                   NS -> parseName >>= return . RRNS
+                   CNAME -> parseName >>= return . RRCNAME
+                   SOA -> parseSOA
+                   PTR -> parseName >>= return . RRPTR
+                   MX -> parseMX >>= return . RRMX
+                   TXT -> parseTXT >>= return . RRTXT
+                   AAAA -> parseAAAA >>= return . RRAAAA
+  let (err, _) = G.runGet parse bytes
+  case err of
+       Left error -> fail error
+       Right rr -> return (name, ttl, rr)
+
+data Packet = Packet Header [(String, DNSType)] [Entry] [Entry] [Entry]
+            deriving (Show)
+
+parsePacket :: B.ByteString -> Either String Packet
+parsePacket input = fst $ G.runGet (do
+  header <- parseHeader
+  a <- sequence $ replicate (headQuestionCount header) $ parseQuestion input
+  b <- sequence $ replicate (headAnswerCount header) $ parseRR input
+  c <- sequence $ replicate (headNSCount header) $ parseRR input
+  d <- sequence $ replicate (headAdditionalCount header) $ parseRR input
+
+  return $ Packet header a b c d) input
diff --git a/Network/DNS/ResolveConfParse.hs b/Network/DNS/ResolveConfParse.hs
new file mode 100644
--- /dev/null
+++ b/Network/DNS/ResolveConfParse.hs
@@ -0,0 +1,119 @@
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+module Network.DNS.ResolveConfParse
+  ( ResolveConf(..)
+  , parseResolveConf
+  ) where
+
+import Data.Word
+import Data.Bits
+import Text.ParserCombinators.Parsec
+import System.Posix.Unistd (getSystemID, nodeName)
+
+import Network.DNS.Common
+
+-- | This is the result of parsing a resolv.conf like file. The search path is
+--   either taken from a "search" line in the config, a "domain" line in the
+--   config or from the domainname of the current host. If it's taken from the
+--   config, the last line takes precedent. It's in a form like:
+--
+--   > [["london", "myorg", "org"], ["myorg", "org"]]
+--
+--   The maybe members are set to the found values if they are there. Otherwise
+--   they are Nothing.
+data ResolveConf =
+  ResolveConf { resolveNameservers :: [Word32]  -- ^ a list of IPs in big-endian format
+              , resolveSearch :: [[String]]  -- ^ the search path
+              , resolveNdots :: Maybe Int
+              , resolveTimeout :: Maybe Int
+              , resolveAttempts :: Maybe Int
+              } deriving (Show)
+
+comment = do
+  char '#'
+  skipMany (noneOf "\n")
+  return '\n'
+
+ws = many (char ' ' <|> char '\t' <|> char '\n' <|> try comment)
+sep = many (char ' ' <|> char '\t')
+
+octet = do
+  s <- many1 $ oneOf "0123456789"
+  let n = (read s) :: Word32
+  if n > 255
+     then fail "Octet of IP address out of range"
+     else return n
+
+int = do
+  s <- many1 $ oneOf "0123456789"
+  return ((read s) :: Int)
+
+nonws = many1 (noneOf " \t\n")
+
+nameserverLine = do
+  string "nameserver"
+  sep
+  a <- octet
+  char '.'
+  b <- octet
+  char '.'
+  c <- octet
+  char '.'
+  d <- octet
+
+  let ip = htonl $ (a `shiftL` 24) .|. (b `shiftL` 16) .|. (c `shiftL` 8) .|. d
+
+  getState >>= (\st -> setState $ st { resolveNameservers = ip : resolveNameservers st })
+
+domainLine = do
+  string "domain"
+  sep
+  domain <- many1 (noneOf "\n")
+
+  getState >>= (\st -> setState $ st { resolveSearch = [splitDNSName domain] })
+
+searchLine = do
+  string "search"
+  sep
+  domains <- sepBy1 nonws sep
+
+  getState >>= (\st -> setState $ st { resolveSearch = map splitDNSName domains })
+
+optionsLine = do
+  string "options"
+  sep
+  let optionChunk = try ndots <|> try attempts <|> try timeout <|> (nonws >> return ())
+      ndots = string "ndots:" >> int >>= (\n -> getState >>= (\st -> setState $ st { resolveNdots = Just n }))
+      attempts = string "attempts:" >> int >>= (\n -> getState >>= (\st -> setState $ st { resolveAttempts = Just n }))
+      timeout = string "timeout:" >> int >>= (\n -> getState >>= (\st -> setState $ st { resolveTimeout = Just n }))
+  sepBy optionChunk sep
+  return ()
+
+parseLine = do
+  try nameserverLine <|> try domainLine <|> try optionsLine <|> try searchLine <|> (nonws >> return ())
+
+parseResolveConf' defaultSearch = do
+  ws
+  sepEndBy parseLine ws
+  ws
+  eof
+  st <- getState
+  -- if resolv.conf didn't include a search path or domain line we use the
+  -- default state from the domainname of the host
+  let st' = if null $ resolveSearch st
+               then st { resolveSearch = defaultSearch }
+               else st
+      st'' = if null $ resolveNameservers st
+                then st' { resolveNameservers = [htonl 0x7f000001] }
+                else st'
+  return st''
+
+-- | Parse a resolv.conf like file.
+parseResolveConf filename = do
+  -- we need to get the default search path from the domain name
+  node <- getSystemID >>= return . splitDNSName . nodeName
+  let defaultSearch =
+        if length node < 2
+           then []
+           else [tail node]
+  input <- readFile filename
+  return $ runParser (parseResolveConf' defaultSearch) (ResolveConf [] [] Nothing Nothing Nothing) filename input
diff --git a/network-dns.cabal b/network-dns.cabal
--- a/network-dns.cabal
+++ b/network-dns.cabal
@@ -1,5 +1,5 @@
 name:            network-dns
-version:         0.1
+version:         0.1.1
 license:         BSD3
 license-file:    LICENSE
 author:          Adam Langley <agl@imperialviolet.org>
@@ -15,6 +15,8 @@
 stability:       provisional
 tested-with:     GHC == 6.8.2
 exposed-modules: Network.DNS.Client, Network.DNS.Types
+extra-source-files: Network/DNS/ResolveConfParse.hs,
+                    Network/DNS/Common.hs
 extensions:      ForeignFunctionInterface
 ghc-options:     -Wall -fno-warn-name-shadowing -fwarn-tabs
 build-type:      Simple
