diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,3 +23,7 @@
 
 * Made it work with GHC again, and added some tests.
 * Worked on the Haddock documentation, before eventually putting it on Hackage.
+
+## 0.1.0.6 -- 2026-08-06
+
+* Verified that it works with a bunch more GHC cmopilers.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+# network-light
+
+[![Hackage](https://img.shields.io/hackage/v/network-light.svg)](https://hackage.haskell.org/package/network-light)
+[![Apache 2.0 License](<https://img.shields.io/badge/license-Apache%202.0-blue.svg>)](LICENSE)
+
+A small, portable subset of the `network` package's socket API.
+
+## What this is
+
+`network-light` implements a small subset of the socket functionality found in the `network` package: creating TCP/UDP sockets addressable via IPv4, connecting, binding, listening, accepting connections, and sending/receiving raw bytes, `String`, or `ByteString`.
+The API deliberately mirrors `network`'s in spirit, so moving between the two should feel familiar.
+
+It exists because [MicroHs](https://github.com/augustss/MicroHs) (`mhs`), a small Haskell compiler, cannot yet compile the full `network` package. `network-light` is implemented directly on top of the C `socket()`/`connect()`/`send()`/`recv()`/... calls via plain FFI imports (no C stubs) which keeps it simple enough to compile under both GHC and MicroHs from the same source. Until MicroHs can compile `network` in full, this package is the quickest way to get sockets working under both compilers.
+
+**This is not, and does not try to be, a replacement for `network`.** It only implements what has been needed so far — `Domain` and `SockOpt`, for example, each model a handful of constructors, not the full POSIX surface.
+
+## Scope
+
+- TCP (`SOCK_STREAM`) and UDP (`SOCK_DGRAM`) sockets over IPv4 (`AF_INET`)
+- `connect`, `bind`, `listen`, `accept`, `close`
+- Sending and receiving raw buffers, `String`, or `ByteString`, either "best effort"   or looped until the full amount is sent/received
+- A handful of socket options: `SO_REUSEADDR`, `SO_DEBUG`, `SO_TYPE`, and   non-blocking mode
+- Sockets are non-blocking by default and integrate correctly with both GHC's I/O manager and MicroHs's cooperative, green-thread concurrency
+- Compiles under GHC and MicroHs, on Linux; a `zephyr` cabal flag selects the differing `sockaddr_in` layout needed for Zephyr RTOS embedded targets
+
+If you need something this package doesn't have yet, such as IPv6, more socket options, Unix domain sockets, and so on, please fork it, add what you need, and open a pull request. Contributions are very welcome, as long as they keep to the existing style: plain FFI imports, no C stubs unless truly unavoidable, and code that compiles under both GHC and MicroHs.
+
+## Installation
+
+```
+cabal install network-light
+```
+
+or add it to your `.cabal` file:
+
+```
+build-depends: network-light
+```
+
+## Example
+
+```haskell
+module Main where
+
+import System.Network
+
+port :: Int
+port = 4242
+
+server :: IO ()
+server = do
+    serverFd <- socket AF_INET SOCK_STREAM
+    setsocketopt serverFd SO_REUSEADDR 1
+    bind serverFd (mkSockAddr port Nothing)
+    listen serverFd 1
+
+    (clientFd, clientAddr) <- accept serverFd
+    putStrLn ("received connection from: " <> show clientAddr)
+
+    msg <- recvString clientFd 100
+    putStrLn msg
+    _ <- sendString clientFd "Hello, client!"
+
+    close clientFd
+    close serverFd
+
+client :: IO ()
+client = do
+    fd <- socket AF_INET SOCK_STREAM
+    connect fd (mkSockAddr port (Just "127.0.0.1"))
+
+    _ <- sendString fd "Hello, server!"
+    reply <- recvString fd 100
+    putStrLn reply
+
+    close fd
+
+-- Run `server` in one terminal and `client` in another; they will talk to each other.
+main :: IO ()
+main = server
+```
+
+## Testing
+
+The test suite lives under `tests/` and is driven by `make` rather than
+`cabal test`, so that every test is built and run against both GHC and MicroHs from the same source:
+
+```
+cd tests
+make test          # run every test under both mhs and ghc
+make test HC=ghc   # GHC only
+make test HC=mhs   # MicroHs only
+```
+
+## License
+
+Apache License 2.0. See [LICENSE](LICENSE).
diff --git a/examples/Client.hs b/examples/Client.hs
new file mode 100644
--- /dev/null
+++ b/examples/Client.hs
@@ -0,0 +1,18 @@
+module Client where
+import System.Network
+import System.Environment
+import Secret
+
+main :: IO ()
+main = do
+  args <- getArgs
+  let msg | null args = "Hello"
+          | otherwise = unwords args
+  fd <- socket AF_INET SOCK_STREAM
+  connect fd (mkSockAddr 9900 (Just "127.0.0.1"))
+  sendString fd secret
+  putStrLn $ "Sending: " ++ msg
+  sendString fd msg
+  str <- recvString fd 1000
+  putStrLn $ "Got: " ++ show str
+  close fd
diff --git a/examples/Secret.hs b/examples/Secret.hs
new file mode 100644
--- /dev/null
+++ b/examples/Secret.hs
@@ -0,0 +1,3 @@
+module Secret where
+secret :: String
+secret = "Secret"
diff --git a/examples/Server.hs b/examples/Server.hs
new file mode 100644
--- /dev/null
+++ b/examples/Server.hs
@@ -0,0 +1,27 @@
+module Server where
+import System.Network
+import Secret
+
+main :: IO ()
+main = do
+  sfd <- socket AF_INET SOCK_STREAM
+  setsocketopt sfd SO_REUSEADDR 1
+  bind sfd (mkSockAddr 9900 Nothing)
+  listen sfd 2
+  serve sfd
+
+serve :: Socket -> IO ()
+serve sfd = do
+  putStrLn "accepting"
+  (fd, addr) <- accept sfd
+  putStrLn $ "got connection " ++ show addr
+  sec <- recvString fd (length secret)
+  if (sec == secret) then do
+    str <- recvString fd 1000
+    putStrLn $ "echoing"
+    sendString fd (reverse str)
+    return ()
+   else do
+    putStrLn "bad secret"
+  close fd
+  serve sfd
diff --git a/network-light.cabal b/network-light.cabal
--- a/network-light.cabal
+++ b/network-light.cabal
@@ -1,8 +1,8 @@
 cabal-version:      3.0
 name:               network-light
-version:            0.1.0.5
+version:            0.1.0.6
 synopsis:           A slimmed down version of network
-description:        A slimmed down version of network, that works with both GHC and MHS. Very incomplete -- pull-requests welcome.
+description:        A slimmed down version of network, that works with both GHC and MHS.
 license:            Apache-2.0
 license-file:       LICENSE
 author:             Robert Krook
@@ -11,12 +11,29 @@
 category:           Network
 build-type:         Simple
 extra-doc-files:    CHANGELOG.md
+                    README.md
+extra-source-files: examples/*.hs
+                    tests/Makefile
+                    tests/unittests/**/*.hs
+                    tests/unittests/bash-pipes/Makefile
+                    tests/unittests/concurrent-clients/Makefile
+                    tests/unittests/connect-closed-port/Makefile
+                    tests/unittests/hello-world/Makefile
+                    tests/unittests/send-string/Makefile
+                    tests/unittests/so-reuseaddr-churn/Makefile
 bug-reports:        https://github.com/Rewbert/network-light/issues
-tested-with:        GHC ==9.10.3
+tested-with:        GHC ==9.4.8
+                     || ==9.6.7
+                     || ==9.8.1
+                     || ==9.8.2
+                     || ==9.8.4
+                     || ==9.10.1
+                     || ==9.10.2
+                     || ==9.10.3
                      || ==9.12.2
-                     || ==9.14.1
+                     || ==9.12.4
+                     || ==9.14.1,
                     MHS ==0.16.4.0
--- extra-source-files:
 
 source-repository head
   type:     git
@@ -37,13 +54,14 @@
     -- other-extensions:
     if flag(zephyr)
         cpp-options: -DZEPHYR
-    build-depends:    base >=4.20 && <5, bytestring >=0.12.2.0 && <0.13
+    build-depends:    base >=4.17 && <5, bytestring >=0.12.2.0 && <0.13
     hs-source-dirs:   src
     default-language: Haskell2010
 
 Test-Suite test-sockaddr-storable
     type: exitcode-stdio-1.0
     Default-language: Haskell2010
-    hs-source-dirs: test/Network
+    hs-source-dirs: test/Network, src
     main-is: SockAddr.hs
-    build-depends: base, network-light, QuickCheck
+    other-modules: System.Network.Types
+    build-depends: base >=4.17 && <5, network-light, QuickCheck
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/Makefile
@@ -0,0 +1,42 @@
+# Runs every test under unittests/ with both compilers, so a change can
+# be checked against cabal/ghc and mcabal/mhs in one shot.
+#
+# Usage:
+#   make test              # run every test under both mhs and ghc
+#   make test HC=mhs       # run every test under mhs only
+#   make test HC=ghc       # run every test under ghc only
+#   make clean             # clean every test directory
+
+.PHONY: all test clean
+
+UNITTESTS := $(sort $(patsubst %/,%,$(dir $(wildcard unittests/*/Makefile))))
+
+ifeq ($(HC),)
+HCS := mhs ghc
+else
+HCS := $(HC)
+endif
+
+all: test
+
+test:
+	@fail=0; \
+	total=0; \
+	for d in $(UNITTESTS); do \
+		for hc in $(HCS); do \
+			total=$$((total+1)); \
+			$(MAKE) -s -C $$d clean >/dev/null 2>&1; \
+			if ! $(MAKE) -s -C $$d test HC=$$hc; then fail=$$((fail+1)); fi; \
+			$(MAKE) -s -C $$d clean >/dev/null 2>&1; \
+		done; \
+	done; \
+	echo "-----"; \
+	if [ $$fail -eq 0 ]; then \
+		echo "all $$total test runs passed"; \
+	else \
+		echo "$$fail of $$total test runs FAILED"; \
+	fi; \
+	exit $$fail
+
+clean:
+	@for d in $(UNITTESTS); do $(MAKE) -s -C $$d clean; done
diff --git a/tests/unittests/bash-pipes/Client.hs b/tests/unittests/bash-pipes/Client.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/bash-pipes/Client.hs
@@ -0,0 +1,21 @@
+module Client where
+
+import System.IO
+import System.Network
+
+port :: Int
+port = 9935
+
+main :: IO ()
+main = do
+    input <- getContents
+
+    fd <- socket AF_INET SOCK_STREAM
+    connect fd (mkSockAddr port (Just "127.0.0.1"))
+
+    sendString fd input
+    reply <- recvString fd 65536
+    close fd
+
+    putStr reply
+    hFlush stdout
diff --git a/tests/unittests/bash-pipes/Makefile b/tests/unittests/bash-pipes/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/unittests/bash-pipes/Makefile
@@ -0,0 +1,55 @@
+# Checks that a network-light client can sit inside a normal bash
+# pipeline: stdin is piped in from a real shell command, the client
+# forwards that over a socket to a network-light server, the server's
+# reply comes back out the client's stdout, and a further bash stage
+# consumes it. Exercises the library's sockets together with mhs/GHC's
+# ordinary stdin/stdout handling in the same process, not just the
+# toolchain in isolation.
+#
+# Usage:
+#   make test              # build and run with mhs (default)
+#   make test HC=ghc       # build and run with GHC, against the library source
+#   make clean
+
+.PHONY: all test clean
+
+HC     ?= mhs
+TOPSRC := ../../../src
+
+all: test
+
+ifeq ($(HC),mhs)
+server: Server.hs
+	mhs -i$(TOPSRC) Server -o server
+client: Client.hs
+	mhs -i$(TOPSRC) Client -o client
+else ifeq ($(HC),ghc)
+server: Server.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server
+client: Client.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client
+else
+server client:
+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')
+endif
+
+test: server client
+	@rm -f server.out
+	@( timeout 10 ./server > server.out 2>&1 & )
+	@i=0; \
+	while [ $$i -lt 50 ] && ! grep -q READY server.out 2>/dev/null; do sleep 0.1; i=$$((i+1)); done; \
+	if ! grep -q READY server.out 2>/dev/null; then \
+		echo "FAIL: bash-pipes ($(HC)): server never became ready"; \
+		cat server.out; exit 1; \
+	fi
+	@got=$$(printf "banana\napple\ncherry\n" | ./client | grep A | wc -l); \
+	if [ "$$got" = "2" ]; then \
+		echo "PASS: bash-pipes ($(HC))"; \
+	else \
+		echo "FAIL: bash-pipes ($(HC)): expected 2, got '$$got'"; \
+		cat server.out; \
+		exit 1; \
+	fi
+
+clean:
+	rm -rf server client dist-ghc *.hi *.o server.out
diff --git a/tests/unittests/bash-pipes/Server.hs b/tests/unittests/bash-pipes/Server.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/bash-pipes/Server.hs
@@ -0,0 +1,25 @@
+module Server where
+
+import Data.Char
+import System.IO
+import System.Network
+
+port :: Int
+port = 9935
+
+main :: IO ()
+main = do
+    sfd <- socket AF_INET SOCK_STREAM
+    setsocketopt sfd SO_REUSEADDR 1
+    bind sfd (mkSockAddr port Nothing)
+    listen sfd 1
+
+    putStrLn "READY"
+    hFlush stdout
+
+    (cfd, _) <- accept sfd
+    msg <- recvString cfd 65536
+    hPutStrLn stderr ("SERVER_GOT:" ++ show (length msg) ++ " bytes")
+    sendString cfd (map toUpper msg)
+    close cfd
+    close sfd
diff --git a/tests/unittests/concurrent-clients/Client.hs b/tests/unittests/concurrent-clients/Client.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/concurrent-clients/Client.hs
@@ -0,0 +1,23 @@
+module Client where
+
+import System.Environment
+import System.IO
+import System.Network
+
+port :: Int
+port = 9932
+
+main :: IO ()
+main = do
+    [idx] <- getArgs
+    let msg = "client-" ++ idx
+
+    fd <- socket AF_INET SOCK_STREAM
+    connect fd (mkSockAddr port (Just "127.0.0.1"))
+
+    sendString fd msg
+    reply <- recvString fd 1024
+    putStrLn ("CLIENT_" ++ idx ++ "_GOT:" ++ reply)
+    hFlush stdout
+
+    close fd
diff --git a/tests/unittests/concurrent-clients/Makefile b/tests/unittests/concurrent-clients/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/unittests/concurrent-clients/Makefile
@@ -0,0 +1,64 @@
+# Starts one server and N clients launched at (roughly) the same time,
+# and checks every client got the reply meant for it. Concurrency here
+# is at the OS-process level (N separate client binaries backgrounded
+# from the shell) rather than forkIO inside one process -- see
+# so-reuseaddr-churn/Makefile for why that matters under GHC.
+#
+# Usage:
+#   make test              # build and run with mhs (default)
+#   make test HC=ghc       # build and run with GHC, against the library source
+#   make clean
+
+.PHONY: all test clean
+
+HC       ?= mhs
+TOPSRC   := ../../../src
+NCLIENTS := 8
+
+all: test
+
+ifeq ($(HC),mhs)
+server: Server.hs
+	mhs -i$(TOPSRC) Server -o server
+client: Client.hs
+	mhs -i$(TOPSRC) Client -o client
+else ifeq ($(HC),ghc)
+server: Server.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server
+client: Client.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client
+else
+server client:
+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')
+endif
+
+test: server client
+	@rm -f server.out client-*.out
+	@( timeout 20 ./server $(NCLIENTS) > server.out 2>&1 & )
+	@i=0; \
+	while [ $$i -lt 50 ] && ! grep -q READY server.out 2>/dev/null; do sleep 0.1; i=$$((i+1)); done; \
+	if ! grep -q READY server.out 2>/dev/null; then \
+		echo "FAIL: concurrent-clients ($(HC)): server never became ready"; \
+		cat server.out; exit 1; \
+	fi
+	@for i in $$(seq 1 $(NCLIENTS)); do \
+		./client $$i > client-$$i.out 2>&1 & \
+	done; \
+	wait
+	@sleep 0.3
+	@ok=1; \
+	for i in $$(seq 1 $(NCLIENTS)); do \
+		grep -q "SERVER_GOT:client-$$i$$" server.out || { echo "FAIL: server never saw client-$$i"; ok=0; }; \
+		grep -q "CLIENT_$${i}_GOT:ack:client-$$i$$" client-$$i.out || { echo "FAIL: client $$i got the wrong (or no) reply"; ok=0; }; \
+	done; \
+	grep -q SERVER_DONE server.out || { echo "FAIL: server did not finish"; ok=0; }; \
+	if [ $$ok -eq 1 ]; then \
+		echo "PASS: concurrent-clients ($(HC))"; \
+	else \
+		echo "--- server.out ---"; cat server.out; \
+		for i in $$(seq 1 $(NCLIENTS)); do echo "--- client-$$i.out ---"; cat client-$$i.out; done; \
+		exit 1; \
+	fi
+
+clean:
+	rm -rf server client dist-ghc *.hi *.o server.out client-*.out
diff --git a/tests/unittests/concurrent-clients/Server.hs b/tests/unittests/concurrent-clients/Server.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/concurrent-clients/Server.hs
@@ -0,0 +1,40 @@
+module Server where
+
+import System.Environment
+import System.IO
+import System.Network
+
+port :: Int
+port = 9932
+
+serve :: Socket -> IO ()
+serve sfd = do
+    (cfd, _) <- accept sfd
+    msg <- recvString cfd 1024
+    putStrLn ("SERVER_GOT:" ++ msg)
+    hFlush stdout
+    sendString cfd ("ack:" ++ msg)
+    close cfd
+
+main :: IO ()
+main = do
+    [nStr] <- getArgs
+    let n = read nStr :: Int
+
+    sfd <- socket AF_INET SOCK_STREAM
+    setsocketopt sfd SO_REUSEADDR 1
+    bind sfd (mkSockAddr port Nothing)
+    listen sfd n
+
+    putStrLn "READY"
+    hFlush stdout
+
+    -- Clients connect concurrently (the OS queues them in the listen
+    -- backlog); the server itself just drains that backlog one at a
+    -- time, which is enough to prove several clients can be in flight
+    -- against the same listening socket at once.
+    mapM_ (const (serve sfd)) [1 .. n]
+
+    close sfd
+    putStrLn "SERVER_DONE"
+    hFlush stdout
diff --git a/tests/unittests/connect-closed-port/ConnectSafe.hs b/tests/unittests/connect-closed-port/ConnectSafe.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/connect-closed-port/ConnectSafe.hs
@@ -0,0 +1,13 @@
+module ConnectSafe where
+
+import System.Network
+
+-- Nothing listens here; connect' should return False instead of throwing.
+port :: Int
+port = 9934
+
+main :: IO ()
+main = do
+    fd <- socket AF_INET SOCK_STREAM
+    ok <- connect' fd (mkSockAddr port (Just "127.0.0.1"))
+    putStrLn ("CONNECT_RESULT:" ++ show ok)
diff --git a/tests/unittests/connect-closed-port/ConnectThrows.hs b/tests/unittests/connect-closed-port/ConnectThrows.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/connect-closed-port/ConnectThrows.hs
@@ -0,0 +1,13 @@
+module ConnectThrows where
+
+import System.Network
+
+-- Nothing listens here; connecting should raise an IO exception.
+port :: Int
+port = 9934
+
+main :: IO ()
+main = do
+    fd <- socket AF_INET SOCK_STREAM
+    connect fd (mkSockAddr port (Just "127.0.0.1"))
+    putStrLn "UNEXPECTED_SUCCESS"
diff --git a/tests/unittests/connect-closed-port/Makefile b/tests/unittests/connect-closed-port/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/unittests/connect-closed-port/Makefile
@@ -0,0 +1,45 @@
+# Checks the two failure-reporting conventions the library offers for a
+# connect that can't succeed: `connect` throws, `connect'` returns False.
+#
+# Usage:
+#   make test              # build and run with mhs (default)
+#   make test HC=ghc       # build and run with GHC, against the library source
+#   make clean
+
+.PHONY: all test clean
+
+HC     ?= mhs
+TOPSRC := ../../../src
+
+all: test
+
+ifeq ($(HC),mhs)
+throws: ConnectThrows.hs
+	mhs -i$(TOPSRC) ConnectThrows -o throws
+safe: ConnectSafe.hs
+	mhs -i$(TOPSRC) ConnectSafe -o safe
+else ifeq ($(HC),ghc)
+throws: ConnectThrows.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is ConnectThrows ConnectThrows.hs -o throws
+safe: ConnectSafe.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is ConnectSafe ConnectSafe.hs -o safe
+else
+throws safe:
+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')
+endif
+
+test: throws safe
+	@./throws > throws.out 2>&1; rc=$$?; \
+	if [ $$rc -eq 0 ]; then \
+		echo "FAIL: connect-closed-port ($(HC)): connect to a closed port did not fail"; \
+		cat throws.out; exit 1; \
+	fi
+	@./safe > safe.out 2>&1; rc=$$?; \
+	if [ $$rc -ne 0 ] || ! grep -q "CONNECT_RESULT:False" safe.out; then \
+		echo "FAIL: connect-closed-port ($(HC)): connect' did not cleanly return False"; \
+		cat safe.out; exit 1; \
+	fi
+	@echo "PASS: connect-closed-port ($(HC))"
+
+clean:
+	rm -rf throws safe dist-ghc *.hi *.o *.out
diff --git a/tests/unittests/hello-world/Client.hs b/tests/unittests/hello-world/Client.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/hello-world/Client.hs
@@ -0,0 +1,15 @@
+module Client where
+
+import System.Network
+
+port :: Int
+port = 9936
+
+main :: IO ()
+main = do
+    fd <- socket AF_INET SOCK_STREAM
+    connect fd (mkSockAddr port (Just "127.0.0.1"))
+
+    sendString fd "hello world"
+
+    close fd
diff --git a/tests/unittests/hello-world/Makefile b/tests/unittests/hello-world/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/unittests/hello-world/Makefile
@@ -0,0 +1,48 @@
+# Minimal integration test: a server accepts one connection and prints
+# whatever the client sent. No handshake protocol between the two --
+# just a short sleep to give the server time to start listening before
+# the client connects.
+#
+# Usage:
+#   make test              # build and run with mhs (default)
+#   make test HC=ghc       # build and run with GHC, against the library source
+#   make clean
+
+.PHONY: all test clean
+
+HC     ?= mhs
+TOPSRC := ../../../src
+
+all: test
+
+ifeq ($(HC),mhs)
+server: Server.hs
+	mhs -i$(TOPSRC) Server -o server
+client: Client.hs
+	mhs -i$(TOPSRC) Client -o client
+else ifeq ($(HC),ghc)
+server: Server.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server
+client: Client.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client
+else
+server client:
+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')
+endif
+
+test: server client
+	@rm -f server.out
+	@( timeout 5 ./server > server.out 2>&1 & )
+	@sleep 0.2
+	@./client
+	@sleep 0.2
+	@if grep -q "hello world" server.out; then \
+		echo "PASS: hello-world ($(HC))"; \
+	else \
+		echo "FAIL: hello-world ($(HC))"; \
+		cat server.out; \
+		exit 1; \
+	fi
+
+clean:
+	rm -rf server client dist-ghc *.hi *.o server.out
diff --git a/tests/unittests/hello-world/Server.hs b/tests/unittests/hello-world/Server.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/hello-world/Server.hs
@@ -0,0 +1,20 @@
+module Server where
+
+import System.Network
+
+port :: Int
+port = 9936
+
+main :: IO ()
+main = do
+    sfd <- socket AF_INET SOCK_STREAM
+    setsocketopt sfd SO_REUSEADDR 1
+    bind sfd (mkSockAddr port Nothing)
+    listen sfd 1
+
+    (cfd, _) <- accept sfd
+    msg <- recvString cfd 1024
+    putStrLn msg
+
+    close cfd
+    close sfd
diff --git a/tests/unittests/send-string/Client.hs b/tests/unittests/send-string/Client.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/send-string/Client.hs
@@ -0,0 +1,22 @@
+module Client where
+
+import System.IO
+import System.Network
+
+port :: Int
+port = 9931
+
+message :: String
+message = "hello-network-light"
+
+main :: IO ()
+main = do
+    fd <- socket AF_INET SOCK_STREAM
+    connect fd (mkSockAddr port (Just "127.0.0.1"))
+
+    sendString fd message
+    reply <- recvString fd 1024
+    putStrLn ("CLIENT_GOT:" ++ reply)
+    hFlush stdout
+
+    close fd
diff --git a/tests/unittests/send-string/Makefile b/tests/unittests/send-string/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/unittests/send-string/Makefile
@@ -0,0 +1,62 @@
+# Integration test: a server accepts one connection, echoes back an
+# acknowledgement of whatever string the client sent, and the harness
+# checks that both sides saw the expected bytes.
+#
+# Usage:
+#   make test              # build and run with mhs (default)
+#   make test HC=ghc       # build and run with GHC, against the library source
+#   make clean
+
+.PHONY: all test clean
+
+HC     ?= mhs
+TOPSRC := ../../../src
+
+EXPECT_SERVER := SERVER_GOT:hello-network-light
+EXPECT_CLIENT := CLIENT_GOT:ack:hello-network-light
+
+all: test
+
+ifeq ($(HC),mhs)
+# -i points straight at the in-tree library source, so the test always
+# exercises the current checkout instead of whatever network-light build
+# last happened to be installed into the shared mcabal package cache.
+server: Server.hs
+	mhs -i$(TOPSRC) Server -o server
+client: Client.hs
+	mhs -i$(TOPSRC) Client -o client
+else ifeq ($(HC),ghc)
+server: Server.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server
+client: Client.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Client Client.hs -o client
+else
+server client:
+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')
+endif
+
+test: server client
+	@rm -f server.out client.out
+	@( timeout 5 ./server > server.out 2>&1 & )
+	@i=0; \
+	while [ $$i -lt 50 ] && ! grep -q READY server.out 2>/dev/null; do \
+		sleep 0.1; i=$$((i+1)); \
+	done; \
+	if ! grep -q READY server.out 2>/dev/null; then \
+		echo "FAIL: send-string ($(HC)): server never became ready"; \
+		cat server.out; \
+		exit 1; \
+	fi
+	@./client > client.out 2>&1
+	@sleep 0.2
+	@if grep -q "$(EXPECT_SERVER)" server.out && grep -q "$(EXPECT_CLIENT)" client.out; then \
+		echo "PASS: send-string ($(HC))"; \
+	else \
+		echo "FAIL: send-string ($(HC))"; \
+		echo "--- server.out ---"; cat server.out; \
+		echo "--- client.out ---"; cat client.out; \
+		exit 1; \
+	fi
+
+clean:
+	rm -rf server client dist-ghc *.hi *.o server.out client.out
diff --git a/tests/unittests/send-string/Server.hs b/tests/unittests/send-string/Server.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/send-string/Server.hs
@@ -0,0 +1,27 @@
+module Server where
+
+import System.IO
+import System.Network
+
+port :: Int
+port = 9931
+
+main :: IO ()
+main = do
+    sfd <- socket AF_INET SOCK_STREAM
+    setsocketopt sfd SO_REUSEADDR 1
+    bind sfd (mkSockAddr port Nothing)
+    listen sfd 1
+
+    -- Tell the test harness we're ready to accept a connection.
+    putStrLn "READY"
+    hFlush stdout
+
+    (cfd, _) <- accept sfd
+    msg <- recvString cfd 1024
+    putStrLn ("SERVER_GOT:" ++ msg)
+    hFlush stdout
+
+    sendString cfd ("ack:" ++ msg)
+    close cfd
+    close sfd
diff --git a/tests/unittests/so-reuseaddr-churn/Makefile b/tests/unittests/so-reuseaddr-churn/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/unittests/so-reuseaddr-churn/Makefile
@@ -0,0 +1,64 @@
+# Rebinds the same port many times in a row, each time via a fresh
+# socket, right after actively closing an accepted connection on it.
+# Without SO_REUSEADDR this fails with EADDRINUSE once TIME_WAIT sockets
+# pile up; with it, it should sail through every iteration.
+#
+# Deliberately drives the connections from plain bash (/dev/tcp) rather
+# than another Haskell process, so this test doesn't depend on
+# concurrency working inside the compiled binary (see the note in
+# concurrent-clients/ and send-string/'s README about forkIO deadlocking
+# blocking FFI calls under GHC).
+#
+# Usage:
+#   make test              # build and run with mhs (default)
+#   make test HC=ghc       # build and run with GHC, against the library source
+#   make clean
+
+.PHONY: all test clean
+
+SHELL  := /bin/bash
+HC     ?= mhs
+TOPSRC := ../../../src
+PORT   := 9933
+ITERS  := 20
+
+all: test
+
+ifeq ($(HC),mhs)
+server: Server.hs
+	mhs -i$(TOPSRC) Server -o server
+else ifeq ($(HC),ghc)
+server: Server.hs
+	ghc -outputdir dist-ghc -i$(TOPSRC) -main-is Server Server.hs -o server
+else
+server:
+	$(error Unknown HC '$(HC)', expected 'mhs' or 'ghc')
+endif
+
+test: server
+	@rm -f server.out
+	@( timeout 20 ./server $(ITERS) > server.out 2>&1 & )
+	@for i in $$(seq 1 $(ITERS)); do \
+		j=0; \
+		while [ $$j -lt 50 ] && ! grep -q "READY_$$i$$" server.out 2>/dev/null; do \
+			sleep 0.05; j=$$((j+1)); \
+		done; \
+		if ! grep -q "READY_$$i$$" server.out 2>/dev/null; then \
+			echo "FAIL: so-reuseaddr-churn ($(HC)): server never reached iteration $$i"; \
+			cat server.out; exit 1; \
+		fi; \
+		bash -c 'exec 3<>/dev/tcp/127.0.0.1/$(PORT); sleep 0.2' 2>/dev/null || { \
+			echo "FAIL: so-reuseaddr-churn ($(HC)): could not connect on iteration $$i"; \
+			cat server.out; exit 1; \
+		}; \
+	done
+	@sleep 0.3
+	@if grep -q SERVER_DONE server.out; then \
+		echo "PASS: so-reuseaddr-churn ($(HC))"; \
+	else \
+		echo "FAIL: so-reuseaddr-churn ($(HC)): server did not complete all iterations"; \
+		cat server.out; exit 1; \
+	fi
+
+clean:
+	rm -rf server dist-ghc *.hi *.o server.out
diff --git a/tests/unittests/so-reuseaddr-churn/Server.hs b/tests/unittests/so-reuseaddr-churn/Server.hs
new file mode 100644
--- /dev/null
+++ b/tests/unittests/so-reuseaddr-churn/Server.hs
@@ -0,0 +1,34 @@
+module Server where
+
+import System.Environment
+import System.IO
+import System.Network
+
+port :: Int
+port = 9933
+
+-- One churn cycle: bind a fresh socket to the same port, accept exactly
+-- one connection, and actively close it from our end (the side that
+-- closes first is the side that lands in TIME_WAIT). Without
+-- SO_REUSEADDR the next iteration's bind fails with EADDRINUSE while
+-- that TIME_WAIT entry is still around.
+churn :: Int -> IO ()
+churn i = do
+    sfd <- socket AF_INET SOCK_STREAM
+    setsocketopt sfd SO_REUSEADDR 1
+    bind sfd (mkSockAddr port Nothing)
+    listen sfd 1
+
+    putStrLn ("READY_" ++ show i)
+    hFlush stdout
+
+    (afd, _) <- accept sfd
+    close afd
+    close sfd
+
+main :: IO ()
+main = do
+    [nStr] <- getArgs
+    mapM_ churn [1 .. (read nStr :: Int)]
+    putStrLn "SERVER_DONE"
+    hFlush stdout
