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

Proposal: Add support for proxying p2p connections to/from lnd #6843

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

aakselrod
Copy link
Contributor

We're considering ways to reduce the attack area in LND deployments. In addition to existing work on external signing, we are interested in adding support for decoupling Brontide and using a proxy running in a separate process for peer-to-peer connections. This would enable doing some transport-level checks away from the LND process/hardware, preventing several types of resource exhaustion/denial of service attacks. However, there is also a trade-off in that the node key will need to be copied to the Brontide proxy.

The way we envision this working is to add an option such as proxylisten or proxymux to lnd, which would connect over a single Brontide connection (or TLS or something) to the proxy and allow incoming/outgoing connections to be multiplexed over the connection. The proxy would allow connections only to and from known nodes, verifying ECDH for the connection before decrypting the traffic and proxying it over the multiplexed connection. This draft PR starts with proof of concept brontide proxy which multiplexes multiple connections over a single one.

There are several questions to consider and other TODOs. A non-exhaustive list follows:

  • Handle large messages, message fragmentation/defragmentation, partial reads, timeouts, reconnections, etc.
  • Possibly change location of the MessageConnWithPubkey interface (maybe in the peer package?)
  • Set up more robust/extensible signaling including connection key derivation
  • Add capability for dialing out
  • Integrate with the rest of lnd including peer, config, server, etc. code for connecting to a proxy
  • Create a proxy executable (external project? a binary under /cmd?)
  • Add means to allow-list peers
  • Add support for rate and resource limiting (per process, per peer, global)
  • Improve notifications

We would love feedback on the direction this is taking or additional suggestions. If this looks like it's going in the right direction, I'll keep moving forward with the TODOs listed above under this PR until it's usable.

@aakselrod aakselrod force-pushed the brontide-proxy branch 3 times, most recently from bfa8967 to 39a73a2 Compare August 30, 2022 23:08
This commit adds a new `brontide/proxy` subpackage containing
two new structs. `proxy.Mux` is a multiplexer that allows multiple
`proxy.Conn` connections to be handled over a single brontide
connection. This can be used to allow a proxy running in a separate
process to handle connections to and from peers, freeing up the
node's process and dedicated hardware to ensuring the safety of
money entrusted to it.
@Roasbeef
Copy link
Member

Roasbeef commented Sep 2, 2022

Hi Alex, very cool stuff! I'm happy to hear y'all are looking into ways to further harden lnd's network stack.

However, there is also a trade-off in that the node key will need to be copied to the Brontide proxy.

So this actually isn't the case. All brontide needs is to be able to do ECDH between a node's public key and the incoming key of the connecting peer

The ECDHRing interface exposes this functionality:

lnd/keychain/derivation.go

Lines 268 to 280 in 9d04b0c

// ECDHRing is an interface that abstracts away basic low-level ECDH shared key
// generation on keys within a key ring.
type ECDHRing interface {
// ECDH performs a scalar multiplication (ECDH-like operation) between
// the target key descriptor and remote public key. The output
// returned will be the sha256 of the resulting shared point serialized
// in compressed format. If k is our private key, and P is the public
// key, we perform the following operation:
//
// sx := k*P
// s := sha256(sx.SerializeCompressed())
ECDH(keyDesc KeyDescriptor, pubKey *btcec.PublicKey) ([32]byte, error)
}

A more restricted version that only works with a single key also exists:

lnd/keychain/derivation.go

Lines 282 to 293 in 9d04b0c

// SingleKeyECDH is an abstraction interface that hides the implementation of an
// ECDH operation by wrapping a single, specific private key.
type SingleKeyECDH interface {
// PubKey returns the public key of the wrapped private key.
PubKey() *btcec.PublicKey
// ECDH performs a scalar multiplication (ECDH-like operation) between
// the wrapped private key and remote public key. The output returned
// will be the sha256 of the resulting shared point serialized in
// compressed format.
ECDH(pubKey *btcec.PublicKey) ([32]byte, error)
}

The brontide state machine as is uses the above interface to access the ECDH operation:

lnd/brontide/noise.go

Lines 394 to 395 in 9d04b0c

func NewBrontideMachine(initiator bool, localKey keychain.SingleKeyECDH,
remotePub *btcec.PublicKey, options ...func(*Machine)) *Machine {

So today someone could instantiate a proxy that uses the existing brontide package, and the ECDH operation of the signerrpc to create something that accepts connections outside of lnd: https://github.com/lightningnetwork/lnd/blob/master/lnrpc/signrpc/signer.proto#L54-L64.

The missing component here would then be feeding the decrypted messages into lnd itself. One generic way to do that would be to extend the peersrpc sub-server to feed in non-custom messages to the lnd node. Being able to send/recv non-custom messages (below the 65k type) has been requested by ppl working on other external daemons, such as a bolt12 daemon. Here's a relevant comment outlining what it would take to instantiate all gossip handling externally, and then feed in the relevant gossip messages either to a read-only graph copy, or a peerrpc extension.

Today the custom message routing just goes between the endpoint, it would then need to be extended to contextually deliver the message to the relevant sub-system (funding to fundingmgr, channel updates to the link, etc, etc). This switch statement in the peer's readHandler does pretty much this. Also note that the peer object can already be fully instantiated outside of lnd, once the external brontide proxy obtains the connection, it can hand it off to the peer to drive all normal activities (abstracting out the raw channel message deliver to work over gRPC or w/e). There's a pending PR which does a pretty nice refactor of the existing peer+server interaction which should make projects like this a lot easier: #5700.

FWIW, will the path laid out (which has a lot of overlap w/ this PR), I don't necessarily think something like this needs to live within lnd, given we can add some extra APIs to make it possible to do it all fully externally. This would let the project move at a quicker pace in its early days, as it finds its sweet spot w.r.t the needs of node operators and services.

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.

2 participants