Skip to content

Commit

Permalink
feat: basic challange implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
evlli committed Nov 13, 2023
1 parent e08488d commit 84ac0a2
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 53 deletions.
31 changes: 31 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, long_about = None)]
pub (crate) struct CliArgs {
#[arg(short, long)]
pub config: std::path::PathBuf,

#[arg(short, long)]
pub identifier: hickory_client::rr::Name,

#[arg(short, long)]
pub proof: String,

#[arg(short, long)]
pub debug: Option<log::Level>,

#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
/// sets the challenge record
Set,
/// removes the challenge record
Cleanup,
}

pub(crate) fn parse() -> CliArgs {
CliArgs::parse()
}
33 changes: 24 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
use serde::{Deserialize, Deserializer};
use serde_with::{serde_as, MapPreventDuplicates, DisplayFromStr, base64::Base64};
use anyhow::{Context, Result};
use hickory_client::client::SyncClient;
use hickory_client::rr::rdata::tsig::TsigAlgorithm;
use hickory_client::rr::Name;
use hickory_client::udp::UdpClientConnection;
use hickory_proto::rr::dnssec::tsig::TSigner;
use serde::{Deserialize, Deserializer};
use serde_with::{base64::Base64, serde_as, DisplayFromStr, MapPreventDuplicates};
use std::collections::HashMap;
use std::str::FromStr;
use trust_dns_client::rr::Name;
use trust_dns_client::rr::rdata::tsig::{TsigAlgorithm};

#[serde_as]
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
pub(crate) struct Config {
pub(crate) resolver: std::net::SocketAddr,
#[serde_as(as = "MapPreventDuplicates<DisplayFromStr, _>")]
pub(crate) zones: HashMap<Name, ZoneConfig>,
}

#[serde_as]
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
pub(crate) struct ZoneConfig {
pub(crate) primary_ns: std::net::SocketAddr,
#[serde_as(as = "DisplayFromStr")]
Expand All @@ -26,15 +29,27 @@ pub(crate) struct ZoneConfig {
pub(crate) tsig_algorithm: TsigAlgorithm,
}

impl ZoneConfig {
pub fn create_client(&self) -> SyncClient<UdpClientConnection> {
let signer = TSigner::new(
self.tsig_key.clone(),
self.tsig_algorithm.clone(),
self.tsig_name.clone(),
300,
)
.unwrap();
let conn = UdpClientConnection::new(self.primary_ns.clone()).unwrap();
SyncClient::with_tsigner(conn, signer)
}
}

pub(crate) fn read_config(path: &std::path::Path) -> Result<Config> {
// TODO: error message on config not found
let config_file_content = std::fs::read_to_string(path).context("Couldn't read config file")?;
toml::from_str(&config_file_content).context("Couldn't parse config file")
}

fn deserialize_tsig_algorithm<'de, D>(
deserializer: D
) -> Result<TsigAlgorithm, D::Error>
fn deserialize_tsig_algorithm<'de, D>(deserializer: D) -> Result<TsigAlgorithm, D::Error>
where
D: Deserializer<'de>,
{
Expand Down
38 changes: 38 additions & 0 deletions src/happy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::str::FromStr;

use hickory_client::{
client::{Client, SyncClient},
error::ClientResult,
op::DnsResponse,
rr::{
Name,
RecordType,
DNSClass
},
udp::UdpClientConnection,
};

/*
TODO:
This needs to be TCP and have Timeout Retries
*/
pub fn search(
identifier: Name,
rtype: RecordType,
) -> anyhow::Result<DnsResponse> {
debug!("searching {:?} for {}", rtype, identifier);
let socket_addr = std::net::SocketAddr::new(std::net::IpAddr::from_str("1.1.1.1")?, 53);
let client = happy_eyeballs::connect(socket_addr)?;
let questionable_addr = client.peer_addr()?;
let conn = UdpClientConnection::new(socket_addr).unwrap();
let client: SyncClient<UdpClientConnection> = SyncClient::new(conn);

let q: ClientResult<DnsResponse> = client.query(&identifier, DNSClass::IN, rtype);
let response = match q {
Ok(response) => response,
Err(e) => panic!("{}", e),
};
trace!("found {:?}", response);
Ok(response)
}
163 changes: 119 additions & 44 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,136 @@
use anyhow::{Context};
use clap::Parser;

use crate::config::read_config;
use anyhow::Context;
use hickory_proto::xfer::DnsResponse;

use std::str::FromStr;
use time::Duration;
use trust_dns_client::client::{Client, SyncClient};
use trust_dns_client::udp::UdpClientConnection;
use trust_dns_client::op::DnsResponse;
use trust_dns_client::rr::dnssec::tsig::{TSigner};
use trust_dns_client::rr::rdata::{TXT};
use trust_dns_client::rr::{Name, RData, Record, RecordType};
use crate::config::{read_config};

mod config;
use hickory_client::{
client::Client,
op::ResponseCode,
rr::{Name, RData, Record, RecordType, rdata::TXT},
};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(long)]
config: std::path::PathBuf,
#[arg(long)]
identifier: String,
#[arg(long)]
proof: String,
}
use log::{debug, info, trace};
extern crate pretty_env_logger;
#[macro_use]
extern crate log;

mod happy;
mod config;
mod cli;

fn main() -> anyhow::Result<()> {
let cli = Cli::parse();

pretty_env_logger::init_timed();

let cli = cli::parse();

let config = read_config(&cli.config)?;
let mut record = find_record(&cli.identifier)?;
let origin = record.name().clone(); // TODO: find out zone via SOA
debug!("initializing with zones {:?}", config.zones.keys());

let zone_config = &config.zones.get(record.name()).unwrap(); // TODO: Handle None
let tsigner = TSigner::new(zone_config.tsig_key.clone(),
zone_config.tsig_algorithm.clone(),
zone_config.tsig_name.clone(), 300).unwrap();
let challenge_name: Name = find_record(cli.identifier)?;
info!("challenge name is {}", challenge_name);

let conn = UdpClientConnection::new(zone_config.primary_ns).unwrap();
let client = SyncClient::with_tsigner(conn, tsigner);
let zone: Name = find_zone(challenge_name.clone())?;
info!("challenge zone is {}", zone);

record.set_data(Some(RData::TXT(TXT::new(Vec::from([cli.proof])))));
let result = client.create(record, origin).context("Couldn't set record")?;
let primary_ns_client = &config.zones.get(&zone)
.context("Couldn't find challenge zone in config")?
.create_client();

Ok(())
let challenge_record = Record::from_rdata(
challenge_name.clone(),
Duration::minutes(1).whole_seconds() as u32,
RData::TXT(
TXT::new(
Vec::from([cli.proof])
)
),
);

match &cli.command.unwrap() {
cli::Commands::Set => {
let result = primary_ns_client
.create(challenge_record.clone(), zone)
.context("Couldn't set record")?;
match result.response_code() {
ResponseCode::NoError => {
info!("record was successfully set");
Ok(())
},
ResponseCode::YXRRSet => {
warn!("{:?} already exists", challenge_record.clone());
Ok(())
},
_ => todo!()
}
},
cli::Commands::Cleanup => {
let result = primary_ns_client
.delete_rrset(challenge_record.clone(), zone)
.context("Couldn't remove record")?;
match result.response_code() {
ResponseCode::NoError => {
info!("record was removed");
Ok(())
},
_ => {
debug!("{:?}",result);
Ok(())
},
}
}
}
}

fn find_record(identifier: &String) -> anyhow::Result<Record> {
let name = Name::from_str(&("_acme-challenge.".to_owned() + identifier)).context("Invalid input for identifier")?;
fn find_record(identifier: Name) -> anyhow::Result<Name> {
let prefix: Name = Name::from_str("_acme-challenge").unwrap();
let q = happy::search(
prefix.append_name(&identifier)?, // this should be called prepend because that's what it actualyl does
RecordType::TXT,
)?;
let last_record = q.answers().last().unwrap().clone().into_parts();
let challenge_name = match last_record.rdata.unwrap() {
RData::CNAME(name) => name.0,
RData::TXT(_) => {
info!(
"found existing TXT record for {:?}",
last_record.name_labels
);
last_record.name_labels
}
_ => panic!("unexpected record response"),
};
Ok(challenge_name)
}

// TODO: traverse CNAMEs, find last CNAME
// let address = "1.1.1.1:53".parse().unwrap();
// let conn = UdpClientConnection::new(address).unwrap();
// let client = SyncClient::new(conn);
// let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::TXT).unwrap();
// let answers: &[Record] = response.answers();
// println!("answer: {:?}", answers);
fn find_zone(identifier: Name) -> anyhow::Result<Name> {

return Ok(Record::with(name,
RecordType::TXT,
Duration::minutes(1).whole_seconds() as u32));
fn from_name_labels(response: DnsResponse) -> Option<Name> {
response
.name_servers()
.iter()
.map(|record| record.clone().into_parts().name_labels)
.last()
}

let q = happy::search(identifier.clone(), RecordType::SOA)?;
let name = match q.response_code() {
ResponseCode::NoError => from_name_labels(q),
ResponseCode::NXDomain => from_name_labels(q),
/*
ResponseCode::NoError => {
let cut_id = identifier.base_name();
trace!("new search id {:?}", cut_id);
Some(find_zone(cut_id)?)
} */
_ => {
error!("unhandled response type, body was: {:?}", q);
None
}
}
.unwrap();
Ok(name)
}

0 comments on commit 84ac0a2

Please sign in to comment.