diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Dustin Sallings (c) 2018
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * 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.
+
+    * Neither the name of Dustin Sallings nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 COPYRIGHT
+OWNER 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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# mqtt
+
+An [MQTT][mqtt] protocol implementation for Haskell.
+
+## Client Examples
+
+### Publish
+
+```haskell
+main :: IO
+main = do
+  mc <- runClient mqttConfig{}
+  publish mc "tmp/topic" "hello!" False
+```
+
+### Subscribe
+
+```haskell
+main :: IO
+main = do
+  mc <- runClient mqttConfig{_msgCB=Just msgReceived}
+  print =<< subscribe mc [("tmp/topic1", QoS0), ("tmp/topic2", QoS0)]
+  print =<< waitForClient mc   -- wait for the the client to disconnect
+
+  where
+    msgReceived t m = print (t,m)
+```
+
+[mqtt]: http://mqtt.org/
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import           Control.Concurrent       (threadDelay)
+import           Control.Concurrent.Async (async, cancel)
+import           Control.Monad            (forever)
+
+import           Network.MQTT.Client
+
+main :: IO ()
+main = do
+  mc <- runClient mqttConfig{_hostname="localhost", _port=1883, _connID="hasqttl",
+                             -- _cleanSession=False,
+                             _lwt=Just $ mkLWT "tmp/haskquit" "bye for now" False,
+                             _msgCB=Just showme}
+  putStrLn "connected!"
+  print =<< subscribe mc [("oro/#", QoS0), ("tmp/#", QoS2)]
+  p <- async $ forever $ publish mc "tmp/mqtths" "hi from haskell" False >> threadDelay 10000000
+
+  putStrLn "Publishing at QoS > 0"
+  publishq mc "tmp/q" "this message is at QoS 2" False QoS2
+  putStrLn "Published!"
+
+  print =<< waitForClient mc
+  cancel p
+
+    where showme t m = print (t,m)
diff --git a/net-mqtt.cabal b/net-mqtt.cabal
new file mode 100644
--- /dev/null
+++ b/net-mqtt.cabal
@@ -0,0 +1,100 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.1.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 2e4a3950e20e2a35101f270d00e002870b13f22dc165ced24aa166762c501924
+
+name:           net-mqtt
+version:        0.1.0.0
+synopsis:       An MQTT Protocol Implementation.
+description:    Please see the README on GitHub at <https://github.com/dustin/mqtt#readme>
+category:       Network
+homepage:       https://github.com/dustin/mqtt#readme
+bug-reports:    https://github.com/dustin/mqtt/issues
+author:         Dustin Sallings
+maintainer:     dustin@spy.net
+copyright:      BSD3
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/dustin/mqtt
+
+library
+  exposed-modules:
+      Network.MQTT.Client
+      Network.MQTT.Types
+  other-modules:
+      Paths_net_mqtt
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      async >=2.2.1 && <2.3
+    , attoparsec >=0.13.2 && <0.14
+    , base >=4.7 && <5
+    , binary >=0.8.6 && <0.9
+    , bytestring >=0.10.8 && <0.11
+    , conduit >=1.3.1 && <1.4
+    , conduit-extra >=1.3.0 && <1.4
+    , containers >=0.6.0 && <0.7
+    , network-conduit-tls >=1.3.2 && <1.4
+    , stm >=2.5.0 && <2.6
+    , text >=1.2.3 && <1.3
+  default-language: Haskell2010
+
+executable mqtt
+  main-is: Main.hs
+  other-modules:
+      Paths_net_mqtt
+  hs-source-dirs:
+      app
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      async >=2.2.1 && <2.3
+    , attoparsec >=0.13.2 && <0.14
+    , base >=4.7 && <5
+    , binary >=0.8.6 && <0.9
+    , bytestring >=0.10.8 && <0.11
+    , conduit >=1.3.1 && <1.4
+    , conduit-extra >=1.3.0 && <1.4
+    , containers >=0.6.0 && <0.7
+    , net-mqtt
+    , network-conduit-tls >=1.3.2 && <1.4
+    , stm >=2.5.0 && <2.6
+    , text >=1.2.3 && <1.3
+  default-language: Haskell2010
+
+test-suite mqtt-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_net_mqtt
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HUnit
+    , QuickCheck
+    , async >=2.2.1 && <2.3
+    , attoparsec >=0.13.2 && <0.14
+    , base >=4.7 && <5
+    , binary >=0.8.6 && <0.9
+    , bytestring >=0.10.8 && <0.11
+    , conduit >=1.3.1 && <1.4
+    , conduit-extra >=1.3.0 && <1.4
+    , containers >=0.6.0 && <0.7
+    , net-mqtt
+    , network-conduit-tls >=1.3.2 && <1.4
+    , stm >=2.5.0 && <2.6
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , text >=1.2.3 && <1.3
+  default-language: Haskell2010
diff --git a/src/Network/MQTT/Client.hs b/src/Network/MQTT/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/MQTT/Client.hs
@@ -0,0 +1,348 @@
+{-|
+Module      : Network.MQTT.Client.
+Description : An MQTT client.
+Copyright   : (c) Dustin Sallings, 2019
+License     : BSD3
+Maintainer  : dustin@spy.net
+Stability   : experimental
+
+An MQTT protocol client, based on the 3.1.1 specification:
+<http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html>
+-}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+module Network.MQTT.Client (
+  -- * Configuring the client.
+  MQTTConfig(..), MQTTClient, QoS(..), Topic, mqttConfig,  mkLWT, LastWill(..),
+  -- * Running and waiting for the client.
+  runClient, runClientTLS, waitForClient,
+  disconnect,
+  -- * General client interactions.
+  subscribe, unsubscribe, publish, publishq
+  ) where
+
+import           Control.Concurrent         (threadDelay)
+import           Control.Concurrent.Async   (Async, async, cancel, cancelWith,
+                                             race_, wait, waitCatch, withAsync)
+import           Control.Concurrent.STM     (STM, TChan, TVar, atomically,
+                                             modifyTVar', newTChan, newTChanIO,
+                                             newTVarIO, readTChan, readTVar,
+                                             readTVarIO, retry, writeTChan,
+                                             writeTVar)
+import qualified Control.Exception          as E
+import           Control.Monad              (forever, void, when)
+import           Control.Monad.IO.Class     (liftIO)
+import qualified Data.ByteString.Char8      as BCS
+import qualified Data.ByteString.Lazy       as BL
+import qualified Data.ByteString.Lazy.Char8 as BC
+import           Data.Conduit               (runConduit, yield, (.|))
+import           Data.Conduit.Attoparsec    (conduitParser, sinkParser)
+import qualified Data.Conduit.Combinators   as C
+import           Data.Conduit.Network       (AppData, appSink, appSource,
+                                             clientSettings, runTCPClient)
+import           Data.Conduit.Network.TLS   (runTLSClient, tlsClientConfig)
+import           Data.Map.Strict            (Map)
+import qualified Data.Map.Strict            as Map
+import           Data.Text                  (Text)
+import qualified Data.Text.Encoding         as TE
+import           Data.Word                  (Word16)
+
+
+import           Network.MQTT.Types         as T
+
+-- | Topic is a type alias for topic values.
+type Topic = Text
+
+data ConnState = Starting | Connected | Disconnected deriving (Eq, Show)
+
+data DispatchType = DSubACK | DUnsubACK | DPubACK | DPubREC | DPubREL | DPubCOMP
+  deriving (Eq, Show, Ord, Enum, Bounded)
+
+-- | The MQTT client.
+-- A client may be built using either runClient or runClientTLS.  For example:
+--
+-- @
+--   mc <- runClient mqttConfig{}
+--   publish mc "some/topic" "some message" False
+-- @
+--
+data MQTTClient = MQTTClient {
+  _ch      :: TChan MQTTPkt
+  , _pktID :: TVar Word16
+  , _cb    :: Maybe (Topic -> BL.ByteString -> IO ())
+  , _ts    :: TVar [Async ()]
+  , _acks  :: TVar (Map (DispatchType,Word16) (TChan MQTTPkt))
+  , _st    :: TVar ConnState
+  , _ct    :: TVar (Async ())
+  }
+
+-- | Configuration for setting up an MQTT client.
+data MQTTConfig = MQTTConfig{
+  _hostname       :: String -- ^ Host to connect to.
+  , _port         :: Int -- ^ Port number.
+  , _connID       :: String -- ^ Unique connection ID (required).
+  , _username     :: Maybe String -- ^ Optional username.
+  , _password     :: Maybe String -- ^ Optional password.
+  , _cleanSession :: Bool -- ^ False if a session should be reused.
+  , _lwt          :: Maybe LastWill -- ^ LastWill message to be sent on client disconnect.
+  , _msgCB        :: Maybe (Topic -> BL.ByteString -> IO ()) -- ^ Callback for incoming messages.
+  }
+
+-- | A default MQTTConfig.  A _connID /should/ be provided by the client in the returned config,
+-- but the defaults should work for testing.
+mqttConfig :: MQTTConfig
+mqttConfig = MQTTConfig{_hostname="localhost", _port=1883, _connID="haskell-mqtt",
+                        _username=Nothing, _password=Nothing,
+                        _cleanSession=True, _lwt=Nothing,
+                        _msgCB=Nothing}
+
+
+-- | Set up and run a client from the given config.
+runClient :: MQTTConfig -> IO MQTTClient
+runClient cfg@MQTTConfig{..} = runClientAppData (runTCPClient (clientSettings _port (BCS.pack _hostname))) cfg
+
+-- | Set up and run a client connected via TLS.
+runClientTLS :: MQTTConfig -> IO MQTTClient
+runClientTLS cfg@MQTTConfig{..} = runClientAppData (runTLSClient (tlsClientConfig _port (BCS.pack _hostname))) cfg
+
+-- | Set up and run a client from the given conduit AppData function.
+runClientAppData :: ((AppData -> IO ()) -> IO ()) -> MQTTConfig -> IO MQTTClient
+runClientAppData mkconn MQTTConfig{..} = do
+  ch <- newTChanIO
+  pid <- newTVarIO 0
+  thr <- newTVarIO []
+  acks <- newTVarIO mempty
+  st <- newTVarIO Starting
+  ct <- newTVarIO undefined
+
+  let cli = MQTTClient{_ch=ch,
+                       _cb=_msgCB,
+                       _pktID=pid,
+                       _ts=thr,
+                       _acks=acks,
+                       _st=st,
+                       _ct=ct}
+
+  t <- async $ clientThread cli
+  s <- atomically (waitForLaunch cli t)
+
+  when (s /= Connected) $ wait t
+
+  pure cli
+
+  where
+    clientThread cli = E.finally connectAndRun markDisco
+      where
+        connectAndRun = mkconn $ \ad ->
+          E.bracket (start cli ad) cancelAll (run ad)
+        markDisco = atomically $ writeTVar (_st cli) Disconnected
+
+    start c@MQTTClient{..} ad = do
+      runConduit $ do
+        let req = connectRequest{T._connID=BC.pack _connID,
+                                 T._lastWill=_lwt,
+                                 T._username=BC.pack <$> _username,
+                                 T._password=BC.pack <$> _password,
+                                 T._cleanSession=_cleanSession}
+        yield (BL.toStrict $ toByteString req) .| appSink ad
+        (ConnACKPkt (ConnACKFlags _ val)) <- appSource ad .| sinkParser parsePacket
+        case val of
+          ConnAccepted -> pure ()
+          x            -> fail (show x)
+
+      pure c
+
+    run ad c@MQTTClient{..} = do
+      o <- async processOut
+      p <- async doPing
+
+      atomically $ do
+        modifyTVar' _ts (\l -> o:p:l)
+        writeTVar _st Connected
+
+      runConduit $ appSource ad
+        .| conduitParser parsePacket
+        .| C.mapM_ (\(_,x) -> liftIO (dispatch c x))
+
+      where
+        processOut = runConduit $
+          C.repeatM (liftIO (atomically $ readTChan _ch))
+          .| C.map (BL.toStrict . toByteString)
+          .| appSink ad
+
+        doPing = forever $ threadDelay 30000000 >> sendPacketIO c PingPkt
+
+    waitForLaunch MQTTClient{..} t = do
+      writeTVar _ct t
+      c <- readTVar _st
+      if c == Starting then retry else pure c
+
+    cancelAll MQTTClient{..} = mapM_ cancel =<< readTVarIO _ts
+
+-- | Wait for a client to terminate its connection.
+waitForClient :: MQTTClient -> IO (Either E.SomeException ())
+waitForClient MQTTClient{..} = waitCatch =<< readTVarIO _ct
+
+data MQTTException = Timeout | BadData deriving(Eq, Show)
+
+instance E.Exception MQTTException
+
+dispatch :: MQTTClient -> MQTTPkt -> IO ()
+dispatch c@MQTTClient{..} pkt =
+  case pkt of
+    (PublishPkt p)                        -> pubMachine p
+    (SubACKPkt (SubscribeResponse i _))   -> delegate DSubACK i
+    (UnsubACKPkt (UnsubscribeResponse i)) -> delegate DUnsubACK i
+    (PubACKPkt (PubACK i))                -> delegate DPubACK i
+    (PubRECPkt (PubREC i))                -> delegate DPubREC i
+    (PubRELPkt (PubREL i))                -> delegate DPubREL i
+    (PubCOMPPkt (PubCOMP i))              -> delegate DPubCOMP i
+    PongPkt                               -> pure ()
+    x                                     -> print x
+
+  where delegate dt pid = atomically $ do
+          m <- readTVar _acks
+          case Map.lookup (dt, pid) m of
+            Nothing -> pure ()
+            Just ch -> writeTChan ch pkt
+
+        pubMachine PublishRequest{..}
+          | _pubQoS == QoS2 = void $ async manageQoS2
+          | _pubQoS == QoS1 = notify >> sendPacketIO c (PubACKPkt (PubACK _pubPktID))
+          | otherwise = notify
+
+          where
+            notify = case _cb of
+                       Nothing -> pure ()
+                       Just x  -> x (blToText _pubTopic) _pubBody
+
+            manageQoS2 = do
+              ch <- newTChanIO
+              atomically $ modifyTVar' _acks (Map.insert (DPubREL, _pubPktID) ch)
+              E.finally (manageQoS2' ch) (atomically $ releasePktID c (DPubREL, _pubPktID))
+                where
+                  manageQoS2' ch = do
+                    sendPacketIO c (PubRECPkt (PubREC _pubPktID))
+                    (PubRELPkt _) <- atomically $ readTChan ch
+                    notify
+                    sendPacketIO c (PubCOMPPkt (PubCOMP _pubPktID))
+
+sendPacket :: MQTTClient -> MQTTPkt -> STM ()
+sendPacket MQTTClient{..} p = do
+  st <- readTVar _st
+  when (st /= Connected) $ fail "not connected"
+  writeTChan _ch p
+
+sendPacketIO :: MQTTClient -> MQTTPkt -> IO ()
+sendPacketIO c = atomically . sendPacket c
+
+textToBL :: Text -> BL.ByteString
+textToBL = BL.fromStrict . TE.encodeUtf8
+
+blToText :: BL.ByteString -> Text
+blToText = TE.decodeUtf8 . BL.toStrict
+
+reservePktID :: MQTTClient -> [DispatchType] -> STM (TChan MQTTPkt, Word16)
+reservePktID MQTTClient{..} dts = do
+  ch <- newTChan
+  pid <- readTVar _pktID
+  modifyTVar' _pktID succ
+  modifyTVar' _acks (Map.union (Map.fromList [((t, pid), ch) | t <- dts]))
+  pure (ch,pid)
+
+releasePktID :: MQTTClient -> (DispatchType,Word16) -> STM ()
+releasePktID MQTTClient{..} k = modifyTVar' _acks (Map.delete k)
+
+releasePktIDs :: MQTTClient -> [(DispatchType,Word16)] -> STM ()
+releasePktIDs MQTTClient{..} ks = modifyTVar' _acks deleteMany
+  where deleteMany m = foldr Map.delete m ks
+
+sendAndWait :: MQTTClient -> DispatchType -> (Word16 -> MQTTPkt) -> IO MQTTPkt
+sendAndWait c@MQTTClient{..} dt f = do
+  (ch,pid) <- atomically $ do
+    (ch,pid) <- reservePktID c [dt]
+    sendPacket c (f pid)
+    pure (ch,pid)
+
+  -- Wait for the response in a separate transaction.
+  atomically $ releasePktID c (dt,pid) >> readTChan ch
+
+-- | Subscribe to a list of topics with their respective QoSes.  The
+-- accepted QoSes are returned in the same order as requested.
+subscribe :: MQTTClient -> [(Topic, QoS)] -> IO [Maybe QoS]
+subscribe c@MQTTClient{..} ls = do
+  r <- sendAndWait c DSubACK (\pid -> SubscribePkt $ SubscribeRequest pid ls')
+  let (SubACKPkt (SubscribeResponse _ rs)) = r
+  pure rs
+
+    where ls' = map (\(s, i) -> (textToBL s, i)) ls
+
+-- | Unsubscribe from a list of topics.
+unsubscribe :: MQTTClient -> [Topic] -> IO ()
+unsubscribe c@MQTTClient{..} ls =
+  void $ sendAndWait c DUnsubACK (\pid -> UnsubscribePkt $ UnsubscribeRequest pid (map textToBL ls))
+
+-- | Publish a message (QoS 0).
+publish :: MQTTClient
+        -> Topic         -- ^ Topic
+        -> BL.ByteString -- ^ Message body
+        -> Bool          -- ^ Retain flag
+        -> IO ()
+publish c t m r = void $ publishq c t m r QoS0
+
+-- | Publish a message with the specified QoS.
+publishq :: MQTTClient
+         -> Topic         -- ^ Topic
+         -> BL.ByteString -- ^ Message body
+         -> Bool          -- ^ Retain flag
+         -> QoS           -- ^ QoS
+         -> IO ()
+publishq c t m r q = do
+  (ch,pid) <- atomically $ reservePktID c types
+  E.finally (publishAndWait ch pid) (atomically $ releasePktIDs c [(t',pid) | t' <- types])
+
+    where
+      types = [DPubREC, DPubCOMP]
+      publishAndWait ch pid = withAsync (pub False pid) (\p -> satisfyQoS p ch pid)
+
+      pub dup pid = do
+        sendPacketIO c (PublishPkt $ PublishRequest {
+                           _pubDup = dup,
+                           _pubQoS = q,
+                           _pubPktID = pid,
+                           _pubRetain = r,
+                           _pubTopic = textToBL t,
+                           _pubBody = m})
+        threadDelay 5000000
+        pub True pid
+
+      satisfyQoS p ch pid
+        | q == QoS0 = pure ()
+        | q == QoS1 = void $ atomically $ readTChan ch
+        | q == QoS2 = waitRec
+        | otherwise = error "invalid QoS"
+
+        where
+          waitRec = do
+            (PubRECPkt _) <- atomically $ readTChan ch
+            sendPacketIO c (PubRELPkt $ PubREL pid)
+            cancel p -- must not publish after rel
+            void $ atomically $ readTChan ch
+
+-- | Disconnect from the MQTT server.
+disconnect :: MQTTClient -> IO ()
+disconnect c@MQTTClient{..} = race_ getDisconnected orDieTrying
+  where
+    getDisconnected = sendPacketIO c DisconnectPkt >> waitForClient c
+    orDieTrying = threadDelay 10000000 >> readTVarIO _ct >>= \t -> cancelWith t Timeout
+
+-- | A convenience method for creating a LastWill.
+mkLWT :: Topic -> BL.ByteString -> Bool -> T.LastWill
+mkLWT t m r = T.LastWill{
+  T._willRetain=r,
+  T._willQoS=QoS0,
+  T._willTopic = textToBL t,
+  T._willMsg=m
+  }
diff --git a/src/Network/MQTT/Types.hs b/src/Network/MQTT/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/MQTT/Types.hs
@@ -0,0 +1,417 @@
+{-|
+Module      : Network.MQTT.Types
+Description : Parsers and serializers for MQTT.
+Copyright   : (c) Dustin Sallings, 2019
+License     : BSD3
+Maintainer  : dustin@spy.net
+Stability   : experimental
+
+MQTT Types.
+-}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+module Network.MQTT.Types (
+  LastWill(..), MQTTPkt(..), QoS(..),
+  ConnectRequest(..), connectRequest, ConnACKFlags(..), ConnACKRC(..),
+  PublishRequest(..), PubACK(..), PubREC(..), PubREL(..), PubCOMP(..),
+  SubscribeRequest(..), SubscribeResponse(..),
+  UnsubscribeRequest(..), UnsubscribeResponse(..),
+  parsePacket, ByteMe(toByteString),
+  -- for testing
+  encodeLength, parseHdrLen, connACKRC
+  ) where
+
+import           Control.Applicative             (liftA2, (<|>))
+import           Control.Monad                   (replicateM)
+import qualified Data.Attoparsec.ByteString.Lazy as A
+import           Data.Bits                       (Bits (..), shiftL, testBit,
+                                                  (.&.), (.|.))
+import qualified Data.ByteString.Lazy            as BL
+import           Data.Maybe                      (isJust)
+import           Data.Word                       (Word16, Word8)
+
+-- | QoS values for publishing and subscribing.
+data QoS = QoS0 | QoS1 | QoS2 deriving (Bounded, Enum, Eq, Show)
+
+qosW :: QoS -> Word8
+qosW = toEnum.fromEnum
+
+wQos :: Word8 -> QoS
+wQos = toEnum.fromEnum
+
+(≫) :: Bits a => a -> Int -> a
+(≫) = shiftR
+
+(≪) :: Bits a => a -> Int -> a
+(≪) = shiftL
+
+class ByteMe a where
+  toBytes :: a -> [Word8]
+  toBytes = BL.unpack . toByteString
+  toByteString :: a -> BL.ByteString
+  toByteString = BL.pack . toBytes
+
+boolBit :: Bool -> Word8
+boolBit False = 0
+boolBit True  = 1
+
+parseHdrLen :: A.Parser Int
+parseHdrLen = go 0 1
+  where
+    go :: Int -> Int -> A.Parser Int
+    go v m = do
+      x <- A.anyWord8
+      let a = fromEnum (x .&. 127) * m + v
+      if x .&. 128 /= 0
+        then go a (m*128)
+        else pure a
+
+encodeLength :: Int -> [Word8]
+encodeLength n = go (n `quotRem` 128)
+  where
+    go (x,e)
+      | x > 0 = en (e .|. 128) : go (x `quotRem` 128)
+      | otherwise = [en e]
+
+    en :: Int -> Word8
+    en = toEnum
+
+encodeWord16 :: Word16 -> BL.ByteString
+encodeWord16 a = let (h,l) = a `quotRem` 256 in BL.pack [w h, w l]
+    where w = toEnum.fromEnum
+
+
+blLength :: BL.ByteString -> BL.ByteString
+blLength = BL.pack . encodeLength . fromEnum . BL.length
+
+withLength :: BL.ByteString -> BL.ByteString
+withLength a = blLength a <> a
+
+instance ByteMe BL.ByteString where
+  toByteString a = (encodeWord16 . toEnum . fromEnum . BL.length) a <> a
+
+data ProtocolLevel = Protocol311 deriving(Eq, Show)
+
+instance ByteMe ProtocolLevel where toByteString _ = BL.singleton 4
+
+-- | An MQTT Will message.
+data LastWill = LastWill {
+  _willRetain  :: Bool
+  , _willQoS   :: QoS
+  , _willTopic :: BL.ByteString
+  , _willMsg   :: BL.ByteString
+  } deriving(Eq, Show)
+
+data ConnectRequest = ConnectRequest {
+  _username       :: Maybe BL.ByteString
+  , _password     :: Maybe BL.ByteString
+  , _lastWill     :: Maybe LastWill
+  , _cleanSession :: Bool
+  , _keepAlive    :: Word16
+  , _connID       :: BL.ByteString
+  } deriving (Eq, Show)
+
+connectRequest :: ConnectRequest
+connectRequest = ConnectRequest{_username=Nothing, _password=Nothing, _lastWill=Nothing,
+                                _cleanSession=True, _keepAlive=300, _connID=""}
+
+instance ByteMe ConnectRequest where
+  toByteString ConnectRequest{..} = BL.singleton 0x10
+                                    <> withLength val
+    where
+      val :: BL.ByteString
+      val =  "\NUL\EOTMQTT\EOT" -- MQTT + Protocol311
+             <> BL.singleton connBits
+             <> encodeWord16 _keepAlive
+             <> toByteString _connID
+             <> lwt _lastWill
+             <> perhaps _username
+             <> perhaps _password
+      connBits = hasu .|. hasp .|. willBits .|. clean
+        where
+          hasu = boolBit (isJust _username) ≪ 7
+          hasp = boolBit (isJust _password) ≪ 6
+          clean = boolBit _cleanSession ≪ 1
+          willBits = case _lastWill of
+                       Nothing -> 0
+                       Just LastWill{..} -> 4 .|. ((qosW _willQoS .&. 0x3) ≪ 4) .|. (boolBit _willRetain ≪ 5)
+
+      lwt :: Maybe LastWill -> BL.ByteString
+      lwt Nothing = mempty
+      lwt (Just LastWill{..}) = toByteString _willTopic <> toByteString _willMsg
+
+      perhaps :: Maybe BL.ByteString -> BL.ByteString
+      perhaps Nothing  = ""
+      perhaps (Just s) = toByteString s
+
+data MQTTPkt = ConnPkt ConnectRequest
+             | ConnACKPkt ConnACKFlags
+             | PublishPkt PublishRequest
+             | PubACKPkt PubACK
+             | PubRECPkt PubREC
+             | PubRELPkt PubREL
+             | PubCOMPPkt PubCOMP
+             | SubscribePkt SubscribeRequest
+             | SubACKPkt SubscribeResponse
+             | UnsubscribePkt UnsubscribeRequest
+             | UnsubACKPkt UnsubscribeResponse
+             | PingPkt
+             | PongPkt
+             | DisconnectPkt
+  deriving (Eq, Show)
+
+instance ByteMe MQTTPkt where
+  toByteString (ConnPkt x)        = toByteString x
+  toByteString (ConnACKPkt x)     = toByteString x
+  toByteString (PublishPkt x)     = toByteString x
+  toByteString (PubACKPkt x)      = toByteString x
+  toByteString (PubRELPkt x)      = toByteString x
+  toByteString (PubRECPkt x)      = toByteString x
+  toByteString (PubCOMPPkt x)     = toByteString x
+  toByteString (SubscribePkt x)   = toByteString x
+  toByteString (SubACKPkt x)      = toByteString x
+  toByteString (UnsubscribePkt x) = toByteString x
+  toByteString (UnsubACKPkt x)    = toByteString x
+  toByteString PingPkt            = "\192\NUL"
+  toByteString PongPkt            = "\208\NUL"
+  toByteString DisconnectPkt      = "\224\NUL"
+
+parsePacket :: A.Parser MQTTPkt
+parsePacket = parseConnect <|> parseConnectACK
+              <|> parsePublish <|> parsePubACK
+              <|> parsePubREC <|> parsePubREL <|> parsePubCOMP
+              <|> parseSubscribe <|> parseSubACK
+              <|> parseUnsubscribe <|> parseUnsubACK
+              <|> PingPkt <$ A.string "\192\NUL" <|> PongPkt <$ A.string "\208\NUL"
+              <|> DisconnectPkt <$ A.string "\224\NUL"
+
+aWord16 :: A.Parser Word16
+aWord16 = A.anyWord8 >>= \h -> A.anyWord8 >>= \l -> pure $ (c h ≪ 8) .|. c l
+  where c = toEnum . fromEnum
+
+aString :: A.Parser BL.ByteString
+aString = do
+  n <- aWord16
+  s <- A.take (fromEnum n)
+  pure $ BL.fromStrict s
+
+parseConnect :: A.Parser MQTTPkt
+parseConnect = do
+  _ <- A.word8 0x10
+  _ <- parseHdrLen
+  _ <- A.string "\NUL\EOTMQTT\EOT" -- "MQTT" Protocol level = 4
+  connFlagBits <- A.anyWord8
+  keepAlive <- aWord16
+  cid <- aString
+  lwt <- parseLwt connFlagBits
+  u <- mstr (testBit connFlagBits 7)
+  p <- mstr (testBit connFlagBits 6)
+  pure $ ConnPkt ConnectRequest{_connID=cid, _username=u, _password=p,
+                                _lastWill=lwt, _keepAlive=keepAlive,
+                                _cleanSession=testBit connFlagBits 1}
+
+  where
+    mstr :: Bool -> A.Parser (Maybe BL.ByteString)
+    mstr False = pure Nothing
+    mstr True  = Just <$> aString
+
+    parseLwt bits
+      | testBit bits 2 = do
+          top <- aString
+          msg <- aString
+          pure $ Just LastWill{_willTopic=top, _willMsg=msg,
+                               _willRetain=testBit bits 5,
+                               _willQoS=wQos $ (bits ≫ 3) .&. 0x3}
+      | otherwise = pure Nothing
+
+data ConnACKRC = ConnAccepted | UnacceptableProtocol
+               | IdentifierRejected | ServerUnavailable
+               | BadCredentials | NotAuthorized
+               | InvalidConnACKRC Word8 deriving(Eq, Show)
+
+connACKRC :: Word8 -> ConnACKRC
+connACKRC 0 = ConnAccepted
+connACKRC 1 = UnacceptableProtocol
+connACKRC 2 = IdentifierRejected
+connACKRC 3 = ServerUnavailable
+connACKRC 4 = BadCredentials
+connACKRC 5 = NotAuthorized
+connACKRC x = InvalidConnACKRC x
+
+connACKVal :: ConnACKRC -> Word8
+connACKVal ConnAccepted         = 0
+connACKVal UnacceptableProtocol = 1
+connACKVal IdentifierRejected   = 2
+connACKVal ServerUnavailable    = 3
+connACKVal BadCredentials       = 4
+connACKVal NotAuthorized        = 5
+connACKVal (InvalidConnACKRC x) = x
+
+data ConnACKFlags = ConnACKFlags Bool ConnACKRC deriving (Eq, Show)
+
+instance ByteMe ConnACKFlags where
+  toBytes (ConnACKFlags sp rc) = [0x20, 2, boolBit sp, connACKVal rc]
+
+parseConnectACK :: A.Parser MQTTPkt
+parseConnectACK = do
+  _ <- A.word8 0x20
+  _ <- A.word8 2 -- two bytes left
+  ackFlags <- A.anyWord8
+  rc <- A.anyWord8
+  pure $ ConnACKPkt $ ConnACKFlags (testBit ackFlags 0) (connACKRC rc)
+
+data PublishRequest = PublishRequest{
+  _pubDup      :: Bool
+  , _pubQoS    :: QoS
+  , _pubRetain :: Bool
+  , _pubTopic  :: BL.ByteString
+  , _pubPktID  :: Word16
+  , _pubBody   :: BL.ByteString
+  } deriving(Eq, Show)
+
+instance ByteMe PublishRequest where
+  toByteString PublishRequest{..} = BL.singleton (0x30 .|. f)
+                                    <> withLength val
+    where f = (db ≪ 3) .|. (qb ≪ 1) .|. rb
+          db = boolBit _pubDup
+          qb = qosW _pubQoS .&. 0x3
+          rb = boolBit _pubRetain
+          pktid
+            | _pubQoS == QoS0 = mempty
+            | otherwise = encodeWord16 _pubPktID
+          val = toByteString _pubTopic <> pktid <> _pubBody
+
+parsePublish :: A.Parser MQTTPkt
+parsePublish = do
+  w <- A.satisfy (\x -> x .&. 0xf0 == 0x30)
+  plen <- parseHdrLen
+  let _pubDup = w .&. 0x8 == 0x8
+      _pubQoS = wQos $ (w ≫ 1) .&. 3
+      _pubRetain = w .&. 1 == 1
+  _pubTopic <- aString
+  _pubPktID <- if _pubQoS == QoS0 then pure 0 else aWord16
+  _pubBody <- BL.fromStrict <$> A.take (plen - (fromEnum $ BL.length _pubTopic) - 2 - qlen _pubQoS)
+  pure $ PublishPkt PublishRequest{..}
+
+  where qlen QoS0 = 0
+        qlen _    = 2
+
+data SubscribeRequest = SubscribeRequest Word16 [(BL.ByteString, QoS)]
+                      deriving(Eq, Show)
+
+instance ByteMe SubscribeRequest where
+  toByteString (SubscribeRequest pid sreq) =
+    BL.singleton 0x82
+    <> withLength (encodeWord16 pid <> reqs)
+    where reqs = (BL.concat . map (\(bs,q) -> toByteString bs <> BL.singleton (qosW q))) sreq
+
+newtype PubACK = PubACK Word16 deriving(Eq, Show)
+
+instance ByteMe PubACK where
+  toByteString (PubACK pid) = BL.singleton 0x40 <> withLength (encodeWord16 pid)
+
+parsePubACK :: A.Parser MQTTPkt
+parsePubACK = do
+  _ <- A.word8 0x40
+  _ <- parseHdrLen
+  PubACKPkt . PubACK <$> aWord16
+
+newtype PubREC = PubREC Word16 deriving(Eq, Show)
+
+instance ByteMe PubREC where
+  toByteString (PubREC pid) = BL.singleton 0x50 <> withLength (encodeWord16 pid)
+
+parsePubREC :: A.Parser MQTTPkt
+parsePubREC = do
+  _ <- A.word8 0x50
+  _ <- parseHdrLen
+  PubRECPkt . PubREC <$> aWord16
+
+newtype PubREL = PubREL Word16 deriving(Eq, Show)
+
+instance ByteMe PubREL where
+  toByteString (PubREL pid) = BL.singleton 0x62 <> withLength (encodeWord16 pid)
+
+parsePubREL :: A.Parser MQTTPkt
+parsePubREL = do
+  _ <- A.word8 0x62
+  _ <- parseHdrLen
+  PubRELPkt . PubREL <$> aWord16
+
+newtype PubCOMP = PubCOMP Word16 deriving(Eq, Show)
+
+instance ByteMe PubCOMP where
+  toByteString (PubCOMP pid) = BL.singleton 0x70 <> withLength (encodeWord16 pid)
+
+parsePubCOMP :: A.Parser MQTTPkt
+parsePubCOMP = do
+  _ <- A.word8 0x70
+  _ <- parseHdrLen
+  PubCOMPPkt . PubCOMP <$> aWord16
+
+parseSubscribe :: A.Parser MQTTPkt
+parseSubscribe = do
+  _ <- A.word8 0x82
+  hl <- parseHdrLen
+  pid <- aWord16
+  content <- A.take (fromEnum hl - 2)
+  SubscribePkt . SubscribeRequest pid <$> parseSubs content
+    where
+      parseSubs l = case A.parseOnly (A.many1 parseSub) l of
+                      Left x  -> fail x
+                      Right x -> pure x
+      parseSub = liftA2 (,) aString (wQos <$> A.anyWord8)
+
+data SubscribeResponse = SubscribeResponse Word16 [Maybe QoS] deriving (Eq, Show)
+
+instance ByteMe SubscribeResponse where
+  toByteString (SubscribeResponse pid sres) =
+    BL.singleton 0x90 <> withLength (encodeWord16 pid <> BL.pack (b <$> sres))
+
+    where
+      b Nothing  = 0x80
+      b (Just q) = qosW q
+
+parseSubACK :: A.Parser MQTTPkt
+parseSubACK = do
+  _ <- A.word8 0x90
+  hl <- parseHdrLen
+  pid <- aWord16
+  SubACKPkt . SubscribeResponse pid <$> replicateM (hl-2) (p <$> A.anyWord8)
+
+  where
+    p 0x80 = Nothing
+    p x    = Just (wQos x)
+
+data UnsubscribeRequest = UnsubscribeRequest Word16 [BL.ByteString]
+                        deriving(Eq, Show)
+
+instance ByteMe UnsubscribeRequest where
+  toByteString (UnsubscribeRequest pid sreq) =
+    BL.singleton 0xa2
+    <> withLength (encodeWord16 pid <> mconcat (toByteString <$> sreq))
+
+parseUnsubscribe :: A.Parser MQTTPkt
+parseUnsubscribe = do
+  _ <- A.word8 0xa2
+  hl <- parseHdrLen
+  pid <- aWord16
+  content <- A.take (fromEnum hl - 2)
+  UnsubscribePkt . UnsubscribeRequest pid <$> parseSubs content
+    where
+      parseSubs l = case A.parseOnly (A.many1 aString) l of
+                      Left x  -> fail x
+                      Right x -> pure x
+
+newtype UnsubscribeResponse = UnsubscribeResponse Word16 deriving(Eq, Show)
+
+instance ByteMe UnsubscribeResponse where
+  toByteString (UnsubscribeResponse pid) = BL.singleton 0xb0 <> withLength (encodeWord16 pid)
+
+parseUnsubACK :: A.Parser MQTTPkt
+parseUnsubACK = do
+  _ <- A.word8 0xb0
+  _ <- parseHdrLen
+  UnsubACKPkt . UnsubscribeResponse <$> aWord16
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+import           Control.Applicative             (liftA2)
+import           Control.Monad                   (mapM_)
+import qualified Data.Attoparsec.ByteString.Lazy as A
+import qualified Data.ByteString                 as B
+import qualified Data.ByteString.Char8           as BC
+import qualified Data.ByteString.Lazy            as L
+import           Data.Word                       (Word8)
+import           Numeric                         (showHex)
+import           Test.QuickCheck
+import           Test.Tasty
+import           Test.Tasty.HUnit
+import           Test.Tasty.QuickCheck           as QC
+
+import           Network.MQTT.Types
+
+prop_rtLengthParser :: NonNegative (Large Int) -> Property
+prop_rtLengthParser (NonNegative (Large x)) =
+  x <= 268435455 ==> label (show (length e) <> "B") $
+  cover 20 (length e > 1) "multi-byte" $
+  d e == x
+
+  where e = encodeLength x
+        d :: [Word8] -> Int
+        d l = case A.parse parseHdrLen (L.pack l) of
+                (A.Fail _ _ _) -> undefined
+                (A.Done _ v)   -> v
+
+testPacketRT :: Assertion
+testPacketRT = mapM_ tryParse [
+  "\DLE0\NUL\EOTMQTT\EOT\198\SOH,\NUL\ACKsomeid\NUL\btmp/test\NUL\STXhi\NUL\ACKdustin\NUL\ACKpasswd",
+  " \STX\SOH\NUL"
+  ]
+
+  where
+    tryParse s = do
+      let (A.Done _ x) = A.parse parsePacket s
+      case A.parse parsePacket (toByteString x) of
+        f@(A.Fail _ _ _) -> assertFailure (show f)
+        (A.Done _ x')    -> assertEqual (show s) x x'
+
+instance Arbitrary ConnectRequest where
+  arbitrary = do
+    u <- mastr
+    p <- mastr
+    cid <- astr
+    cs <- arbitrary
+    ka <- arbitrary
+
+    pure ConnectRequest{_username=u, _password=p, _lastWill=Nothing,
+                        _cleanSession=cs, _keepAlive=ka, _connID=cid}
+
+mastr :: Gen (Maybe L.ByteString)
+mastr = fmap (L.fromStrict . BC.pack . getUnicodeString) <$> arbitrary
+
+astr :: Gen L.ByteString
+astr = L.fromStrict . BC.pack . getUnicodeString <$> arbitrary
+
+instance Arbitrary QoS where
+  arbitrary = arbitraryBoundedEnum
+
+instance Arbitrary ConnACKFlags where arbitrary = ConnACKFlags <$> arbitrary <*> (connACKRC <$> choose (0,5))
+
+instance Arbitrary PublishRequest where
+  arbitrary = do
+    _pubDup <- arbitrary
+    _pubQoS <- arbitrary
+    _pubRetain <- arbitrary
+    _pubTopic <- astr
+    _pubPktID <- if _pubQoS == QoS0 then pure 0 else arbitrary
+    _pubBody <- astr
+    pure PublishRequest{..}
+
+instance Arbitrary PubACK where
+  arbitrary = PubACK <$> arbitrary
+
+instance Arbitrary PubREL where
+  arbitrary = PubREL <$> arbitrary
+
+instance Arbitrary PubREC where
+  arbitrary = PubREC <$> arbitrary
+
+instance Arbitrary PubCOMP where
+  arbitrary = PubCOMP <$> arbitrary
+
+instance Arbitrary SubscribeRequest where
+  arbitrary = arbitrary >>= \pid -> choose (1,11) >>= \n -> SubscribeRequest pid <$> vectorOf n sub
+    where sub = liftA2 (,) astr arbitrary
+
+instance Arbitrary SubscribeResponse where
+  arbitrary = arbitrary >>= \pid -> choose (1,11) >>= \n -> SubscribeResponse pid <$> vectorOf n sub
+    where sub = oneof (pure <$> [Just QoS0, Just QoS1, Just QoS2, Nothing])
+  shrink (SubscribeResponse pid l)
+    | length l == 1 = []
+    | otherwise = [SubscribeResponse pid sl | sl <- shrinkList (:[]) l, length sl > 0]
+
+instance Arbitrary UnsubscribeRequest where
+  arbitrary = arbitrary >>= \pid -> choose (1,11) >>= \n -> UnsubscribeRequest pid <$> vectorOf n astr
+  shrink (UnsubscribeRequest p l)
+    | length l == 1 = []
+    | otherwise = [UnsubscribeRequest p sl | sl <- shrinkList (:[]) l, length sl > 0]
+
+instance Arbitrary UnsubscribeResponse where
+  arbitrary = UnsubscribeResponse <$> arbitrary
+
+instance Arbitrary MQTTPkt where
+  arbitrary = oneof [
+    ConnPkt <$> arbitrary,
+    ConnACKPkt <$> arbitrary,
+    PublishPkt <$> arbitrary,
+    PubACKPkt <$> arbitrary,
+    PubRELPkt <$> arbitrary,
+    PubRECPkt <$> arbitrary,
+    PubCOMPPkt <$> arbitrary,
+    SubscribePkt <$> arbitrary,
+    SubACKPkt <$> arbitrary,
+    UnsubscribePkt <$> arbitrary,
+    UnsubACKPkt <$> arbitrary,
+    pure PingPkt, pure PongPkt, pure DisconnectPkt
+    ]
+  shrink (SubACKPkt x)      = SubACKPkt <$> shrink x
+  shrink (UnsubscribePkt x) = UnsubscribePkt <$> shrink x
+  shrink _                  = []
+
+prop_PacketRT :: MQTTPkt -> Property
+prop_PacketRT p = label (lab p) $ case A.parse parsePacket (toByteString p) of
+                                    (A.Fail _ _ _) -> False
+                                    (A.Done _ r)   -> r == p
+
+  where lab x = let (s,_) = break (== ' ') . show $ x in s
+
+tests :: [TestTree]
+tests = [
+  localOption (QC.QuickCheckTests 10000) $ testProperty "header length rt (parser)" prop_rtLengthParser,
+
+  testCase "rt some packets" testPacketRT,
+  localOption (QC.QuickCheckTests 1000) $ testProperty "rt packets" prop_PacketRT
+  ]
+
+main :: IO ()
+main = defaultMain $ testGroup "All Tests" tests
