11#[ cfg( target_os = "macos" ) ]
2- use std:: io:: { BufRead , BufReader , Cursor , Error as IoError } ;
2+ use std:: io:: { Cursor , Error as IoError } ;
33#[ cfg( any( target_os = "freebsd" , target_os = "macos" , target_os = "netbsd" ) ) ]
44use std:: net:: { Ipv4Addr , Ipv6Addr } ;
5- use std:: net:: { SocketAddr , ToSocketAddrs } ;
5+ #[ cfg( any( target_os = "freebsd" , target_os = "linux" , target_os = "netbsd" ) ) ]
6+ use std:: path:: Path ;
67#[ cfg( target_os = "linux" ) ]
78use std:: { collections:: HashSet , fs:: OpenOptions } ;
89#[ cfg( any( target_os = "freebsd" , target_os = "linux" , target_os = "netbsd" ) ) ]
910use std:: { io:: Write , process:: Stdio } ;
11+ use std:: {
12+ io:: { BufRead , BufReader } ,
13+ net:: { SocketAddr , ToSocketAddrs } ,
14+ } ;
1015#[ cfg( not( target_os = "windows" ) ) ]
1116use std:: { net:: IpAddr , process:: Command } ;
1217
@@ -24,6 +29,55 @@ use crate::{
2429#[ cfg( target_os = "linux" ) ]
2530use crate :: { IpVersion , check_command_output_status, netlink} ;
2631
32+ #[ cfg( any( target_os = "freebsd" , target_os = "linux" , target_os = "netbsd" ) ) ]
33+ const IFACE_ORDER_PATH : & str = "/etc/resolvconf/interface-order" ;
34+
35+ /// Constructs the resolvconf interface name for manipulating DNS settings.
36+ /// Resolvconf may be symlinked to resolvectl on some systems that use systemd-resolved.
37+ /// In such cases there is no need to prefix the interface name and this function just returns
38+ /// the base interface name.
39+ ///
40+ /// On other systems, especially those that don't use systemd-resolved (Debian 13)
41+ /// resolvconf may be a binary from the "resolvconf" package. In such cases, this function
42+ /// reads the interface-order file to find a highest priority interface prefix and constructs
43+ /// the full interface name prefixing the base interface name with the found prefix.
44+ #[ cfg( any( target_os = "freebsd" , target_os = "linux" , target_os = "netbsd" ) ) ]
45+ fn construct_resolvconf_ifname ( base_ifname : & str ) -> String {
46+ let iface_order = Path :: new ( IFACE_ORDER_PATH ) ;
47+ if iface_order. exists ( ) {
48+ // Check if resolvconf command is a symlink or a binary
49+ if let Ok ( Some ( resolvconf_path) ) = get_command_path ( "resolvconf" ) {
50+ if let Ok ( metadata) = std:: fs:: symlink_metadata ( & resolvconf_path) {
51+ if !metadata. file_type ( ) . is_symlink ( ) {
52+ // It's a binary, proceed to read interface_order file
53+ let iface_regex = regex:: Regex :: new ( r"^([A-Za-z0-9-]+)\*$" ) . unwrap ( ) ;
54+ if let Ok ( file) = std:: fs:: File :: open ( iface_order) {
55+ let reader = BufReader :: new ( file) ;
56+ if let Some ( constructed_ifname) = reader. lines ( ) . map_while ( Result :: ok) . find_map ( |line| {
57+ let iface = line. trim ( ) ;
58+ iface_regex. captures ( iface) . and_then ( |captures| {
59+ captures. get ( 1 ) . map ( |matched_iface| {
60+ // Output format: <highest_priority_iface>.<base_ifname>
61+ let constructed_ifname =
62+ format ! ( "{}.{base_ifname}" , matched_iface. as_str( ) ) ;
63+ debug ! (
64+ "Constructed interface name from interface_order: {constructed_ifname}"
65+ ) ;
66+ constructed_ifname
67+ } )
68+ } )
69+ } ) {
70+ return constructed_ifname;
71+ }
72+ }
73+ }
74+ }
75+ }
76+ }
77+
78+ base_ifname. into ( )
79+ }
80+
2781#[ cfg( any( target_os = "freebsd" , target_os = "linux" , target_os = "netbsd" ) ) ]
2882pub ( crate ) fn configure_dns (
2983 ifname : & str ,
@@ -36,7 +90,8 @@ pub(crate) fn configure_dns(
3690 domains: {search_domains:?}"
3791 ) ;
3892 let mut cmd = Command :: new ( "resolvconf" ) ;
39- let mut args = vec ! [ "-a" , ifname, "-m" , "0" ] ;
93+ let ifname = construct_resolvconf_ifname ( ifname) ;
94+ let mut args = vec ! [ "-a" , & ifname, "-m" , "0" ] ;
4095 // Set the exclusive flag if no search domains are provided,
4196 // making the DNS servers a preferred route for any domain
4297 if search_domains. is_empty ( ) {
@@ -170,7 +225,8 @@ pub(crate) fn configure_dns(
170225#[ cfg( any( target_os = "freebsd" , target_os = "linux" , target_os = "netbsd" ) ) ]
171226pub ( crate ) fn clear_dns ( ifname : & str ) -> Result < ( ) , WireguardInterfaceError > {
172227 debug ! ( "Removing DNS configuration for interface {ifname}" ) ;
173- let args = [ "-d" , ifname, "-f" ] ;
228+ let ifname = construct_resolvconf_ifname ( ifname) ;
229+ let args = [ "-d" , & ifname, "-f" ] ;
174230 debug ! ( "Executing resolvconf with args: {args:?}" ) ;
175231 let mut cmd = Command :: new ( "resolvconf" ) ;
176232 let output = cmd. args ( args) . output ( ) ?;
@@ -537,3 +593,33 @@ pub(crate) fn resolve(addr: &str) -> Result<SocketAddr, WireguardInterfaceError>
537593 . next ( )
538594 . ok_or_else ( error)
539595}
596+
597+ pub ( crate ) fn get_command_path (
598+ command : & str ,
599+ ) -> Result < Option < std:: path:: PathBuf > , WireguardInterfaceError > {
600+ use std:: env;
601+
602+ debug ! ( "Searching for command {command} in PATH" ) ;
603+ let paths = env:: var_os ( "PATH" ) . ok_or_else ( || {
604+ WireguardInterfaceError :: MissingDependency ( "Environment variable `PATH` not found" . into ( ) )
605+ } ) ?;
606+ debug ! ( "PATH variable: {paths:?}" ) ;
607+
608+ Ok ( env:: split_paths ( & paths) . find_map ( |dir| {
609+ let full_path = dir. join ( command) ;
610+ match full_path. try_exists ( ) {
611+ Ok ( true ) => {
612+ debug ! ( "Command {command} found in {dir:?}" ) ;
613+ Some ( full_path)
614+ }
615+ Ok ( false ) => {
616+ debug ! ( "Command {command} not found in {dir:?}" ) ;
617+ None
618+ }
619+ Err ( err) => {
620+ warn ! ( "Error while checking for {command} in {dir:?}: {err}" ) ;
621+ None
622+ }
623+ }
624+ } ) )
625+ }
0 commit comments