Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split the TLV payload processing into parsing and validation #3278

Merged
merged 9 commits into from
Nov 22, 2019

Conversation

cdecker
Copy link
Member

@cdecker cdecker commented Nov 19, 2019

In order to be useful #3260 requires that we allow TLV payloads with types that we may not know how to handle. In this case the plugin needs to terminate the onion and handle it by either forwarding manually using sendonion or terminate it either successfully or fail it.

This PR splits the parsing of the TLV payload from the validation. Parsing just stashes the entire payload in a tlv_payload instance, with typed pointers to fields that we know how to parse and handle. Validation then goes through and checks that there are no TLV fields that we don't know how to handle, that the types are in the correct order and that we have all the fields that we need in order to forward. The validity is then only used if lightningd isn't explicitly told how to handle the HTLC and to forward or terminate with an internal invoice.

@cdecker cdecker added this to the 0.7.4 milestone Nov 19, 2019
@cdecker cdecker self-assigned this Nov 19, 2019
Copy link
Contributor

@rustyrussell rustyrussell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup, minor nits mainly, but please convert the TLV parsing tests in wire/test-run-tlvstream.c to ensure these new functions are correct.

}
/* We've read bytes in ->fromwire, so update max */
*max -= field.length;
tal_expand(&record->fields, &field, 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tal_arr_expand() in utils.h does this a bit more compactly, BTW.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, was looking for that but couldn't find it so I thought I was misremembering the name xD

*max -= field.length;
tal_expand(&record->fields, &field, 1);
}
assert(tal_count(record->fields) > 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we ever hand a zero-length field in here, this will assert, right? *max == 0? That seems wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one slipped in, it checks whether we had any TLV fields,not whether one of the fields had zero-length. However the check is wrong since an empty TLV is valid per spec, even though not very sensible.

@@ -305,6 +305,39 @@ fail:
fromwire_fail(cursor, max);
return false;
}
bool ${tlv.name}_is_valid(const struct ${tlv.struct_name()} *record)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have this return the problematic offset within the TLV? There's a FIXME for this requirement in the onion parsing code, where we're supposed to return the offset which was the problem in the error msg.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return the exact offset of the offending field in fromwire_tlv_payload which has the locations, while here we would have to track the offset by accumulating the varint lengths and field lengths while verifying. I can add the offset in the fromwire code and just add the field index that we get upset about if you'd like :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make more sense anyway, since we can then say things like "Unknown even type x in field y" instead of saying I got upset somewhere at byte z.

u64 prev_type = 0;
for (size_t i=0; i<numfields; i++) {
struct tlv_field *f = &record->fields[i];
if (f->numtype % 2 == 0 && f->meta == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f->meta == NULL please.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch, sorry. Copy pasted it without checking.

SUPERVERBOSE("invalid ordering");
return false;
}
first = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code doesn't actually work; we don't update prev_type!

We had tests for the generic code which I think we should replicate here which should have caught this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll migrate the run-tlvstream tests to use the typesafe wrapper and remove the old ones. 👍

common/sphinx.c Outdated
*/
static void route_step_decode(struct route_step *rs)
{
if (rs->type == SPHINX_V0_PAYLOAD) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a switch() to catch if we ever add another type?

result = htlc_accepted_hook_deserialize(buffer, toks, &payment_preimage, &failure_code, &channel_update);

/* In order to handle it internally the payload needs to have at least
* the `outgoing_cltv_value`, the `amt_to_forward` and the
* `short_channel_id` if we are forwarding. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please quote the BOLT directly instead, so we know when this requirement changes (which it will!).

* - No unknown even types
* - Types in monotonical non-repeating order
*/
valid = valid && (rs->type == SPHINX_V0_PAYLOAD || tlv_payload_is_valid(rs->payload.tlv));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like it'd be nicer to have a separate method for validity check, instead of making it all inline here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a htlc_accepted_can_continue function which tells us whether we can continue with the route-step if we're told to by the hook.

switch (result) {
case htlc_accepted_continue:
if (rs->type == SPHINX_TLV_PAYLOAD && !tlv_payload_is_valid(rs->payload.tlv)) {
log_debug(channel->log, "Failing HTLC because of an invalid TLV payload");
if (!valid) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could just call the valid() method here

@cdecker cdecker force-pushed the tlv-validation-split branch 2 times, most recently from a8fbe3e to aecdf78 Compare November 21, 2019 16:33
We were weaving in and out of generic code through `fromwire_tlvs` with custom
parameters defining the types in that namespace. This hard-wires the parser
with the namespace's types. Slowly trying to deprecate `fromwire_tlvs` in
favor of this typesafe variant.
Since the parser itself just parses and doesn't include validation anymore we
need to put that functionality somewhere. The validation consists of enforcing
that the types are in monotonically increasing order without duplicates and
that for the even types we know how to handle it.
We wire in the code-generated function, which removes the upfront validation
and add the validation back after the `htlc_accepted` hook returns. If a
plugin wanted to handle the onion in a special way it'll not have told us to
just continue.
We'll need to pass them around anyway, so just make them easier to access by
doing a bit more to `process_onionpacket`.
We make the fields in `htlc_accepted_payload` optional (NULL if not present in
the payload) and defer validation till after the hook call.
This now enforces all rules for validity, both for the TLV format and checking
that the required fields have been provided.
We have consolidated the two functions into a single `route_step_decode`
function, and made it static since we call it in the `process_onionpacket`
function. We remove the two exposed functions since they're no longer useful.
This function ensures we have all the infos we need to continue if the
htlc_accepted hook tells us to. It also enforces well-formedness of the TLV
payload if we have a TLV payload.

Suggested-by: List Neigut <@niftynei>
Signed-off-by: Christian Decker <@cdecker>
@cdecker
Copy link
Member Author

cdecker commented Nov 21, 2019

Updated the PR to address the feedback:

Notice that there are two new commit that will not be shown in the range-diff: 02fb929 and df4c187 😉

@rustyrussell
Copy link
Contributor

Ack df4c187

Note: Travis picked up an unrelated race, causing #3286 ...

@rustyrussell rustyrussell merged commit d1df4d6 into ElementsProject:master Nov 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants