diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2011-2013 Mikhail Vorozhtsov
+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 names of the copyright owners nor the names of the 
+  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/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/network-ip.cabal b/network-ip.cabal
new file mode 100644
--- /dev/null
+++ b/network-ip.cabal
@@ -0,0 +1,60 @@
+Name: network-ip
+Version: 0.0.1
+Category: Network
+Stability: experimental
+Synopsis: Internet Protocol data structures
+Description:
+  This package provides Internet Protocol data structures
+
+Homepage: https://github.com/mvv/network-ip
+Bug-Reports: https://github.com/mvv/network-ip/issues
+
+Author: Mikhail Vorozhtsov <mikhail.vorozhtsov@gmail.com>
+Maintainer: Mikhail Vorozhtsov <mikhail.vorozhtsov@gmail.com>
+Copyright: 2011-2013 Mikhail Vorozhtsov <mikhail.vorozhtsov@gmail.com>
+License: BSD3
+License-File: LICENSE
+
+Cabal-Version: >= 1.10.0
+Build-Type: Simple
+
+Source-Repository head
+  Type: git
+  Location: https://github.com/mvv/network-ip.git
+
+Library
+  Default-Language: Haskell2010
+  Build-Depends:
+    base         >= 4.3 && < 5,
+    data-default >= 0.2,
+    data-endian  >= 0.0.1,
+    data-dword   >= 0.2,
+    tagged       >= 0.2,
+    hashable     >= 1.1,
+    binary       >= 0.5,
+    cereal       >= 0.3,
+    text-printer >= 0.3,
+    data-textual >= 0.1,
+    parsers      >= 0.5
+  Hs-Source-Dirs: src
+  GHC-Options: -Wall
+  Exposed-Modules:
+    Network.IP.Addr
+
+Test-Suite tests
+  Default-Language: Haskell2010
+  Type: exitcode-stdio-1.0
+  Build-Depends:
+    base                       >= 4 && < 5,
+    test-framework             >= 0.6,
+    test-framework-quickcheck2 >= 0.2,
+    QuickCheck                 >= 2.4,
+    data-dword,
+    text-printer,
+    data-textual,
+    parsers,
+    network-ip
+  Hs-Source-Dirs: tests
+  GHC-Options: -Wall
+  Main-Is: Tests.hs
+
diff --git a/src/Network/IP/Addr.hs b/src/Network/IP/Addr.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/IP/Addr.hs
@@ -0,0 +1,672 @@
+{-# LANGUAGE UnicodeSyntax #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | Internet Protocol addressing.
+module Network.IP.Addr
+  ( 
+    -- * Host address
+    -- ** IPv4 address
+    IP4(..)
+  , anIP4
+  , ip4ToOctets
+  , ip4ToOctetList
+  , ip4FromOctets
+  , ip4FromOctetList
+  , anyIP4
+    -- ** IPv6 address
+  , IP6(..)
+  , anIP6
+  , ip6ToWords
+  , ip6ToWordList
+  , ip6FromWords
+  , ip6FromWordList
+  , anyIP6
+    -- ** IP address
+  , IP(..)
+  , anIP
+  -- * Network address
+  , IsNetAddr(..)
+  , Net4Addr
+  , Net6Addr
+  , NetAddr
+  , aNetAddr
+  , aNetAddrOf
+  , aNet4Addr
+  , aNet6Addr
+  , aNetAddrIP
+  , printNetAddr
+  , net4Parser
+  , net6Parser
+  , netParser
+  -- * Port number
+  , InetPort(..)
+  , anInetPort
+  -- * Socket address
+  , InetAddr(..)
+  , Inet4Addr
+  , Inet6Addr
+  , anInetAddr
+  , anInetAddrOf
+  , anInet4Addr
+  , anInet6Addr
+  , anInetAddrIP
+  ) where
+
+import Prelude hiding (print)
+import Data.Typeable (Typeable, Typeable1)
+import Data.Proxy (Proxy(..))
+import Data.Word
+import Data.Bits
+import Data.DoubleWord (BinaryWord(..), DoubleWord(..), Word128)
+import Data.Ix (Ix)
+import Data.Endian
+import Data.Default
+import Data.Hashable
+import Data.Binary (Binary)
+import qualified Data.Binary as B
+import Data.Serialize (Serialize)
+import qualified Data.Serialize as S
+import Text.Printer (Printer, (<>))
+import qualified Text.Printer as P
+import qualified Text.Printer.Numerals as P
+import Data.Textual
+import Data.Textual.Numerals hiding (Binary)
+import Text.Parser.Combinators ((<?>), try)
+import Text.Parser.Char (CharParsing)
+import qualified Text.Parser.Char as PC
+import Text.Printf (printf)
+import Control.Applicative
+import Control.Monad (void, when)
+import Foreign.Ptr (castPtr)
+import Foreign.Storable (Storable(..))
+
+-- | IPv4 address.
+newtype IP4 = IP4 { unIP4 ∷ Word32 }
+  deriving (Typeable, Eq, Ord, Bounded, Enum, Ix, Num, Bits, Hashable,
+            Binary, Serialize)
+
+-- | 'IP4' proxy value.
+anIP4 ∷ Proxy IP4
+anIP4 = Proxy
+
+instance Show IP4 where
+  showsPrec p a = showParen (p > 10)
+                $ showString "ip4FromOctets "
+                . showsPrec 11 o1
+                . showString " "
+                . showsPrec 11 o2
+                . showString " "
+                . showsPrec 11 o3
+                . showString " "
+                . showsPrec 11 o4
+    where (o1, o2, o3, o4) = ip4ToOctets a
+
+instance Printable IP4 where
+  print = P.fsep (P.char7 '.') . (P.nonNegative Decimal <$>) . ip4ToOctetList
+  {-# INLINE print #-}
+
+instance Textual IP4 where
+  textual  =  (<?> "IPv4 address")
+           $  ip4FromOctets
+          <$> (octet <* PC.char '.')
+          <*> (octet <* PC.char '.')
+          <*> (octet <* PC.char '.')
+          <*> octet
+    where octet = (<?> "IPv4 octet") $ do
+            o ← nnUpTo Decimal 3
+            if o > 255
+              then fail "out of bounds"
+              else return $ fromIntegral (o ∷ Int)
+
+instance Storable IP4 where
+  alignment _ = alignment (undefined ∷ Word32)
+  sizeOf _    = 4
+  peek p      = IP4 . fromBigEndian <$> peek (castPtr p)
+  poke p      = poke (castPtr p) . toBigEndian . unIP4
+
+-- | The octets of an IPv4 address.
+ip4ToOctets ∷ IP4 → (Word8, Word8, Word8, Word8)
+ip4ToOctets (IP4 w) = ( fromIntegral $ w `shiftR` 24
+                      , fromIntegral $ w `shiftR` 16
+                      , fromIntegral $ w `shiftR` 8
+                      , fromIntegral w
+                      )
+{-# INLINABLE ip4ToOctets #-}
+
+-- | List the octets of an IPv4 address.
+ip4ToOctetList ∷ IP4 → [Word8]
+ip4ToOctetList (IP4 w) = fromIntegral <$>
+                           [w `shiftR` 24, w `shiftR` 16, w `shiftR` 8, w]
+{-# INLINABLE ip4ToOctetList #-}
+
+-- | Assemble IPv4 address from the octets.
+ip4FromOctets ∷ Word8 → Word8 → Word8 → Word8 → IP4
+ip4FromOctets o1 o2 o3 o4 =
+  IP4  $  fromIntegral o1 `shiftL` 24
+      .|. fromIntegral o2 `shiftL` 16
+      .|. fromIntegral o3 `shiftL` 8
+      .|. fromIntegral o4
+{-# INLINABLE ip4FromOctets #-}
+
+-- | Assemble IPv4 address from the octet list.
+ip4FromOctetList ∷ [Word8] → Maybe IP4
+ip4FromOctetList [o1, o2, o3, o4] = Just $ ip4FromOctets o1 o2 o3 o4
+ip4FromOctetList _                = Nothing
+{-# INLINABLE ip4FromOctetList #-}
+
+-- | IPv4 address @0.0.0.0@.
+anyIP4 ∷ IP4
+anyIP4 = IP4 0
+{-# INLINE anyIP4 #-}
+
+instance Default IP4 where
+  def = anyIP4
+  {-# INLINE def #-}
+
+-- | IPv6 address.
+newtype IP6 = IP6 { unIP6 ∷ Word128 }
+  deriving (Typeable, Eq, Ord, Bounded, Enum, Ix, Num, Bits, Hashable)
+
+-- | 'IP6' proxy value.
+anIP6 ∷ Proxy IP6
+anIP6 = Proxy
+
+instance Show IP6 where
+  showsPrec p a
+      = showParen (p > 10)
+      $ showString
+      $ printf "ip6FromWords 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x"
+               w1 w2 w3 w4 w5 w6 w7 w8
+    where
+      (w1, w2, w3, w4, w5, w6, w7, w8) = ip6ToWords a
+
+instance Printable IP6 where
+  print addr = case addrZeroes of
+      Nothing → P.fsep (P.char7 ':') $ hex <$> addrWords
+      Just (i, n) →  P.fsep (P.char7 ':') (hex <$> take i addrWords)
+                  <> P.string7 "::"
+                  <> P.fsep (P.char7 ':') (hex <$> drop (i + n) addrWords)
+    where hex        = P.nonNegative LowHex
+          addrWords  = ip6ToWordList addr
+          addrZeroes = go 0 0 0 addrWords
+          go !i zi zn = \case
+            []           → if zn <= 1 then Nothing else Just (zi, zn)
+            (0 : words') → inZeroes (i + 1) zi zn i 1 words'
+            (_ : words') → go (i + 1) zi zn words'
+          inZeroes !i zi zn zi' !zn' = \case
+            [] | zn' <= 1  → if zn <= 1 then Nothing else Just (zi, zn)
+               | zn <= 1   → Just (zi', zn')
+               | otherwise → Just $ if zn >= zn' then (zi, zn) else (zi', zn')
+            (0 : words') → inZeroes (i + 1) zi zn zi' (zn' + 1) words'
+            (_ : words') | zn' > zn  → go (i + 1) zi' zn' words'
+                         | otherwise → go (i + 1) zi zn words'
+
+instance Textual IP6 where
+  textual = (<?> "IPv6 address") $ optional (PC.char ':') >>= \case
+              Just _ → do
+                void $ PC.char ':'
+                optional word >>= \case
+                  Just w → after 6 0 (fromIntegral w)
+                  Nothing → return anyIP6
+              Nothing → do
+                w ← word
+                before 7 (fromIntegral w)
+    where 
+      word = nnUpTo Hexadecimal 4 `hintTypeArg` aWord16 <?> "word"
+      before 0 w = return $ IP6 w
+      before 1 w = do
+        void $ PC.char ':'
+        optional (PC.char ':') >>= \case
+          Just _ →
+            return $ IP6 $ w `shiftL` 16
+          Nothing → do
+            w' ← word
+            return $ IP6 $ (w `shiftL` 16) .|. fromIntegral w'
+      before upTo w = do
+        void $ PC.char ':'
+        optional (PC.char ':') >>= \case
+          Just _ → optional word >>= \case
+            Just w' →
+              after (upTo - 2) (w `shiftL` (upTo * 16)) (fromIntegral w')
+            Nothing →
+              return $ IP6 $ w `shiftL` (upTo * 16)
+          Nothing → do
+            w' ← word
+            before (upTo - 1) ((w `shiftL` 16) .|. fromIntegral w')
+      after 0 w1 w2 = return $ IP6 $ w1 .|. w2
+      after upTo w1 w2 = optional (PC.char ':') >>= \case
+        Just _ → do
+          w ← word
+          after (upTo - 1) w1 ((w2 `shiftL` 16) .|. fromIntegral w)
+        Nothing →
+          return $ IP6 $ w1 .|. w2
+
+instance Storable IP6 where
+  alignment _ = alignment (undefined ∷ Word64)
+  sizeOf _    = 16
+  peek p      = fmap IP6
+              $ fromHiAndLo <$> (fromBigEndian <$> peek (castPtr p))
+                            <*> (fromBigEndian <$> peek (castPtr p))
+  poke p (IP6 w) = do
+    poke (castPtr p) $ toBigEndian $ hiWord w
+    poke (castPtr p) $ toBigEndian $ loWord w
+
+instance Binary IP6 where
+  get = fmap IP6 $ fromHiAndLo <$> B.get <*> B.get
+  put (IP6 w) = do
+    B.put $ hiWord w
+    B.put $ loWord w
+
+instance Serialize IP6 where
+  get = fmap IP6 $ fromHiAndLo <$> S.get <*> S.get
+  put (IP6 w) = do
+    S.put $ hiWord w
+    S.put $ loWord w
+
+-- | The 16-bit pieces of an IPv6 address.
+ip6ToWords ∷ IP6 → (Word16, Word16, Word16, Word16,
+                    Word16, Word16, Word16, Word16)
+ip6ToWords (IP6 w) =
+    ( fromIntegral $ hi `shiftR` 48
+    , fromIntegral $ hi `shiftR` 32
+    , fromIntegral $ hi `shiftR` 16
+    , fromIntegral hi
+    , fromIntegral $ lo `shiftR` 48
+    , fromIntegral $ lo `shiftR` 32
+    , fromIntegral $ lo `shiftR` 16
+    , fromIntegral lo
+    )
+  where hi = hiWord w
+        lo = loWord w
+{-# INLINABLE ip6ToWords #-}
+
+-- | List the 16-bit pieces of an IPv6 address.
+ip6ToWordList ∷ IP6 → [Word16]
+ip6ToWordList (IP6 w) = fromIntegral <$>
+    [ hi `shiftR` 48
+    , hi `shiftR` 32
+    , hi `shiftR` 16
+    , hi
+    , lo `shiftR` 48
+    , lo `shiftR` 32
+    , lo `shiftR` 16
+    , lo
+    ]
+  where hi = hiWord w
+        lo = loWord w
+{-# INLINABLE ip6ToWordList #-}
+
+-- | Assemble IPv6 address from the 16-bit pieces.
+ip6FromWords ∷ Word16 → Word16 → Word16 → Word16
+             → Word16 → Word16 → Word16 → Word16
+             → IP6
+ip6FromWords w1 w2 w3 w4 w5 w6 w7 w8 = IP6 $ fromHiAndLo hi lo
+  where hi  =  fromIntegral w1 `shiftL` 48
+           .|. fromIntegral w2 `shiftL` 32
+           .|. fromIntegral w3 `shiftL` 16
+           .|. fromIntegral w4
+        lo  =  fromIntegral w5 `shiftL` 48
+           .|. fromIntegral w6 `shiftL` 32
+           .|. fromIntegral w7 `shiftL` 16
+           .|. fromIntegral w8
+{-# INLINABLE ip6FromWords #-}
+
+-- | Assemble IPv6 address from the list of 16-bit pieces.
+ip6FromWordList ∷ [Word16] → Maybe IP6
+ip6FromWordList [w1, w2, w3, w4, w5, w6, w7, w8] =
+  Just $ ip6FromWords w1 w2 w3 w4 w5 w6 w7 w8
+ip6FromWordList _ = Nothing
+{-# INLINABLE ip6FromWordList #-}
+
+-- | IPv6 address @::@.
+anyIP6 ∷ IP6
+anyIP6 = IP6 0
+{-# INLINE anyIP6 #-}
+
+instance Default IP6 where
+  def = anyIP6
+  {-# INLINE def #-}
+
+-- IP address.
+data IP = IPv4 {-# UNPACK #-} !IP4
+        | IPv6 {-# UNPACK #-} !IP6
+        deriving (Typeable, Eq, Ord, Show)
+
+-- | 'IP' proxy value.
+anIP ∷ Proxy IP
+anIP = Proxy
+
+instance Printable IP where
+  print (IPv4 a) = print a
+  print (IPv6 a) = print a
+  {-# INLINABLE print #-}
+
+instance Textual IP where
+  textual = try (IPv4 <$> textual) <|> (IPv6 <$> textual)
+  {-# INLINABLE textual #-}
+
+instance Binary IP where
+  get = B.get >>= return . either IPv4 IPv6
+  put (IPv4 a) = B.put (Left a ∷ Either IP4 IP6)
+  put (IPv6 a) = B.put (Right a ∷ Either IP4 IP6)
+
+instance Serialize IP where
+  get = S.get >>= return . either IPv4 IPv6
+  put (IPv4 a) = S.put (Left a ∷ Either IP4 IP6)
+  put (IPv6 a) = S.put (Right a ∷ Either IP4 IP6)
+
+-- | Network address.
+class IsNetAddr n where
+  -- | The address type.
+  type NetHost n
+  -- | Host address.
+  netHost   ∷ n → NetHost n
+  -- | Host index.
+  netHostIx ∷ n → NetHost n
+  -- | Network prefix.
+  netPrefix ∷ n → NetHost n
+  -- | Network mask.
+  netMask   ∷ n → NetHost n
+  -- | Network prefix length.
+  netLength ∷ n → Word8
+  -- | Make a network address.
+  netAddr   ∷ NetHost n -- ^ Host address
+            → Word8     -- ^ Network prefix length
+            → n
+
+-- | Network address: host address + network mask length.
+data NetAddr a = NetAddr a {-# UNPACK #-} !Word8
+                 deriving (Typeable, Eq)
+
+type Net4Addr = NetAddr IP4
+type Net6Addr = NetAddr IP6
+
+-- | 'NetAddr' proxy value.
+aNetAddr ∷ Proxy NetAddr
+aNetAddr = Proxy
+
+-- | 'NetAddr' /a/ proxy value.
+aNetAddrOf ∷ Proxy a → Proxy (NetAddr a)
+aNetAddrOf _ = Proxy
+
+-- | 'Net4Addr' proxy value.
+aNet4Addr ∷ Proxy Net4Addr
+aNet4Addr = Proxy
+
+-- | 'Net6Addr' proxy value.
+aNet6Addr ∷ Proxy Net6Addr
+aNet6Addr = Proxy
+
+-- | 'NetAddr' 'IP' proxy value.
+aNetAddrIP ∷ Proxy (NetAddr IP)
+aNetAddrIP = Proxy
+
+instance Show a ⇒ Show (NetAddr a) where
+  showsPrec p (NetAddr a w) = showParen (p > 10)
+                            $ showString "netAddr "
+                            . showsPrec 11 a
+                            . showString " "
+                            . showsPrec 11 w
+
+instance Printable a ⇒ Printable (NetAddr a) where
+  print (NetAddr a m) = print a <> P.char7 '/' <> print m
+  {-# INLINE print #-}
+
+instance Textual (NetAddr IP4) where
+  textual = net4Parser
+  {-# INLINE textual #-}
+
+instance Textual (NetAddr IP6) where
+  textual = net6Parser
+  {-# INLINE textual #-}
+
+instance Textual (NetAddr IP) where
+  textual = netParser
+  {-# INLINE textual #-}
+
+instance Binary Net4Addr where
+  get = netAddr <$> B.get <*> B.get
+  put (NetAddr a w) = B.put a >> B.put w
+
+instance Binary Net6Addr where
+  get = netAddr <$> B.get <*> B.get
+  put (NetAddr a w) = B.put a >> B.put w
+
+instance Binary (NetAddr IP) where
+  get = netAddr <$> B.get <*> B.get
+  put (NetAddr a w) = B.put a >> B.put w
+
+instance Serialize Net4Addr where
+  get = netAddr <$> S.get <*> S.get
+  put (NetAddr a w) = S.put a >> S.put w
+
+instance Serialize Net6Addr where
+  get = netAddr <$> S.get <*> S.get
+  put (NetAddr a w) = S.put a >> S.put w
+
+instance Serialize (NetAddr IP) where
+  get = netAddr <$> S.get <*> S.get
+  put (NetAddr a w) = S.put a >> S.put w
+
+instance IsNetAddr Net4Addr where
+  type NetHost Net4Addr = IP4
+  netHost (NetAddr a _) = a
+  {-# INLINE netHost #-}
+  netHostIx (NetAddr a w) = (a `shiftL` l) `shiftR` l
+    where l = fromIntegral w
+  {-# INLINE netHostIx #-}
+  netPrefix (NetAddr a w) = (a `shiftR` l) `shiftL` l
+    where l = 32 - fromIntegral w
+  {-# INLINE netPrefix #-}
+  netMask (NetAddr _ w) = IP4 $ (allOnes `shiftR` l) `shiftL` l
+    where l = 32 - fromIntegral w
+  {-# INLINE netMask #-}
+  netLength (NetAddr _ w) = w
+  {-# INLINE netLength #-}
+  netAddr a w = NetAddr a (w `min` 32)
+  {-# INLINE netAddr #-}
+
+instance IsNetAddr Net6Addr where
+  type NetHost Net6Addr = IP6
+  netHost (NetAddr a _) = a
+  {-# INLINE netHost #-}
+  netHostIx (NetAddr a w) = (a `shiftL` l) `shiftR` l
+    where l = fromIntegral w
+  {-# INLINE netHostIx #-}
+  netPrefix (NetAddr a w) = (a `shiftR` l) `shiftL` l
+    where l = 128 - fromIntegral w
+  {-# INLINE netPrefix #-}
+  netMask (NetAddr _ w) = IP6 $ (allOnes `shiftR` l) `shiftL` l
+    where l = 128 - fromIntegral w
+  {-# INLINE netMask #-}
+  netLength (NetAddr _ w) = w
+  {-# INLINE netLength #-}
+  netAddr a w = NetAddr a (w `min` 128)
+  {-# INLINE netAddr #-}
+
+instance IsNetAddr (NetAddr IP) where
+  type NetHost (NetAddr IP) = IP
+  netHost (NetAddr a _) = a
+  {-# INLINE netHost #-}
+  netHostIx (NetAddr a w) = case a of
+      IPv4 a1 → IPv4 $ (a1 `shiftL` l) `shiftR` l
+      IPv6 a1 → IPv6 $ (a1 `shiftL` l) `shiftR` l
+    where l = fromIntegral w
+  {-# INLINABLE netHostIx #-}
+  netPrefix (NetAddr a w) = case a of
+    IPv4 a1 → IPv4 $ (a1 `shiftR` l) `shiftL` l
+      where l = 32 - fromIntegral w
+    IPv6 a1 → IPv6 $ (a1 `shiftR` l) `shiftL` l
+      where l = 128 - fromIntegral w
+  {-# INLINABLE netPrefix #-}
+  netMask (NetAddr a w) = case a of
+    IPv4 _ → IPv4 $ IP4 $ (allOnes `shiftR` l) `shiftL` l
+      where l = 32 - fromIntegral w
+    IPv6 _ → IPv6 $ IP6 $ (allOnes `shiftR` l) `shiftL` l
+      where l = 128 - fromIntegral w
+  {-# INLINABLE netMask #-}
+  netLength (NetAddr _ w) = w
+  {-# INLINE netLength #-}
+  netAddr a w = NetAddr a (w `min` m)
+    where m = case a of
+                IPv4 _ → 32
+                IPv6 _ → 128
+  {-# INLINABLE netAddr #-}
+
+-- | Print network address (CIDR notation).
+printNetAddr ∷ (IsNetAddr n, Printable (NetHost n), Printer p) ⇒ n → p
+printNetAddr n = print (netHost n) <> P.char7 '/' <> print (netLength n)
+{-# INLINABLE printNetAddr #-}
+
+ip4Mask ∷ (CharParsing μ, Monad μ) ⇒ μ Word8
+ip4Mask = (<?> "network prefix length") $ do
+  m ← nncUpTo Decimal 2
+  when (m > 32) $ fail "out of bounds"
+  return m
+
+-- | IPv4 network address parser (CIDR notation).
+net4Parser ∷ (CharParsing μ, Monad μ, IsNetAddr n, NetHost n ~ IP4) ⇒ μ n
+net4Parser  =  netAddr <$> textual <*> (PC.char '/' *> ip4Mask)
+           <?> "IPv4 network address"
+{-# INLINE net4Parser #-}
+
+ip6Mask ∷ (CharParsing μ, Monad μ) ⇒ μ Word8
+ip6Mask = (<?> "network prefix length") $ do
+  m ← nncUpTo Decimal 3
+  when (m > (128 ∷ Int)) $ fail "out of bounds"
+  return $ fromIntegral m
+
+-- | IPv6 network address parser (CIDR notation).
+net6Parser ∷ (CharParsing μ, Monad μ, IsNetAddr n, NetHost n ~ IP6) ⇒ μ n
+net6Parser  =  netAddr <$> textual <*> (PC.char '/' *> ip6Mask)
+           <?> "IPv6 network address"
+{-# INLINE net6Parser #-}
+
+-- | IP network address parser (CIDR notation).
+netParser ∷ (IsNetAddr n, NetHost n ~ IP, CharParsing μ, Monad μ) ⇒ μ n
+netParser = (<?> "IP network address") $ do
+    a ← textual
+    void $ PC.char '/'
+    m ← mask a
+    return $ netAddr a m
+  where
+    mask (IPv4 _) = ip4Mask
+    mask (IPv6 _) = ip6Mask
+
+-- | Port number.
+newtype InetPort = InetPort { unInetPort ∷ Word16 }
+  deriving (Typeable, Eq, Ord, Bounded, Enum, Ix, Num, Real, Integral, Bits,
+            Hashable, Binary, Serialize, Printable)
+
+-- | 'InetPort' proxy value.
+anInetPort ∷ Proxy InetPort
+anInetPort = Proxy
+
+instance Show InetPort where
+  show (InetPort p) = show p
+  {-# INLINE show #-}
+
+instance Textual InetPort where
+  textual = InetPort <$> nncBounded Decimal <?> "port number"
+
+instance Storable InetPort where
+  alignment _ = alignment (undefined ∷ Word16)
+  sizeOf _    = 2
+  peek p      = InetPort . fromBigEndian <$> peek (castPtr p)
+  poke p      = poke (castPtr p) . toBigEndian . unInetPort
+
+-- | Socket address: host address + port number.
+data InetAddr a = InetAddr { inetHost ∷ a
+                           , inetPort ∷ {-# UNPACK #-} !InetPort
+                           } deriving (Eq, Ord, Show)
+
+deriving instance Typeable1 InetAddr
+
+type Inet4Addr = InetAddr IP4
+type Inet6Addr = InetAddr IP6
+
+-- | 'InetAddr' proxy value.
+anInetAddr ∷ Proxy (InetAddr IP)
+anInetAddr = Proxy
+
+-- | 'InetAddr' /a/ proxy value.
+anInetAddrOf ∷ Proxy a → Proxy (InetAddr a)
+anInetAddrOf _ = Proxy
+
+-- | 'Inet4Addr' proxy value.
+anInet4Addr ∷ Proxy Inet4Addr
+anInet4Addr = Proxy
+
+-- | 'Inet6Addr' proxy value.
+anInet6Addr ∷ Proxy Inet6Addr
+anInet6Addr = Proxy
+
+-- | 'InetAddr' 'IP' proxy value.
+anInetAddrIP ∷ Proxy (InetAddr IP)
+anInetAddrIP = Proxy
+
+instance Functor InetAddr where
+  fmap f (InetAddr a p) = InetAddr (f a) p
+  {-# INLINE fmap #-}
+
+instance Printable Inet4Addr where
+  print (InetAddr a p) = print a <> P.char7 ':' <> print p
+  {-# INLINABLE print #-}
+
+instance Printable Inet6Addr where
+  print (InetAddr a p) = P.brackets (print a) <> P.char7 ':' <> print p
+  {-# INLINABLE print #-}
+
+instance Printable (InetAddr IP) where
+  print (InetAddr (IPv4 a) p) = print (InetAddr a p)
+  print (InetAddr (IPv6 a) p) = print (InetAddr a p)
+  {-# INLINABLE print #-}
+
+instance Textual Inet4Addr where
+  textual  =  InetAddr <$> textual <*> (PC.char ':' *> textual)
+          <?> "IPv4 socket address"
+  {-# INLINE textual #-}
+
+instance Textual Inet6Addr where
+  textual  =  InetAddr <$> (PC.char '[' *> textual <* PC.char ']')
+                      <*> (PC.char ':' *> textual)
+          <?> "IPv6 socket address"
+  {-# INLINE textual #-}
+
+instance Textual (InetAddr IP) where
+  textual  =  InetAddr
+         <$> (optional (PC.char '[') >>= \case
+                Nothing → IPv4 <$> textual
+                Just _  → IPv6 <$> textual <* PC.char ']')
+         <*> (PC.char ':' *> textual)
+         <?> "IP socket address"
+  {-# INLINABLE textual #-}
+
+instance Hashable a ⇒ Hashable (InetAddr a) where
+#if MIN_VERSION_hashable(1,2,0)
+  hashWithSalt s (InetAddr a p) = s `hashWithSalt` a `hashWithSalt` p
+  {-# INLINE hashWithSalt #-}
+#else
+  hash (InetAddr a p) = hash a `combine` hash p
+  {-# INLINE hash #-}
+#endif
+
+instance Binary a ⇒ Binary (InetAddr a) where
+  get = InetAddr <$> B.get <*> B.get
+  put (InetAddr a p) = B.put a >> B.put p
+
+instance Serialize a ⇒ Serialize (InetAddr a) where
+  get = InetAddr <$> S.get <*> S.get
+  put (InetAddr a p) = S.put a >> S.put p
+
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,226 @@
+{-# LANGUAGE UnicodeSyntax #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+
+import Test.Framework (defaultMain, testGroup)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+import Test.QuickCheck
+
+import Data.DoubleWord
+import Data.Textual
+import Network.IP.Addr
+import Control.Applicative
+
+instance Arbitrary IP4 where
+  arbitrary = IP4 <$> arbitrary
+  shrink = fmap IP4 . shrink . unIP4
+
+instance Arbitrary IP6 where
+  arbitrary = fmap IP6 $ fromHiAndLo <$> arbitrary <*> arbitrary
+  shrink = fmap IP6 . shrinkIntegral . unIP6
+
+instance Arbitrary IP where
+  arbitrary = oneof [IPv4 <$> arbitrary, IPv6 <$> arbitrary]
+  shrink (IPv4 a) = IPv4 <$> shrink a
+  shrink (IPv6 a) = IPv6 <$> shrink a
+
+instance Arbitrary Net4Addr where
+  arbitrary = netAddr <$> arbitrary <*> suchThat arbitrary (<= 32)
+  shrink n = uncurry netAddr <$> shrink (netHost n, netLength n)
+
+instance Arbitrary Net6Addr where
+  arbitrary = netAddr <$> arbitrary <*> suchThat arbitrary (<= 128)
+  shrink n = uncurry netAddr <$> shrink (netHost n, netLength n)
+
+instance Arbitrary (NetAddr IP) where
+  arbitrary = do
+      a ← arbitrary
+      m ← mask a
+      return $ netAddr a m
+    where
+      mask (IPv4 _) = suchThat arbitrary (<= 32)
+      mask (IPv6 _) = suchThat arbitrary (<= 128)
+  shrink n = uncurry netAddr <$> shrink (netHost n, netLength n)
+
+instance Arbitrary InetPort where
+  arbitrary = InetPort <$> arbitrary
+  shrink = fmap InetPort . shrink . unInetPort
+
+instance Arbitrary a ⇒ Arbitrary (InetAddr a) where
+  arbitrary = InetAddr <$> arbitrary <*> arbitrary
+  shrink n = uncurry InetAddr <$> shrink (inetHost n, inetPort n)
+
+main = defaultMain
+         [ testGroup "IP4"
+             [ testProperty "print -> parse" $ \a →
+                 fromString (toString a) == Just (a ∷ IP4)
+             , testProperty "parsing \"1.2.3.4\"" $
+                 fromString "1.2.3.4" == Just (IP4 0x01020304)
+             , testProperty "parsing \"11.22.33.44\"" $
+                 fromString "11.22.33.44" == Just (IP4 0x0b16212c)
+             , testProperty "parsing \"1.256.3.4\" fails" $
+                 fromStringAs anIP4 "1.256.3.4" == Nothing
+             , testProperty "parsing \" 1.2.3.4\" fails" $
+                 fromStringAs anIP4 " 1.2.3.4" == Nothing
+             , testProperty "parsing \"1.2.3.4 \" fails" $
+                 fromStringAs anIP4 "1.2.3.4 " == Nothing
+             , testProperty "parsing \"1.2. 3.4\" fails" $
+                 fromStringAs anIP4 "1.2. 3.4 " == Nothing
+             , testProperty "parsing \"1.2.3..4\" fails" $
+                 fromStringAs anIP4 "1.2.3..4" == Nothing
+             , testProperty "parsing \"1.2.3.\" fails" $
+                 fromStringAs anIP4 "1.2.3." == Nothing
+             , testProperty "parsing \"1.2.3\" fails" $
+                 fromStringAs anIP4 "1.2.3" == Nothing
+             , testProperty "parsing \"1.2.3.4.5\" fails" $
+                 fromStringAs anIP4 "1.2.3.4.5" == Nothing
+             , testProperty "parsing \"1a.2.3.4\" fails" $
+                 fromStringAs anIP4 "1a.2.3.4" == Nothing
+             ]
+         , testGroup "IP6"
+             [ testProperty "print -> parse" $ \a →
+                 fromString (toString a) == Just (a ∷ IP6)
+             , testProperty "parsing \"::\"" $
+                 fromString "::" == Just anyIP6
+             , testProperty "parsing \":::\" fails" $
+                 fromStringAs anIP6 ":::" == Nothing
+             , testProperty "parsing \" 1::2\" fails" $
+                 fromStringAs anIP6 " 1::2" == Nothing
+             , testProperty "parsing \"1::2 \" fails" $
+                 fromStringAs anIP6 "1::2 " == Nothing
+             , testProperty "parsing \"1: :2\" fails" $
+                 fromStringAs anIP6 "1: :2" == Nothing
+             , testProperty "parsing \"1::2:\" fails" $
+                 fromStringAs anIP6 "1::2:" == Nothing
+             , testProperty "parsing \":1::2\" fails" $
+                 fromStringAs anIP6 ":1::2" == Nothing
+             , testProperty "parsing \"1:abcde::2\" fails" $
+                 fromStringAs anIP6 "1:abcde::2" == Nothing
+             , testProperty "parsing \"1:abcg::2\" fails" $
+                 fromStringAs anIP6 "1:abcg::2" == Nothing
+             , testProperty "parsing \"1:2:3:4a:5:6:7:8\"" $
+                 fromString "1:2:3:4a:5:6:7:8" ==
+                   Just (IP6 0x000100020003004a0005000600070008)
+             , testProperty "parsing \"1:2:3:4:5bc:6:7::\"" $
+                 fromString "1:2:3:4:5bc:6:7::" ==
+                   Just (IP6 0x000100020003000405bc000600070000)
+             , testProperty "parsing \"::1:2:3ad9:4:5:6:7\"" $
+                 fromString "::1:2:3ad9:4:5:6:7" ==
+                   Just (IP6 0x000100023ad90004000500060007)
+             , testProperty "parsing \"1:2:3:4:a5:6::\"" $
+                 fromString "1:2:3:4:a5:6::" ==
+                   Just (IP6 0x000100020003000400a5000600000000)
+             , testProperty "parsing \"::1:2:3:4:5:6\"" $
+                 fromString "::1:2:3:4:5:6" ==
+                   Just (IP6 0x000100020003000400050006)
+             , testProperty "parsing \"1::2:3:4:5:6\"" $
+                 fromString "1::2:3:4:5:6" ==
+                   Just (IP6 0x00010000000000020003000400050006)
+             , testProperty "parsing \"1:2:3::4:5:6\"" $
+                 fromString "1:2:3::4:5:6" ==
+                   Just (IP6 0x00010002000300000000000400050006)
+             , testProperty "parsing \"1:2:3::4:5::6\" fails" $
+                 fromStringAs anIP6 "1:2:3::4:5::6" == Nothing
+             , testProperty "parsing \"::1:2:3::4:5:6\" fails" $
+                 fromStringAs anIP6 "::1:2:3::4:5:6" == Nothing
+             , testProperty "parsing \"1:2::3:4:5g\" fails" $
+                 fromStringAs anIP6 "1:2::3:4:5g" == Nothing
+             , testProperty "parsing \"1:2:3:4:5:6:7:8g\" fails" $
+                 fromStringAs anIP6 "1:2:3:4:5:6:7:8g" == Nothing
+             ]
+         , testGroup "IP"
+             [ testProperty "print -> parse" $ \a →
+                 fromString (toString a) == Just (a ∷ IP)
+             ]
+         , testGroup "Net4Addr"
+             [ testProperty "print -> parse" $ \n →
+                 fromString (toString n) == Just (n ∷ Net4Addr)
+             , testProperty "parsing \"11.22.33.44/14\"" $
+                 case fromStringAs aNet4Addr "11.22.33.44/14" of
+                   Just n | netHost n   == IP4 0x0b16212c
+                          , netHostIx n == IP4 0x0002212c
+                          , netPrefix n == IP4 0x0b140000
+                          , netMask n   == IP4 0xfffc0000
+                          , netLength n == 14
+                          → True
+                   _ → False
+             , testProperty "parsing \"1.2.3/16\" fails" $
+                 fromStringAs aNet4Addr "1.2.3/16" == Nothing
+             , testProperty "parsing \"1.2 .3.4/16\" fails" $
+                 fromStringAs aNet4Addr "1.2 .3.4/16" == Nothing
+             , testProperty "parsing \"1.2.3.4/ 16\" fails" $
+                 fromStringAs aNet4Addr "1.2.3.4/ 16" == Nothing
+             , testProperty "parsing \"1.2.3.4 / 16\" fails" $
+                 fromStringAs aNet4Addr "1.2.3.4 / 16" == Nothing
+             , testProperty "parsing \"1.2.3.4/16 \" fails" $
+                 fromStringAs aNet4Addr "1.2.3.4/16 " == Nothing
+             , testProperty "parsing \"1.2.3.4/33\" fails" $
+                 fromStringAs aNet4Addr "1.2.3.4/33" == Nothing
+             , testProperty "parsing \"1.2.3.4/012\" fails" $
+                 fromStringAs aNet4Addr "1.2.3.4/012" == Nothing
+             ]
+         , testGroup "Net6Addr"
+             [ testProperty "print -> parse" $ \n →
+                 fromString (toString n) == Just (n ∷ Net6Addr)
+             , testProperty "parsing \"1234:5678:9abc:def::/22\"" $
+                 case fromStringAs aNet6Addr "1234:5678:9abc:def::/22" of
+                   Just n | netHost n   == IP6 0x123456789abc0def0000000000000000
+                          , netHostIx n == IP6 0x000002789abc0def0000000000000000
+                          , netPrefix n == IP6 0x12345400000000000000000000000000
+                          , netMask n   == IP6 0xfffffc00000000000000000000000000
+                          , netLength n == 22
+                          → True
+                   _ → False
+             , testProperty "parsing \"1:2:3/16\" fails" $
+                 fromStringAs aNet6Addr "1:2:3/16" == Nothing
+             , testProperty "parsing \"1:2 :3:4::/16\" fails" $
+                 fromStringAs aNet6Addr "1:2 :3:4::/16" == Nothing
+             , testProperty "parsing \"::1:2:3:4/ 16\" fails" $
+                 fromStringAs aNet6Addr "::1:2:3:4/ 16" == Nothing
+             , testProperty "parsing \"::1:2:3:4 / 16\" fails" $
+                 fromStringAs aNet6Addr "::1:2:3:4 / 16" == Nothing
+             , testProperty "parsing \"::1:2:3:4/16 \" fails" $
+                 fromStringAs aNet6Addr "::1:2:3:4/16 " == Nothing
+             , testProperty "parsing \"::/129\" fails" $
+                 fromStringAs aNet6Addr "::/129" == Nothing
+             , testProperty "parsing \"::/01\" fails" $
+                 fromStringAs aNet6Addr "::/01" == Nothing
+             ]
+         , testGroup "NetAddr"
+             [ testProperty "print -> parse" $ \n →
+                 fromString (toString n) == Just (n ∷ NetAddr IP)
+             ]
+         , testGroup "InetPort"
+             [ testProperty "print -> parse" $ \p →
+                 fromString (toString p) == Just (p ∷ InetPort)
+             , testProperty "parsing \"01\" fails" $
+                 fromStringAs anInetPort "01" == Nothing
+             , testProperty "parsing \"123456\" fails" $
+                 fromStringAs anInetPort "123456" == Nothing
+             , testProperty "parsing \"65536\" fails" $
+                 fromStringAs anInetPort "65536" == Nothing
+             ]
+         , testGroup "Inet4Addr"
+             [ testProperty "print -> parse" $ \a →
+                 fromString (toString a) == Just (a ∷ Inet4Addr)
+             , testProperty "parsing \"11.22.33.44:55\"" $
+                 case fromString "11.22.33.44:55" of
+                   Just (InetAddr (IP4 0x0b16212c) 55) → True
+                   _ → False
+             ]
+         , testGroup "Inet6Addr"
+             [ testProperty "print -> parse" $ \a →
+                 fromString (toString a) == Just (a ∷ Inet6Addr)
+             , testProperty "parsing \"[1234:5678:9abc:def::]:55\"" $
+                 case fromString "[1234:5678:9abc:def::]:55" of
+                   Just (InetAddr (IP6 0x123456789abc0def0000000000000000) 55) → True
+                   _ → False
+             ]
+         , testGroup "InetAddr"
+             [ testProperty "print -> parse" $ \a →
+                 fromString (toString a) == Just (a ∷ InetAddr IP)
+             ]
+         ]
+
