diff --git a/ip.cabal b/ip.cabal
--- a/ip.cabal
+++ b/ip.cabal
@@ -1,5 +1,5 @@
 name: ip
-version: 1.3.0
+version: 1.4.0
 synopsis: Library for IP and MAC addresses
 homepage: https://github.com/andrewthad/haskell-ip#readme
 license: BSD3
@@ -36,7 +36,6 @@
   exposed-modules:
     Net.Mac
     Net.IPv4
-    Net.IPv4.Range
     Net.IPv6
     Net.IP
     Net.Types
@@ -47,9 +46,9 @@
     Data.Text.Builder.Common.Internal
     Data.ByteString.Builder.Fixed
   build-depends:
-      base >= 4.9 && < 5
+      base >= 4.9.1.0 && < 5
     , attoparsec >= 0.13 && < 0.14
-    , aeson >= 0.9 && < 1.5
+    , aeson >= 1.0 && < 1.5
     , hashable >= 1.2 && < 1.3
     , text >= 1.2  && < 1.3
     , bytestring >= 0.10 && < 0.11
@@ -68,7 +67,7 @@
     , test-framework
     , test-framework-quickcheck2
     , QuickCheck
-    , quickcheck-classes >= 0.3 && < 0.5
+    , quickcheck-classes >= 0.4.13 && < 0.5
     , text
     , bytestring
     , HUnit
diff --git a/src/Net/IPv4.hs b/src/Net/IPv4.hs
--- a/src/Net/IPv4.hs
+++ b/src/Net/IPv4.hs
@@ -13,7 +13,7 @@
     This module provides the IPv4 data type and functions for working
     with it. There are also encoding and decoding functions provided
     in this module, but they should be imported from
-    @Net.IPv4.Text@ and @Net.IPv4.ByteString.Char8@ instead. They are
+    @Net.IPv4.Text@ and @Net.ByteString.Char8@ instead. They are
     defined here so that the 'FromJSON' and 'ToJSON' instances can
     use them.
 
@@ -54,58 +54,78 @@
   , decodeString
     -- ** Printing
   , print
+    -- * IPv4 Ranges
+    -- ** Range functions
+  , range
+  , fromBounds
+  , normalize
+  , contains
+  , member
+  , lowerInclusive
+  , upperInclusive
+    -- ** Conversion to IPv4
+  , toList
+  , toGenerator
+    -- ** Private Ranges
+  , private24
+  , private20
+  , private16
+    -- ** Textual Conversion
+    -- *** Text
+  , encodeRange
+  , decodeRange
+  , builderRange
+  , parserRange
+  , printRange
     -- * Types
   , IPv4(..)
+  , IPv4Range(..)
     -- * Interoperability
     -- $interoperability
   ) where
 
-import Prelude hiding (any, print)
-import Data.Bits ((.&.),(.|.),shiftR,shiftL,unsafeShiftR)
-import Data.Word
-import Data.Hashable
-import Data.Aeson (FromJSON(..),ToJSON(..))
-import GHC.Generics (Generic)
 import Control.Monad
-import Data.Text.Internal (Text(..))
+import Control.Monad.ST (ST,runST)
+import Data.Aeson (FromJSON(..),ToJSON(..))
+import Data.Aeson (ToJSONKey(..),FromJSONKey(..),ToJSONKeyFunction(..),FromJSONKeyFunction(..))
+import Data.Bits ((.&.),(.|.),shiftR,shiftL,unsafeShiftR,complement,shift)
 import Data.ByteString (ByteString)
-import Data.Vector.Generic.Mutable (MVector(..))
-import Foreign.Ptr (Ptr,plusPtr)
-import Foreign.Storable (poke)
+import Data.Hashable
 import Data.Monoid ((<>))
-import Data.Text.Encoding (decodeUtf8')
-import Foreign.Storable (Storable)
 import Data.Primitive.Types (Prim)
-import Control.Monad.ST (ST,runST)
+import Data.Text (Text)
+import Data.Text.Encoding (decodeUtf8')
+import Data.Text.Internal (Text(..))
+import Data.Vector.Generic.Mutable (MVector(..))
+import Data.Word
+import Foreign.Ptr (Ptr,plusPtr)
+import Foreign.Storable (Storable, poke)
+import GHC.Generics (Generic)
+import Prelude hiding (any, print, print)
+import Text.ParserCombinators.ReadPrec (prec,step)
 import Text.Printf (printf)
 import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
-import Text.ParserCombinators.ReadPrec (prec,step)
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.Attoparsec.ByteString.Char8 as AB
+import qualified Data.Attoparsec.Text as AT
 import qualified Data.Bits as Bits
-import qualified Data.Text.Read as TextRead
 import qualified Data.ByteString.Builder as Builder
-import qualified Data.ByteString.Unsafe as ByteString
-import qualified Data.Text.Read as Text (Reader)
-import qualified Data.Text.Lazy.Builder as TBuilder
-import qualified Data.Attoparsec.Text as AT
-import qualified Data.Attoparsec.ByteString.Char8 as AB
 import qualified Data.ByteString.Char8 as BC8
-import qualified Data.Text as Text
 import qualified Data.ByteString.Internal as I
-import qualified Data.ByteString.Unsafe as BSU
+import qualified Data.ByteString.Unsafe as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Array as TArray
+import qualified Data.Text.IO as TIO
+import qualified Data.Text.Lazy as LText
+import qualified Data.Text.Lazy.Builder as TBuilder
+import qualified Data.Text.Lazy.Builder.Int as TBI
+import qualified Data.Text.Read as TextRead
 import qualified Data.Vector.Generic as GVector
 import qualified Data.Vector.Generic.Mutable as MGVector
-import qualified Data.Vector.Unboxed as UVector
 import qualified Data.Vector.Primitive as PVector
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.ByteString.Builder as BB
-import qualified Data.Text.Array as TArray
-import qualified Data.Text.IO as TIO
-
-#if MIN_VERSION_aeson(1,0,0)
-import Data.Aeson (ToJSONKey(..),FromJSONKey(..),
-  ToJSONKeyFunction(..),FromJSONKeyFunction(..))
-#endif
+import qualified Data.Vector.Unboxed as UVector
+import qualified Data.Vector.Unboxed.Mutable as MUVector
 
 -- $setup
 --
@@ -115,7 +135,7 @@
 -- >>> import qualified Prelude as P
 -- >>> import qualified Data.Text.IO as T
 -- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary }
---
+-- >>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary }
 
 -- | Create an 'IPv4' address from four octets. The first argument
 --   is the most significant octet. The last argument is the least
@@ -241,7 +261,7 @@
 builder :: IPv4 -> TBuilder.Builder
 builder = toDotDecimalBuilder
 
-reader :: Text.Reader IPv4
+reader :: TextRead.Reader IPv4
 reader = decodeIPv4TextReader
 
 parser :: AT.Parser IPv4
@@ -274,7 +294,7 @@
           | word >= 100 = do
               let int = fromIntegral word
                   indx = int + int + int
-                  get3 = fromIntegral . BSU.unsafeIndex threeDigits
+                  get3 = fromIntegral . ByteString.unsafeIndex threeDigits
               poke ptr (get3 indx)
               poke (ptr `plusPtr` 1) (get3 (indx + 1))
               poke (ptr `plusPtr` 2) (get3 (indx + 2))
@@ -282,7 +302,7 @@
           | word >= 10 = do
               let int = fromIntegral word
                   indx = int + int
-                  get2 = fromIntegral . BSU.unsafeIndex twoDigits
+                  get2 = fromIntegral . ByteString.unsafeIndex twoDigits
               poke ptr (get2 indx)
               poke (ptr `plusPtr` 1) (get2 (indx + 1))
               return 2
@@ -419,7 +439,7 @@
 instance ToJSONKey IPv4 where
   toJSONKey = ToJSONKeyText
     encode
-    (\addr -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 addr <> BB.char7 '"')
+    (\addr -> Aeson.unsafeToEncoding $ Builder.char7 '"' <> builderUtf8 addr <> Builder.char7 '"')
 
 instance FromJSONKey IPv4 where
   fromJSONKey = FromJSONKeyTextParser aesonParser
@@ -633,4 +653,383 @@
 picking up a dependency on @network@.
 
 -}
+
+-- $setup
+--
+-- These are here to get doctest's property checking to work.
+--
+-- >>> import qualified Prelude as P
+-- >>> import qualified Data.Text.IO as T
+-- >>> import Net.IPv4 (fromOctets,ipv4)
+-- >>> import Test.QuickCheck (Arbitrary(..))
+-- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary }
+-- >>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary }
+--
+
+-- | Smart constructor for 'IPv4Range'. Ensures the mask is appropriately
+--   sized and sets masked bits in the 'IPv4' to zero.
+range :: IPv4 -> Word8 -> IPv4Range
+range addr len = normalize (IPv4Range addr len)
+
+-- | Given an inclusive lower and upper ip address, create the smallest
+-- 'IPv4Range' that contains the two. This is helpful in situations where
+-- input given as a range like @192.168.16.0-192.168.19.255@ needs to be
+-- handled. This makes the range broader if it cannot be represented in
+-- CIDR notation.
+--
+-- >>> printRange $ fromBounds (fromOctets 192 168 16 0) (fromOctets 192 168 19 255)
+-- 192.168.16.0/22
+-- >>> printRange $ fromBounds (fromOctets 10 0 5 7) (fromOctets 10 0 5 14)
+-- 10.0.5.0/28
+fromBounds :: IPv4 -> IPv4 -> IPv4Range
+fromBounds (IPv4 a) (IPv4 b) =
+  normalize (IPv4Range (IPv4 a) (maskFromBounds a b))
+
+maskFromBounds :: Word32 -> Word32 -> Word8
+maskFromBounds lo hi = fromIntegral (Bits.countLeadingZeros (Bits.xor lo hi))
+
+-- | Checks to see if an 'IPv4' address belongs in the 'IPv4Range'.
+--
+-- >>> let ip = fromOctets 10 10 1 92
+-- >>> contains (IPv4Range (fromOctets 10 0 0 0) 8) ip
+-- True
+-- >>> contains (IPv4Range (fromOctets 10 11 0 0) 16) ip
+-- False
+--
+-- Typically, element-testing functions are written to take the element
+-- as the first argument and the set as the second argument. This is intentionally
+-- written the other way for better performance when iterating over a collection.
+-- For example, you might test elements in a list for membership like this:
+--
+-- >>> let r = IPv4Range (fromOctets 10 10 10 6) 31
+-- >>> mapM_ (P.print . contains r) (take 5 $ iterate succ $ fromOctets 10 10 10 5)
+-- False
+-- True
+-- True
+-- False
+-- False
+--
+-- The implementation of 'contains' ensures that (with GHC), the bitmask
+-- creation and range normalization only occur once in the above example.
+-- They are reused as the list is iterated.
+contains :: IPv4Range -> IPv4 -> Bool
+contains (IPv4Range (IPv4 wsubnet) len) =
+  let theMask = mask len
+      wsubnetNormalized = wsubnet .&. theMask
+   in \(IPv4 w) -> (w .&. theMask) == wsubnetNormalized
+
+mask :: Word8 -> Word32
+mask = complement . shiftR 0xffffffff . fromIntegral
+
+-- | This is provided to mirror the interface provided by @Data.Set@. It
+-- behaves just like 'contains' but with flipped arguments.
+--
+-- prop> member ip r == contains r ip
+member :: IPv4 -> IPv4Range -> Bool
+member = flip contains
+
+-- | The inclusive lower bound of an 'IPv4Range'. This is conventionally
+--   understood to be the broadcast address of a subnet. For example:
+--
+-- >>> T.putStrLn $ encode $ lowerInclusive $ IPv4Range (ipv4 10 10 1 160) 25
+-- 10.10.1.128
+--
+-- Note that the lower bound of a normalized 'IPv4Range' is simply the
+-- ip address of the range:
+--
+-- prop> lowerInclusive r == ipv4RangeBase (normalize r)
+lowerInclusive :: IPv4Range -> IPv4
+lowerInclusive (IPv4Range (IPv4 w) len) =
+  IPv4 (w .&. mask len)
+
+upperInclusive :: IPv4Range -> IPv4
+upperInclusive (IPv4Range (IPv4 w) len) =
+  let theInvertedMask = shiftR 0xffffffff (fromIntegral len)
+      theMask = complement theInvertedMask
+   in IPv4 ((w .&. theMask) .|. theInvertedMask)
+
+-- Given the size of the mask, return the total number of ips in the subnet. This
+-- only works for IPv4 addresses because an IPv6 subnet can have up to 2^128
+-- addresses. Not exported.
+countAddrs :: Word8 -> Word64
+countAddrs w =
+  let amountToShift = if w > 32
+        then 0
+        else 32 - fromIntegral w
+   in shift 1 amountToShift
+
+wordSuccessors :: Word64 -> IPv4 -> [IPv4]
+wordSuccessors !w (IPv4 !a) = if w > 0
+  then IPv4 a : wordSuccessors (w - 1) (IPv4 (a + 1))
+  else []
+
+wordSuccessorsM :: MonadPlus m => Word64 -> IPv4 -> m IPv4
+wordSuccessorsM = go where
+  go !w (IPv4 !a) = if w > 0
+    then mplus (return (IPv4 a)) (go (w - 1) (IPv4 (a + 1)))
+    else mzero
+
+-- | Convert an 'IPv4Range' into a list of the 'IPv4' addresses that
+--   are in it.
+-- >>> let r = IPv4Range (fromOctets 192 168 1 8) 30
+-- >>> mapM_ (T.putStrLn . encode) (toList r)
+-- 192.168.1.8
+-- 192.168.1.9
+-- 192.168.1.10
+-- 192.168.1.11
+
+toList :: IPv4Range -> [IPv4]
+toList (IPv4Range ip len) =
+  let totalAddrs = countAddrs len
+   in wordSuccessors totalAddrs ip
+
+toGenerator :: MonadPlus m => IPv4Range -> m IPv4
+toGenerator (IPv4Range ip len) =
+  let totalAddrs = countAddrs len
+   in wordSuccessorsM totalAddrs ip
+
+-- | The RFC1918 24-bit block. Subnet mask: @10.0.0.0/8@
+private24 :: IPv4Range
+private24 = IPv4Range (fromOctets 10 0 0 0) 8
+
+-- | The RFC1918 20-bit block. Subnet mask: @172.16.0.0/12@
+private20 :: IPv4Range
+private20  = IPv4Range (fromOctets 172 16 0 0) 12
+
+-- | The RFC1918 16-bit block. Subnet mask: @192.168.0.0/16@
+private16 :: IPv4Range
+private16 = IPv4Range (fromOctets 192 168 0 0) 16
+
+-- | Normalize an 'IPv4Range'. The first result of this is that the
+-- 'IPv4' inside the 'IPv4Range' is changed so that the insignificant
+-- bits are zeroed out. For example:
+--
+-- >>> printRange $ normalize $ IPv4Range (fromOctets 192 168 1 19) 24
+-- 192.168.1.0/24
+-- >>> printRange $ normalize $ IPv4Range (fromOctets 192 168 1 163) 28
+-- 192.168.1.160/28
+--
+-- The second effect of this is that the mask length is lowered to
+-- be 32 or smaller. Working with 'IPv4Range's that have not been
+-- normalized does not cause any issues for this library, although
+-- other applications may reject such ranges (especially those with
+-- a mask length above 32).
+--
+-- Note that 'normalize' is idempotent, that is:
+--
+-- prop> normalize r == (normalize . normalize) r
+normalize :: IPv4Range -> IPv4Range
+normalize (IPv4Range (IPv4 w) len) =
+  let len' = min len 32
+      w' = w .&. mask len'
+   in IPv4Range (IPv4 w') len'
+
+encodeRange :: IPv4Range -> Text
+encodeRange = rangeToDotDecimalText
+
+decodeRange :: Text -> Maybe IPv4Range
+decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput)
+
+builderRange :: IPv4Range -> TBuilder.Builder
+builderRange = rangeToDotDecimalBuilder
+
+parserRange :: AT.Parser IPv4Range
+parserRange = do
+  ip <- parser
+  _ <- AT.char '/'
+  theMask <- AT.decimal >>= limitSize
+  return (normalize (IPv4Range ip theMask))
+  where
+  limitSize i =
+    if i > 32
+      then fail "An IP range length must be between 0 and 32"
+      else return i
+
+-- | This exists mostly for testing purposes.
+printRange :: IPv4Range -> IO ()
+printRange = TIO.putStrLn . encodeRange
+
+-- | The length should be between 0 and 32. These bounds are inclusive.
+--   This expectation is not in any way enforced by this library because
+--   it does not cause errors. A mask length greater than 32 will be
+--   treated as if it were 32.
+data IPv4Range = IPv4Range
+  { ipv4RangeBase   :: {-# UNPACK #-} !IPv4
+  , ipv4RangeLength :: {-# UNPACK #-} !Word8
+  } deriving (Eq,Ord,Show,Read,Generic)
+
+
+instance Hashable IPv4Range
+
+instance ToJSON IPv4Range where
+  toJSON = Aeson.String . encodeRange
+
+instance FromJSON IPv4Range where
+  parseJSON (Aeson.String t) = case decodeRange t of
+    Nothing -> fail "Could not decodeRange IPv4 range"
+    Just res -> return res
+  parseJSON _ = mzero
+
+data instance MUVector.MVector s IPv4Range = MV_IPv4Range
+  !(MUVector.MVector s IPv4)
+  !(MUVector.MVector s Word8)
+data instance UVector.Vector IPv4Range = V_IPv4Range
+  !(UVector.Vector IPv4)
+  !(UVector.Vector Word8)
+
+instance UVector.Unbox IPv4Range
+instance MGVector.MVector MUVector.MVector IPv4Range where
+  {-# INLINE basicLength  #-}
+  basicLength (MV_IPv4Range as _) = MGVector.basicLength as
+  {-# INLINE basicUnsafeSlice  #-}
+  basicUnsafeSlice i_ m_ (MV_IPv4Range as bs)
+      = MV_IPv4Range (MGVector.basicUnsafeSlice i_ m_ as)
+                     (MGVector.basicUnsafeSlice i_ m_ bs)
+  {-# INLINE basicOverlaps  #-}
+  basicOverlaps (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2)
+      = MGVector.basicOverlaps as1 as2
+        || MGVector.basicOverlaps bs1 bs2
+  {-# INLINE basicUnsafeNew  #-}
+  basicUnsafeNew n_
+      = do
+          as <- MGVector.basicUnsafeNew n_
+          bs <- MGVector.basicUnsafeNew n_
+          return $ MV_IPv4Range as bs
+  {-# INLINE basicInitialize  #-}
+  basicInitialize (MV_IPv4Range as bs)
+      = do
+          MGVector.basicInitialize as
+          MGVector.basicInitialize bs
+  {-# INLINE basicUnsafeReplicate  #-}
+  basicUnsafeReplicate n_ (IPv4Range a b)
+      = do
+          as <- MGVector.basicUnsafeReplicate n_ a
+          bs <- MGVector.basicUnsafeReplicate n_ b
+          return (MV_IPv4Range as bs)
+  {-# INLINE basicUnsafeRead  #-}
+  basicUnsafeRead (MV_IPv4Range as bs) i_
+      = do
+          a <- MGVector.basicUnsafeRead as i_
+          b <- MGVector.basicUnsafeRead bs i_
+          return (IPv4Range a b)
+  {-# INLINE basicUnsafeWrite  #-}
+  basicUnsafeWrite (MV_IPv4Range as bs) i_ (IPv4Range a b)
+      = do
+          MGVector.basicUnsafeWrite as i_ a
+          MGVector.basicUnsafeWrite bs i_ b
+  {-# INLINE basicClear  #-}
+  basicClear (MV_IPv4Range as bs)
+      = do
+          MGVector.basicClear as
+          MGVector.basicClear bs
+  {-# INLINE basicSet  #-}
+  basicSet (MV_IPv4Range as bs) (IPv4Range a b)
+      = do
+          MGVector.basicSet as a
+          MGVector.basicSet bs b
+  {-# INLINE basicUnsafeCopy  #-}
+  basicUnsafeCopy (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2)
+      = do
+          MGVector.basicUnsafeCopy as1 as2
+          MGVector.basicUnsafeCopy bs1 bs2
+  {-# INLINE basicUnsafeMove  #-}
+  basicUnsafeMove (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2)
+      = do
+          MGVector.basicUnsafeMove as1 as2
+          MGVector.basicUnsafeMove bs1 bs2
+  {-# INLINE basicUnsafeGrow  #-}
+  basicUnsafeGrow (MV_IPv4Range as bs) m_
+      = do
+          as' <- MGVector.basicUnsafeGrow as m_
+          bs' <- MGVector.basicUnsafeGrow bs m_
+          return $ MV_IPv4Range as' bs'
+
+instance GVector.Vector UVector.Vector IPv4Range where
+  {-# INLINE basicUnsafeFreeze  #-}
+  basicUnsafeFreeze (MV_IPv4Range as bs)
+      = do
+          as' <- GVector.basicUnsafeFreeze as
+          bs' <- GVector.basicUnsafeFreeze bs
+          return $ V_IPv4Range as' bs'
+  {-# INLINE basicUnsafeThaw  #-}
+  basicUnsafeThaw (V_IPv4Range as bs)
+      = do
+          as' <- GVector.basicUnsafeThaw as
+          bs' <- GVector.basicUnsafeThaw bs
+          return $ MV_IPv4Range as' bs'
+  {-# INLINE basicLength  #-}
+  basicLength (V_IPv4Range as _) = GVector.basicLength as
+  {-# INLINE basicUnsafeSlice  #-}
+  basicUnsafeSlice i_ m_ (V_IPv4Range as bs)
+      = V_IPv4Range (GVector.basicUnsafeSlice i_ m_ as)
+                    (GVector.basicUnsafeSlice i_ m_ bs)
+  {-# INLINE basicUnsafeIndexM  #-}
+  basicUnsafeIndexM (V_IPv4Range as bs) i_
+      = do
+          a <- GVector.basicUnsafeIndexM as i_
+          b <- GVector.basicUnsafeIndexM bs i_
+          return (IPv4Range a b)
+  {-# INLINE basicUnsafeCopy  #-}
+  basicUnsafeCopy (MV_IPv4Range as1 bs1) (V_IPv4Range as2 bs2)
+      = do
+          GVector.basicUnsafeCopy as1 as2
+          GVector.basicUnsafeCopy bs1 bs2
+  {-# INLINE elemseq  #-}
+  elemseq _ (IPv4Range a b)
+      = GVector.elemseq (undefined :: UVector.Vector a) a
+        . GVector.elemseq (undefined :: UVector.Vector b) b
+
+rangeBitwise :: (IPv4 -> IPv4 -> IPv4) -> IPv4Range -> IPv4Range -> IPv4Range
+rangeBitwise fun l r = range ip len
+  where
+    -- Normalise first
+    l' = normalize l
+    r' = normalize r
+    ip = (ipv4RangeBase l') `fun` (ipv4RangeBase r')
+    len = maximum [ipv4RangeLength l, ipv4RangeLength r]
+
+rangeRebase :: (IPv4 -> IPv4) -> IPv4Range -> IPv4Range
+rangeRebase fun r = range (fun $ ipv4RangeBase r) (ipv4RangeLength r)
+
+-- | Notes:
+--
+--     * bit operations use network order (big endian),
+--
+--     * do not operate on host bits,
+--
+--     * return a normalized range dropping host bits,
+--
+--     * and "promote operands" by extending the length to the larger of two
+--       ranges.
+--
+instance Bits.Bits IPv4Range where
+  (.&.) = rangeBitwise (.&.)
+  (.|.) = rangeBitwise (.|.)
+  xor = rangeBitwise Bits.xor
+  complement = rangeRebase Bits.complement
+  shift r i = rangeRebase (flip Bits.shift i) r
+  rotate r i = rangeRebase (flip Bits.rotate i) r
+  bitSize = Bits.finiteBitSize
+  bitSizeMaybe = Just . Bits.finiteBitSize
+  isSigned = Bits.isSigned . ipv4RangeBase
+  testBit ip i = Bits.testBit (ipv4RangeBase ip) i
+  bit i = IPv4Range (Bits.bit i) $ fromIntegral $ i + 1
+  popCount = Bits.popCount . ipv4RangeBase . normalize
+
+-- | Note: the size is determined by the range length
+instance Bits.FiniteBits IPv4Range where
+  finiteBitSize = fromIntegral . ipv4RangeLength
+
+-----------------
+-- Internal Stuff
+-----------------
+
+rangeToDotDecimalText :: IPv4Range -> Text
+rangeToDotDecimalText = LText.toStrict . TBuilder.toLazyText . rangeToDotDecimalBuilder
+
+rangeToDotDecimalBuilder :: IPv4Range -> TBuilder.Builder
+rangeToDotDecimalBuilder (IPv4Range addr len) =
+     builder addr
+  <> TBuilder.singleton '/'
+  <> TBI.decimal len
 
diff --git a/src/Net/IPv4/Range.hs b/src/Net/IPv4/Range.hs
deleted file mode 100644
--- a/src/Net/IPv4/Range.hs
+++ /dev/null
@@ -1,443 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE UnboxedTuples #-}
-{-# LANGUAGE TypeFamilies #-}
-
-{-# OPTIONS_GHC -Wall #-}
-module Net.IPv4.Range
-  ( -- * Range functions
-    range
-  , fromBounds
-  , normalize
-  , contains
-  , member
-  , lowerInclusive
-  , upperInclusive
-    -- * Conversion to IPv4
-  , toList
-  , toGenerator
-    -- * Private Ranges
-  , private24
-  , private20
-  , private16
-    -- * Textual Conversion
-    -- ** Text
-  , encode
-  , decode
-  , builder
-  , parser
-  , print
-    -- * Types
-  , IPv4Range(..)
-  ) where
-
-import Prelude hiding (print)
-import Net.IPv4 (IPv4(..))
-import Data.Bits ((.&.),(.|.),shiftR,complement,shift)
-import Control.Monad
-import Data.Text (Text)
-import Data.Word (Word8,Word32,Word64)
-import Data.Hashable (Hashable)
-import Data.Aeson (FromJSON(..),ToJSON(..))
-import GHC.Generics (Generic)
-import Data.Monoid ((<>))
-import qualified Net.IPv4 as IPv4
-import qualified Data.Bits as Bits
-import qualified Data.Text.IO as Text
-import qualified Data.Attoparsec.Text as AT
-import qualified Data.Text.Lazy.Builder as TBuilder
-import qualified Data.Text.Lazy.Builder.Int as TBI
-import qualified Data.Vector.Generic as GVector
-import qualified Data.Vector.Generic.Mutable as MGVector
-import qualified Data.Vector.Unboxed.Mutable as MUVector
-import qualified Data.Vector.Unboxed as UVector
-import qualified Data.Aeson as Aeson
-import qualified Data.Text.Lazy as LText
-
--- $setup
---
--- These are here to get doctest's property checking to work.
---
--- >>> import qualified Prelude as P
--- >>> import qualified Net.IPv4 as I
--- >>> import qualified Data.Text.IO as T
--- >>> import Net.IPv4 (fromOctets,ipv4)
--- >>> import Test.QuickCheck (Arbitrary(..))
--- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary }
--- >>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary }
---
-
--- | Smart constructor for 'IPv4Range'. Ensures the mask is appropriately
---   sized and sets masked bits in the 'IPv4' to zero.
-range :: IPv4 -> Word8 -> IPv4Range
-range addr len = normalize (IPv4Range addr len)
-
--- | Given an inclusive lower and upper ip address, create the smallest
--- 'IPv4Range' that contains the two. This is helpful in situations where
--- input given as a range like @192.168.16.0-192.168.19.255@ needs to be
--- handled. This makes the range broader if it cannot be represented in
--- CIDR notation.
---
--- >>> print $ fromBounds (fromOctets 192 168 16 0) (fromOctets 192 168 19 255)
--- 192.168.16.0/22
--- >>> print $ fromBounds (fromOctets 10 0 5 7) (fromOctets 10 0 5 14)
--- 10.0.5.0/28
-fromBounds :: IPv4 -> IPv4 -> IPv4Range
-fromBounds (IPv4 a) (IPv4 b) =
-  let lo = min a b
-      hi = max a b
-   in normalize (IPv4Range (IPv4 lo) (maskFromBounds lo hi))
-
-maskFromBounds :: Word32 -> Word32 -> Word8
-maskFromBounds lo hi = fromIntegral (Bits.countLeadingZeros (Bits.xor lo hi))
-
--- | Checks to see if an 'IPv4' address belongs in the 'IPv4Range'.
---
--- >>> let ip = fromOctets 10 10 1 92
--- >>> contains (IPv4Range (fromOctets 10 0 0 0) 8) ip
--- True
--- >>> contains (IPv4Range (fromOctets 10 11 0 0) 16) ip
--- False
---
--- Typically, element-testing functions are written to take the element
--- as the first argument and the set as the second argument. This is intentionally
--- written the other way for better performance when iterating over a collection.
--- For example, you might test elements in a list for membership like this:
---
--- >>> let r = IPv4Range (fromOctets 10 10 10 6) 31
--- >>> mapM_ (P.print . contains r) (take 5 $ iterate succ $ fromOctets 10 10 10 5)
--- False
--- True
--- True
--- False
--- False
---
--- The implementation of 'contains' ensures that (with GHC), the bitmask
--- creation and range normalization only occur once in the above example.
--- They are reused as the list is iterated.
-contains :: IPv4Range -> IPv4 -> Bool
-contains (IPv4Range (IPv4 wsubnet) len) =
-  let theMask = mask len
-      wsubnetNormalized = wsubnet .&. theMask
-   in \(IPv4 w) -> (w .&. theMask) == wsubnetNormalized
-
-mask :: Word8 -> Word32
-mask = complement . shiftR 0xffffffff . fromIntegral
-
--- | This is provided to mirror the interface provided by @Data.Set@. It
--- behaves just like 'contains' but with flipped arguments.
---
--- prop> member ip r == contains r ip
-member :: IPv4 -> IPv4Range -> Bool
-member = flip contains
-
--- | The inclusive lower bound of an 'IPv4Range'. This is conventionally
---   understood to be the broadcast address of a subnet. For example:
---
--- >>> T.putStrLn $ I.encode $ lowerInclusive $ IPv4Range (ipv4 10 10 1 160) 25
--- 10.10.1.128
---
--- Note that the lower bound of a normalized 'IPv4Range' is simply the
--- ip address of the range:
---
--- prop> lowerInclusive r == ipv4RangeBase (normalize r)
-lowerInclusive :: IPv4Range -> IPv4
-lowerInclusive (IPv4Range (IPv4 w) len) =
-  IPv4 (w .&. mask len)
-
-upperInclusive :: IPv4Range -> IPv4
-upperInclusive (IPv4Range (IPv4 w) len) =
-  let theInvertedMask = shiftR 0xffffffff (fromIntegral len)
-      theMask = complement theInvertedMask
-   in IPv4 ((w .&. theMask) .|. theInvertedMask)
-
--- Given the size of the mask, return the total number of ips in the subnet. This
--- only works for IPv4 addresses because an IPv6 subnet can have up to 2^128
--- addresses. Not exported.
-countAddrs :: Word8 -> Word64
-countAddrs w =
-  let amountToShift = if w > 32
-        then 0
-        else 32 - fromIntegral w
-   in shift 1 amountToShift
-
-wordSuccessors :: Word64 -> IPv4 -> [IPv4]
-wordSuccessors !w (IPv4 !a) = if w > 0
-  then IPv4 a : wordSuccessors (w - 1) (IPv4 (a + 1))
-  else []
-
-wordSuccessorsM :: MonadPlus m => Word64 -> IPv4 -> m IPv4
-wordSuccessorsM = go where
-  go !w (IPv4 !a) = if w > 0
-    then mplus (return (IPv4 a)) (go (w - 1) (IPv4 (a + 1)))
-    else mzero
-
--- | Convert an 'IPv4Range' into a list of the 'IPv4' addresses that
---   are in it.
--- >>> let r = IPv4Range (fromOctets 192 168 1 8) 30
--- >>> mapM_ (T.putStrLn . I.encode) (toList r)
--- 192.168.1.8
--- 192.168.1.9
--- 192.168.1.10
--- 192.168.1.11
-
-toList :: IPv4Range -> [IPv4]
-toList (IPv4Range ip len) =
-  let totalAddrs = countAddrs len
-   in wordSuccessors totalAddrs ip
-
-toGenerator :: MonadPlus m => IPv4Range -> m IPv4
-toGenerator (IPv4Range ip len) =
-  let totalAddrs = countAddrs len
-   in wordSuccessorsM totalAddrs ip
-
--- | The RFC1918 24-bit block. Subnet mask: @10.0.0.0/8@
-private24 :: IPv4Range
-private24 = IPv4Range (IPv4.fromOctets 10 0 0 0) 8
-
--- | The RFC1918 20-bit block. Subnet mask: @172.16.0.0/12@
-private20 :: IPv4Range
-private20  = IPv4Range (IPv4.fromOctets 172 16 0 0) 12
-
--- | The RFC1918 16-bit block. Subnet mask: @192.168.0.0/16@
-private16 :: IPv4Range
-private16 = IPv4Range (IPv4.fromOctets 192 168 0 0) 16
-
--- | Normalize an 'IPv4Range'. The first result of this is that the
--- 'IPv4' inside the 'IPv4Range' is changed so that the insignificant
--- bits are zeroed out. For example:
---
--- >>> print $ normalize $ IPv4Range (fromOctets 192 168 1 19) 24
--- 192.168.1.0/24
--- >>> print $ normalize $ IPv4Range (fromOctets 192 168 1 163) 28
--- 192.168.1.160/28
---
--- The second effect of this is that the mask length is lowered to
--- be 32 or smaller. Working with 'IPv4Range's that have not been
--- normalized does not cause any issues for this library, although
--- other applications may reject such ranges (especially those with
--- a mask length above 32).
---
--- Note that 'normalize' is idempotent, that is:
---
--- prop> normalize r == (normalize . normalize) r
-normalize :: IPv4Range -> IPv4Range
-normalize (IPv4Range (IPv4 w) len) =
-  let len' = min len 32
-      w' = w .&. mask len'
-   in IPv4Range (IPv4 w') len'
-
-encode :: IPv4Range -> Text
-encode = rangeToDotDecimalText
-
-decode :: Text -> Maybe IPv4Range
-decode = rightToMaybe . AT.parseOnly (parser <* AT.endOfInput)
-
-builder :: IPv4Range -> TBuilder.Builder
-builder = rangeToDotDecimalBuilder
-
-parser :: AT.Parser IPv4Range
-parser = do
-  ip <- IPv4.parser
-  _ <- AT.char '/'
-  theMask <- AT.decimal >>= limitSize
-  return (normalize (IPv4Range ip theMask))
-  where
-  limitSize i =
-    if i > 32
-      then fail "An IP range length must be between 0 and 32"
-      else return i
-
--- | This exists mostly for testing purposes.
-print :: IPv4Range -> IO ()
-print = Text.putStrLn . encode
-
-rightToMaybe :: Either a b -> Maybe b
-rightToMaybe = either (const Nothing) Just
-
--- | The length should be between 0 and 32. These bounds are inclusive.
---   This expectation is not in any way enforced by this library because
---   it does not cause errors. A mask length greater than 32 will be
---   treated as if it were 32.
-data IPv4Range = IPv4Range
-  { ipv4RangeBase   :: {-# UNPACK #-} !IPv4
-  , ipv4RangeLength :: {-# UNPACK #-} !Word8
-  } deriving (Eq,Ord,Show,Read,Generic)
-
-
-instance Hashable IPv4Range
-
-instance ToJSON IPv4Range where
-  toJSON = Aeson.String . encode
-
-instance FromJSON IPv4Range where
-  parseJSON (Aeson.String t) = case decode t of
-    Nothing -> fail "Could not decode IPv4 range"
-    Just res -> return res
-  parseJSON _ = mzero
-
-data instance MUVector.MVector s IPv4Range = MV_IPv4Range
-  !(MUVector.MVector s IPv4)
-  !(MUVector.MVector s Word8)
-data instance UVector.Vector IPv4Range = V_IPv4Range
-  !(UVector.Vector IPv4)
-  !(UVector.Vector Word8)
-
-instance UVector.Unbox IPv4Range
-instance MGVector.MVector MUVector.MVector IPv4Range where
-  {-# INLINE basicLength  #-}
-  basicLength (MV_IPv4Range as _) = MGVector.basicLength as
-  {-# INLINE basicUnsafeSlice  #-}
-  basicUnsafeSlice i_ m_ (MV_IPv4Range as bs)
-      = MV_IPv4Range (MGVector.basicUnsafeSlice i_ m_ as)
-                     (MGVector.basicUnsafeSlice i_ m_ bs)
-  {-# INLINE basicOverlaps  #-}
-  basicOverlaps (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2)
-      = MGVector.basicOverlaps as1 as2
-        || MGVector.basicOverlaps bs1 bs2
-  {-# INLINE basicUnsafeNew  #-}
-  basicUnsafeNew n_
-      = do
-          as <- MGVector.basicUnsafeNew n_
-          bs <- MGVector.basicUnsafeNew n_
-          return $ MV_IPv4Range as bs
-  {-# INLINE basicInitialize  #-}
-  basicInitialize (MV_IPv4Range as bs)
-      = do
-          MGVector.basicInitialize as
-          MGVector.basicInitialize bs
-  {-# INLINE basicUnsafeReplicate  #-}
-  basicUnsafeReplicate n_ (IPv4Range a b)
-      = do
-          as <- MGVector.basicUnsafeReplicate n_ a
-          bs <- MGVector.basicUnsafeReplicate n_ b
-          return (MV_IPv4Range as bs)
-  {-# INLINE basicUnsafeRead  #-}
-  basicUnsafeRead (MV_IPv4Range as bs) i_
-      = do
-          a <- MGVector.basicUnsafeRead as i_
-          b <- MGVector.basicUnsafeRead bs i_
-          return (IPv4Range a b)
-  {-# INLINE basicUnsafeWrite  #-}
-  basicUnsafeWrite (MV_IPv4Range as bs) i_ (IPv4Range a b)
-      = do
-          MGVector.basicUnsafeWrite as i_ a
-          MGVector.basicUnsafeWrite bs i_ b
-  {-# INLINE basicClear  #-}
-  basicClear (MV_IPv4Range as bs)
-      = do
-          MGVector.basicClear as
-          MGVector.basicClear bs
-  {-# INLINE basicSet  #-}
-  basicSet (MV_IPv4Range as bs) (IPv4Range a b)
-      = do
-          MGVector.basicSet as a
-          MGVector.basicSet bs b
-  {-# INLINE basicUnsafeCopy  #-}
-  basicUnsafeCopy (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2)
-      = do
-          MGVector.basicUnsafeCopy as1 as2
-          MGVector.basicUnsafeCopy bs1 bs2
-  {-# INLINE basicUnsafeMove  #-}
-  basicUnsafeMove (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2)
-      = do
-          MGVector.basicUnsafeMove as1 as2
-          MGVector.basicUnsafeMove bs1 bs2
-  {-# INLINE basicUnsafeGrow  #-}
-  basicUnsafeGrow (MV_IPv4Range as bs) m_
-      = do
-          as' <- MGVector.basicUnsafeGrow as m_
-          bs' <- MGVector.basicUnsafeGrow bs m_
-          return $ MV_IPv4Range as' bs'
-
-instance GVector.Vector UVector.Vector IPv4Range where
-  {-# INLINE basicUnsafeFreeze  #-}
-  basicUnsafeFreeze (MV_IPv4Range as bs)
-      = do
-          as' <- GVector.basicUnsafeFreeze as
-          bs' <- GVector.basicUnsafeFreeze bs
-          return $ V_IPv4Range as' bs'
-  {-# INLINE basicUnsafeThaw  #-}
-  basicUnsafeThaw (V_IPv4Range as bs)
-      = do
-          as' <- GVector.basicUnsafeThaw as
-          bs' <- GVector.basicUnsafeThaw bs
-          return $ MV_IPv4Range as' bs'
-  {-# INLINE basicLength  #-}
-  basicLength (V_IPv4Range as _) = GVector.basicLength as
-  {-# INLINE basicUnsafeSlice  #-}
-  basicUnsafeSlice i_ m_ (V_IPv4Range as bs)
-      = V_IPv4Range (GVector.basicUnsafeSlice i_ m_ as)
-                    (GVector.basicUnsafeSlice i_ m_ bs)
-  {-# INLINE basicUnsafeIndexM  #-}
-  basicUnsafeIndexM (V_IPv4Range as bs) i_
-      = do
-          a <- GVector.basicUnsafeIndexM as i_
-          b <- GVector.basicUnsafeIndexM bs i_
-          return (IPv4Range a b)
-  {-# INLINE basicUnsafeCopy  #-}
-  basicUnsafeCopy (MV_IPv4Range as1 bs1) (V_IPv4Range as2 bs2)
-      = do
-          GVector.basicUnsafeCopy as1 as2
-          GVector.basicUnsafeCopy bs1 bs2
-  {-# INLINE elemseq  #-}
-  elemseq _ (IPv4Range a b)
-      = GVector.elemseq (undefined :: UVector.Vector a) a
-        . GVector.elemseq (undefined :: UVector.Vector b) b
-
-rangeBitwise :: (IPv4 -> IPv4 -> IPv4) -> IPv4Range -> IPv4Range -> IPv4Range
-rangeBitwise fun l r = range ip len
-  where
-    -- Normalise first
-    l' = normalize l
-    r' = normalize r
-    ip = (ipv4RangeBase l') `fun` (ipv4RangeBase r')
-    len = maximum [ipv4RangeLength l, ipv4RangeLength r]
-
-rangeRebase :: (IPv4 -> IPv4) -> IPv4Range -> IPv4Range
-rangeRebase fun r = range (fun $ ipv4RangeBase r) (ipv4RangeLength r)
-
--- | Notes:
---
---     * bit operations use network order (big endian),
---
---     * do not operate on host bits,
---
---     * return a normalized range dropping host bits,
---
---     * and "promote operands" by extending the length to the larger of two
---       ranges.
---
-instance Bits.Bits IPv4Range where
-  (.&.) = rangeBitwise (.&.)
-  (.|.) = rangeBitwise (.|.)
-  xor = rangeBitwise Bits.xor
-  complement = rangeRebase Bits.complement
-  shift r i = rangeRebase (flip Bits.shift i) r
-  rotate r i = rangeRebase (flip Bits.rotate i) r
-  bitSize = Bits.finiteBitSize
-  bitSizeMaybe = Just . Bits.finiteBitSize
-  isSigned = Bits.isSigned . ipv4RangeBase
-  testBit ip i = Bits.testBit (ipv4RangeBase ip) i
-  bit i = IPv4Range (Bits.bit i) $ fromIntegral $ i + 1
-  popCount = Bits.popCount . ipv4RangeBase . normalize
-
--- | Note: the size is determined by the range length
-instance Bits.FiniteBits IPv4Range where
-  finiteBitSize = fromIntegral . ipv4RangeLength
-
------------------
--- Internal Stuff
------------------
-
-rangeToDotDecimalText :: IPv4Range -> Text
-rangeToDotDecimalText = LText.toStrict . TBuilder.toLazyText . rangeToDotDecimalBuilder
-
-rangeToDotDecimalBuilder :: IPv4Range -> TBuilder.Builder
-rangeToDotDecimalBuilder (IPv4Range addr len) =
-     IPv4.builder addr
-  <> TBuilder.singleton '/'
-  <> TBI.decimal len
-
diff --git a/src/Net/IPv6.hs b/src/Net/IPv6.hs
--- a/src/Net/IPv6.hs
+++ b/src/Net/IPv6.hs
@@ -4,14 +4,14 @@
 {-# LANGUAGE MagicHash           #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE UnboxedTuples       #-}
+{-# LANGUAGE DeriveGeneric       #-}
+{-# LANGUAGE CPP                 #-}
 
 {-# OPTIONS_GHC -Wall #-}
 
 module Net.IPv6
-  ( -- * Types
-    IPv6(..)
-    -- * Convert
-  , ipv6
+  ( -- * Convert
+    ipv6
   , fromOctets
   , fromWord16s
   , fromWord32s
@@ -29,6 +29,24 @@
   , parser
     -- ** Printing
   , print
+    -- * IPv6 Ranges
+    -- ** Range functions
+  , range
+  , fromBounds
+  , normalize
+  , contains
+  , member
+  , lowerInclusive
+  , upperInclusive
+    -- ** Textual Conversion
+    -- *** Text
+  , encodeRange
+  , decodeRange
+  , parserRange
+  , printRange
+    -- * Types
+  , IPv6(..)
+  , IPv6Range(..)
   ) where
 
 import Net.IPv4 (IPv4(..))
@@ -38,6 +56,7 @@
 import Control.Monad.Primitive
 import Control.Monad.ST
 import Data.Bits
+import Data.ByteString (ByteString)
 import Data.Char (chr)
 import Data.List (intercalate, group)
 import Data.Primitive.Addr
@@ -45,14 +64,18 @@
 import Data.Primitive.Types (Prim(..))
 import Data.Text (Text)
 import Data.Word
+import GHC.Enum (predError, succError)
 import GHC.Exts
+import GHC.Generics (Generic)
 import Numeric (showHex)
 import Prelude hiding (any, print)
 import Text.ParserCombinators.ReadPrec (prec,step)
+import Text.Printf (printf)
 import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
 import qualified Data.Aeson as Aeson
 import qualified Data.Attoparsec.Text as AT
 import qualified Data.Attoparsec.Text as Atto
+import qualified Data.ByteString.Char8 as BC8
 import qualified Data.Text as Text
 import qualified Data.Text.IO as TIO
 
@@ -62,6 +85,9 @@
 --
 -- >>> import qualified Prelude as P
 -- >>> import qualified Data.Text.IO as T
+-- >>> import Test.QuickCheck (Arbitrary(..))
+-- >>> instance Arbitrary IPv6 where { arbitrary = IPv6 <$> arbitrary <*> arbitrary }
+-- >>> instance Arbitrary IPv6Range where { arbitrary = IPv6Range <$> arbitrary <*> arbitrary }
 --
 
 -- | A 128-bit Internet Protocol version 6 address.
@@ -70,6 +96,58 @@
   , ipv6B :: {-# UNPACK #-} !Word64
   } deriving (Eq,Ord)
 
+-- | Since 'IPv6' has more inhabitants than 'Int', the
+-- implementation of 'fromEnum' discards information.
+-- Currently, 'enumFromThen' and 'enumFromThenTo' emit
+-- an error, but this could be remedied if someone
+-- wants to provide an implementation of them.
+instance Enum IPv6 where
+  succ (IPv6 a b) 
+    | a == maxBound && b == maxBound = succError "IPv6"
+    | otherwise =
+        case b + 1 of
+          0 -> IPv6 (a + 1) 0
+          s -> IPv6 a s
+
+  pred (IPv6 a b)
+    | a == 0 && b == 0 = predError "IPv6"
+    | otherwise =
+        case b of
+          0 -> IPv6 (a - 1) maxBound
+          _ -> IPv6 a (b - 1)
+
+  toEnum :: Int -> IPv6
+  toEnum i = IPv6 0 (toEnum i)
+
+  fromEnum :: IPv6 -> Int
+  fromEnum (IPv6 _ b) = fromEnum b
+
+  {-# INLINE enumFrom #-}
+  enumFrom x = unfoldrLast (Just maxBound) (\b -> if b < maxBound then Just (b,succ b) else Nothing) x
+  {-# INLINE enumFromTo #-}
+  enumFromTo x y = unfoldrLast (if x <= y then Just y else Nothing) (\b -> if b < y then Just (b,succ b) else Nothing) x
+
+  enumFromThen = error "IPv6 currently lacks an implementation of enumFromThen"
+  enumFromThenTo = error "IPv6 currently lacks an implementation of enumFromThenTo"
+
+-- This is like unfoldr except that it adds an additional element
+-- at the end.
+unfoldrLast :: Maybe a -> (b -> Maybe (a, b)) -> b -> [a]
+{-# INLINE unfoldrLast #-}
+unfoldrLast a0 f b0 = build
+  (\c n ->
+    let go b = case f b of
+          Just (a, new_b) -> a `c` go new_b
+          Nothing -> case a0 of
+            Nothing -> n
+            Just x -> x `c` n
+     in go b0
+  )
+
+instance Bounded IPv6 where
+  minBound = IPv6 0 0
+  maxBound = IPv6 maxBound maxBound
+
 instance Show IPv6 where
   showsPrec p addr = showParen (p > 10)
     $ showString "ipv6 "
@@ -316,84 +394,52 @@
 decode t = rightToMaybe (AT.parseOnly (parser <* AT.endOfInput) t)
 
 parser :: Atto.Parser IPv6
-parser = startIP >>= makeIP
+parser = makeIP <$> ip
   where
-  -- handles the case where an IP starts with ::
-  startIP :: Atto.Parser ([Word16], Maybe [Word16])
-  startIP = 
-    (\ends -> ([], Just ends)) <$> (Atto.char ':' *> Atto.char ':' *> restOfIP []) <|>
-    fullIP []
+  makeIP [w1, w2, w3, w4, w5, w6, w7, w8] = fromWord16s w1 w2 w3 w4 w5 w6 w7 w8
+  makeIP _ = error "Net.IPv6.parser: Implementation error. Please open a bug report."
 
-  -- a full IP that might contain ::
-  fullIP :: [Word16] -> Atto.Parser ([Word16], Maybe [Word16])
-  fullIP starts =
-    ((\x -> (x ++ starts, Nothing)) <$> ipv4) <|>
-    startPart starts
+  ip = (Atto.char ':' *> Atto.char ':' *> doubleColon 0) <|> part 0
 
-  -- an IP that cannot contain ::
-  restOfIP :: [Word16] -> Atto.Parser [Word16]
-  restOfIP ends =
-    ((\x -> x ++ ends) <$> ipv4) <|>
-    endPart ends <|>
-    pure ends
-  
-  ipv4 = ipv4ToWord16s <$> IPv4.parser
-  ipv4ToWord16s (IPv4 ip) = [fromIntegral (ip .&. 0xFFFF), fromIntegral (ip `unsafeShiftR` 16)]
-  
-  -- a colon-separated part before ::
-  startPart (starts) = do
-    part <- Atto.hexadecimal
-    let result = (part : starts)
-    Atto.peekChar >>= \case 
-      Just ':' -> do
-        _ <- Atto.anyChar -- will be ':'
-        Atto.peekChar >>= \case
-          Just ':' -> do
-            _ <- Atto.anyChar -- will be ':' 
-            (\ends -> (result, Just ends)) <$> restOfIP []
-          _ ->
-            fullIP result
-      _ ->
-        pure (result, Nothing)
+  part :: Int -> Atto.Parser [Word16]
+  part n =
+    case n of
+      -- max 8 parts in an IPv6 address
+      7 -> pure <$> Atto.hexadecimal
+      -- after 6 parts it could end in IPv4 dotted notation
+      6 -> ipv4 <|> hexPart
+      _ -> hexPart
+    where
+    hexPart = (:)
+      <$> Atto.hexadecimal
+      <*> (Atto.char ':' *>
+            (
+             (Atto.char ':' *> doubleColon (n+1))
+             <|>
+             part (n+1)
+            )
+          )
 
-  -- a colon-separated part after ::
-  endPart ends = do
-    part <- Atto.hexadecimal
-    let result = part : ends
-    Atto.peekChar >>= \case 
-      Just ':' -> do
-        _ <- Atto.anyChar -- will be ':'
-        Atto.peekChar >>= \case
-          Just ':' -> do
-            fail "Cannot use double colon for omitting zeroes more than once in an IPv6 address"
-          _ ->
-            restOfIP result
-      _ ->
-        pure result
+  doubleColon :: Int -> Atto.Parser [Word16]
+  doubleColon count = do
+    rest <- afterDoubleColon <|> pure []
+    let fillerLength = (8 - count - length rest)
+    if fillerLength <= 0
+      then fail "too many parts in IPv6 address"
+      else pure (replicate fillerLength 0 ++ rest)
 
-  makeIP :: ([Word16], Maybe [Word16]) -> Atto.Parser IPv6
-  makeIP (starts, mends) =
-    case mends of 
-      -- Nothing indicates we never encountered double-colon, so we must have
-      -- all 8 parts:
-      Nothing -> do
-        if numStarts /= 8
-        then fail "not enough colon-separated parts in IPv6 address"
-        else
-          let [w1, w2, w3, w4, w5, w6, w7, w8] = reverse starts in
-          pure (fromWord16s w1 w2 w3 w4 w5 w6 w7 w8)
-      -- otherwise, we did encounter a double-colon, so we expand it to fill:
-      Just ends ->
-        let numEnds = length ends in
-        case compare (numStarts + numEnds) 8 of
-          GT -> fail "too many colon-separated parts in IPv6 address"
-          EQ -> fail "unnecessary double-colon in IPv6 address"
-          LT -> 
-            let [w1, w2, w3, w4, w5, w6, w7, w8] = reverse starts ++ replicate (8 - (numStarts + numEnds)) 0 ++ reverse ends in
-            pure (fromWord16s w1 w2 w3 w4 w5 w6 w7 w8)
-    where
-    numStarts = length starts
+  -- after double colon, IPv4 dotted notation could appear anywhere
+  afterDoubleColon :: Atto.Parser [Word16]
+  afterDoubleColon =
+    ipv4 <|>
+    (:) <$> Atto.hexadecimal <*> ((Atto.char ':' *> afterDoubleColon) <|> pure [])
 
+  ipv4 :: Atto.Parser [Word16]
+  ipv4 = ipv4ToWord16s <$> IPv4.parser
+
+  ipv4ToWord16s :: IPv4 -> [Word16]
+  ipv4ToWord16s (IPv4 word) = [fromIntegral (word `unsafeShiftR` 16), fromIntegral (word .&. 0xFFFF)]
+
 fromOctetsV6 ::
      Word64 -> Word64 -> Word64 -> Word64
   -> Word64 -> Word64 -> Word64 -> Word64
@@ -445,4 +491,125 @@
 
 fromWord32Word64 :: Word64 -> Word64 -> Word64
 fromWord32Word64 a b = fromIntegral (unsafeShiftL a 32 .|. b)
+
+data IPv6Range = IPv6Range
+  { ipv6RangeBase   :: {-# UNPACK #-} !IPv6
+  , ipv6RangeLength :: {-# UNPACK #-} !Word8
+  } deriving (Eq,Ord,Show,Read,Generic)
+
+mask :: Word8 -> Word64
+mask w = if w > 63
+  then 0xffffffffffffffff 
+  else complement (shiftR 0xffffffffffffffff (fromIntegral w))
+
+normalize :: IPv6Range -> IPv6Range
+normalize (IPv6Range (IPv6 w1 w2) len) =
+  let len' = min len 128
+      norm
+        | len' < 64 =  (IPv6Range (IPv6 (w1 .&. mask len') (w2 .&. mask 0)) len')
+        | otherwise =  (IPv6Range (IPv6 (w1 .&. mask 64) (w2 .&. mask (len' - 64))) len')
+  in norm
+
+encodeRange :: IPv6Range -> Text
+encodeRange x = encode (ipv6RangeBase x) <> Text.pack "/" <> (Text.pack $ (show . fromEnum) $ ipv6RangeLength x)
+
+decodeRange :: Text -> Maybe IPv6Range
+decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput)
+
+parserRange :: AT.Parser IPv6Range
+parserRange = do
+  ip <- parser
+  _ <- AT.char '/'
+  theMask <- AT.decimal >>= limitSize
+  return (normalize (IPv6Range ip theMask))
+  where
+  limitSize i =
+    if i > 128
+      then fail "An IP range length must be between 0 and 128"
+      else return i
+
+-- | Checks to see if an 'IPv6' address belongs in the 'IPv6Range'.
+--
+-- >>> let ip = ipv6 0x2001 0x0db8 0x0db8 0x1094 0x2051 0x0000 0x0000 0x0001
+-- >>> let iprange mask = IPv6Range (ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) mask
+-- >>> contains (iprange 8) ip
+-- True
+-- >>> contains (iprange 48) ip
+-- False
+--
+-- Typically, element-testing functions are written to take the element
+-- as the first argument and the set as the second argument. This is intentionally
+-- written the other way for better performance when iterating over a collection.
+-- For example, you might test elements in a list for membership like this:
+--
+-- >>> let r = IPv6Range (ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 64
+-- >>> fmap (contains r) (take 5 $ iterate succ $ ipv6 0x2001 0x0db8 0x0000 0x0000 0xffff 0xffff 0xffff 0xfffe)
+-- [True,True,False,False,False]
+--
+-- The implementation of 'contains' ensures that (with GHC), the bitmask
+-- creation and range normalization only occur once in the above example.
+-- They are reused as the list is iterated.
+contains :: IPv6Range -> IPv6 -> Bool
+contains (IPv6Range (IPv6 wsubnetA wsubnetB) len) = 
+  let lenA = if len > 64 then 64 else len
+      lenB = if len > 64 then len - 64 else 0
+      theMaskA = mask lenA
+      theMaskB = mask lenB
+      wsubnetNormalizedA = wsubnetA .&. theMaskA
+      wsubnetNormalizedB = wsubnetB .&. theMaskB
+   in \(IPv6 wA wB) ->
+        (wA .&. theMaskA) == wsubnetNormalizedA
+        &&
+        (wB .&. theMaskB) == wsubnetNormalizedB
+
+-- | This is provided to mirror the interface provided by @Data.Set@. It
+-- behaves just like 'contains' but with flipped arguments.
+--
+-- prop> member ip r == contains r ip
+member :: IPv6 -> IPv6Range -> Bool
+member = flip contains
+
+-- | The inclusive lower bound of an 'IPv6Range'. This is conventionally
+--   understood to be the broadcast address of a subnet. For example:
+--
+-- >>> T.putStrLn $ encode $ lowerInclusive $ IPv6Range (ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 25
+-- 2001:d80::
+--
+-- Note that the lower bound of a normalized 'IPv4Range' is simply the
+-- ip address of the range:
+--
+-- prop> lowerInclusive r == ipv6RangeBase (normalize r)
+lowerInclusive :: IPv6Range -> IPv6
+lowerInclusive (IPv6Range (IPv6 w1 w2) len) =
+  ipv6RangeBase (normalize (IPv6Range (IPv6 w1 w2) len))
+
+upperInclusive :: IPv6Range -> IPv6
+upperInclusive (IPv6Range (IPv6 w1 w2) len) =
+  let len' = min 128 len
+      theInvertedMask :: Word64
+      theInvertedMask = shiftR 0xffffffffffffffff (fromIntegral len')
+      theInvertedMask2 = shiftR 0xffffffffffffffff ((fromIntegral len')-64)
+      upper
+        | len' < 64 =  IPv6 ((w1 .|. theInvertedMask)) ((w2 .|. shiftR 0xffffffffffffffff 0))
+        | otherwise =  IPv6 (w1) (w2 .|. theInvertedMask2)
+  in upper
+
+-- | This exists mostly for testing purposes.
+printRange :: IPv6Range -> IO ()
+printRange = TIO.putStrLn . encodeRange
+
+range :: IPv6 -> Word8 -> IPv6Range
+range addr len = normalize (IPv6Range addr len)
+
+fromBounds :: IPv6 -> IPv6 -> IPv6Range
+fromBounds (IPv6 a1 a2) (IPv6 b1 b2) =
+  normalize (IPv6Range (IPv6 a1 a2) (maskFromBounds a1 b1 a2 b2))
+
+maskFromBounds :: Word64 -> Word64 -> Word64 -> Word64 -> Word8
+maskFromBounds lo1 hi1 lo2 hi2 =
+  let x = countLeadingZeros (xor lo1 hi1)
+      check
+        | x < 64    = fromIntegral x
+        | otherwise = fromIntegral $ x + (countLeadingZeros (xor lo2 hi2))
+  in check
 
diff --git a/src/Net/Types.hs b/src/Net/Types.hs
--- a/src/Net/Types.hs
+++ b/src/Net/Types.hs
@@ -5,14 +5,14 @@
   , IPv6(..)
   , IP(..)
   , IPv4Range(..)
+  , IPv6Range(..)
   , Mac(..)
   , MacCodec(..)
   , MacGrouping(..)
   ) where
 
-import Net.IPv6 (IPv6(..))
-import Net.IPv4 (IPv4(..))
-import Net.IPv4.Range (IPv4Range(..))
+import Net.IPv6 (IPv6(..),IPv6Range(..))
+import Net.IPv4 (IPv4(..), IPv4Range(..))
 import Net.IP (IP(..))
 import Net.Mac (Mac(..),MacCodec(..),MacGrouping(..))
 
diff --git a/test/Doctests.hs b/test/Doctests.hs
--- a/test/Doctests.hs
+++ b/test/Doctests.hs
@@ -5,7 +5,6 @@
   [ "src/Net/IPv4.hs"
   , "src/Net/IPv6.hs"
   , "src/Net/IP.hs"
-  , "src/Net/IPv4/Range.hs"
   , "src/Data/Word/Synthetic/Word12.hs"
   , "src/Data/Text/Builder/Common/Internal.hs"
   , "src/Data/Text/Builder/Fixed.hs"
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -6,23 +6,23 @@
 module Main (main) where
 
 import Naive
+import Control.Applicative (liftA2)
 import Data.Proxy (Proxy(..))
 import Test.Framework (defaultMain, testGroup, Test)
 import Test.Framework.Providers.QuickCheck2 (testProperty)
-import Test.QuickCheck (Arbitrary(..),Property,oneof,Gen,elements,choose)
+import Test.QuickCheck (Arbitrary(..),Property,oneof,Gen,elements,choose,(===))
 import Test.HUnit (Assertion,(@?=),(@=?))
 import Numeric (showHex)
 import Test.QuickCheck.Property (failed,succeeded,Result(..))
 import Data.Bifunctor
-import Test.QuickCheck.Classes (Laws(..),jsonLaws,showReadLaws,bitsLaws,primLaws)
+import Test.QuickCheck.Classes (Laws(..),jsonLaws,showReadLaws,bitsLaws,primLaws,boundedEnumLaws)
 import qualified Test.Framework.Providers.HUnit as PH
 
-import Net.Types (IP,IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..))
+import Net.Types (IP,IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..),IPv6Range(..))
 import qualified Data.Text as Text
 import qualified Data.ByteString.Char8 as BC8
 import qualified Net.IPv4 as IPv4
 import qualified Net.IPv6 as IPv6
-import qualified Net.IPv4.Range as IPv4Range
 import qualified Net.Mac as Mac
 import qualified Net.IP as IP
 
@@ -88,13 +88,36 @@
       , PH.testCase "Parser Failure Test Cases" testIPv6ParserFailure
       ]
     ]
-  , testGroup "IP Range Operations"
+  , testGroup "IPv4 Range Operations"
     [ testProperty "Idempotence of normalizing IPv4 range"
-        $ propIdempotence IPv4Range.normalize
+        $ propIdempotence IPv4.normalize
     , testProperty "Normalize does not affect membership" propNormalizeMember
     , testProperty "Membership agrees with bounds" propMemberUpperLower
     , testProperty "Range contains self" propRangeSelf
     ]
+  , testGroup "IPv6 Range Operations"
+    [ testProperty "Idempotence of normalizing IPv6 range"
+        $ propIdempotence IPv6.normalize
+    , testProperty "Normalize does not affect membership" $ \i r ->
+        IPv6.member i r == IPv6.member i (IPv6.normalize r)
+    , testProperty "Membership agrees with bounds" $ \i r ->
+        (i >= IPv6.lowerInclusive r && i <= IPv6.upperInclusive r) == IPv6.member i r
+    , testProperty "Range contains self" $ \r ->
+        IPv6.member (ipv6RangeBase r) r == True
+    , testProperty "Idempotence of upperInclusive-lowerInclusive and fromBounds" $ \r ->
+        IPv6.fromBounds (IPv6.lowerInclusive r) (IPv6.upperInclusive r) === r
+    , testGroup "Cases"
+      [ PH.testCase "A" $ False @=? IPv6.contains
+          (IPv6.range (IPv6.ipv6 0 0 0 1 0 0 0 0) 64)
+          (IPv6.ipv6 0 0 0 0 0 0 0 0)
+      , PH.testCase "B" $ True @=? IPv6.contains
+          (IPv6.range (IPv6.ipv6 0 0 0 0 0 0 0 0) 126)
+          (IPv6.ipv6 0 0 0 0 0 0 0 1)
+      , PH.testCase "C" $ False @=? IPv6.contains
+          (IPv6.range (IPv6.ipv6 0 0 0 0 0 0 0 0) 125)
+          (IPv6.ipv6 0 0 0 0 0 0 0 0xFFFF)
+      ]
+    ]
   , testGroup "Instances"
     [ testGroup "IPv4"
       [ lawsToTest (jsonLaws (Proxy :: Proxy IPv4))
@@ -110,6 +133,7 @@
       [ lawsToTest (jsonLaws (Proxy :: Proxy IPv6))
       , lawsToTest (showReadLaws (Proxy :: Proxy IPv6))
       , lawsToTest (primLaws (Proxy :: Proxy IPv6))
+      , lawsToTest (boundedEnumLaws (Proxy :: Proxy IPv6))
       ]
     , testGroup "IP"
       [ lawsToTest (jsonLaws (Proxy :: Proxy IP))
@@ -150,14 +174,14 @@
 propIdempotence f a = f a == f (f a)
 
 propNormalizeMember :: IPv4 -> IPv4Range -> Bool
-propNormalizeMember i r = IPv4Range.member i r == IPv4Range.member i (IPv4Range.normalize r)
+propNormalizeMember i r = IPv4.member i r == IPv4.member i (IPv4.normalize r)
 
 propMemberUpperLower :: IPv4 -> IPv4Range -> Bool
 propMemberUpperLower i r =
-  (i >= IPv4Range.lowerInclusive r && i <= IPv4Range.upperInclusive r) == IPv4Range.member i r
+  (i >= IPv4.lowerInclusive r && i <= IPv4.upperInclusive r) == IPv4.member i r
 
 propRangeSelf :: IPv4Range -> Bool
-propRangeSelf r = IPv4Range.member (ipv4RangeBase r) r == True
+propRangeSelf r = IPv4.member (ipv4RangeBase r) r == True
 
 testIPv4Decode :: Assertion
 testIPv4Decode = IPv4.decode (Text.pack "124.222.255.0")
@@ -218,16 +242,46 @@
 
 testIPv6ParserFailure :: Assertion
 testIPv6ParserFailure = do
-  go "1111:2222:3333:4444:5555:6666::7777:8888"
+  -- must not start or end in colon:
+  go ":::"
+  go "1::2:"
+  go ":1::2"
+  go "1:::"
+  go ":1::"
+  go "::1:"
+  go "1:2:3:4:5:6:777:8:"
+  go ":1:2:3:4:5:6:7777:8"
+
+  -- Incorrect numbers of parts:
+  go ""
+  go "1111"
+  go "1111:2222"
+  go "1111:2222:3333"
+  go "1111:2222:3333:4444"
+  go "1111:2222:3333:4444:5555"
+  go "1111:2222:3333:4444:5555:6666"
+  go "1111:2222:3333:4444:5555:6666:7777"
   go "1111:2222:3333:4444:5555:6666:7777:8888:9999"
-  go "1111:2222:3333:4444:5555:6666:7777:8888::9999"
 
-  go "1:127.0.0.1" -- not enough
+  -- Incorrect use of double-colon:
+  go "1111::2222::3333"
+  go "1111:2222:3333:4444:5555:6666::7777:8888" -- not needed
+  go "1111:2222:3333:4444:5555:6666:7777:8888::9999" -- too long
+
+  -- IPv4 decimal embedded, with not enough parts:
+  go "1:127.0.0.1"
   go "1:2:3:127.0.0.1"
   go "1:2:3:4:127.0.0.1"
   go "1:2:3:4:5:127.0.0.1"
 
-  go "1:2:3:4:5:6:7:127.0.0.1" -- too much
+  -- IPv4 decimal before double-colon:
+  go "1:127.0.0.1::"
+
+  -- Only IPv4:
+  go "127.0.0.1"
+
+  -- IPv4 decimal embedded, with too many parts:
+  go "1:2:3:4:5:6:7:127.0.0.1"
   go "1:2:3:4:5:6:7:8:127.0.0.1"
   where
   go str =
@@ -235,53 +289,53 @@
     @=? bimap (\_ -> ()) HexIPv6
       (AT.parseOnly
         (IPv6.parser <* AT.endOfInput)
-        (Text.pack str)
-      )
+        (Text.pack str))
 
 testIPv6Encode :: Assertion
 testIPv6Encode = do
 
-    -- degenerate cases:
-    "::" `roundTripsTo` "::"
-    "1234::" `roundTripsTo` "1234::"
-    "::1234" `roundTripsTo` "::1234"
+  -- degenerate cases:
+  "::" `roundTripsTo` "::"
+  "1234::" `roundTripsTo` "1234::"
+  "::1234" `roundTripsTo` "::1234"
 
-    -- zero-compression works:
-    "1234:1234:0000:0000:0000:0000:3456:3434" `roundTripsTo` "1234:1234::3456:3434"
+  -- zero-compression works:
+  "1234:1234:0000:0000:0000:0000:3456:3434" `roundTripsTo` "1234:1234::3456:3434"
 
-    -- picks first case:
-    "1234:0000:1234:0000:1234:0000:0123:1234" `roundTripsTo` "1234::1234:0:1234:0:123:1234"
+  -- picks first case:
+  "1234:0000:1234:0000:1234:0000:0123:1234" `roundTripsTo` "1234::1234:0:1234:0:123:1234"
 
-    -- picks longest case:
-    "1234:0000:1234:0000:0:0000:0123:1234" `roundTripsTo` "1234:0:1234::123:1234"
+  -- picks longest case:
+  "1234:0000:1234:0000:0:0000:0123:1234" `roundTripsTo` "1234:0:1234::123:1234"
 
-    -- can exclude all but first and last:
-    "1234::1234" `roundTripsTo` "1234::1234"
+  -- can exclude all but first and last:
+  "1234::1234" `roundTripsTo` "1234::1234"
 
-    -- prefers leftmost part to zero-compress:
-    "1:2:0:0:5::8" `roundTripsTo` "1:2::5:0:0:8"
+  -- prefers leftmost part to zero-compress:
+  "1:2:0:0:5::8" `roundTripsTo` "1:2::5:0:0:8"
 
-    -- can work with no zeroes:
-    "1:2:3:4:5:6:7:8" `roundTripsTo` "1:2:3:4:5:6:7:8"
+  -- can work with no zeroes:
+  "1:2:3:4:5:6:7:8" `roundTripsTo` "1:2:3:4:5:6:7:8"
 
-    -- works with only first or last:
-    "::2:3:4:5:6:7:8" `roundTripsTo` "::2:3:4:5:6:7:8"
-    "1:2:3:4:5:6:7::" `roundTripsTo` "1:2:3:4:5:6:7::"
+  -- works with only first or last:
+  "::2:3:4:5:6:7:8" `roundTripsTo` "::2:3:4:5:6:7:8"
+  "1:2:3:4:5:6:7::" `roundTripsTo` "1:2:3:4:5:6:7::"
 
-    -- decimal notation in IPv6 addresses:
-    "1:2:3:4:5:6:0.7.0.8" `roundTripsTo` "1:2:3:4:5:6:7:8"
-    "::0.0.0.0" `roundTripsTo` "::"
+  -- decimal notation in IPv6 addresses:
+  "1:2:3:4:5:6:0.7.0.8" `roundTripsTo` "1:2:3:4:5:6:7:8"
+  "::0.0.0.0" `roundTripsTo` "::"
 
-    -- per https://tools.ietf.org/html/rfc5952#section-5
-    "::ffff:0:0" `roundTripsTo` "::ffff:0.0.0.0"
-    "::ffff:00ff:ff00" `roundTripsTo` "::ffff:0.255.255.0"
-    "::ffff:203.0.113.17" `roundTripsTo` "::ffff:203.0.113.17"
+  -- per https://tools.ietf.org/html/rfc5952#section-5
+  "::ffff:0:0" `roundTripsTo` "::ffff:0.0.0.0"
+  "::ffff:00ff:ff00" `roundTripsTo` "::ffff:0.255.255.0"
+  "::ffff:203.0.113.17" `roundTripsTo` "::ffff:203.0.113.17"
+  "1234:5678::10.0.1.2" `roundTripsTo` "1234:5678::a00:102"
 
-   where
-   roundTripsTo s sExpected =
-     case AT.parseOnly (IPv6.parser <* AT.endOfInput) (Text.pack s) of
-        Right result -> IPv6.encode result @?= Text.pack sExpected
-        Left failMsg -> fail failMsg -- parse shouldn't fail here
+ where
+ roundTripsTo s sExpected =
+   case AT.parseOnly (IPv6.parser <* AT.endOfInput) (Text.pack s) of
+      Right result -> IPv6.encode result @?= Text.pack sExpected
+      Left failMsg -> fail ("failed to parse '" ++ s ++ "': " ++ failMsg)
 
 textBadIPv4 :: [String]
 textBadIPv4 =
@@ -343,6 +397,11 @@
 
 instance Arbitrary IPv6 where
   arbitrary = IPv6 <$> arbitrary <*> arbitrary
+  shrink (IPv6 a b) = filter (/= IPv6 a b)
+    [ IPv6 0 0
+    , IPv6 (div a 2) b
+    , IPv6 a (div b 2)
+    ]
 
 -- Half of the test cases generated are IPv6 mapped
 -- IPv4 addresses.
@@ -364,7 +423,14 @@
 -- This instance can generate masks that exceed the recommended
 -- length of 32.
 instance Arbitrary IPv4Range where
-  arbitrary = IPv4Range.range <$> arbitrary <*> choose (0,32)
+  arbitrary = IPv4.range <$> arbitrary <*> choose (0,32)
+
+instance Arbitrary IPv6Range where
+  arbitrary = IPv6.range <$> arbitrary <*> choose (0,128)
+  shrink (IPv6Range addr mask) = liftA2 IPv6.range
+    (shrink addr)
+    (filter (/= mask) [0,div mask 2,if mask > 0 then mask - 1 else 0])
+    
 
 instance Arbitrary MacCodec where
   arbitrary = MacCodec <$> arbitrary <*> arbitrary
