use crate::config::PluginConfig;
use crate::dns::{Message, RData, ResourceRecord};
use crate::plugin::{Context, ExecPlugin, Plugin};
use crate::{RegisterExecPlugin, RegisterPlugin, Result};
use async_trait::async_trait;
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
const PLUGIN_BLACKHOLE_IDENTIFIER: &str = "blackhole";
#[derive(RegisterPlugin, RegisterExecPlugin)]
pub struct BlackholePlugin {
ipv4: Vec<Ipv4Addr>,
ipv6: Vec<Ipv6Addr>,
}
impl BlackholePlugin {
pub fn new_from_strs<I, S>(ips: I) -> Result<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut ipv4 = Vec::new();
let mut ipv6 = Vec::new();
for s in ips {
let s = s.as_ref();
if let Ok(a4) = s.parse::<Ipv4Addr>() {
ipv4.push(a4);
} else if let Ok(a6) = s.parse::<Ipv6Addr>() {
ipv6.push(a6);
} else {
return Err(crate::Error::Other(format!("invalid ip: {}", s)));
}
}
Ok(Self { ipv4, ipv6 })
}
fn make_response_for_a(&self, req: &Message) -> Option<Message> {
if req.question_count() != 1 || self.ipv4.is_empty() {
return None;
}
let q = &req.questions()[0];
if q.qtype() != crate::dns::types::RecordType::A {
return None;
}
let mut r = Message::new();
r.set_id(req.id());
r.set_response(true);
r.add_question(q.clone());
for ip in &self.ipv4 {
r.add_answer(ResourceRecord::new(
q.qname().to_string(),
crate::dns::types::RecordType::A,
crate::dns::types::RecordClass::IN,
300,
RData::A(*ip),
));
}
Some(r)
}
fn make_response_for_aaaa(&self, req: &Message) -> Option<Message> {
if req.question_count() != 1 || self.ipv6.is_empty() {
return None;
}
let q = &req.questions()[0];
if q.qtype() != crate::dns::types::RecordType::AAAA {
return None;
}
let mut r = Message::new();
r.set_id(req.id());
r.set_response(true);
r.add_question(q.clone());
for ip in &self.ipv6 {
r.add_answer(ResourceRecord::new(
q.qname().to_string(),
crate::dns::types::RecordType::AAAA,
crate::dns::types::RecordClass::IN,
300,
RData::AAAA(*ip),
));
}
Some(r)
}
}
impl fmt::Debug for BlackholePlugin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BlackHolePlugin")
.field("ipv4_count", &self.ipv4.len())
.field("ipv6_count", &self.ipv6.len())
.finish()
}
}
#[async_trait]
impl Plugin for BlackholePlugin {
fn name(&self) -> &str {
PLUGIN_BLACKHOLE_IDENTIFIER
}
async fn execute(&self, ctx: &mut Context) -> Result<()> {
let req = ctx.request();
if let Some(resp) = self
.make_response_for_a(req)
.or_else(|| self.make_response_for_aaaa(req))
{
ctx.set_response(Some(resp));
}
Ok(())
}
fn init(config: &PluginConfig) -> Result<Arc<dyn Plugin>> {
let args = config.effective_args();
use serde_yaml::Value;
let ips: Vec<String> = if let Some(Value::Sequence(seq)) = args.get("ips") {
seq.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
} else {
Vec::new() };
let plugin = BlackholePlugin::new_from_strs(ips)?;
Ok(Arc::new(plugin))
}
fn aliases() -> &'static [&'static str] {
&["sinkhole", "black_hole", "null_dns"]
}
}
impl ExecPlugin for BlackholePlugin {
fn quick_setup(prefix: &str, exec_str: &str) -> Result<Arc<dyn Plugin>> {
if prefix != PLUGIN_BLACKHOLE_IDENTIFIER && !Self::aliases().contains(&prefix) {
return Err(crate::Error::Config(format!(
"ExecPlugin quick_setup: unsupported prefix '{}', expected one of {:?}",
prefix,
Self::aliases()
)));
}
let ips: Vec<String> = exec_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let plugin = BlackholePlugin::new_from_strs(ips)?;
Ok(Arc::new(plugin))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dns::types::{RecordClass, RecordType};
#[tokio::test]
async fn test_black_hole_a() {
let plugin = BlackholePlugin::new_from_strs(["192.0.2.1"]).unwrap();
let mut req = Message::new();
req.add_question(crate::dns::question::Question::new(
"example.com".to_string(),
RecordType::A,
RecordClass::IN,
));
let mut ctx = Context::new(req);
plugin.execute(&mut ctx).await.unwrap();
let resp = ctx.response().unwrap();
assert_eq!(resp.answer_count(), 1);
if let RData::A(ip) = resp.answers()[0].rdata() {
assert_eq!(*ip, Ipv4Addr::new(192, 0, 2, 1));
} else {
panic!("expected A");
}
}
#[test]
fn test_exec_plugin_quick_setup() {
let plugin =
<BlackholePlugin as ExecPlugin>::quick_setup("blackhole", "192.0.2.1").unwrap();
assert_eq!(plugin.name(), "blackhole");
let result = <BlackholePlugin as ExecPlugin>::quick_setup("invalid", "192.0.2.1");
assert!(result.is_err());
let plugin =
<BlackholePlugin as ExecPlugin>::quick_setup("blackhole", "192.0.2.1,2001:db8::1")
.unwrap();
assert_eq!(plugin.name(), "blackhole");
}
}