use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
num::NonZeroUsize,
os::{
fd::{AsFd, AsRawFd, FromRawFd, RawFd},
unix::ffi::OsStrExt,
},
process::exit,
ptr::NonNull,
sync::atomic::Ordering,
};
use libc::{
c_uint, epoll_event, AF_INET, AF_INET6, AF_UNIX, IPPROTO_TCP, SHUT_WR, SOCK_CLOEXEC,
SOCK_NONBLOCK, SOCK_STREAM, TCP_QUICKACK,
};
use libseccomp::{scmp_cmp, RawSyscall, ScmpAction, ScmpFilterContext, ScmpSyscall};
use nix::{
errno::Errno,
fcntl::{splice, OFlag, SpliceFFlags, AT_FDCWD},
poll::PollTimeout,
sched::CloneFlags,
setsockopt_impl, sockopt_impl,
sys::{
epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags},
mman::{mmap_anonymous, MapFlags, ProtFlags},
resource::{getrlimit, Resource},
socket::{
listen, setsockopt, sockopt, Backlog, Shutdown, SockFlag, SockaddrLike,
SockaddrStorage, UnixAddr,
},
stat::{umask, Mode},
},
unistd::{chdir, sysconf, SysconfVar},
};
use crate::{
compat::{
epoll_ctl_safe, openat2, set_dumpable, set_name, set_no_new_privs, sockaddr_family,
AddressFamily, OpenHow, ResolveFlag, SockType,
},
config::{ALLOC_SYSCALLS, TOR_FCNTL_OPS, TOR_PRCTL_OPS, VDSO_SYSCALLS},
confine::{
confine_landlock_tor, confine_mdwe, confine_rlimit, confine_rlimit_zero,
confine_scmp_close, confine_scmp_exit_group, confine_scmp_fcntl, confine_scmp_madvise,
confine_scmp_pipe2, confine_scmp_prctl, confine_scmp_sigaction, confine_scmp_wx_syd,
pivot_root_cwd, safe_drop_caps, secure_getenv, try_unshare, Sydcall, CLONE_NEWTIME,
SYS_ACCEPT4, SYS_CONNECT, SYS_SHUTDOWN, SYS_SOCKET,
},
cookie::{
safe_accept4, safe_connect, safe_exit_group, safe_pipe2, safe_shutdown, safe_socket,
CookieIdx, SYSCOOKIE_POOL,
},
eprintfln,
err::{SydError, SydResult},
fd::{closeexcept, recv_with_fd, to_active_fd, SafeOwnedFd},
hash::SydHashMap,
id::SydId,
ignore_signals,
log::LOG_FD,
lookup::{file_type, FileType},
main,
path::XPathBuf,
printfln,
retry::retry_on_eintr,
rng::{duprand, fillrandom, randint},
sealbox::{madvise_dontdump, madvise_dontfork, mprotect_none, mprotect_readonly, mseal},
IgnoreSignalOpts,
};
const PIPE_BUF: usize = 16 * libc::PIPE_BUF;
main! { tor_bin_main =>
let result = tor_bin_run(None);
let code = match result {
Ok(()) => 0,
Err(err) => err.errno().map(|errno| errno as i32).unwrap_or(128),
};
safe_exit_group(code)
}
pub(crate) fn tor_bin_run(opts: Option<TorBinOpts>) -> SydResult<()> {
let _ = set_name(SydId::get_cname(c"syd-tor"));
safe_drop_caps()?;
set_no_new_privs()?;
if !cfg!(feature = "trusted") {
tor_confine_dump()?;
tor_confine_rlim()?;
}
ignore_signals(IgnoreSignalOpts::empty())?;
umask(Mode::from_bits_truncate(0o777));
let opts = match opts {
Some(opts) => opts,
None => parse_options()?,
};
let TorBinOpts {
sock,
addr,
unix,
is_debug,
} = opts;
let mut keep = [0, 0];
#[expect(clippy::cast_sign_loss)]
let n = {
keep[0] = sock.as_raw_fd() as c_uint;
if let Some(ref unix) = unix {
keep[1] = unix.as_raw_fd() as c_uint;
2
} else {
1
}
};
keep[..n].sort_unstable();
closeexcept(&keep[..n])?;
LOG_FD.store(-42, Ordering::Release);
if let Ok((soft_limit, hard_limit)) = getrlimit(Resource::RLIMIT_NOFILE) {
if soft_limit < hard_limit {
let _ = confine_rlimit(Resource::RLIMIT_NOFILE, Some(hard_limit));
}
}
let unix = if let Some(unix) = unix {
let fd = duprand(unix.as_raw_fd(), OFlag::O_CLOEXEC)?;
drop(unix);
Some(fd)
} else {
None
};
let addr = conv_addr(addr, unix.as_ref())?;
let port = conv_port(&addr)?;
let addr = seal_addr(addr)?;
if !cfg!(feature = "trusted") {
tor_confine_name(addr)?;
tor_confine_lock(port, unix.as_ref())?;
} else if !is_debug {
tor_confine_dump()?;
tor_confine_rlim()?;
tor_confine_name(addr)?;
tor_confine_lock(port, unix.as_ref())?;
}
let proxy = recv_proxy(sock)?;
let fd = duprand(proxy.as_raw_fd(), OFlag::O_CLOEXEC)?;
drop(proxy);
let proxy = fd;
listen(&proxy, Backlog::MAXCONN)?;
set_socket_options(&proxy);
let epoll = Epoll::new(EpollCreateFlags::EPOLL_CLOEXEC)?;
let fd = duprand(epoll.0.as_raw_fd(), OFlag::O_CLOEXEC)?;
drop(epoll);
let epoll = Epoll(fd.into());
if !is_debug {
let ctx = tor_confine_scmp(&proxy, addr)?;
ctx.load()?;
}
let mut proxy = Proxy::new(epoll, proxy, addr);
proxy.run()?;
drop(SafeOwnedFd::from(proxy.epoll.0));
Ok(())
}
type FdMap = SydHashMap<RawFd, RawFd>;
type CliMap = SydHashMap<RawFd, Client>;
struct Proxy<'a> {
armed: bool,
epoll: Epoll,
proxy: SafeOwnedFd,
extaddr: &'a SockaddrStorage,
clients: CliMap,
fdindex: FdMap,
}
impl<'a> Proxy<'a> {
fn run(&mut self) -> Result<(), Errno> {
self.accept_connections(true )?;
let mut events = vec![EpollEvent::empty(); 1024];
loop {
let n = match self.epoll.wait(&mut events, PollTimeout::NONE) {
Ok(n) => n,
Err(Errno::EINTR) => continue,
Err(errno) => return Err(errno.into()),
};
let mut accept = false;
for event in events.iter().take(n) {
let fd = event.data() as RawFd;
let event_flags = event.events();
if fd == self.proxy.as_fd().as_raw_fd() {
accept |= event_flags.contains(EpollFlags::EPOLLIN);
continue;
}
self.handle_connection(fd, event_flags)?;
}
if accept {
match self.add_connection() {
Ok(_) => {}
Err(Errno::EMFILE | Errno::ENFILE | Errno::ENOMEM | Errno::ENOSPC) => {
self.accept_connections(false )?;
}
Err(errno) => return Err(errno),
};
}
}
}
fn accept_connections(&mut self, listen: bool) -> Result<(), Errno> {
let fd = self.proxy.as_fd().as_raw_fd();
if listen {
#[expect(clippy::cast_sign_loss)]
let event = epoll_event {
events: (EpollFlags::EPOLLET | EpollFlags::EPOLLIN | EpollFlags::EPOLLRDHUP).bits()
as u32,
u64: self.proxy.as_fd().as_raw_fd() as u64,
};
epoll_ctl_safe(&self.epoll.0, fd, Some(event))?;
self.armed = true;
} else {
epoll_ctl_safe(&self.epoll.0, fd, None)?;
self.armed = false;
}
Ok(())
}
fn handle_connection(&mut self, fd: RawFd, mut event_flags: EpollFlags) -> Result<(), Errno> {
let client = if let Some(client) = self.get_client_mut(fd) {
client
} else {
return Ok(());
};
let is_ext = fd == client.fd_ext.as_raw_fd();
let is_inp = event_flags
.intersects(EpollFlags::EPOLLIN | EpollFlags::EPOLLPRI)
.then(|| event_flags.remove(EpollFlags::EPOLLIN | EpollFlags::EPOLLPRI))
.is_some();
let is_out = event_flags
.contains(EpollFlags::EPOLLOUT)
.then(|| event_flags.remove(EpollFlags::EPOLLOUT))
.is_some();
let is_hup = event_flags
.intersects(EpollFlags::EPOLLHUP)
.then(|| event_flags.remove(EpollFlags::EPOLLHUP))
.is_some();
let is_rd_hup = event_flags
.intersects(EpollFlags::EPOLLRDHUP)
.then(|| event_flags.remove(EpollFlags::EPOLLRDHUP))
.is_some();
let is_err = event_flags
.intersects(EpollFlags::EPOLLERR)
.then(|| event_flags.remove(EpollFlags::EPOLLERR))
.is_some();
assert!(event_flags.is_empty(), "BUG: {event_flags:?}");
let mut progressed = false;
if is_inp || is_rd_hup || is_hup || is_err {
progressed |= client.handle_input(is_ext).unwrap_or(false);
}
if is_out || is_hup || is_err {
progressed |= client.handle_output(is_ext).unwrap_or(false);
progressed |= client.handle_output(!is_ext).unwrap_or(false);
}
if is_hup || is_err {
let _ = client.handle_input(!is_ext);
self.close_connection(fd)?;
return Ok(());
}
while progressed {
progressed = false;
progressed |= client.handle_output(is_ext).unwrap_or(false);
progressed |= client.handle_output(!is_ext).unwrap_or(false);
progressed |= client.handle_input(is_ext).unwrap_or(false);
progressed |= client.handle_input(!is_ext).unwrap_or(false);
}
Ok(())
}
fn add_connection(&mut self) -> Result<(), Errno> {
loop {
#[expect(unreachable_patterns)]
match retry_on_eintr(|| {
safe_accept4(
&self.proxy,
SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC,
false,
)
}) {
Ok((fd_cli, _)) => {
let client = Client::new(fd_cli, self.extaddr)?;
self.add_client(client)?;
}
Err(
Errno::EAGAIN
| Errno::EHOSTDOWN
| Errno::EHOSTUNREACH
| Errno::EINPROGRESS
| Errno::ENETDOWN
| Errno::ENETUNREACH
| Errno::ENONET
| Errno::ENOPROTOOPT
| Errno::EOPNOTSUPP
| Errno::EPROTO
| Errno::EWOULDBLOCK,
) => break, Err(errno) => return Err(errno.into()),
}
}
Ok(())
}
fn close_connection(&mut self, fd: RawFd) -> Result<(), Errno> {
if self.remove_client(fd).is_some() && !self.armed {
self.accept_connections(true )?;
}
Ok(())
}
fn add_client(&mut self, client: Client) -> Result<(), Errno> {
let fd_cli = client.fd_cli.as_fd();
let fd_ext = client.fd_ext.as_fd();
#[expect(clippy::cast_sign_loss)]
let event = epoll_event {
events: (EpollFlags::EPOLLET
| EpollFlags::EPOLLIN
| EpollFlags::EPOLLOUT
| EpollFlags::EPOLLHUP
| EpollFlags::EPOLLRDHUP)
.bits() as u32,
u64: fd_cli.as_raw_fd() as u64,
};
epoll_ctl_safe(&self.epoll.0, fd_cli.as_raw_fd(), Some(event))?;
#[expect(clippy::cast_sign_loss)]
let event = epoll_event {
events: (EpollFlags::EPOLLET
| EpollFlags::EPOLLIN
| EpollFlags::EPOLLOUT
| EpollFlags::EPOLLHUP
| EpollFlags::EPOLLRDHUP)
.bits() as u32,
u64: fd_ext.as_raw_fd() as u64,
};
epoll_ctl_safe(&self.epoll.0, fd_ext.as_raw_fd(), Some(event))?;
let fd_cli = fd_cli.as_raw_fd();
let fd_ext = fd_ext.as_raw_fd();
self.fdindex.insert(fd_cli, fd_cli);
self.fdindex.insert(fd_ext, fd_cli);
self.clients.insert(fd_cli, client);
Ok(())
}
fn remove_client(&mut self, fd: RawFd) -> Option<Client> {
let fd_cli = self.fdindex.remove(&fd)?;
let client = self.clients.remove(&fd_cli)?;
self.fdindex.remove(&client.fd_ext.as_raw_fd());
Some(client)
}
fn get_client_mut(&mut self, fd: RawFd) -> Option<&mut Client> {
self.fdindex
.get(&fd)
.and_then(|fd| self.clients.get_mut(fd))
}
fn new(epoll: Epoll, proxy: SafeOwnedFd, extaddr: &'a SockaddrStorage) -> Self {
Self {
armed: false,
epoll,
proxy,
extaddr,
clients: CliMap::default(),
fdindex: FdMap::default(),
}
}
}
struct Client {
fd_cli: SafeOwnedFd,
fd_ext: SafeOwnedFd,
eof_cli: bool,
eof_ext: bool,
pipe_cli: (Option<SafeOwnedFd>, Option<SafeOwnedFd>),
pipe_ext: (Option<SafeOwnedFd>, Option<SafeOwnedFd>),
}
impl Client {
fn new(fd_cli: SafeOwnedFd, extaddr: &SockaddrStorage) -> Result<Self, Errno> {
let (pipe_in_from_cli, pipe_out_to_cli) = safe_pipe2(OFlag::O_NONBLOCK | OFlag::O_CLOEXEC)?;
let (pipe_in_from_ext, pipe_out_to_ext) = safe_pipe2(OFlag::O_NONBLOCK | OFlag::O_CLOEXEC)?;
set_socket_options(&fd_cli);
let fd_ext = safe_socket(
sockaddr_family(extaddr),
SockType::Stream,
SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC,
0,
)?;
set_socket_options(&fd_ext);
retry_on_eintr(|| safe_connect(&fd_ext, extaddr)).or_else(|e| {
if matches!(e, Errno::EAGAIN | Errno::EINPROGRESS) {
Ok(())
} else {
Err(e)
}
})?;
Ok(Self {
fd_cli,
fd_ext,
eof_cli: false,
eof_ext: false,
pipe_cli: (Some(pipe_in_from_cli), Some(pipe_out_to_cli)),
pipe_ext: (Some(pipe_in_from_ext), Some(pipe_out_to_ext)),
})
}
fn handle_input(&mut self, ext: bool) -> Result<bool, Errno> {
let (src_fd, dst_fd, pipe_in_fd, pipe_out_fd, eof) = if ext {
(
self.fd_ext.as_fd(),
self.fd_cli.as_fd(),
&self.pipe_ext.0,
&mut self.pipe_ext.1,
&mut self.eof_ext,
)
} else {
(
self.fd_cli.as_fd(),
self.fd_ext.as_fd(),
&self.pipe_cli.0,
&mut self.pipe_cli.1,
&mut self.eof_cli,
)
};
let mut progressed = false;
while !*eof {
let result = match pipe_out_fd.as_ref() {
Some(fd) => Self::splice(&src_fd, fd),
None => break, };
match result {
Ok(0) => {
*eof = true;
if let Some(fd) = pipe_out_fd.take() {
drop(fd);
}
break;
}
Ok(_) => {
progressed = true;
if let Some(fd) = pipe_in_fd.as_ref() {
loop {
match Self::splice(fd, &dst_fd) {
Ok(0) | Err(Errno::EAGAIN) => break,
Ok(_) => {}
Err(errno) => return Err(errno),
}
}
}
}
Err(Errno::EAGAIN) => break,
Err(errno) => return Err(errno),
};
}
Ok(progressed)
}
fn handle_output(&mut self, ext: bool) -> Result<bool, Errno> {
let (dst_fd, pipe_in_fd) = if ext {
(self.fd_cli.as_fd(), &mut self.pipe_ext.0)
} else {
(self.fd_ext.as_fd(), &mut self.pipe_cli.0)
};
let mut eof = false;
let mut progressed = false;
while let Some(fd) = pipe_in_fd.as_ref() {
match Self::splice(fd, &dst_fd) {
Ok(0) => {
eof = true;
break;
}
Ok(_) => progressed = true,
Err(Errno::EAGAIN) => break,
Err(errno) => return Err(errno),
};
}
if eof {
if let Some(fd) = pipe_in_fd.take() {
drop(fd);
}
let _ = safe_shutdown(dst_fd, Shutdown::Write);
}
Ok(progressed)
}
fn splice<SF: AsFd, DF: AsFd>(src_fd: &SF, dst_fd: &DF) -> Result<usize, Errno> {
splice(
src_fd,
None,
dst_fd,
None,
PIPE_BUF,
SpliceFFlags::SPLICE_F_NONBLOCK | SpliceFFlags::SPLICE_F_MORE,
)
}
}
pub(crate) struct TorBinOpts {
pub(crate) sock: SafeOwnedFd,
pub(crate) addr: Option<(IpAddr, u16)>,
pub(crate) unix: Option<SafeOwnedFd>,
pub(crate) is_debug: bool,
}
impl TorBinOpts {
pub(crate) fn new(
sock: SafeOwnedFd,
addr: Option<(IpAddr, u16)>,
unix: Option<SafeOwnedFd>,
is_debug: bool,
) -> SydResult<Self> {
match (addr, &unix) {
(Some((_, port)), None) if port != 0 => {}
(None, Some(_)) => {}
_ => return Err(Errno::EINVAL.into()),
}
Ok(TorBinOpts {
sock,
addr,
unix,
is_debug,
})
}
}
fn parse_options() -> SydResult<TorBinOpts> {
use lexopt::prelude::*;
let mut opt_addr = Some((IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9050));
let mut opt_unix = None;
let mut opt_sock = None;
let mut opt_debug = secure_getenv("SYD_TOR_DEBUG").is_some();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help()?;
exit(0);
}
Short('i') => opt_sock = Some(parser.value()?),
Short('o') => {
let value = parser.value()?.parse::<String>()?;
let (addr, port) = value
.rsplit_once(':')
.ok_or::<SydError>(Errno::EAFNOSUPPORT.into())?;
let addr = addr.parse::<IpAddr>()?;
let port = port.parse::<u16>()?;
if port == 0 {
return Err(Errno::EINVAL.into());
}
if opt_unix.is_none() {
opt_addr = Some((addr, port));
}
}
Short('u') => {
opt_unix = Some(parser.value().map(XPathBuf::from)?);
opt_addr = None;
}
Short('d') if cfg!(feature = "trusted") => opt_debug = true,
Short('d') => {
eprintfln!("syd-tor: Error: -d option isn't permitted.")?;
eprintfln!("syd-tor: Syd isn't built with trusted feature.")?;
return Err(Errno::EPERM.into());
}
_ => return Err(arg.unexpected().into()),
}
}
let sock = if let Some(sock) = opt_sock {
to_active_fd(sock.as_bytes())?
} else {
eprintfln!("syd-tor: Error: -i is required.")?;
help()?;
exit(1);
};
let unix = if let Some(unix) = opt_unix.as_deref() {
if unix.has_parent_dot() {
return Err(Errno::EPERM.into());
}
let how = OpenHow::new()
.flags(OFlag::O_PATH | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW)
.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
#[expect(clippy::disallowed_methods)]
let fd = retry_on_eintr(|| openat2(AT_FDCWD, unix, how))?;
if file_type(&fd, None, false)? != FileType::Sock {
return Err(Errno::ENOTSOCK.into());
}
Some(fd)
} else {
None
};
TorBinOpts::new(sock, opt_addr, unix, opt_debug)
}
fn tor_confine_dump() -> Result<(), Errno> {
std::panic::set_hook(Box::new(|_| {}));
set_dumpable(false)?;
let _ = confine_mdwe(false);
Ok(())
}
fn tor_confine_rlim() -> Result<(), Errno> {
confine_rlimit_zero(&[
Resource::RLIMIT_CORE,
Resource::RLIMIT_FSIZE,
Resource::RLIMIT_NPROC,
Resource::RLIMIT_LOCKS,
Resource::RLIMIT_MEMLOCK,
Resource::RLIMIT_MSGQUEUE,
])
}
fn tor_confine_lock<Fd: AsFd>(port: Option<u16>, unix: Option<Fd>) -> Result<(), Errno> {
let _ = confine_landlock_tor(port, unix);
Ok(())
}
fn tor_confine_name(addr: &SockaddrStorage) -> SydResult<()> {
chdir_safe(addr)?;
let mut namespaces = CloneFlags::CLONE_NEWUSER
| CloneFlags::CLONE_NEWNS
| CloneFlags::CLONE_NEWUTS
| CloneFlags::CLONE_NEWIPC
| CloneFlags::CLONE_NEWPID
| CloneFlags::CLONE_NEWCGROUP
| CLONE_NEWTIME;
if sockaddr_family(addr) == AddressFamily::Unix {
namespaces.insert(CloneFlags::CLONE_NEWNET);
}
namespaces = try_unshare(namespaces)?;
if namespaces.contains(CloneFlags::CLONE_NEWNS) {
pivot_root_cwd()?; }
if namespaces.contains(CloneFlags::CLONE_NEWUSER) {
safe_drop_caps()?;
}
Ok(())
}
fn tor_confine_scmp<Fd: AsFd>(
proxy_fd: Fd,
addr: &SockaddrStorage,
) -> SydResult<ScmpFilterContext> {
SYSCOOKIE_POOL.init()?;
let domain = match sockaddr_family(addr) {
AddressFamily::Inet => AF_INET,
AddressFamily::Inet6 => AF_INET6,
AddressFamily::Unix => AF_UNIX,
_ => return Err(Errno::EAFNOSUPPORT.into()),
};
let addr_ptr = addr.as_ptr() as u64;
#[expect(clippy::cast_lossless)]
let addr_len = addr.len() as u64;
let mut filter = new_filter(ScmpAction::KillProcess)?;
let allow_call = [
"brk",
"mremap",
"munmap",
"splice",
"setsockopt",
"epoll_ctl",
"epoll_wait",
"epoll_pwait",
"epoll_pwait2",
];
for name in allow_call.iter().chain(ALLOC_SYSCALLS).chain(VDSO_SYSCALLS) {
if let Ok(syscall) = Sydcall::from_name(name) {
filter.add_rule(ScmpAction::Allow, syscall)?;
}
}
confine_scmp_madvise(&mut filter)?;
confine_scmp_wx_syd(&mut filter)?;
confine_scmp_fcntl(&mut filter, TOR_FCNTL_OPS)?;
confine_scmp_prctl(&mut filter, TOR_PRCTL_OPS)?;
confine_scmp_sigaction(&mut filter)?;
confine_scmp_close(&mut filter, true )?;
confine_scmp_exit_group(&mut filter, true )?;
confine_scmp_pipe2(
&mut filter,
true,
true,
Some(OFlag::O_CLOEXEC | OFlag::O_NONBLOCK),
)?;
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_sign_loss)]
#[expect(clippy::disallowed_methods)]
#[expect(clippy::useless_conversion)]
if let Some(syscall) = SYS_SOCKET.map(|n| ScmpSyscall::from_raw_syscall(n as RawSyscall)) {
let sock_domain = domain as u64;
let sock_type = (SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC) as u64;
let sock_protocol = 0u64;
filter.add_rule_conditional(
ScmpAction::Allow,
syscall,
&[
scmp_cmp!($arg0 == sock_domain),
scmp_cmp!($arg1 == sock_type),
scmp_cmp!($arg2 == sock_protocol),
scmp_cmp!($arg3 == SYSCOOKIE_POOL.try_get(CookieIdx::SocketArg3)?.into()),
scmp_cmp!($arg4 == SYSCOOKIE_POOL.try_get(CookieIdx::SocketArg4)?.into()),
scmp_cmp!($arg5 == SYSCOOKIE_POOL.try_get(CookieIdx::SocketArg5)?.into()),
],
)?;
} else {
filter.add_rule(ScmpAction::Allow, Sydcall::from_name("socket").unwrap())?;
}
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::disallowed_methods)]
#[expect(clippy::useless_conversion)]
if let Some(syscall) = SYS_CONNECT.map(|n| ScmpSyscall::from_raw_syscall(n as RawSyscall)) {
filter.add_rule_conditional(
ScmpAction::Allow,
syscall,
&[
scmp_cmp!($arg1 == addr_ptr),
scmp_cmp!($arg2 == addr_len),
scmp_cmp!($arg3 == SYSCOOKIE_POOL.try_get(CookieIdx::ConnectArg3)?.into()),
scmp_cmp!($arg4 == SYSCOOKIE_POOL.try_get(CookieIdx::ConnectArg4)?.into()),
scmp_cmp!($arg5 == SYSCOOKIE_POOL.try_get(CookieIdx::ConnectArg5)?.into()),
],
)?;
} else {
filter.add_rule(ScmpAction::Allow, Sydcall::from_name("connect").unwrap())?;
}
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_sign_loss)]
#[expect(clippy::disallowed_methods)]
#[expect(clippy::useless_conversion)]
if let Some(syscall) = SYS_ACCEPT4.map(|n| ScmpSyscall::from_raw_syscall(n as RawSyscall)) {
filter.add_rule_conditional(
ScmpAction::Allow,
syscall,
&[
scmp_cmp!($arg0 == proxy_fd.as_fd().as_raw_fd() as u64),
scmp_cmp!($arg1 == 0u64), scmp_cmp!($arg2 == 0u64), scmp_cmp!($arg3 == (SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC).bits() as u64),
scmp_cmp!($arg4 == SYSCOOKIE_POOL.try_get(CookieIdx::Accept4Arg4)?.into()),
scmp_cmp!($arg5 == SYSCOOKIE_POOL.try_get(CookieIdx::Accept4Arg5)?.into()),
],
)?;
} else {
filter.add_rule(ScmpAction::Allow, Sydcall::from_name("accept4").unwrap())?;
}
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::disallowed_methods)]
#[expect(clippy::useless_conversion)]
if let Some(syscall) = SYS_SHUTDOWN.map(|n| ScmpSyscall::from_raw_syscall(n as RawSyscall)) {
filter.add_rule_conditional(
ScmpAction::Allow,
syscall,
&[
scmp_cmp!($arg1 == SHUT_WR as u64),
scmp_cmp!($arg2 == SYSCOOKIE_POOL.try_get(CookieIdx::ShutdownArg2)?.into()),
scmp_cmp!($arg3 == SYSCOOKIE_POOL.try_get(CookieIdx::ShutdownArg3)?.into()),
scmp_cmp!($arg4 == SYSCOOKIE_POOL.try_get(CookieIdx::ShutdownArg4)?.into()),
scmp_cmp!($arg5 == SYSCOOKIE_POOL.try_get(CookieIdx::ShutdownArg5)?.into()),
],
)?;
} else {
filter.add_rule(ScmpAction::Allow, Sydcall::from_name("shutdown").unwrap())?;
}
Ok(filter)
}
fn chdir_safe(addr: &SockaddrStorage) -> Result<(), Errno> {
let safe_cwd = if sockaddr_family(addr) == AddressFamily::Unix {
"/proc/self/fd"
} else {
"/proc/self/fdinfo"
};
chdir(safe_cwd)
}
fn recv_proxy<Fd: AsFd>(sock: Fd) -> Result<SafeOwnedFd, Errno> {
let mut buf = [0u8; 1];
let mut lfd = [-1; 1];
match recv_with_fd(sock, &mut buf, &mut lfd) {
Ok((_, 1)) if lfd[0] >= 0 => {
Ok(unsafe { SafeOwnedFd::from_raw_fd(lfd[0]) })
}
Ok(_) => Err(Errno::EBADF),
Err(errno) => Err(errno),
}
}
fn conv_addr<Fd: AsFd>(
addr: Option<(IpAddr, u16)>,
unix: Option<Fd>,
) -> Result<SockaddrStorage, Errno> {
let addr: SockaddrStorage = match (addr, unix) {
(Some((_, 0)), None) => return Err(Errno::EINVAL),
(Some((addr, port)), None) => {
let addr = SocketAddr::new(addr, port);
match addr {
SocketAddr::V4(addr) => SockaddrStorage::from(addr),
SocketAddr::V6(addr) => SockaddrStorage::from(addr),
}
}
(None, Some(unix)) => {
let mut buf = itoa::Buffer::new();
let unix = UnixAddr::new(buf.format(unix.as_fd().as_raw_fd()))?;
unsafe { SockaddrStorage::from_raw(unix.as_ptr().cast(), Some(unix.len())) }
.ok_or(Errno::EINVAL)?
}
_ => return Err(Errno::EINVAL),
};
Ok(addr)
}
fn conv_port(addr: &SockaddrStorage) -> Result<Option<u16>, Errno> {
let port: Option<u16> = if let Some(sa_in) = addr.as_sockaddr_in() {
let port = sa_in.port();
if port == 0 {
return Err(Errno::EINVAL);
}
Some(port)
} else if let Some(sa_in6) = addr.as_sockaddr_in6() {
let port = sa_in6.port();
if port == 0 {
return Err(Errno::EINVAL);
}
Some(port)
} else if addr.as_unix_addr().is_some() {
None
} else {
return Err(Errno::EAFNOSUPPORT);
};
Ok(port)
}
fn seal_addr(addr: SockaddrStorage) -> Result<&'static SockaddrStorage, Errno> {
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_sign_loss)]
let page_siz =
NonZeroUsize::new(sysconf(SysconfVar::PAGE_SIZE)?.ok_or(Errno::EINVAL)? as usize)
.ok_or(Errno::EINVAL)?;
#[expect(clippy::arithmetic_side_effects)]
let base_siz = NonZeroUsize::new(page_siz.get() * 3).ok_or(Errno::EINVAL)?;
let base_ptr = unsafe {
mmap_anonymous(
None,
base_siz,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
MapFlags::MAP_PRIVATE,
)
}?;
madvise_dontdump(base_ptr, base_siz)?;
madvise_dontfork(base_ptr, base_siz)?;
let guard_ptr_1 = base_ptr;
#[expect(clippy::arithmetic_side_effects)]
let addr_ptr = NonNull::new(((guard_ptr_1.as_ptr() as usize) + page_siz.get()) as *mut _)
.ok_or(Errno::EINVAL)?;
#[expect(clippy::arithmetic_side_effects)]
let guard_ptr_2 = NonNull::new(((addr_ptr.as_ptr() as usize) + page_siz.get()) as *mut _)
.ok_or(Errno::EINVAL)?;
let addr: &SockaddrStorage = unsafe {
let addr_ptr = addr_ptr.as_ptr();
let addr_vec = std::slice::from_raw_parts_mut(addr_ptr as *mut u8, page_siz.get());
fillrandom(addr_vec)?;
let addr_size = size_of::<SockaddrStorage>();
let addr_align = align_of::<SockaddrStorage>();
let max_offset = page_siz.get().checked_sub(addr_size).ok_or(Errno::EINVAL)?;
#[expect(clippy::cast_possible_truncation)]
let rand_off = randint(0..=max_offset as u64)? as usize;
#[expect(clippy::arithmetic_side_effects)]
let aligned_off = rand_off & !(addr_align - 1);
let addr_ptr = (addr_ptr as usize)
.checked_add(aligned_off)
.ok_or(Errno::EINVAL)? as *mut SockaddrStorage;
addr_ptr.write(addr);
&*addr_ptr
};
mprotect_readonly(addr_ptr, page_siz)?;
mprotect_none(guard_ptr_1, page_siz)?;
mprotect_none(guard_ptr_2, page_siz)?;
match mseal(base_ptr, base_siz) {
Ok(_) | Err(Errno::ENOSYS | Errno::EPERM) => {}
Err(errno) => return Err(errno),
}
Ok(addr)
}
fn set_socket_options<Fd: AsFd>(fd: Fd) {
let _ = setsockopt(&fd, sockopt::TcpNoDelay, &true);
let _ = setsockopt(&fd, sockopt::KeepAlive, &true);
sockopt_impl!(TcpQuickAck, SetOnly, IPPROTO_TCP, TCP_QUICKACK, bool);
let _ = setsockopt(&fd, TcpQuickAck, &true);
}
fn new_filter(action: ScmpAction) -> SydResult<ScmpFilterContext> {
let mut filter = ScmpFilterContext::new(action)?;
filter.set_ctl_nnp(true)?;
filter.set_act_badarch(ScmpAction::KillProcess)?;
let _ = filter.set_ctl_optimize(2);
Ok(filter)
}
fn help() -> Result<(), Errno> {
printfln!("Usage: syd-tor [-dh] -i <socket-fd> [-o addr:port] [-u unix-sock]")?;
printfln!("Syd's SOCKS Proxy Forwarder")?;
printfln!(
"Receives listening socket from fd and forwards traffic to addr:port or UNIX socket."
)?;
printfln!("External address must be an IPv4 or IPv6 address.")?;
printfln!(" -h Print this help message and exit.")?;
printfln!(" -i <socket-fd> Socket file descriptor to receive listening socket from.")?;
printfln!(" -o <addr:port> Specify external address to forward traffic to.")?;
printfln!(" Defaults to 127.0.0.1:9050.")?;
printfln!(" -u <unix-sock> Specify UNIX domain socket to forward traffic to.")?;
printfln!(" This option has precedence over -o.")?;
printfln!(" -d Run in debug mode without confinement.")?;
printfln!(" This requires Syd built with trusted feature.")?;
Ok(())
}