#!/bin/sh # SecuBox Jabber Manager - LXC Debian container with Prosody XMPP Server CONFIG="jabber" LXC_NAME="jabber" LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONF="$LXC_PATH/$LXC_NAME/config" DATA_PATH_DEFAULT="/srv/jabber" PROSODY_VERSION="0.12" OPKG_UPDATED=0 usage() { cat <<'USAGE' Usage: jabberctl Installation: install Create LXC container with Prosody XMPP server uninstall Remove container (preserves data) update Update Prosody to latest version check Run prerequisite checks Service: start Start Jabber/XMPP server (via init) stop Stop Jabber/XMPP server restart Restart Jabber/XMPP server status Show container and service status logs [N] Show last N lines of logs (default: 50) shell Open interactive shell in container Users: user add [password] Create user (e.g. user@domain) user del Delete user user passwd [password] Change password user list List all users Rooms (MUC): room create Create conference room room delete Delete conference room room list List all rooms Exposure: configure-haproxy Setup HAProxy vhost for HTTPS/WSS emancipate Full exposure (HAProxy + ACME + DNS + S2S) Backup: backup [path] Backup database and config restore Restore from backup Internal: service-run Run container via procd service-stop Stop container USAGE } # ---------- helpers ---------- require_root() { [ "$(id -u)" -eq 0 ]; } uci_get() { local key="$1" local section="${2:-main}" uci -q get ${CONFIG}.${section}.$key } uci_set() { local key="$1" local value="$2" local section="${3:-main}" uci set ${CONFIG}.${section}.$key="$value" } log_info() { echo "[INFO] $*"; logger -t jabberctl "$*"; } log_warn() { echo "[WARN] $*"; logger -t jabberctl -p warning "$*"; } log_error() { echo "[ERROR] $*" >&2; logger -t jabberctl -p err "$*"; } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } ensure_packages() { for pkg in "$@"; do if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then if [ "$OPKG_UPDATED" -eq 0 ]; then opkg update || return 1 OPKG_UPDATED=1 fi opkg install "$pkg" || return 1 fi done } defaults() { data_path="$(uci_get data_path || echo $DATA_PATH_DEFAULT)" memory_limit="$(uci_get memory_limit || echo 512)" hostname="$(uci_get hostname server || echo jabber.local)" c2s_port="$(uci_get c2s_port server || echo 5222)" s2s_port="$(uci_get s2s_port server || echo 5269)" http_port="$(uci_get http_port server || echo 5280)" https_port="$(uci_get https_port server || echo 5281)" } detect_arch() { case "$(uname -m)" in aarch64) echo "aarch64" ;; armv7l) echo "armv7" ;; x86_64) echo "x86_64" ;; *) echo "x86_64" ;; esac } generate_password() { head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16 } # ---------- LXC helpers ---------- lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" } lxc_exists() { [ -f "$LXC_CONF" ] && [ -d "$LXC_ROOTFS" ] } lxc_exec() { lxc-attach -n "$LXC_NAME" -- "$@" } lxc_stop() { if lxc_running; then lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true sleep 2 fi } # ---------- prosodyctl wrapper ---------- prosodyctl() { lxc_exec prosodyctl "$@" } # ---------- rootfs creation ---------- lxc_create_rootfs() { local arch=$(detect_arch) # Map to Debian architecture names local debian_arch case "$arch" in aarch64) debian_arch="arm64" ;; armv7) debian_arch="armhf" ;; x86_64) debian_arch="amd64" ;; *) debian_arch="amd64" ;; esac ensure_dir "$LXC_ROOTFS" # Minimal Debian rootfs via tarball from LXC image server local rootfs_url="https://images.linuxcontainers.org/images/debian/bookworm/${debian_arch}/default/" log_info "Downloading Debian bookworm rootfs for ${debian_arch}..." # Get latest build directory local latest_path latest_path=$(wget -q -O - "$rootfs_url" 2>/dev/null | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | tail -1) if [ -z "$latest_path" ]; then log_error "Failed to find latest Debian rootfs build" return 1 fi local tarball="/tmp/debian-jabber.tar.xz" local tarball_url="${rootfs_url}${latest_path}/rootfs.tar.xz" wget -q -O "$tarball" "$tarball_url" || { log_error "Failed to download Debian rootfs from $tarball_url" return 1 } tar -xJf "$tarball" -C "$LXC_ROOTFS" || { log_error "Failed to extract Debian rootfs" return 1 } rm -f "$tarball" # DNS cp /etc/resolv.conf "$LXC_ROOTFS/etc/resolv.conf" 2>/dev/null || \ echo "nameserver 8.8.8.8" > "$LXC_ROOTFS/etc/resolv.conf" # Create minimal /dev for chroot operations mkdir -p "$LXC_ROOTFS/dev" [ -c "$LXC_ROOTFS/dev/null" ] || mknod -m 666 "$LXC_ROOTFS/dev/null" c 1 3 2>/dev/null [ -c "$LXC_ROOTFS/dev/zero" ] || mknod -m 666 "$LXC_ROOTFS/dev/zero" c 1 5 2>/dev/null [ -c "$LXC_ROOTFS/dev/random" ] || mknod -m 666 "$LXC_ROOTFS/dev/random" c 1 8 2>/dev/null [ -c "$LXC_ROOTFS/dev/urandom" ] || mknod -m 666 "$LXC_ROOTFS/dev/urandom" c 1 9 2>/dev/null # Configure apt sources cat > "$LXC_ROOTFS/etc/apt/sources.list" <<'SOURCES' deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware SOURCES # Install Prosody XMPP server log_info "Installing Prosody XMPP server..." chroot "$LXC_ROOTFS" /bin/sh -c " export DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get install -y --no-install-recommends \ prosody \ prosody-modules \ lua-sec \ lua-event \ lua-dbi-sqlite3 \ lua-zlib \ ca-certificates \ openssl \ procps " || { log_error "Failed to install Prosody" return 1 } # Create directories mkdir -p "$LXC_ROOTFS/var/lib/prosody" mkdir -p "$LXC_ROOTFS/var/log/prosody" mkdir -p "$LXC_ROOTFS/etc/prosody/conf.d" mkdir -p "$LXC_ROOTFS/var/lib/prosody/http_upload" # Create startup script create_startup_script # Clean up apt cache chroot "$LXC_ROOTFS" /bin/sh -c " apt-get clean rm -rf /var/lib/apt/lists/* " log_info "Rootfs created successfully" } create_startup_script() { cat > "$LXC_ROOTFS/opt/start-jabber.sh" <<'STARTUP' #!/bin/bash export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin # Get hostname from environment or default XMPP_DOMAIN="${XMPP_HOSTNAME:-jabber.local}" ADMIN_USER="${XMPP_ADMIN:-admin}" # Generate Prosody config if not exists if [ ! -f /etc/prosody/prosody.cfg.lua.configured ]; then echo "[JABBER] Generating Prosody configuration..." # Create main config FIRST (before cert generation) cat > /etc/prosody/prosody.cfg.lua </dev/null chown prosody:prosody "${XMPP_DOMAIN}.crt" "${XMPP_DOMAIN}.key" chmod 600 "${XMPP_DOMAIN}.key" touch /etc/prosody/prosody.cfg.lua.configured echo "[JABBER] Configuration generated" fi # Ensure proper permissions chown -R prosody:prosody /var/lib/prosody chown -R prosody:prosody /var/log/prosody echo "[JABBER] Starting Prosody XMPP server..." # Run Prosody in foreground exec /usr/bin/prosody --no-daemonize STARTUP chmod +x "$LXC_ROOTFS/opt/start-jabber.sh" } lxc_create_config() { defaults local mem_bytes=$((memory_limit * 1024 * 1024)) ensure_dir "$LXC_PATH/$LXC_NAME" ensure_dir "$data_path" ensure_dir "$data_path/data" ensure_dir "$data_path/certs" cat > "$LXC_CONF" </dev/null || true fi local lan_ip=$(uci -q get network.lan.ipaddr || echo '192.168.255.1') log_info "" log_info "==============================================" log_info " Jabber/XMPP Server installed!" log_info "==============================================" log_info "" log_info " Domain: $hostname" log_info " C2S Port: $c2s_port (client connections)" log_info " S2S Port: $s2s_port (server federation)" log_info " HTTP/BOSH: http://${lan_ip}:$http_port/http-bind" log_info " WebSocket: ws://${lan_ip}:$http_port/xmpp-websocket" log_info "" log_info " Admin JID: ${admin_user}@${hostname}" log_info " Password: $admin_pass" log_info "" log_info " Clients: Conversations (Android), Monal (iOS)," log_info " Gajim (Desktop), Dino (Linux)" log_info "" log_info " Expose externally:" log_info " jabberctl emancipate xmpp.example.com" log_info "" } cmd_uninstall() { require_root || { log_error "Must run as root"; return 1; } log_info "Uninstalling Jabber/XMPP server..." # Stop and disable /etc/init.d/jabber stop 2>/dev/null /etc/init.d/jabber disable 2>/dev/null lxc_stop # Remove container but keep data rm -rf "$LXC_ROOTFS" "$LXC_CONF" uci_set enabled '0' uci commit "$CONFIG" defaults log_info "Container removed. Data preserved in $data_path" } cmd_update() { require_root || { log_error "Must run as root"; return 1; } log_info "Updating Prosody..." if lxc_running; then lxc_exec apt-get update lxc_exec apt-get upgrade -y prosody prosody-modules lxc_exec prosodyctl restart log_info "Prosody updated successfully" else log_error "Container not running" return 1 fi } cmd_check() { echo "Jabber/XMPP Prerequisites Check" echo "================================" # LXC if command -v lxc-start >/dev/null 2>&1; then echo "[OK] LXC installed" else echo "[FAIL] LXC not installed" fi # Container exists if lxc_exists; then echo "[OK] Container exists" else echo "[--] Container not created" fi # Container running if lxc_running; then echo "[OK] Container running" else echo "[--] Container not running" fi # Prosody ports defaults for port in $c2s_port $s2s_port $http_port; do if netstat -tln 2>/dev/null | grep -q ":${port} " || \ grep -q ":$(printf '%04X' $port) " /proc/net/tcp 2>/dev/null; then echo "[OK] Port $port listening" else echo "[--] Port $port not listening" fi done # Prosody process (runs as lua5.4) if lxc_running; then if lxc_exec pgrep -f "lua.*prosody" >/dev/null 2>&1; then echo "[OK] Prosody process running" else echo "[FAIL] Prosody process not running" fi fi } cmd_status() { defaults # JSON output for RPCD if [ "$1" = "--json" ]; then local running=0 local prosody_proc=0 local user_count=0 lxc_running && running=1 if [ "$running" = "1" ]; then lxc_exec pgrep -f "lua.*prosody" >/dev/null 2>&1 && prosody_proc=1 user_count=$(lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) fi cat </dev/null 2>&1 && echo " Prosody: UP" || echo " Prosody: DOWN" # User count local users=$(lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) echo "" echo "Users: $users registered" else echo "State: STOPPED" fi echo "" local lan_ip=$(uci -q get network.lan.ipaddr || echo '192.168.255.1') echo "Connection:" echo " XMPP: ${hostname}:${c2s_port}" echo " BOSH: http://${lan_ip}:${http_port}/http-bind" echo " WebSocket: ws://${lan_ip}:${http_port}/xmpp-websocket" } cmd_logs() { local lines="${1:-50}" if lxc_running; then echo "=== Prosody logs ===" lxc_exec tail -n "$lines" /var/log/prosody/prosody.log 2>/dev/null || \ echo "No Prosody logs found" else echo "Container not running" fi } cmd_shell() { if lxc_running; then lxc_exec /bin/bash || lxc_exec /bin/sh else log_error "Container not running" return 1 fi } cmd_start() { require_root || { log_error "Must run as root"; return 1; } /etc/init.d/jabber start } cmd_stop() { require_root || { log_error "Must run as root"; return 1; } /etc/init.d/jabber stop } cmd_restart() { require_root || { log_error "Must run as root"; return 1; } /etc/init.d/jabber restart } # ---------- user management ---------- cmd_user() { local subcmd="$1" shift case "$subcmd" in add) cmd_user_add "$@" ;; del|delete) cmd_user_del "$@" ;; passwd|password) cmd_user_passwd "$@" ;; list) cmd_user_list ;; *) echo "Usage: jabberctl user " return 1 ;; esac } cmd_user_add() { local jid="$1" local password="$2" [ -z "$jid" ] && { echo "Usage: jabberctl user add [password]" return 1 } lxc_running || { log_error "Container not running"; return 1; } # Parse JID local user=$(echo "$jid" | cut -d@ -f1) local domain=$(echo "$jid" | cut -d@ -f2) [ -z "$domain" ] && { defaults domain="$hostname" } [ -z "$password" ] && password=$(generate_password) prosodyctl register "$user" "$domain" "$password" log_info "User created: ${user}@${domain}" log_info "Password: $password" } cmd_user_del() { local jid="$1" [ -z "$jid" ] && { echo "Usage: jabberctl user del " return 1 } lxc_running || { log_error "Container not running"; return 1; } # Parse JID local user=$(echo "$jid" | cut -d@ -f1) local domain=$(echo "$jid" | cut -d@ -f2) [ -z "$domain" ] && { defaults domain="$hostname" } prosodyctl deluser "${user}@${domain}" log_info "User deleted: ${user}@${domain}" } cmd_user_passwd() { local jid="$1" local password="$2" [ -z "$jid" ] && { echo "Usage: jabberctl user passwd [password]" return 1 } lxc_running || { log_error "Container not running"; return 1; } # Parse JID local user=$(echo "$jid" | cut -d@ -f1) local domain=$(echo "$jid" | cut -d@ -f2) [ -z "$domain" ] && { defaults domain="$hostname" } [ -z "$password" ] && password=$(generate_password) # Delete and recreate user with new password prosodyctl deluser "${user}@${domain}" 2>/dev/null prosodyctl register "$user" "$domain" "$password" log_info "Password changed for: ${user}@${domain}" log_info "New password: $password" } cmd_user_list() { lxc_running || { log_error "Container not running"; return 1; } defaults echo "Users for $hostname:" echo "====================" # List all account files lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | while read f; do user=$(basename "$f" .dat) domain=$(echo "$f" | grep -oE '[^/]+/accounts' | cut -d/ -f1 | tr '%' '.') echo " ${user}@${domain}" done } # ---------- room management ---------- cmd_room() { local subcmd="$1" shift case "$subcmd" in create) cmd_room_create "$@" ;; delete) cmd_room_delete "$@" ;; list) cmd_room_list ;; *) echo "Usage: jabberctl room " return 1 ;; esac } cmd_room_create() { local name="$1" [ -z "$name" ] && { echo "Usage: jabberctl room create " return 1 } lxc_running || { log_error "Container not running"; return 1; } defaults local muc_host=$(uci_get host muc || echo conference) # Create room via telnet/adhoc (Prosody doesn't have CLI for this) log_info "Room: ${name}@${muc_host}.${hostname}" log_info "Rooms are created automatically when first user joins." log_info "Or create via XMPP client's room creation dialog." } cmd_room_delete() { local name="$1" [ -z "$name" ] && { echo "Usage: jabberctl room delete " return 1 } lxc_running || { log_error "Container not running"; return 1; } defaults local muc_host=$(uci_get host muc || echo conference) # Remove room data directory local room_path="/var/lib/prosody/${muc_host}%2e${hostname}/rooms/${name}.dat" lxc_exec rm -f "$room_path" 2>/dev/null log_info "Room deleted: ${name}@${muc_host}.${hostname}" } cmd_room_list() { lxc_running || { log_error "Container not running"; return 1; } defaults local muc_host=$(uci_get host muc || echo conference) echo "Rooms on ${muc_host}.${hostname}:" echo "=================================" lxc_exec find /var/lib/prosody -name "*.dat" -path "*rooms*" 2>/dev/null | while read f; do room=$(basename "$f" .dat) echo " ${room}@${muc_host}.${hostname}" done } # ---------- HAProxy integration ---------- cmd_configure_haproxy() { require_root || { log_error "Must run as root"; return 1; } defaults local domain=$(uci_get domain network) [ -z "$domain" ] && domain="$hostname" # Create backend for BOSH/WebSocket local backend_name="jabber_http" uci set haproxy.${backend_name}=backend uci set haproxy.${backend_name}.name="$backend_name" uci set haproxy.${backend_name}.mode='http' uci set haproxy.${backend_name}.balance='roundrobin' uci set haproxy.${backend_name}.enabled='1' uci set haproxy.${backend_name}.timeout_server='3600s' uci set haproxy.${backend_name}.timeout_tunnel='3600s' uci set haproxy.${backend_name}.server="jabber 127.0.0.1:${http_port} check" # Create vhost local vhost_name=$(echo "$domain" | tr '.-' '_') uci set haproxy.${vhost_name}=vhost uci set haproxy.${vhost_name}.domain="$domain" uci set haproxy.${vhost_name}.backend="$backend_name" uci set haproxy.${vhost_name}.ssl='1' uci set haproxy.${vhost_name}.ssl_redirect='1' uci set haproxy.${vhost_name}.acme='1' uci set haproxy.${vhost_name}.enabled='1' uci commit haproxy # Update network config uci_set haproxy '1' network uci_set domain "$domain" network uci commit "$CONFIG" # Regenerate and reload if command -v haproxyctl >/dev/null 2>&1; then haproxyctl generate /etc/init.d/haproxy reload fi log_info "HAProxy configured for $domain" log_info "BOSH: https://$domain/http-bind" log_info "WebSocket: wss://$domain/xmpp-websocket" } cmd_emancipate() { local domain="$1" [ -z "$domain" ] && { echo "Usage: jabberctl emancipate " return 1 } require_root || { log_error "Must run as root"; return 1; } log_info "Emancipating Jabber at $domain..." # Update hostname uci_set hostname "$domain" server uci_set domain "$domain" network uci commit "$CONFIG" # Update Prosody config if lxc_running; then # Regenerate certs for new domain lxc_exec prosodyctl cert generate "$domain" # Update config file with new domain lxc_exec sed -i "s/XMPP_HOSTNAME=.*/XMPP_HOSTNAME=$domain/" /opt/start-jabber.sh # Remove old config marker to trigger regeneration lxc_exec rm -f /etc/prosody/prosody.cfg.lua.configured # Restart to apply cmd_restart fi # Configure HAProxy cmd_configure_haproxy # Enable S2S federation uci_set enabled '1' s2s uci commit "$CONFIG" # Open firewall ports local wan_open=$(uci_get firewall_wan network) if [ "$wan_open" = "1" ]; then # C2S port uci add firewall rule uci set firewall.@rule[-1].name='Jabber-C2S' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port="${c2s_port}" uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' # S2S port uci add firewall rule uci set firewall.@rule[-1].name='Jabber-S2S' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port="${s2s_port}" uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' uci commit firewall /etc/init.d/firewall reload fi log_info "" log_info "==============================================" log_info " Jabber/XMPP Emancipated!" log_info "==============================================" log_info "" log_info " Domain: $domain" log_info " XMPP C2S: ${domain}:${c2s_port}" log_info " XMPP S2S: ${domain}:${s2s_port}" log_info " BOSH: https://$domain/http-bind" log_info " WebSocket: wss://$domain/xmpp-websocket" log_info "" log_info " DNS Records needed:" log_info " A $domain -> your-ip" log_info " SRV _xmpp-client._tcp.$domain 5222" log_info " SRV _xmpp-server._tcp.$domain 5269" log_info "" } # ---------- backup/restore ---------- cmd_backup() { local backup_path="${1:-/srv/jabber/backup}" require_root || { log_error "Must run as root"; return 1; } lxc_running || { log_error "Container must be running"; return 1; } ensure_dir "$backup_path" local timestamp=$(date +%Y%m%d_%H%M%S) local backup_file="$backup_path/jabber_${timestamp}.tar.gz" log_info "Creating backup..." # Create tarball with data defaults tar -czf "$backup_file" \ -C "$data_path" data certs log_info "Backup created: $backup_file" } cmd_restore() { local backup_file="$1" [ -z "$backup_file" ] || [ ! -f "$backup_file" ] && { echo "Usage: jabberctl restore " return 1 } require_root || { log_error "Must run as root"; return 1; } log_info "Restoring from $backup_file..." # Stop container lxc_stop # Restore data defaults tar -xzf "$backup_file" -C "$data_path" # Start container cmd_start log_info "Restore complete." } # ---------- service management ---------- cmd_service_run() { require_root || exit 1 defaults # Verify container exists lxc_exists || { log_error "Container not found. Run: jabberctl install"; exit 1; } log_info "Starting Jabber/XMPP container..." # Start container in foreground exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONF" } cmd_service_stop() { log_info "Stopping Jabber/XMPP container..." lxc_stop } # ---------- main ---------- case "$1" in install) cmd_install ;; uninstall) cmd_uninstall ;; update) cmd_update ;; check) cmd_check ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) cmd_shell ;; user) shift; cmd_user "$@" ;; room) shift; cmd_room "$@" ;; configure-haproxy) cmd_configure_haproxy ;; emancipate) shift; cmd_emancipate "$@" ;; backup) shift; cmd_backup "$@" ;; restore) shift; cmd_restore "$@" ;; service-run) cmd_service_run ;; service-stop) cmd_service_stop ;; *) usage; exit 1 ;; esac