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

Improve protocol-level handshaking protocol #3049

Closed
wants to merge 10 commits into from
Closed

Improve protocol-level handshaking protocol #3049

wants to merge 10 commits into from

Conversation

nbougalis
Copy link
Contributor

This commit restructures the HTTP based protocol negotiation that rippled executes and introduces support for negotiation of compression for peer links which, if implemented, should result in significant bandwidth savings for some server roles.

This commit also introduces the new [network_id] configuration option that administrators can use to specify which network the server is part of and intends to join. This makes it possible for servers from different networks to drop the link early.

The changeset also improves the log messages generated when negotiation of a peer link upgrade fails. In the past, no useful information would be logged, making it more difficult for admins to troubleshoot errors.

@nbougalis
Copy link
Contributor Author

@MarkusTeufelberger, I would appreciate a review from you as well, since this also serves to correct several issues you've previously identified or mentioned in passing, including updating documentation.

@ripplelabs-jenkins
Copy link
Collaborator

ripplelabs-jenkins commented Aug 16, 2019

Jenkins Build Summary

Built from this commit

Built at 20190823 - 16:04:16

Test Results

Build Type Log Result Status
msvc.Debug logfile 1174 cases, 0 failed, t: 587s PASS ✅
msvc.Debug,
NINJA_BUILD=true
logfile 1174 cases, 0 failed, t: 601s PASS ✅
msvc.Debug
-Dunity=OFF
logfile 1173 cases, 0 failed, t: 1087s PASS ✅
msvc.Release logfile 1174 cases, 0 failed, t: 418s PASS ✅

Copy link
Collaborator

@MarkusTeufelberger MarkusTeufelberger left a comment

Choose a reason for hiding this comment

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

Phew, that took much longer to read through and understand than expected.

Thanks for the PR!

I'd really like to see at least some options besides signing the encoded TLS "finished" messages available. RFC5705 sounds more reasonable to me, or also just offering to use actual widely trusted certificates, if available (e.g. via Let's Encrypt).

src/ripple/overlay/README.md Outdated Show resolved Hide resolved
src/ripple/overlay/README.md Outdated Show resolved Hide resolved
src/ripple/overlay/README.md Outdated Show resolved Hide resolved
src/ripple/overlay/README.md Outdated Show resolved Hide resolved
src/ripple/overlay/README.md Outdated Show resolved Hide resolved
src/ripple/overlay/impl/OverlayImpl.cpp Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolMessage.h Outdated Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolMessage.h Show resolved Hide resolved
return hdr;
}

// Version 2 header: compressed payload.
Copy link
Collaborator

Choose a reason for hiding this comment

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

A simpler(?) solution might be to add a "header" message type in Protobuf that encodes all these parameters (payload_wire_size, payload_real_size, compression_algorithm) and only have a "header_wire_size" parameter to delimit stuff on the wire (might be possible to just use 1-2 bytes of length data).

You'd then send header_wire_size | header_proto | payload_proto on the wire. You can add the actual version of the protocol used to the header_proto as well to be more flexible and have more options to extend it in the future.

The code below just seems a bit like re-inventing yet another wire protocol, even though you are already using protobuf.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hear you. I actually don't really like this binary prefix business, and am seriously considering migrating to a pure WebSocket-based transport (which would make the binary header unnecessary) or adopting an approach closer to what you describe (involving coded protobuf streams).

Copy link
Collaborator

Choose a reason for hiding this comment

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

gRPC might be worth a look too then...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that gRPC is probably overkill and will require more rearchitecting than I'd like to do at this time. But I will take a look.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I just started looking into libp2p as well (used by Ethereum 2.0, IPFS etc.), but there's no C++ implementation so far and I suspect they are also vulnerable to active MITM with their TLS handshake.

Cap'n'Proto might also be worth a look, gRPC has the advantage that it might also take care of the transport stuff and is probably more widely used and implemented in other software. Wouldn't solve the MITM handshake mitigation, but there were options discussed for that already.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, libp2p's handshake is not vulnerable and also prevents MITM attacks with a standard certificate based approach. The only extra part is that one needs to check that the host key embedded in the certificate did actually sign the public key of the certificate (and that the connection is using this key). That's more related to establishing a connection though, not so much to the stream of protobuf messages here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While you can certainly address this issue using certificates, I still think the extra work that users have to go through makes it unlikely that they will. The primary objection to the existing MITM mitigation technique seems to be "yuck, I don't like how this is done." And that's fine but it's not really disposative.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, it is "this is not possible without adding a patch to Python's stdlib" in my case. Accessing these messages is just not possible in a lot of implementations of TLS, verifying a special field in a certificate is.

The certs in libp2p are self-signed and can be created by the application ad-hoc, so there's no work on the user's end...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. This actually does give me an idea. I may do this, but it'll have to wait for a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Based on what was described, my vote would be for the "header" message type & header_wire_size, seems like a simple solution that does the job. Since 'finished' and 'peer_finished' messages are part of the public openssl API, higher level wrappers should expose this functionality, if they do not we can push for this as a community (it's FOSS afterall!).

Reading about Cap'n'Proto, it seems cool though is a binary protocol which has downsides (additional debugging complexity, etc). Didn't look into libp2p as @MarkusTeufelberger said there was no C++ implementation. Self-signed certs would work but introduce additional management complexity (yes rippled could implement it internally but so would each protocol implementation). And what would we get beyond what is already provided in that case?

src/ripple/overlay/impl/ProtocolMessage.h Outdated Show resolved Hide resolved
@movitto
Copy link
Contributor

movitto commented Aug 20, 2019

Phew, that took much longer to read through and understand than expected.

Thanks for the PR!

I'd really like to see at least some options besides signing the encoded TLS "finished" messages available. RFC5705 sounds more reasonable to me, or also just offering to use actual widely trusted certificates, if available (e.g. via Let's Encrypt).

Just jumping in here as I start to take a look at this. We've already implemented the handshake protocol in the ruby library and I'd like to keep that up to date.

Reading RFC5705, it specified the necessity for a 'master_secret' and 'label' for the EKM context, what were you thinking for those?

I'd personally vote against certificates as it's a centralizing component and the whole point on the P2P protocol is to be decentralized.

@MarkusTeufelberger
Copy link
Collaborator

master_secret and label can be public values as far as I understand it.

Certificates don't have to be signed by a CA, they can be self-signed. Take a look at libp2p's TLS 1.3 handshake protocol for example. Nothing centralized about that one.

@nbougalis
Copy link
Contributor Author

Just jumping in here as I start to take a look at this. We've already implemented the handshake protocol in the ruby library and I'd like to keep that up to date.

That's neat. Even if we implement something different, it's likely that this mechanism will stick around for a while, and you'll have plenty of time to implement it. Also, hopefully it'll be something that doesn't require custom ruby gems.

Reading RFC5705, it specified the necessity for a 'master_secret' and 'label' for the EKM context, what were you thinking for those?

I'm not convinced the RFC5705 solution(s) prevent the kind of attack that our simple construct prevents anyways. I have technique that can do it and which doesn't require reaching into the SSL session and pulling something out of it, but it's a more complicated process than what we do currently requiring at least 5 messages (3 in one direction, 2 in the other direction) to be exchanged.

I'd personally vote against certificates as it's a centralizing component and the whole point on the P2P protocol is to be decentralized.

I think that we could implement something where the server creates a self-signed certificate (if none exists) and it would be no more difficult for the user, but I need to carefully consider the ramifications and ensure that we get the security guarantees we want.

@codecov-io
Copy link

codecov-io commented Aug 27, 2019

Codecov Report

Merging #3049 into develop will increase coverage by 0.14%.
The diff coverage is 13.13%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #3049      +/-   ##
===========================================
+ Coverage    68.94%   69.08%   +0.14%     
===========================================
  Files          688      690       +2     
  Lines        55743    55636     -107     
===========================================
+ Hits         38431    38436       +5     
+ Misses       17312    17200     -112
Impacted Files Coverage Δ
src/ripple/protocol/impl/BuildInfo.cpp 91.66% <ø> (+12.5%) ⬆️
src/ripple/overlay/Message.h 0% <ø> (ø) ⬆️
src/ripple/protocol/SecretKey.h 77.77% <ø> (ø) ⬆️
src/ripple/protocol/PublicKey.h 100% <ø> (ø) ⬆️
src/ripple/overlay/Peer.h 0% <ø> (ø) ⬆️
src/ripple/overlay/Overlay.h 78.94% <ø> (ø) ⬆️
src/ripple/rpc/handlers/WalletPropose.cpp 90.62% <ø> (ø) ⬆️
src/ripple/protocol/KeyType.h 91.66% <ø> (ø)
src/ripple/peerfinder/PeerfinderManager.h 100% <ø> (ø) ⬆️
src/ripple/overlay/impl/PeerImp.h 0% <ø> (ø) ⬆️
... and 19 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 98d55b9...c83f543. Read the comment docs.


// Version 1 header: uncompressed payload.
// The top six bits of the first byte are 0.
if ((*iter & 0xFC) == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason as to use the top 6 bits of the first byte (part of the payload_wire_size field) for the compression flag? Is it to maintain compatibility w/ the current header format (size)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could certainly do that. I have a different plan for compression in mind though and I'll probably repurpose the high bit to indicate compression when I do that PR.

@movitto
Copy link
Contributor

movitto commented Aug 27, 2019

Great job @nbougalis, this certainly looks like it sets the stage for some awesome changes when it comes to compression! As @MarkusTeufelberger said it was a little tricky to review this one though due to all the features / changes spread out in the many patches.

  • TMHello -> Handshake 👍
  • ProtocolVersion representation 👍 (fan of this though just to make sure I didn't miss anything, the different versions are not being used / distinguished yet correct?)
  • network_id 👍
  • message encapsulation changes (see comments on inline discussion above)
  • README update (few nits, see inline comments above)
  • Additional handshake fields (again see comments above)
  • Refactoring (logging, style, etc) 👍

Copy link
Collaborator

@seelabs seelabs left a comment

Choose a reason for hiding this comment

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

@nbougalis I like these changes a lot. I did a pass for nits/language-level feedback. I still need to do a pass and think about the meat of these changes.

src/ripple/overlay/impl/Handshake.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/Handshake.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/Handshake.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/PeerImp.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/PeerImp.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolVersion.cpp Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolVersion.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolVersion.cpp Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolVersion.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolVersion.cpp Show resolved Hide resolved
msg.insert("Server", BuildInfo::getFullVersionString());
msg.insert("Remote-Address", remote_address.to_string());
msg.insert(boost::beast::http::field::connection, "close");
msg.body() = text;
msg.prepare_payload();
return std::make_shared<SimpleWriter>(msg);
Copy link
Collaborator

Choose a reason for hiding this comment

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

SimpleWriter is also deprecated, tho I don't know what replaces it (and obviously not part of this patch).

@ximinez
Copy link
Collaborator

ximinez commented Oct 23, 2019

Just FYI, VS doesn't like the make_function_output_iterator
https://travis-ci.com/ripple/rippled/jobs/231590174#L860. I'm not sure how to fix it.

@ximinez
Copy link
Collaborator

ximinez commented Nov 1, 2019

This commit works around the compile error I pointed out above by doing it "the hard way" for MSVC builds. ximinez@c974971

}
};

BEAST_DEFINE_TESTSUITE(TMHello,overlay,ripple);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am always reluctant to get rid of unit tests without a really good reason. I don't think you've entirely removed the functionality being tested here, so why remove the tests?

}
};

BEAST_DEFINE_TESTSUITE(BuildInfo,ripple_data,ripple);
Copy link
Collaborator

Choose a reason for hiding this comment

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

As above, I am always reluctant to get rid of unit tests without a really good reason. I don't think you've entirely removed the functionality being tested here, so why remove the tests?

Copy link
Contributor Author

@nbougalis nbougalis Nov 2, 2019

Choose a reason for hiding this comment

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

This code changes ProtocolVersion to be a simple std::pair and all the relevant operators that were being tested here before are now provided by std so I don't believe that testing that functionality is necessary.

The only test that is no longer there is BEAST_EXPECT(current_protocol >= minimum_protocol) and the new implementation of this code simply doesn't have such values in a way that makes sense to test. Rather the implementation requires that all supported versions are listed (in sorted order, with no duplicates) in the supportedProtocolList array.

I had a static_assert that was meant to test the sorted & no dupes requirement, but it turns out that it was broken and I didn't know because I had incorrectly gated it so that it would require compiling with C++20. So your comment did help me find that, thanks!

I rewrote the static_assert to execute under C++17 and verified that it works correctly.

This commit restructures the HTTP based protocol negotiation that `rippled`
executes and introduces support for negotiation of compression for peer
links which, if implemented, should result in significant bandwidth savings
for some server roles.

This commit also introduces the new `[network_id]` configuration option
that administrators can use to specify which network the server is part of
and intends to join. This makes it possible for servers from different
networks to drop the link early.

The changeset also improves the log messages generated when negotiation
of a peer link upgrade fails. In the past, no useful information would
be logged, making it more difficult for admins to troubleshoot errors.

This commit also fixes RIPD-237 and RIPD-451
Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

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

I haven't done a full review, but did check out the more recently added commits.

I also fixed the Windows build by ripping off the boost::function_output_iterator with an iterator that skips the lambda and does the assignment directly. It's probably not the best option, but it works.

Edit: The branch is https://github.com/ximinez/rippled/tree/nb--handshake

src/ripple/overlay/impl/ProtocolVersion.cpp Outdated Show resolved Hide resolved
@nbougalis
Copy link
Contributor Author

nbougalis commented Nov 13, 2019

I also fixed the Windows build by ripping off the boost::function_output_iterator with an iterator that skips the lambda and does the assignment directly. It's probably not the best option, but it works.

The change in question is: ximinez@878b1d8

I appreciate you doing this, but it seems to me that it is overkill to address what is, at the end of the day, either a compiler deficiency or an issue with Boost against Windows. The fix that I put in place after your initial report (creating a std::function to wrap the lambda) is more compact and easy to understand IMO. So unless you feel particularly strong about your approach, I'll stick with what I have, @ximinez.

@ximinez
Copy link
Collaborator

ximinez commented Nov 13, 2019

I appreciate you doing this, but it seems to me that it is overkill to address what is, at the end of the day, either a compiler deficiency or an issue with Boost against Windows. The fix that I put in place after your initial report (creating a std::function to wrap the lambda) is more compact and easy to understand IMO. So unless you feel particularly strong about your approach, I'll stick with what I have, @ximinez.

Welp, egg on my face, because I'm having no problems building at all now. I have no idea what changed, but appears to be a moot point either way.

Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

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

I went through the whole thing this time, and it looks good. I have a couple of very minor and optional suggestions.

I notice there's a CI failure building the validator keys tool, which is because KeyType.h got moved. I think that'll have to be addressed separately, but probably before this change is released.

src/ripple/overlay/impl/ProtocolVersion.cpp Outdated Show resolved Hide resolved
src/ripple/overlay/impl/ProtocolVersion.h Outdated Show resolved Hide resolved
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.

10 participants