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

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

Open
eltorio opened this issue Mar 30, 2024 · 7 comments
Open
Labels
enhancement New feature or request

Comments

@eltorio
Copy link

eltorio commented Mar 30, 2024

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

  • It needs to use the new FramedStream with Option<Encrypt>
  • It needs to send RendezvousMessage KeyExchange
  • It needs to send stream.set_key(Key) for enabling secure 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

  • Please write in english only. If you provide some images in different languages, you're required to write a translation in english.
  • In any case, NEVER put here the content if your id_ed25519 file
@eltorio eltorio added the enhancement New feature or request label Mar 30, 2024
@eltorio
Copy link
Author

eltorio commented Mar 31, 2024

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
Copy link
Author

eltorio commented Apr 1, 2024

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
Copy link
Author

eltorio commented Apr 4, 2024

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 eltorio changed the title rustdesk_server tcp only handshake / secured tcp stream Proof of concept: rustdesk_server tcp only handshake / secured tcp stream Apr 4, 2024
@eltorio
Copy link
Author

eltorio commented Apr 7, 2024

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:

  • It's relatively easy to use with tokio_tungstenite.
  • It supports a secure TLS version.
  • It's supported by modern reverse proxies.

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

@herokukms
Copy link

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

@eltorio
Copy link
Author

eltorio commented Apr 7, 2024

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
Copy link

eltorio published a PR with his code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants