libp2p / specs

Technical specifications for the libp2p networking stack
https://libp2p.io
1.55k stars 274 forks source link

Document dialing peer without their peer ID #458

Open mxinden opened 1 year ago

mxinden commented 1 year ago

The discussion on whether one should be able to dial a peer without knowing their peer ID has come up many times.

Today two implementations (nim-libp2p and rust-libp2p) support dialing a peer without knowing their peer ID.

Copying use-cases from @Menduist from a recent discussion on Slack:

While I don't think we have nor will find consensus on whether an implementation supports dialing a peer without their peer ID, I do think there is value documenting that implementations may support this and the rational for it.

I think a short paragraph in https://github.com/libp2p/specs/blob/master/connections/README.md would suffice.

@Menduist would you want to tackle this?

marten-seemann commented 1 year ago

Connecting to a peer with a trusted IP & port combo, without having to know his PeerId. For instance, in eth2 you can have one / multiple public facing nodes, and one hidden node in your network actually doing the validation, to prevent ddos attacks on that node. Since this is in a private / secure network, you can trust peers by IP & port without validating their PeerId. And allowing them to change PeerId regularly is a bit more secure

Not sure I understand what a trusted IP is. Even if it's your own datacenter, you can never be sure that there's no attacker in the network. Just posting this slide here... image

Assuming dnssec, dialing multiple peers a dnsaddr provides you without having to know their PeerId before-hand. For instance, in eth devp2p, they have a nice DHT crawler which puts every peer it finds into a dnsaddr like recursive record, allowing to bootstrap without dedicated servers.

How do you prevent an MITM from intercepting a connection to an IP? How does the fact that you discovered the IP via DNSSEC and not DNS change any of the properties?

lidel commented 1 year ago

cc @Stebalien (I vaguely remember we talked about this years ago in context of /dnsaddr and ipfs swarm connect in Kubo)

Menduist commented 1 year ago

I'm honestly surprised to get push back on this. If they are made aware of the risks, let your users do what they want to do. "Who can do more can do less."

In nim-libp2p's cases, you have to use a separate procedure to connect to someone without it's PeerId, and it's being made very clear that you don't know it's PeerId.

Anyway, I'll respond here. For of all, keep in mind that in some libp2p networks, we don't care that much about PeerId. In nimbus, the peer management looks like this:

while gossipSubNotHealthy():
  let peer = await discoverRandomPeer()
  await peer.connect()

Not sure I understand what a trusted IP is.

You can trust IPs in some scenarios, "secured network" (as in, VPNs, or local networks with secured access. If someone has physical access to your server, most of the regular security assumptions go out of the window anyway)

How does the fact that you discovered the IP via DNSSEC and not DNS change any of the properties?

Ok so let's see the chain of trust. If you have /dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN setup as a bootstrap node in your application, it means that:

So basically, as a user, I have to trust the developer, which will then trust* someone holding a private key for that PeerId. (here, this is actually the same entity developing the application and holding these private key, but that's not always the case)

Now, if that private key gets compromised, any DNS provider could redirect the flow to it's own nodes, and the only way to fix it is push a new version of your binary. You have no way to revoke it on the fly etc.

Now, let's imagine that you just have /dnsaddr/bootstrap.libp2p.io/ as a bootstrap node, with dnssec. The flow becomes:

So the chain of trust, and security properties don't change that much, I think. However, you can now cycle your peer ids, revoke & add nodes more easily, etc. Up to a point, where, as show here, you can forego the dedicated bootstrap servers, and instead just put a list of every node you can find in that DNS record.

* in practice, you should have bootstrap nodes, so an application developer doesn't have to put all of his trust in a single bootstrap "manager", but to trust the overall set.

marten-seemann commented 1 year ago

I'm honestly surprised to get push back on this.

Not pushing back. I just want us to be very clear on the security properties (or lack thereof) that this entails, and it seems like there's some confusion here.

Dialing without a peer ID is equivalent to saying that you'll trust any peer ID that is presented to you. To translate this into web2 terms, it means that you're not checking the TLS certificate at all (in the browser: you're happy to click that big red "ignore certificate warning" for whatever site you're visiting). What does DNSSEC resolution actually give you? I'd argue not a lot. As much as getting your bank's IP address over DNSSEC, and then happily ignoring any certificate warning you get accessing the site.

The root problem is that there's no authentication at the IP layer, and just because you sent a packet to 1.2.3.4 and got a reply from 1.2.3.4 doesn't mean that it was the server you deployed at 1.2.3.4 that gave the answer. Any attacker sitting on the path between you and 1.2.3.4 could have intercepted the handshake, either by MITM-ing it, or by just pretending to be that server altogether.

Now is this always a bad idea? No, if you genuinely don't care about peer IDs, then it's probably fine. Your application protocol will (hopefully!) have some other way of authentication that happens after the libp2p handshake. If you don't have that, any middlebox can just intercept all your connection and see the plaintext of all your communication.

Menduist commented 1 year ago

Just to clarify:

In the scenario where you dial /dnsaddr/bootstrap.libp2p.io/, the DNS record will hold the PeerIds, and these PeerIds will be checked:

$ dig +short -t txt _dnsaddr.bootstrap.libp2p.io
"dnsaddr=/dnsaddr/sv16.bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp"
"dnsaddr=/dnsaddr/am6.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb"
"dnsaddr=/dnsaddr/ams-rust.bootstrap.libp2p.io/p2p/12D3KooWEZXjE41uU4EL2gpkAQeDXYok6wghN7wwNVPF5bwkaNfS"
"dnsaddr=/dnsaddr/ny5.bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
"dnsaddr=/dnsaddr/sg1.bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt"
"dnsaddr=/dnsaddr/sv15.bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"

dnssec gives you that you can trust this data (as much as you can trust whoever is holding the private key for each of these PeerId). Without dnssec, any DNS server in your DNS chain could forge a fake response

marten-seemann commented 1 year ago

Without dnssec, any DNS server in your DNS chain could forge a fake response

That's true. If you load the peer ID via DNS, then you'll to do DNSSEC and peer ID validation to authenticate the bootstrap node. You could also have a /dnsaddr/bootstrap.libp2p.io/p2p/<peer_id> hardcoded in your application. That would allow you to move the server around to different IPs, as long as you keep the private key. In that case, DNSSEC doesn't buy you a lot.

Menduist commented 1 year ago

You could also have a /dnsaddr/bootstrap.libp2p.io/p2p/ hardcoded in your application.

Yes, but see my previous points:

Not trying to push for "one being better than the other", it's just two legitimate use cases, which different properties, but same security guarantees in fine, imo

marten-seemann commented 1 year ago

I agree that DNSSEC provides you the nice properties you list. My point is that no matter if you do DNS or DNSSEC, you need to verify the peer ID, otherwise an attacker can trivially MITM your connection and you lose all the security properties you're hoping for.

Stebalien commented 1 year ago

We specifically added this in kubo for user convenience. However, this feature has no security implications as you're just asking kubo to form a connection to some random peer.

If we did support this in libp2p, it would have to be in the same way. I.e., some generic DialAddress(multiaddr) (PeerID, error) function. This would be pretty useful for general-purpose "peering" where you don't care about the specific node you're connecting to.

If they are made aware of the risks, let your users do what they want to do. "Who can do more can do less."

Please avoid arguments like this:

Basically, "why not" is never a good reason.

Menduist commented 1 year ago

some generic DialAddress(multiaddr) (PeerID, error) function

That's how it's done in nim-libp2p:

let peerId = await switch2.connect(someMultiaddresses)

(In Nim you have to consume the return value of a function so you can't "ignore" the peer id)

Any new feature has a maintenance burden.

Of course, I'm not forcing anyone to add something to their implementation, I was just surprised at the amount of push-back (now clarified as "not push back") for getting it in our implementation.

We do not build footguns. We can't prevent users from using our software incorrectly, but we can at least avoid providing dangerously insecure functionality.

I think it's clear at this point that this feature, while it can be mis-used, does have real like applications. I thought that was already clear from my original slack messages, hence my response: "If some advanced users wants to do this, in situations where they have no other choice, and knowing that they are getting out of our safe boundaries, why stop them?"

MarcoPolo commented 1 year ago

Lots of good discussion here. Thanks all. I'll add a couple thoughts to the pool as well:

You can trust IPs in some scenarios, "secured network" (as in, VPNs, or local networks with secured access. If someone has physical access to your server, most of the regular security assumptions go out of the window anyway)

This is similar to the "Castle and Moat" network security model. The idea that you can defer your security concerns to your VPN, is nice, but in practice a bit risky. The best practice is to move off this security model and towards a "Zero Trust" model.

In the scenario where you dial /dnsaddr/bootstrap.libp2p.io/, the DNS record will hold the PeerIds, and these PeerIds will be checked...

I think if you're using DNSSEC, this is a great way to enable discovering a set of peers and their peer IDs. But the key here is that we must get their peer ids when we resolve the query. This subtlety should be obvious to the user since it has very different security properties.

For example, if example.com's dnsaddr resolved to /ip4/10.0.0.1/tcp/1234 with no peer id this would be insecure (no matter if you used DNSSEC):

let peerId = await switch2.connect(/dnsaddr/example.com/)

but if example.com resolved to /ip4/10.1.1.1/tcp/1234/p2p/QmFoo than the same exact code would be secure. This subtlety is a bit scary.

Probably what would be a better interface is to discover peers+peerids from a dnsaddr query, then dial those addrs once you know if you have a peer id (or not).


While I don't mean or want to block anyone's implementation from doing this if they want, I'm still curious what the use cases are. The three original use cases don't seem worth it to me since they seem to introduce foot guns or scary subtleties. To recap the three use cases:

Connecting to a peer with a trusted IP & port combo, without having to know his PeerId. For instance, in eth2 you can have one / multiple public facing nodes, and one hidden node in your network actually doing the validation, to prevent ddos attacks on that node. Since this is in a private / secure network, you can trust peers by IP & port without validating their PeerId. And allowing them to change PeerId regularly is a bit more secure

This is the same as the "castle and moat" security model, which no longer recommended in general.

Assuming dnssec, dialing multiple peers a dnsaddr provides you without having to know their PeerId before-hand. For instance, in eth devp2p, they have a nice DHT crawler which puts every peer it finds into a dnsaddr like recursive record, allowing to bootstrap without dedicated servers. More infos here

I think we can still support this use case by using a dnsaddr to discover multiple peers+peerids via dnssec (as mentioned above). You then don't need to dial without a peer id.

Random crawling with a nmap, but that seems unlikely to me

This seems interesting, but I agree unlikely. This seems specific/unlikely enough that a we wouldn't have to worry about supporting this as a first class use case.


On the flip side, do we gain anything by knowing the peer id before hand? I think so. I think we could try alternate Noise handshakes that could reduce our RTTs. There may be other benefits here too.