working key handshake, client subcommands, just need to start encrypting and sending content
This commit is contained in:
parent
6e954a9795
commit
6cef74968c
6
dnstp-client/src/download.rs
Normal file
6
dnstp-client/src/download.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use crate::NetSettings;
|
||||
|
||||
pub fn download(net_settings: NetSettings)
|
||||
{
|
||||
|
||||
}
|
@ -1,32 +1,58 @@
|
||||
//! # Client Side
|
||||
//!
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::net::SocketAddr;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use clap::Parser;
|
||||
use log::{info, LevelFilter};
|
||||
use rand::RngCore;
|
||||
use simplelog::*;
|
||||
use dnstplib::DomainConfig;
|
||||
mod test;
|
||||
mod upload;
|
||||
mod download;
|
||||
|
||||
use dnstplib::message::DNSMessage;
|
||||
use dnstplib::net::{DNSSocket, NetworkMessage};
|
||||
use dnstplib::processor::ResponseProcesor;
|
||||
use std::fs::OpenOptions;
|
||||
use clap::{Parser, Subcommand};
|
||||
use log::{LevelFilter};
|
||||
use simplelog::*;
|
||||
|
||||
use crate::download::download;
|
||||
use crate::test::send_test_requests;
|
||||
use crate::upload::upload;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Addresses to send requests
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
/// Send test requests on loop to the server
|
||||
Test {
|
||||
#[clap(flatten)]
|
||||
net_options: NetSettings
|
||||
},
|
||||
/// Upload data to the remote server
|
||||
Upload {
|
||||
#[clap(flatten)]
|
||||
net_options: NetSettings,
|
||||
#[arg(short, long)]
|
||||
value: String
|
||||
},
|
||||
/// Download a payload from the remote server
|
||||
Download {
|
||||
#[clap(flatten)]
|
||||
net_options: NetSettings
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct NetSettings {
|
||||
/// Server address to send requests to
|
||||
#[arg(short, long)]
|
||||
address: String,
|
||||
/// Base domain to operate on
|
||||
/// Base domain server is operating on
|
||||
#[arg(long)]
|
||||
base_domain: String,
|
||||
/// Sub-domain to handle key handling when requested
|
||||
#[arg(long, default_value = "static")]
|
||||
key_endpoint: String
|
||||
#[arg(short, long, default_value = "static")]
|
||||
key_endpoint: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@ -44,40 +70,15 @@ fn main() {
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let address = SocketAddr::from(([127, 0, 0, 1], 0));
|
||||
|
||||
let mut socket = DNSSocket::new(vec!(address));
|
||||
socket.bind();
|
||||
socket.run_tx();
|
||||
|
||||
let tx_channel = socket.get_tx_message_channel().unwrap();
|
||||
|
||||
let mut processor = ResponseProcesor::new();
|
||||
processor.run();
|
||||
|
||||
socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel"));
|
||||
|
||||
let domain_config = DomainConfig {
|
||||
base_domain: args.base_domain,
|
||||
key_endpoint: args.key_endpoint
|
||||
};
|
||||
|
||||
let domain = domain_config.get_fq_key_endpoint();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
loop {
|
||||
|
||||
info!("sending...");
|
||||
|
||||
let message = DNSMessage::req_from_hostname(address, rng.next_u32() as u16, domain.clone());
|
||||
|
||||
let bytes = message.to_bytes();
|
||||
|
||||
tx_channel.send(Box::new(NetworkMessage {
|
||||
buffer: Box::new(bytes),
|
||||
peer: args.address.parse().unwrap()
|
||||
}));
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
match args.command {
|
||||
Command::Test { net_options } => {
|
||||
send_test_requests(net_options);
|
||||
}
|
||||
Command::Upload { net_options, value } => {
|
||||
upload(net_options, value);
|
||||
}
|
||||
Command::Download { net_options } => {
|
||||
download(net_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
52
dnstp-client/src/test.rs
Normal file
52
dnstp-client/src/test.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use log::info;
|
||||
use rand::RngCore;
|
||||
use dnstplib::client_crypto_context::ClientCryptoContext;
|
||||
use dnstplib::DomainConfig;
|
||||
use dnstplib::message::DNSMessage;
|
||||
use dnstplib::net::{DNSSocket, NetworkMessage};
|
||||
use dnstplib::processor::ResponseProcesor;
|
||||
use crate::NetSettings;
|
||||
|
||||
pub fn send_test_requests(args: NetSettings)
|
||||
{
|
||||
let address = SocketAddr::from(([127, 0, 0, 1], 0));
|
||||
|
||||
let mut socket = DNSSocket::new(vec!(address));
|
||||
socket.bind();
|
||||
socket.run_tx();
|
||||
|
||||
let tx_channel = socket.get_tx_message_channel().unwrap();
|
||||
|
||||
let crypto_context = Arc::new(Mutex::new(ClientCryptoContext::new()));
|
||||
let mut processor = ResponseProcesor::new(crypto_context.clone());
|
||||
processor.run();
|
||||
|
||||
socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel"));
|
||||
|
||||
let domain_config = DomainConfig {
|
||||
base_domain: args.base_domain,
|
||||
key_endpoint: args.key_endpoint
|
||||
};
|
||||
|
||||
let domain = domain_config.get_fq_key_endpoint();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
loop {
|
||||
info!("sending...");
|
||||
|
||||
let message = DNSMessage::req_from_hostname(address, rng.next_u32() as u16, domain.clone());
|
||||
|
||||
let bytes = message.to_bytes();
|
||||
|
||||
tx_channel.send(Box::new(NetworkMessage {
|
||||
buffer: Box::new(bytes),
|
||||
peer: args.address.parse().unwrap()
|
||||
}));
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
81
dnstp-client/src/upload.rs
Normal file
81
dnstp-client/src/upload.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use log::info;
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use dnstplib::client_crypto_context::ClientCryptoContext;
|
||||
use dnstplib::{DomainConfig, send_message};
|
||||
use dnstplib::message::{Direction, DNSHeader, DNSMessage, DNSQuestion, Opcode, QClass, QType, ResponseCode};
|
||||
use dnstplib::net::DNSSocket;
|
||||
use dnstplib::processor::ResponseProcesor;
|
||||
use crate::NetSettings;
|
||||
|
||||
pub fn upload(net_settings: NetSettings, value: String)
|
||||
{
|
||||
let address = SocketAddr::from(([127, 0, 0, 1], 0));
|
||||
|
||||
let mut socket = DNSSocket::new(vec!(address));
|
||||
socket.bind();
|
||||
socket.run_tx();
|
||||
|
||||
let tx_channel = socket.get_tx_message_channel().unwrap();
|
||||
|
||||
let crypto_context = Arc::new(Mutex::new(ClientCryptoContext::new()));
|
||||
let mut processor = ResponseProcesor::new(crypto_context.clone());
|
||||
processor.run();
|
||||
|
||||
socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel"));
|
||||
|
||||
let domain_config = DomainConfig {
|
||||
base_domain: net_settings.base_domain,
|
||||
key_endpoint: net_settings.key_endpoint
|
||||
};
|
||||
|
||||
info!("sending handshake...");
|
||||
|
||||
let message = DNSMessage {
|
||||
header: DNSHeader {
|
||||
id: OsRng.next_u32() as u16,
|
||||
direction: Direction::Request,
|
||||
opcode: Opcode::Query,
|
||||
authoritative: false,
|
||||
truncation: false,
|
||||
recursion_desired: false,
|
||||
recursion_available: false,
|
||||
valid_zeroes: true,
|
||||
response: ResponseCode::NoError,
|
||||
question_count: 2,
|
||||
answer_record_count: 0,
|
||||
authority_record_count: 0,
|
||||
additional_record_count: 0,
|
||||
},
|
||||
questions: vec![
|
||||
DNSQuestion {
|
||||
qname: domain_config.get_fq_key_endpoint(),
|
||||
qtype: QType::A,
|
||||
qclass: QClass::Internet,
|
||||
},
|
||||
DNSQuestion {
|
||||
qname: crypto_context.lock().unwrap().get_public_key_domain(&domain_config.base_domain),
|
||||
qtype: QType::A,
|
||||
qclass: QClass::Internet,
|
||||
}
|
||||
],
|
||||
answer_records: vec![],
|
||||
authority_records: vec![],
|
||||
additional_records: vec![],
|
||||
peer: net_settings.address.parse().unwrap(),
|
||||
};
|
||||
|
||||
send_message(message, &tx_channel);
|
||||
|
||||
while !crypto_context.lock().unwrap().is_complete() {
|
||||
info!("waiting for crypto completion...");
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
info!("crypto complete, sending data");
|
||||
}
|
39
dnstp/src/client_crypto_context.rs
Normal file
39
dnstp/src/client_crypto_context.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use aes_gcm_siv::Aes256GcmSiv;
|
||||
use p256::ecdh::EphemeralSecret;
|
||||
use crate::crypto::{get_random_asym_pair, trim_public_key};
|
||||
use crate::string::append_base_domain_to_key;
|
||||
|
||||
/// Represents the server from the perspective of a client
|
||||
pub struct ClientCryptoContext {
|
||||
pub shared_key: Option<Aes256GcmSiv>,
|
||||
pub client_private: EphemeralSecret,
|
||||
pub client_public: String,
|
||||
pub server_public: Option<String>
|
||||
}
|
||||
|
||||
impl ClientCryptoContext {
|
||||
pub fn new() -> Self {
|
||||
let (client_private, client_public) = get_random_asym_pair();
|
||||
|
||||
Self {
|
||||
shared_key: None,
|
||||
client_private,
|
||||
client_public,
|
||||
server_public: None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool
|
||||
{
|
||||
self.server_public.is_some() && self.shared_key.is_some()
|
||||
}
|
||||
|
||||
pub fn get_public_key_domain(&self, base_domain: &String) -> String
|
||||
{
|
||||
append_base_domain_to_key(
|
||||
trim_public_key(&self.client_public),
|
||||
base_domain
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,44 @@
|
||||
//! Structures for managing the state of connected clients from the perspective of the server
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
use aes_gcm_siv::Aes256GcmSiv;
|
||||
|
||||
/// A single client including when they connected and their shared cryptographic key
|
||||
pub struct Client {
|
||||
pub first_seen: SystemTime,
|
||||
pub last_seen: SystemTime,
|
||||
pub shared_key: Aes256GcmSiv
|
||||
}
|
||||
|
||||
impl Client {
|
||||
|
||||
/// Create a new client as
|
||||
pub fn new(shared_key: Aes256GcmSiv) -> Client
|
||||
{
|
||||
let time = SystemTime::now();
|
||||
|
||||
Client {
|
||||
first_seen: SystemTime::now(),
|
||||
first_seen: time,
|
||||
last_seen: time,
|
||||
shared_key
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bump_last_seen(&mut self)
|
||||
{
|
||||
self.last_seen = SystemTime::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Container for managing connected clients and their keys
|
||||
pub struct Clients {
|
||||
client_map: HashMap<String, Client>
|
||||
}
|
||||
|
||||
impl Clients {
|
||||
|
||||
/// Create a new collection of clients
|
||||
pub fn new() -> Clients
|
||||
{
|
||||
Clients {
|
||||
@ -36,8 +51,26 @@ impl Clients {
|
||||
// self.client_map.insert(client_id, Client::new(shared_key));
|
||||
// }
|
||||
|
||||
/// Add a newly connected client to the collection of connections. Index the client by public key.
|
||||
pub fn add(&mut self, client_id: String, client:Client)
|
||||
{
|
||||
self.client_map.insert(client_id, client);
|
||||
}
|
||||
|
||||
pub fn client_is_connected(&self, client_id: &String) -> bool
|
||||
{
|
||||
self.client_map.contains_key(client_id)
|
||||
}
|
||||
|
||||
pub fn bump_last_seen(&mut self, client_id: &String) -> Result<(), ()>
|
||||
{
|
||||
match self.client_map.get_mut(client_id)
|
||||
{
|
||||
None => Err(()),
|
||||
Some(client) => {
|
||||
client.bump_last_seen();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -37,9 +37,9 @@ pub fn get_random_asym_pair() -> (EphemeralSecret, String)
|
||||
}
|
||||
|
||||
/// Use one private key and an opposing public key to arrive at the same shared secret
|
||||
pub fn get_shared_asym_secret(secret: EphemeralSecret, opposing_public_key: String) -> Result<SharedSecret<NistP256>, ()> {
|
||||
pub fn get_shared_asym_secret(secret: &EphemeralSecret, opposing_public_key: &String) -> Result<SharedSecret<NistP256>, ()> {
|
||||
|
||||
match PublicKey::from_str(&opposing_public_key) {
|
||||
match PublicKey::from_str(opposing_public_key) {
|
||||
Ok(other_public) => {
|
||||
Ok(secret.diffie_hellman(&other_public))
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ fn matching_shared_secrets() {
|
||||
let (secret_alice, point_alice) = get_random_asym_pair();
|
||||
let (secret_bob, point_bob) = get_random_asym_pair();
|
||||
|
||||
let shared_alice = get_shared_asym_secret(secret_alice, point_bob).unwrap();
|
||||
let shared_bob = get_shared_asym_secret(secret_bob, point_alice).unwrap();
|
||||
let shared_alice = get_shared_asym_secret(&secret_alice, &point_bob).unwrap();
|
||||
let shared_bob = get_shared_asym_secret(&secret_bob, &point_alice).unwrap();
|
||||
|
||||
assert_eq!(shared_alice.raw_secret_bytes(), shared_bob.raw_secret_bytes());
|
||||
}
|
||||
@ -19,8 +19,8 @@ fn arbitrary_string_back_and_forth() {
|
||||
let (secret_alice, point_alice) = get_random_asym_pair();
|
||||
let (secret_bob, point_bob) = get_random_asym_pair();
|
||||
|
||||
let shared_alice = get_shared_asym_secret(secret_alice, point_bob).unwrap();
|
||||
let shared_bob = get_shared_asym_secret(secret_bob, point_alice).unwrap();
|
||||
let shared_alice = get_shared_asym_secret(&secret_alice, &point_bob).unwrap();
|
||||
let shared_bob = get_shared_asym_secret(&secret_bob, &point_alice).unwrap();
|
||||
|
||||
assert_eq!(shared_alice.raw_secret_bytes(), shared_bob.raw_secret_bytes());
|
||||
|
||||
|
@ -10,6 +10,35 @@ pub mod net;
|
||||
pub mod string;
|
||||
pub mod config;
|
||||
pub mod crypto;
|
||||
mod clients;
|
||||
pub mod clients;
|
||||
pub mod client_crypto_context;
|
||||
|
||||
pub use config::DomainConfig;
|
||||
use std::sync::mpsc::{Sender};
|
||||
use log::error;
|
||||
pub use config::DomainConfig;
|
||||
use crate::message::DNSMessage;
|
||||
use crate::net::{NetworkMessage, NetworkMessagePtr};
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum RequestError {
|
||||
/// Trying to perform an operation without having handshaked first
|
||||
NoHandshake,
|
||||
WrongNumberOfQuestions,
|
||||
CryptoFailure
|
||||
}
|
||||
|
||||
pub fn send_message(response: DNSMessage, sending_channel: &Sender<NetworkMessagePtr>)
|
||||
{
|
||||
match sending_channel.send(Box::new(
|
||||
NetworkMessage {
|
||||
buffer: Box::new(response.to_bytes()),
|
||||
peer: response.peer
|
||||
}
|
||||
)){
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("failed to pass a message to the network layer for delivery [{}]", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use crate::message::{DNSQuestion, DNSHeader, questions_to_bytes, Direction, ResponseCode, QType, QClass, ResourceRecord, records_to_bytes, ARdata};
|
||||
use crate::message::{DNSQuestion, DNSHeader, questions_to_bytes, Direction, ResponseCode, QType, QClass, ResourceRecord, records_to_bytes, ARdata, TXTRdata, RData};
|
||||
use crate::RequestError;
|
||||
|
||||
/// A DNS message which can be used as either a request or response based on its direction and composition
|
||||
#[derive(Debug)]
|
||||
@ -108,6 +109,26 @@ impl DNSMessage {
|
||||
response
|
||||
}
|
||||
|
||||
pub fn dumb_resp_from_request(&self) -> DNSMessage
|
||||
{
|
||||
let mut response = DNSMessage{
|
||||
header: self.header.clone(),
|
||||
questions: self.questions.clone(),
|
||||
answer_records: vec![],
|
||||
authority_records: vec![],
|
||||
additional_records: vec![],
|
||||
peer: self.peer
|
||||
};
|
||||
|
||||
response.header.direction = Direction::Response;
|
||||
response.header.response = ResponseCode::NotImplemented;
|
||||
response.header.answer_record_count = 0;
|
||||
response.header.authority_record_count = 0;
|
||||
response.header.additional_record_count = 0;
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn empty_resp_from_request(&self) -> DNSMessage
|
||||
{
|
||||
let mut response = DNSMessage{
|
||||
@ -131,4 +152,33 @@ impl DNSMessage {
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn protocol_error_from_request(&self, error_code: RequestError) -> DNSMessage
|
||||
{
|
||||
let txt = Box::new(TXTRdata::from(String::new()));
|
||||
|
||||
let mut response = DNSMessage{
|
||||
header: self.header.clone(),
|
||||
questions: self.questions.clone(),
|
||||
answer_records: vec![ResourceRecord {
|
||||
name_offset: 12,
|
||||
answer_type: QType::TXT,
|
||||
class: QClass::Internet,
|
||||
ttl: 0,
|
||||
rd_length: txt.to_bytes().len() as u16,
|
||||
r_data: txt
|
||||
}],
|
||||
authority_records: vec![],
|
||||
additional_records: vec![],
|
||||
peer: self.peer
|
||||
};
|
||||
|
||||
response.header.direction = Direction::Response;
|
||||
response.header.response = ResponseCode::ServerFailure;
|
||||
response.header.answer_record_count = 1;
|
||||
response.header.authority_record_count = 0;
|
||||
response.header.additional_record_count = 0;
|
||||
|
||||
response
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@ pub use cname_rdata::CnameRdata;
|
||||
mod tests;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
use crate::byte::{four_byte_combine, four_byte_split, push_split_bytes, two_byte_combine};
|
||||
use crate::message::question::{DNSQuestion, QClass, QType};
|
||||
|
||||
|
0
dnstp/src/processor/request/download.rs
Normal file
0
dnstp/src/processor/request/download.rs
Normal file
@ -2,17 +2,23 @@ use std::net::Ipv4Addr;
|
||||
use p256::ecdh::EphemeralSecret;
|
||||
use crate::clients::Client;
|
||||
use crate::crypto::{asym_to_sym_key, fatten_public_key, get_random_asym_pair, get_shared_asym_secret, trim_public_key};
|
||||
use crate::message::{ARdata, DNSMessage, DNSQuestion, QClass, QType, ResourceRecord};
|
||||
use crate::message::{ARdata, DNSMessage, QClass, QType, ResourceRecord};
|
||||
use crate::message::record::CnameRdata;
|
||||
use crate::string::{append_base_domain_to_key, strip_base_domain_from_key};
|
||||
use crate::string::{append_base_domain_to_key, encode_domain_name, strip_base_domain_from_key};
|
||||
|
||||
/// Result of a client's handshake request including server key pair and prepared response
|
||||
pub struct KeySwapContext {
|
||||
/// New client structure to track derived shared secret and last seen time
|
||||
pub new_client: Client,
|
||||
/// Response message to send to the client with the server's public key
|
||||
pub response: DNSMessage,
|
||||
/// Public key of the server's key pair
|
||||
pub server_public: String,
|
||||
/// Public key extracted from the client's request
|
||||
pub client_public: String
|
||||
}
|
||||
|
||||
/// Generate a random asymmetric key pair, append the dnstp base domain to the secret
|
||||
pub fn get_key_request_with_base_domain(base_domain: String) -> (EphemeralSecret, String)
|
||||
{
|
||||
let (private, public) = get_random_asym_pair();
|
||||
@ -20,9 +26,10 @@ pub fn get_key_request_with_base_domain(base_domain: String) -> (EphemeralSecret
|
||||
(private, append_base_domain_to_key(trim_public_key(&public), &base_domain))
|
||||
}
|
||||
|
||||
pub fn get_fattened_public_key(key_question: &DNSQuestion) -> (String, String)
|
||||
/// Extract the client's public key from the DNS message, turn the hostname back into the full fat public key with --- BEGIN KEY --- headers and trailers
|
||||
pub fn get_fattened_public_key(key_question: &String) -> (String, String)
|
||||
{
|
||||
let public_key = &key_question.qname;
|
||||
let public_key = key_question;
|
||||
let (trimmed_public_key, base_domain) = strip_base_domain_from_key(public_key);
|
||||
|
||||
(fatten_public_key(&trimmed_public_key), base_domain)
|
||||
@ -36,7 +43,10 @@ pub enum KeyDecodeError {
|
||||
SharedSecretDerivation,
|
||||
}
|
||||
|
||||
pub fn decode_key_request(message: DNSMessage) -> Result<KeySwapContext, KeyDecodeError>
|
||||
/// Take a client's handshake request, process the crypto and prepare a response
|
||||
///
|
||||
/// Includes generating a server key pair, using the public key in the response, deriving the shared secret.
|
||||
pub fn decode_key_request(message: &DNSMessage) -> Result<KeySwapContext, KeyDecodeError>
|
||||
{
|
||||
if message.questions.len() == 2 {
|
||||
|
||||
@ -52,18 +62,20 @@ pub fn decode_key_request(message: DNSMessage) -> Result<KeySwapContext, KeyDeco
|
||||
return Err(KeyDecodeError::SecondQuestionNotA(key_question.qtype));
|
||||
}
|
||||
|
||||
let (fattened_public_key, base_domain) = get_fattened_public_key(&key_question);
|
||||
// key is transmitted wihout --- BEGIN KEY -- header and trailer bits and with '.' instead of new lines
|
||||
let (fattened_public_key, base_domain) = get_fattened_public_key(&key_question.qname);
|
||||
// generate the servers public/private key pair
|
||||
let (server_private, server_public) = get_random_asym_pair();
|
||||
|
||||
let shared_secret = get_shared_asym_secret(server_private, fattened_public_key);
|
||||
|
||||
match shared_secret {
|
||||
// do the Diffie-Hellman shared secret derivation for the server side symmetric encryption
|
||||
match get_shared_asym_secret(&server_private, &fattened_public_key) {
|
||||
Ok(secret) => {
|
||||
|
||||
let sym_key = asym_to_sym_key(&secret);
|
||||
let new_client = Client::new(sym_key);
|
||||
let mut response = message.empty_resp_from_request();
|
||||
|
||||
// return an empty null response for the key hostname (static.BLANK.TLD) question, not that important
|
||||
let first_record = ResourceRecord {
|
||||
name_offset: 12,
|
||||
answer_type: QType::A,
|
||||
@ -73,22 +85,25 @@ pub fn decode_key_request(message: DNSMessage) -> Result<KeySwapContext, KeyDeco
|
||||
r_data: Box::new(ARdata::from(Ipv4Addr::from([127,0,0,1])))
|
||||
};
|
||||
|
||||
let server_public_domain = append_base_domain_to_key(
|
||||
trim_public_key(&server_public),
|
||||
&base_domain
|
||||
);
|
||||
// for the public key question that the client sent, respond with the server's public key
|
||||
let second_record = ResourceRecord {
|
||||
name_offset: 12 + (&message.questions[0]).to_bytes().len() as u16,
|
||||
answer_type: QType::CNAME,
|
||||
class: QClass::Internet,
|
||||
ttl: 0,
|
||||
rd_length: 4,
|
||||
rd_length: encode_domain_name(&server_public_domain).len() as u16,
|
||||
r_data: Box::new(
|
||||
CnameRdata::from(
|
||||
append_base_domain_to_key(
|
||||
trim_public_key(&server_public),
|
||||
&base_domain
|
||||
)
|
||||
server_public_domain
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
response.header.answer_record_count = 2;
|
||||
response.answer_records = vec![
|
||||
first_record, second_record
|
||||
];
|
||||
@ -109,4 +124,4 @@ pub fn decode_key_request(message: DNSMessage) -> Result<KeySwapContext, KeyDeco
|
||||
{
|
||||
return Err(KeyDecodeError::QuestionCount(message.questions.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, mpsc, Mutex};
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::thread;
|
||||
@ -6,16 +6,19 @@ use log::{error, info};
|
||||
use crate::clients::Clients;
|
||||
use crate::config::DomainConfig;
|
||||
|
||||
use crate::message::DNSMessage;
|
||||
use crate::net::{NetworkMessage, NetworkMessagePtr};
|
||||
use crate::message::{DNSMessage, QType};
|
||||
use crate::net::{NetworkMessagePtr};
|
||||
use crate::message_parser::parse_message;
|
||||
use crate::processor::print_error;
|
||||
use crate::processor::request::encryption::{decode_key_request, KeyDecodeError};
|
||||
use crate::{RequestError, send_message};
|
||||
|
||||
pub mod encryption;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod upload;
|
||||
mod download;
|
||||
|
||||
pub struct RequestProcesor {
|
||||
message_channel: Option<Sender<NetworkMessagePtr>>,
|
||||
@ -37,6 +40,7 @@ impl RequestProcesor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Spin a thread to process parsed DNS requests and respond as appropriate
|
||||
pub fn run(&mut self, sending_channel: Sender<NetworkMessagePtr>)
|
||||
{
|
||||
let (tx, rx): (Sender<NetworkMessagePtr>, Receiver<NetworkMessagePtr>) = mpsc::channel();
|
||||
@ -50,8 +54,6 @@ impl RequestProcesor {
|
||||
|
||||
thread::spawn(move || {
|
||||
|
||||
let clients = clients;
|
||||
|
||||
for m in rx
|
||||
{
|
||||
let peer = m.peer.clone();
|
||||
@ -60,64 +62,15 @@ impl RequestProcesor {
|
||||
Ok(r) => {
|
||||
info!("received dns message: {:?}", r);
|
||||
|
||||
// If there is a question containing the protocol base domain, treat it as a dnstp request
|
||||
// (handshake, upload, download) and handle as such
|
||||
if r.questions.iter().any(|q| q.qname.ends_with(&base_domain_equality))
|
||||
{
|
||||
if r.questions[0].qname.eq_ignore_ascii_case(&fq_key_endpoint)
|
||||
{
|
||||
info!("[{}] received encryption key request", peer);
|
||||
|
||||
match decode_key_request(r)
|
||||
{
|
||||
Ok(context) => {
|
||||
|
||||
clients.lock().unwrap().add(context.client_public, context.new_client);
|
||||
|
||||
sending_channel.send(Box::new(
|
||||
NetworkMessage {
|
||||
buffer: Box::new(context.response.to_bytes()),
|
||||
peer: context.response.peer
|
||||
}
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
match e {
|
||||
KeyDecodeError::QuestionCount(qc) => {
|
||||
error!("[{}] failed to parse public key, wrong question count [{}]", peer, qc);
|
||||
}
|
||||
KeyDecodeError::FirstQuestionNotA(qtype) => {
|
||||
error!("[{}] failed to parse public key, first question wasn't an A request [{}]", peer, qtype);
|
||||
}
|
||||
KeyDecodeError::SecondQuestionNotA(qtype) => {
|
||||
error!("[{}] failed to parse public key, second question wasn't an A request [{}]", peer, qtype);
|
||||
}
|
||||
KeyDecodeError::SharedSecretDerivation => {
|
||||
error!("[{}] failed to parse public key, failed to derived shared secret", peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let response = DNSMessage::a_resp_from_request(&r, |_| Ipv4Addr::from([127, 0, 0, 1]));
|
||||
|
||||
sending_channel.send(Box::new(
|
||||
NetworkMessage {
|
||||
buffer: Box::new(response.to_bytes()),
|
||||
peer: response.peer
|
||||
}
|
||||
));
|
||||
}
|
||||
Self::handle_dnstp_request(r, &sending_channel, &clients, peer, &fq_key_endpoint);
|
||||
}
|
||||
else {
|
||||
let response = DNSMessage::a_resp_from_request(&r, |_| Ipv4Addr::from([127, 0, 0, 1]));
|
||||
|
||||
sending_channel.send(Box::new(
|
||||
NetworkMessage {
|
||||
buffer: Box::new(response.to_bytes()),
|
||||
peer: response.peer
|
||||
}
|
||||
));
|
||||
else // otherwise it's for something else and reply with some dumb empty answer
|
||||
{
|
||||
send_message(DNSMessage::dumb_resp_from_request(&r), &sending_channel);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@ -130,8 +83,100 @@ impl RequestProcesor {
|
||||
});
|
||||
}
|
||||
|
||||
/// The message is trying to do some dnstp, work out whether it's hello handshaking, uplaoding or downloading data and handoff the message to that workflow
|
||||
fn handle_dnstp_request(r: DNSMessage, sending_channel: &Sender<NetworkMessagePtr>, clients: &Arc<Mutex<Clients>>, peer: SocketAddr, fq_key_endpoint: &String)
|
||||
{
|
||||
// if the first question is for the key domain (static.BLANK.TLD) it's a handshake for swapping keys
|
||||
if r.questions[0].qname.eq_ignore_ascii_case(fq_key_endpoint)
|
||||
{
|
||||
Self::handle_encryption_handshake(r, sending_channel, clients, peer);
|
||||
}
|
||||
// if we're not handshaking then the client should be known to the server
|
||||
else if clients.lock().unwrap().client_is_connected(&r.questions[0].qname) {
|
||||
info!("[{}] received request from known client", peer);
|
||||
|
||||
// for now lets deal with three questions, first one is the client id, second is the actual request, third is the nonce
|
||||
if r.questions.len() == 3
|
||||
{
|
||||
match r.questions[1].qtype {
|
||||
QType::A => {
|
||||
Self::handle_upload_request(r, sending_channel, clients, peer);
|
||||
}
|
||||
QType::CNAME => { // makes sense for a cname to return lots of text
|
||||
Self::handle_download_request(r, sending_channel, clients, peer);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Self::send_protocol_error(RequestError::WrongNumberOfQuestions, &r, &sending_channel);
|
||||
}
|
||||
}
|
||||
// otherwise return protocol error for trying to do something without handshaking
|
||||
else
|
||||
{
|
||||
Self::send_protocol_error(RequestError::NoHandshake, &r, &sending_channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a hello message from a new client with a public key and send the response.
|
||||
///
|
||||
/// Generate a key pair and repspond with the public key, generate the shared secret and store this in the connected clients.
|
||||
fn handle_encryption_handshake(r: DNSMessage, sending_channel: &Sender<NetworkMessagePtr>, clients: &Arc<Mutex<Clients>>, peer: SocketAddr)
|
||||
{
|
||||
info!("[{}] received encryption key request", peer);
|
||||
|
||||
// crypto bulk happens in decode, result includes message to be responded with
|
||||
match decode_key_request(&r)
|
||||
{
|
||||
Ok(context) => {
|
||||
clients.lock().unwrap().add(context.client_public, context.new_client);
|
||||
|
||||
send_message(context.response, &sending_channel);
|
||||
}
|
||||
Err(e) => {
|
||||
match e {
|
||||
KeyDecodeError::QuestionCount(qc) => {
|
||||
error!("[{}] failed to parse public key, wrong question count [{}]", peer, qc);
|
||||
}
|
||||
KeyDecodeError::FirstQuestionNotA(qtype) => {
|
||||
error!("[{}] failed to parse public key, first question wasn't an A request [{}]", peer, qtype);
|
||||
}
|
||||
KeyDecodeError::SecondQuestionNotA(qtype) => {
|
||||
error!("[{}] failed to parse public key, second question wasn't an A request [{}]", peer, qtype);
|
||||
}
|
||||
KeyDecodeError::SharedSecretDerivation => {
|
||||
error!("[{}] failed to parse public key, failed to derived shared secret", peer);
|
||||
}
|
||||
}
|
||||
|
||||
Self::send_protocol_error(RequestError::CryptoFailure, &r, &sending_channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_download_request(r: DNSMessage, sending_channel: &Sender<NetworkMessagePtr>, clients: &Arc<Mutex<Clients>>, peer: SocketAddr)
|
||||
{
|
||||
info!("[{}] received download request", peer);
|
||||
let client_id = &r.questions[0].qname;
|
||||
clients.lock().unwrap().bump_last_seen(client_id);
|
||||
}
|
||||
|
||||
fn handle_upload_request(r: DNSMessage, sending_channel: &Sender<NetworkMessagePtr>, clients: &Arc<Mutex<Clients>>, peer: SocketAddr)
|
||||
{
|
||||
info!("[{}] received upload request", peer);
|
||||
let client_id = &r.questions[0].qname;
|
||||
clients.lock().unwrap().bump_last_seen(client_id);
|
||||
}
|
||||
|
||||
pub fn get_message_channel(&mut self) -> Option<Sender<NetworkMessagePtr>>
|
||||
{
|
||||
self.message_channel.clone()
|
||||
}
|
||||
|
||||
pub fn send_protocol_error(error_code: RequestError, r: &DNSMessage, sending_channel: &Sender<NetworkMessagePtr>)
|
||||
{
|
||||
send_message(DNSMessage::protocol_error_from_request(&r, error_code), sending_channel);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::crypto::{fatten_public_key, get_random_asym_pair, trim_public_key};
|
||||
use crate::string::encode_domain_name;
|
||||
use super::*;
|
||||
use super::encryption::*;
|
||||
|
0
dnstp/src/processor/request/upload.rs
Normal file
0
dnstp/src/processor/request/upload.rs
Normal file
38
dnstp/src/processor/response/encryption.rs
Normal file
38
dnstp/src/processor/response/encryption.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::client_crypto_context::ClientCryptoContext;
|
||||
use crate::crypto::{asym_to_sym_key, get_shared_asym_secret};
|
||||
use crate::message::DNSMessage;
|
||||
use crate::processor::request::encryption::get_fattened_public_key;
|
||||
use crate::string::decode_domain_name;
|
||||
|
||||
pub fn decode_key_response(message: &DNSMessage, client_crypto_context: Arc<Mutex<ClientCryptoContext>>)
|
||||
{
|
||||
if message.answer_records.len() == 2 {
|
||||
// if message.questions[0].qtype != QType::A
|
||||
// {
|
||||
// return Err(KeyDecodeError::FirstQuestionNotA(message.questions[0].qtype));
|
||||
// }
|
||||
|
||||
let key_answer = &message.answer_records[1];
|
||||
|
||||
// if key_answer.answer_type != QType::A
|
||||
// {
|
||||
// return Err(KeyDecodeError::SecondQuestionNotA(key_answer.answer_type));
|
||||
// }
|
||||
|
||||
let data_string = decode_domain_name(key_answer.r_data.to_bytes());
|
||||
// key is transmitted wihout --- BEGIN KEY -- header and trailer bits and with '.' instead of new lines
|
||||
let (fattened_public_key, _) = get_fattened_public_key(&data_string);
|
||||
|
||||
let mut context = client_crypto_context.lock().unwrap();
|
||||
|
||||
match get_shared_asym_secret(&context.client_private, &fattened_public_key)
|
||||
{
|
||||
Ok(k) => {
|
||||
context.server_public = Some(fattened_public_key);
|
||||
context.shared_key = Some(asym_to_sym_key(&k));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,25 @@
|
||||
use std::sync::mpsc;
|
||||
mod encryption;
|
||||
|
||||
use std::sync::{Arc, mpsc, Mutex};
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::thread;
|
||||
use log::info;
|
||||
use crate::client_crypto_context::ClientCryptoContext;
|
||||
use crate::net::raw_request::NetworkMessagePtr;
|
||||
use crate::message_parser::parse_message;
|
||||
use crate::processor::print_error;
|
||||
use crate::processor::response::encryption::decode_key_response;
|
||||
|
||||
pub struct ResponseProcesor {
|
||||
message_channel: Option<Sender<NetworkMessagePtr>>
|
||||
message_channel: Option<Sender<NetworkMessagePtr>>,
|
||||
crypto_context: Arc<Mutex<ClientCryptoContext>>
|
||||
}
|
||||
|
||||
impl ResponseProcesor {
|
||||
pub fn new() -> ResponseProcesor {
|
||||
pub fn new(crypto_context: Arc<Mutex<ClientCryptoContext>>) -> ResponseProcesor {
|
||||
ResponseProcesor{
|
||||
message_channel: None
|
||||
message_channel: None,
|
||||
crypto_context
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +28,8 @@ impl ResponseProcesor {
|
||||
let (tx, rx): (Sender<NetworkMessagePtr>, Receiver<NetworkMessagePtr>) = mpsc::channel();
|
||||
self.message_channel = Some(tx);
|
||||
|
||||
let crypto_context = self.crypto_context.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
|
||||
for m in rx
|
||||
@ -31,6 +39,8 @@ impl ResponseProcesor {
|
||||
match parse_message(*m) {
|
||||
Ok(r) => {
|
||||
info!("received dns message: {:?}", r);
|
||||
|
||||
decode_key_response(&r, crypto_context.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
print_error(e, &peer);
|
||||
|
@ -1,45 +0,0 @@
|
||||
//! Utility functions for manipulating strings
|
||||
|
||||
use urlencoding::encode;
|
||||
|
||||
pub fn encode_domain_name(name: &String) -> Vec<u8>
|
||||
{
|
||||
let mut ret: Vec<u8> = Vec::with_capacity(name.len() + 3);
|
||||
|
||||
for part in name.split(".")
|
||||
{
|
||||
let encoded_string = encode(part);
|
||||
let count = encoded_string.len();
|
||||
|
||||
ret.push(count as u8);
|
||||
for x in encoded_string.bytes() {
|
||||
ret.push(x);
|
||||
};
|
||||
}
|
||||
|
||||
ret.push(0);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn strip_base_domain_from_key(public_key: &String) -> (String, String)
|
||||
{
|
||||
let periods: Vec<_> = public_key.rmatch_indices(".").collect();
|
||||
|
||||
if periods.len() >= 2 {
|
||||
(public_key[0 .. periods[1].0].to_string(),
|
||||
public_key[periods[1].0 .. ].to_string())
|
||||
}
|
||||
else if periods.len() == 1 {
|
||||
(public_key[0 .. periods[0].0].to_string(),
|
||||
public_key[periods[0].0 .. ].to_string())
|
||||
}
|
||||
else {
|
||||
(public_key.to_string(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_base_domain_to_key(trimmed_key: String, base_domain: &String) -> String
|
||||
{
|
||||
vec![trimmed_key, base_domain.to_string()].join(".")
|
||||
}
|
89
dnstp/src/string/mod.rs
Normal file
89
dnstp/src/string/mod.rs
Normal file
@ -0,0 +1,89 @@
|
||||
//! Utility functions for manipulating strings
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use urlencoding::{decode, encode};
|
||||
|
||||
pub fn encode_domain_name(name: &String) -> Vec<u8>
|
||||
{
|
||||
let mut ret: Vec<u8> = Vec::with_capacity(name.len() + 3);
|
||||
|
||||
for part in name.split(".")
|
||||
{
|
||||
let encoded_string = encode(part);
|
||||
let count = encoded_string.len();
|
||||
|
||||
ret.push(count as u8);
|
||||
for x in encoded_string.bytes() {
|
||||
ret.push(x);
|
||||
};
|
||||
}
|
||||
|
||||
ret.push(0);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn decode_domain_name(name: Vec<u8>) -> String
|
||||
{
|
||||
let mut full_domain: String = String::new();
|
||||
let mut current_query: Vec<u8> = Vec::with_capacity(10);
|
||||
let mut current_length: Option<u8> = None;
|
||||
let mut remaining_length: u8 = 0;
|
||||
|
||||
for char in name {
|
||||
|
||||
match current_length {
|
||||
None => {
|
||||
current_length = Some(char);
|
||||
remaining_length = char;
|
||||
}
|
||||
Some(_) => {
|
||||
if remaining_length == 0 {
|
||||
|
||||
let current_part = String::from_utf8(current_query.clone()).unwrap();
|
||||
let url_decoded = decode(current_part.as_str()).unwrap();
|
||||
|
||||
full_domain.push_str(&url_decoded.to_string());
|
||||
|
||||
if char != 0 {
|
||||
full_domain.push('.');
|
||||
}
|
||||
|
||||
current_query.clear();
|
||||
current_length = Some(char);
|
||||
remaining_length = char;
|
||||
}
|
||||
else {
|
||||
current_query.push(char);
|
||||
remaining_length -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
full_domain
|
||||
}
|
||||
|
||||
pub fn strip_base_domain_from_key(public_key: &String) -> (String, String)
|
||||
{
|
||||
let periods: Vec<_> = public_key.rmatch_indices(".").collect();
|
||||
|
||||
if periods.len() >= 2 {
|
||||
(public_key[0 .. periods[1].0].to_string(),
|
||||
public_key[periods[1].0 .. ].to_string())
|
||||
}
|
||||
else if periods.len() == 1 {
|
||||
(public_key[0 .. periods[0].0].to_string(),
|
||||
public_key[periods[0].0 .. ].to_string())
|
||||
}
|
||||
else {
|
||||
(public_key.to_string(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_base_domain_to_key(trimmed_key: String, base_domain: &String) -> String
|
||||
{
|
||||
vec![trimmed_key, base_domain.to_string()].join(".")
|
||||
}
|
35
dnstp/src/string/tests.rs
Normal file
35
dnstp/src/string/tests.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_encode()
|
||||
{
|
||||
let payload = "google.com";
|
||||
|
||||
let encoded = encode_domain_name(&payload.to_string());
|
||||
|
||||
assert_eq!(encoded.len(), "google".len() + "com".len() + 1 + 1 + 1);
|
||||
assert_eq!(encoded[0], "google".len() as u8);
|
||||
// assert_eq!(encoded["google".len()], "com".len() as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode()
|
||||
{
|
||||
let payload = "google.com";
|
||||
|
||||
let encoded = encode_domain_name(&payload.to_string());
|
||||
let decoded = decode_domain_name(encoded);
|
||||
|
||||
assert_eq!(payload, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_two()
|
||||
{
|
||||
let payload = "sub.domain.com";
|
||||
|
||||
let encoded = encode_domain_name(&payload.to_string());
|
||||
let decoded = decode_domain_name(encoded);
|
||||
|
||||
assert_eq!(payload, decoded);
|
||||
}
|
@ -45,15 +45,19 @@ fn test_key_swap()
|
||||
// SERVER
|
||||
/////////////////
|
||||
|
||||
let question_count = message.questions.len();
|
||||
// handle message "received by client"
|
||||
let resp = decode_key_request(message).unwrap();
|
||||
let resp = decode_key_request(&message).unwrap();
|
||||
|
||||
assert_eq!(question_count, resp.response.questions.len());
|
||||
assert_eq!(question_count, resp.response.answer_records.len());
|
||||
|
||||
////////////
|
||||
// CLIENT
|
||||
////////////
|
||||
|
||||
// client has received message from above and constructs shared secret
|
||||
let shared_secret_client = asym_to_sym_key(&get_shared_asym_secret(client_private, resp.server_public).unwrap());
|
||||
let shared_secret_client = asym_to_sym_key(&get_shared_asym_secret(&client_private, &resp.server_public).unwrap());
|
||||
|
||||
///////////////////////////////
|
||||
// TEST ENCRYPTION/DECRYPTION
|
||||
|
Loading…
Reference in New Issue
Block a user