1
use std::{fs, path::PathBuf};
2

            
3
use anyhow::Context;
4
use clap_verbosity_flag::Verbosity;
5
use nix::libc::{EXIT_SUCCESS, exit};
6
use std::os::unix::net::UnixStream as StdUnixStream;
7
use tokio::net::UnixStream as TokioUnixStream;
8

            
9
use crate::{
10
    core::common::{
11
        DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser, executable_is_suid_or_sgid,
12
    },
13
    server::{config::read_config_from_path, server_loop::handle_requests_for_single_session},
14
};
15

            
16
/// Determine whether we will make a connection to an external server
17
/// or start an internal server with elevated privileges.
18
///
19
/// If neither is feasible, an error is returned.
20
fn will_connect_to_external_server(
21
    server_socket_path: Option<&PathBuf>,
22
    // This parameter is only used in suid-sgid-mode
23
    #[allow(unused_variables)] config_path: Option<&PathBuf>,
24
) -> anyhow::Result<bool> {
25
    if server_socket_path.is_some() {
26
        return Ok(true);
27
    }
28

            
29
    #[cfg(feature = "suid-sgid-mode")]
30
    if config_path.is_some() {
31
        return Ok(false);
32
    }
33

            
34
    if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() {
35
        return Ok(true);
36
    }
37

            
38
    #[cfg(feature = "suid-sgid-mode")]
39
    if fs::metadata(DEFAULT_CONFIG_PATH).is_ok() {
40
        return Ok(false);
41
    }
42

            
43
    #[cfg(feature = "suid-sgid-mode")]
44
    anyhow::bail!("No socket path or config path provided, and no default socket or config found");
45

            
46
    #[cfg(not(feature = "suid-sgid-mode"))]
47
    anyhow::bail!("No socket path provided, and no default socket found");
48
}
49

            
50
/// This function is used to bootstrap the connection to the server.
51
/// This can happen in two ways:
52
///
53
/// 1. If a socket path is provided, or exists in the default location,
54
///    the function will connect to the socket and authenticate with the
55
///    server to ensure that the server knows the uid of the client.
56
///
57
/// 2. If a config path is provided, or exists in the default location,
58
///    and the config is readable, the function will assume it is either
59
///    setuid or setgid, and will fork a child process to run the server
60
///    with the provided config. The server will exit silently by itself
61
///    when it is done, and this function will only return for the client
62
///    with the socket for the server.
63
///
64
/// If neither of these options are available, the function will fail.
65
///
66
/// Note that this function is also responsible for setting up logging,
67
/// because in the case of an internal server, we need to drop privileges
68
/// before we can initialize logging.
69
///
70
/// **WARNING:** This function may be run with elevated privileges.
71
pub fn bootstrap_server_connection_and_drop_privileges(
72
    server_socket_path: Option<PathBuf>,
73
    config: Option<PathBuf>,
74
    verbose: Verbosity,
75
) -> anyhow::Result<StdUnixStream> {
76
    if will_connect_to_external_server(server_socket_path.as_ref(), config.as_ref())? {
77
        assert!(
78
            !executable_is_suid_or_sgid()?,
79
            "The executable should not be SUID or SGID when connecting to an external server"
80
        );
81

            
82
        env_logger::Builder::new()
83
            .filter_level(verbose.log_level_filter())
84
            .init();
85

            
86
        connect_to_external_server(server_socket_path)
87
    } else if cfg!(feature = "suid-sgid-mode") {
88
        // NOTE: We need to be really careful with the code up until this point,
89
        //       as we might be running with elevated privileges.
90
        let server_connection = bootstrap_internal_server_and_drop_privs(config)?;
91

            
92
        env_logger::Builder::new()
93
            .filter_level(verbose.log_level_filter())
94
            .init();
95

            
96
        Ok(server_connection)
97
    } else {
98
        anyhow::bail!("SUID/SGID support is not enabled, cannot start internal server");
99
    }
100
}
101

            
102
fn connect_to_external_server(
103
    server_socket_path: Option<PathBuf>,
104
) -> anyhow::Result<StdUnixStream> {
105
    // TODO: ensure this is both readable and writable
106
    if let Some(socket_path) = server_socket_path {
107
        log::debug!("Connecting to socket at {:?}", socket_path);
108
        return match StdUnixStream::connect(socket_path) {
109
            Ok(socket) => Ok(socket),
110
            Err(e) => match e.kind() {
111
                std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
112
                std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
113
                _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
114
            },
115
        };
116
    }
117

            
118
    if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() {
119
        log::debug!("Connecting to default socket at {:?}", DEFAULT_SOCKET_PATH);
120
        return match StdUnixStream::connect(DEFAULT_SOCKET_PATH) {
121
            Ok(socket) => Ok(socket),
122
            Err(e) => match e.kind() {
123
                std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
124
                std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
125
                _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
126
            },
127
        };
128
    }
129

            
130
    anyhow::bail!("No socket path provided, and no default socket found");
131
}
132

            
133
// TODO: this function is security critical, it should be integration tested
134
//       in isolation.
135
/// Drop privileges to the real user and group of the process.
136
/// If the process is not running with elevated privileges, this function
137
/// is a no-op.
138
fn drop_privs() -> anyhow::Result<()> {
139
    log::debug!("Dropping privileges");
140
    let real_uid = nix::unistd::getuid();
141
    let real_gid = nix::unistd::getgid();
142

            
143
    nix::unistd::setuid(real_uid).context("Failed to drop privileges")?;
144
    nix::unistd::setgid(real_gid).context("Failed to drop privileges")?;
145

            
146
    debug_assert_eq!(nix::unistd::getuid(), real_uid);
147
    debug_assert_eq!(nix::unistd::getgid(), real_gid);
148

            
149
    log::debug!("Privileges dropped successfully");
150
    Ok(())
151
}
152

            
153
fn bootstrap_internal_server_and_drop_privs(
154
    config_path: Option<PathBuf>,
155
) -> anyhow::Result<StdUnixStream> {
156
    if let Some(config_path) = config_path {
157
        if !executable_is_suid_or_sgid()? {
158
            anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
159
        }
160

            
161
        // ensure config exists and is readable
162
        if fs::metadata(&config_path).is_err() {
163
            return Err(anyhow::anyhow!("Config file not found or not readable"));
164
        }
165

            
166
        log::debug!("Starting server with config at {:?}", config_path);
167
        let socket = invoke_server_with_config(config_path)?;
168
        drop_privs()?;
169
        return Ok(socket);
170
    };
171

            
172
    let config_path = PathBuf::from(DEFAULT_CONFIG_PATH);
173
    if fs::metadata(&config_path).is_ok() {
174
        if !executable_is_suid_or_sgid()? {
175
            anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
176
        }
177
        log::debug!("Starting server with default config at {:?}", config_path);
178
        let socket = invoke_server_with_config(config_path)?;
179
        drop_privs()?;
180
        return Ok(socket);
181
    };
182

            
183
    anyhow::bail!("No config path provided, and no default config found");
184
}
185

            
186
// TODO: we should somehow ensure that the forked process is killed on completion,
187
//       just in case the client does not behave properly.
188
/// Fork a child process to run the server with the provided config.
189
/// The server will exit silently by itself when it is done, and this function
190
/// will only return for the client with the socket for the server.
191
fn invoke_server_with_config(config_path: PathBuf) -> anyhow::Result<StdUnixStream> {
192
    let (server_socket, client_socket) = StdUnixStream::pair()?;
193
    let unix_user = UnixUser::from_uid(nix::unistd::getuid().as_raw())?;
194

            
195
    match (unsafe { nix::unistd::fork() }).context("Failed to fork")? {
196
        nix::unistd::ForkResult::Parent { child } => {
197
            log::debug!("Forked child process with PID {}", child);
198
            Ok(client_socket)
199
        }
200
        nix::unistd::ForkResult::Child => {
201
            log::debug!("Running server in child process");
202

            
203
            match run_forked_server(config_path, server_socket, unix_user) {
204
                Err(e) => Err(e),
205
                Ok(_) => unreachable!(),
206
            }
207
        }
208
    }
209
}
210

            
211
/// Run the server in the forked child process.
212
/// This function will not return, but will exit the process with a success code.
213
fn run_forked_server(
214
    config_path: PathBuf,
215
    server_socket: StdUnixStream,
216
    unix_user: UnixUser,
217
) -> anyhow::Result<()> {
218
    let config = read_config_from_path(Some(config_path))?;
219

            
220
    let result: anyhow::Result<()> = tokio::runtime::Builder::new_current_thread()
221
        .enable_all()
222
        .build()
223
        .unwrap()
224
        .block_on(async {
225
            let socket = TokioUnixStream::from_std(server_socket)?;
226
            handle_requests_for_single_session(socket, &unix_user, &config).await?;
227
            Ok(())
228
        });
229

            
230
    result?;
231

            
232
    unsafe {
233
        exit(EXIT_SUCCESS);
234
    }
235
}