#!/usr/bin/env bash set -euo pipefail # One-shot OpenSIPS + OpenSIPS-CP + RTPengine installer for Ubuntu 24.04. # Tested against OpenSIPS 3.6.x on Ubuntu Noble. # # Usage: # sudo bash install-opensips-stack.sh # # Optional environment variables: # SIP_IP=100.93.185.30 DB_PASS=change-me CP_VERSION=9.3.6 sudo -E bash install-opensips-stack.sh export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a OPEN_SIPS_SERIES="${OPEN_SIPS_SERIES:-3.6}" CP_VERSION="${CP_VERSION:-9.3.6}" SIP_IP="${SIP_IP:-$(hostname -I | awk '{print $1}')}" SIP_DOMAIN="${SIP_DOMAIN:-$SIP_IP}" DB_NAME="${DB_NAME:-opensips}" DB_USER="${DB_USER:-opensips}" DB_PASS="${DB_PASS:-opensipsrw}" DB_RO_USER="${DB_RO_USER:-opensipsro}" DB_RO_PASS="${DB_RO_PASS:-opensipsro}" CP_PATH="${CP_PATH:-/var/www/html/opensips-cp}" MI_HTTP_IP="${MI_HTTP_IP:-127.0.0.1}" MI_HTTP_PORT="${MI_HTTP_PORT:-8888}" RTPENGINE_NG="${RTPENGINE_NG:-127.0.0.1:2223}" RTPENGINE_PORT_MIN="${RTPENGINE_PORT_MIN:-30000}" RTPENGINE_PORT_MAX="${RTPENGINE_PORT_MAX:-40000}" MONIT_IP="${MONIT_IP:-127.0.0.1}" MONIT_PORT="${MONIT_PORT:-2812}" MONIT_USER="${MONIT_USER:-admin}" MONIT_PASS="${MONIT_PASS:-monit}" log() { printf '\n### %s\n' "$*" } require_root() { if [ "$(id -u)" -ne 0 ]; then echo "Run as root, for example: sudo -E bash $0" >&2 exit 1 fi } backup_once() { local file="$1" [ -f "$file" ] || return 0 cp -an "$file" "$file.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true } apt_install() { apt-get install -y "$@" } setup_apt_repo() { log "Configure OpenSIPS APT repository" apt-get update apt_install ca-certificates curl gnupg lsb-release git unzip sed gawk curl -fsSL https://apt.opensips.org/opensips-org.gpg -o /usr/share/keyrings/opensips-org.gpg cat > /etc/apt/sources.list.d/opensips.list < /etc/apt/sources.list.d/opensips-cli.list < /etc/opensips/opensipsctlrc < 0 and 'account only INVITEs' in lines[i - 1]: out.append('\t\tcreate_dialog();') out.append('\t\tset_dlg_profile("outbound");') out.append('\t\tset_dlg_profile("caller", "$fU");') out.append('\t\tset_dlg_profile("callee", "$rU");') inserted = True if inserted: s = "\n".join(out) + "\n" if 'trace("tid", "m", "sip", "$fU");' not in s: s = s.replace('route{\n', 'route{\n\n\ttrace("tid", "m", "sip", "$fU");\n', 1) cfg.write_text(s) PY opensips -C -f "$cfg" } deploy_opensips_cp() { log "Deploy OpenSIPS Control Panel ${CP_VERSION}" if [ ! -d "$CP_PATH/.git" ]; then rm -rf "$CP_PATH" git clone --depth 1 --branch "$CP_VERSION" https://github.com/OpenSIPS/opensips-cp.git "$CP_PATH" else git -C "$CP_PATH" fetch --tags --depth 1 origin "$CP_VERSION" || true git -C "$CP_PATH" checkout "$CP_VERSION" fi if ! mysql --protocol=socket -uroot -D "$DB_NAME" -NBe "SHOW TABLES LIKE 'ocp_admin_privileges'" | grep -qx ocp_admin_privileges; then mysql --protocol=socket -uroot "$DB_NAME" < "$CP_PATH/config/db_schema.mysql" else echo "OpenSIPS-CP schema already present" fi mysql --protocol=socket -uroot "$DB_NAME" <{key}" if stripped.startswith(needle) and "=" in line: out.append(f" \$config->{key} = '{value}';") replaced = True break if not replaced: if "db_port" in line and "db_host" in line and "port=" in line: out.append(' if (!empty(\$config->db_port) ) \$config->db_host = \$config->db_host . ";port=" . \$config->db_port;') else: out.append(line) p.write_text("\\n".join(out) + "\\n") PY find "$CP_PATH/config" -type f \( -name '*.inc.php' -o -name '*.php' \) -print0 | xargs -0 sed -i \ -e "s/'db_host'[[:space:]]*=>[[:space:]]*'[^']*'/'db_host' => 'localhost'/g" \ -e "s/'db_name'[[:space:]]*=>[[:space:]]*'[^']*'/'db_name' => '${DB_NAME}'/g" \ -e "s/'db_user'[[:space:]]*=>[[:space:]]*'[^']*'/'db_user' => '${DB_USER}'/g" \ -e "s/'db_pass'[[:space:]]*=>[[:space:]]*'[^']*'/'db_pass' => '${DB_PASS}'/g" || true # OpenSIPS-CP 9.3.6 passes validation regexes to browser-side JavaScript # through preg_quote(), which turns regexes into literal strings for # JavaScript's RegExp(). This breaks forms such as Domains, where both IPs # and FQDNs are intended to be valid. Keep regex semantics and only escape # quotes for the generated JS string. local forms_file="$CP_PATH/web/common/forms.php" backup_once "$forms_file" python3 - "$forms_file" <<'PY' from pathlib import Path import sys p = Path(sys.argv[1]) s = p.read_text() s = s.replace("preg_quote($re, '/')", "addslashes($re)") s = s.replace("preg_quote($value['validation_regex'], '/')", "addslashes($value['validation_regex'])") p.write_text(s) PY # Some OpenSIPS-CP 9.3.6 Domains installs still over-escape the SIP Domain # field regex after the common generator patch. Keep the field required, but # let the backend accept normal SIP domains such as an IP or an FQDN. local domains_form="$CP_PATH/web/tools/system/domains/template/domains.form.php" if [ -f "$domains_form" ]; then backup_once "$domains_form" python3 - "$domains_form" <<'PY' from pathlib import Path import sys p = Path(sys.argv[1]) s = p.read_text() lines = [] changed = False for line in s.splitlines(): if '"domain", "n", $domain_form' in line: indent = line[:len(line) - len(line.lstrip())] lines.append(indent + '"domain", "n", $domain_form[\'domain\'], 128, null);') changed = True else: lines.append(line) if changed: p.write_text("\n".join(lines) + "\n") PY php -l "$domains_form" fi php -l "$CP_PATH/config/db.inc.php" php -l "$forms_file" } configure_apache() { log "Configure Apache /cp alias" cat > /etc/apache2/conf-available/opensips-cp.conf < Options Indexes FollowSymLinks MultiViews AllowOverride None Require all granted Options Indexes FollowSymLinks MultiViews AllowOverride None Require all denied Require all denied EOF a2enconf opensips-cp >/dev/null || true chown -R www-data:www-data "$CP_PATH" systemctl enable --now apache2 systemctl reload apache2 } configure_rtpengine() { log "Configure RTPengine" local defaults=/etc/default/rtpengine-daemon [ -f "$defaults" ] || defaults=/etc/default/rtpengine backup_once "$defaults" if [ -f "$defaults" ]; then grep -q '^RUN_RTPENGINE=' "$defaults" && sed -i 's|^#*RUN_RTPENGINE=.*|RUN_RTPENGINE=yes|' "$defaults" || echo 'RUN_RTPENGINE=yes' >> "$defaults" local opts="OPTIONS=\"--interface=${SIP_IP} --listen-ng=${RTPENGINE_NG} --port-min=${RTPENGINE_PORT_MIN} --port-max=${RTPENGINE_PORT_MAX} --log-level=6\"" grep -q '^OPTIONS=' "$defaults" && sed -i "s|^OPTIONS=.*|${opts}|" "$defaults" || echo "$opts" >> "$defaults" fi systemctl enable rtpengine-daemon systemctl restart rtpengine-daemon || true } configure_monit() { log "Configure Monit HTTP interface for OpenSIPS-CP" local monitrc=/etc/monit/monitrc backup_once "$monitrc" python3 - "$monitrc" <