diff --git a/hw-ip.cabal b/hw-ip.cabal
--- a/hw-ip.cabal
+++ b/hw-ip.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: hw-ip
-version: 2.3.1.2
+version: 2.3.2.0
 license: BSD-3-Clause
 license-file: LICENSE
 copyright: 2018-2019 John Ky, David Turnbull, Jian Wan
@@ -53,7 +53,7 @@
           , text
   exposed-modules:
     HaskellWorks.Data.Network.Ip
-    HaskellWorks.Data.Network.Ip.Internal
+    HaskellWorks.Data.Network.Ip.Internal.Appar
     HaskellWorks.Data.Network.Ip.Ip
     HaskellWorks.Data.Network.Ip.Ipv4
     HaskellWorks.Data.Network.Ip.Ipv6
diff --git a/src/HaskellWorks/Data/Network/Ip/Internal.hs b/src/HaskellWorks/Data/Network/Ip/Internal.hs
deleted file mode 100644
--- a/src/HaskellWorks/Data/Network/Ip/Internal.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-module HaskellWorks.Data.Network.Ip.Internal where
-
-import Control.Applicative
-import Control.Monad
-import Data.Char
-import Data.Word
-import HaskellWorks.Data.Bits.BitWise
-
-import qualified Text.Appar.String as AP
-
-fourOctetsToWord32 :: Word8 -> Word8 -> Word8 -> Word8 -> Word32
-fourOctetsToWord32 a b c d =
-  (fromIntegral a .<. 24) .|.
-  (fromIntegral b .<. 16) .|.
-  (fromIntegral c .<.  8) .|.
-   fromIntegral d
-{-# INLINE fourOctetsToWord32 #-}
-
-infixl 4 #<*>#
-
-(#<*>#) :: AP.Parser Word8 -> AP.Parser Word8 -> AP.Parser Word8
-(#<*>#) pa pb = paste <$> pa <*> pb
-  where paste a b = a * 10 + b
-
-octet :: AP.Parser Word8
-octet = AP.try ((digits 1 2 #<*>#  digit 5  ) #<*># digits 0 5)
-  <|>   AP.try ((digits 1 2 #<*># digits 0 4) #<*># digits 0 9)
-  <|>   AP.try (( digit 1   #<*># digits 0 9) #<*># digits 0 9)
-  <|>   AP.try ( digits 1 9 #<*># digits 0 9)
-  <|>            digits 0 9
-
-whitespace :: AP.Parser ()
-whitespace = void $ many (AP.satisfy isSpace)
-
-ipv4Address :: AP.Parser Word32
-ipv4Address = fourOctetsToWord32
-  <$> (octet <* AP.char '.')
-  <*> (octet <* AP.char '.')
-  <*> (octet <* AP.char '.')
-  <*>  octet
-
-ipv4NetMask :: AP.Parser Word8
-ipv4NetMask =  AP.try (digit 3   #<*># digits 0 2)
-  <|>          AP.try (digit 2   #<*># digits 0 9)
-  <|>          AP.try (digit 1   #<*># digits 0 9)
-  <|>           digits 0 9
-
-digit :: Int -> AP.Parser Word8
-digit c      = fromIntegral . (+ (-48)) . ord <$> AP.satisfy (== chr (c + 48))
-
-digits :: Int -> Int -> AP.Parser Word8
-digits c1 c2 = fromIntegral . (+ (-48)) . ord <$> AP.satisfy (\c -> c >= chr (c1 + 48) && c <= chr (c2 + 48))
-
-ipv4Block :: AP.Parser (Word32, Word8)
-ipv4Block = do
-  addr <- ipv4Address
-  _    <- AP.char '/'
-  mask <- ipv4NetMask
-  return (addr, mask)
-
-word32x4ToWords :: (Word32, Word32, Word32, Word32) -> [Word32]
-word32x4ToWords (a, b, c, d) = [a, b, c, d]
-
-bitPower :: Word8 -> Word64
-bitPower m = fromIntegral (32 - m)
-
-blockSize :: Word8 -> Int
-blockSize m = 2 ^ bitPower m
-
-bitPower128 :: Word8 -> Integer
-bitPower128 m = fromIntegral (128 - m)
-
-blockSize128 :: Word8 -> Integer
-blockSize128 m = 2 ^ bitPower128 m
-
-readsPrecOnParser :: AP.Parser a -> Int -> String -> [(a, String)]
-readsPrecOnParser p _ s = case AP.runParser (whitespace *> p) s of
-    (Just a, r) -> [(a, r)]
-    _           -> []
diff --git a/src/HaskellWorks/Data/Network/Ip/Internal/Appar.hs b/src/HaskellWorks/Data/Network/Ip/Internal/Appar.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskellWorks/Data/Network/Ip/Internal/Appar.hs
@@ -0,0 +1,95 @@
+module HaskellWorks.Data.Network.Ip.Internal.Appar
+  ( fourOctetsToWord32
+  , (#<*>#)
+  , octet
+  , whitespace
+  , ipv4Address
+  , ipv4NetMask
+  , digit
+  , digits
+  , ipv4Block
+  , word32x4ToWords
+  , bitPower
+  , blockSize
+  , bitPower128
+  , blockSize128
+  , readsPrecOnParser
+  ) where
+
+import Control.Applicative
+import Control.Monad
+import Data.Char
+import Data.Word
+import HaskellWorks.Data.Bits.BitWise
+
+import qualified Text.Appar.String as AP
+
+fourOctetsToWord32 :: Word8 -> Word8 -> Word8 -> Word8 -> Word32
+fourOctetsToWord32 a b c d =
+  (fromIntegral a .<. 24) .|.
+  (fromIntegral b .<. 16) .|.
+  (fromIntegral c .<.  8) .|.
+   fromIntegral d
+{-# INLINE fourOctetsToWord32 #-}
+
+infixl 4 #<*>#
+
+(#<*>#) :: AP.Parser Word8 -> AP.Parser Word8 -> AP.Parser Word8
+(#<*>#) pa pb = paste <$> pa <*> pb
+  where paste a b = a * 10 + b
+
+octet :: AP.Parser Word8
+octet = AP.try ((digits 1 2 #<*>#  digit 5  ) #<*># digits 0 5)
+  <|>   AP.try ((digits 1 2 #<*># digits 0 4) #<*># digits 0 9)
+  <|>   AP.try (( digit 1   #<*># digits 0 9) #<*># digits 0 9)
+  <|>   AP.try ( digits 1 9 #<*># digits 0 9)
+  <|>            digits 0 9
+
+whitespace :: AP.Parser ()
+whitespace = void $ many (AP.satisfy isSpace)
+
+ipv4Address :: AP.Parser Word32
+ipv4Address = fourOctetsToWord32
+  <$> (octet <* AP.char '.')
+  <*> (octet <* AP.char '.')
+  <*> (octet <* AP.char '.')
+  <*>  octet
+
+ipv4NetMask :: AP.Parser Word8
+ipv4NetMask =  AP.try (digit 3   #<*># digits 0 2)
+  <|>          AP.try (digit 2   #<*># digits 0 9)
+  <|>          AP.try (digit 1   #<*># digits 0 9)
+  <|>           digits 0 9
+
+digit :: Int -> AP.Parser Word8
+digit c      = fromIntegral . (+ (-48)) . ord <$> AP.satisfy (== chr (c + 48))
+
+digits :: Int -> Int -> AP.Parser Word8
+digits c1 c2 = fromIntegral . (+ (-48)) . ord <$> AP.satisfy (\c -> c >= chr (c1 + 48) && c <= chr (c2 + 48))
+
+ipv4Block :: AP.Parser (Word32, Word8)
+ipv4Block = do
+  addr <- ipv4Address
+  _    <- AP.char '/'
+  mask <- ipv4NetMask
+  return (addr, mask)
+
+word32x4ToWords :: (Word32, Word32, Word32, Word32) -> [Word32]
+word32x4ToWords (a, b, c, d) = [a, b, c, d]
+
+bitPower :: Word8 -> Word64
+bitPower m = fromIntegral (32 - m)
+
+blockSize :: Word8 -> Int
+blockSize m = 2 ^ bitPower m
+
+bitPower128 :: Word8 -> Integer
+bitPower128 m = fromIntegral (128 - m)
+
+blockSize128 :: Word8 -> Integer
+blockSize128 m = 2 ^ bitPower128 m
+
+readsPrecOnParser :: AP.Parser a -> Int -> String -> [(a, String)]
+readsPrecOnParser p _ s = case AP.runParser (whitespace *> p) s of
+    (Just a, r) -> [(a, r)]
+    _           -> []
diff --git a/src/HaskellWorks/Data/Network/Ip/Ip.hs b/src/HaskellWorks/Data/Network/Ip/Ip.hs
--- a/src/HaskellWorks/Data/Network/Ip/Ip.hs
+++ b/src/HaskellWorks/Data/Network/Ip/Ip.hs
@@ -6,8 +6,10 @@
 
 module HaskellWorks.Data.Network.Ip.Ip
   ( IpBlock(..)
+  , Unaligned, Canonical
   , isCanonical
   , canonicalise
+  , canonicaliseIpBlock
   , firstIpAddress
   , lastIpAddress
   ) where
@@ -46,6 +48,11 @@
 canonicalise :: IpBlock Unaligned -> Maybe (IpBlock Canonical)
 canonicalise (IpBlockV4 (V4.IpBlock a m)) = mfilter isCanonical (Just $ IpBlockV4 (V4.IpBlock a m))
 canonicalise (IpBlockV6 (V6.IpBlock a m)) = mfilter isCanonical (Just $ IpBlockV6 (V6.IpBlock a m))
+
+-- | Canonicalise the block by zero-ing out the host bits
+canonicaliseIpBlock :: IpBlock v -> IpBlock Canonical
+canonicaliseIpBlock (IpBlockV4 b) = IpBlockV4 (V4.canonicaliseIpBlock b)
+canonicaliseIpBlock (IpBlockV6 b) = IpBlockV6 (V6.canonicaliseIpBlock b)
 
 firstIpAddress :: IpBlock Canonical -> (Word32, Word32, Word32, Word32)
 firstIpAddress (IpBlockV4 v4Block)                          = firstIpAddress (IpBlockV6 (V6.fromIpv4Block v4Block))
diff --git a/src/HaskellWorks/Data/Network/Ip/Ipv4.hs b/src/HaskellWorks/Data/Network/Ip/Ipv4.hs
--- a/src/HaskellWorks/Data/Network/Ip/Ipv4.hs
+++ b/src/HaskellWorks/Data/Network/Ip/Ipv4.hs
@@ -9,6 +9,7 @@
   ( IpAddress(..)
   , IpNetMask(..)
   , IpBlock(..)
+  , Unaligned, Canonical
   , bitPower
   , isCanonical
   , splitBlock
@@ -41,11 +42,11 @@
 import HaskellWorks.Data.Network.Ip.SafeEnum
 import HaskellWorks.Data.Network.Ip.Validity
 
-import qualified Data.Bits                             as B
-import qualified Data.Sequence                         as S
-import qualified Data.Text                             as T
-import qualified HaskellWorks.Data.Network.Ip.Internal as I
-import qualified Text.Appar.String                     as AP
+import qualified Data.Bits                                   as B
+import qualified Data.Sequence                               as S
+import qualified Data.Text                                   as T
+import qualified HaskellWorks.Data.Network.Ip.Internal.Appar as I
+import qualified Text.Appar.String                           as AP
 
 newtype IpAddress = IpAddress
   { word :: Word32
diff --git a/src/HaskellWorks/Data/Network/Ip/Ipv6.hs b/src/HaskellWorks/Data/Network/Ip/Ipv6.hs
--- a/src/HaskellWorks/Data/Network/Ip/Ipv6.hs
+++ b/src/HaskellWorks/Data/Network/Ip/Ipv6.hs
@@ -10,6 +10,7 @@
   ( IpAddress(..)
   , IpNetMask(..)
   , IpBlock(..)
+  , Unaligned, Canonical
   , fromIpv4
   , fromIpv4Block
   , fromV4
@@ -41,12 +42,12 @@
 import Prelude                               hiding (words)
 import Text.Read
 
-import qualified Data.Bits                             as B
-import qualified Data.IP                               as D
-import qualified Data.Text                             as T
-import qualified HaskellWorks.Data.Network.Ip.Internal as I
-import qualified HaskellWorks.Data.Network.Ip.Ipv4     as V4
-import qualified HaskellWorks.Data.Network.Ip.Word128  as W
+import qualified Data.Bits                                   as B
+import qualified Data.IP                                     as D
+import qualified Data.Text                                   as T
+import qualified HaskellWorks.Data.Network.Ip.Internal.Appar as I
+import qualified HaskellWorks.Data.Network.Ip.Ipv4           as V4
+import qualified HaskellWorks.Data.Network.Ip.Word128        as W
 
 newtype IpAddress = IpAddress W.Word128 deriving (Enum, Eq, Ord, Bounded, Generic, SafeEnum)
 
diff --git a/test/HaskellWorks/Data/Network/Ipv4Spec.hs b/test/HaskellWorks/Data/Network/Ipv4Spec.hs
--- a/test/HaskellWorks/Data/Network/Ipv4Spec.hs
+++ b/test/HaskellWorks/Data/Network/Ipv4Spec.hs
@@ -3,7 +3,7 @@
 
 module HaskellWorks.Data.Network.Ipv4Spec (spec) where
 
-import HaskellWorks.Data.Network.Ip.Internal
+import HaskellWorks.Data.Network.Ip.Internal.Appar
 import HaskellWorks.Data.Network.Ip.Ipv4
 import HaskellWorks.Data.Network.Ip.Range
 import HaskellWorks.Data.Network.Ip.Validity
@@ -11,14 +11,14 @@
 import Hedgehog
 import Test.Hspec
 
-import qualified Data.List                             as DL
-import qualified HaskellWorks.Data.Network.Gen         as G
-import qualified HaskellWorks.Data.Network.Ip.Internal as I
-import qualified HaskellWorks.Data.Network.Ip.Ipv4     as I
-import qualified HaskellWorks.Data.Network.Ip.Range    as I
-import qualified Hedgehog.Gen                          as G
-import qualified Hedgehog.Range                        as R
-import qualified Text.Appar.String                     as AP
+import qualified Data.List                                   as DL
+import qualified HaskellWorks.Data.Network.Gen               as G
+import qualified HaskellWorks.Data.Network.Ip.Internal.Appar as I
+import qualified HaskellWorks.Data.Network.Ip.Ipv4           as I
+import qualified HaskellWorks.Data.Network.Ip.Range          as I
+import qualified Hedgehog.Gen                                as G
+import qualified Hedgehog.Range                              as R
+import qualified Text.Appar.String                           as AP
 
 {-# ANN module ("HLint: ignore Redundant do"  :: String) #-}
 
