commit 499ce2d09a3377914cb20799504f006cf0960871
parent 79540cf45574e0c3e2b4d1e05c8a5b94fa65ebee
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:11:21 +0400
lib: follow the BOLT #7 address descriptor rules
decodeNodeAnnouncement rejected the whole message on any unknown
address type, a deprecated Tor v2 descriptor, or a DNS descriptor
with an empty hostname. The spec says receivers ignore these.
The address parser now:
- stops at the first unknown type (0 included) and ignores the rest
of the address bytes, since its length is unknown;
- skips Tor v2 (type 3) descriptors by their fixed length of 12;
- skips descriptors with port 0 ("SHOULD ignore that address
descriptor");
- skips DNS descriptors whose hostname is empty or not ASCII.
A known descriptor that addrlen cannot hold is still an error.
The hostname smart constructor now rejects non-ASCII bytes ("hostname
bytes MUST be ASCII characters"). validateNodeAnnouncement fails with
the new ValidateMultipleDns when more than one DNS hostname is
present: the origin MUST NOT announce more than one, and a receiver
MUST NOT forward such a message.
Diffstat:
3 files changed, 55 insertions(+), 42 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT7/Codec.hs b/lib/Lightning/Protocol/BOLT7/Codec.hs
@@ -201,6 +201,18 @@ decodeFeatureBits bs = do
{-# INLINE decodeFeatureBits #-}
-- | Decode addresses list (length-prefixed).
+--
+-- Follows the BOLT #7 receiver rules for address descriptors:
+--
+-- * parsing stops at the first descriptor of an unknown type, and
+-- the remaining address bytes are ignored;
+-- * deprecated Tor v2 descriptors (type 3) are skipped;
+-- * descriptors with port 0 are skipped;
+-- * DNS descriptors whose hostname is empty or not ASCII are
+-- skipped.
+--
+-- A descriptor of a known type that @addrlen@ cannot hold is a
+-- decoding error.
decodeAddresses :: ByteString -> Either DecodeError ([Address], ByteString)
decodeAddresses bs = do
(addrData, rest) <- decodeLenPrefixed bs
@@ -211,41 +223,33 @@ decodeAddresses bs = do
parseAddrs !d
| BS.null d = Right []
| otherwise = do
- (addr, d') <- parseOneAddr d
- addrs <- parseAddrs d'
- Right (addr : addrs)
-
- parseOneAddr :: ByteString -> Either DecodeError (Address, ByteString)
- parseOneAddr d = do
- (typ, d1) <- decodeU8 d
- case typ of
- 1 -> do -- IPv4
- (addrBytes, d2) <- decodeBytes ipv4AddrLen d1
- (port, d3) <- decodeU16 d2
- case ipv4Addr addrBytes of
- Nothing -> Left DecodeInvalidAddress
- Just a -> Right (AddrIPv4 a port, d3)
- 2 -> do -- IPv6
- (addrBytes, d2) <- decodeBytes ipv6AddrLen d1
- (port, d3) <- decodeU16 d2
- case ipv6Addr addrBytes of
- Nothing -> Left DecodeInvalidAddress
- Just a -> Right (AddrIPv6 a port, d3)
- 4 -> do -- Tor v3
- (addrBytes, d2) <- decodeBytes torV3AddrLen d1
- (port, d3) <- decodeU16 d2
- case torV3Addr addrBytes of
- Nothing -> Left DecodeInvalidAddress
- Just a -> Right (AddrTorV3 a port, d3)
- 5 -> do -- DNS hostname
- (hostLen, d2) <- decodeU8 d1
- (hostBytes, d3) <-
- decodeBytes (fromIntegral hostLen) d2
- (port, d4) <- decodeU16 d3
- case hostname hostBytes of
- Nothing -> Left DecodeInvalidAddress
- Just h -> Right (AddrDNS h port, d4)
- _ -> Left DecodeInvalidAddress -- Unknown address type
+ (typ, d1) <- decodeU8 d
+ case typ of
+ 1 -> fixedAddr ipv4AddrLen (fmap AddrIPv4 . ipv4Addr) d1
+ 2 -> fixedAddr ipv6AddrLen (fmap AddrIPv6 . ipv6Addr) d1
+ 3 -> do -- Tor v2 (deprecated; 10-byte onion + port)
+ (_, d2) <- decodeBytes 12 d1
+ parseAddrs d2
+ 4 -> fixedAddr torV3AddrLen (fmap AddrTorV3 . torV3Addr) d1
+ 5 -> do -- DNS hostname
+ (hostLen, d2) <- decodeU8 d1
+ (hostBytes, d3) <- decodeBytes (fromIntegral hostLen) d2
+ (port, d4) <- decodeU16 d3
+ keep (fmap AddrDNS (hostname hostBytes)) port d4
+ _ -> Right [] -- unknown type: ignore the rest
+
+ fixedAddr :: Int -> (ByteString -> Maybe (Word16 -> Address))
+ -> ByteString -> Either DecodeError [Address]
+ fixedAddr len mk d = do
+ (addrBytes, d1) <- decodeBytes len d
+ (port, d2) <- decodeU16 d1
+ keep (mk addrBytes) port d2
+
+ keep :: Maybe (Word16 -> Address) -> Word16 -> ByteString
+ -> Either DecodeError [Address]
+ keep mk port d = case mk of
+ Just f | port /= 0 -> fmap (f port :) (parseAddrs d)
+ _ -> parseAddrs d
-- Channel announcement --------------------------------------------------------
diff --git a/lib/Lightning/Protocol/BOLT7/Types.hs b/lib/Lightning/Protocol/BOLT7/Types.hs
@@ -324,11 +324,12 @@ torV3Addr !bs
| otherwise = Nothing
{-# INLINE torV3Addr #-}
--- | DNS hostname (1-255 bytes).
+-- | DNS hostname (1-255 ASCII bytes).
--
-- Per BOLT #7 address descriptor type 5, the hostname is
--- a length-prefixed DNS name. The length byte limits it to
--- 255 bytes.
+-- a length-prefixed DNS name whose bytes MUST be ASCII
+-- (non-ASCII names are Punycode-encoded). The length byte
+-- limits it to 255 bytes.
newtype Hostname = Hostname { getHostname :: ByteString }
deriving (Eq, Show, Generic)
@@ -336,12 +337,13 @@ instance NFData Hostname
-- | Smart constructor for Hostname.
--
--- Returns Nothing if the hostname is empty or exceeds
--- 255 bytes.
+-- Returns Nothing if the hostname is empty, exceeds 255
+-- bytes, or contains a non-ASCII byte.
hostname :: ByteString -> Maybe Hostname
hostname !bs
| BS.null bs = Nothing
| BS.length bs > 255 = Nothing
+ | BS.any (> 0x7f) bs = Nothing
| otherwise = Just (Hostname bs)
{-# INLINE hostname #-}
diff --git a/lib/Lightning/Protocol/BOLT7/Validate.hs b/lib/Lightning/Protocol/BOLT7/Validate.hs
@@ -41,6 +41,7 @@ data ValidationError
| ValidateHtlcAmounts -- ^ htlc_minimum_msat > htlc_maximum_msat
| ValidateBlockOverflow -- ^ first_blocknum + number_of_blocks overflow
| ValidateScidNotAscending -- ^ short_channel_ids not in ascending order
+ | ValidateMultipleDns -- ^ more than one DNS hostname address
deriving (Eq, Show, Generic)
instance NFData ValidationError
@@ -68,12 +69,18 @@ validateChannelAnnouncement msg = do
-- Checks:
--
-- * Feature bits do not contain unknown even bits
+-- * At most one DNS hostname address. Per BOLT #7 the origin MUST
+-- NOT announce more than one; a receiver SHOULD ignore all but the
+-- first, and MUST NOT forward the message.
--
--- Note: Address list validation (duplicate DNS entries) and alias
--- UTF-8 validation are not enforced; the spec allows non-UTF-8 aliases.
+-- Note: alias UTF-8 validation is not enforced; the spec allows
+-- non-UTF-8 aliases.
validateNodeAnnouncement :: NodeAnnouncement -> Either ValidationError ()
validateNodeAnnouncement msg = do
validateFeatureBits (nodeAnnFeatures msg)
+ if length [ () | AddrDNS _ _ <- nodeAnnAddresses msg ] > 1
+ then Left ValidateMultipleDns
+ else Right ()
-- | Validate channel_update message.
--