Skip to main content

pharos_plugins/
bind.rs

1//! Bind Directive setup and interface expansion.
2
3use 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                    // RFC 4007 ยง11.2: interface names can identify scoped IPv6 zones.
49                    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    // CoreDNS plugin/bind/setup_test.go cases, with deterministic interface addresses.
128    #[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}