rustdesk / rustdesk-server

RustDesk Server Program
https://rustdesk.com/server
GNU Affero General Public License v3.0
6.48k stars 1.36k forks source link

Proof of concept: rustdesk_server tcp only handshake / secured tcp stream #394

Open eltorio opened 6 months ago

eltorio commented 6 months ago

Describe the solution you'd like Please publish the server side related to client [ start_tcp(server: ServerPtr, host: String)] (https://github.com/rustdesk/rustdesk/blob/0d75f71d16b9712b959423f6ae8fe5de7502e8f2/src/rendezvous_mediator.rs#L334)

Describe alternatives you've considered Forking the rustdesk-server for allowing tcp only handshake.
RustDesk already have a option for allowing a tcp only handshake while it is compiled with TEST_TCP .
It works perfectly with hbbs / hbbr from rustdesk-server-pro docker image but not with oss server.

After updating libs/hbb_common on rustdesk_server we can refactor the handle_tcp

as a proof of concept I modified the RustDesk client for adding an option to choose between UDP and TCP mode.
Next I quickly modified an oss rustdesk-server for working with this tcp enabled RustDesk client. I added this when the tcp connection

let (our_pk_b, out_sk_b) = box_::gen_keypair();
// …
let mut msg_out = RendezvousMessage::new();

let (key, sk) = Self::get_server_sk(&key);
match sk {
    Some(sk) => {
        let pk = sk.public_key();
        let sm = sign::sign(&our_pk_b.0, &sk);

        let bytes_sm = Bytes::from(sm);
        msg_out.set_key_exchange(KeyExchange {
            keys: vec![bytes_sm],
            ..Default::default()
        });
        log::debug!(
            "KeyExchange {:?} -> bytes: {:?}",
            addr,
            hex::encode(Bytes::from(msg_out.write_to_bytes().unwrap()))
        );
        //stream.set_key(pk);
        Self::send_to_sink(&mut sink, msg_out).await;
    }
    None => {
    }
}

it sends a correct message to the client, the client answers also a KeyExchange message but with with two keys generated with create_symmetric_key_msg(their_pk_b: [u8; 32]) . Basically it creates a symetric key with sodiumoxide, encrypt it with the server ed25519 public key a null nonce and our ed25519 private key.
The server receive the KeyExchange, because it has 2 keys, it decrypts the sodiumoxide secret box with client pk and server sk , get the symetric key and issue stream.set_key(pk);. Now the 21116 tcp port must be secured and can handle RegisterPeer…
The protocol can be hardened by using a random nonce and transmit it back to the server.

Additional context Add any other context about the feature request here.

Notes

eltorio commented 6 months ago

this is my key encrypting et decrypting in a test program

use hbb_common::{
    bytes::Bytes,
    sodiumoxide::{
        crypto::{box_, secretbox},
        hex,
    },
};
use std::error::Error;

pub fn create_symmetric_key_msg(their_pk_b: [u8; 32]) -> ([u8; 32], [u8; 32], Bytes, secretbox::Key) {
    let their_pk_b = box_::PublicKey(their_pk_b);
    let (our_pk_b, our_sk_b) = box_::gen_keypair();
    let key = secretbox::gen_key();
    let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
    let sealed_key = box_::seal(&key.0, &nonce, &their_pk_b, &our_sk_b);
    (our_pk_b.0, our_sk_b.0, sealed_key.into(), key)
}

pub fn get_symetric_key_from_msg(
    our_sk_b: [u8; 32],
    their_pk_b: [u8; 32],
    sealed_value: &[u8; 48],
) -> [u8; 32] {
    let their_pk_b = box_::PublicKey(their_pk_b);
    let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
    let sk = box_::SecretKey(our_sk_b);
    let key = box_::open(sealed_value, &nonce, &their_pk_b, &sk);
    match key {
        Ok(key) => {
            let mut key_array = [0u8; 32];
            key_array.copy_from_slice(&key);
            key_array
        }
        Err(e) => panic!("Error while opening the seal key{:?}", e),
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let (theirpk, theirsk) = box_::gen_keypair();
    println!("theirpk {:?}", hex::encode(&theirpk.0));
    println!("theirsk {:?}", hex::encode(&theirsk.0));

    let (ourpk,oursk, sealed_value, key) = create_symmetric_key_msg(theirpk.0);
    println!("ourpk {:?}", hex::encode(&ourpk));
    println!("oursk {:?}", hex::encode(&oursk));

    println!(
        "symmetric_value {:?} [u8;{:?}]",
        hex::encode(&sealed_value),
        &sealed_value.len()
    );
    println!("key {:?} [u8;{:?}]", hex::encode(key.0), key.0.len());

    let vec: Vec<u8> = sealed_value.to_vec();
    let sealed_value_48: [u8; 48] = vec.try_into().unwrap();

    let clear = get_symetric_key_from_msg(oursk, theirpk.0, &sealed_value_48);
    println!("clear {:?} [u8;{:?}]", hex::encode(&clear),clear.len());
    Ok(())
}
eltorio commented 6 months ago

this is my server side handshake Phase 1, Server to client after nat test answer

async fn key_exchange_phase1(
&mut self,
key: &str,
addr: SocketAddr,
connection: &mut FramedStream,
) {
let mut msg_out = RendezvousMessage::new();

let (_, sk) = Self::get_server_sk(&key);
match sk {
    Some(sk) => {
        let our_pk_b = self.our_pk_b.clone();
        let sm = sign::sign(&our_pk_b.0, &sk);

        let bytes_sm = Bytes::from(sm);
        msg_out.set_key_exchange(KeyExchange {
            keys: vec![bytes_sm],
            ..Default::default()
        });
        log::debug!(
            "KeyExchange {:?} -> bytes: {:?}",
            addr,
            hex::encode(Bytes::from(msg_out.write_to_bytes().unwrap()))
        );
        //TODO handle return
        let _ = connection.send(&msg_out).await;
    }
    None => {}
  }
}

Phase1b: client generates a symetric key and use hbb_common::tcp::FramedStream::set_key([u8; 32]) Phase 2: decrypt the symetric key received from the client

async fn key_exchange_phase2(
    &mut self,
    addr: SocketAddr,
    connection: &mut FramedStream,
    bytes: &BytesMut,
) {
    if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) {
        match msg_in.union {
            Some(rendezvous_message::Union::KeyExchange(ex)) => {
                log::debug!("KeyExchange {:?} <- bytes: {:?}", addr, hex::encode(&bytes));
                if ex.keys.len() != 2 {
                    log::error!("Handshake failed: invalid phase 2 key exchange message");
                    return;
                }

                log::debug!("KeyExchange their_pk: {:?}", hex::encode(&ex.keys[0]));
                log::debug!("KeyExchange box: {:?}", hex::encode(&ex.keys[1]));
                let their_pk: [u8; 32] = ex.keys[0].to_vec().try_into().unwrap();
                let cryptobox: [u8; 48] = ex.keys[1].to_vec().try_into().unwrap();
                let symetric_key =
                    get_symetric_key_from_msg(self.our_sk_b.0, their_pk, &cryptobox);
                log::debug!("KeyExchange symetric key: {:?}", hex::encode(&symetric_key));
                let key = secretbox::Key::from_slice(&symetric_key);
                match key {
                    Some(key) => {
                        connection.set_key(key);
                        log::debug!("KeyExchange symetric key set");
                        return;
                    }
                    None => {
                        log::error!("KeyExchange symetric key NOT set");
                        return;
                    }
                }
            }
            _ => {}
        }
    }
}

Voilà !

eltorio commented 6 months ago

Proof of Concept

The complete proof of concept is divided into two parts:

  1. Server Code: The server code can be found at this link.
  2. Client Code: The client code, which includes the UDP/TCP switch, is available at this link.

Please note that this is purely a proof of concept. It consists of a significant amount of copied and pasted code, and some parts are currently disabled. Despite these limitations, the client successfully connects, a symmetric key exchange occurs, the TCP connection is encrypted, and the public key registers.

Moving forward, I believe that Rustdesk will likely share its code rather than creating a separate TCP fork.

Rustdesk made a wonderfull opensource code…

eltorio commented 5 months ago

The code isn't fully optimized, as that isn't my primary goal. My ultimate aim is to demonstrate the viability of hosting hbbs in my Kubernetes cluster, using HAProxy as the ingress controller.

Like many others, I'd like to host hbbs behind HAProxy, but this requires several modifications: 1 - RustDesk needs to be compiled with TEST_TCP. 2 - The TCP handshake must be implemented in the open-source RustDesk server (the pro version already has this feature). 3 - The real peer address must be detected. I've tested the HAProxy v2 protocol here.

@rustdesk, are there any plans to include the TCP server in the open-source server?

Another, and potentially better, option is to use WebSocket encapsulation, similar to the web client. However, this would require additions to both the client and the server. The server would need to handle RegisterPeer and RegisterPK. WebSocket offers several advantages:

@rustdesk, are there any plans to develop WebSocket support?

herokukms commented 5 months ago

Thanks @eltorio for sharing your code how do you enable HAProxy v2 protocol ?

eltorio commented 5 months ago

Thanks @eltorio for sharing your code. How do you enable the HAProxy v2 protocol?

Firstly, my code is not production-ready! It lacks testing and cleaning. If @rustdesk has no plans to publish their code, I'll consider writing clean code. However, to be honest, I would prefer the WebSocket solution.

To answer your question, you simply need to have a backend like this one:

backend hbbs_hbbs-route
  mode tcp
  default-server check send-proxy-v2
  server SRV_1 172.28.5.131:21116 enabled
herokukms commented 5 months ago

eltorio published a PR with his code