1use crate::types::{
2 NssAction, NssCriterion, NssSource, NssStatus, NsswitchConfig, ResolutionStep, StepResult,
3};
4use std::net::IpAddr;
5
6pub trait SourceResolver {
11 fn resolve(&mut self, source: &NssSource, name: &str) -> StepResult;
12}
13
14pub struct SimulationOutcome {
16 pub steps: Vec<ResolutionStep>,
20 pub final_addresses: Vec<IpAddr>,
23}
24
25impl SimulationOutcome {
26 pub fn resolved(&self) -> bool {
28 !self.final_addresses.is_empty()
29 }
30}
31
32pub fn simulate(
39 config: &NsswitchConfig,
40 name: &str,
41 resolver: &mut impl SourceResolver,
42) -> SimulationOutcome {
43 let mut steps = Vec::new();
44 let mut final_addresses = Vec::new();
45
46 for entry in &config.hosts {
47 let result = resolver.resolve(&entry.source, name);
48 let status = classify(&result);
49 let matched_criterion = entry.criteria.iter().find(|c| c.status == status).cloned();
50
51 let halts = match &matched_criterion {
52 Some(NssCriterion {
53 action: NssAction::Return,
54 ..
55 }) => true,
56 None => status == NssStatus::Success,
59 _ => false,
60 };
61
62 if let StepResult::Found(ref addrs) = result {
63 final_addresses = addrs.clone();
64 }
65
66 steps.push(ResolutionStep {
67 source: entry.source.clone(),
68 result,
69 halted_chain: if halts { matched_criterion } else { None },
70 });
71
72 if halts {
73 break;
74 }
75 }
76
77 SimulationOutcome {
78 steps,
79 final_addresses,
80 }
81}
82
83fn classify(result: &StepResult) -> NssStatus {
84 match result {
85 StepResult::Found(_) => NssStatus::Success,
86 StepResult::NotFound => NssStatus::NotFound,
87 StepResult::Skipped { .. } => NssStatus::Unavail,
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::types::{NssAction, NssCriterion, NssEntry, NssStatus};
95
96 struct ScriptedResolver {
98 answers: Vec<(NssSource, StepResult)>,
99 }
100
101 impl SourceResolver for ScriptedResolver {
102 fn resolve(&mut self, source: &NssSource, _name: &str) -> StepResult {
103 self.answers
104 .iter()
105 .find(|(s, _)| s == source)
106 .map(|(_, r)| r.clone())
107 .unwrap_or(StepResult::NotFound)
108 }
109 }
110
111 #[test]
112 fn notfound_return_halts_before_dns_is_ever_reached() {
113 let config = NsswitchConfig {
114 hosts: vec![
115 NssEntry {
116 source: NssSource::Files,
117 criteria: vec![],
118 },
119 NssEntry {
120 source: NssSource::Mdns4Minimal,
121 criteria: vec![NssCriterion {
122 status: NssStatus::NotFound,
123 action: NssAction::Return,
124 }],
125 },
126 NssEntry {
127 source: NssSource::Dns,
128 criteria: vec![],
129 },
130 ],
131 };
132 let mut resolver = ScriptedResolver {
133 answers: vec![
134 (NssSource::Files, StepResult::NotFound),
135 (NssSource::Mdns4Minimal, StepResult::NotFound),
136 (
137 NssSource::Dns,
138 StepResult::Found(vec!["192.168.1.50".parse().unwrap()]),
139 ),
140 ],
141 };
142
143 let outcome = simulate(&config, "api.local", &mut resolver);
144
145 assert_eq!(outcome.steps.len(), 2, "dns step must never be reached");
146 assert!(!outcome.resolved());
147 assert!(outcome.steps[1].halted_chain.is_some());
148 }
149
150 #[test]
151 fn success_without_criterion_still_halts_by_default() {
152 let config = NsswitchConfig {
153 hosts: vec![
154 NssEntry {
155 source: NssSource::Files,
156 criteria: vec![],
157 },
158 NssEntry {
159 source: NssSource::Dns,
160 criteria: vec![],
161 },
162 ],
163 };
164 let mut resolver = ScriptedResolver {
165 answers: vec![(
166 NssSource::Files,
167 StepResult::Found(vec!["127.0.0.1".parse().unwrap()]),
168 )],
169 };
170
171 let outcome = simulate(&config, "localhost", &mut resolver);
172
173 assert_eq!(outcome.steps.len(), 1);
174 assert!(outcome.resolved());
175 }
176
177 #[test]
178 fn notfound_without_criterion_falls_through_to_next_source() {
179 let config = NsswitchConfig {
183 hosts: vec![
184 NssEntry {
185 source: NssSource::Files,
186 criteria: vec![],
187 },
188 NssEntry {
189 source: NssSource::Dns,
190 criteria: vec![],
191 },
192 ],
193 };
194 let mut resolver = ScriptedResolver {
195 answers: vec![
196 (NssSource::Files, StepResult::NotFound),
197 (
198 NssSource::Dns,
199 StepResult::Found(vec!["93.184.216.34".parse().unwrap()]),
200 ),
201 ],
202 };
203
204 let outcome = simulate(&config, "example.com", &mut resolver);
205
206 assert_eq!(outcome.steps.len(), 2, "must fall through to dns");
207 assert!(outcome.resolved());
208 assert_eq!(
209 outcome.final_addresses,
210 vec!["93.184.216.34".parse::<IpAddr>().unwrap()]
211 );
212 }
213
214 #[test]
215 fn explicit_continue_action_falls_through_even_on_success() {
216 let config = NsswitchConfig {
219 hosts: vec![
220 NssEntry {
221 source: NssSource::Myhostname,
222 criteria: vec![NssCriterion {
223 status: NssStatus::Success,
224 action: NssAction::Continue,
225 }],
226 },
227 NssEntry {
228 source: NssSource::Dns,
229 criteria: vec![],
230 },
231 ],
232 };
233 let mut resolver = ScriptedResolver {
234 answers: vec![
235 (
236 NssSource::Myhostname,
237 StepResult::Found(vec!["127.0.1.1".parse().unwrap()]),
238 ),
239 (
240 NssSource::Dns,
241 StepResult::Found(vec!["10.0.0.5".parse().unwrap()]),
242 ),
243 ],
244 };
245
246 let outcome = simulate(&config, "myhost", &mut resolver);
247
248 assert_eq!(outcome.steps.len(), 2, "continue must not stop the chain");
249 assert_eq!(
251 outcome.final_addresses,
252 vec!["10.0.0.5".parse::<IpAddr>().unwrap()]
253 );
254 }
255
256 #[test]
257 fn full_chain_exhausted_nothing_found_anywhere() {
258 let config = NsswitchConfig {
259 hosts: vec![
260 NssEntry {
261 source: NssSource::Files,
262 criteria: vec![],
263 },
264 NssEntry {
265 source: NssSource::Dns,
266 criteria: vec![],
267 },
268 ],
269 };
270 let mut resolver = ScriptedResolver {
271 answers: vec![
272 (NssSource::Files, StepResult::NotFound),
273 (NssSource::Dns, StepResult::NotFound),
274 ],
275 };
276
277 let outcome = simulate(&config, "doesnotexist.invalid", &mut resolver);
278
279 assert_eq!(outcome.steps.len(), 2, "every source must be tried");
280 assert!(!outcome.resolved());
281 }
282}