#!/usr/bin/env sh # ferrite — install script # Downloads pre-built Linux binaries from GitHub Releases and sets up the system service. # # Usage (Linux, run as root): # curl -fsSL https://raw.githubusercontent.com/koroblabs/ferrite-server/main/install.sh | sudo sh # # Or download and inspect first: # curl -fsSL https://raw.githubusercontent.com/koroblabs/ferrite-server/main/install.sh -o install.sh # sudo sh install.sh set -eu # ── Config ──────────────────────────────────────────────────────────────────── GITHUB_BASE_URL="${GITHUB_BASE_URL:-https://github.com}" GITHUB_API_BASE="${GITHUB_API_BASE:-https://api.github.com}" GITHUB_OWNER="${GITHUB_OWNER:-koroblabs}" GITHUB_REPO_SERVER="${GITHUB_REPO_SERVER:-ferrite-server}" GITHUB_REPO_WEB="${GITHUB_REPO_WEB:-ferrite-web}" BIN_DIR="/usr/local/bin" SERVICE_UPDATE_BIN_DIR="/usr/local/lib/ferrite/bin" CONFIG_DIR="/etc/ferrite" DATA_DIR="/var/lib/ferrite" WEB_PARENT="/usr/share/ferrite" WEB_DIR="${WEB_PARENT}/web" SERVICE_USER="ferrite" SERVICE_GROUP="ferrite" SYSTEMD_SERVICE="/etc/systemd/system/ferrite.service" OPENRC_SERVICE="/etc/init.d/ferrite" # ── Colors ──────────────────────────────────────────────────────────────────── if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; NC='' fi info() { printf "${BLUE}→${NC} %s\n" "$*"; } ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } die() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; } bold() { printf "${BOLD}%s${NC}\n" "$*"; } # ── Checks ──────────────────────────────────────────────────────────────────── [ "$(id -u)" -eq 0 ] || die "Run as root: sudo sh install.sh" for cmd in curl tar; do command -v "$cmd" >/dev/null 2>&1 || die "Required tool not found: $cmd" done # ── Platform detection ──────────────────────────────────────────────────────── OS=$(uname -s | tr '[:upper:]' '[:lower:]') # Asset names use Rust target triples — must match current_platform_target() in updater/server.rs. detect_asset_pattern() { case "$(uname -s)-$(uname -m)" in Linux-x86_64) printf "x86_64-unknown-linux-musl" ;; Linux-aarch64) printf "aarch64-unknown-linux-musl" ;; *) die "Unsupported platform: $(uname -s) $(uname -m). The installer currently supports Linux x86_64/arm64 only." ;; esac } ASSET_PATTERN=$(detect_asset_pattern) # Human-friendly name for display only. detect_platform_display() { case "$(uname -s)-$(uname -m)" in Linux-x86_64) printf "linux-x86_64" ;; Linux-aarch64) printf "linux-arm64" ;; *) printf "unknown" ;; esac } PLATFORM=$(detect_platform_display) # Detect Alpine Linux. IS_ALPINE=false [ -f /etc/alpine-release ] && IS_ALPINE=true # Detect init system. detect_init_system() { if [ -d /run/systemd/system ] || (command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1); then printf "systemd" elif command -v rc-service >/dev/null 2>&1; then printf "openrc" else printf "none" fi } INIT_SYSTEM=$(detect_init_system) if [ "$OS" = "linux" ] && { [ "$INIT_SYSTEM" = "systemd" ] || [ "$INIT_SYSTEM" = "openrc" ]; }; then SERVICE_BIN_DIR="$SERVICE_UPDATE_BIN_DIR" else SERVICE_BIN_DIR="$BIN_DIR" fi SERVICE_BIN="${SERVICE_BIN_DIR}/ferrite" PUBLIC_BIN="${BIN_DIR}/ferrite" # ── Release helpers ─────────────────────────────────────────────────────────── release_token() { if [ -n "${FERRITE_RELEASE_TOKEN:-}" ]; then printf '%s' "$FERRITE_RELEASE_TOKEN" elif [ -n "${GITHUB_TOKEN:-}" ]; then printf '%s' "$GITHUB_TOKEN" elif [ -n "${GITEA_TOKEN:-}" ]; then printf '%s' "$GITEA_TOKEN" fi } release_latest() { token=$(release_token) if [ -n "$token" ]; then curl -fsSL \ -H "Accept: application/json" \ -H "Authorization: Bearer ${token}" \ "${GITHUB_API_BASE}/repos/${GITHUB_OWNER}/$1/releases/latest" else curl -fsSL \ -H "Accept: application/json" \ "${GITHUB_API_BASE}/repos/${GITHUB_OWNER}/$1/releases/latest" fi } release_asset_url() { release_asset_field "$1" "$2" url } release_asset_digest() { release_asset_field "$1" "$2" digest } release_asset_field() { # Use awk over GitHub's release JSON to avoid a jq dependency. printf '%s\n' "$1" | tr "," "\n" | awk -v pattern="$2" -v want="$3" ' function json_value(line) { sub(/^[^:]*:[[:space:]]*"/, "", line) sub(/".*$/, "", line) return line } function maybe_emit() { target = name " " url if (url != "" && target ~ pattern && name !~ /\.sha256$/ && url !~ /\.sha256$/) { if (want == "url") { print url exit } if (want == "digest" && digest != "") { print digest exit } } } /"name"[[:space:]]*:/ { name = json_value($0) url = "" digest = "" } /"browser_download_url"[[:space:]]*:/ { url = json_value($0) maybe_emit() } /"digest"[[:space:]]*:/ { digest = json_value($0) maybe_emit() } ' } release_tag() { printf '%s' "$1" \ | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ | head -1 } release_version() { printf '%s' "$1" | sed 's/^v//' } download_asset() { token=$(release_token) if [ -n "$token" ]; then curl -fL --progress-bar -H "Authorization: Bearer ${token}" "$1" -o "$2" else curl -fL --progress-bar "$1" -o "$2" fi } normalize_sha256() { printf '%s\n' "$1" | grep -Eo '[A-Fa-f0-9]{64}' | head -1 | tr '[:upper:]' '[:lower:]' } sha256_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$1" | awk '{print $NF}' else return 1 fi } verify_download_sha256() { file="$1" expected="$2" label="$3" [ -n "$expected" ] || return 0 actual=$(sha256_file "$file") || { warn "No SHA256 tool found; skipping checksum verification for ${label}." return 0 } [ "$actual" = "$expected" ] || die "${label} checksum mismatch: expected ${expected}, got ${actual}" } # ── Install server binary ───────────────────────────────────────────────────── install_server() { bold "Installing ferrite server..." mkdir -p "$SERVICE_BIN_DIR" "$BIN_DIR" json=$(release_latest "$GITHUB_REPO_SERVER") \ || die "Failed to fetch release info from GitHub. Check your network and release assets." version=$(release_tag "$json") [ -n "$version" ] || die "Could not determine release version." info "Latest version: ${version}" # Try tar.gz first (matches release.yml packaging), fall back to bare binary. is_archive=false asset_pattern="${ASSET_PATTERN}.*\\.tar\\.gz" url=$(release_asset_url "$json" "$asset_pattern") if [ -n "$url" ]; then is_archive=true else asset_pattern="${ASSET_PATTERN}" url=$(release_asset_url "$json" "$asset_pattern") fi [ -n "$url" ] || die "No release asset found for '${ASSET_PATTERN}'. The platform may not have a build yet." expected_sha=$(normalize_sha256 "$(release_asset_digest "$json" "$asset_pattern")") info "Downloading from: ${url}" tmp=$(mktemp) download_asset "$url" "$tmp" if [ -n "$expected_sha" ]; then verify_download_sha256 "$tmp" "$expected_sha" "Server archive" else warn "GitHub release did not expose a SHA256 digest for the server asset; skipping checksum verification." fi if $is_archive; then # Extract ferrite binary from the archive. Uses a temp dir for BusyBox tar # compatibility (Alpine) — avoids GNU tar-only flags like --wildcards and -O. tmp_dir=$(mktemp -d) tar -xzf "$tmp" -C "$tmp_dir" bin_path=$(find "$tmp_dir" -name 'ferrite' -type f | head -1) [ -n "$bin_path" ] || die "Could not find 'ferrite' binary in the release archive." install -m 755 "$bin_path" "$SERVICE_BIN" rm -rf "$tmp_dir" "$tmp" else install -m 755 "$tmp" "$SERVICE_BIN" rm -f "$tmp" fi if [ "$SERVICE_BIN" != "$PUBLIC_BIN" ]; then ln -sfn "$SERVICE_BIN" "$PUBLIC_BIN" fi if [ -n "$expected_sha" ]; then printf '%s\n' "$expected_sha" > "${SERVICE_BIN}.sha256" if [ "$SERVICE_BIN" != "$PUBLIC_BIN" ]; then ln -sfn "${SERVICE_BIN}.sha256" "${PUBLIC_BIN}.sha256" fi else rm -f "${SERVICE_BIN}.sha256" "${PUBLIC_BIN}.sha256" fi ok "Installed ferrite ${version} → ${SERVICE_BIN}" if [ "$SERVICE_BIN" != "$PUBLIC_BIN" ]; then ok "CLI link: ${PUBLIC_BIN} → ${SERVICE_BIN}" fi } # ── Install web UI ──────────────────────────────────────────────────────────── install_web() { bold "Installing ferrite web UI..." json=$(release_latest "$GITHUB_REPO_WEB" 2>/dev/null) || { warn "Could not fetch web UI release (repo may not have releases yet). Skipping." return 0 } version=$(release_tag "$json") [ -n "$version" ] || { warn "No web UI release found. Skipping."; return 0; } info "Latest web UI version: ${version}" installed_version=$(release_version "$version") asset_pattern="\\.tar\\.gz" url=$(release_asset_url "$json" "$asset_pattern") if [ -z "$url" ]; then asset_pattern="\\.zip" url=$(release_asset_url "$json" "$asset_pattern") fi [ -n "$url" ] || { warn "No web UI asset found in release. Skipping."; return 0; } expected_sha=$(normalize_sha256 "$(release_asset_digest "$json" "$asset_pattern")") info "Downloading from: ${url}" tmp=$(mktemp) download_asset "$url" "$tmp" if [ -n "$expected_sha" ]; then verify_download_sha256 "$tmp" "$expected_sha" "Web UI archive" else warn "GitHub release did not expose a SHA256 digest for the web UI asset; skipping checksum verification." fi rm -rf "$WEB_DIR" mkdir -p "$WEB_DIR" if tar -tzf "$tmp" | head -1 | grep -q '/'; then tar -xzf "$tmp" -C "$WEB_DIR" --strip-components=1 else tar -xzf "$tmp" -C "$WEB_DIR" fi rm -f "$tmp" printf '%s\n' "$installed_version" > "${WEB_DIR}.version" if [ -n "$expected_sha" ]; then printf '%s\n' "$expected_sha" > "${WEB_DIR}.sha256" else rm -f "${WEB_DIR}.sha256" fi ok "Installed web UI ${version} → ${WEB_DIR}" } # ── Create system user ──────────────────────────────────────────────────────── group_exists() { getent group "$1" >/dev/null 2>&1 || grep -q "^$1:" /etc/group 2>/dev/null } ensure_linux_group() { group_exists "$SERVICE_GROUP" && return 0 if command -v groupadd >/dev/null 2>&1; then groupadd --system "$SERVICE_GROUP" elif command -v addgroup >/dev/null 2>&1; then addgroup -S "$SERVICE_GROUP" 2>/dev/null || addgroup "$SERVICE_GROUP" else die "Could not create group '${SERVICE_GROUP}': groupadd/addgroup not found." fi ok "Created system group: ${SERVICE_GROUP}" } ensure_linux_user_group() { if command -v usermod >/dev/null 2>&1; then usermod -g "$SERVICE_GROUP" "$SERVICE_USER" 2>/dev/null || true elif command -v addgroup >/dev/null 2>&1; then addgroup "$SERVICE_USER" "$SERVICE_GROUP" 2>/dev/null || true fi } service_owner() { if [ "$OS" = "linux" ]; then printf "%s:%s" "$SERVICE_USER" "$SERVICE_GROUP" else printf "%s" "$SERVICE_USER" fi } configured_api_port() { awk ' /^\[api\]/ { in_api = 1; next } /^\[/ { in_api = 0 } in_api && /^[[:space:]]*bind_addr[[:space:]]*=/ { value = $0 sub(/^[^"]*"/, "", value) sub(/".*$/, "", value) sub(/^.*:/, "", value) if (value ~ /^[0-9]+$/) { print value exit } } ' "${CONFIG_DIR}/config.toml" } create_user() { if [ "$OS" = "linux" ]; then ensure_linux_group if id "$SERVICE_USER" >/dev/null 2>&1; then ensure_linux_user_group ok "System user '${SERVICE_USER}' already exists" return 0 fi if $IS_ALPINE; then # Alpine uses BusyBox adduser (no useradd). adduser -S -D -H -s /sbin/nologin -h "$DATA_DIR" -G "$SERVICE_GROUP" "$SERVICE_USER" else useradd --system --gid "$SERVICE_GROUP" --no-create-home --shell /usr/sbin/nologin \ --home-dir "$DATA_DIR" "$SERVICE_USER" fi ok "Created system user: ${SERVICE_USER}" fi } # ── Directories and config ──────────────────────────────────────────────────── install_dirs_and_config() { bold "Setting up directories and config..." mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$WEB_PARENT" "$WEB_DIR" "$SERVICE_BIN_DIR" if [ -f "${CONFIG_DIR}/config.toml" ]; then ok "Config already exists at ${CONFIG_DIR}/config.toml — skipping (not overwritten)" else cat > "${CONFIG_DIR}/config.toml" << EOF # ferrite configuration # Edit to your needs, then restart the ferrite service. web_dir = "${WEB_DIR}" custom_records = [] [dns] bind_addr = "[::]:53" cache_size = 10000 min_ttl = 360 max_ttl = 3600 log_ignore = [ "fe.te", "*.arpa", "*.local", "*.localdomain", ] [[upstream]] type = "https" url = "https://cloudflare-dns.com/dns-query" bootstrap_ip = "1.1.1.1" [storage] path = "${DATA_DIR}/ferrite.db" log_retention_days = 30 [api] bind_addr = "0.0.0.0:80" [panel] enabled = true domain = "fe.te" [blocklist] whitelist = [] wildcard_block = [] [[blocklist.lists]] name = "StevenBlack" url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" enabled = true EOF ok "Config written to ${CONFIG_DIR}/config.toml" fi chown -R "$(service_owner)" "$CONFIG_DIR" "$DATA_DIR" "$WEB_PARENT" 2>/dev/null || true chmod 750 "$CONFIG_DIR" "$DATA_DIR" "$WEB_PARENT" "$WEB_DIR" chmod 640 "${CONFIG_DIR}/config.toml" if [ "$SERVICE_BIN_DIR" != "$BIN_DIR" ]; then chown -R "$(service_owner)" "$SERVICE_BIN_DIR" 2>/dev/null || true chmod 755 "$SERVICE_BIN_DIR" fi } # ── systemd-resolved conflict (Debian/Ubuntu) ──────────────────────────────── fix_resolved() { [ "$OS" = "linux" ] || return 0 [ "$INIT_SYSTEM" = "systemd" ] || return 0 systemctl is-active --quiet systemd-resolved 2>/dev/null || return 0 if ss -ulnp 2>/dev/null | grep -q ':53 ' \ || ss -tlnp 2>/dev/null | grep -q ':53 '; then info "Disabling systemd-resolved stub listener (it occupies port 53)..." mkdir -p /etc/systemd/resolved.conf.d cat > /etc/systemd/resolved.conf.d/ferrite.conf << 'EOF' [Resolve] DNSStubListener=no EOF systemctl restart systemd-resolved ok "systemd-resolved stub listener disabled" fi } # ── Systemd service ─────────────────────────────────────────────────────────── install_systemd() { [ "$OS" = "linux" ] || return 0 [ "$INIT_SYSTEM" = "systemd" ] || return 0 bold "Installing systemd service..." cat > "$SYSTEMD_SERVICE" << EOF [Unit] Description=Ferrite DNS ad-blocker Documentation=${GITHUB_BASE_URL}/${GITHUB_OWNER}/${GITHUB_REPO_SERVER} After=network-online.target Wants=network-online.target [Service] Type=simple User=${SERVICE_USER} Group=${SERVICE_GROUP} ExecStart=${SERVICE_BIN} # Reload = restart in place: a fresh process from the binary on disk, keeping # the DNS and panel sockets, so not a query is dropped. ExecReload=/bin/kill -HUP \$MAINPID Restart=always RestartSec=5 TimeoutStopSec=30 # CAP_NET_BIND_SERVICE: bind port 53 without running as root. # CAP_NET_ADMIN: kernel-WireGuard tunnels (netlink wg netdev + policy routing); # without it wireguard egresses fall back to the userspace backend. # CAP_NET_RAW: IPv6 router advertisements announcing ferrite as the DNS server # (off unless switched on in the panel). AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_NET_RAW CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_NET_RAW # Hardening NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=${DATA_DIR} ${CONFIG_DIR} ${WEB_PARENT} ${SERVICE_BIN_DIR} RuntimeDirectory=ferrite StandardOutput=journal StandardError=journal SyslogIdentifier=ferrite [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable ferrite ok "systemd service installed and enabled" } # ── WireGuard kernel module (kernel-mode tunnels) ───────────────────────────── # Best-effort: with the module loaded (and CAP_NET_ADMIN from the service unit) # WireGuard egresses use the kernel backend — multicore crypto, line-rate # throughput. Without it ferrite still works: tunnels fall back to the # userspace backend automatically. enable_wireguard_module() { [ "$OS" = "linux" ] || return 0 if modprobe wireguard 2>/dev/null; then # Persist across reboots. Netdev creation would normally autoload the # module anyway; loading it explicitly gives a clear signal at install # time and survives hardened setups that disable autoload. if [ -d /etc/modules-load.d ]; then printf 'wireguard\n' > /etc/modules-load.d/ferrite.conf fi ok "wireguard kernel module loaded (kernel-mode tunnels available)" else info "wireguard kernel module unavailable — tunnels will use the userspace backend" fi } # ── OpenRC service (Alpine) ─────────────────────────────────────────────────── install_openrc() { [ "$OS" = "linux" ] || return 0 [ "$INIT_SYSTEM" = "openrc" ] || return 0 bold "Installing OpenRC service..." cat > "$OPENRC_SERVICE" << EOF #!/sbin/openrc-run name="ferrite" description="Ferrite DNS ad-blocker" supervisor=supervise-daemon command="${SERVICE_BIN}" command_user="${SERVICE_USER}:${SERVICE_GROUP}" capabilities="^cap_net_bind_service,^cap_net_admin,^cap_net_raw" respawn_delay=5 respawn_max=3 respawn_period=60 pidfile="/run/\${RC_SVCNAME}.pid" output_log="/var/log/ferrite.log" error_log="/var/log/ferrite.log" depend() { need net after firewall } # Reload = restart in place: a fresh process from the binary on disk, keeping # the DNS and panel sockets, so not a query is dropped. extra_started_commands="reload" reload() { ebegin "Restarting \${RC_SVCNAME} in place" supervise-daemon "\${RC_SVCNAME}" --signal HUP eend \$? } EOF chmod +x "$OPENRC_SERVICE" rc-update add ferrite default ok "OpenRC service installed and enabled (default runlevel, supervise-daemon)" } # ── Firewall: open port 53 ──────────────────────────────────────────────────── open_port_53() { [ "$OS" = "linux" ] || return 0 bold "Opening port 53 in firewall..." if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "active"; then ufw allow 53/udp comment "ferrite DNS" >/dev/null ufw allow 53/tcp comment "ferrite DNS" >/dev/null ok "UFW: port 53/udp and 53/tcp allowed" elif command -v firewall-cmd >/dev/null 2>&1 \ && systemctl is-active --quiet firewalld 2>/dev/null; then firewall-cmd --permanent --add-port=53/udp >/dev/null firewall-cmd --permanent --add-port=53/tcp >/dev/null firewall-cmd --reload >/dev/null ok "firewalld: port 53/udp and 53/tcp allowed" elif command -v iptables >/dev/null 2>&1; then iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null \ || iptables -I INPUT -p udp --dport 53 -j ACCEPT iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null \ || iptables -I INPUT -p tcp --dport 53 -j ACCEPT warn "iptables: port 53 opened (not persistent). To persist:" if $IS_ALPINE; then warn " apk add iptables && rc-update add iptables boot && rc-service iptables save" else warn " Debian/Ubuntu: apt install iptables-persistent && netfilter-persistent save" warn " RHEL/Rocky: service iptables save" fi else warn "No active firewall detected. Make sure port 53 is accessible on your network interface." fi } # ── Start / restart service ─────────────────────────────────────────────────── start_service() { if [ "$OS" = "linux" ]; then bold "Starting ferrite service..." case "$INIT_SYSTEM" in systemd) if systemctl is-active --quiet ferrite 2>/dev/null; then systemctl restart ferrite else systemctl start ferrite fi sleep 1 if systemctl is-active --quiet ferrite; then ok "Service is running" else warn "Service may have failed to start. Check: journalctl -u ferrite -n 30" fi ;; openrc) rc-service ferrite start sleep 1 if rc-service ferrite status 2>/dev/null | grep -q "started"; then ok "Service is running" else warn "Service may have failed to start. Check: cat /var/log/ferrite.log" fi ;; *) warn "Unknown init system. Start manually: ${SERVICE_BIN}" ;; esac fi } # ── Main ────────────────────────────────────────────────────────────────────── main() { echo bold " Ferrite DNS ad-blocker — installer" printf " ════════════════════════════════════\n" echo info "Platform: ${PLATFORM} (${INIT_SYSTEM})" echo install_server echo install_web echo create_user install_dirs_and_config echo fix_resolved case "$OS" in linux) enable_wireguard_module case "$INIT_SYSTEM" in systemd) install_systemd ;; openrc) install_openrc ;; *) warn "Unknown init system — skipping service setup. Start manually: ${SERVICE_BIN}" ;; esac ;; esac echo open_port_53 echo start_service # Determine LAN IP for display. if [ "$OS" = "linux" ]; then lan_ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}') else lan_ip=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || printf "localhost") fi lan_ip="${lan_ip:-localhost}" api_port=$(configured_api_port) api_port="${api_port:-80}" echo bold " ✓ Installation complete!" echo printf " Config: %s/config.toml\n" "$CONFIG_DIR" printf " Database: %s/ferrite.db\n" "$DATA_DIR" printf " Web UI: http://%s:%s\n" "$lan_ip" "$api_port" echo printf " Next steps:\n" printf " 1. Point your router's DNS to %s\n" "$lan_ip" printf " 2. Run 'ferrite setup' to auto-detect your local network zones\n" printf " 3. Run 'ferrite passwd' to set a web UI password\n" echo case "$INIT_SYSTEM" in systemd) printf " Service commands:\n" printf " systemctl status ferrite\n" printf " journalctl -u ferrite -f\n" printf " systemctl restart ferrite\n" ;; openrc) printf " Service commands:\n" printf " rc-service ferrite status\n" printf " tail -f /var/log/ferrite.log\n" printf " rc-service ferrite restart\n" ;; esac echo } main "$@"