-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: basic challange implementation
- Loading branch information
Showing
4 changed files
with
212 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |