Support CAP_NET_BIND_SERVICE raising for privileged TCP TLS ports
When file capabilities are granted with `sudo setcap ...+p`, capabilities are placed into the thread's permitted set rather than its effective set. While TUN interface provisioning in `tsunagi` properly acquired and dropped CAP_NET_ADMIN using `caps::raise`, `tsunagi-tcp-tls` attempted to bind port 443 directly without activating CAP_NET_BIND_SERVICE, resulting in EACCES (os error 13 Permission denied). Fixes and improvements: - Add `caps` dependency to `tsunagi-tcp-tls` on Linux. - Implement `NetBindServiceGuard` to temporarily raise CAP_NET_BIND_SERVICE into the effective set for the synchronous `TcpListener::bind` invocation and immediately drop it afterwards. - Clarify diagnostic warnings to distinguish between default port 443 fallback and explicitly requested port failure. - Update all privilege diagnostics and error hints across the codebase to uniformly recommend granting both capabilities: `sudo setcap cap_net_admin,cap_net_bind_service+p <path>`. Validation on Linux: - `cargo fmt --all -- --check` passed. - `cargo clippy --locked --workspace --all-targets -- -D warnings` passed. - `cargo test --locked --workspace --all-targets` passed (all workspace unit, integration, and protocol tests passed).
This commit is contained in:
1 parent
fac3bd7bb9
commit
5495cb5b34
6 files changed
+102
-18
No files matched your search
Generated
+1
@@ -4170,6 +4170,7 @@ name = "tsunagi-tcp-tls"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"caps",
|
||||
"chacha20poly1305",
|
||||
"hex",
|
||||
"hkdf",
|
||||
|
||||
@@ -2890,8 +2890,12 @@ fn host_section() -> report::Section {
|
||||
// explanation belongs in the runtime error, not in a column
|
||||
// the eye is meant to scan.
|
||||
host.push(
|
||||
Row::new(Health::Degraded, "privileges", "CAP_NET_ADMIN not held")
|
||||
.with_note(format!("sudo setcap cap_net_admin+p {}", program_path())),
|
||||
Row::new(Health::Degraded, "privileges", "CAP_NET_ADMIN not held").with_note(
|
||||
format!(
|
||||
"sudo setcap cap_net_admin,cap_net_bind_service+p {}",
|
||||
program_path()
|
||||
),
|
||||
),
|
||||
);
|
||||
host.push(Row::new(
|
||||
Health::Degraded,
|
||||
|
||||
@@ -29,6 +29,9 @@ thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
netdev = "0.46"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
caps = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tsunagi = { path = "../tsunagi", features = ["testing"] }
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -264,6 +264,60 @@ impl std::fmt::Debug for TcpTlsTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
struct NetBindServiceGuard {
|
||||
raised: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl NetBindServiceGuard {
|
||||
fn acquire() -> Self {
|
||||
let already = caps::has_cap(
|
||||
None,
|
||||
caps::CapSet::Effective,
|
||||
caps::Capability::CAP_NET_BIND_SERVICE,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
|
||||
if already {
|
||||
return Self { raised: false };
|
||||
}
|
||||
|
||||
let permitted = caps::has_cap(
|
||||
None,
|
||||
caps::CapSet::Permitted,
|
||||
caps::Capability::CAP_NET_BIND_SERVICE,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
|
||||
if permitted
|
||||
&& caps::raise(
|
||||
None,
|
||||
caps::CapSet::Effective,
|
||||
caps::Capability::CAP_NET_BIND_SERVICE,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Self { raised: true }
|
||||
} else {
|
||||
Self { raised: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl Drop for NetBindServiceGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.raised {
|
||||
let _ = caps::drop(
|
||||
None,
|
||||
caps::CapSet::Effective,
|
||||
caps::Capability::CAP_NET_BIND_SERVICE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TcpTlsTransport {
|
||||
/// Binds a new TCP TLS transport using the device identity and configured port.
|
||||
pub async fn bind(
|
||||
@@ -276,8 +330,21 @@ impl TcpTlsTransport {
|
||||
let target_port = requested_port.unwrap_or(DEFAULT_PORT);
|
||||
let bind_addr = SocketAddr::from(([0, 0, 0, 0], target_port));
|
||||
|
||||
let listener = match TcpListener::bind(bind_addr).await {
|
||||
Ok(l) => {
|
||||
let bind_result = {
|
||||
#[cfg(target_os = "linux")]
|
||||
let _guard = if target_port < 1024 {
|
||||
Some(NetBindServiceGuard::acquire())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
std::net::TcpListener::bind(bind_addr)
|
||||
};
|
||||
|
||||
let listener = match bind_result {
|
||||
Ok(std_listener) => {
|
||||
std_listener.set_nonblocking(true)?;
|
||||
let l = TcpListener::from_std(std_listener)?;
|
||||
tracing::info!(port = target_port, "TCP TLS listening");
|
||||
Some(Arc::new(l))
|
||||
}
|
||||
@@ -286,21 +353,28 @@ impl TcpTlsTransport {
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|_| "tsunagi".into());
|
||||
|
||||
tracing::warn!(
|
||||
port = target_port,
|
||||
%err,
|
||||
"Cannot bind to port {target_port} without privileges. \
|
||||
To allow port 443 and TUN on Linux, grant capabilities once with:\n \
|
||||
sudo setcap cap_net_admin,cap_net_bind_service+p {exe}\n\
|
||||
Falling back to port {FALLBACK_PORT}."
|
||||
);
|
||||
|
||||
if requested_port.is_none() {
|
||||
tracing::warn!(
|
||||
port = target_port,
|
||||
%err,
|
||||
"Cannot bind to default port {target_port} without privileges. \
|
||||
To allow port 443 and TUN on Linux, grant capabilities once with:\n \
|
||||
sudo setcap cap_net_admin,cap_net_bind_service+p {exe}\n\
|
||||
Falling back to port {FALLBACK_PORT}."
|
||||
);
|
||||
|
||||
let fallback_addr = SocketAddr::from(([0, 0, 0, 0], FALLBACK_PORT));
|
||||
let l = TcpListener::bind(fallback_addr).await?;
|
||||
tracing::info!(port = FALLBACK_PORT, "TCP TLS listening on fallback port");
|
||||
Some(Arc::new(l))
|
||||
} else {
|
||||
tracing::warn!(
|
||||
port = target_port,
|
||||
%err,
|
||||
"Cannot bind to requested port {target_port} without privileges. \
|
||||
To allow port {target_port} and TUN on Linux, grant capabilities once with:\n \
|
||||
sudo setcap cap_net_admin,cap_net_bind_service+p {exe}"
|
||||
);
|
||||
return Err(Box::new(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! Grant it with:
|
||||
//!
|
||||
//! ```text
|
||||
//! sudo setcap cap_net_admin+p /usr/local/bin/tsunagi
|
||||
//! sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi
|
||||
//! ```
|
||||
//!
|
||||
//! `+p` rather than `+ep`: with `+p` the capability is permitted but not
|
||||
@@ -49,7 +49,7 @@ impl Privilege {
|
||||
/// How to obtain it, for a diagnostic.
|
||||
pub fn how_to_grant(program: &str) -> String {
|
||||
format!(
|
||||
"Grant it once with `sudo setcap cap_net_admin+p {program}` \
|
||||
"Grant it once with `sudo setcap cap_net_admin,cap_net_bind_service+p {program}` \
|
||||
and the agent manages its own interface. Without it, run with \
|
||||
`--no-tun`: the tunnels still form, they just do not reach the \
|
||||
operating system."
|
||||
@@ -181,7 +181,9 @@ mod tests {
|
||||
#[test]
|
||||
fn the_grant_instructions_name_the_program() {
|
||||
let text = Privilege::how_to_grant("/usr/local/bin/tsunagi");
|
||||
assert!(text.contains("setcap cap_net_admin+p /usr/local/bin/tsunagi"));
|
||||
assert!(
|
||||
text.contains("setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi")
|
||||
);
|
||||
assert!(text.contains("--no-tun"), "the fallback is offered too");
|
||||
}
|
||||
|
||||
|
||||
@@ -435,7 +435,7 @@ mod system {
|
||||
start `tsunagi up` there."
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"Start `tsunagi up` as root or grant CAP_NET_ADMIN with \
|
||||
`sudo setcap cap_net_admin+p /path/to/tsunagi`."
|
||||
`sudo setcap cap_net_admin,cap_net_bind_service+p /path/to/tsunagi`."
|
||||
} else {
|
||||
"Start `tsunagi up` with the privileges required to create a TUN interface."
|
||||
};
|
||||
@@ -455,7 +455,7 @@ mod system {
|
||||
fn open_hint() -> &'static str {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
"Creating one needs CAP_NET_ADMIN; grant it with `setcap cap_net_admin+p`, \
|
||||
"Creating one needs CAP_NET_ADMIN; grant it with `setcap cap_net_admin,cap_net_bind_service+p`, \
|
||||
or run with `--no-tun` to keep the tunnels off the operating system."
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
Reference in new issue
Block a user