clboss/Ln/OnionError.cpp

163 lines
4.9 KiB
C++
Raw Permalink Normal View History

Ln/OnionError: shared onion-failure parser; XMoveFunds adopts the hardened version XMoveFunds/Main.cpp carried a duplicate of FundsMover/Attempter.cpp's parse_chan_update that had drifted behind the PR320 deep-review hardening: it lacked the fee_proportional_millionths > 1e6 reject, its TLV bounds check (tpos + tlen > cu_size) could wrap on an attacker-supplied bigsize length, and it accepted inbound-fee TLVs of any length >= 8 instead of exactly 8. Extract the parser into a new shared module, Ln/OnionError, carrying the hardened semantics: - failcode_name(): BOLT 04 failcode -> grep-friendly name (previously Track-A-only). - ChanUpdate: the parsed channel_update policy subset askrene consumes, plus the bLIP-18 inbound-fee TLV fields, and an operator== that deliberately compares only the askrene-visible policy (the repeat-update detection use case). - parse_chan_update(): extract the embedded BOLT 07 channel_update from a sendpay 204 raw_message, with the prop clamp doubling as an overflow guarantee for callers' ceil(amt * prop / 1e6) math. XMoveFunds switches to the shared module via using-declarations, so call sites are unchanged; its three 204 log lines now print the failcode name next to the hex. Also correct a stale comment above failure_summary() claiming the channel_update-refresh branch is skipped -- it has existed since the FEE_INSUFFICIENT dissection work; only the in-call retry is (deliberately) absent. FundsMover/Attempter.cpp intentionally keeps its own copy: Track A is being treated as done, and switching it to Ln/OnionError is deferred to the next time Track A is touched for its own reasons. New unit test tests/ln/test_onionerror covers the onion wire format for all carrying failcodes, both type-prefix forms, the prop clamp boundary (1e6 accepted, 1e6+1 rejected), signed inbound-fee values, exact-8 TLV length enforcement, and the bigsize-wrap attack that the old Track B bounds check would have mishandled. 86/86 tests pass.
2026-07-06 15:55:02 -07:00
#include"Ln/OnionError.hpp"
#include"Util/Str.hpp"
#include<vector>
namespace {
/* Read a big-endian unsigned integer of 1..8 bytes from `data`
* starting at `offset`. Caller ensures the read is in-bounds.
*/
std::uint64_t read_be( std::uint8_t const* data
, std::size_t offset
, std::size_t nbytes
) {
auto v = std::uint64_t(0);
for (auto i = std::size_t(0); i < nbytes; ++i)
v = (v << 8) | std::uint64_t(data[offset + i]);
return v;
}
/* Read a BOLT 01 BigSize at `pos` in `data` (size `size`), advancing
* `pos` past it. Returns false if truncated. */
bool read_bigsize( std::uint8_t const* data
, std::size_t size
, std::size_t& pos
, std::uint64_t& out
) {
if (pos >= size)
return false;
auto first = data[pos];
auto nbytes = std::size_t( first < 0xfd ? 0
: first == 0xfd ? 2
: first == 0xfe ? 4
: 8 );
if (nbytes == 0) {
out = first;
pos += 1;
return true;
}
if (pos + 1 + nbytes > size)
return false;
out = read_be(data, pos + 1, nbytes);
pos += 1 + nbytes;
return true;
}
}
namespace Ln { namespace OnionError {
char const* failcode_name(std::uint16_t f) {
switch (f) {
case 0x1007: return "TEMPORARY_CHANNEL_FAILURE";
case 0x100b: return "AMOUNT_BELOW_MINIMUM";
case 0x100c: return "FEE_INSUFFICIENT";
case 0x100d: return "INCORRECT_CLTV_EXPIRY";
case 0x100e: return "EXPIRY_TOO_SOON";
case 0x1014: return "CHANNEL_DISABLED";
case 0x2002: return "TEMPORARY_NODE_FAILURE";
case 0x4008: return "PERMANENT_CHANNEL_FAILURE";
case 0x400a: return "UNKNOWN_NEXT_PEER";
case 0x4010: return "REQUIRED_CHANNEL_FEATURE_MISSING";
case 0x6002: return "PERMANENT_NODE_FAILURE";
default: return "UNKNOWN";
}
}
bool parse_chan_update( std::string const& raw_message_hex
, ChanUpdate& out
) {
std::vector<std::uint8_t> bytes;
try {
bytes = Util::Str::hexread(raw_message_hex);
} catch (std::exception const&) {
return false;
}
if (bytes.size() < 4)
return false;
auto failcode = std::uint16_t((bytes[0] << 8) | bytes[1]);
auto header = std::size_t(0);
switch (failcode) {
case 0x1007: case 0x100e: header = 0; break;
case 0x100b: case 0x100c: header = 8; break;
case 0x100d: header = 4; break;
default: return false;
}
auto pos = std::size_t(2) + header;
if (bytes.size() < pos + 2)
return false;
auto cu_len = std::size_t((bytes[pos] << 8) | bytes[pos + 1]);
pos += 2;
if (cu_len == 0 || bytes.size() < pos + cu_len)
return false;
auto cu = bytes.data() + pos;
auto cu_size = cu_len;
/* Skip the optional 2-byte type prefix 0x0102 if present. */
if (cu_size >= 2 && cu[0] == 0x01 && cu[1] == 0x02) {
cu += 2;
cu_size -= 2;
}
/* Fixed-layout body from offset 0 of the post-prefix
* channel_update: 64 sig + 32 chain_hash + 8
* short_channel_id + 4 timestamp + 1 message_flags + 1
* channel_flags + 2 cltv_expiry_delta + 8 htlc_minimum_msat
* + 4 fee_base_msat + 4 fee_proportional_millionths + 8
* htlc_maximum_msat = 136.
*/
if (cu_size < 136)
return false;
auto channel_flags = cu[109];
out.enabled = !(channel_flags & 0x02);
out.cltv_expiry_delta = std::uint16_t(read_be(cu, 110, 2));
out.htlc_minimum_msat = read_be(cu, 112, 8);
out.fee_base_msat = std::uint32_t(read_be(cu, 120, 4));
out.fee_proportional_millionths = std::uint32_t(read_be(cu, 124, 4));
out.htlc_maximum_msat = read_be(cu, 128, 8);
/* Reject absurd signed policies rather than propagating them.
* A proportional fee above 100% is never a policy we would
* pay, and bounding it here is the overflow guarantee the
* header doc promises callers. Failing the parse routes the
* failure into the callers' max_msat=0 hard-exclusion
* fallback -- the right response to a forwarder signing
* garbage.
*/
if (out.fee_proportional_millionths > 1000000)
return false;
/* Scan the trailing TLV stream for bLIP-18 inbound fees
* (type 55555): value is [i32 base][i32 prop], both signed. */
out.has_inbound_fee = false;
out.inbound_fee_base_msat = 0;
out.inbound_fee_proportional_millionths = 0;
auto tpos = std::size_t(136);
while (tpos < cu_size) {
auto ttype = std::uint64_t(0);
auto tlen = std::uint64_t(0);
if (!read_bigsize(cu, cu_size, tpos, ttype))
break;
if (!read_bigsize(cu, cu_size, tpos, tlen))
break;
/* Overflow-safe: tpos <= cu_size (guaranteed by
* read_bigsize) so cu_size - tpos cannot underflow,
* whereas tpos + tlen can wrap for an attacker-supplied
* tlen and slip past a `> cu_size` check. */
if (tlen > std::uint64_t(cu_size - tpos))
break;
if (ttype == 55555 && tlen == 8) {
out.has_inbound_fee = true;
out.inbound_fee_base_msat =
std::int32_t(std::uint32_t(read_be(cu, tpos, 4)));
out.inbound_fee_proportional_millionths =
std::int32_t(std::uint32_t(read_be(cu, tpos + 4, 4)));
}
tpos += tlen;
}
return true;
}
}}