network-dns (empty) → 0.1
raw patch · 5 files changed
+588/−0 lines, 5 filesdep +arraydep +basedep +binarysetup-changed
Dependencies added: array, base, binary, binary-strict, bytestring, containers, control-timeout, network, network-bytestring, parsec, random, stm, time, unix
Files
- LICENSE +30/−0
- Network/DNS/Client.hs +450/−0
- Network/DNS/Types.hs +85/−0
- Setup.lhs +3/−0
- network-dns.cabal +20/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) Adam Langley++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of his contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ Network/DNS/Client.hs view
@@ -0,0 +1,450 @@+-- | A DNS resolver. This code acts like the resolver library from libc, except+-- that it can work asynchronously, and can return much more information.+--+-- At the moment, the interface is very much undecided but currently looks+-- like this:+--+-- > import qualified Network.DNS.Client as DNS+-- >+-- > DNS.resolve DNS.A "somedomain.com"+-- > Right [(2008-02-01 00:27:14.861098 UTC, DNS.RRA [2466498203])]+--+-- The first element of the tuple is the time when the information expires.+-- The second depends on the record type requested (A, in this case) and+-- A records contain IP address, so that's a HostAddress in there.+--+-- This module parses @/etc/resolv.conf@ for it's configuration. It needs a+-- recursive server to do the hard work. If you're lacking a recursive+-- server, you can setup dnscache (from djbdns) locally and point at that.++module Network.DNS.Client+ ( module Network.DNS.Types+ , resolve+ , resolveAsync+ , DNSError(..)+ ) where++import Data.Word+import Data.List (nub)+import Data.Maybe (fromMaybe)+import Data.Time+import Control.Monad (when)+import Control.Timeout+import Control.Concurrent (forkIO)+import System.IO.Unsafe (unsafePerformIO)+import System.Random (mkStdGen, random, Random, randomR)+import Control.Concurrent.STM++import qualified Data.Binary.Get as G++import qualified Data.Map as Map++import Network.Socket hiding (sendTo, recvFrom)+import Network.Socket.ByteString++import qualified Data.Binary.Put as P+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL++import Network.DNS.Common+import Network.DNS.ResolveConfParse+import Network.DNS.Types++queryHeader :: Word16 -> Header+queryHeader id = Header id False QUERY False False True False NoError 1 0 0 0++-- | This is the type of errors from the library. Either it's one of the first+-- two errors (which are generated from within this code), or it's an error+-- directly from the DNS server.+data DNSError = Timeout -- ^ the DNS server didn't answer+ | AnswerNotIncluded+ -- ^ this is returned when the DNS server returned a valid+ -- answer, but the answer didn't include the information we+ -- were looking for. Firstly, this isn't a recursive+ -- resolver, so if you point it at a non-recursive server+ -- you'll get this for nearly every query as the server will+ -- just be telling us the location of the roots.+ --+ -- This can also occur when you ask for a resource which+ -- doesn't exist - like a AAAA record from www.google.com+ | DNSError ResponseCode -- ^ errors from the DNS server+ deriving (Show, Eq)++data InflightRequest =+ InflightRequest { infRequest :: B.ByteString -- ^ the question section+ , infCallback :: (Either DNSError ([String], ([Entry], [Entry], [Entry])) -> IO ())+ , infCurrent :: [String] -- ^ the current name being queried+ , infSearch :: [[String]] -- ^ alternative names to try+ , infAttempts :: Int -- ^ number of times transmitted+ , infTimeout :: TimeoutTag -- ^ the timeout+ , infType :: DNSType -- ^ the type of the query+ -- | In the case that we need to always send this request to+ -- a specific nameserver (e.g. it's a probe packet), this+ -- is set to a non-Nothing value.+ , infSpecificNameserver :: Maybe Nameserver+ }++data Nameserver = Nameserver { nsAddress :: Word32 -- ^ ip address+ , nsUp :: TVar Bool -- ^ do we believe that this server is up?+ , nsInflight :: TVar (Map.Map Word16 InflightRequest)+ , nsTimeouts :: TVar Int -- ^ how many requests have timed out in a row+ }++data ResolverConfig = ResolverConfig+ { rcNameservers :: [Nameserver]+ -- | This is a list of lists of labels. If a requested domain name is short+ -- enougth (see ndots) it's tried with these suffixes first+ , rcSearchPath :: [[String]]+ -- | If a name has this many dots, we try an initial absolute query+ , rcNdots :: Int+ -- | Number of attempts per request+ , rcAttempts :: Int+ -- | Number of seconds to wait for the nameserver+ , rcTimeout :: Int+ -- | This is the next nameserver to try+ , rcRobin :: TVar Int+ -- | This is an infinite list of random seeds+ , rcSeeds :: TVar [Int]+ }++-- | This is the max number of requests which can be outstanding against+-- any one server at a time. Since, to generate ids, we roll until we find+-- a good id setting this > 60000 is dangerous. If it hit 2**16 we would+-- livelock+maxInflightPerServer :: Int+maxInflightPerServer = 2048++-- | This is a socket that is bound, locally, to a port and all outgoing+-- packets are written to it. It's also the socket that we listen on.+globalSocket :: Socket+globalSocket = unsafePerformIO $ do+ s <- socket AF_INET Datagram 0+ bindSocket s $ SockAddrInet (PortNum 0) iNADDR_ANY+ return s++-- | Lookup a reply from the network+lookupReply :: ResolverConfig+ -> Word16 -- ^ The id of the reply+ -> Word32 -- ^ The IP address of the source+ -> IO (Maybe (InflightRequest, Nameserver))+lookupReply config id addr = do+ case filter (\x -> nsAddress x == addr) $ rcNameservers config of+ [] -> return Nothing+ (ns:_) -> do+ minf <- atomically (do+ inflight <- readTVar $ nsInflight ns+ let (minf, inflight') = Map.updateLookupWithKey (const $ const Nothing) id inflight+ writeTVar (nsInflight ns) inflight'+ -- cancel the timeout if we found an inflight+ case minf of+ Just inf -> cancelTimeout $ infTimeout inf+ _ -> return False+ return minf)+ return $ minf >>= (\inf -> return (inf, ns))++-- | This function never returns an it's expected that it runs in it's own+-- thread, forever reading from the network.+readerThread :: Socket -> IO ()+readerThread socket = do+ (bytes, SockAddrInet _ addr) <- recvFrom socket 1500+ case parsePacket bytes of+ Left _ -> readerThread socket+ Right (Packet header _ ans nses additional) -> do+ if (headIsResponse header == False) || (headIsTruncated header)+ then readerThread socket+ else do config <- getResolveConfig+ minfns <- lookupReply config (headId header) addr+ case minfns of+ Nothing -> readerThread socket+ Just (inf, ns) -> do+ case headResponseCode header of+ ServerError -> handleTransientError config inf $ DNSError ServerError+ NoError -> handleReply config inf ns (ans, nses, additional)+ x -> handleFailure config inf $ DNSError x++ readerThread socket++-- | There can only ever really be a single config in action at any one time,+-- because there is only a single @readerThread@. One could imaging having a+-- socket bound to get packets only from a single nameserver, and that would+-- work so long as the sets of nameservers didn't overlap. However, I don't+-- think that's a common requirement, so I don't support it.+globalConfig :: TVar (Maybe ResolverConfig)+globalConfig = unsafePerformIO $ newTVarIO Nothing++-- | Build a ResolverConfig by parsing @/etc/resolv.conf@+resolverConfigFromResolvConf :: IO ResolverConfig+resolverConfigFromResolvConf = do+ Right resolvconf <- parseResolveConf "/etc/resolv.conf"+ robin <- atomically $ newTVar 0+ let toNameserver ip = do+ up <- atomically $ newTVar True+ inflight <- atomically $ newTVar Map.empty+ timeouts <- atomically $ newTVar 0+ return $ Nameserver ip up inflight timeouts++ -- For the list of seeds we have a lazy IO on /dev/urandom going through a+ -- Get monad, which parses 4 byte lumps as Ints+ urandom <- BL.readFile "/dev/urandom"+ let urandomParser :: G.Get [Int]+ urandomParser = do+ v <- G.getWord32be+ rest <- urandomParser+ return $ fromIntegral v : rest+ seeds = G.runGet urandomParser urandom++ tseeds <- atomically $ newTVar seeds++ ns <- mapM toNameserver $ nub $ resolveNameservers resolvconf+ return $ ResolverConfig ns+ (resolveSearch resolvconf)+ (fromMaybe 1 (resolveNdots resolvconf))+ (fromMaybe 2 (resolveAttempts resolvconf))+ (fromMaybe 5 (resolveTimeout resolvconf))+ robin+ tseeds++-- | Pick a nameserver from a config. We try to round robin the nameservers,+-- avoiding the down nameservers. If all nameservers are down, we just pick+-- one anyway.+selectNameserver :: ResolverConfig -> STM Nameserver+selectNameserver config = do+ robin <- readTVar $ rcRobin config+ writeTVar (rcRobin config) $ (robin + 1) `mod` (length (rcNameservers config))+ let servers = (drop robin $ rcNameservers config) +++ (take robin $ rcNameservers config)+ servers' <- mapM (\x -> readTVar (nsUp x) >>= \up -> return (up, x)) servers+ case filter fst servers' of+ [] -> return $ head servers -- all down, just pick one+ x:_ -> return $ snd x++instance Random Word16 where+ random g = (fromIntegral result, g') where+ result :: Int+ (result, g') = randomR (0, 65535) g+ randomR (lo, hi) g = (fromIntegral result, g') where+ result :: Int+ (result, g') = randomR (fromIntegral lo, fromIntegral hi) g++submit4 :: ResolverConfig+ -> InflightRequest+ -> (IO () -> STM TimeoutTag)+ -> STM (Word16, Word32)+submit4 config inf mtag = do+ -- We cannot reuse a id number if we already have an inflight request to the+ -- same nameserver with the same id. Thus, once we have picked a nameserver+ -- we need to generate a stream of numbers until we find one which isn't+ -- currently in use. So we generate a strong random seed and use it with the+ -- standard Haskell PRNG to generate the stream of ids in the STM monad+ ns <- case infSpecificNameserver inf of+ Nothing -> selectNameserver config+ Just ns -> return ns+ inflight <- readTVar $ nsInflight ns+ when (Map.size inflight > maxInflightPerServer) retry+ seeds <- readTVar (rcSeeds config)+ let seed = head seeds+ writeTVar (rcSeeds config) $ tail seeds+ let prng = mkStdGen seed+ f prng = r where+ r = if Map.member candidate inflight+ then f prng'+ else candidate+ (candidate, prng') = random prng+ id = f prng+ addr = nsAddress ns+ tag <- mtag $ handleTimeout config id addr+ let inf' = inf { infTimeout = tag, infAttempts = infAttempts inf + 1 }+ writeTVar (nsInflight ns) $ Map.insert id inf' inflight+ return (id, addr)++-- | This is the maximum number of timeouts, in a row, that can happen from a+-- single nameserver before we consider that server to be down. When we mark+-- a server as down we start sending probes to it. Once it responds to one of+-- those probes with anything save a Timeout, we'll mark it good again.+maxTimeoutsPerServer :: Int+maxTimeoutsPerServer = 5++-- | This is the callback from a probe request. If it came back with a+-- reasonable reply we mark the nameserver as up+probeCallback :: Nameserver+ -> Either DNSError ([String], ([Entry], [Entry], [Entry])) -> IO ()+probeCallback ns result =+ case result of+ Left Timeout -> (addTimeout 60 $ probeNameserver ns) >> return ()+ _ -> atomically (writeTVar (nsUp ns) True)++-- | This is a timeout callback. The timeout is called when we mark a+-- nameserver as down. This timeout is to start a probe. If the probe returns+-- any kind of DNS packet, we call the nameserver up again.+probeNameserver :: Nameserver -> IO ()+probeNameserver ns = do+ config <- getResolveConfig+ let labels = ["www", "google", "com"]+ Just name = serialiseDNSName labels+ req = B.concat $ BL.toChunks $ P.runPut $ serialiseQuestion name A+ inf = InflightRequest req (probeCallback ns) labels [labels] 1 undefined A $ Just ns+ transmit config inf++handleTimeout :: ResolverConfig -- ^ the config when the request started+ -> Word16 -- ^ the id of the request which has timed out+ -> Word32 -- ^ the IP address of the nameserver+ -> IO ()+handleTimeout config id addr = do+ -- There has to be a nameserver with the correct IP address because+ -- we chose it from this same config+ minfns <- lookupReply config id addr+ case minfns of+ Nothing -> return () -- we raced the network and lost+ Just (inf, ns) -> do+ -- First, deal with the nameserver. If it's been handling a lot of failures+ -- recently we might want to consider it down+ timeouts <- atomically $ do+ timeouts <- readTVar $ nsTimeouts ns+ writeTVar (nsTimeouts ns) $ timeouts + 1+ return timeouts+ when (timeouts > maxTimeoutsPerServer) $ do+ mtimeout <- addTimeoutAtomic 60+ atomically $ do+ upflag <- readTVar $ nsUp ns+ when (upflag == True) $ do+ writeTVar (nsUp ns) False+ mtimeout $ probeNameserver ns+ return ()++ handleTransientError config inf Timeout++handleReply :: ResolverConfig -> InflightRequest -> Nameserver -> ([Entry], [Entry], [Entry]) -> IO ()+handleReply _ inf ns answers = do+ -- we got a good reply from a nameserver, mark as up and good+ atomically $ do+ writeTVar (nsTimeouts ns) 0+ writeTVar (nsUp ns) True++ (infCallback inf) $ Right (infCurrent inf, answers)++transmit :: ResolverConfig -> InflightRequest -> IO ()+transmit config inf = do+ mtag <- addTimeoutAtomic $ fromIntegral $ rcTimeout config+ (id, addr) <- atomically $ submit4 config inf mtag+ let header = B.concat $ BL.toChunks $ P.runPut $ serialiseHeader $ queryHeader id+ query = header `B.append` infRequest inf+ sendTo globalSocket query $ SockAddrInet 53 addr+ -- FIXME: check return value?+ return ()++-- | Increment the attempts, pop the next name from the search list and+-- transmit the given inflight+sendInflight :: ResolverConfig -> InflightRequest -> IO ()+sendInflight config inf =+ if null $ infSearch inf+ then (infCallback inf) $ Left $ DNSError NXDomain+ else do let inf' = inf { infSearch = tail $ infSearch inf, infAttempts = 1 }+ target = head $ infSearch inf+ case serialiseDNSName target of+ Nothing -> sendInflight config inf'+ Just name -> do+ transmit config $ inf' { infRequest = B.concat $ BL.toChunks $ P.runPut $ serialiseQuestion name $ infType inf+ , infCurrent = target }++-- | This is for when we get a timeout or ServerError - there might not+-- be anything wrong with the query so we retry it.+handleTransientError :: ResolverConfig -> InflightRequest -> DNSError -> IO ()+handleTransientError config inf result = do+ if infAttempts inf > rcAttempts config+ then handleFailure config inf result+ else transmit config $ inf { infAttempts = 1 + infAttempts inf }++-- | This is for when we have a harder error - either multiple timeouts or+-- NXDomain etc from the server. The timeout must have been canceled by here+handleFailure :: ResolverConfig -> InflightRequest -> DNSError -> IO ()+handleFailure config inf result = do+ if not $ null $ infSearch inf+ then sendInflight config inf+ else (infCallback inf) $ Left result++type DNSDB = Map.Map ([String], DNSType) [(UTCTime, RR)]++answersToDB :: UTCTime -> ([Entry], [Entry], [Entry]) -> DNSDB+answersToDB currentTime (ans, nses, additional) =+ Map.unionsWith (++) $ map toMap [ans, nses, additional] where+ toMap :: [Entry] -> Map.Map ([String], DNSType) [(UTCTime, RR)]+ toMap = Map.fromListWith (++) . map (\(host, secs, rr) -> ((host, rrToType rr), [(t secs, rr)]))+ t :: Word32 -> UTCTime+ t = (flip addUTCTime) currentTime . fromIntegral++dbGet :: [String] -> DNSType -> DNSDB -> [(UTCTime, RR)]+dbGet host ty db = find host (0 :: Int) where+ -- this limits the CNAME depth+ find _ 16 = []+ find host n =+ case Map.lookup (host, ty) db of+ Just x -> x+ Nothing -> case Map.lookup (host, CNAME) db of+ Nothing -> []+ Just ((_, RRCNAME host'):_) -> find host' (n + 1)++-- | This has to turn the information from the server into something+-- useful for the caller. At the moment this is pretty stupid, just+-- grab A records from the answer section+parseQuery :: DNSType+ -> (Either DNSError [(UTCTime, RR)] -> IO ())+ -> Either DNSError ([String], ([Entry], [Entry], [Entry]))+ -> IO ()+parseQuery ty cb e =+ case e of+ Left x -> cb $ Left x+ Right (host, answers) -> do+ currentTime <- getCurrentTime+ let db = answersToDB currentTime answers+ case dbGet host ty db of+ [] -> cb $ Left AnswerNotIncluded+ x -> cb $ Right x++getResolveConfig :: IO ResolverConfig+getResolveConfig = do+ config <- atomically $ readTVar globalConfig+ case config of+ Nothing -> do+ config <- resolverConfigFromResolvConf+ (set, config') <- atomically $ do+ mconfig <- readTVar globalConfig+ case mconfig of+ Nothing -> do+ writeTVar globalConfig $ Just config+ return (True, config)+ Just config'' -> return (False, config'')+ when set (forkIO (readerThread globalSocket) >> return ())+ return config'++ Just config' -> return config'++-- | Lookup some information from DNS+resolve :: DNSType -- ^ the type of DNS information requested+ -> String -- ^ the domain to query+ -> IO (Either DNSError [(UTCTime, RR)]) -- ^ The RR values here will+ -- always be of the correct+ -- type for the requested+ -- DNSType+resolve ty hostname = do+ var <- atomically $ newEmptyTMVar+ resolveAsync ty hostname (atomically . putTMVar var)+ atomically $ takeTMVar var++-- | This is the same as resolve, below, put you get the answer asynchronously.+-- Blocking the thread which makes the callback in this case is bad - it'll+-- block the DNS network reading thread.+resolveAsync :: DNSType+ -> String+ -> (Either DNSError [(UTCTime, RR)] -> IO ())+ -> IO ()+resolveAsync ty host cb = do+ config <- getResolveConfig+ let labels = splitDNSName host+ wrappedCb = parseQuery ty cb+ inf = InflightRequest undefined wrappedCb labels [] 1 undefined ty Nothing+ -- get the list of names that we'll try to resolve, in order.+ -- if we are searching, that could be a list of length > 1+ let names = if last host /= '.' && length labels - 1 < rcNdots config && length (rcSearchPath config) > 0+ then (map ((++) labels) $ rcSearchPath config) ++ [labels]+ else [labels]+ sendInflight config $ inf { infSearch = names }
+ Network/DNS/Types.hs view
@@ -0,0 +1,85 @@+-- | Contains DNS types which users of the DNS library may need to use. Private+-- types are mostly kept in Network.DNS.Common++module Network.DNS.Types+ ( DNSType(..)+ , ResponseCode(..)+ , RR(..)+ , rrToType+ ) where++import qualified Data.ByteString as B+import Network.Socket (HostAddress, HostAddress6)+import Data.Word (Word32)++-- | Types of DNS resources. RFC 1035, 3.2.2.+data DNSType = A | NS | CNAME | SOA | PTR | MX | TXT | AAAA+ | UnknownDNSType -- ^ This is for internal error handling+ -- and should never be used or seen outside+ -- Network.DNS+ deriving (Show, Eq, Ord)++instance Enum DNSType where+ fromEnum A = 1+ fromEnum NS = 2+ fromEnum CNAME = 5+ fromEnum SOA = 6+ fromEnum PTR = 12+ fromEnum MX = 15+ fromEnum TXT = 16+ fromEnum AAAA = 28+ fromEnum UnknownDNSType = error "fromEnum of UnknownDNSType"++ toEnum 1 = A+ toEnum 2 = NS+ toEnum 5 = CNAME+ toEnum 6 = SOA+ toEnum 12 = PTR+ toEnum 15 = MX+ toEnum 16 = TXT+ toEnum 28 = AAAA+ toEnum _ = UnknownDNSType++-- | Error codes. RFC 1035, 4.1.1.+data ResponseCode = NoError+ -- | The name server was unable to interpret the query+ | FormatError+ -- | The name server was unable to process this query due to a problem with the name server+ | ServerError+ -- | Meaningful only for responses from an authoritative name server, this code signifies that the domain name referenced in the query does not exist+ | NXDomain+ -- | The name server does not support the requested kind of query.+ | NotImplemented+ -- | The name server refuses to perform the specified operation for policy reasons. For example, a name server may not wish to provide the information to the particular requester, or a name server may not wish to perform a particular operation (e.g., zone transfer) for particular data.+ | AccessDenied+ deriving (Show, Eq, Enum, Bounded)++-- | Resource record types. There store the actual data in the DNS database.+-- There's one for each DNSType+data RR = RRCNAME [String]+ | RRMX [(Int, [String])] -- ^ a list of preferences and hostnames+ | RRNS [String]+ | RRPTR [String]+ | RRSOA { soaName :: [String]+ , soaRname :: [String]+ , soaSerial :: Word32+ , soaRefresh :: Word32+ , soaRetry :: Word32+ , soaExpire :: Word32+ , soaMinTTL :: Word32 }+ | RRTXT B.ByteString+ | RRA [HostAddress]+ | RRAAAA [HostAddress6]+ deriving (Show)++-- | Given an RR, this tells you the resource type of it+rrToType :: RR -> DNSType+rrToType (RRCNAME _) = CNAME+rrToType (RRMX _) = MX+rrToType (RRNS _) = NS+rrToType (RRPTR _) = PTR+rrToType (RRSOA { }) = SOA+rrToType (RRTXT _) = TXT+rrToType (RRA _) = A+rrToType (RRAAAA _) = AAAA+
+ Setup.lhs view
@@ -0,0 +1,3 @@+#!/usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMain
+ network-dns.cabal view
@@ -0,0 +1,20 @@+name: network-dns+version: 0.1+license: BSD3+license-file: LICENSE+author: Adam Langley <agl@imperialviolet.org>+maintainer: Adam Langley <agl@imperialviolet.org>+homepage: http://darcs.imperialviolet.org/network-dns+description: A pure Haskell, asyncronous DNS client library+synopsis: A pure Haskell, asyncronous DNS client library+category: Network+build-depends: base, containers, array, bytestring>=0.9, binary-strict>=0.2.4,+ control-timeout>=0.1.2, network>=2.1, network-bytestring,+ binary>=0.4.1, unix>=2.3, parsec>=2.1, stm>=2.1, random>=1.0,+ time>=1.1+stability: provisional+tested-with: GHC == 6.8.2+exposed-modules: Network.DNS.Client, Network.DNS.Types+extensions: ForeignFunctionInterface+ghc-options: -Wall -fno-warn-name-shadowing -fwarn-tabs+build-type: Simple