feat: Invite over internet - #1254
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
gmaclennan
left a comment
There was a problem hiding this comment.
I left some comments on the approach. I'm not sure the division of responsiblities between the member-api and the remote-discovery class is quite right, but it depends on how you resolve the validation / authentication of incoming connections, which is the most important challenge to address.
|
I've got the connection and invite flow going. We want to make it harder to track specific peers via the DHT, therefore we want to use a fresh DHT keypair each time. The difficult part is that the keypair is used for the Gregor proposed we have something like this to send the real identity as the first packet down the noise stream after handshaking. This could leave the option to join by remote public key instead of a fresh topic. We will also need some sort of access control step to have the invite ID before we expose the protomux channel for RPC. |
…t.$member.getMany
|
Been wrestling with some sort of race condition for several hours now. I think I traced it down to the protomux channel recieving data before it's fully opened. |
gmaclennan
left a comment
There was a problem hiding this comment.
I still think we need an authentication step before connecting to the RPC channel:
Validating the inviteID and allowing the user (invitor) to confirm before connecting, which means queueing the connection somewhere until the user confirms
|
Been blocked getting the tests to work with the handshake logic. Seems the connection is breaking when we try to write the public key down the wire and isn't draining. |
|
Might do a flag in local-peers to ignore incoming RPC messages until we get authenticated? I think this extra handshake step is making everything more fragile. |
|
Been trying a bunch of ways to rework this and I'm getting a bit stuck. Gonna write out some thoughts on approaches here. Initial approach:
This worked fine Ephemeral keys approach
This didn't work because the connection was "breaking" after sending the first packet. I think it's got something to do with how we're reading a packet of the stream before sending it off elsewhere, but it's been really difficult to debug. Edit: I got this working! I needed to pause the stream after getting the first packet, else we'd drop an event. Queue up connections before RPCIn order to guard the RPC methods from spam, we should prompt the user to verify them before replicating anything. If we queue up the connections (at the manager level) we need a new place to track them that isn't local peers and would need to somehow tie them to the project they're trying to join via the member API. This extra book keeping and connection management is IMO not ideal. We'd likely need to add the invite ID into the handshake after establishing hyperswarm, which would also mean getting the remote discovery module to ask each project's Alternate: Disable RPC until connection is verifiedInstead we could mark peers inside localPeers as being |
|
TODO: Test all the edge cases |
|
TODO: Convert all |
gmaclennan
left a comment
There was a problem hiding this comment.
I see there are some checks that are failing that need to pass before we can merge. Also, some API feedback and some comments that were not resolved from the last review pass, and some new comments.
| * @returns {Promise<Buffer>} | ||
| */ | ||
| export async function readHandshakeBuffer(stream) { | ||
| const handshakeLengthBytes = await readChunk(stream, LENGTH_BYTES_LENGTH) |
|
|
||
| const swarm = new Hyperswarm({ | ||
| keyPair: this.#deriveSwarmIdentityKeypair(), | ||
| maxPeers: 16, |
There was a problem hiding this comment.
should we test this maxPeers? What happens if it's exceeded? Would be rare, e.g. workshop-based setup where invites are sent out to multiple invitees at the same time. I think it's fine to restrict this, just want to understand what the consequence is if it is exceeded, particularly for the "spamming" case (someone on the swarm, attempting multiple connections opportunistically - we block them doing anything, but they would be connected)
There was a problem hiding this comment.
Actually, it seems to limit outgoing connections, not incoming.
There was a problem hiding this comment.
what I mean is that if we get spammed with connections, once we reach 16 we won't be able to make new outgoing connections to join a link. Pruning the untrusted connections (and maybe banning them?) should reduce the risks further
| */ | ||
| #replicate(noiseStream) { | ||
| const replicationStream = this.#localPeers.connect(noiseStream) | ||
| const isTrusted = `isTrusted` in noiseStream ? noiseStream.isTrusted : true |
There was a problem hiding this comment.
We should use our new type RemoteAuthedNoiseStream - right now this is fragile for future maintenance - isTrusted defaults to true, so there is implied knowledge that you must only call #replicate with a trusted stream.
Suggestion: define the shared type in the noise strem helpers:
/** @typedef {OpenedNoiseStream & { authenticatedPublicKey: Buffer, isTrusted: boolean }} AuthedNoiseStream */
Then in LocalDiscovery, set socket.authenticatedPublicKey = socket.remotePublicKey, and isTrusted=true.
then you can simplify the checks in local-peers.js:1248-1257, mapeo-manager.js:408 and remote-discover.js:140-144. It avoids a future maintainer accidentially passing a stream through connect() and defaulting to trusted.
gmaclennan
left a comment
There was a problem hiding this comment.
A few edge-cases and remaining security issues to clean up
| this.#pendingHandshakes.delete(socket) | ||
| pendingDefer.resolve(true) | ||
| } catch (err) { | ||
| this.emit('error', ensureKnownError(err)) |
There was a problem hiding this comment.
I don't think you attach an error listener to the remoteDiscovery instance, so this will throw an unhandled 'error' and crash node.
| * @returns {Promise<Buffer>} | ||
| */ | ||
| export async function readHandshakeBuffer(stream) { | ||
| const handshakeLengthBytes = await readChunk(stream, LENGTH_BYTES_LENGTH) |
There was a problem hiding this comment.
Have you maybe not pushed this change? The code I see is still unbounded length.
… respect force close param
|
Sorry to come late with some additional architecture comments, but I've been thinking through more details about privacy and security related to connections over the internet, and how we manage future internet sync. I think it merits some changes in this PR before we roll it out because it reduces changes that are needed in the future. Block project replication based on trust Fix: Test: during invite, with either/both invitee and invitor with multiple projects, no New protomux auth channel Fix: Create a protomux in remote-discovery.js and store in Defer invitor's Fix: only send the |
Closes #1207