use std::io;
use std::net::{SocketAddr, TcpListener};
#[cfg(any(target_os = "macos", target_os = "linux", test))]
use std::os::fd::RawFd;
use tracing::{info, warn};
use crate::proxy::{Listener, ListenerOptions, Proxy};
pub async fn proxy_listeners(
proxy: &Proxy,
name: &str,
activated: Vec<TcpListener>,
addrs: &[SocketAddr],
options: ListenerOptions,
) -> io::Result<Vec<Listener>> {
if activated.is_empty() {
let mut listeners = Vec::new();
for (i, addr) in addrs.iter().enumerate() {
match proxy.bind(*addr, options.clone()).await {
Ok(listener) => listeners.push(listener),
Err(err) if i > 0 => warn!("not listening on {addr}: {err}"),
Err(err) => return Err(err),
}
}
return Ok(listeners);
}
let manager = if cfg!(target_os = "macos") {
"launchd"
} else {
"systemd"
};
activated
.into_iter()
.map(|socket| {
let listener = proxy.listen(socket, options.clone())?;
if let Ok(local) = listener.local_addr() {
info!("listening on {local} (socket {name:?} from {manager})");
if let Some(port) = addrs.first().map(SocketAddr::port)
&& local.port() != port
{
warn!(
"socket {name:?} from {manager} is on port {}, but URLs use port {port} from the machine config",
local.port()
);
}
}
Ok(listener)
})
.collect()
}
#[cfg(target_os = "macos")]
pub fn activate_socket(name: &str) -> io::Result<Vec<TcpListener>> {
use std::ffi::CString;
use libc::{c_char, c_int, size_t};
unsafe extern "C" {
fn launch_activate_socket(
name: *const c_char,
fds: *mut *mut c_int,
cnt: *mut size_t,
) -> c_int;
}
let name = CString::new(name).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let mut fds: *mut c_int = std::ptr::null_mut();
let mut count: size_t = 0;
let err = unsafe { launch_activate_socket(name.as_ptr(), &mut fds, &mut count) };
match err {
0 => {}
libc::ESRCH | libc::ENOENT => return Ok(Vec::new()),
err => return Err(io::Error::from_raw_os_error(err)),
}
if fds.is_null() {
return Ok(Vec::new());
}
let raw = unsafe { std::slice::from_raw_parts(fds, count) }.to_vec();
unsafe { libc::free(fds.cast()) };
listeners(raw)
}
#[cfg(target_os = "linux")]
static SYSTEMD_SOCKETS: std::sync::Mutex<Vec<(String, RawFd)>> = std::sync::Mutex::new(Vec::new());
#[cfg(target_os = "linux")]
pub fn take_systemd_sockets() {
let keys = ["LISTEN_PID", "LISTEN_FDS", "LISTEN_FDNAMES"];
let [pid, fds, names] = keys.map(|key| std::env::var(key).ok());
for key in keys {
unsafe { std::env::remove_var(key) };
}
let count = match systemd_socket_count(std::process::id(), pid.as_deref(), fds.as_deref()) {
Ok(count) => count,
Err(err) => {
warn!("ignoring the sockets from systemd: {err}");
return;
}
};
let mut sockets = match systemd_sockets(count, names.as_deref()) {
Ok(sockets) => sockets,
Err(err) => {
warn!("ignoring the sockets from systemd: {err}");
Vec::new()
}
};
let mut open = Vec::new();
for fd in (3..).take(count) {
match crate::process::set_cloexec(fd, true) {
Ok(()) => open.push(fd),
Err(err) => warn!("ignoring socket {fd} from systemd: {err}"),
}
}
sockets.retain(|(_, fd)| open.contains(fd));
*SYSTEMD_SOCKETS.lock().unwrap_or_else(|e| e.into_inner()) = sockets;
}
#[cfg(not(target_os = "linux"))]
pub fn take_systemd_sockets() {}
#[cfg(target_os = "linux")]
pub fn warn_unclaimed_sockets() {
let sockets = std::mem::take(&mut *SYSTEMD_SOCKETS.lock().unwrap_or_else(|e| e.into_inner()));
for (name, fd) in sockets {
warn!(
"ignoring socket {name:?} from systemd (fd {fd}); \
set FileDescriptorName=http or https in its socket unit"
);
}
}
#[cfg(not(target_os = "linux"))]
pub fn warn_unclaimed_sockets() {}
#[cfg(any(target_os = "linux", test))]
fn systemd_socket_count(
pid: u32,
listen_pid: Option<&str>,
listen_fds: Option<&str>,
) -> Result<usize, String> {
let (Some(listen_pid), Some(listen_fds)) = (listen_pid, listen_fds) else {
return Ok(0);
};
if listen_pid.parse::<u32>().ok() != Some(pid) {
return Ok(0);
}
listen_fds
.parse()
.ok()
.filter(|count| *count <= 1024)
.ok_or_else(|| format!("LISTEN_FDS={listen_fds:?} isn't a number of sockets"))
}
#[cfg(any(target_os = "linux", test))]
fn systemd_sockets(
count: usize,
listen_fdnames: Option<&str>,
) -> Result<Vec<(String, RawFd)>, String> {
if count == 0 {
return Ok(Vec::new());
}
let names: Vec<&str> = match listen_fdnames {
Some(names) => names.split(':').collect(),
None => vec!["unknown"; count],
};
if names.len() != count {
return Err(format!(
"LISTEN_FDNAMES has {} names for {count} sockets",
names.len()
));
}
Ok(names
.into_iter()
.zip(3..)
.map(|(name, fd)| (name.to_string(), fd))
.collect())
}
#[cfg(target_os = "linux")]
pub fn activate_socket(name: &str) -> io::Result<Vec<TcpListener>> {
let fds: Vec<RawFd> = {
let mut sockets = SYSTEMD_SOCKETS.lock().unwrap_or_else(|e| e.into_inner());
let (named, rest): (Vec<_>, Vec<_>) = std::mem::take(&mut *sockets)
.into_iter()
.partition(|(n, _)| n == name);
*sockets = rest;
named.into_iter().map(|(_, fd)| fd).collect()
};
if let Some(fd) = fds.iter().find(|fd| !is_listening_tcp(**fd)) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("socket {name:?} from systemd (fd {fd}) isn't a listening TCP socket"),
));
}
listeners(fds)
}
#[cfg(target_os = "linux")]
fn is_listening_tcp(fd: RawFd) -> bool {
let option = |name| {
let mut value: libc::c_int = 0;
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
let ret = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
name,
(&raw mut value).cast(),
&mut len,
)
};
(ret == 0).then_some(value)
};
option(libc::SO_ACCEPTCONN) == Some(1)
&& option(libc::SO_TYPE) == Some(libc::SOCK_STREAM)
&& matches!(
option(libc::SO_DOMAIN),
Some(libc::AF_INET | libc::AF_INET6)
)
}
#[cfg(any(target_os = "macos", target_os = "linux", test))]
fn listeners(fds: Vec<RawFd>) -> io::Result<Vec<TcpListener>> {
use std::os::fd::{AsRawFd, FromRawFd};
let listeners: Vec<TcpListener> = fds
.into_iter()
.map(|fd| unsafe { TcpListener::from_raw_fd(fd) })
.collect();
for listener in &listeners {
crate::process::set_cloexec(listener.as_raw_fd(), true)?;
}
Ok(listeners)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn activate_socket(_name: &str) -> io::Result<Vec<TcpListener>> {
Ok(Vec::new())
}
#[cfg(test)]
mod tests {
use std::os::fd::AsRawFd;
use super::*;
#[test]
fn handed_over_sockets_are_closed_on_exec() {
let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
assert!(fd >= 0);
assert_eq!(
unsafe { libc::fcntl(fd, libc::F_GETFD) } & libc::FD_CLOEXEC,
0
);
let listeners = listeners(vec![fd]).unwrap();
let flags = unsafe { libc::fcntl(listeners[0].as_raw_fd(), libc::F_GETFD) };
assert_eq!(flags & libc::FD_CLOEXEC, libc::FD_CLOEXEC);
}
#[test]
fn finds_no_sockets_without_activation() {
assert!(activate_socket("http").unwrap().is_empty());
}
fn named(pairs: &[(&str, RawFd)]) -> Vec<(String, RawFd)> {
pairs.iter().map(|(n, fd)| (n.to_string(), *fd)).collect()
}
#[test]
fn systemd_sockets_need_both_variables() {
assert_eq!(systemd_socket_count(42, None, None), Ok(0));
assert_eq!(systemd_socket_count(42, Some("42"), None), Ok(0));
assert_eq!(systemd_socket_count(42, None, Some("1")), Ok(0));
assert_eq!(systemd_socket_count(42, Some("42"), Some("2")), Ok(2));
}
#[test]
fn systemd_sockets_for_another_process_are_ignored() {
assert_eq!(systemd_socket_count(42, Some("43"), Some("1")), Ok(0));
assert_eq!(systemd_socket_count(42, Some("nova"), Some("1")), Ok(0));
assert_eq!(systemd_socket_count(42, Some("43"), Some("many")), Ok(0));
}
#[test]
fn systemd_socket_count_must_be_a_number() {
assert!(systemd_socket_count(42, Some("42"), Some("-1")).is_err());
assert!(systemd_socket_count(42, Some("42"), Some("many")).is_err());
assert!(systemd_socket_count(42, Some("42"), Some("100000")).is_err());
assert_eq!(systemd_socket_count(42, Some("42"), Some("0")), Ok(0));
}
#[test]
fn systemd_sockets_are_named_from_fd_3() {
assert_eq!(
systemd_sockets(2, Some("http:https")),
Ok(named(&[("http", 3), ("https", 4)]))
);
assert_eq!(
systemd_sockets(3, Some("https:http:http")),
Ok(named(&[("https", 3), ("http", 4), ("http", 5)]))
);
}
#[test]
fn unnamed_systemd_sockets_are_unknown() {
assert_eq!(
systemd_sockets(2, None),
Ok(named(&[("unknown", 3), ("unknown", 4)]))
);
assert_eq!(
systemd_sockets(2, Some("http:")),
Ok(named(&[("http", 3), ("", 4)]))
);
}
#[test]
fn systemd_sockets_must_match_their_names() {
assert!(systemd_sockets(1, Some("http:https")).is_err());
assert!(systemd_sockets(3, Some("http:https")).is_err());
assert_eq!(systemd_sockets(0, None), Ok(Vec::new()));
assert_eq!(systemd_sockets(0, Some("http")), Ok(Vec::new()));
}
#[cfg(target_os = "linux")]
#[test]
fn only_listening_tcp_sockets_are_taken() {
use std::os::fd::{FromRawFd, OwnedFd};
let listening = TcpListener::bind("127.0.0.1:0").unwrap();
assert!(is_listening_tcp(listening.as_raw_fd()));
let connected = std::net::TcpStream::connect(listening.local_addr().unwrap()).unwrap();
assert!(!is_listening_tcp(connected.as_raw_fd()));
let unbound = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
assert!(unbound >= 0);
let unbound = unsafe { OwnedFd::from_raw_fd(unbound) };
assert!(!is_listening_tcp(unbound.as_raw_fd()));
let udp = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
assert!(!is_listening_tcp(udp.as_raw_fd()));
let dir = tempfile::tempdir().unwrap();
let unix = std::os::unix::net::UnixListener::bind(dir.path().join("s")).unwrap();
assert!(!is_listening_tcp(unix.as_raw_fd()));
}
}