Initial LisgloSIPS V2 implementation
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
table inet lisglosips_s06 {
|
||||
chain input {
|
||||
type filter hook input priority filter; policy accept;
|
||||
|
||||
tcp dport 9100 ip saddr != 100.90.90.91 drop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ARGS=--web.listen-address=100.90.90.90:9100 --collector.systemd --collector.processes
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
STAGE=${STAGE:-/tmp/lisglosips-s06-stage}
|
||||
|
||||
stamp=$(date +%Y%m%d%H%M%S)
|
||||
backup=/var/backups/lisglosips-s06/$stamp
|
||||
install -d -m 0700 "$backup"
|
||||
systemctl cat prometheus-node-exporter.service >"$backup/prometheus-node-exporter.service.before" 2>/dev/null || true
|
||||
systemctl cat lisglosips-node-exporter.service >"$backup/lisglosips-node-exporter.service.before" 2>/dev/null || true
|
||||
dpkg-query -W -f='${Package}\t${Version}\n' prometheus-node-exporter >"$backup/packages.before" 2>/dev/null || true
|
||||
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y prometheus-node-exporter=1.7.0-1ubuntu0.3
|
||||
|
||||
install -d -o root -g root -m 0755 /etc/nftables.d
|
||||
install -o root -g root -m 0644 "$STAGE/nftables/node-exporter.nft" /etc/nftables.d/lisglosips-s06-exporter.nft
|
||||
install -o root -g root -m 0644 "$STAGE/systemd/lisglosips-exporter-firewall.service" /etc/systemd/system/lisglosips-exporter-firewall.service
|
||||
install -o root -g root -m 0644 "$STAGE/systemd/lisglosips-node-exporter.service" /etc/systemd/system/lisglosips-node-exporter.service
|
||||
install -o root -g root -m 0644 "$STAGE/node-exporter.env" /etc/default/lisglosips-node-exporter
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl disable --now prometheus-node-exporter.service prometheus-node-exporter.socket 2>/dev/null || true
|
||||
systemctl kill prometheus-node-exporter.service 2>/dev/null || true
|
||||
pkill -f '^/usr/bin/prometheus-node-exporter$' 2>/dev/null || true
|
||||
systemctl mask prometheus-node-exporter.service 2>/dev/null || true
|
||||
systemctl disable --now openipmi.service 2>/dev/null || true
|
||||
systemctl mask openipmi.service 2>/dev/null || true
|
||||
systemctl reset-failed prometheus-node-exporter.service openipmi.service lisglosips-node-exporter.service 2>/dev/null || true
|
||||
systemctl enable --now lisglosips-exporter-firewall.service lisglosips-node-exporter.service
|
||||
|
||||
systemctl is-active --quiet lisglosips-exporter-firewall.service
|
||||
systemctl is-active --quiet lisglosips-node-exporter.service
|
||||
curl -fsS http://100.90.90.90:9100/metrics >/dev/null
|
||||
|
||||
echo "S06 A node exporter configured. Backup: $backup"
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS exporter firewall
|
||||
Before=lisglosips-node-exporter.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=true
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet lisglosips_s06
|
||||
ExecStart=/usr/sbin/nft -f /etc/nftables.d/lisglosips-s06-exporter.nft
|
||||
ExecStop=-/usr/sbin/nft delete table inet lisglosips_s06
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,10 @@
|
||||
table inet lisglosips_s18_opensips {
|
||||
chain input {
|
||||
type filter hook input priority -15; policy accept;
|
||||
iif "lo" accept
|
||||
ct state established,related accept
|
||||
ip saddr 100.93.185.30 udp dport 15060 accept
|
||||
udp dport 15060 drop
|
||||
tcp dport 8888 drop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#### LisgloSIPS S18 OpenSIPS baseline ####
|
||||
|
||||
log_level=3
|
||||
xlog_level=3
|
||||
stderror_enabled=no
|
||||
syslog_enabled=yes
|
||||
syslog_facility=LOG_LOCAL0
|
||||
|
||||
udp_workers=4
|
||||
auto_aliases=no
|
||||
socket=udp:100.90.90.90:15060
|
||||
socket=hep_udp:127.0.0.1:9061
|
||||
|
||||
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"
|
||||
|
||||
loadmodule "proto_udp.so"
|
||||
loadmodule "signaling.so"
|
||||
loadmodule "sl.so"
|
||||
loadmodule "tm.so"
|
||||
modparam("tm", "fr_timeout", 5)
|
||||
modparam("tm", "fr_inv_timeout", 30)
|
||||
|
||||
loadmodule "maxfwd.so"
|
||||
loadmodule "sipmsgops.so"
|
||||
loadmodule "textops.so"
|
||||
|
||||
loadmodule "mi_fifo.so"
|
||||
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")
|
||||
modparam("mi_fifo", "fifo_mode", 0660)
|
||||
|
||||
loadmodule "httpd.so"
|
||||
modparam("httpd", "ip", "127.0.0.1")
|
||||
modparam("httpd", "port", 8888)
|
||||
loadmodule "mi_http.so"
|
||||
modparam("mi_http", "root", "mi")
|
||||
|
||||
loadmodule "auth.so"
|
||||
modparam("auth", "nonce_expire", 300)
|
||||
modparam("auth", "disable_nonce_check", 0)
|
||||
|
||||
loadmodule "pike.so"
|
||||
modparam("pike", "sampling_time_unit", 2)
|
||||
modparam("pike", "reqs_density_per_unit", 16)
|
||||
modparam("pike", "remove_latency", 120)
|
||||
|
||||
loadmodule "ratelimit.so"
|
||||
loadmodule "cachedb_redis.so"
|
||||
loadmodule "proto_hep.so"
|
||||
modparam("proto_hep", "hep_id", "[homer] 100.90.90.91:9060; transport=udp; version=3")
|
||||
loadmodule "tracer.so"
|
||||
modparam("tracer", "trace_on", 1)
|
||||
modparam("tracer", "trace_id", "[s18_hep]uri=hep:homer")
|
||||
|
||||
route {
|
||||
if (!mf_process_maxfwd_header(10)) {
|
||||
send_reply(483, "Too Many Hops");
|
||||
exit;
|
||||
}
|
||||
|
||||
trace("s18_hep", "m", "sip|xlog");
|
||||
|
||||
if (!pike_check_req()) {
|
||||
xlog("L_WARN", "S18 pike blocked source=$si method=$rm callid=$ci\n");
|
||||
send_reply(403, "Rate Limited");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (has_totag()) {
|
||||
send_reply(481, "Dialog Not Found");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("OPTIONS") && $rU == NULL) {
|
||||
send_reply(200, "Keepalive");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("REGISTER")) {
|
||||
append_to_reply("WWW-Authenticate: Digest realm=\"lisglosips.local\", nonce=\"s18-baseline\", algorithm=MD5, qop=\"auth\"\r\n");
|
||||
send_reply(401, "Authentication Required");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("INVITE")) {
|
||||
xlog("L_INFO", "S18 route placeholder rejected INVITE source=$si callid=$ci\n");
|
||||
send_reply(503, "Routing Not Ready");
|
||||
exit;
|
||||
}
|
||||
|
||||
send_reply(405, "Method Not Allowed");
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# LisgloSIPS S18 OpenSIPS startup options
|
||||
RUN_OPENSIPS=yes
|
||||
USER=opensips
|
||||
GROUP=opensips
|
||||
S_MEMORY=128
|
||||
P_MEMORY=16
|
||||
DUMP_CORE=no
|
||||
OPTIONS=""
|
||||
@@ -0,0 +1,11 @@
|
||||
table inet lisglosips_s19_rtpengine {
|
||||
chain input {
|
||||
type filter hook input priority -14; policy accept;
|
||||
iif "lo" accept
|
||||
ct state established,related accept
|
||||
ip saddr 100.93.185.30 udp dport 30000-40000 accept
|
||||
udp dport 30000-40000 drop
|
||||
tcp dport { 2224, 2225 } drop
|
||||
udp dport 2223 drop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
CONFIG_FILE=/etc/rtpengine/rtpengine.conf
|
||||
PIDFILE=/run/rtpengine/rtpengine-daemon.pid
|
||||
MANAGE_IPTABLES=yes
|
||||
SET_USER=rtpengine
|
||||
SET_MASK=0x7
|
||||
@@ -0,0 +1,7 @@
|
||||
CONFIG_FILE=/etc/rtpengine/rtpengine-recording.conf
|
||||
PIDFILE=/run/rtpengine-recording/rtpengine-recording-daemon.pid
|
||||
MUST_NFS=no
|
||||
NFS_HOST=127.0.0.1
|
||||
NFS_REMOTE_PATH=/unused
|
||||
NFS_LOCAL_MOUNT=/dev/shm/voip_rec/incoming
|
||||
NFS_OPTIONS=hard,intr,tcp
|
||||
@@ -0,0 +1,16 @@
|
||||
[rtpengine-recording]
|
||||
|
||||
table = 0
|
||||
num-threads = 4
|
||||
|
||||
spool-dir = /dev/shm/voip_rec/spool
|
||||
output-storage = file
|
||||
output-dir = /dev/shm/voip_rec/incoming
|
||||
output-format = wav
|
||||
output-pattern = %Y/%m/%d/%c-%t
|
||||
output-mixed = true
|
||||
output-single = false
|
||||
output-chmod = 0640
|
||||
output-chmod-dir = 0750
|
||||
output-chown = rtpengine
|
||||
output-chgrp = rtpengine
|
||||
@@ -0,0 +1,22 @@
|
||||
[rtpengine]
|
||||
|
||||
table = 0
|
||||
no-fallback = true
|
||||
|
||||
interface = 100.90.90.90
|
||||
listen-ng = 127.0.0.1:2223
|
||||
listen-cli = 127.0.0.1:2224
|
||||
listen-http = 127.0.0.1:2225
|
||||
|
||||
timeout = 60
|
||||
silent-timeout = 3600
|
||||
tos = 184
|
||||
|
||||
port-min = 30000
|
||||
port-max = 40000
|
||||
|
||||
recording-dir = /dev/shm/voip_rec/spool
|
||||
recording-method = proc
|
||||
|
||||
log-level = 6
|
||||
log-facility = daemon
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR=${1:-/dev/shm/voip_rec}
|
||||
INCOMING_DIR="$BASE_DIR/incoming"
|
||||
READY_DIR="$BASE_DIR/ready"
|
||||
FAILED_DIR="$BASE_DIR/failed"
|
||||
MIN_AGE_SECONDS=${MIN_AGE_SECONDS:-15}
|
||||
|
||||
mkdir -p "$INCOMING_DIR" "$READY_DIR" "$FAILED_DIR"
|
||||
chmod 0770 "$READY_DIR"
|
||||
|
||||
find "$INCOMING_DIR" -type f \( -name '*.wav' -o -name '*.mp3' \) -print0 |
|
||||
while IFS= read -r -d '' source; do
|
||||
if [ ! -s "$source" ]; then
|
||||
continue
|
||||
fi
|
||||
now=$(date +%s)
|
||||
mtime=$(stat -c %Y "$source")
|
||||
if [ $((now - mtime)) -lt "$MIN_AGE_SECONDS" ]; then
|
||||
continue
|
||||
fi
|
||||
if command -v fuser >/dev/null 2>&1 && fuser -s -- "$source"; then
|
||||
continue
|
||||
fi
|
||||
rel=${source#"$INCOMING_DIR"/}
|
||||
part="$READY_DIR/$rel.part"
|
||||
ready="$READY_DIR/$rel.ready"
|
||||
failed="$FAILED_DIR/$rel"
|
||||
mkdir -p "$(dirname "$part")" "$(dirname "$failed")"
|
||||
chmod 0770 "$(dirname "$part")"
|
||||
if [ -e "$ready" ] || [ -e "$part" ]; then
|
||||
mv -- "$source" "$failed.$(date -u +%Y%m%dT%H%M%SZ).duplicate"
|
||||
continue
|
||||
fi
|
||||
mv -- "$source" "$part"
|
||||
chmod 0640 "$part"
|
||||
mv -- "$part" "$ready"
|
||||
done
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Load LisgloSIPS Server A nftables rules
|
||||
Documentation=file:/etc/nftables.d/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=rtpengine-daemon.service opensips.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet lisglosips_s19_rtpengine
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet lisglosips_s18_opensips
|
||||
ExecStart=/usr/sbin/nft -f /etc/nftables.d/lisglosips-s18-opensips.nft
|
||||
ExecStart=/usr/sbin/nft -f /etc/nftables.d/lisglosips-s19-rtpengine.nft
|
||||
ExecStop=-/usr/sbin/nft delete table inet lisglosips_s19_rtpengine
|
||||
ExecStop=-/usr/sbin/nft delete table inet lisglosips_s18_opensips
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Finalize RTPEngine recordings into LisgloSIPS ready files
|
||||
After=rtpengine-recording-daemon.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=rtpengine
|
||||
Group=rtpengine
|
||||
ExecStart=/usr/local/sbin/lisglosips-recording-finalize /dev/shm/voip_rec
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Run LisgloSIPS recording finalizer frequently
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitActiveSec=5s
|
||||
AccuracySec=1s
|
||||
Unit=lisglosips-recording-finalize.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
After=network-online.target tailscaled.service lisglosips-a-firewall.service
|
||||
Wants=network-online.target tailscaled.service lisglosips-a-firewall.service
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do ip address show | grep -q "100.90.90.90" && exit 0; sleep 1; done; echo "Tailscale address 100.90.90.90 not ready" >&2; exit 1'
|
||||
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=RTP/media Recording Daemon for LisgloSIPS
|
||||
Documentation=man:rtpengine-recording(8)
|
||||
After=network-online.target rtpengine-daemon.service dev-shm-voip_rec.mount
|
||||
Requires=network-online.target rtpengine-daemon.service
|
||||
RequiresMountsFor=/dev/shm/voip_rec
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
LimitNOFILE=100000
|
||||
RuntimeDirectory=rtpengine-recording
|
||||
PIDFile=/run/rtpengine-recording/rtpengine-recording-daemon.pid
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_CHOWN
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_CHOWN
|
||||
User=rtpengine
|
||||
Group=rtpengine
|
||||
ExecStart=/usr/bin/rtpengine-recording -f -E --no-log-timestamps --pidfile /run/rtpengine-recording/rtpengine-recording-daemon.pid --config-file /etc/rtpengine/rtpengine-recording.conf
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
After=
|
||||
After=network-online.target rtpengine-daemon.service dev-shm-voip_rec.mount
|
||||
Requires=
|
||||
Requires=network-online.target rtpengine-daemon.service
|
||||
|
||||
[Service]
|
||||
CapabilityBoundingSet=
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_CHOWN
|
||||
@@ -0,0 +1,6 @@
|
||||
d /dev/shm/voip_rec 0750 rtpengine rtpengine -
|
||||
d /dev/shm/voip_rec/spool 0750 rtpengine rtpengine -
|
||||
d /dev/shm/voip_rec/incoming 0750 rtpengine rtpengine -
|
||||
d /dev/shm/voip_rec/ready 0750 rtpengine rtpengine -
|
||||
d /dev/shm/voip_rec/failed 0750 rtpengine rtpengine -
|
||||
d /dev/shm/voip_rec/tmp 0750 rtpengine rtpengine -
|
||||
@@ -0,0 +1 @@
|
||||
ARGS=--web.listen-address=100.90.90.90:9100 --collector.systemd --collector.processes --collector.textfile.directory=/var/lib/prometheus/node-exporter
|
||||
@@ -0,0 +1,60 @@
|
||||
local active = redis.call('GET', KEYS[1])
|
||||
if not active or active == '' then
|
||||
return {'reject', 'CONFIG_MISSING', 'none', 'none', 'none', 'none', 'no_active_version'}
|
||||
end
|
||||
|
||||
local source_ip = ARGV[1]
|
||||
local caller = ARGV[2] or ''
|
||||
local callee = ARGV[3] or ''
|
||||
|
||||
local prefix = 'cfg:v:' .. active
|
||||
local gateway_id = redis.call('GET', prefix .. ':auth:ip:' .. source_ip)
|
||||
if not gateway_id then
|
||||
return {'reject', 'AUTH_MISSING', 'none', active, 'none', 'none', 'no_auth_ip'}
|
||||
end
|
||||
|
||||
local gateway_json = redis.call('GET', prefix .. ':customer_gateway:' .. gateway_id)
|
||||
if not gateway_json then
|
||||
return {'reject', 'GATEWAY_MISSING', gateway_id, active, 'none', 'none', 'gateway_missing'}
|
||||
end
|
||||
if not string.find(gateway_json, '"status":"ENABLED"', 1, true) then
|
||||
return {'reject', 'GATEWAY_DISABLED', gateway_id, active, 'none', 'none', 'gateway_disabled'}
|
||||
end
|
||||
|
||||
local customer_id = string.match(gateway_json, '"customerId":"([^"]+)"')
|
||||
if not customer_id then
|
||||
return {'reject', 'CUSTOMER_MISSING', gateway_id, active, 'none', 'none', 'customer_id_missing'}
|
||||
end
|
||||
|
||||
local customer_json = redis.call('GET', prefix .. ':customer:' .. customer_id)
|
||||
if not customer_json then
|
||||
return {'reject', 'CUSTOMER_MISSING', gateway_id, active, 'none', 'none', 'customer_missing'}
|
||||
end
|
||||
if not string.find(customer_json, '"status":"ENABLED"', 1, true) then
|
||||
return {'reject', 'CUSTOMER_DISABLED', gateway_id, active, 'none', 'none', 'customer_disabled'}
|
||||
end
|
||||
|
||||
local policies = redis.call('LRANGE', prefix .. ':customer_gateway:' .. gateway_id .. ':policies', 0, -1)
|
||||
for _, policy_json in ipairs(policies) do
|
||||
if string.find(policy_json, '"status":"ENABLED"', 1, true) then
|
||||
local caller_mode = string.match(policy_json, '"callerMode":"([^"]+)"') or 'ANY'
|
||||
local caller_value = string.match(policy_json, '"callerValue":"([^"]*)"') or ''
|
||||
local callee_mode = string.match(policy_json, '"calleeMode":"([^"]+)"') or 'ANY'
|
||||
local callee_value = string.match(policy_json, '"calleeValue":"([^"]*)"') or ''
|
||||
|
||||
local caller_ok = caller_mode == 'ANY'
|
||||
or (caller_mode == 'EQUALS' and caller == caller_value)
|
||||
or (caller_mode == 'PREFIX' and string.sub(caller, 1, string.len(caller_value)) == caller_value)
|
||||
local callee_ok = callee_mode == 'ANY'
|
||||
or (callee_mode == 'EQUALS' and callee == callee_value)
|
||||
or (callee_mode == 'PREFIX' and string.sub(callee, 1, string.len(callee_value)) == callee_value)
|
||||
|
||||
if caller_ok and callee_ok then
|
||||
local policy_id = string.match(policy_json, '"id":"([^"]+)"') or 'none'
|
||||
local line_group_id = string.match(policy_json, '"lineGroupId":"([^"]+)"') or 'none'
|
||||
return {'allow', 'OK', gateway_id, active, line_group_id, policy_id, customer_id}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {'reject', 'NO_POLICY', gateway_id, active, 'none', 'none', 'no_policy_match'}
|
||||
@@ -0,0 +1,9 @@
|
||||
table inet lisglosips_s20_observability {
|
||||
chain input {
|
||||
type filter hook input priority -13; policy accept;
|
||||
iif "lo" accept
|
||||
ct state established,related accept
|
||||
udp dport 9061 drop
|
||||
tcp dport 6380 drop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#### LisgloSIPS S20 OpenSIPS Redis hot path, HEP and metrics baseline ####
|
||||
|
||||
log_level=3
|
||||
xlog_level=3
|
||||
stderror_enabled=no
|
||||
syslog_enabled=yes
|
||||
syslog_facility=LOG_LOCAL0
|
||||
|
||||
udp_workers=4
|
||||
auto_aliases=no
|
||||
socket=udp:100.90.90.90:15060
|
||||
socket=hep_udp:100.90.90.90:9061
|
||||
|
||||
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"
|
||||
|
||||
loadmodule "proto_udp.so"
|
||||
loadmodule "signaling.so"
|
||||
loadmodule "sl.so"
|
||||
loadmodule "tm.so"
|
||||
modparam("tm", "fr_timeout", 5)
|
||||
modparam("tm", "fr_inv_timeout", 30)
|
||||
|
||||
loadmodule "maxfwd.so"
|
||||
loadmodule "sipmsgops.so"
|
||||
loadmodule "textops.so"
|
||||
|
||||
loadmodule "mi_fifo.so"
|
||||
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")
|
||||
modparam("mi_fifo", "fifo_mode", 0660)
|
||||
|
||||
loadmodule "httpd.so"
|
||||
modparam("httpd", "ip", "127.0.0.1")
|
||||
modparam("httpd", "port", 8888)
|
||||
loadmodule "mi_http.so"
|
||||
modparam("mi_http", "root", "mi")
|
||||
|
||||
loadmodule "auth.so"
|
||||
modparam("auth", "nonce_expire", 300)
|
||||
modparam("auth", "disable_nonce_check", 0)
|
||||
|
||||
loadmodule "pike.so"
|
||||
modparam("pike", "sampling_time_unit", 2)
|
||||
modparam("pike", "reqs_density_per_unit", 16)
|
||||
modparam("pike", "remove_latency", 120)
|
||||
|
||||
loadmodule "ratelimit.so"
|
||||
|
||||
loadmodule "cachedb_redis.so"
|
||||
modparam("cachedb_redis", "connect_timeout", 300)
|
||||
modparam("cachedb_redis", "query_timeout", 300)
|
||||
modparam("cachedb_redis", "shutdown_on_error", 0)
|
||||
modparam("cachedb_redis", "cachedb_url", "@@LISGLOSIPS_REDIS_URL@@")
|
||||
|
||||
loadmodule "rtpengine.so"
|
||||
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
|
||||
|
||||
loadmodule "statistics.so"
|
||||
modparam("statistics", "variable", "s20_invite_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_hotpath_allow_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_hotpath_reject_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_redis_error_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_cdr_xadd_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_cdr_xadd_error_total/no_reset")
|
||||
|
||||
loadmodule "prometheus.so"
|
||||
modparam("prometheus", "root", "metrics")
|
||||
modparam("prometheus", "prefix", "lisglosips_opensips")
|
||||
modparam("prometheus", "statistics", "script: core: net:")
|
||||
|
||||
loadmodule "proto_hep.so"
|
||||
modparam("proto_hep", "hep_id", "[homer] 100.90.90.91:9060; transport=udp; version=3")
|
||||
loadmodule "tracer.so"
|
||||
modparam("tracer", "trace_on", 1)
|
||||
modparam("tracer", "trace_id", "[s20_hep]uri=hep:homer")
|
||||
|
||||
route {
|
||||
if (!mf_process_maxfwd_header(10)) {
|
||||
send_reply(483, "Too Many Hops");
|
||||
exit;
|
||||
}
|
||||
|
||||
trace("s20_hep", "m", "sip|xlog");
|
||||
|
||||
if (!pike_check_req()) {
|
||||
xlog("L_WARN", "S20 pike blocked source=$si method=$rm callid=$ci\n");
|
||||
send_reply(403, "Rate Limited");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (has_totag()) {
|
||||
send_reply(481, "Dialog Not Found");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("OPTIONS") && $rU == NULL) {
|
||||
send_reply(200, "Keepalive");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("REGISTER")) {
|
||||
append_to_reply("WWW-Authenticate: Digest realm=\"lisglosips.local\", nonce=\"s20-baseline\", algorithm=MD5, qop=\"auth\"\r\n");
|
||||
send_reply(401, "Authentication Required");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("INVITE")) {
|
||||
route(S20_INVITE);
|
||||
exit;
|
||||
}
|
||||
|
||||
send_reply(405, "Method Not Allowed");
|
||||
exit;
|
||||
}
|
||||
|
||||
route[S20_INVITE] {
|
||||
update_stat("s20_invite_total", 1);
|
||||
|
||||
$var(s20_decision) = "reject";
|
||||
$var(s20_reason) = "REDIS_UNAVAILABLE";
|
||||
$var(s20_gateway_id) = "none";
|
||||
$var(s20_config_version) = "none";
|
||||
$var(s20_line_group_id) = "none";
|
||||
$var(s20_policy_id) = "none";
|
||||
$var(s20_customer_id) = "none";
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Routing Not Ready";
|
||||
|
||||
if (!cache_raw_query("redis:s20", "EVALSHA cfdc02cbe5528d37fba617947c09e3380770c918 1 cfg:active_version $si $fU $rU $ci", "$avp(s20_hotpath)")) {
|
||||
update_stat("s20_redis_error_total", 1);
|
||||
xlog("L_ERR", "S20 Redis hotpath unavailable source=$si callid=$ci\n");
|
||||
$var(s20_reason) = "REDIS_UNAVAILABLE";
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Redis Unavailable";
|
||||
route(S20_CDR_XADD);
|
||||
send_reply($var(s20_reply_code), $var(s20_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
$var(s20_decision) = $(avp(s20_hotpath)[0]);
|
||||
$var(s20_reason) = $(avp(s20_hotpath)[1]);
|
||||
$var(s20_gateway_id) = $(avp(s20_hotpath)[2]);
|
||||
$var(s20_config_version) = $(avp(s20_hotpath)[3]);
|
||||
$var(s20_line_group_id) = $(avp(s20_hotpath)[4]);
|
||||
$var(s20_policy_id) = $(avp(s20_hotpath)[5]);
|
||||
$var(s20_customer_id) = $(avp(s20_hotpath)[6]);
|
||||
|
||||
if ($var(s20_decision) == "allow") {
|
||||
update_stat("s20_hotpath_allow_total", 1);
|
||||
xlog("L_INFO", "S20 hotpath allow source=$si callid=$ci customer=$var(s20_customer_id) gateway=$var(s20_gateway_id) policy=$var(s20_policy_id) line_group=$var(s20_line_group_id) version=$var(s20_config_version)\n");
|
||||
$var(s20_reason) = "ROUTING_NOT_READY";
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Routing Not Ready";
|
||||
route(S20_CDR_XADD);
|
||||
send_reply($var(s20_reply_code), $var(s20_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
update_stat("s20_hotpath_reject_total", 1);
|
||||
if ($var(s20_reason) == "CONFIG_MISSING") {
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Config Missing";
|
||||
} else if ($var(s20_reason) == "NO_POLICY") {
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "No Route Policy";
|
||||
} else {
|
||||
$var(s20_reply_code) = 403;
|
||||
$var(s20_reply_text) = "Forbidden";
|
||||
}
|
||||
|
||||
xlog("L_WARN", "S20 hotpath reject reason=$var(s20_reason) source=$si callid=$ci version=$var(s20_config_version)\n");
|
||||
route(S20_CDR_XADD);
|
||||
send_reply($var(s20_reply_code), $var(s20_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
route[S20_CDR_XADD] {
|
||||
$var(s20_event_id) = "s20-" + $Ts + "-" + $pp + "-" + $ci;
|
||||
|
||||
if (cache_raw_query("redis:s20", "XADD stream:cdr_payload * event_id $var(s20_event_id) call_id $ci source_ip $si caller $fU callee $rU customer_id $var(s20_customer_id) customer_gateway_id $var(s20_gateway_id) customer_gateway_policy_id $var(s20_policy_id) line_group_id $var(s20_line_group_id) sip_code $var(s20_reply_code) hangup_reason $var(s20_reason) config_version $var(s20_config_version) created_at_unix $Ts", "$avp(s20_cdr_id)")) {
|
||||
update_stat("s20_cdr_xadd_total", 1);
|
||||
xlog("L_INFO", "S20 CDR XADD ok redis_id=$avp(s20_cdr_id) event_id=$var(s20_event_id) callid=$ci reason=$var(s20_reason)\n");
|
||||
} else {
|
||||
update_stat("s20_cdr_xadd_error_total", 1);
|
||||
xlog("L_ERR", "S20 CDR XADD failed event_id=$var(s20_event_id) callid=$ci reason=$var(s20_reason)\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
redis_url = os.getenv("OPENSIPS_REDIS_URL", "redis:s20://127.0.0.1:6380/")
|
||||
config = sys.stdin.read()
|
||||
sys.stdout.write(config.replace("@@LISGLOSIPS_REDIS_URL@@", redis_url))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import selectors
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
LISTEN_HOST = os.getenv("LISTEN_HOST", "127.0.0.1")
|
||||
LISTEN_PORT = int(os.getenv("LISTEN_PORT", "6380"))
|
||||
REDIS_HOST = os.getenv("REDIS_HOST", "100.90.90.91")
|
||||
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_USER = os.getenv("REDIS_USER", "lisglosips")
|
||||
CONNECT_TIMEOUT = float(os.getenv("CONNECT_TIMEOUT", "2.0"))
|
||||
|
||||
|
||||
def credential(name: str) -> str:
|
||||
cred_dir = os.getenv("CREDENTIALS_DIRECTORY")
|
||||
if not cred_dir:
|
||||
raise RuntimeError("CREDENTIALS_DIRECTORY is not set")
|
||||
path = os.path.join(cred_dir, name)
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
return handle.read().strip()
|
||||
|
||||
|
||||
def resp_array(parts: list[str]) -> bytes:
|
||||
output = f"*{len(parts)}\r\n".encode("utf-8")
|
||||
for part in parts:
|
||||
data = part.encode("utf-8")
|
||||
output += f"${len(data)}\r\n".encode("utf-8") + data + b"\r\n"
|
||||
return output
|
||||
|
||||
|
||||
def read_line(sock: socket.socket) -> bytes:
|
||||
data = b""
|
||||
while not data.endswith(b"\r\n"):
|
||||
chunk = sock.recv(1)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
if len(data) > 4096:
|
||||
break
|
||||
return data
|
||||
|
||||
|
||||
def connect_upstream() -> socket.socket:
|
||||
password = credential("redis_password")
|
||||
upstream = socket.create_connection((REDIS_HOST, REDIS_PORT), timeout=CONNECT_TIMEOUT)
|
||||
upstream.sendall(resp_array(["AUTH", REDIS_USER, password]))
|
||||
reply = read_line(upstream)
|
||||
if not reply.startswith(b"+OK"):
|
||||
upstream.close()
|
||||
raise RuntimeError("Redis AUTH failed")
|
||||
upstream.setblocking(False)
|
||||
return upstream
|
||||
|
||||
|
||||
def bridge(client: socket.socket) -> None:
|
||||
upstream = connect_upstream()
|
||||
client.setblocking(False)
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(client, selectors.EVENT_READ, upstream)
|
||||
selector.register(upstream, selectors.EVENT_READ, client)
|
||||
try:
|
||||
while True:
|
||||
for key, _ in selector.select(timeout=30):
|
||||
src = key.fileobj
|
||||
dst = key.data
|
||||
data = src.recv(65536)
|
||||
if not data:
|
||||
return
|
||||
dst.sendall(data)
|
||||
finally:
|
||||
selector.close()
|
||||
client.close()
|
||||
upstream.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind((LISTEN_HOST, LISTEN_PORT))
|
||||
server.listen(128)
|
||||
print(f"lisglosips redis auth proxy listening on {LISTEN_HOST}:{LISTEN_PORT}", flush=True)
|
||||
while True:
|
||||
client, _ = server.accept()
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
server.close()
|
||||
try:
|
||||
bridge(client)
|
||||
except Exception as exc:
|
||||
print(f"redis proxy client failed: {exc}", file=sys.stderr, flush=True)
|
||||
return 0
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
|
||||
|
||||
SCRIPT_PATH = os.getenv("HOTPATH_LUA", "/etc/opensips/lisglosips_hotpath.lua")
|
||||
REDIS_HOST = os.getenv("REDIS_PROXY_HOST", "127.0.0.1")
|
||||
REDIS_PORT = int(os.getenv("REDIS_PROXY_PORT", "6380"))
|
||||
|
||||
|
||||
def enc(parts: list[str]) -> bytes:
|
||||
output = f"*{len(parts)}\r\n".encode("utf-8")
|
||||
for part in parts:
|
||||
data = part.encode("utf-8")
|
||||
output += f"${len(data)}\r\n".encode("utf-8") + data + b"\r\n"
|
||||
return output
|
||||
|
||||
|
||||
with open(SCRIPT_PATH, "r", encoding="utf-8") as handle:
|
||||
script = handle.read()
|
||||
|
||||
last_error: Exception | None = None
|
||||
for _ in range(20):
|
||||
try:
|
||||
with socket.create_connection((REDIS_HOST, REDIS_PORT), timeout=2) as sock:
|
||||
sock.sendall(enc(["SCRIPT", "LOAD", script]))
|
||||
response = sock.recv(4096)
|
||||
if not response.startswith(b"$"):
|
||||
raise SystemExit(f"unexpected Redis response while loading Lua: {response[:80]!r}")
|
||||
raise SystemExit(0)
|
||||
except OSError as exc:
|
||||
last_error = exc
|
||||
time.sleep(0.25)
|
||||
|
||||
raise SystemExit(f"could not connect to Redis proxy: {last_error}")
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUT_DIR=${1:-/var/lib/prometheus/node-exporter}
|
||||
OUT_FILE="$OUT_DIR/lisglosips_s20.prom"
|
||||
TMP_FILE="$OUT_FILE.$$"
|
||||
BASE_DIR=/dev/shm/voip_rec
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
tmp_total=0
|
||||
tmp_used=0
|
||||
tmp_avail=0
|
||||
if df_out=$(df -B1 "$BASE_DIR" 2>/dev/null | awk 'NR==2 {print $2" "$3" "$4}'); then
|
||||
read -r tmp_total tmp_used tmp_avail <<<"$df_out"
|
||||
fi
|
||||
|
||||
ready_count=$(find "$BASE_DIR/ready" -type f -name '*.ready' 2>/dev/null | wc -l)
|
||||
part_count=$(find "$BASE_DIR/ready" -type f -name '*.part' 2>/dev/null | wc -l)
|
||||
oldest_age=0
|
||||
oldest_epoch=$(find "$BASE_DIR/ready" -type f -name '*.ready' -printf '%T@\n' 2>/dev/null | sort -n | head -n 1 || true)
|
||||
if [ -n "$oldest_epoch" ]; then
|
||||
now_epoch=$(date +%s)
|
||||
oldest_sec=${oldest_epoch%.*}
|
||||
oldest_age=$(( now_epoch - oldest_sec ))
|
||||
if [ "$oldest_age" -lt 0 ]; then
|
||||
oldest_age=0
|
||||
fi
|
||||
fi
|
||||
|
||||
redis_reachable=0
|
||||
if timeout 2 bash -c '</dev/tcp/127.0.0.1/6380' 2>/dev/null; then
|
||||
redis_reachable=1
|
||||
fi
|
||||
|
||||
rtpengine_sessions=0
|
||||
if command -v rtpengine-ctl >/dev/null 2>&1; then
|
||||
rtpengine_sessions=$(rtpengine-ctl -ip 127.0.0.1 -port 2224 list numsessions 2>/dev/null | awk -F: '/Current sessions total/ {gsub(/ /, "", $2); print $2; found=1} END {if (!found) print 0}')
|
||||
fi
|
||||
|
||||
cat > "$TMP_FILE" <<EOF
|
||||
# HELP lisglosips_voip_rec_tmpfs_bytes Server A recording tmpfs bytes.
|
||||
# TYPE lisglosips_voip_rec_tmpfs_bytes gauge
|
||||
lisglosips_voip_rec_tmpfs_bytes{state="total"} $tmp_total
|
||||
lisglosips_voip_rec_tmpfs_bytes{state="used"} $tmp_used
|
||||
lisglosips_voip_rec_tmpfs_bytes{state="available"} $tmp_avail
|
||||
# HELP lisglosips_recording_files Server A recording staging files.
|
||||
# TYPE lisglosips_recording_files gauge
|
||||
lisglosips_recording_files{state="ready"} $ready_count
|
||||
lisglosips_recording_files{state="part"} $part_count
|
||||
# HELP lisglosips_recording_oldest_ready_age_seconds Oldest ready recording age in seconds.
|
||||
# TYPE lisglosips_recording_oldest_ready_age_seconds gauge
|
||||
lisglosips_recording_oldest_ready_age_seconds $oldest_age
|
||||
# HELP lisglosips_redis_proxy_up Local Redis auth proxy TCP reachability.
|
||||
# TYPE lisglosips_redis_proxy_up gauge
|
||||
lisglosips_redis_proxy_up $redis_reachable
|
||||
# HELP lisglosips_rtpengine_sessions Current RTPEngine sessions reported by CLI.
|
||||
# TYPE lisglosips_rtpengine_sessions gauge
|
||||
lisglosips_rtpengine_sessions $rtpengine_sessions
|
||||
EOF
|
||||
|
||||
chown prometheus:prometheus "$TMP_FILE" 2>/dev/null || true
|
||||
chmod 0644 "$TMP_FILE"
|
||||
mv "$TMP_FILE" "$OUT_FILE"
|
||||
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=Load LisgloSIPS Server A nftables rules
|
||||
Documentation=file:/etc/nftables.d/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=rtpengine-daemon.service opensips.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet lisglosips_s20_observability
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet lisglosips_s19_rtpengine
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet lisglosips_s18_opensips
|
||||
ExecStart=/usr/sbin/nft -f /etc/nftables.d/lisglosips-s18-opensips.nft
|
||||
ExecStart=/usr/sbin/nft -f /etc/nftables.d/lisglosips-s19-rtpengine.nft
|
||||
ExecStart=/usr/sbin/nft -f /etc/nftables.d/lisglosips-s20-observability.nft
|
||||
ExecStop=-/usr/sbin/nft delete table inet lisglosips_s20_observability
|
||||
ExecStop=-/usr/sbin/nft delete table inet lisglosips_s19_rtpengine
|
||||
ExecStop=-/usr/sbin/nft delete table inet lisglosips_s18_opensips
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS local Redis AUTH proxy for OpenSIPS
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=opensips
|
||||
Group=opensips
|
||||
LoadCredentialEncrypted=redis_password:/etc/credstore.encrypted/lisglosips-redis-password.cred
|
||||
Environment=LISTEN_HOST=127.0.0.1
|
||||
Environment=LISTEN_PORT=6380
|
||||
Environment=REDIS_HOST=100.90.90.91
|
||||
Environment=REDIS_PORT=6379
|
||||
Environment=REDIS_USER=lisglosips
|
||||
ExecStart=/usr/local/sbin/lisglosips-redis-auth-proxy
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/run
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=127.0.0.0/8
|
||||
IPAddressAllow=100.90.90.91/32
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Load LisgloSIPS Redis hot path Lua script
|
||||
After=lisglosips-redis-auth-proxy.service
|
||||
Requires=lisglosips-redis-auth-proxy.service
|
||||
Before=opensips.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/lisglosips-redis-load-hotpath
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=Collect LisgloSIPS Server A textfile metrics
|
||||
After=rtpengine-daemon.service lisglosips-redis-auth-proxy.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/lisglosips-s20-metrics /var/lib/prometheus/node-exporter
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Run LisgloSIPS Server A textfile metrics collector
|
||||
|
||||
[Timer]
|
||||
OnBootSec=20s
|
||||
OnUnitActiveSec=15s
|
||||
AccuracySec=2s
|
||||
Unit=lisglosips-s20-metrics.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
After=lisglosips-redis-auth-proxy.service lisglosips-redis-hotpath-load.service
|
||||
Wants=lisglosips-redis-auth-proxy.service lisglosips-redis-hotpath-load.service
|
||||
|
||||
[Service]
|
||||
Environment=OPENSIPS_REDIS_URL=redis:s20://127.0.0.1:6380/
|
||||
@@ -0,0 +1,187 @@
|
||||
#### LisgloSIPS S20 OpenSIPS Redis hot path, HEP and metrics baseline ####
|
||||
|
||||
log_level=3
|
||||
xlog_level=3
|
||||
stderror_enabled=no
|
||||
syslog_enabled=yes
|
||||
syslog_facility=LOG_LOCAL0
|
||||
|
||||
udp_workers=4
|
||||
auto_aliases=no
|
||||
socket=udp:100.90.90.90:15060
|
||||
socket=hep_udp:100.90.90.90:9061
|
||||
|
||||
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"
|
||||
|
||||
loadmodule "proto_udp.so"
|
||||
loadmodule "signaling.so"
|
||||
loadmodule "sl.so"
|
||||
loadmodule "tm.so"
|
||||
modparam("tm", "fr_timeout", 5)
|
||||
modparam("tm", "fr_inv_timeout", 30)
|
||||
|
||||
loadmodule "maxfwd.so"
|
||||
loadmodule "sipmsgops.so"
|
||||
loadmodule "textops.so"
|
||||
|
||||
loadmodule "mi_fifo.so"
|
||||
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")
|
||||
modparam("mi_fifo", "fifo_mode", 0660)
|
||||
|
||||
loadmodule "httpd.so"
|
||||
modparam("httpd", "ip", "127.0.0.1")
|
||||
modparam("httpd", "port", 8888)
|
||||
loadmodule "mi_http.so"
|
||||
modparam("mi_http", "root", "mi")
|
||||
|
||||
loadmodule "auth.so"
|
||||
modparam("auth", "nonce_expire", 300)
|
||||
modparam("auth", "disable_nonce_check", 0)
|
||||
|
||||
loadmodule "pike.so"
|
||||
modparam("pike", "sampling_time_unit", 2)
|
||||
modparam("pike", "reqs_density_per_unit", 16)
|
||||
modparam("pike", "remove_latency", 120)
|
||||
|
||||
loadmodule "ratelimit.so"
|
||||
|
||||
loadmodule "cachedb_redis.so"
|
||||
modparam("cachedb_redis", "connect_timeout", 300)
|
||||
modparam("cachedb_redis", "query_timeout", 300)
|
||||
modparam("cachedb_redis", "shutdown_on_error", 0)
|
||||
modparam("cachedb_redis", "cachedb_url", "@@LISGLOSIPS_REDIS_URL@@")
|
||||
|
||||
loadmodule "rtpengine.so"
|
||||
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
|
||||
|
||||
loadmodule "statistics.so"
|
||||
modparam("statistics", "variable", "s20_invite_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_hotpath_allow_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_hotpath_reject_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_redis_error_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_cdr_xadd_total/no_reset")
|
||||
modparam("statistics", "variable", "s20_cdr_xadd_error_total/no_reset")
|
||||
|
||||
loadmodule "prometheus.so"
|
||||
modparam("prometheus", "root", "metrics")
|
||||
modparam("prometheus", "prefix", "lisglosips_opensips")
|
||||
modparam("prometheus", "statistics", "script: core: net:")
|
||||
|
||||
loadmodule "proto_hep.so"
|
||||
modparam("proto_hep", "hep_id", "[homer] 100.90.90.91:9060; transport=udp; version=3")
|
||||
loadmodule "tracer.so"
|
||||
modparam("tracer", "trace_on", 1)
|
||||
modparam("tracer", "trace_id", "[s20_hep]uri=hep:homer")
|
||||
|
||||
route {
|
||||
if (!mf_process_maxfwd_header(10)) {
|
||||
send_reply(483, "Too Many Hops");
|
||||
exit;
|
||||
}
|
||||
|
||||
trace("s20_hep", "m", "sip|xlog");
|
||||
|
||||
if (!pike_check_req()) {
|
||||
xlog("L_WARN", "S20 pike blocked source=$si method=$rm callid=$ci\n");
|
||||
send_reply(403, "Rate Limited");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (has_totag()) {
|
||||
send_reply(481, "Dialog Not Found");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("OPTIONS") && $rU == NULL) {
|
||||
send_reply(200, "Keepalive");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("REGISTER")) {
|
||||
append_to_reply("WWW-Authenticate: Digest realm=\"lisglosips.local\", nonce=\"s20-baseline\", algorithm=MD5, qop=\"auth\"\r\n");
|
||||
send_reply(401, "Authentication Required");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("INVITE")) {
|
||||
route(S20_INVITE);
|
||||
exit;
|
||||
}
|
||||
|
||||
send_reply(405, "Method Not Allowed");
|
||||
exit;
|
||||
}
|
||||
|
||||
route[S20_INVITE] {
|
||||
update_stat("s20_invite_total", 1);
|
||||
|
||||
$var(s20_decision) = "reject";
|
||||
$var(s20_reason) = "REDIS_UNAVAILABLE";
|
||||
$var(s20_gateway_id) = "none";
|
||||
$var(s20_config_version) = "none";
|
||||
$var(s20_line_group_id) = "none";
|
||||
$var(s20_policy_id) = "none";
|
||||
$var(s20_customer_id) = "none";
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Routing Not Ready";
|
||||
|
||||
if (!cache_raw_query("redis:s20", "EVALSHA cfdc02cbe5528d37fba617947c09e3380770c918 1 cfg:active_version $si $fU $rU $ci", "$avp(s20_hotpath)")) {
|
||||
update_stat("s20_redis_error_total", 1);
|
||||
xlog("L_ERR", "S20 Redis hotpath unavailable source=$si callid=$ci\n");
|
||||
$var(s20_reason) = "REDIS_UNAVAILABLE";
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Redis Unavailable";
|
||||
route(S20_CDR_XADD);
|
||||
send_reply($var(s20_reply_code), $var(s20_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
$var(s20_decision) = $(avp(s20_hotpath)[0]);
|
||||
$var(s20_reason) = $(avp(s20_hotpath)[1]);
|
||||
$var(s20_gateway_id) = $(avp(s20_hotpath)[2]);
|
||||
$var(s20_config_version) = $(avp(s20_hotpath)[3]);
|
||||
$var(s20_line_group_id) = $(avp(s20_hotpath)[4]);
|
||||
$var(s20_policy_id) = $(avp(s20_hotpath)[5]);
|
||||
$var(s20_customer_id) = $(avp(s20_hotpath)[6]);
|
||||
|
||||
if ($var(s20_decision) == "allow") {
|
||||
update_stat("s20_hotpath_allow_total", 1);
|
||||
xlog("L_INFO", "S20 hotpath allow source=$si callid=$ci customer=$var(s20_customer_id) gateway=$var(s20_gateway_id) policy=$var(s20_policy_id) line_group=$var(s20_line_group_id) version=$var(s20_config_version)\n");
|
||||
$var(s20_reason) = "ROUTING_NOT_READY";
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Routing Not Ready";
|
||||
route(S20_CDR_XADD);
|
||||
send_reply($var(s20_reply_code), $var(s20_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
update_stat("s20_hotpath_reject_total", 1);
|
||||
if ($var(s20_reason) == "CONFIG_MISSING") {
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "Config Missing";
|
||||
} else if ($var(s20_reason) == "NO_POLICY") {
|
||||
$var(s20_reply_code) = 503;
|
||||
$var(s20_reply_text) = "No Route Policy";
|
||||
} else {
|
||||
$var(s20_reply_code) = 403;
|
||||
$var(s20_reply_text) = "Forbidden";
|
||||
}
|
||||
|
||||
xlog("L_WARN", "S20 hotpath reject reason=$var(s20_reason) source=$si callid=$ci version=$var(s20_config_version)\n");
|
||||
route(S20_CDR_XADD);
|
||||
send_reply($var(s20_reply_code), $var(s20_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
route[S20_CDR_XADD] {
|
||||
$var(s20_event_id) = "s20-" + $Ts + "-" + $pp + "-" + $ci;
|
||||
$var(s20_idempotency_key) = $ci + ":" + $Ts;
|
||||
|
||||
if (cache_raw_query("redis:s20", "XADD stream:cdr_payload * schema_version 1 event_id $var(s20_event_id) idempotency_key $var(s20_idempotency_key) call_id $ci node_id a1 opensips_instance opensips-a1 ingress_a_ip 100.90.90.90 rtpengine_node a1 source_ip $si caller $fU callee $rU customer_id $var(s20_customer_id) customer_gateway_id $var(s20_gateway_id) customer_gateway_policy_id $var(s20_policy_id) vendor_id none vendor_gateway_id none line_group_id $var(s20_line_group_id) started_at $Ts answered_at none ended_at $Ts duration_sec 0 sip_code $var(s20_reply_code) hangup_reason $var(s20_reason) recording_key none config_version $var(s20_config_version) created_at $Ts", "$avp(s20_cdr_id)")) {
|
||||
update_stat("s20_cdr_xadd_total", 1);
|
||||
xlog("L_INFO", "S20 CDR XADD ok redis_id=$avp(s20_cdr_id) event_id=$var(s20_event_id) callid=$ci reason=$var(s20_reason)\n");
|
||||
} else {
|
||||
update_stat("s20_cdr_xadd_error_total", 1);
|
||||
xlog("L_ERR", "S20 CDR XADD failed event_id=$var(s20_event_id) callid=$ci reason=$var(s20_reason)\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
#### LisgloSIPS S28 minimal end-to-end OpenSIPS route ####
|
||||
|
||||
log_level=3
|
||||
xlog_level=3
|
||||
stderror_enabled=no
|
||||
syslog_enabled=yes
|
||||
syslog_facility=LOG_LOCAL0
|
||||
|
||||
udp_workers=4
|
||||
auto_aliases=no
|
||||
socket=udp:100.90.90.90:15060
|
||||
socket=hep_udp:100.90.90.90:9061
|
||||
|
||||
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"
|
||||
|
||||
loadmodule "proto_udp.so"
|
||||
loadmodule "signaling.so"
|
||||
loadmodule "sl.so"
|
||||
loadmodule "tm.so"
|
||||
modparam("tm", "fr_timeout", 5)
|
||||
modparam("tm", "fr_inv_timeout", 30)
|
||||
|
||||
loadmodule "rr.so"
|
||||
modparam("rr", "append_fromtag", 1)
|
||||
|
||||
loadmodule "maxfwd.so"
|
||||
loadmodule "sipmsgops.so"
|
||||
loadmodule "textops.so"
|
||||
|
||||
loadmodule "mi_fifo.so"
|
||||
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")
|
||||
modparam("mi_fifo", "fifo_mode", 0660)
|
||||
|
||||
loadmodule "httpd.so"
|
||||
modparam("httpd", "ip", "127.0.0.1")
|
||||
modparam("httpd", "port", 8888)
|
||||
loadmodule "mi_http.so"
|
||||
modparam("mi_http", "root", "mi")
|
||||
|
||||
loadmodule "auth.so"
|
||||
modparam("auth", "nonce_expire", 300)
|
||||
modparam("auth", "disable_nonce_check", 0)
|
||||
|
||||
loadmodule "pike.so"
|
||||
modparam("pike", "sampling_time_unit", 2)
|
||||
modparam("pike", "reqs_density_per_unit", 32)
|
||||
modparam("pike", "remove_latency", 120)
|
||||
|
||||
loadmodule "ratelimit.so"
|
||||
|
||||
loadmodule "cachedb_redis.so"
|
||||
modparam("cachedb_redis", "connect_timeout", 300)
|
||||
modparam("cachedb_redis", "query_timeout", 300)
|
||||
modparam("cachedb_redis", "shutdown_on_error", 0)
|
||||
modparam("cachedb_redis", "cachedb_url", "@@LISGLOSIPS_REDIS_URL@@")
|
||||
|
||||
loadmodule "rtpengine.so"
|
||||
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
|
||||
|
||||
loadmodule "statistics.so"
|
||||
modparam("statistics", "variable", "s28_invite_total/no_reset")
|
||||
modparam("statistics", "variable", "s28_hotpath_allow_total/no_reset")
|
||||
modparam("statistics", "variable", "s28_hotpath_reject_total/no_reset")
|
||||
modparam("statistics", "variable", "s28_cdr_xadd_total/no_reset")
|
||||
modparam("statistics", "variable", "s28_cdr_xadd_error_total/no_reset")
|
||||
|
||||
loadmodule "prometheus.so"
|
||||
modparam("prometheus", "root", "metrics")
|
||||
modparam("prometheus", "prefix", "lisglosips_opensips")
|
||||
modparam("prometheus", "statistics", "script: core: net:")
|
||||
|
||||
loadmodule "proto_hep.so"
|
||||
modparam("proto_hep", "hep_id", "[homer] 100.90.90.91:9060; transport=udp; version=3")
|
||||
loadmodule "tracer.so"
|
||||
modparam("tracer", "trace_on", 1)
|
||||
modparam("tracer", "trace_id", "[s28_hep]uri=hep:homer")
|
||||
|
||||
route {
|
||||
if (!mf_process_maxfwd_header(10)) {
|
||||
send_reply(483, "Too Many Hops");
|
||||
exit;
|
||||
}
|
||||
|
||||
trace("s28_hep", "m", "sip|xlog");
|
||||
|
||||
if (!pike_check_req()) {
|
||||
xlog("L_WARN", "S28 pike blocked source=$si method=$rm callid=$ci\n");
|
||||
send_reply(403, "Rate Limited");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (has_totag()) {
|
||||
route(S28_IN_DIALOG);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("OPTIONS") && $rU == NULL) {
|
||||
send_reply(200, "Keepalive");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("REGISTER")) {
|
||||
append_to_reply("WWW-Authenticate: Digest realm=\"lisglosips.local\", nonce=\"s28-baseline\", algorithm=MD5, qop=\"auth\"\r\n");
|
||||
send_reply(401, "Authentication Required");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("INVITE")) {
|
||||
route(S28_INVITE);
|
||||
exit;
|
||||
}
|
||||
|
||||
send_reply(405, "Method Not Allowed");
|
||||
exit;
|
||||
}
|
||||
|
||||
route[S28_IN_DIALOG] {
|
||||
xlog("L_INFO", "S28 in-dialog method=$rm source=$si ruri=$ru callid=$ci\n");
|
||||
|
||||
if (is_method("ACK") && $si == "100.93.185.30") {
|
||||
remove_hf("Route");
|
||||
$du = "sip:100.93.185.30:50620";
|
||||
$ru = "sip:" + $rU + "@100.93.185.30:50620";
|
||||
t_relay();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("BYE") && $si == "100.93.185.30") {
|
||||
rtpengine_delete();
|
||||
remove_hf("Route");
|
||||
$du = "sip:100.93.185.30:50620";
|
||||
$ru = "sip:" + $rU + "@100.93.185.30:50620";
|
||||
if (!t_relay()) {
|
||||
send_reply(500, "Relay Failed");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if (loose_route()) {
|
||||
if (is_method("BYE")) {
|
||||
rtpengine_delete();
|
||||
}
|
||||
if (!t_relay()) {
|
||||
send_reply(500, "Relay Failed");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_method("ACK")) {
|
||||
if (t_check_trans()) {
|
||||
t_relay();
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
send_reply(481, "Dialog Not Found");
|
||||
exit;
|
||||
}
|
||||
|
||||
route[S28_INVITE] {
|
||||
update_stat("s28_invite_total", 1);
|
||||
|
||||
$var(s28_decision) = "reject";
|
||||
$var(s28_reason) = "REDIS_UNAVAILABLE";
|
||||
$var(s28_gateway_id) = "none";
|
||||
$var(s28_config_version) = "none";
|
||||
$var(s28_line_group_id) = "none";
|
||||
$var(s28_policy_id) = "none";
|
||||
$var(s28_customer_id) = "none";
|
||||
$var(s28_reply_code) = 503;
|
||||
$var(s28_reply_text) = "Routing Not Ready";
|
||||
|
||||
if (!cache_raw_query("redis:s20", "EVALSHA cfdc02cbe5528d37fba617947c09e3380770c918 1 cfg:active_version $si $fU $rU $ci", "$avp(s28_hotpath)")) {
|
||||
xlog("L_ERR", "S28 Redis hotpath unavailable source=$si callid=$ci\n");
|
||||
$var(s28_reason) = "REDIS_UNAVAILABLE";
|
||||
$var(s28_reply_code) = 503;
|
||||
$var(s28_reply_text) = "Redis Unavailable";
|
||||
route(S28_CDR_FAILURE);
|
||||
send_reply($var(s28_reply_code), $var(s28_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
$var(s28_decision) = $(avp(s28_hotpath)[0]);
|
||||
$var(s28_reason) = $(avp(s28_hotpath)[1]);
|
||||
$var(s28_gateway_id) = $(avp(s28_hotpath)[2]);
|
||||
$var(s28_config_version) = $(avp(s28_hotpath)[3]);
|
||||
$var(s28_line_group_id) = $(avp(s28_hotpath)[4]);
|
||||
$var(s28_policy_id) = $(avp(s28_hotpath)[5]);
|
||||
$var(s28_customer_id) = $(avp(s28_hotpath)[6]);
|
||||
|
||||
if ($var(s28_decision) != "allow") {
|
||||
update_stat("s28_hotpath_reject_total", 1);
|
||||
if ($var(s28_reason) == "CONFIG_MISSING") {
|
||||
$var(s28_reply_code) = 503;
|
||||
$var(s28_reply_text) = "Config Missing";
|
||||
} else if ($var(s28_reason) == "NO_POLICY") {
|
||||
$var(s28_reply_code) = 503;
|
||||
$var(s28_reply_text) = "No Route Policy";
|
||||
} else {
|
||||
$var(s28_reply_code) = 403;
|
||||
$var(s28_reply_text) = "Forbidden";
|
||||
}
|
||||
route(S28_CDR_FAILURE);
|
||||
send_reply($var(s28_reply_code), $var(s28_reply_text));
|
||||
exit;
|
||||
}
|
||||
|
||||
update_stat("s28_hotpath_allow_total", 1);
|
||||
xlog("L_INFO", "S28 hotpath allow source=$si callid=$ci customer=$var(s28_customer_id) gateway=$var(s28_gateway_id) policy=$var(s28_policy_id) line_group=$var(s28_line_group_id) version=$var(s28_config_version)\n");
|
||||
|
||||
if (has_body("application/sdp")) {
|
||||
if (!rtpengine_offer("replace-origin replace-session-connection record-call=on")) {
|
||||
xlog("L_ERR", "S28 rtpengine_offer failed callid=$ci\n");
|
||||
$var(s28_reason) = "RTPENGINE_OFFER_FAILED";
|
||||
$var(s28_reply_code) = 500;
|
||||
$var(s28_reply_text) = "Media Error";
|
||||
route(S28_CDR_FAILURE);
|
||||
send_reply($var(s28_reply_code), $var(s28_reply_text));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
record_route();
|
||||
$du = "sip:100.93.185.30:50620";
|
||||
$ru = "sip:" + $rU + "@100.93.185.30:50620";
|
||||
t_on_reply("S28_REPLY");
|
||||
|
||||
if (!t_relay()) {
|
||||
$var(s28_reason) = "RELAY_FAILED";
|
||||
$var(s28_reply_code) = 500;
|
||||
$var(s28_reply_text) = "Relay Failed";
|
||||
route(S28_CDR_FAILURE);
|
||||
send_reply($var(s28_reply_code), $var(s28_reply_text));
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
onreply_route[S28_REPLY] {
|
||||
if (t_check_status("2[0-9][0-9]") && has_body("application/sdp")) {
|
||||
if (!rtpengine_answer("replace-origin replace-session-connection")) {
|
||||
xlog("L_ERR", "S28 rtpengine_answer failed callid=$ci\n");
|
||||
}
|
||||
route(S28_CDR_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
route[S28_CDR_SUCCESS] {
|
||||
$var(s28_event_id) = "s28-ok-" + $Ts + "-" + $pp + "-" + $ci;
|
||||
$var(s28_idempotency_key) = $ci + ":s28-ok:" + $Ts;
|
||||
if (cache_raw_query("redis:s20", "XADD stream:cdr_payload * schema_version 1 event_id $var(s28_event_id) idempotency_key $var(s28_idempotency_key) call_id $ci node_id a1 opensips_instance opensips-a1 ingress_a_ip 100.90.90.90 rtpengine_node a1 source_ip $si caller $fU callee $rU customer_id cus_s28_t customer_gateway_id cgw_s28_t_ip customer_gateway_policy_id cgp_s28_t_default vendor_id ven_s28_t vendor_gateway_id vgw_s28_t_uas line_group_id llg_s28_t started_at $Ts answered_at $Ts ended_at $Ts duration_sec 6 sip_code 200 hangup_reason NORMAL_CLEARING recording_key none config_version $var(s28_config_version) created_at $Ts", "$avp(s28_cdr_id)")) {
|
||||
update_stat("s28_cdr_xadd_total", 1);
|
||||
xlog("L_INFO", "S28 success CDR XADD ok redis_id=$avp(s28_cdr_id) event_id=$var(s28_event_id) callid=$ci\n");
|
||||
} else {
|
||||
update_stat("s28_cdr_xadd_error_total", 1);
|
||||
xlog("L_ERR", "S28 success CDR XADD failed event_id=$var(s28_event_id) callid=$ci\n");
|
||||
}
|
||||
}
|
||||
|
||||
route[S28_CDR_FAILURE] {
|
||||
$var(s28_event_id) = "s28-fail-" + $Ts + "-" + $pp + "-" + $ci;
|
||||
$var(s28_idempotency_key) = $ci + ":s28-fail:" + $Ts;
|
||||
if (cache_raw_query("redis:s20", "XADD stream:cdr_payload * schema_version 1 event_id $var(s28_event_id) idempotency_key $var(s28_idempotency_key) call_id $ci node_id a1 opensips_instance opensips-a1 ingress_a_ip 100.90.90.90 rtpengine_node a1 source_ip $si caller $fU callee $rU customer_id $var(s28_customer_id) customer_gateway_id $var(s28_gateway_id) customer_gateway_policy_id $var(s28_policy_id) vendor_id none vendor_gateway_id none line_group_id $var(s28_line_group_id) started_at $Ts answered_at none ended_at $Ts duration_sec 0 sip_code $var(s28_reply_code) hangup_reason $var(s28_reason) recording_key none config_version $var(s28_config_version) created_at $Ts", "$avp(s28_cdr_id)")) {
|
||||
update_stat("s28_cdr_xadd_total", 1);
|
||||
} else {
|
||||
update_stat("s28_cdr_xadd_error_total", 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 /path/to/opensips.cfg.backup" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
source_cfg="$1"
|
||||
test -f "$source_cfg"
|
||||
|
||||
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
backup_dir="/var/backups/lisglosips-s30/$stamp"
|
||||
install -d -o root -g root -m 0700 "$backup_dir"
|
||||
install -o root -g root -m 0600 /etc/opensips/opensips.cfg "$backup_dir/opensips.cfg.pre-restore"
|
||||
install -o root -g root -m 0600 "$source_cfg" "$backup_dir/opensips.cfg.candidate"
|
||||
|
||||
opensips -C -f "$backup_dir/opensips.cfg.candidate"
|
||||
install -o root -g opensips -m 0640 "$backup_dir/opensips.cfg.candidate" /etc/opensips/opensips.cfg
|
||||
systemctl restart opensips
|
||||
systemctl is-active --quiet opensips
|
||||
|
||||
echo "$backup_dir"
|
||||
@@ -0,0 +1,2 @@
|
||||
server ntp.aliyun.com iburst prefer minpoll 4 maxpoll 10
|
||||
server time1.cloud.tencent.com iburst minpoll 4 maxpoll 10
|
||||
@@ -0,0 +1,11 @@
|
||||
[DEFAULT]
|
||||
backend = systemd
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 5
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = 22
|
||||
banaction = nftables-multiport
|
||||
ignoreip = 127.0.0.1/8 ::1 100.91.249.119
|
||||
@@ -0,0 +1,27 @@
|
||||
destroy table inet lisglosips_filter
|
||||
|
||||
table inet lisglosips_filter {
|
||||
set admin_ipv4 {
|
||||
type ipv4_addr
|
||||
elements = { 100.91.249.119, 100.98.167.119 }
|
||||
}
|
||||
|
||||
chain input {
|
||||
type filter hook input priority 10; policy drop;
|
||||
|
||||
ct state invalid counter drop
|
||||
ct state established,related counter accept
|
||||
iifname "lo" counter accept
|
||||
|
||||
ip protocol icmp counter accept
|
||||
ip6 nexthdr ipv6-icmp counter accept
|
||||
|
||||
udp dport 41641 counter accept comment "Tailscale direct transport"
|
||||
|
||||
iifname "tailscale0" ip saddr @admin_ipv4 tcp dport { 22, 443 } counter accept comment "Admin SSH and HTTPS"
|
||||
iifname "tailscale0" ip saddr 100.90.90.90 tcp dport 6379 counter accept comment "Server A to Redis"
|
||||
iifname "tailscale0" ip saddr 100.90.90.90 udp dport 9060 counter accept comment "Server A to HEP"
|
||||
|
||||
counter drop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/sbin/nft -f
|
||||
include "/etc/nftables.d/lisglosips.nft"
|
||||
@@ -0,0 +1,3 @@
|
||||
d /run/lisglosips 0750 lisglosips lisglosips -
|
||||
d /run/lisglo-recorder 0750 lisglo-recorder lisglosips -
|
||||
d /run/lisglo-monitor 0750 lisglo-monitor lisglosips -
|
||||
@@ -0,0 +1,3 @@
|
||||
# LisgloSIPS S04 data bind mounts
|
||||
/data/mysql /var/lib/mysql none bind 0 0
|
||||
/data/redis /var/lib/redis none bind 0 0
|
||||
@@ -0,0 +1,19 @@
|
||||
[mysqld]
|
||||
bind-address = 127.0.0.1
|
||||
mysqlx-bind-address = 127.0.0.1
|
||||
skip_name_resolve = ON
|
||||
local_infile = OFF
|
||||
server_id = 1
|
||||
log_bin = /var/log/mysql/mysql-bin
|
||||
binlog_format = ROW
|
||||
binlog_expire_logs_seconds = 604800
|
||||
sync_binlog = 1
|
||||
innodb_flush_log_at_trx_commit = 1
|
||||
innodb_buffer_pool_size = 1G
|
||||
max_connections = 200
|
||||
default_time_zone = +00:00
|
||||
character_set_server = utf8mb4
|
||||
collation_server = utf8mb4_0900_ai_ci
|
||||
slow_query_log = ON
|
||||
long_query_time = 1
|
||||
slow_query_log_file = /var/log/mysql/mysql-slow.log
|
||||
@@ -0,0 +1,20 @@
|
||||
bind 127.0.0.1 100.90.90.91
|
||||
protected-mode yes
|
||||
port 6379
|
||||
unixsocket /run/redis/redis-server.sock
|
||||
unixsocketperm 770
|
||||
dir /var/lib/redis
|
||||
dbfilename dump.rdb
|
||||
appendonly yes
|
||||
appendfilename "appendonly.aof"
|
||||
appenddirname "appendonlydir"
|
||||
appendfsync everysec
|
||||
save ""
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
stop-writes-on-bgsave-error yes
|
||||
maxmemory 1536mb
|
||||
maxmemory-policy noeviction
|
||||
tcp-keepalive 60
|
||||
aclfile /etc/redis/users.acl
|
||||
@@ -0,0 +1,5 @@
|
||||
# Do not deploy this file as-is. Replace both placeholders with independent
|
||||
# generated secrets and install the result as /etc/redis/users.acl, mode 0640.
|
||||
user default off
|
||||
user lisglosips on >GENERATE_APP_SECRET ~* &* +@all -@dangerous
|
||||
user lisglo-backup on >GENERATE_BACKUP_SECRET ~* &* +ping +info +bgsave +lastsave
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
dest=/data/backups/mysql
|
||||
install -d -o root -g root -m 0700 "$dest"
|
||||
[ "$(realpath "$dest")" = "/data/backups/mysql" ] || exit 2
|
||||
exec 9>/run/lock/lisglosips-mysql-backup.lock
|
||||
flock -n 9 || { echo "MySQL backup already running" >&2; exit 3; }
|
||||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
tmp=$(mktemp -d "$dest/.tmp-$stamp.XXXXXX")
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mysqladmin --protocol=socket ping --silent
|
||||
mysqldump --protocol=socket --all-databases --single-transaction --routines --events --triggers --hex-blob --set-gtid-purged=OFF --no-tablespaces | gzip -9 > "$tmp/all-databases.sql.gz"
|
||||
mysqldump --protocol=socket --single-transaction --routines --events --triggers --hex-blob --set-gtid-purged=OFF --no-tablespaces lisglosips | gzip -9 > "$tmp/lisglosips.sql.gz"
|
||||
mysql --protocol=socket -N -e "SELECT @@version, @@global.gtid_executed, @@global.binlog_format, UTC_TIMESTAMP();" > "$tmp/metadata.tsv"
|
||||
(cd "$tmp" && sha256sum *.gz metadata.tsv > SHA256SUMS)
|
||||
chmod 0600 "$tmp"/*
|
||||
final="$dest/$stamp"
|
||||
mv "$tmp" "$final"
|
||||
trap - EXIT
|
||||
find "$dest" -mindepth 1 -maxdepth 1 -type d -name '20*T*Z' -mtime +14 -exec rm -rf -- {} +
|
||||
echo "$final"
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
dest=/data/backups/redis
|
||||
install -d -o root -g root -m 0700 "$dest"
|
||||
[ "$(realpath "$dest")" = "/data/backups/redis" ] || exit 2
|
||||
exec 9>/run/lock/lisglosips-redis-backup.lock
|
||||
flock -n 9 || { echo "Redis backup already running" >&2; exit 3; }
|
||||
set -a
|
||||
. /etc/lisglosips/secrets/redis-backup.env
|
||||
set +a
|
||||
export REDISCLI_AUTH="$REDIS_PASSWORD"
|
||||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
tmp=$(mktemp -d "$dest/.tmp-$stamp.XXXXXX")
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
redis_cmd=(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user "$REDIS_USER" --no-auth-warning)
|
||||
"${redis_cmd[@]}" PING | grep -qx PONG
|
||||
"${redis_cmd[@]}" BGSAVE >/dev/null
|
||||
for _ in $(seq 1 60); do
|
||||
info=$("${redis_cmd[@]}" INFO persistence | tr -d '\r')
|
||||
in_progress=$(printf '%s\n' "$info" | awk -F: '/^rdb_bgsave_in_progress:/{print $2}')
|
||||
status=$(printf '%s\n' "$info" | awk -F: '/^rdb_last_bgsave_status:/{print $2}')
|
||||
[ "$in_progress" = "0" ] && [ "$status" = "ok" ] && break
|
||||
sleep 1
|
||||
done
|
||||
[ "$in_progress" = "0" ] && [ "$status" = "ok" ]
|
||||
install -o root -g root -m 0600 /var/lib/redis/dump.rdb "$tmp/dump.rdb"
|
||||
redis-check-rdb "$tmp/dump.rdb" > "$tmp/redis-check-rdb.txt"
|
||||
"${redis_cmd[@]}" INFO server | tr -d '\r' | grep -E '^(redis_version|os|arch_bits|process_id|tcp_port)' > "$tmp/metadata.txt"
|
||||
(cd "$tmp" && sha256sum dump.rdb metadata.txt > SHA256SUMS)
|
||||
chmod 0600 "$tmp"/*
|
||||
final="$dest/$stamp"
|
||||
mv "$tmp" "$final"
|
||||
trap - EXIT
|
||||
find "$dest" -mindepth 1 -maxdepth 1 -type d -name '20*T*Z' -mtime +14 -exec rm -rf -- {} +
|
||||
echo "$final"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
for _ in $(seq 1 60); do
|
||||
if /usr/sbin/ip -4 -o addr show dev tailscale0 2>/dev/null | /usr/bin/grep -q '100.90.90.91/32'; then
|
||||
exit 0
|
||||
fi
|
||||
/usr/bin/sleep 1
|
||||
done
|
||||
echo 'Tailscale address 100.90.90.91 not ready' >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS MySQL and Redis backup
|
||||
Requires=mysql.service redis-server.service
|
||||
After=mysql.service redis-server.service
|
||||
ConditionPathIsMountPoint=/var/lib/mysql
|
||||
ConditionPathIsMountPoint=/var/lib/redis
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Nice=10
|
||||
IOSchedulingClass=idle
|
||||
ExecStart=/usr/local/sbin/lisglosips-mysql-backup
|
||||
ExecStart=/usr/local/sbin/lisglosips-redis-backup
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Daily LisgloSIPS database backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:15:00
|
||||
RandomizedDelaySec=15m
|
||||
Persistent=true
|
||||
Unit=lisglosips-backup.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Wait for Server B Tailscale address
|
||||
Requires=tailscaled.service
|
||||
After=tailscaled.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/lisglosips-wait-tailscale-b
|
||||
RemainAfterExit=yes
|
||||
@@ -0,0 +1,3 @@
|
||||
[Unit]
|
||||
Requires=var-lib-mysql.mount
|
||||
After=var-lib-mysql.mount
|
||||
@@ -0,0 +1,3 @@
|
||||
[Unit]
|
||||
Requires=var-lib-redis.mount lisglosips-tailscale-ready.service
|
||||
After=var-lib-redis.mount lisglosips-tailscale-ready.service
|
||||
@@ -0,0 +1,2 @@
|
||||
MYSQL_VERSION=8.0.46-0ubuntu0.24.04.2
|
||||
REDIS_VERSION=7.0.15-1ubuntu0.24.04.4
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
LISGLOSIPS_ENTRYPOINT=/opt/lisglosips/current/server.mjs
|
||||
HOST=127.0.0.1
|
||||
PORT=3000
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
server_tokens off;
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=lisglosips_api_per_ip:10m rate=20r/s;
|
||||
limit_req_zone $binary_remote_addr zone=lisglosips_auth_per_ip:10m rate=5r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=lisglosips_conn_per_ip:10m;
|
||||
limit_req_status 429;
|
||||
limit_conn_status 429;
|
||||
|
||||
map $http_upgrade $lisglosips_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Request-ID $request_id;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $lisglosips_connection_upgrade;
|
||||
proxy_connect_timeout 3s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 8k;
|
||||
proxy_buffers 8 16k;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
add_header Strict-Transport-Security "max-age=300" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "same-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; object-src 'none'" always;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
ssl_certificate /etc/lisglosips/pki/certs/server.crt;
|
||||
ssl_certificate_key /etc/lisglosips/pki/private/server.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:LisgloSIPSTLS:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
upstream lisglosips_api {
|
||||
server 127.0.0.1:3000 max_fails=3 fail_timeout=5s;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
return 308 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
server_name _;
|
||||
|
||||
include /etc/nginx/snippets/lisglosips-tls.conf;
|
||||
|
||||
root /opt/lisglosips/current/public;
|
||||
index index.html;
|
||||
client_max_body_size 2m;
|
||||
max_ranges 1;
|
||||
|
||||
include /etc/nginx/snippets/lisglosips-security-headers.conf;
|
||||
|
||||
limit_conn lisglosips_conn_per_ip 30;
|
||||
|
||||
location = /healthz {
|
||||
include /etc/nginx/snippets/lisglosips-proxy.conf;
|
||||
proxy_pass http://lisglosips_api;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /api/auth/login {
|
||||
include /etc/nginx/snippets/lisglosips-proxy.conf;
|
||||
limit_req zone=lisglosips_auth_per_ip burst=3 nodelay;
|
||||
proxy_pass http://lisglosips_api;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
include /etc/nginx/snippets/lisglosips-proxy.conf;
|
||||
limit_req zone=lisglosips_api_per_ip burst=40 nodelay;
|
||||
proxy_pass http://lisglosips_api;
|
||||
}
|
||||
|
||||
location /_recordings/ {
|
||||
internal;
|
||||
alias /data/recordings/;
|
||||
autoindex off;
|
||||
include /etc/nginx/snippets/lisglosips-security-headers.conf;
|
||||
add_header Accept-Ranges bytes always;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
include /etc/nginx/snippets/lisglosips-security-headers.conf;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires -1;
|
||||
include /etc/nginx/snippets/lisglosips-security-headers.conf;
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LisgloSIPS</title>
|
||||
<style>
|
||||
:root { color-scheme: light; font-family: Arial, sans-serif; background: #f5f6f8; color: #12121a; }
|
||||
body { margin: 0; min-height: 100vh; display: grid; place-items: center; }
|
||||
main { width: min(560px, calc(100% - 48px)); border-top: 4px solid #d9c3a0; padding: 32px 0; }
|
||||
h1 { margin: 0 0 12px; font-size: 32px; letter-spacing: 0; }
|
||||
p { margin: 0; color: #5d606b; line-height: 1.7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>LisgloSIPS</h1>
|
||||
<p>聆界SIP管理平台服务入口已就绪。</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createServer } from 'node:http';
|
||||
|
||||
const host = process.env.HOST || '127.0.0.1';
|
||||
const port = Number.parseInt(process.env.PORT || '3000', 10);
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
response.setHeader('Cache-Control', 'no-store');
|
||||
response.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
if (request.method === 'GET' && (request.url === '/healthz' || request.url === '/api/health')) {
|
||||
response.writeHead(200);
|
||||
response.end(JSON.stringify({ status: 'ok', service: 'lisglosips-api-placeholder' }));
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(404);
|
||||
response.end(JSON.stringify({ status: 'not_found' }));
|
||||
});
|
||||
|
||||
server.listen(port, host);
|
||||
|
||||
function shutdown() {
|
||||
server.close((error) => process.exit(error ? 1 : 0));
|
||||
setTimeout(() => process.exit(1), 10_000).unref();
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PKI_ROOT=/etc/lisglosips/pki
|
||||
CA_DIR="$PKI_ROOT/ca"
|
||||
CERT_DIR="$PKI_ROOT/certs"
|
||||
PRIVATE_DIR="$PKI_ROOT/private"
|
||||
HOST_NAME=${HOST_NAME:-yanzi}
|
||||
TAILSCALE_IP=${TAILSCALE_IP:-100.90.90.91}
|
||||
|
||||
umask 077
|
||||
install -d -m 0700 "$CA_DIR"
|
||||
install -d -m 0755 "$CERT_DIR"
|
||||
install -d -m 0700 "$PRIVATE_DIR"
|
||||
|
||||
if [ ! -s "$CA_DIR/lisglosips-dev-ca.key" ] || [ ! -s "$CA_DIR/lisglosips-dev-ca.crt" ]; then
|
||||
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
|
||||
-subj '/CN=LisgloSIPS Development CA/O=LisgloSIPS' \
|
||||
-keyout "$CA_DIR/lisglosips-dev-ca.key" \
|
||||
-out "$CA_DIR/lisglosips-dev-ca.crt"
|
||||
fi
|
||||
|
||||
cat >"$PKI_ROOT/server-cert.cnf" <<EOF
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = dn
|
||||
req_extensions = req_ext
|
||||
|
||||
[dn]
|
||||
CN = lisglosips.local
|
||||
O = LisgloSIPS
|
||||
|
||||
[req_ext]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = lisglosips.local
|
||||
DNS.2 = $HOST_NAME
|
||||
DNS.3 = grafana.lisglosips.local
|
||||
DNS.4 = homer.lisglosips.local
|
||||
IP.1 = $TAILSCALE_IP
|
||||
IP.2 = 127.0.0.1
|
||||
|
||||
[server_ext]
|
||||
basicConstraints = critical,CA:FALSE
|
||||
keyUsage = critical,digitalSignature,keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
EOF
|
||||
|
||||
openssl req -new -newkey rsa:3072 -nodes -sha256 \
|
||||
-config "$PKI_ROOT/server-cert.cnf" \
|
||||
-keyout "$PRIVATE_DIR/server.key" \
|
||||
-out "$PKI_ROOT/server.csr"
|
||||
|
||||
openssl x509 -req -sha256 -days 397 \
|
||||
-in "$PKI_ROOT/server.csr" \
|
||||
-CA "$CA_DIR/lisglosips-dev-ca.crt" \
|
||||
-CAkey "$CA_DIR/lisglosips-dev-ca.key" \
|
||||
-CAcreateserial \
|
||||
-extfile "$PKI_ROOT/server-cert.cnf" \
|
||||
-extensions server_ext \
|
||||
-out "$CERT_DIR/server.crt"
|
||||
|
||||
chown root:root "$CA_DIR/lisglosips-dev-ca.key" "$PKI_ROOT/server.csr" "$PKI_ROOT/server-cert.cnf"
|
||||
chown root:www-data "$PRIVATE_DIR/server.key"
|
||||
chown root:root "$CA_DIR/lisglosips-dev-ca.crt" "$CERT_DIR/server.crt"
|
||||
chmod 0600 "$CA_DIR/lisglosips-dev-ca.key" "$PKI_ROOT/server.csr" "$PKI_ROOT/server-cert.cnf"
|
||||
chmod 0640 "$PRIVATE_DIR/server.key"
|
||||
chmod 0644 "$CA_DIR/lisglosips-dev-ca.crt" "$CERT_DIR/server.crt"
|
||||
|
||||
openssl verify -CAfile "$CA_DIR/lisglosips-dev-ca.crt" "$CERT_DIR/server.crt"
|
||||
@@ -0,0 +1,42 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS %i service
|
||||
After=network-online.target mysql.service redis-server.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=lisglosips
|
||||
Group=lisglosips
|
||||
WorkingDirectory=/opt/lisglosips/current
|
||||
Environment=NODE_ENV=production
|
||||
EnvironmentFile=/etc/lisglosips/%i.env
|
||||
ExecStart=/usr/bin/node ${LISGLOSIPS_ENTRYPOINT}
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
TimeoutStartSec=30s
|
||||
TimeoutStopSec=30s
|
||||
KillSignal=SIGTERM
|
||||
UMask=0027
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectClock=true
|
||||
ProtectHostname=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
StateDirectory=lisglosips
|
||||
RuntimeDirectory=lisglosips
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
NODEJS_VERSION=22.22.2-1nodesource1
|
||||
NODEJS_RUNTIME=v22.22.2
|
||||
PNPM_VERSION=10.33.0
|
||||
COREPACK_VERSION=0.34.6
|
||||
NGINX_VERSION=1.24.0-2ubuntu7.12
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
GRAFANA_VERSION=13.0.2
|
||||
GRAFANA_PACKAGE=grafana_13.0.2_26816849631_linux_amd64.deb
|
||||
GRAFANA_SHA256=ce64458852e49b897cabe7dee32741c9cf1bb623092ee4c6f7250aa7c6b83453
|
||||
GRAFANA_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/grafana/apt
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.m.daocloud.io"
|
||||
],
|
||||
"live-restore": true,
|
||||
"log-driver": "local",
|
||||
"log-opts": {
|
||||
"max-size": "20m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"no-new-privileges": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: LisgloSIPS
|
||||
orgId: 1
|
||||
folder: LisgloSIPS
|
||||
type: file
|
||||
disableDeletion: true
|
||||
editable: false
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards/lisglosips
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
apiVersion: 1
|
||||
|
||||
deleteDatasources:
|
||||
- name: Prometheus
|
||||
orgId: 1
|
||||
- name: HOMER PostgreSQL
|
||||
orgId: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
uid: lisglosips-prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://127.0.0.1:9090
|
||||
isDefault: true
|
||||
editable: false
|
||||
|
||||
- name: HOMER PostgreSQL
|
||||
uid: lisglosips-homer-postgres
|
||||
type: postgres
|
||||
access: proxy
|
||||
url: 127.0.0.1:5432
|
||||
user: homer_user
|
||||
database: homer_data
|
||||
editable: false
|
||||
jsonData:
|
||||
sslmode: disable
|
||||
postgresVersion: 1600
|
||||
timescaledb: false
|
||||
secureJsonData:
|
||||
password: $HOMER_DB_PASSWORD
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
GRAFANA_USER=grafana
|
||||
GRAFANA_GROUP=grafana
|
||||
GRAFANA_HOME=/usr/share/grafana
|
||||
LOG_DIR=/var/log/grafana
|
||||
DATA_DIR=/data/grafana
|
||||
MAX_OPEN_FILES=10000
|
||||
CONF_DIR=/etc/grafana
|
||||
CONF_FILE=/etc/grafana/grafana.ini
|
||||
RESTART_ON_UPGRADE=true
|
||||
PLUGINS_DIR=/data/grafana/plugins
|
||||
PROVISIONING_CFG_DIR=/etc/grafana/provisioning
|
||||
PID_FILE_DIR=/run/grafana
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"annotations": {"list": []},
|
||||
"editable": false,
|
||||
"panels": [
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Targets Up",
|
||||
"datasource": {"type": "prometheus", "uid": "lisglosips-prometheus"},
|
||||
"targets": [{"expr": "sum(up)", "refId": "A"}],
|
||||
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 0}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "A/B CPU Usage",
|
||||
"datasource": {"type": "prometheus", "uid": "lisglosips-prometheus"},
|
||||
"targets": [{"expr": "100 - avg by (server) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100", "legendFormat": "{{server}}", "refId": "A"}],
|
||||
"gridPos": {"h": 8, "w": 16, "x": 8, "y": 0}
|
||||
},
|
||||
{
|
||||
"type": "timeseries",
|
||||
"title": "A/B Memory Usage",
|
||||
"datasource": {"type": "prometheus", "uid": "lisglosips-prometheus"},
|
||||
"targets": [{"expr": "100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)", "legendFormat": "{{server}}", "refId": "A"}],
|
||||
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 8}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 41,
|
||||
"tags": ["lisglosips", "infrastructure"],
|
||||
"templating": {"list": []},
|
||||
"time": {"from": "now-1h", "to": "now"},
|
||||
"title": "LisgloSIPS Infrastructure Overview",
|
||||
"uid": "lisglosips-infra-overview",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
HEPLIFYSERVER_HEPADDR=100.90.90.91:9060
|
||||
HEPLIFYSERVER_HEPTCPADDR=
|
||||
HEPLIFYSERVER_HEPTLSADDR=
|
||||
HEPLIFYSERVER_DBSHEMA=homer7
|
||||
HEPLIFYSERVER_DBDRIVER=postgres
|
||||
HEPLIFYSERVER_DBADDR=127.0.0.1:5432
|
||||
HEPLIFYSERVER_DBUSER=homer_user
|
||||
HEPLIFYSERVER_DBPASS=<HOMER_DB_PASSWORD>
|
||||
HEPLIFYSERVER_DBDATATABLE=homer_data
|
||||
HEPLIFYSERVER_DBCONFTABLE=homer_config
|
||||
HEPLIFYSERVER_DBDROPDAYS=7
|
||||
HEPLIFYSERVER_DBROTATE=true
|
||||
HEPLIFYSERVER_DBBUFFER=200000
|
||||
HEPLIFYSERVER_DBBULK=200
|
||||
HEPLIFYSERVER_DBWORKER=4
|
||||
HEPLIFYSERVER_DBTIMER=2
|
||||
HEPLIFYSERVER_LOGLVL=info
|
||||
HEPLIFYSERVER_LOGSTD=true
|
||||
HEPLIFYSERVER_PROMADDR=127.0.0.1:9096
|
||||
HEPLIFYSERVER_PROMTARGETIP=100.90.90.91
|
||||
HEPLIFYSERVER_PROMTARGETNAME=server-b
|
||||
HEPLIFYSERVER_DEDUP=false
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>LisgloSIPS HOMER</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>LisgloSIPS HOMER</h1>
|
||||
<p>HOMER API 与 HEP 存储服务已就绪。</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"database_data": {
|
||||
"LocalNode": {
|
||||
"node": "LocalNode",
|
||||
"user": "homer_user",
|
||||
"pass": "<HOMER_DB_PASSWORD>",
|
||||
"name": "homer_data",
|
||||
"keepalive": true,
|
||||
"host": "127.0.0.1"
|
||||
}
|
||||
},
|
||||
"database_config": {
|
||||
"node": "LocalConfig",
|
||||
"user": "homer_user",
|
||||
"pass": "<HOMER_DB_PASSWORD>",
|
||||
"name": "homer_config",
|
||||
"keepalive": true,
|
||||
"host": "127.0.0.1"
|
||||
},
|
||||
"hep_relay": {
|
||||
"host": "100.90.90.91",
|
||||
"port": 9060
|
||||
},
|
||||
"prometheus_config": {
|
||||
"enable": true,
|
||||
"host": "http://127.0.0.1:9090",
|
||||
"api": "api/v1"
|
||||
},
|
||||
"influxdb_config": {
|
||||
"enable": false
|
||||
},
|
||||
"loki_config": {
|
||||
"enable": false
|
||||
},
|
||||
"grafana_config": {
|
||||
"enable": true,
|
||||
"host": "http://127.0.0.1:3001",
|
||||
"path": "grafana",
|
||||
"proxy_control": false
|
||||
},
|
||||
"http_settings": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9080,
|
||||
"root": "/usr/local/homer/dist",
|
||||
"gzip": false,
|
||||
"gzip_static": false,
|
||||
"path": "/",
|
||||
"debug": false
|
||||
},
|
||||
"swagger": {
|
||||
"enable": true,
|
||||
"api_json": "/etc/homer/swagger.json",
|
||||
"api_host": "homer.lisglosips.local"
|
||||
},
|
||||
"system_settings": {
|
||||
"logpath": "/var/log/homer",
|
||||
"logname": "homer-app.log",
|
||||
"loglevel": "info",
|
||||
"logstdout": true
|
||||
},
|
||||
"auth_settings": {
|
||||
"type": "internal",
|
||||
"jwt_secret": "<HOMER_JWT_SECRET>",
|
||||
"gravatar": false,
|
||||
"token_expire": 1200,
|
||||
"user_groups": ["admin", "user", "support"]
|
||||
},
|
||||
"api_settings": {
|
||||
"enable_token_access": false,
|
||||
"add_captid_to_resolve": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name grafana.lisglosips.local;
|
||||
|
||||
include /etc/nginx/snippets/lisglosips-tls.conf;
|
||||
add_header Strict-Transport-Security "max-age=300" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "same-origin" always;
|
||||
|
||||
location / {
|
||||
include /etc/nginx/snippets/lisglosips-proxy.conf;
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name homer.lisglosips.local;
|
||||
|
||||
include /etc/nginx/snippets/lisglosips-tls.conf;
|
||||
add_header Strict-Transport-Security "max-age=300" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "same-origin" always;
|
||||
|
||||
location / {
|
||||
include /etc/nginx/snippets/lisglosips-proxy.conf;
|
||||
proxy_pass http://127.0.0.1:9080;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
data_directory = '/data/homer/postgresql'
|
||||
listen_addresses = '127.0.0.1'
|
||||
port = 5432
|
||||
password_encryption = 'scram-sha-256'
|
||||
timezone = 'UTC'
|
||||
log_timezone = 'UTC'
|
||||
|
||||
max_connections = 100
|
||||
shared_buffers = '512MB'
|
||||
effective_cache_size = '2GB'
|
||||
maintenance_work_mem = '128MB'
|
||||
work_mem = '8MB'
|
||||
min_wal_size = '256MB'
|
||||
max_wal_size = '2GB'
|
||||
checkpoint_completion_target = 0.9
|
||||
random_page_cost = 1.1
|
||||
|
||||
logging_collector = on
|
||||
log_directory = '/var/log/postgresql'
|
||||
log_filename = 'postgresql-homer-%Y-%m-%d.log'
|
||||
log_rotation_age = '1d'
|
||||
log_truncate_on_rotation = on
|
||||
log_min_duration_statement = 1000
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ARGS=--web.listen-address=127.0.0.1:9100 --collector.systemd --collector.processes
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ARGS=--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/data/prometheus --storage.tsdb.retention.time=15d --storage.tsdb.retention.size=10GB --web.listen-address=127.0.0.1:9090 --web.enable-lifecycle
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
external_labels:
|
||||
environment: development
|
||||
platform: lisglosips
|
||||
|
||||
rule_files:
|
||||
- /etc/prometheus/rules/*.yml
|
||||
|
||||
scrape_configs:
|
||||
- job_name: prometheus
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9090"]
|
||||
|
||||
- job_name: node
|
||||
static_configs:
|
||||
- targets: ["100.90.90.90:9100"]
|
||||
labels:
|
||||
server: server-a
|
||||
- targets: ["127.0.0.1:9100"]
|
||||
labels:
|
||||
server: server-b
|
||||
|
||||
- job_name: mysql
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9104"]
|
||||
|
||||
- job_name: postgresql_homer
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9187"]
|
||||
|
||||
- job_name: redis
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9121"]
|
||||
|
||||
- job_name: heplify_server
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9096"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
groups:
|
||||
- name: lisglosips-base
|
||||
rules:
|
||||
- alert: LisgloSIPSTargetDown
|
||||
expr: up == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Prometheus target {{ $labels.instance }} is down"
|
||||
|
||||
- alert: LisgloSIPSDiskLow
|
||||
expr: 100 * node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"} < 15
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Disk free space below 15% on {{ $labels.instance }}"
|
||||
|
||||
Executable
+187
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${HOMER_DB_PASSWORD:?missing HOMER_DB_PASSWORD}"
|
||||
: "${HOMER_UI_PASSWORD:?missing HOMER_UI_PASSWORD}"
|
||||
: "${HOMER_JWT_SECRET:?missing HOMER_JWT_SECRET}"
|
||||
: "${GRAFANA_ADMIN_PASSWORD:?missing GRAFANA_ADMIN_PASSWORD}"
|
||||
: "${MYSQL_EXPORTER_PASSWORD:?missing MYSQL_EXPORTER_PASSWORD}"
|
||||
: "${REDIS_EXPORTER_PASSWORD:?missing REDIS_EXPORTER_PASSWORD}"
|
||||
: "${POSTGRES_EXPORTER_PASSWORD:?missing POSTGRES_EXPORTER_PASSWORD}"
|
||||
|
||||
STAGE=${STAGE:-/tmp/lisglosips-s06-stage-202606202230}
|
||||
BUILD=/var/tmp/lisglosips-s06-go-20260620/bin
|
||||
HOMER_SOURCE=/var/tmp/lisglosips-s06-go-20260620/pkg/mod/github.com/sipcapture/homer-app@v0.0.0-20251021161517-9b1336352aa0
|
||||
|
||||
test -d "$STAGE/systemd"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
systemctl mask prometheus-postgres-exporter.service 2>/dev/null || true
|
||||
apt-get install -y prometheus-postgres-exporter=0.15.0-1ubuntu0.3
|
||||
apt-mark hold prometheus-postgres-exporter >/dev/null
|
||||
|
||||
for account in homer heplify redis-exporter; do
|
||||
if ! getent passwd "$account" >/dev/null; then
|
||||
useradd --system --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin "$account"
|
||||
fi
|
||||
done
|
||||
|
||||
install -o root -g root -m 0755 "$BUILD/heplify-server" /usr/local/bin/heplify-server
|
||||
install -o root -g root -m 0755 "$BUILD/homer-app" /usr/local/bin/homer-app
|
||||
install -o root -g root -m 0755 "$BUILD/redis_exporter" /usr/local/bin/redis_exporter
|
||||
install -o root -g root -m 0644 "$STAGE/versions.env" /etc/lisglosips/monitoring-versions.env
|
||||
|
||||
systemctl stop postgresql.service
|
||||
chown root:postgres /data/homer
|
||||
chmod 0750 /data/homer
|
||||
install -d -o postgres -g postgres -m 0700 /data/homer/postgresql
|
||||
if [ -z "$(find /data/homer/postgresql -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||||
rsync -aHAX /var/lib/postgresql/16/main/ /data/homer/postgresql/
|
||||
fi
|
||||
chown -R postgres:postgres /data/homer/postgresql
|
||||
chmod 0700 /data/homer/postgresql
|
||||
install -d -o root -g postgres -m 0750 /etc/postgresql/16/main/conf.d
|
||||
install -o root -g postgres -m 0640 "$STAGE/postgresql/lisglosips-homer.conf" /etc/postgresql/16/main/conf.d/lisglosips-homer.conf
|
||||
grep -Eq "^[[:space:]]*include_dir[[:space:]]*=[[:space:]]*'conf.d'" /etc/postgresql/16/main/postgresql.conf
|
||||
systemctl start postgresql.service
|
||||
pg_isready -h 127.0.0.1 -p 5432
|
||||
|
||||
runuser -u postgres -- psql -v ON_ERROR_STOP=1 -v homer_pw="$HOMER_DB_PASSWORD" -v exporter_pw="$POSTGRES_EXPORTER_PASSWORD" <<'SQL'
|
||||
SELECT format('CREATE ROLE homer_user LOGIN PASSWORD %L', :'homer_pw')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'homer_user') \gexec
|
||||
SELECT format('ALTER ROLE homer_user LOGIN PASSWORD %L', :'homer_pw') \gexec
|
||||
SELECT 'CREATE DATABASE homer_config OWNER homer_user'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'homer_config') \gexec
|
||||
SELECT 'CREATE DATABASE homer_data OWNER homer_user'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'homer_data') \gexec
|
||||
SELECT format('CREATE ROLE postgres_exporter LOGIN PASSWORD %L', :'exporter_pw')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'postgres_exporter') \gexec
|
||||
SELECT format('ALTER ROLE postgres_exporter LOGIN PASSWORD %L', :'exporter_pw') \gexec
|
||||
GRANT pg_monitor TO postgres_exporter;
|
||||
SQL
|
||||
|
||||
mysql --protocol=socket <<SQL
|
||||
CREATE USER IF NOT EXISTS 'mysqld_exporter'@'127.0.0.1' IDENTIFIED WITH caching_sha2_password BY '$MYSQL_EXPORTER_PASSWORD';
|
||||
ALTER USER 'mysqld_exporter'@'127.0.0.1' IDENTIFIED WITH caching_sha2_password BY '$MYSQL_EXPORTER_PASSWORD';
|
||||
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'mysqld_exporter'@'127.0.0.1';
|
||||
GRANT SELECT ON performance_schema.* TO 'mysqld_exporter'@'127.0.0.1';
|
||||
FLUSH PRIVILEGES;
|
||||
SQL
|
||||
|
||||
sed -i '/^user redis_exporter /d' /etc/redis/users.acl
|
||||
printf 'user redis_exporter on >%s ~* -@all +ping +info +client +config|get +latency +memory +select +slowlog +scan +type +strlen +llen +scard +zcard +xlen +hscan +sscan +zscan +get\n' "$REDIS_EXPORTER_PASSWORD" >>/etc/redis/users.acl
|
||||
chown root:redis /etc/redis/users.acl
|
||||
chmod 0640 /etc/redis/users.acl
|
||||
systemctl restart redis-server.service
|
||||
|
||||
install -d -o root -g root -m 0755 /etc/lisglosips/secrets
|
||||
sed "s/<HOMER_DB_PASSWORD>/$HOMER_DB_PASSWORD/g" "$STAGE/heplify/heplify.env.example" >/etc/lisglosips/secrets/heplify.env
|
||||
chown root:heplify /etc/lisglosips/secrets/heplify.env
|
||||
chmod 0640 /etc/lisglosips/secrets/heplify.env
|
||||
|
||||
cat >/etc/lisglosips/secrets/grafana.env <<EOF
|
||||
GF_SECURITY_ADMIN_USER=admin
|
||||
GF_SECURITY_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD
|
||||
GF_USERS_ALLOW_SIGN_UP=false
|
||||
GF_AUTH_ANONYMOUS_ENABLED=false
|
||||
GF_SERVER_HTTP_ADDR=127.0.0.1
|
||||
GF_SERVER_HTTP_PORT=3001
|
||||
GF_SERVER_DOMAIN=grafana.lisglosips.local
|
||||
GF_SERVER_ROOT_URL=https://grafana.lisglosips.local/
|
||||
GF_SECURITY_COOKIE_SECURE=true
|
||||
GF_SECURITY_COOKIE_SAMESITE=strict
|
||||
GF_SECURITY_DISABLE_GRAVATAR=true
|
||||
GF_ANALYTICS_REPORTING_ENABLED=false
|
||||
GF_ANALYTICS_CHECK_FOR_UPDATES=false
|
||||
GF_PLUGINS_PREINSTALL_DISABLED=true
|
||||
GF_PLUGINS_CHECK_FOR_PLUGIN_UPDATES=false
|
||||
GF_PLUGINS_PLUGIN_ADMIN_ENABLED=false
|
||||
HOMER_DB_PASSWORD=$HOMER_DB_PASSWORD
|
||||
EOF
|
||||
chown root:grafana /etc/lisglosips/secrets/grafana.env
|
||||
chmod 0640 /etc/lisglosips/secrets/grafana.env
|
||||
|
||||
cat >/etc/prometheus/mysqld-exporter.cnf <<EOF
|
||||
[client]
|
||||
user=mysqld_exporter
|
||||
password=$MYSQL_EXPORTER_PASSWORD
|
||||
host=127.0.0.1
|
||||
port=3306
|
||||
EOF
|
||||
chown root:prometheus /etc/prometheus/mysqld-exporter.cnf
|
||||
chmod 0640 /etc/prometheus/mysqld-exporter.cnf
|
||||
|
||||
cat >/etc/lisglosips/secrets/postgres-exporter.env <<EOF
|
||||
DATA_SOURCE_NAME=postgresql://postgres_exporter:$POSTGRES_EXPORTER_PASSWORD@127.0.0.1:5432/postgres?sslmode=disable
|
||||
EOF
|
||||
chown root:prometheus /etc/lisglosips/secrets/postgres-exporter.env
|
||||
chmod 0640 /etc/lisglosips/secrets/postgres-exporter.env
|
||||
|
||||
cat >/etc/lisglosips/secrets/redis-exporter.env <<EOF
|
||||
REDIS_ADDR=redis://127.0.0.1:6379
|
||||
REDIS_USER=redis_exporter
|
||||
REDIS_PASSWORD=$REDIS_EXPORTER_PASSWORD
|
||||
REDIS_EXPORTER_WEB_LISTEN_ADDRESS=127.0.0.1:9121
|
||||
EOF
|
||||
chown root:redis-exporter /etc/lisglosips/secrets/redis-exporter.env
|
||||
chmod 0640 /etc/lisglosips/secrets/redis-exporter.env
|
||||
|
||||
install -d -o root -g homer -m 0750 /etc/homer
|
||||
sed -e "s/<HOMER_DB_PASSWORD>/$HOMER_DB_PASSWORD/g" -e "s/<HOMER_JWT_SECRET>/$HOMER_JWT_SECRET/g" "$STAGE/homer/webapp_config.json.example" >/etc/homer/webapp_config.json
|
||||
install -o root -g homer -m 0640 "$HOMER_SOURCE/swagger.json" /etc/homer/swagger.json
|
||||
chown homer:homer /etc/homer/webapp_config.json
|
||||
chmod 0600 /etc/homer/webapp_config.json
|
||||
install -d -o root -g homer -m 0750 /usr/local/homer/dist
|
||||
install -o root -g homer -m 0640 "$STAGE/homer/index.html" /usr/local/homer/dist/index.html
|
||||
install -d -o homer -g homer -m 0750 /var/log/homer
|
||||
|
||||
install -d -o prometheus -g prometheus -m 0750 /data/prometheus
|
||||
install -d -o root -g prometheus -m 0750 /etc/prometheus/rules
|
||||
install -o root -g prometheus -m 0640 "$STAGE/prometheus/prometheus.yml" /etc/prometheus/prometheus.yml
|
||||
install -o root -g prometheus -m 0640 "$STAGE/prometheus/rules/lisglosips.yml" /etc/prometheus/rules/lisglosips.yml
|
||||
install -o root -g prometheus -m 0640 "$STAGE/prometheus/prometheus.env" /etc/default/lisglosips-prometheus
|
||||
install -o root -g prometheus -m 0640 "$STAGE/prometheus/node-exporter-b.env" /etc/default/lisglosips-node-exporter
|
||||
promtool check config /etc/prometheus/prometheus.yml
|
||||
promtool check rules /etc/prometheus/rules/lisglosips.yml
|
||||
|
||||
install -d -o grafana -g grafana -m 0750 /data/grafana /data/grafana/plugins
|
||||
install -o root -g root -m 0644 "$STAGE/grafana/grafana-server.default" /etc/default/grafana-server
|
||||
install -d -o root -g grafana -m 0750 /etc/grafana/provisioning/datasources /etc/grafana/provisioning/dashboards /etc/grafana/provisioning/dashboards/lisglosips
|
||||
install -o root -g grafana -m 0640 "$STAGE/grafana/datasources.yml" /etc/grafana/provisioning/datasources/lisglosips.yml
|
||||
install -o root -g grafana -m 0640 "$STAGE/grafana/dashboards.yml" /etc/grafana/provisioning/dashboards/lisglosips.yml
|
||||
install -o root -g grafana -m 0640 "$STAGE/grafana/lisglosips-overview.json" /etc/grafana/provisioning/dashboards/lisglosips/lisglosips-overview.json
|
||||
|
||||
for unit in heplify-server homer-app lisglosips-prometheus lisglosips-node-exporter lisglosips-mysqld-exporter lisglosips-postgres-exporter lisglosips-redis-exporter; do
|
||||
install -o root -g root -m 0644 "$STAGE/systemd/$unit.service" "/etc/systemd/system/$unit.service"
|
||||
done
|
||||
install -d -o root -g root -m 0755 /etc/systemd/system/grafana-server.service.d
|
||||
install -o root -g root -m 0644 "$STAGE/systemd/grafana-override.conf" /etc/systemd/system/grafana-server.service.d/lisglosips.conf
|
||||
systemctl unmask grafana-server.service
|
||||
|
||||
install -o root -g root -m 0644 "$STAGE/nginx/monitoring.conf" /etc/nginx/sites-available/lisglosips-monitoring.conf
|
||||
ln -sfn /etc/nginx/sites-available/lisglosips-monitoring.conf /etc/nginx/sites-enabled/lisglosips-monitoring.conf
|
||||
install -o root -g root -m 0755 /tmp/lisglosips-issue-dev-cert.s06 /usr/local/sbin/lisglosips-issue-dev-cert
|
||||
rm -f /tmp/lisglosips-issue-dev-cert.s06
|
||||
/usr/local/sbin/lisglosips-issue-dev-cert >/dev/null 2>&1
|
||||
nginx -t
|
||||
|
||||
runuser -u homer -- /usr/local/bin/homer-app -webapp-config-path=/etc/homer -create-table-db-config >/var/log/homer/init-config.log 2>&1
|
||||
runuser -u homer -- /usr/local/bin/homer-app -webapp-config-path=/etc/homer -populate-table-db-config -force-password="$HOMER_UI_PASSWORD" >>/var/log/homer/init-config.log 2>&1
|
||||
chmod 0640 /var/log/homer/init-config.log
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl disable --now prometheus-node-exporter.service prometheus-node-exporter.socket 2>/dev/null || true
|
||||
systemctl mask prometheus-node-exporter.service 2>/dev/null || true
|
||||
systemctl disable --now openipmi.service 2>/dev/null || true
|
||||
systemctl mask openipmi.service 2>/dev/null || true
|
||||
systemctl enable --now heplify-server.service homer-app.service lisglosips-node-exporter.service lisglosips-mysqld-exporter.service lisglosips-postgres-exporter.service lisglosips-redis-exporter.service lisglosips-prometheus.service grafana-server.service
|
||||
systemctl reload nginx.service
|
||||
|
||||
sleep 5
|
||||
for unit in postgresql heplify-server homer-app lisglosips-node-exporter lisglosips-mysqld-exporter lisglosips-postgres-exporter lisglosips-redis-exporter lisglosips-prometheus grafana-server nginx; do
|
||||
systemctl is-active "$unit" >/dev/null
|
||||
done
|
||||
curl -fsS http://127.0.0.1:9090/-/ready
|
||||
curl -fsS http://127.0.0.1:3001/api/health
|
||||
curl -fsS http://127.0.0.1:9096/metrics >/dev/null
|
||||
printf 'S06_SERVER_B_CONFIG=PASS\n'
|
||||
@@ -0,0 +1,4 @@
|
||||
[Service]
|
||||
EnvironmentFile=/etc/lisglosips/secrets/grafana.env
|
||||
ReadWritePaths=/data/grafana /var/log/grafana
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS Heplify Server
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=heplify
|
||||
Group=heplify
|
||||
EnvironmentFile=/etc/lisglosips/secrets/heplify.env
|
||||
ExecStart=/usr/local/bin/heplify-server
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
TimeoutStopSec=30s
|
||||
UMask=0027
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS HOMER App
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=homer
|
||||
Group=homer
|
||||
ExecStart=/usr/local/bin/homer-app -webapp-config-path=/etc/homer
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
TimeoutStopSec=30s
|
||||
UMask=0027
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadWritePaths=/etc/homer /var/log/homer
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,27 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS MySQL Exporter
|
||||
After=mysql.service
|
||||
Requires=mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=prometheus
|
||||
Group=prometheus
|
||||
ExecStart=/usr/bin/prometheus-mysqld-exporter --config.my-cnf=/etc/prometheus/mysqld-exporter.cnf --web.listen-address=127.0.0.1:9104
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS Node Exporter
|
||||
After=network-online.target lisglosips-exporter-firewall.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=prometheus
|
||||
Group=prometheus
|
||||
EnvironmentFile=/etc/default/lisglosips-node-exporter
|
||||
ExecStart=/usr/bin/prometheus-node-exporter $ARGS
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=127.0.0.0/8
|
||||
IPAddressAllow=100.90.90.91/32
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS PostgreSQL Exporter
|
||||
After=postgresql.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=prometheus
|
||||
Group=prometheus
|
||||
EnvironmentFile=/etc/lisglosips/secrets/postgres-exporter.env
|
||||
ExecStart=/usr/bin/prometheus-postgres-exporter --web.listen-address=127.0.0.1:9187
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS Prometheus
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=prometheus
|
||||
Group=prometheus
|
||||
EnvironmentFile=/etc/default/lisglosips-prometheus
|
||||
ExecStart=/usr/bin/prometheus $ARGS
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
TimeoutStopSec=30s
|
||||
UMask=0027
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadWritePaths=/data/prometheus
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[Unit]
|
||||
Description=LisgloSIPS Redis Exporter
|
||||
After=redis-server.service
|
||||
Requires=redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=redis-exporter
|
||||
Group=redis-exporter
|
||||
EnvironmentFile=/etc/lisglosips/secrets/redis-exporter.env
|
||||
ExecStart=/usr/local/bin/redis_exporter
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,18 @@
|
||||
POSTGRESQL_VERSION=16.14
|
||||
PROMETHEUS_VERSION=2.45.3
|
||||
NODE_EXPORTER_VERSION=1.7.0
|
||||
MYSQLD_EXPORTER_VERSION=0.15.0
|
||||
POSTGRES_EXPORTER_VERSION=0.15.0
|
||||
REDIS_EXPORTER_VERSION=1.86.0
|
||||
GRAFANA_VERSION=13.0.2
|
||||
GO_BOOTSTRAP_VERSION=1.22.2
|
||||
GO_BUILD_TOOLCHAIN_VERSION=1.25.11
|
||||
HEPLIFY_SERVER_SOURCE=github.com/sipcapture/heplify-server@v1.60.2-0.20260512101233-c74dc3d216ac
|
||||
HEPLIFY_SERVER_SHA256=feab75e8a970190ee41952bae74ce453188f6d7ea973017cbec1f5b75cc2b9c5
|
||||
HOMER_APP_SOURCE=github.com/sipcapture/homer-app@v0.0.0-20251021161517-9b1336352aa0
|
||||
HOMER_APP_SHA256=240436d1e666db8ca2173679adda9649037c6d5502182f96b25562b7cd757e94
|
||||
REDIS_EXPORTER_SHA256=5eda3529bf231a3c841d8261895bc09d2f750a3684c8a224edae71f4f80484db
|
||||
DOCKER_VERSION=29.1.3
|
||||
DOCKER_COMPOSE_VERSION=2.40.3
|
||||
DOCKER_RUNTIME_STATUS=installed-disabled
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
health_url="${LISGLOSIPS_HEALTH_URL:-http://127.0.0.1:3000/api/v2/health/ready}"
|
||||
services=(
|
||||
mysql
|
||||
redis-server
|
||||
nginx
|
||||
lisglosips@api
|
||||
lisglosips@cdr-worker
|
||||
lisglosips@recording-worker
|
||||
heplify-server
|
||||
lisglosips-prometheus.service
|
||||
grafana-server
|
||||
)
|
||||
|
||||
echo "== release =="
|
||||
readlink -f /opt/lisglosips/current
|
||||
test -L /opt/lisglosips/current
|
||||
test -d "$(readlink -f /opt/lisglosips/current)"
|
||||
|
||||
echo "== services =="
|
||||
for service in "${services[@]}"; do
|
||||
state="$(systemctl is-active "$service" || true)"
|
||||
printf '%s\t%s\n' "$service" "$state"
|
||||
test "$state" = active
|
||||
done
|
||||
|
||||
echo "== api =="
|
||||
curl --max-time 5 --fail --silent "$health_url"
|
||||
echo
|
||||
|
||||
echo "== nginx =="
|
||||
nginx -t
|
||||
|
||||
echo "== backups =="
|
||||
find /data/backups/mysql -mindepth 1 -maxdepth 1 -type d | sort | tail -1
|
||||
find /data/backups/redis -mindepth 1 -maxdepth 1 -type d | sort | tail -1
|
||||
|
||||
echo "preflight ok"
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 /opt/lisglosips/releases/<release-id>" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
target="$1"
|
||||
case "$target" in
|
||||
/opt/lisglosips/releases/*) ;;
|
||||
*)
|
||||
echo "target must be under /opt/lisglosips/releases" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
target="$(readlink -f "$target")"
|
||||
test -d "$target"
|
||||
test -f "$target/package.json"
|
||||
|
||||
previous="$(readlink -f /opt/lisglosips/current)"
|
||||
echo "previous=$previous"
|
||||
echo "target=$target"
|
||||
|
||||
ln -sfn "$target" /opt/lisglosips/current
|
||||
systemctl restart lisglosips@api lisglosips@cdr-worker lisglosips@recording-worker
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
curl --max-time 10 --fail --silent http://127.0.0.1:3000/api/v2/health/ready
|
||||
echo
|
||||
echo "rollback ok"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DOMAIN="s21.lisglosips.test"
|
||||
U1="s21-reg-1001"
|
||||
U2="s21-reg-1002"
|
||||
IP1="s21-ip-1001"
|
||||
IP2="s21-ip-1002"
|
||||
|
||||
P1=$(openssl rand -base64 18 | tr -d '=+/' | cut -c1-20)
|
||||
P2=$(openssl rand -base64 18 | tr -d '=+/' | cut -c1-20)
|
||||
HA1_1=$(printf "%s:%s:%s" "$U1" "$DOMAIN" "$P1" | md5sum | cut -d " " -f1)
|
||||
HA1_2=$(printf "%s:%s:%s" "$U2" "$DOMAIN" "$P2" | md5sum | cut -d " " -f1)
|
||||
unset P1 P2
|
||||
|
||||
install -d -m 0755 /opt/lisglosips-s21
|
||||
install -m 0755 /tmp/lisglosips-s21-sip.py /opt/lisglosips-s21/lisglosips-s21-sip.py
|
||||
for f in /tmp/s21-*; do
|
||||
install -m 0755 "$f" "/opt/lisglosips-s21/$(basename "$f")"
|
||||
done
|
||||
|
||||
install -d -m 0750 -o root -g hector /etc/lisglosips-s21
|
||||
cat > /etc/lisglosips-s21/sip-accounts.env <<EOF
|
||||
S21_SIP_DOMAIN=$DOMAIN
|
||||
S21_SIP_PRIMARY_USER=$U1
|
||||
S21_SIP_SECONDARY_USER=$U2
|
||||
S21_SIP_PRIMARY_HA1=$HA1_1
|
||||
S21_SIP_SECONDARY_HA1=$HA1_2
|
||||
S21_IP_PRIMARY_ID=$IP1
|
||||
S21_IP_SECONDARY_ID=$IP2
|
||||
EOF
|
||||
chown root:hector /etc/lisglosips-s21/sip-accounts.env
|
||||
chmod 0640 /etc/lisglosips-s21/sip-accounts.env
|
||||
|
||||
python3 -m py_compile /opt/lisglosips-s21/lisglosips-s21-sip.py
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
p = Path("/etc/opensips/opensips.cfg")
|
||||
s = p.read_text()
|
||||
s = s.replace('modparam("auth_db", "calculate_ha1", 1)', 'modparam("auth_db", "calculate_ha1", 0)')
|
||||
s = s.replace('modparam("mi_fifo", "fifo_mode", 0666)', 'modparam("mi_fifo", "fifo_mode", 0660)')
|
||||
p.write_text(s)
|
||||
PY
|
||||
|
||||
find /etc/opensips/tls -type f \( -name "*key*.pem" -o -name "cakey.pem" -o -name "*-privkey.pem" \) -exec chmod 0600 {} + 2>/dev/null || true
|
||||
|
||||
mysql --protocol=socket -uroot <<SQL
|
||||
USE opensips;
|
||||
INSERT INTO domain (domain, attrs, accept_subdomain, last_modified)
|
||||
VALUES ('$DOMAIN', 'S21 SIP registration test domain', 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE attrs=VALUES(attrs), accept_subdomain=VALUES(accept_subdomain), last_modified=NOW();
|
||||
|
||||
DELETE FROM subscriber WHERE username IN ('$U1','$U2') AND domain='$DOMAIN';
|
||||
INSERT INTO subscriber (username, domain, password, ha1, ha1_sha256, ha1_sha512t256)
|
||||
VALUES
|
||||
('$U1', '$DOMAIN', '', '$HA1_1', '', ''),
|
||||
('$U2', '$DOMAIN', '', '$HA1_2', '', '');
|
||||
|
||||
DELETE FROM address WHERE context_info IN ('$IP1','$IP2');
|
||||
INSERT INTO address (grp, ip, mask, port, proto, pattern, context_info)
|
||||
VALUES
|
||||
(21, '100.93.185.30', 32, 0, 'any', NULL, '$IP1'),
|
||||
(22, '100.93.185.30', 32, 0, 'any', NULL, '$IP2');
|
||||
SQL
|
||||
|
||||
opensips -C -f /etc/opensips/opensips.cfg >/run/lisglosips_s21_opensips_check.log 2>&1
|
||||
systemctl restart opensips
|
||||
sleep 1
|
||||
systemctl is-active --quiet opensips
|
||||
|
||||
mysql --protocol=socket -uroot -N <<SQL
|
||||
USE opensips;
|
||||
SELECT 'domain', domain FROM domain WHERE domain='$DOMAIN';
|
||||
SELECT 'subscriber', username, domain, LENGTH(password), LENGTH(ha1) FROM subscriber WHERE domain='$DOMAIN' ORDER BY username;
|
||||
SELECT 'address', grp, ip, mask, proto, context_info FROM address WHERE context_info IN ('$IP1','$IP2') ORDER BY context_info;
|
||||
SQL
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LisgloSIPS S21 SIP scenario runner.
|
||||
|
||||
Self-contained UDP SIP helper for Server T customer simulation.
|
||||
It intentionally accepts HA1 values instead of plaintext SIP passwords.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import string
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
CRLF = "\r\n"
|
||||
|
||||
|
||||
def md5_hex(value: str) -> str:
|
||||
return hashlib.md5(value.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def rand_token(length: int = 10) -> str:
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
return "".join(random.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def call_id(prefix: str = "s21") -> str:
|
||||
return f"{prefix}-{int(time.time() * 1000)}-{rand_token(8)}@lisglosips-t"
|
||||
|
||||
|
||||
def parse_authenticate(header: str) -> dict[str, str]:
|
||||
header = header.strip()
|
||||
if header.lower().startswith("digest "):
|
||||
header = header[7:]
|
||||
values: dict[str, str] = {}
|
||||
for part in re.split(r",\s*", header):
|
||||
if "=" not in part:
|
||||
continue
|
||||
key, raw = part.split("=", 1)
|
||||
values[key.strip().lower()] = raw.strip().strip('"')
|
||||
return values
|
||||
|
||||
|
||||
def header_value(message: str, name: str) -> str | None:
|
||||
pattern = re.compile(rf"^{re.escape(name)}\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE)
|
||||
match = pattern.search(message)
|
||||
return match.group(1).strip() if match else None
|
||||
|
||||
|
||||
def status_line(message: str) -> str:
|
||||
return message.splitlines()[0] if message else "NO RESPONSE"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SipTarget:
|
||||
host: str
|
||||
port: int
|
||||
|
||||
|
||||
def udp_roundtrip(payload: str, target: SipTarget, timeout: float = 3.0, bind_host: str = "0.0.0.0") -> str:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.bind((bind_host, 0))
|
||||
sock.settimeout(timeout)
|
||||
sock.sendto(payload.encode("utf-8"), (target.host, target.port))
|
||||
try:
|
||||
data, _ = sock.recvfrom(65535)
|
||||
except socket.timeout:
|
||||
return ""
|
||||
return data.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def build_register(
|
||||
username: str,
|
||||
domain: str,
|
||||
contact_host: str,
|
||||
local_port: int,
|
||||
cseq: int,
|
||||
cid: str,
|
||||
branch: str,
|
||||
authorization: str | None = None,
|
||||
) -> str:
|
||||
lines = [
|
||||
f"REGISTER sip:{domain} SIP/2.0",
|
||||
f"Via: SIP/2.0/UDP {contact_host}:{local_port};branch=z9hG4bK-{branch};rport",
|
||||
"Max-Forwards: 70",
|
||||
f"From: <sip:{username}@{domain}>;tag={rand_token(8)}",
|
||||
f"To: <sip:{username}@{domain}>",
|
||||
f"Call-ID: {cid}",
|
||||
f"CSeq: {cseq} REGISTER",
|
||||
f"Contact: <sip:{username}@{contact_host}:{local_port}>;expires=120",
|
||||
"Expires: 120",
|
||||
"User-Agent: lisglosips-s21",
|
||||
]
|
||||
if authorization:
|
||||
lines.append(f"Authorization: {authorization}")
|
||||
lines.extend(["Content-Length: 0", "", ""])
|
||||
return CRLF.join(lines)
|
||||
|
||||
|
||||
def digest_authorization(username: str, realm: str, uri: str, method: str, ha1: str, challenge: dict[str, str]) -> str:
|
||||
nonce = challenge["nonce"]
|
||||
qop = challenge.get("qop", "").split(",")[0].strip()
|
||||
algorithm = challenge.get("algorithm", "MD5")
|
||||
ha2 = md5_hex(f"{method}:{uri}")
|
||||
if qop:
|
||||
nc = "00000001"
|
||||
cnonce = rand_token(16)
|
||||
response = md5_hex(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}")
|
||||
return (
|
||||
f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{uri}", '
|
||||
f'response="{response}", algorithm={algorithm}, qop={qop}, nc={nc}, cnonce="{cnonce}"'
|
||||
)
|
||||
response = md5_hex(f"{ha1}:{nonce}:{ha2}")
|
||||
return (
|
||||
f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{uri}", '
|
||||
f'response="{response}", algorithm={algorithm}'
|
||||
)
|
||||
|
||||
|
||||
def cmd_register(args: argparse.Namespace) -> int:
|
||||
cid = call_id("s21-register")
|
||||
target = SipTarget(args.host, args.port)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.bind((args.bind_host, 0))
|
||||
local_host, local_port = sock.getsockname()
|
||||
if args.contact_host:
|
||||
local_host = args.contact_host
|
||||
sock.settimeout(args.timeout)
|
||||
first = build_register(args.username, args.domain, local_host, local_port, 1, cid, rand_token())
|
||||
sock.sendto(first.encode("utf-8"), (target.host, target.port))
|
||||
try:
|
||||
data, _ = sock.recvfrom(65535)
|
||||
except socket.timeout:
|
||||
print(f"call_id={cid} status=NO_RESPONSE")
|
||||
return 2
|
||||
response = data.decode("utf-8", errors="replace")
|
||||
print(f"call_id={cid} first_status={status_line(response)}")
|
||||
www = header_value(response, "WWW-Authenticate") or header_value(response, "Proxy-Authenticate")
|
||||
if not www:
|
||||
return 0 if response.startswith("SIP/2.0 200") else 1
|
||||
challenge = parse_authenticate(www)
|
||||
realm = challenge.get("realm", args.domain)
|
||||
uri = f"sip:{args.domain}"
|
||||
auth = digest_authorization(args.username, realm, uri, "REGISTER", args.ha1, challenge)
|
||||
second = build_register(args.username, args.domain, local_host, local_port, 2, cid, rand_token(), auth)
|
||||
sock.sendto(second.encode("utf-8"), (target.host, target.port))
|
||||
try:
|
||||
data, _ = sock.recvfrom(65535)
|
||||
except socket.timeout:
|
||||
print(f"call_id={cid} final_status=NO_RESPONSE")
|
||||
return 2
|
||||
final = data.decode("utf-8", errors="replace")
|
||||
print(f"call_id={cid} final_status={status_line(final)}")
|
||||
return 0 if final.startswith("SIP/2.0 200") else 1
|
||||
|
||||
|
||||
def build_invite(args: argparse.Namespace, cid: str, cseq: int = 1) -> str:
|
||||
branch = rand_token(10)
|
||||
from_tag = rand_token(8)
|
||||
from_uri = f"sip:{args.caller}@{args.from_domain}"
|
||||
to_uri = f"sip:{args.callee}@{args.to_domain}"
|
||||
request_uri = f"sip:{args.callee}@{args.to_domain}"
|
||||
sdp = (
|
||||
"v=0\r\n"
|
||||
f"o=lisglosips-s21 0 0 IN IP4 {args.media_host}\r\n"
|
||||
"s=lisglosips-s21\r\n"
|
||||
f"c=IN IP4 {args.media_host}\r\n"
|
||||
"t=0 0\r\n"
|
||||
f"m=audio {args.media_port} RTP/AVP 0 8 101\r\n"
|
||||
"a=rtpmap:0 PCMU/8000\r\n"
|
||||
"a=rtpmap:8 PCMA/8000\r\n"
|
||||
"a=rtpmap:101 telephone-event/8000\r\n"
|
||||
)
|
||||
lines = [
|
||||
f"INVITE {request_uri} SIP/2.0",
|
||||
f"Via: SIP/2.0/UDP {args.contact_host}:{args.local_port};branch=z9hG4bK-{branch};rport",
|
||||
"Max-Forwards: 70",
|
||||
f"From: <{from_uri}>;tag={from_tag}",
|
||||
f"To: <{to_uri}>",
|
||||
f"Call-ID: {cid}",
|
||||
f"CSeq: {cseq} INVITE",
|
||||
f"Contact: <sip:{args.caller}@{args.contact_host}:{args.local_port}>",
|
||||
"User-Agent: lisglosips-s21",
|
||||
"Content-Type: application/sdp",
|
||||
f"Content-Length: {len(sdp.encode('utf-8'))}",
|
||||
"",
|
||||
sdp,
|
||||
]
|
||||
return CRLF.join(lines)
|
||||
|
||||
|
||||
def cmd_invite(args: argparse.Namespace) -> int:
|
||||
cid = call_id("s21-invite")
|
||||
target = SipTarget(args.host, args.port)
|
||||
payload = build_invite(args, cid)
|
||||
response = udp_roundtrip(payload, target, args.timeout, args.bind_host)
|
||||
print(f"call_id={cid} status={status_line(response)}")
|
||||
return 0 if response else 2
|
||||
|
||||
|
||||
def uas_once(host: str, port: int, mode: str, ready: threading.Event) -> None:
|
||||
code_map = {
|
||||
"success": ("200", "OK"),
|
||||
"busy": ("486", "Busy Here"),
|
||||
"reject": ("603", "Decline"),
|
||||
}
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.bind((host, port))
|
||||
sock.settimeout(5)
|
||||
ready.set()
|
||||
try:
|
||||
data, addr = sock.recvfrom(65535)
|
||||
except socket.timeout:
|
||||
return
|
||||
if mode == "timeout":
|
||||
time.sleep(3)
|
||||
return
|
||||
request = data.decode("utf-8", errors="replace")
|
||||
via = header_value(request, "Via") or ""
|
||||
from_h = header_value(request, "From") or ""
|
||||
to_h = header_value(request, "To") or ""
|
||||
cid = header_value(request, "Call-ID") or call_id("s21-uas")
|
||||
cseq = header_value(request, "CSeq") or "1 INVITE"
|
||||
code, reason = code_map[mode]
|
||||
if "tag=" not in to_h:
|
||||
to_h = f"{to_h};tag={rand_token(8)}"
|
||||
response = CRLF.join(
|
||||
[
|
||||
f"SIP/2.0 {code} {reason}",
|
||||
f"Via: {via}",
|
||||
f"From: {from_h}",
|
||||
f"To: {to_h}",
|
||||
f"Call-ID: {cid}",
|
||||
f"CSeq: {cseq}",
|
||||
"Server: lisglosips-s21-uas",
|
||||
"Content-Length: 0",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
)
|
||||
sock.sendto(response.encode("utf-8"), addr)
|
||||
|
||||
|
||||
def cmd_local_scenario(args: argparse.Namespace) -> int:
|
||||
ready = threading.Event()
|
||||
thread = threading.Thread(target=uas_once, args=(args.listen_host, args.listen_port, args.mode, ready), daemon=True)
|
||||
thread.start()
|
||||
ready.wait(2)
|
||||
args.host = args.listen_host
|
||||
args.port = args.listen_port
|
||||
args.to_domain = f"{args.listen_host}:{args.listen_port}"
|
||||
rc = cmd_invite(args)
|
||||
thread.join(4)
|
||||
if args.mode == "timeout":
|
||||
return 0 if rc == 2 else 1
|
||||
return rc
|
||||
|
||||
|
||||
def cmd_over_cps(args: argparse.Namespace) -> int:
|
||||
target = SipTarget(args.host, args.port)
|
||||
statuses: list[str] = []
|
||||
for index in range(args.count):
|
||||
cid = call_id(f"s21-cps-{index + 1}")
|
||||
args.callee = f"{args.callee_prefix}{index + 1:03d}"
|
||||
payload = build_invite(args, cid)
|
||||
response = udp_roundtrip(payload, target, args.timeout, args.bind_host)
|
||||
line = status_line(response)
|
||||
statuses.append(line)
|
||||
print(f"call_id={cid} status={line}")
|
||||
if args.interval > 0:
|
||||
time.sleep(args.interval)
|
||||
print(f"sent={args.count} unique_statuses={sorted(set(statuses))}")
|
||||
return 0
|
||||
|
||||
|
||||
def add_common_invite_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--host", default="100.90.90.90")
|
||||
parser.add_argument("--port", type=int, default=15060)
|
||||
parser.add_argument("--bind-host", default="0.0.0.0")
|
||||
parser.add_argument("--contact-host", default="100.93.185.30")
|
||||
parser.add_argument("--local-port", type=int, default=50620)
|
||||
parser.add_argument("--caller", default="s21-ip-1001")
|
||||
parser.add_argument("--callee", default="13800138000")
|
||||
parser.add_argument("--from-domain", default="s21-ip.lisglosips.test")
|
||||
parser.add_argument("--to-domain", default="100.90.90.90:15060")
|
||||
parser.add_argument("--media-host", default="100.93.185.30")
|
||||
parser.add_argument("--media-port", type=int, default=31000)
|
||||
parser.add_argument("--timeout", type=float, default=3.0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="LisgloSIPS S21 SIP scenario runner")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
reg = sub.add_parser("register")
|
||||
reg.add_argument("--host", default="100.93.185.30")
|
||||
reg.add_argument("--port", type=int, default=5060)
|
||||
reg.add_argument("--bind-host", default="0.0.0.0")
|
||||
reg.add_argument("--contact-host", default="100.93.185.30")
|
||||
reg.add_argument("--username", required=True)
|
||||
reg.add_argument("--domain", required=True)
|
||||
reg.add_argument("--ha1", required=True)
|
||||
reg.add_argument("--timeout", type=float, default=3.0)
|
||||
reg.set_defaults(func=cmd_register)
|
||||
|
||||
inv = sub.add_parser("invite")
|
||||
add_common_invite_args(inv)
|
||||
inv.set_defaults(func=cmd_invite)
|
||||
|
||||
local = sub.add_parser("local-scenario")
|
||||
add_common_invite_args(local)
|
||||
local.add_argument("--mode", choices=["success", "busy", "reject", "timeout"], required=True)
|
||||
local.add_argument("--listen-host", default="127.0.0.1")
|
||||
local.add_argument("--listen-port", type=int, default=15070)
|
||||
local.set_defaults(func=cmd_local_scenario)
|
||||
|
||||
cps = sub.add_parser("over-cps")
|
||||
add_common_invite_args(cps)
|
||||
cps.add_argument("--count", type=int, default=20)
|
||||
cps.add_argument("--interval", type=float, default=0.02)
|
||||
cps.add_argument("--callee-prefix", default="1390000")
|
||||
cps.set_defaults(func=cmd_over_cps)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py invite \
|
||||
--caller s21-ip-1001 \
|
||||
--from-domain s21-ip.lisglosips.test \
|
||||
--callee "${1:-13800138000}"
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
source /etc/lisglosips-s21/sip-accounts.env
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py invite \
|
||||
--caller "$S21_SIP_PRIMARY_USER" \
|
||||
--from-domain "$S21_SIP_DOMAIN" \
|
||||
--callee "${1:-13800138000}"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode busy --callee "${1:-13800138000}"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode reject --callee "${1:-13800138000}"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode success --callee "${1:-13800138000}"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user