Skip to content

Commit

Permalink
[#50] refactor!: make authentication modular (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
M0dEx authored Mar 8, 2024
1 parent e81d1d6 commit 7d4ac60
Show file tree
Hide file tree
Showing 21 changed files with 432 additions and 289 deletions.
42 changes: 27 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "quincy"
version = "0.8.3"
version = "0.9.0"
authors = ["Jakub Kubík <jakub.kubik.it@protonmail.com>"]
license = "MIT"
description = "QUIC-based VPN"
Expand Down Expand Up @@ -55,6 +55,7 @@ tokio = { version = "^1.25", features = [
] }
dashmap = "^5.4"
futures = "^0.3.17"
async-trait = "^0.1.77"

# Configuration
figment = { version = "^0.10.8", features = ["toml", "env"] }
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
Quincy is a VPN client and server implementation using the [QUIC](https://en.wikipedia.org/wiki/QUIC) protocol.

## Design
Quincy uses the QUIC protocol implemented by [`quinn`](https://github.com/quinn-rs/quinn) to create an encrypted tunnnel between clients and the server.
Quincy uses the QUIC protocol implemented by [`quinn`](https://github.com/quinn-rs/quinn) to create an encrypted tunnel between clients and the server.

This tunnel serves two purposes:
- authentication using a reliable bidirectional stream
- authentication using a reliable bi-directional stream
- data transfer using unreliable datagrams (for lower latency and avoidance of multiple reliability layers)

After a connection is established and the client is authenticated, a TUN interface is spawned using an IP address provided by the server.
Expand Down Expand Up @@ -72,6 +72,10 @@ With the configuration file in place, the client can be started using the follow
quincy-server --config-path examples/server.toml
```

**Please keep in mind that the pre-generated certificate in [`examples/cert/server_cert.pem`](examples/cert/server_cert.pem)
is self-signed and uses the hostname `quincy`. It should be replaced with a proper certificate,
which can be generated using the instructions in the [Certificate management](#certificate-management) section.**

### Users
The users utility can be used to manage entries in the `users` file.
The `users` file contains usernames and password hashes in a format similar to `/etc/shadow` (example can be found in [`examples/users`](examples/users)).
Expand Down
2 changes: 2 additions & 0 deletions examples/server.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ certificate_key_file = "examples/cert/server_key.pem"
address_tunnel = "10.0.0.1"
# Netmask used to generate the address pool available to clients
address_mask = "255.255.255.0"

[authentication]
# Path to the file containing user credentials
users_file = "examples/users"

Expand Down
3 changes: 0 additions & 3 deletions src/auth.rs

This file was deleted.

86 changes: 35 additions & 51 deletions src/auth/client.rs
Original file line number Diff line number Diff line change
@@ -1,77 +1,61 @@
use anyhow::{anyhow, Context, Result};
use bytes::BytesMut;
use ipnet::IpNet;
use quinn::{Connection, RecvStream, SendStream};
use serde::{Deserialize, Serialize};
use tokio::io::AsyncReadExt;
use std::time::Duration;

use crate::config::ClientAuthenticationConfig;
use anyhow::{anyhow, Result};
use ipnet::IpNet;
use quinn::Connection;

use super::server::AuthServerMessage;
use crate::config::{AuthType, ClientAuthenticationConfig};

/// Represents an authentication message sent by the client.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AuthClientMessage {
Authentication(String, String),
}
use super::{
stream::{AuthMessage, AuthStreamBuilder, AuthStreamMode},
users_file::UsersFileClientAuthenticator,
ClientAuthenticator,
};

/// Represents an authentication client handling initial authentication and session management.
pub struct AuthClient {
send_stream: SendStream,
recv_stream: RecvStream,
username: String,
password: String,
authenticator: Box<dyn ClientAuthenticator>,
auth_timeout: Duration,
}

impl AuthClient {
pub async fn new(
connection: &Connection,
pub fn new(
authentication_config: &ClientAuthenticationConfig,
auth_timeout: Duration,
) -> Result<Self> {
let (send, recv) = connection.open_bi().await?;
let authenticator = match authentication_config.auth_type {
AuthType::UsersFile => UsersFileClientAuthenticator::new(authentication_config),
};

Ok(Self {
send_stream: send,
recv_stream: recv,
username: authentication_config.username.clone(),
password: authentication_config.password.clone(),
authenticator: Box::new(authenticator),
auth_timeout,
})
}

/// Establishes a session with the server.
///
/// ### Arguments
/// - `connection` - The connection to the server
///
/// ### Returns
/// - `IpNet` - the tunnel address received from the server
pub async fn authenticate(&mut self) -> Result<IpNet> {
let basic_auth =
AuthClientMessage::Authentication(self.username.clone(), self.password.clone());
pub async fn authenticate(&self, connection: &Connection) -> Result<IpNet> {
let auth_stream_builder = AuthStreamBuilder::new(AuthStreamMode::Client);
let mut auth_stream = auth_stream_builder
.connect(connection, self.auth_timeout)
.await?;

self.send_message(basic_auth).await?;
let auth_response = self.recv_message().await?;
let authentication_payload = self.authenticator.generate_payload().await?;
auth_stream
.send_message(AuthMessage::Authenticate(authentication_payload))
.await?;

match auth_response {
Some(AuthServerMessage::Authenticated(addr, netmask)) => {
let address = IpNet::with_netmask(addr, netmask)?;
let auth_response = auth_stream.recv_message().await?;

Ok(address)
}
_ => Err(anyhow!("Authentication failed")),
match auth_response {
AuthMessage::Authenticated(addr, netmask) => Ok(IpNet::with_netmask(addr, netmask)?),
_ => Err(anyhow!("authentication failed")),
}
}

#[inline]
async fn send_message(&mut self, message: AuthClientMessage) -> Result<()> {
self.send_stream
.write_all(&serde_json::to_vec(&message)?)
.await
.context("Failed to send AuthServerMessage")
}

#[inline]
async fn recv_message(&mut self) -> Result<Option<AuthServerMessage>> {
let mut buf = BytesMut::with_capacity(1024);
self.recv_stream.read_buf(&mut buf).await?;

serde_json::from_slice(&buf).context("Failed to parse AuthClientMessage")
}
}
31 changes: 31 additions & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pub mod client;
pub mod server;
pub mod stream;
pub mod users_file;

use anyhow::Result;
use async_trait::async_trait;
use ipnet::IpNet;
use serde_json::Value;

use crate::server::address_pool::AddressPool;

/// Represents a user authenticator for the server.
///
/// `async_trait` is used to allow usage with dynamic dispatch.
#[async_trait]
pub trait ServerAuthenticator: Send + Sync {
async fn authenticate_user(
&self,
address_pool: &AddressPool,
authentication_payload: Value,
) -> Result<(String, IpNet)>;
}

/// Represents a user authentication payload generator for the client.
///
/// `async_trait` is used to allow usage with dynamic dispatch.
#[async_trait]
pub trait ClientAuthenticator: Send + Sync {
async fn generate_payload(&self) -> Result<Value>;
}
Loading

0 comments on commit 7d4ac60

Please sign in to comment.