More More P2P Docs (#2525)

* Docs

* Clarify relay upgrades

* caaaaalapse

* Cleanup `sd_p2p_tunnel`
This commit is contained in:
Oscar Beaumont 2024-05-31 15:51:59 +08:00 committed by GitHub
parent 735e80ad4d
commit 58dd5c5d3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 274 additions and 200 deletions

View file

@ -317,7 +317,6 @@ pub fn base_router() -> Router<LocalState> {
request_file(
state.node.p2p.p2p.clone(),
node_identity,
&library.id,
&library.identity,
file_path_pub_id,
Range::Full,

View file

@ -8,6 +8,7 @@ use crate::{
Node,
};
use futures::future::join_all;
use sd_core_sync::SyncMessage;
use sd_p2p::{Identity, RemoteIdentity};
use sd_prisma::prisma::{crdt_operation, instance, location, SortOrder};
@ -382,6 +383,39 @@ impl Libraries {
self.libraries.read().await.get(library_id).cloned()
}
// will return the library context for the given instance
pub async fn get_library_for_instance(
&self,
instance: &RemoteIdentity,
) -> Option<Arc<Library>> {
join_all(
self.libraries
.read()
.await
.iter()
.map(|(_, library)| async move {
library
.db
.instance()
.find_many(vec![instance::remote_identity::equals(
instance.get_bytes().to_vec(),
)])
.exec()
.await
.ok()
.iter()
.flatten()
.filter_map(|i| RemoteIdentity::from_bytes(&i.remote_identity).ok())
.into_iter()
.any(|i| i == *instance)
.then(|| Arc::clone(library))
}),
)
.await
.into_iter()
.find_map(|v| v)
}
// get_ctx will return the library context for the given library id.
pub async fn hash_library(&self, library_id: &Uuid) -> bool {
self.libraries.read().await.get(library_id).is_some()

View file

@ -393,7 +393,6 @@ async fn start(
}) else {
return;
};
let library_id = tunnel.library_id();
let Ok(msg) = SyncMessage::from_stream(&mut tunnel).await.map_err(|err| {
error!("Failed `SyncMessage::from_stream`: {}", err);
@ -401,15 +400,15 @@ async fn start(
return;
};
let Ok(library) =
node.libraries
.get_library(&library_id)
.await
.ok_or_else(|| {
error!("Failed to get library '{library_id}'");
let Ok(library) = node
.libraries
.get_library_for_instance(&tunnel.library_remote_identity())
.await
.ok_or_else(|| {
error!("Failed to get library {}", tunnel.library_remote_identity());
// TODO: Respond to remote client with warning!
})
// TODO: Respond to remote client with warning!
})
else {
return;
};

View file

@ -23,7 +23,6 @@ use crate::{p2p::Header, Node};
pub async fn request_file(
p2p: Arc<P2P>,
identity: RemoteIdentity,
library_id: &Uuid,
library_identity: &Identity,
file_path_id: Uuid,
range: Range,
@ -42,7 +41,7 @@ pub async fn request_file(
)
.await?;
let mut stream = sd_p2p_tunnel::Tunnel::initiator(stream, library_id, library_identity).await?;
let mut stream = sd_p2p_tunnel::Tunnel::initiator(stream, library_identity).await?;
let block_size = BlockSize::from_stream(&mut stream).await?;
let size = stream.read_u64_le().await?;
@ -82,9 +81,9 @@ pub(crate) async fn receiver(
let library = node
.libraries
.get_library(&stream.library_id())
.get_library_for_instance(&stream.library_remote_identity())
.await
.ok_or_else(|| format!("Library not found: {:?}", stream.library_id()))?;
.ok_or_else(|| format!("Library not found: {:?}", stream.library_remote_identity()))?;
let file_path = library
.db
@ -93,12 +92,7 @@ pub(crate) async fn receiver(
.select(file_path_to_handle_p2p_serve_file::select())
.exec()
.await?
.ok_or_else(|| {
format!(
"File path {file_path_id:?} not found in {:?}",
stream.library_id()
)
})?;
.ok_or_else(|| format!("File path {file_path_id:?} not found in {:?}", library.id))?;
let location = file_path.location.as_ref().expect("included in query");
let location_path = location.path.as_ref().expect("included in query");

View file

@ -107,9 +107,7 @@ mod originator {
stream.write_all(&Header::Sync.to_bytes()).await.unwrap();
let mut tunnel = Tunnel::initiator(stream, &library.id, &library.identity)
.await
.unwrap();
let mut tunnel = Tunnel::initiator(stream, &library.identity).await.unwrap();
tunnel
.write_all(&SyncMessage::NewOperations.to_bytes())

View file

@ -1,6 +1,143 @@
//! A system for creating encrypted tunnels between peers over untrusted connections.
mod tunnel;
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
pub use sd_p2p::{Identity, IdentityErr, RemoteIdentity};
pub use tunnel::*;
use sd_p2p_proto::{decode, encode};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
use thiserror::Error;
use sd_p2p::{Identity, IdentityErr, RemoteIdentity, UnicastStream};
#[derive(Debug, Error)]
pub enum TunnelError {
#[error("Error writing discriminator.")]
DiscriminatorWriteError,
#[error("Error reading discriminator. Is this stream actually a tunnel?")]
DiscriminatorReadError,
#[error("Invalid discriminator. Is this stream actually a tunnel?")]
InvalidDiscriminator,
#[error("Error sending library id: {0:?}")]
ErrorSendingLibraryId(io::Error),
#[error("Error receiving library identity: {0:?}")]
ErrorReceivingLibraryIdentity(decode::Error),
#[error("Error decoding library identity: {0:?}")]
ErrorDecodingLibraryIdentity(IdentityErr),
}
/// An encrypted tunnel between two libraries.
///
/// This sits on top of the existing node to node encryption provided by Quic.
///
/// It's primarily designed to avoid an attack where traffic flows:
/// node <-> attacker node <-> node
/// The attackers node can't break TLS but if they get in the middle they can present their own node identity to each side and then intercept library related traffic.
/// To avoid that we use this tunnel to encrypt all library related traffic so it can only be decoded by another instance of the same library.
#[derive(Debug)]
pub struct Tunnel {
stream: UnicastStream,
library_remote_id: RemoteIdentity,
}
impl Tunnel {
/// Create a new tunnel.
///
/// This should be used by the node that initiated the request which this tunnel is used for.
pub async fn initiator(
mut stream: UnicastStream,
library_identity: &Identity,
) -> Result<Self, TunnelError> {
stream
.write_all(&[b'T'])
.await
.map_err(|_| TunnelError::DiscriminatorWriteError)?;
let mut buf = vec![];
encode::buf(&mut buf, &library_identity.to_remote_identity().get_bytes());
stream
.write_all(&buf)
.await
.map_err(TunnelError::ErrorSendingLibraryId)?;
// TODO: Do encryption things
Ok(Self {
stream,
library_remote_id: library_identity.to_remote_identity(),
})
}
/// Create a new tunnel.
///
/// This should be used by the node that responded to the request which this tunnel is used for.
pub async fn responder(mut stream: UnicastStream) -> Result<Self, TunnelError> {
let discriminator = stream
.read_u8()
.await
.map_err(|_| TunnelError::DiscriminatorReadError)?;
if discriminator != b'T' {
return Err(TunnelError::InvalidDiscriminator);
}
// TODO: Blindly decoding this from the stream is not secure. We need a cryptographic handshake here to prove the peer on the other ends is holding the private key.
let library_remote_id = decode::buf(&mut stream)
.await
.map_err(TunnelError::ErrorReceivingLibraryIdentity)?;
let library_remote_id = RemoteIdentity::from_bytes(&library_remote_id)
.map_err(TunnelError::ErrorDecodingLibraryIdentity)?;
// TODO: Do encryption things
Ok(Self {
library_remote_id,
stream,
})
}
/// Get the `RemoteIdentity` of the peer on the other end of the tunnel.
pub fn node_remote_identity(&self) -> RemoteIdentity {
self.stream.remote_identity()
}
/// Get the `RemoteIdentity` of the library instance on the other end of the tunnel.
pub fn library_remote_identity(&self) -> RemoteIdentity {
self.library_remote_id
}
}
impl AsyncRead for Tunnel {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
// TODO: Do decryption
Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
}
}
impl AsyncWrite for Tunnel {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
// TODO: Do encryption
Pin::new(&mut self.get_mut().stream).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_shutdown(cx)
}
}

View file

@ -1,147 +0,0 @@
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
use sd_p2p_proto::{decode, encode};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
use thiserror::Error;
use sd_p2p::{Identity, RemoteIdentity, UnicastStream};
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum TunnelError {
#[error("Error writing discriminator.")]
DiscriminatorWriteError,
#[error("Error reading discriminator. Is this stream actually a tunnel?")]
DiscriminatorReadError,
#[error("Invalid discriminator. Is this stream actually a tunnel?")]
InvalidDiscriminator,
#[error("Error sending library id: {0:?}")]
ErrorSendingLibraryId(io::Error),
#[error("Error receiving library id: {0:?}")]
ErrorReceivingLibraryId(decode::Error),
}
/// An encrypted tunnel between two libraries.
///
/// This sits on top of the existing node to node encryption provided by Quic.
///
/// It's primarily designed to avoid an attack where traffic flows:
/// node <-> attacker node <-> node
/// The attackers node can't break TLS but if they get in the middle they can present their own node identity to each side and then intercept library related traffic.
/// To avoid that we use this tunnel to encrypt all library related traffic so it can only be decoded by another instance of the same library.
#[derive(Debug)]
pub struct Tunnel {
stream: UnicastStream,
library_remote_id: RemoteIdentity,
library_id: Uuid,
}
impl Tunnel {
/// Create a new tunnel.
///
/// This should be used by the node that initiated the request which this tunnel is used for.
pub async fn initiator(
mut stream: UnicastStream,
library_id: &Uuid,
library_identity: &Identity,
) -> Result<Self, TunnelError> {
stream
.write_all(&[b'T'])
.await
.map_err(|_| TunnelError::DiscriminatorWriteError)?;
let mut buf = vec![];
encode::uuid(&mut buf, library_id);
stream
.write_all(&buf)
.await
.map_err(TunnelError::ErrorSendingLibraryId)?;
// TODO: Do encryption tings
Ok(Self {
stream,
library_id: *library_id,
library_remote_id: library_identity.to_remote_identity(),
})
}
/// Create a new tunnel.
///
/// This should be used by the node that responded to the request which this tunnel is used for.
pub async fn responder(mut stream: UnicastStream) -> Result<Self, TunnelError> {
let discriminator = stream
.read_u8()
.await
.map_err(|_| TunnelError::DiscriminatorReadError)?;
if discriminator != b'T' {
return Err(TunnelError::InvalidDiscriminator);
}
let library_id = decode::uuid(&mut stream)
.await
.map_err(TunnelError::ErrorReceivingLibraryId)?;
// TODO: Do encryption tings
Ok(Self {
// TODO: This is wrong but it's fine for now cause we don't use it.
// TODO: Will fix this in a follow up PR when I add encryption
library_remote_id: stream.remote_identity(),
stream,
library_id,
})
}
/// The the ID of the library being tunneled.
pub fn library_id(&self) -> Uuid {
self.library_id
}
/// Get the `RemoteIdentity` of the peer on the other end of the tunnel.
pub fn node_remote_identity(&self) -> RemoteIdentity {
self.stream.remote_identity()
}
/// Get the `RemoteIdentity` of the library instance on the other end of the tunnel.
pub fn library_remote_identity(&self) -> RemoteIdentity {
self.library_remote_id
}
}
impl AsyncRead for Tunnel {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
// TODO: Do decryption
Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
}
}
impl AsyncWrite for Tunnel {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
// TODO: Do encryption
Pin::new(&mut self.get_mut().stream).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_shutdown(cx)
}
}

View file

@ -50,16 +50,30 @@ From my ([oscartbeaumont](https://github.com/oscartbeaumont)'s) perspective this
## Sync
Unimplemented
[Implementation](https://github.com/spacedriveapp/spacedrive/blob/main/core/src/p2p/sync/mod.rs)
In an earlier version of the P2P system we had a method for sending sync messages to other nodes over the peer to peer connection, however this was removed during some refactoring of the sync system.
This protocol takes care of sending sync messages between nodes. This is an alternative to the Cloud-based system.
The code for it could be taken from [here](https://github.com/spacedriveapp/spacedrive/blob/aa72c083c2e5f6cf33f3c1fb66283e5fe0d1ba3b/core/src/p2p/pairing/mod.rs) and upgraded to account for changes to the sync and P2P system to bring back this functionality.
This protocol uses [`sd_p2p_tunnel::Tunnel`](/docs/developers/p2p/sd_p2p_tunnel) so ensure the recipient is paired into the library.
## Loading remote files
## Loading thumbnails and files remotely
TODO - Loading file within location over P2P
[Implementation](https://github.com/spacedriveapp/spacedrive/blob/main/core/src/p2p/operations/library.rs)
This protocol takes care of loading both thumbnails and files remotely. This is used when the media is not available on the local mode.
This protocol transparently extends the custom URI protocol and uses information in the database to determine the node responsible for the file.
This protocol uses [`sd_p2p_tunnel::Tunnel`](/docs/developers/p2p/sd_p2p_tunnel) so ensure the recipient is paired into the library.
### Design issues
Right now the system relies on the `instance_id` column on the `Location` to table to determine which node to route the media request to. This is a suboptimal solution as a location may move nodes, a good example of this is a USB drive.
A better design would be to have an in-memory table of `HashMap<LocationId, NodeRemoteIdentity>` and then using another P2P protocol which sends a message to all connected nodes when a location comes online/offline on the current node.
## Sync preview media
https://linear.app/spacedriveapp/issue/ENG-910/sync-preview-media
Unimplemented
Tracked by [ENG-910](https://linear.app/spacedriveapp/issue/ENG-910/sync-preview-media)

View file

@ -31,6 +31,10 @@ Currently we connect to every relay server that is returned from the discovery s
The issue of connecting to every relay server is tracked as [ENG-1672](https://linear.app/spacedriveapp/issue/ENG-1672/mesh-relays).
## Connection upgrades
The relay will attempt to upgrade the connection between any peers to a direct connection if possible. If the firewall conditions are too challenging the relay will proxy the encrypted traffic between the two peers as a fallback.
## Authentication
Currently the relay service is completly unauthenticated. To prevent abuse we are planning to restrict the relays to Spacedrive accounts.

View file

@ -7,21 +7,51 @@ index: 24
[Implementation](https://github.com/spacedriveapp/spacedrive/tree/main/crates/p2p/crates/block)
A file block protocol based on [SyncThing Block Exchange Protocol v1](https://docs.syncthing.net/specs/bep-v1.html).
This crate contains utilities focused on moving large arrays of bytes (files) between two peers reliabily and quickly. It's implementation is heavily inspired by [SyncThing Block Exchange Protocol v1](https://docs.syncthing.net/specs/bep-v1.html).
The goal of this protocol is to take bytes in and reliabily and quickly transfer them to the other side.
## Rewrite?
[Tracking issue](https://linear.app/spacedriveapp/issue/ENG-1760/block-protocol-v2)
The current implementation is a bit of a mess and is definitely not as performant as it could be. A rewrite is probally a good idea to improve it and as it is a small component of the overall networking system it should be a fairly easy undertaking.
Below I have outlined some of my thoughts on how to build an improved version:
### Progress tracking
Currently the protocol works like the following:
- Send block of file
- Wait for ack
- Send next block
This is obviously not ideal as it means the sender is waiting for the receiver to ack each block before sending the next one.
I originally added this to combat the fact that the progress indicator's were not the same on both sides, however I don't think this was worth the trade off. I was also generally testing on the same machine at this stage so it's entirely possible in the real world the progress is actually close enough without requiring acknowledgements.
We can either, remove acknowledgements or send them less frequently, some testing would be required to determine the best approach.
### Integrity checking
[Tracking issue](https://linear.app/spacedriveapp/issue/ENG-1312/spaceblock-file-checksum)
I think the new version of the protocol should compute a [Blake3](https://en.wikipedia.org/wiki/BLAKE_(hash_function)) hash on both the sender and the receiver and ensure they much before the transfer is considered complete. This ensures both any data corruption or bugs in the Spacedrop protocol don't result in data loss for the user. The current protocol lacks this feature.
### Cancellation
Currently the protocol has a custom system built into the acknowledgements that allows each side to cancel an in-progress transfer. A more efficent system could just close the Quic connection and rely on an [`ErrorKind::UnexpectedEof`](https://doc.rust-lang.org/stable/std/io/enum.ErrorKind.html#variant.UnexpectedEof) on the other side to detect the shutdown condition.
### Remove file names
[Tracking issue](https://linear.app/spacedriveapp/issue/ENG-1292/spaceblock-abstract-name-from-spaceblockrequest)
It doesn't make a lot of sense for `SpaceblockRequests` to have the `name` field as you may send an unnamed buffer. Where it should go instead is still undecided.
### File name overflows
[Tracking issue](https://linear.app/spacedriveapp/issue/ENG-572/spaceblock-file-name-overflow)
Right now it's possible for a file name's length to overflow. Linux allows for bigger file names than Windows so a transfer from Linux to Windows with a long file name will cause an issue on the Windows side. We can probally prompt the user to pick a shorter name if we detect an overflow.
## Example
```rust
# TODO
```
TODO - Outline my idea for a better implementation.
https://linear.app/spacedriveapp/issue/ENG-1760/block-protocol-v2
https://linear.app/spacedriveapp/issue/ENG-1292/spaceblock-abstract-name-from-spaceblockrequest
https://linear.app/spacedriveapp/issue/ENG-1312/spaceblock-file-checksum
https://linear.app/spacedriveapp/issue/ENG-563/spaceblock-error-handling
https://linear.app/spacedriveapp/issue/ENG-567/spaceblock-cancel-transfer
https://linear.app/spacedriveapp/issue/ENG-572/spaceblock-file-name-overflow
Refer to [the tests](https://github.com/spacedriveapp/spacedrive/blob/0392c781d75d8f8a571ed43a61ce90e11c7d73d5/crates/p2p/crates/block/src/lib.rs#L227) to see an example of how to use the protocol.

View file

@ -7,12 +7,18 @@ index: 24
[Implementation](https://github.com/spacedriveapp/spacedrive/tree/main/crates/p2p/crates/tunnel)
TODO
<Notice type="warning" text="The cryptography has not been implemented for this abstraction. It does not provide any security at this time but it should still be used so when it is implemented we can benefit from it." />
You can wrap an `UnicastStream` with a `sd_p2p_tunnel::Tunnel` to authenticate that the remote peer is who they say they are and to encrypt the data being sent between the two peers.
By default all communication is encrypted between the nodes, however we don't check that the remote peer is who they say they are, or are paired.
So an attacker could setup a modified version of Spacedrive which presents it's own certificate but proxies all traffic between two nodes. This would allow them to view or modify the library data sent over the network. With mDNS being very easy to spoof getting a MITM attack like this is not a far feteched idea.
Using `Tunnel` will prevent this attack by cryptographically verifying the remote peer holds the private key for the library instance they are advertising. This process also acts as an authentication mechanism for the remote peer to ensure they are allowed to request data within the library.
You **should** use this is your communicating with a library on a remote node (Eg. sync, request file) but if you're talking with the node (Eg. Spacedrop) you don't need it.
## Example
```rust
# TODO
```
TODO - https://linear.app/spacedriveapp/issue/ENG-753/spacetunnel-encryption
Refer to [the tests](#todo) to see an example of how to use the protocol.

View file

@ -5,7 +5,7 @@ index: 21
# Usage
This is a high-level guide of how to build features within Spacedrive on top of the peer-to-peer system. I would recommend referring to this [example PR](#todo) alongside this guide as a practical reference.
This is a high-level guide of how to build features within Spacedrive on top of the peer-to-peer system. I would recommend referring to this [example PR](https://github.com/spacedriveapp/spacedrive/pull/2523) alongside this guide as a practical reference.
Start by adding a new variant to [`Header` enum](https://github.com/spacedriveapp/spacedrive/blob/main/core/src/p2p/protocol.rs) and adjusting the `Header::from_stream` and `Header::to_bytes` implementation to support it.
@ -75,3 +75,9 @@ let metadata = PeerMetadata::from_hashmap(&peer.metadata()).unwrap();
// You could use the `semver` crate to compare versions
let is_running_version_0_1_0 = metadata.version.as_deref() == Some("0.1.0");
```
## Security
If your devices are sending library scoped data you should ensure you use `sd_p2p_tunnel::Tunnel` to authenticate the library on the remote node.
Refer to [it's docs](/docs/developers/p2p/sd_p2p_tunnel) for more information.