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/UNLICENSE b/UNLICENSE
new file mode 100644
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/pong-server.cabal b/pong-server.cabal
new file mode 100644
--- /dev/null
+++ b/pong-server.cabal
@@ -0,0 +1,47 @@
+name:                pong-server
+version:             0.0.1.0
+synopsis:            A simple embedded pingable server that runs in the background.
+description:         Please see README.md
+homepage:            http://github.com/RobertFischer/pong-server#readme
+license:             PublicDomain
+license-file:        UNLICENSE
+author:              Robert Fischer
+maintainer:          smokejumperit+pong-server@gmail.com
+copyright:           2016 Robert Fischer
+category:            utilities
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Axial.Pong
+  build-depends:       base >= 4.7 && < 5
+                     , network >= 2.6.2.1 && < 2.7
+  default-language:    Haskell2010
+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
+  default-extensions:  OverloadedStrings
+                    ,  LambdaCase
+                    ,  MultiWayIf
+                    ,  TransformListComp
+                    ,  FlexibleInstances
+                    ,  NoMonomorphismRestriction
+                    ,  DeriveGeneric
+                    ,  ScopedTypeVariables
+
+test-suite pong-server-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       base
+                     , pong-server
+                     , hspec >= 2.2.2 && < 3
+                     , QuickCheck >= 2.8.1 && < 3
+                     , network >= 2.6.2.1 && < 2.7
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+  default-extensions:  ScopedTypeVariables
+
+source-repository head
+  type:     git
+  location: https://github.com/RobertFischer/pong-server
diff --git a/src/Axial/Pong.hs b/src/Axial/Pong.hs
new file mode 100644
--- /dev/null
+++ b/src/Axial/Pong.hs
@@ -0,0 +1,45 @@
+module Axial.Pong
+    (
+      PongConfig(..)
+    , defaultPongConfig
+    , withPongServer
+    ) where
+
+import Network (withSocketsDo, listenOn, accept, sClose, PortID(..))
+import System.IO (hClose, hPutStr, hSetBuffering, BufferMode(..))
+import Control.Exception (bracket, finally)
+import Control.Concurrent (forkIO, forkFinally, ThreadId, killThread)
+
+data PongConfig = PongConfig {
+  pongPort :: Int,
+  pongMessage :: () -> IO String
+}
+
+newtype PongServer = PongServer ThreadId
+
+defaultPongConfig :: PongConfig
+defaultPongConfig = PongConfig 10411 $ const $ pure "pong"
+
+withPongServer :: PongConfig -> IO a -> IO a
+withPongServer cfg action = bracket (startServer cfg) stopServer $ const action
+
+startServer :: PongConfig -> IO PongServer
+startServer cfg = withSocketsDo $ do
+  socket <- listenOn portNum
+  threadId <- forkIO $ socketHandler socket
+  pure $ PongServer threadId
+  where
+    socketHandler sock = finally (socketHandlerLoop sock) (sClose sock)
+    socketHandlerLoop sock = do
+      (handle, _, _) <- accept sock
+      _ <- forkFinally (body handle) (const $ hClose handle)
+      socketHandlerLoop sock
+    body handle = do
+      msg <- (pongMessage cfg) ()
+      hSetBuffering handle NoBuffering
+      hPutStr handle msg
+    portNum = PortNumber $ fromIntegral pongPortNum
+    pongPortNum = pongPort cfg
+
+stopServer :: PongServer -> IO ()
+stopServer (PongServer threadId) = killThread threadId
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,84 @@
+import Axial.Pong
+
+import Data.Maybe (isJust, fromJust)
+
+import Test.Hspec
+import Test.QuickCheck
+import Control.Exception (catch, SomeException)
+
+import Network
+
+import System.Timeout
+
+import GHC.IO.Handle (Handle, hGetLine, hClose)
+
+
+main :: IO ()
+main = hspec $ do
+  describe "Axial Pong server" $ do
+
+    it "cannot be reached when not running" $ do
+      reached <- serverReachable ()
+      reached `shouldBe` False
+
+    it "can be started and stopped twice" $ do
+      reachedBefore <- serverReachable ()
+      reachedBefore `shouldBe` False
+
+      withPongServer defaultPongConfig $ do
+        reached <- serverReachable ()
+        reached `shouldBe` True
+
+      reachedBetween <- serverReachable ()
+      reachedBetween `shouldBe` False
+
+      withPongServer defaultPongConfig $ do
+        reached <- serverReachable ()
+        reached `shouldBe` True
+
+      reachedAfter <- serverReachable ()
+      reachedAfter `shouldBe` False
+
+    context "when running" $ around_ (withPongServer defaultPongConfig) $ do
+      it "can be reached" $ do
+        reached <- serverReachable ()
+        reached `shouldBe` True
+
+      it "can be reached twice" $ do
+        reached1 <- serverReachable ()
+        reached1 `shouldBe` True
+        reached2 <- serverReachable ()
+        reached2 `shouldBe` True
+
+      it "survives after a client pre-emptively closes the connection" $ do
+        handle <- connectServer ()
+        hClose handle
+        reached <- serverReachable ()
+        reached `shouldBe` True
+
+connectServer :: () -> IO Handle
+connectServer () = withSocketsDo $ do
+  connResult <- timeout (1000 * 1000) $ doConnect
+  connResult `shouldSatisfy` isJust
+  pure $ fromJust connResult
+  where
+    doConnect = connectTo serverName portNum
+    portNum = PortNumber 10411
+    serverName = "localhost"
+
+serverReachable :: () -> IO Bool
+serverReachable () = withSocketsDo $ catch tryConnect (\(e::SomeException) -> pure False)
+  where
+    tryConnect = do
+      connResult <- timeout (1000 * 1000) doConnect
+      pure $ isJust connResult
+    doConnect = do
+      socket <- connectTo serverName portNum
+      receivedMessage <- hGetLine socket
+      pure $ validateMessage receivedMessage
+    portNum = PortNumber 10411
+    serverName = "localhost"
+
+validateMessage :: String -> Bool
+validateMessage = (==) "pong"
+
