diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,17 @@
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [2.1.1] - 2026-11-04
+
+### Changed
+
+- The loop that parses messages from a peer uses `loopForever` too, like
+  every other long-running loop.
+- Tests use IPv4 only: the mock peer is at `127.0.0.1` instead of `[::1]`,
+  and the address property test generates IPv4 addresses. On a host
+  without IPv6 the mock peer could not be resolved and the peer and chain
+  tests hung.
+
 ## [2.1.0] - 2026-11-03
 
 - Update to haskoin-core-1.5.0.
diff --git a/haskoin-node.cabal b/haskoin-node.cabal
--- a/haskoin-node.cabal
+++ b/haskoin-node.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           haskoin-node
-version:        2.1.0
+version:        2.1.1
 synopsis:       P2P library for Bitcoin and Bitcoin Cash
 description:    Please see the README on GitHub at <https://github.com/jprupp/haskoin-node#readme>
 category:       Bitcoin, Finance, Network
diff --git a/src/Haskoin/Node/Peer.hs b/src/Haskoin/Node/Peer.hs
--- a/src/Haskoin/Node/Peer.hs
+++ b/src/Haskoin/Node/Peer.hs
@@ -47,6 +47,7 @@
 import Data.Word (Word32)
 import Haskoin
 import Haskoin.Node.Logger
+import Haskoin.Node.Loop (loopForever)
 import NQE
 import System.Random
 import UnliftIO
@@ -150,7 +151,7 @@
 -- | Internal conduit to parse messages coming from peer.
 inPeerConduit ::
   (MonadIO m) => PeerConfig -> ConduitT ByteString Message m ()
-inPeerConduit pc@PeerConfig {label, net, logger} = forever $ do
+inPeerConduit pc@PeerConfig {label, net, logger} = loopForever $ do
   logDebug logger (label <> " awaiting message...")
   x <- takeCE 24 .| foldC
   when (B.null x) $ do
diff --git a/test/Haskoin/NodeSpec.hs b/test/Haskoin/NodeSpec.hs
--- a/test/Haskoin/NodeSpec.hs
+++ b/test/Haskoin/NodeSpec.hs
@@ -98,12 +98,12 @@
 spec = do
   let net = bchRegTest
   describe "reads address/port combinations" $ do
-    prop "reads arbitrary addresses" $ \(e, w1, w2, w3, w4, b) -> do
+    -- IPv4 only: resolving an IPv6 address needs IPv6 on the host, which
+    -- containers and CI runners often lack. The IPv6 forms are still parsed
+    -- by the toHostService tests below, which need no resolver.
+    prop "reads arbitrary IPv4 addresses" $ \(e, w) -> do
       let p = toEnum (e `mod` 65536)
-          a =
-            if b
-              then SockAddrInet p w1
-              else SockAddrInet6 p 0 (w1, w2, w3, w4) 0
+          a = SockAddrInet p w
       s <- toSockAddr net (show a)
       s `shouldStartWith` [a]
     it "reads some specific addresses" $ do
@@ -206,7 +206,8 @@
           { maxPeers = 20,
             db = db,
             cf = listToMaybe (R.columnFamilies db),
-            peers = ["[::1]:17486"],
+            -- IPv4, so that the mock peer is reachable without IPv6
+            peers = ["127.0.0.1:17486"],
             discover = False,
             address = na,
             net = net,
