1use std::net::IpAddr;
4
5use pharos_corefile::{Cursor, Error};
6
7fn interfaces() -> Vec<(String, Option<IpAddr>)> {
8 match nix::ifaddrs::getifaddrs() {
9 Ok(addrs) => addrs
10 .map(|entry| {
11 let ip = entry.address.and_then(|address| {
12 address
13 .as_sockaddr_in()
14 .map(|ip| IpAddr::V4(ip.ip()))
15 .or_else(|| address.as_sockaddr_in6().map(|ip| IpAddr::V6(ip.ip())))
16 });
17 (entry.interface_name, ip)
18 })
19 .collect(),
20 Err(error) => {
21 pharos_server::logging::log(
22 "WARNING",
23 "bind",
24 &format!("failed to get interfaces list, cannot bind by interface name: {error}"),
25 );
26 Vec::new()
27 }
28 }
29}
30
31fn expand(
32 args: &[String],
33 ifaces: &[(String, Option<IpAddr>)],
34 cursor: &Cursor<'_>,
35) -> Result<Vec<String>, Error> {
36 let mut result = Vec::new();
37 for arg in args {
38 let matches: Vec<_> = ifaces.iter().filter(|(name, _)| name == arg).collect();
39 if matches.is_empty() {
40 arg.parse::<IpAddr>().map_err(|_| {
41 cursor.error(format!("not a valid IP address or interface name: {arg}"))
42 })?;
43 result.push(arg.clone());
44 } else {
45 for (_, ip) in matches {
46 if let Some(ip) = ip {
47 let mut address = ip.to_string();
48 if let IpAddr::V6(v6) = ip
50 && (v6.is_unicast_link_local()
51 || (v6.is_multicast() && v6.segments()[0] & 0xf == 2))
52 {
53 address.push('%');
54 address.push_str(arg);
55 }
56 result.push(address);
57 }
58 }
59 }
60 }
61 Ok(result)
62}
63
64pub(crate) fn parse(cursor: &mut Cursor<'_>) -> Result<Vec<String>, Error> {
65 let ifaces = interfaces();
66 parse_with_interfaces(cursor, &ifaces)
67}
68
69fn parse_with_interfaces(
70 cursor: &mut Cursor<'_>,
71 ifaces: &[(String, Option<IpAddr>)],
72) -> Result<Vec<String>, Error> {
73 let mut all = Vec::new();
74 while cursor.next() {
75 let args = cursor.remaining_args();
76 if args.is_empty() {
77 return Err(cursor.error("at least one address or interface name is expected"));
78 }
79 let mut except = Vec::new();
80 while cursor.next_block() {
81 if cursor.val() != "except" {
82 return Err(cursor.error(format!("invalid option {:?}", cursor.val())));
83 }
84 except = cursor.remaining_args();
85 if except.is_empty() {
86 return Err(cursor.error(
87 "at least one address or interface must be given to except subdirective",
88 ));
89 }
90 }
91 let ips = expand(&args, ifaces, cursor)?;
92 let except = expand(&except, ifaces, cursor)?;
93 all.extend(ips.into_iter().filter(|ip| !except.contains(ip)));
94 }
95 Ok(all)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use pharos_corefile::Token;
102
103 #[test]
104 fn interface_expansion_preserves_link_local_scope() {
105 let tokens: Vec<Token> = vec![];
106 let cursor = Cursor::new("Corefile", &tokens);
107 let ifaces = vec![
108 ("test0".into(), Some("127.0.0.1".parse().unwrap())),
109 ("test0".into(), Some("::1".parse().unwrap())),
110 ("test0".into(), Some("fe80::1".parse().unwrap())),
111 ("test0".into(), Some("ff02::1".parse().unwrap())),
112 ("test0".into(), Some("ff05::1".parse().unwrap())),
113 ];
114 assert_eq!(
115 expand(&["test0".into()], &ifaces, &cursor).unwrap(),
116 [
117 "127.0.0.1",
118 "::1",
119 "fe80::1%test0",
120 "ff02::1%test0",
121 "ff05::1"
122 ]
123 );
124 assert!(expand(&["missing".into()], &ifaces, &cursor).is_err());
125 }
126
127 #[test]
129 fn interface_setup_expands_and_excludes_addresses() {
130 let ifaces = vec![
131 ("lo".into(), Some("127.0.0.1".parse().unwrap())),
132 ("lo".into(), Some("::1".parse().unwrap())),
133 ];
134 for (directive, expected) in [
135 ("bind 1.2.3.4 lo", vec!["1.2.3.4", "127.0.0.1", "::1"]),
136 ("bind lo {\n except 127.0.0.1\n}", vec!["::1"]),
137 ("bind lo {\n except lo\n}", vec![]),
138 ] {
139 let blocks =
140 pharos_corefile::parse("Corefile", &format!(".:53 {{\n{directive}\n}}")).unwrap();
141 let mut cursor = Cursor::new("Corefile", &blocks[0].directives["bind"]);
142 assert_eq!(
143 parse_with_interfaces(&mut cursor, &ifaces).unwrap(),
144 expected
145 );
146 }
147 }
148}