use crate::*;
#[cfg(target_os = "linux")]
pub(crate) fn ensure_rayfish_group() {
let exists = std::process::Command::new("getent")
.args(["group", "rayfish"])
.status()
.map(|s| s.success())
.unwrap_or(false);
if !exists {
let _ = std::process::Command::new("groupadd")
.args(["--system", "rayfish"])
.status();
}
}
pub(crate) fn strip_deleted_suffix(path: &str) -> &str {
path.strip_suffix(" (deleted)").unwrap_or(path)
}
#[allow(unused_variables)]
pub(crate) fn ensure_service_installed() -> Result<()> {
let exe = std::env::current_exe()
.context("failed to determine current executable path")?
.to_string_lossy()
.into_owned();
let exe = strip_deleted_suffix(&exe).to_owned();
#[cfg(target_os = "linux")]
{
ensure_rayfish_group();
let path = std::path::Path::new("/etc/systemd/system/rayfish.service");
let service =
include_str!("../../contrib/rayfish.service").replace("/usr/local/bin/ray", &exe);
std::fs::write(path, service)
.with_context(|| format!("failed to write {}", path.display()))?;
run_cmd("systemctl", &["daemon-reload"]);
return Ok(());
}
#[cfg(target_os = "macos")]
{
let path = std::path::Path::new("/Library/LaunchDaemons/com.rayfish.vpn.plist");
let plist =
include_str!("../../contrib/com.rayfish.vpn.plist").replace("/usr/local/bin/ray", &exe);
std::fs::write(path, plist)
.with_context(|| format!("failed to write {}", path.display()))?;
return Ok(());
}
#[allow(unreachable_code)]
{
anyhow::bail!("system service not supported on this platform");
}
}
pub(crate) async fn cmd_up(hostname: Option<String>) -> Result<()> {
if let Ok(mut stream) = ipc::connect().await {
ipc::send(&mut stream, ipc::IpcMessage::Up { hostname }).await?;
match ipc::recv(&mut stream).await? {
ipc::IpcMessage::Ok { message } => println!("{message}"),
ipc::IpcMessage::Error { message } => print_error("error", &message, None),
other => eprintln!("Unexpected response: {other:?}"),
}
return Ok(());
}
if unsafe { libc::geteuid() } != 0 {
eprintln!(
"rayfish service is not running. Start it with: sudo ray up\n\
(the daemon needs root to install the system service and create the TUN device)"
);
std::process::exit(1);
}
install_and_start_service(hostname).await
}
pub(crate) async fn install_and_start_service(hostname: Option<String>) -> Result<()> {
ensure_service_installed()?;
#[cfg(target_os = "linux")]
{
run_cmd("systemctl", &["enable", "rayfish"]);
run_cmd("systemctl", &["restart", "rayfish"]);
}
#[cfg(target_os = "macos")]
{
let path = "/Library/LaunchDaemons/com.rayfish.vpn.plist";
run_cmd_quiet("launchctl", &["unload", path]);
run_cmd("launchctl", &["load", "-w", path]);
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
anyhow::bail!("system service not supported on this platform");
}
let spinner = progress::spinner("starting service…");
let daemon = wait_for_daemon(DAEMON_REACHABLE_TIMEOUT).await;
spinner.finish_and_clear();
match daemon {
Some(mut stream) => {
ipc::send(&mut stream, ipc::IpcMessage::Up { hostname }).await?;
match ipc::recv(&mut stream).await? {
ipc::IpcMessage::Ok { message } => println!("rayfish service started. {message}"),
ipc::IpcMessage::Error { message } => print_error("error", &message, None),
other => eprintln!("Unexpected response: {other:?}"),
}
grant_operator_to_invoking_user().await;
Ok(())
}
None => {
eprintln!(
"rayfish service was started but the daemon never became reachable.\n\
It likely crashed on startup — a common cause is another VPN (e.g. Tailscale)\n\
already using the 100.64.0.0/10 range, DNS port 53, or a conflicting route."
);
print_daemon_log_tail();
std::process::exit(1);
}
}
}
pub(crate) async fn grant_operator_to_invoking_user() {
let Ok(user) = std::env::var("SUDO_USER") else {
return;
};
if user == "root" {
return;
}
let Some(uid) = uid_for_user(&user) else {
return;
};
if let Ok(mut stream) = ipc::connect().await {
let _ = ipc::send(&mut stream, ipc::IpcMessage::SetOperator { uid }).await;
if let Ok(ipc::IpcMessage::Ok { .. }) = ipc::recv(&mut stream).await {
println!("granted operator access to '{user}' — run ray without sudo");
}
}
}
pub(crate) fn require_root() -> Result<()> {
if unsafe { libc::geteuid() } != 0 {
eprintln!(
"this command manages the system service and needs root.\n\
Re-run with: sudo ray <command>"
);
std::process::exit(1);
}
Ok(())
}
pub(crate) async fn cmd_install() -> Result<()> {
require_root()?;
install_and_start_service(None).await
}
pub(crate) fn service_unit_exists() -> bool {
#[cfg(target_os = "linux")]
{
return std::path::Path::new("/etc/systemd/system/rayfish.service").exists();
}
#[cfg(target_os = "macos")]
{
return std::path::Path::new("/Library/LaunchDaemons/com.rayfish.vpn.plist").exists();
}
#[allow(unreachable_code)]
false
}
#[allow(unreachable_code)]
pub(crate) async fn restart_service_and_wait() -> Result<()> {
#[cfg(target_os = "linux")]
run_cmd("systemctl", &["restart", "rayfish"]);
#[cfg(target_os = "macos")]
run_cmd("launchctl", &["kickstart", "-k", "system/com.rayfish.vpn"]);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
anyhow::bail!("system service not supported on this platform");
match wait_for_daemon(DAEMON_REACHABLE_TIMEOUT).await {
Some(_) => {
println!("rayfish service restarted.");
Ok(())
}
None => {
eprintln!("rayfish service was restarted but the daemon never became reachable.");
print_daemon_log_tail();
std::process::exit(1);
}
}
}
pub(crate) async fn cmd_restart() -> Result<()> {
require_root()?;
if !service_unit_exists() {
eprintln!("rayfish service is not installed. Run: sudo ray up");
std::process::exit(1);
}
restart_service_and_wait().await
}
#[allow(unreachable_code)]
pub(crate) async fn cmd_stop() -> Result<()> {
require_root()?;
if !service_unit_exists() {
eprintln!("rayfish service is not installed. Nothing to stop.");
std::process::exit(1);
}
#[cfg(target_os = "linux")]
run_cmd("systemctl", &["stop", "rayfish"]);
#[cfg(target_os = "macos")]
run_cmd(
"launchctl",
&["unload", "/Library/LaunchDaemons/com.rayfish.vpn.plist"],
);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
anyhow::bail!("system service not supported on this platform");
println!("rayfish service stopped.");
Ok(())
}
#[allow(unreachable_code)]
pub(crate) async fn cmd_start() -> Result<()> {
require_root()?;
if !service_unit_exists() {
eprintln!("rayfish service is not installed. Run: sudo ray up");
std::process::exit(1);
}
#[cfg(target_os = "linux")]
run_cmd("systemctl", &["start", "rayfish"]);
#[cfg(target_os = "macos")]
run_cmd(
"launchctl",
&["load", "-w", "/Library/LaunchDaemons/com.rayfish.vpn.plist"],
);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
anyhow::bail!("system service not supported on this platform");
match wait_for_daemon(DAEMON_REACHABLE_TIMEOUT).await {
Some(_) => {
println!("rayfish service started.");
Ok(())
}
None => {
eprintln!("rayfish service was started but the daemon never became reachable.");
print_daemon_log_tail();
std::process::exit(1);
}
}
}
pub(crate) const REPO_SLUG: &str = "rayfish/rayfish";