Initial LisgloSIPS V2 implementation

This commit is contained in:
hectorzhao
2026-06-22 10:56:38 +08:00
commit 5fa1bd35e9
303 changed files with 35644 additions and 0 deletions
+585
View File
@@ -0,0 +1,585 @@
#!/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 <<EOF
deb [signed-by=/usr/share/keyrings/opensips-org.gpg] https://apt.opensips.org noble ${OPEN_SIPS_SERIES}-releases
EOF
cat > /etc/apt/sources.list.d/opensips-cli.list <<EOF
deb [signed-by=/usr/share/keyrings/opensips-org.gpg] https://apt.opensips.org noble cli-nightly
EOF
apt-get update
}
install_packages() {
log "Install OpenSIPS, OpenSIPS-CP dependencies and RTPengine"
apt_install \
opensips opensips-cli \
opensips-mysql-module opensips-mysql-dbschema \
opensips-auth-modules opensips-tls-module \
opensips-http-modules opensips-json-module \
rtpengine-daemon \
monit \
mariadb-server mariadb-client \
apache2 libapache2-mod-php \
php php-cli php-mysql php-gd php-pear php-apcu php-curl php-xml php-mbstring \
git unzip curl
}
setup_database() {
log "Create OpenSIPS database and users"
systemctl enable --now mariadb
mysql --protocol=socket -uroot <<SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
CREATE USER IF NOT EXISTS '${DB_RO_USER}'@'localhost' IDENTIFIED BY '${DB_RO_PASS}';
GRANT SELECT ON \`${DB_NAME}\`.* TO '${DB_RO_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
}
import_schema_file_if_needed() {
local sql_file="$1"
local first_table
first_table="$(awk '/CREATE TABLE/{gsub(/`/, "", $3); print $3; exit}' "$sql_file")"
if [ -z "$first_table" ]; then
return 0
fi
if mysql --protocol=socket -uroot -D "$DB_NAME" -NBe "SHOW TABLES LIKE '$first_table'" | grep -qx "$first_table"; then
echo "schema already present: $(basename "$sql_file")"
else
echo "importing schema: $(basename "$sql_file")"
mysql --protocol=socket -uroot "$DB_NAME" < "$sql_file"
fi
}
setup_opensips_schema() {
log "Import OpenSIPS MySQL schemas"
if ! mysql --protocol=socket -uroot -D "$DB_NAME" -NBe "SHOW TABLES LIKE 'version'" | grep -qx version; then
mysql --protocol=socket -uroot "$DB_NAME" < /usr/share/opensips/mysql/standard-create.sql
else
echo "standard schema already present"
fi
local schema
for schema in \
acc alias_db auth_db permissions domain dialog dispatcher drouting dialplan \
load_balancer rtpengine rtpproxy tracer usrloc tls_mgm group speeddial
do
import_schema_file_if_needed "/usr/share/opensips/mysql/${schema}-create.sql"
done
}
configure_opensipsctlrc() {
log "Configure opensipsctlrc"
backup_once /etc/opensips/opensipsctlrc
cat > /etc/opensips/opensipsctlrc <<EOF
SIP_DOMAIN=${SIP_DOMAIN}
DBENGINE=MYSQL
DBHOST=localhost
DBNAME=${DB_NAME}
DBRWUSER=${DB_USER}
DBRWPW="${DB_PASS}"
DBROUSER=${DB_RO_USER}
DBROPW=${DB_RO_PASS}
DBROOTUSER=root
EOF
}
patch_opensips_cfg() {
log "Configure OpenSIPS listener, MI HTTP and permissions"
local cfg=/etc/opensips/opensips.cfg
backup_once "$cfg"
python3 - "$cfg" <<PY
from pathlib import Path
cfg = Path("$cfg")
s = cfg.read_text()
lines = []
socket_done = False
for line in s.splitlines():
if line.strip().startswith("socket=udp:"):
if not socket_done:
lines.append("socket=udp:${SIP_IP}:5060 /* installed by install-opensips-stack.sh */")
socket_done = True
continue
lines.append(line)
s = "\\n".join(lines) + "\\n"
if not socket_done:
marker = "####### Modules Section ########"
s = s.replace(marker, f"socket=udp:${SIP_IP}:5060\\n\\n{marker}", 1)
managed = """\
#### Managed OpenSIPS-CP runtime support
loadmodule "db_mysql.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
modparam("auth_db", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("auth_db", "calculate_ha1", 1)
loadmodule "domain.so"
modparam("domain", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("domain", "db_mode", 1)
loadmodule "httpd.so"
modparam("httpd", "ip", "${MI_HTTP_IP}")
modparam("httpd", "port", ${MI_HTTP_PORT})
loadmodule "mi_http.so"
modparam("mi_http", "root", "mi")
loadmodule "permissions.so"
modparam("permissions", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("permissions", "address_table", "address")
loadmodule "dialog.so"
modparam("dialog", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("dialog", "profiles_no_value", "inbound;outbound")
modparam("dialog", "profiles_with_value", "caller;callee")
loadmodule "dispatcher.so"
modparam("dispatcher", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("dispatcher", "persistent_state", 1)
loadmodule "drouting.so"
modparam("drouting", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("drouting", "drd_table", "dr_gateways")
modparam("drouting", "drr_table", "dr_rules")
modparam("drouting", "drg_table", "dr_groups")
modparam("drouting", "drc_table", "dr_carriers")
loadmodule "rtpengine.so"
modparam("rtpengine", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("rtpengine", "db_table", "rtpengine")
loadmodule "rtpproxy.so"
modparam("rtpproxy", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")
modparam("rtpproxy", "db_table", "rtpproxy_sockets")
loadmodule "tracer.so"
modparam("tracer", "trace_on", 1)
modparam("tracer", "trace_id", "[tid]uri=mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME};table=sip_trace;")
#### End managed OpenSIPS-CP runtime support
"""
start = "#### Managed OpenSIPS-CP runtime support"
end = "#### End managed OpenSIPS-CP runtime support"
if start in s and end in s:
before, rest = s.split(start, 1)
_, after = rest.split(end, 1)
s = before + managed + after
else:
marker = '#### SIGNALING module\\n'
if marker in s:
s = s.replace(marker, managed + "\\n" + marker, 1)
else:
s = s.replace('#### Modules Section ########\\n', '#### Modules Section ########\\n' + managed + "\\n", 1)
lines = []
for line in s.splitlines():
stripped = line.strip()
if stripped.startswith('modparam("usrloc", "working_mode_preset"'):
lines.append('modparam("usrloc", "working_mode_preset", "single-instance-sql-write-through")')
continue
if stripped.startswith('modparam("usrloc", "db_url"'):
lines.append('modparam("usrloc", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")')
continue
lines.append(line)
if stripped == 'loadmodule "usrloc.so"' and 'modparam("usrloc", "db_url"' not in s:
lines.append('modparam("usrloc", "db_url", "mysql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}")')
s = "\n".join(lines) + "\n"
old_register = """\tif (is_method("REGISTER")) {
\t\t# store the registration and generate a SIP reply
\t\tif (!save("location"))
\t\t\txlog("failed to register AoR $tu\\n");
\t\texit;
\t}"""
new_register = """\tif (is_method("REGISTER")) {
\t\tif (!www_authorize("$fd", "subscriber")) {
\t\t\twww_challenge("$fd", "auth");
\t\t\texit;
\t\t}
\t\tif (!save("location"))
\t\t\txlog("failed to register AoR $tu\\n");
\t\texit;
\t}"""
if old_register in s:
s = s.replace(old_register, new_register, 1)
if 'create_dialog();' not in s:
out = []
inserted = False
lines = s.splitlines()
for i, line in enumerate(lines):
out.append(line)
if line.strip() == 'if (is_method("INVITE")) {' and i > 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" <<SQL
UPDATE ocp_boxes_config
SET monit_conn='${MONIT_IP}:${MONIT_PORT}',
monit_user='${MONIT_USER}',
monit_pass='${MONIT_PASS}',
monit_ssl=0
WHERE id=1;
INSERT INTO rtpengine (socket, set_id)
SELECT 'udp:${RTPENGINE_NG}', 0
WHERE NOT EXISTS (
SELECT 1 FROM rtpengine WHERE socket='udp:${RTPENGINE_NG}' AND set_id=0
);
SQL
[ -f "$CP_PATH/config/db.inc.php.sample" ] && [ ! -f "$CP_PATH/config/db.inc.php" ] && cp "$CP_PATH/config/db.inc.php.sample" "$CP_PATH/config/db.inc.php"
[ -f "$CP_PATH/config/boxes.global.inc.php.sample" ] && [ ! -f "$CP_PATH/config/boxes.global.inc.php" ] && cp "$CP_PATH/config/boxes.global.inc.php.sample" "$CP_PATH/config/boxes.global.inc.php"
python3 - "$CP_PATH/config/db.inc.php" <<PY
from pathlib import Path
p = Path("$CP_PATH/config/db.inc.php")
s = p.read_text()
repls = {
"db_host": "localhost",
"db_user": "${DB_USER}",
"db_pass": "${DB_PASS}",
"db_name": "${DB_NAME}",
}
out = []
for line in s.splitlines():
stripped = line.strip()
replaced = False
for key, value in repls.items():
needle = f"\$config->{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 <<EOF
Alias /cp ${CP_PATH}/web
<Directory ${CP_PATH}/web>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
</Directory>
<Directory ${CP_PATH}>
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all denied
</Directory>
<DirectoryMatch "${CP_PATH}/web/tools/.*/.*/(template|custom_actions|lib)/">
Require all denied
</DirectoryMatch>
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" <<PY
from pathlib import Path
import sys
p = Path(sys.argv[1])
s = p.read_text()
block = """set httpd port ${MONIT_PORT} and
use address ${MONIT_IP}
allow ${MONIT_IP}
allow ${MONIT_USER}:${MONIT_PASS}
"""
lines = s.splitlines()
out = []
i = 0
inserted = False
while i < len(lines):
line = lines[i]
stripped = line.strip()
if stripped.startswith("set httpd port") or stripped.startswith("# set httpd port"):
if not inserted:
out.extend(block.rstrip("\\n").splitlines())
inserted = True
i += 1
while i < len(lines):
nxt = lines[i]
nstr = nxt.strip()
if not nstr:
out.append(nxt)
i += 1
break
if nstr.startswith("set ") and not nstr.startswith("set httpd port"):
break
if nxt.startswith(" ") or nxt.startswith("\\t") or nstr.startswith("#"):
i += 1
continue
break
continue
out.append(line)
i += 1
if not inserted:
out.append("")
out.extend(block.rstrip("\\n").splitlines())
p.write_text("\\n".join(out) + "\\n")
PY
monit -t
systemctl enable monit
systemctl restart monit
}
enable_runtime_services() {
log "Enable runtime services at boot"
# OpenSIPS-CP is served by Apache and uses MariaDB; it is not a separate
# daemon. Enabling these services makes http://SERVER_IP/cp/ available after
# reboot, while enabling opensips starts the SIP proxy itself.
systemctl enable --now mariadb
systemctl enable --now apache2
systemctl enable opensips
systemctl restart opensips
systemctl enable rtpengine-daemon
systemctl restart rtpengine-daemon || true
systemctl enable monit
systemctl restart monit
}
restart_and_verify() {
log "Restart services and verify"
enable_runtime_services
systemctl reload apache2
systemctl is-active mariadb apache2 opensips rtpengine-daemon monit
systemctl is-enabled mariadb apache2 opensips rtpengine-daemon monit
opensips -V | head -n 4
curl -fsS --max-time 10 "http://127.0.0.1/cp/" | grep -m1 "OpenSIPS Control Panel"
curl -fsS --max-time 10 -u "${MONIT_USER}:${MONIT_PASS}" "http://${MONIT_IP}:${MONIT_PORT}/" | grep -m1 "Monit"
curl -fsS --max-time 10 -X POST "http://${MI_HTTP_IP}:${MI_HTTP_PORT}/mi" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"rtpengine_show"}'
curl -fsS --max-time 10 -X POST "http://${MI_HTTP_IP}:${MI_HTTP_PORT}/mi" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"rtpproxy_show"}'
curl -fsS --max-time 10 -X POST "http://${MI_HTTP_IP}:${MI_HTTP_PORT}/mi" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"trace"}'
curl -fsS --max-time 10 -X POST "http://${MI_HTTP_IP}:${MI_HTTP_PORT}/mi" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"dr_gw_status"}'
curl -fsS --max-time 10 -X POST "http://${MI_HTTP_IP}:${MI_HTTP_PORT}/mi" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"address_reload"}'
ss -ltnup | grep -E "(:80|:5060|:${MI_HTTP_PORT}|:2223)" || true
}
main() {
require_root
setup_apt_repo
install_packages
setup_database
setup_opensips_schema
configure_opensipsctlrc
patch_opensips_cfg
deploy_opensips_cp
configure_apache
configure_rtpengine
configure_monit
restart_and_verify
cat <<EOF
Install complete.
OpenSIPS-CP URL: http://${SIP_IP}/cp/
Default OpenSIPS-CP login:
username: admin
password: opensips
MI HTTP endpoint is bound to ${MI_HTTP_IP}:${MI_HTTP_PORT}/mi for local CP usage.
Do not expose MI HTTP to the public Internet.
EOF
}
main "$@"