Skip to content

Commit c3286eb

Browse files
authored
feat: add Hickory DNS server module (#472)
1 parent a529e6c commit c3286eb

3 files changed

Lines changed: 208 additions & 2 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ fakecloud = []
4040
gitea = ["http_wait", "dep:rcgen"]
4141
google_cloud_sdk_emulators = []
4242
hashicorp_vault = ["http_wait"]
43+
hickory_dns = []
4344
k3s = []
4445
kafka = []
4546
localstack = []
@@ -98,6 +99,7 @@ aws-sdk-s3 = "1.2.0"
9899
aws-sdk-sqs = "1.2.0"
99100
aws-types = "1.0.1"
100101
databend-driver = "0.28.2"
102+
hickory-resolver = { version = "0.26", features = ["tokio"] }
101103
futures = "0.3"
102104
lapin = "3.0.0"
103105
ldap3 = "0.11.5"

src/hickory_dns/mod.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
use std::borrow::Cow;
2+
3+
use testcontainers::{
4+
core::{ContainerPort, WaitFor},
5+
CopyDataSource, CopyToContainer, Image,
6+
};
7+
8+
const NAME: &str = "hickorydns/hickory-dns";
9+
const TAG: &str = "0.26.0";
10+
11+
const CONFIG_PATH: &str = "/etc/named.toml";
12+
const ZONE_DIR: &str = "/var/named";
13+
14+
/// Module to work with a [`Hickory DNS`] server inside of tests.
15+
///
16+
/// Based on the official [`Hickory DNS docker image`].
17+
///
18+
/// # Example
19+
/// ```
20+
/// use testcontainers_modules::{hickory_dns::HickoryDns, testcontainers::runners::SyncRunner};
21+
///
22+
/// const CONFIG: &str = r#"
23+
/// [[zones]]
24+
/// file = "example.com.zone"
25+
/// zone = "example.com"
26+
/// zone_type = "Primary"
27+
/// "#;
28+
///
29+
/// const ZONE: &str = r#"
30+
/// @ 3600 IN SOA ns.example.com. admin.example.com. (
31+
/// 1 ; SerialNA
32+
/// 1h ; Refresh
33+
/// 10m ; Retry
34+
/// 10d ; Expire
35+
/// 10h ; Negative Caching TTL
36+
/// )
37+
///
38+
/// simple IN A 10.0.0.1
39+
/// "#;
40+
///
41+
/// let instance = HickoryDns::new(CONFIG.as_bytes().to_vec())
42+
/// .with_zone("example.com.zone", ZONE.as_bytes().to_vec())
43+
/// .start()
44+
/// .unwrap();
45+
///
46+
/// let port = instance
47+
/// .get_host_port_ipv4(HickoryDns::INTERNAL_PORT)
48+
/// .unwrap();
49+
///
50+
/// let dig_str = format!("dig @127.0.0.1 -p {port} simple.example.com.");
51+
/// ```
52+
///
53+
/// [`Hickory DNS`]: https://github.com/hickory-dns/hickory-dns
54+
/// [`Hickory DNS docker image`]: https://hub.docker.com/r/hickorydns/hickory-dns
55+
#[derive(Debug, Clone)]
56+
pub struct HickoryDns {
57+
files: Vec<CopyToContainer>,
58+
}
59+
60+
impl HickoryDns {
61+
/// Internal port for TCP and UDP connections.
62+
pub const INTERNAL_PORT: u16 = 53;
63+
64+
/// # Arguments
65+
///
66+
/// - `config`: Server config file to be placed in `/etc/named.toml`. Futher
67+
/// example configurations are provided in the project's [`test_configs`]
68+
/// directory.
69+
///
70+
/// [`test_configs`]: https://github.com/hickory-dns/hickory-dns/tree/main/tests/test-data/test_configs
71+
pub fn new(config: impl Into<CopyDataSource>) -> Self {
72+
let config_file = CopyToContainer::new(config.into(), CONFIG_PATH);
73+
74+
Self {
75+
files: vec![config_file],
76+
}
77+
}
78+
79+
/// # Arguments
80+
///
81+
/// - `filename`: Referenced from the config file for the corresponding zone.
82+
/// - `description`: Zone file description as described in [RFC 1034 (section 3.6.1)][RFC1034] and [RFC 1035 (section 5)][RFC1035]
83+
///
84+
/// [RFC1034]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.1
85+
/// [RFC1035]: https://datatracker.ietf.org/doc/html/rfc1035#section-5
86+
pub fn with_zone(
87+
mut self,
88+
filename: impl Into<Cow<'static, str>>,
89+
description: impl Into<CopyDataSource>,
90+
) -> Self {
91+
let target = format!("{ZONE_DIR}/{}", filename.into());
92+
let zone_file = CopyToContainer::new(description, target);
93+
self.files.push(zone_file);
94+
95+
self
96+
}
97+
}
98+
99+
impl Image for HickoryDns {
100+
fn name(&self) -> &str {
101+
NAME
102+
}
103+
104+
fn tag(&self) -> &str {
105+
TAG
106+
}
107+
108+
fn ready_conditions(&self) -> Vec<WaitFor> {
109+
vec![WaitFor::message_on_stdout(
110+
"server starting up, awaiting connections...",
111+
)]
112+
}
113+
114+
fn expose_ports(&self) -> &[testcontainers::core::ContainerPort] {
115+
&[
116+
ContainerPort::Tcp(Self::INTERNAL_PORT),
117+
ContainerPort::Udp(Self::INTERNAL_PORT),
118+
]
119+
}
120+
121+
fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
122+
&self.files
123+
}
124+
}
125+
126+
#[cfg(test)]
127+
mod tests {
128+
use std::net::{IpAddr, Ipv4Addr};
129+
130+
use hickory_resolver::{
131+
config::{ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig},
132+
net::runtime::TokioRuntimeProvider,
133+
};
134+
use testcontainers::runners::AsyncRunner;
135+
136+
use super::*;
137+
138+
const CONFIG: &str = r#"
139+
[[zones]]
140+
file = "example.com.zone"
141+
zone = "example.com"
142+
zone_type = "Primary"
143+
"#;
144+
145+
const ZONE: &str = r#"
146+
@ 3600 IN SOA ns.example.com. admin.example.com. (
147+
1 ; SerialNA
148+
1h ; Refresh
149+
10m ; Retry
150+
10d ; Expire
151+
10h ; Negative Caching TTL
152+
)
153+
154+
simple IN A 10.0.0.1
155+
"#;
156+
157+
#[tokio::test]
158+
async fn a_record_query() -> Result<(), Box<dyn std::error::Error + 'static>> {
159+
let container = HickoryDns::new(CONFIG.as_bytes().to_vec())
160+
.with_zone("example.com.zone", ZONE.as_bytes().to_vec())
161+
.start()
162+
.await?;
163+
164+
let port = container
165+
.get_host_port_ipv4(HickoryDns::INTERNAL_PORT)
166+
.await?;
167+
168+
let name_server_config = {
169+
let mut tcp_connection = ConnectionConfig::new(ProtocolConfig::Tcp);
170+
tcp_connection.port = port;
171+
172+
let mut udp_connection = ConnectionConfig::new(ProtocolConfig::Udp);
173+
udp_connection.port = port;
174+
175+
NameServerConfig::new(
176+
IpAddr::V4(Ipv4Addr::LOCALHOST),
177+
true,
178+
vec![udp_connection, tcp_connection],
179+
)
180+
};
181+
182+
let resolver_config = ResolverConfig::from_parts(None, vec![], vec![name_server_config]);
183+
184+
let resolver = hickory_resolver::Resolver::builder_with_config(
185+
resolver_config,
186+
TokioRuntimeProvider::new(),
187+
)
188+
.build()?;
189+
190+
let response = resolver.ipv4_lookup("simple.example.com.").await?;
191+
192+
let actual = response.answers().first().unwrap().data.ip_addr().unwrap();
193+
194+
let expected = "10.0.0.1".parse::<IpAddr>().unwrap();
195+
196+
assert_eq!(expected, actual);
197+
198+
Ok(())
199+
}
200+
}

src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ pub mod google_cloud_sdk_emulators;
8181
#[cfg_attr(docsrs, doc(cfg(feature = "hashicorp_vault")))]
8282
/// ‎**HashiCorp Vault** (secrets management) testcontainer
8383
pub mod hashicorp_vault;
84+
#[cfg(feature = "hickory_dns")]
85+
#[cfg_attr(docsrs, doc(cfg(feature = "hickory_dns")))]
86+
/// **Hickory DNS** (DNS server) testcontainer
87+
pub mod hickory_dns;
8488
#[cfg(feature = "k3s")]
8589
#[cfg_attr(docsrs, doc(cfg(feature = "k3s")))]
8690
/// **K3s** (lightweight kubernetes) testcontainer
@@ -135,7 +139,7 @@ pub mod nats;
135139
pub mod neo4j;
136140
#[cfg(feature = "openldap")]
137141
#[cfg_attr(docsrs, doc(cfg(feature = "openldap")))]
138-
/// **Openldap** (ldap authentification) testcontainer
142+
/// **Openldap** (ldap authentication) testcontainer
139143
pub mod openldap;
140144
#[cfg(feature = "oracle")]
141145
#[cfg_attr(docsrs, doc(cfg(feature = "oracle")))]
@@ -187,7 +191,7 @@ pub mod selenium;
187191
pub mod solr;
188192
#[cfg(feature = "surrealdb")]
189193
#[cfg_attr(docsrs, doc(cfg(feature = "surrealdb")))]
190-
/// **surrealdb** (mutli model database) testcontainer
194+
/// **surrealdb** (multi model database) testcontainer
191195
pub mod surrealdb;
192196
#[cfg(feature = "trufflesuite_ganachecli")]
193197
#[cfg_attr(docsrs, doc(cfg(feature = "trufflesuite_ganachecli")))]

0 commit comments

Comments
 (0)