Initial LisgloSIPS V2 implementation

This commit is contained in:
hectorzhao
2026-06-22 10:56:38 +08:00
commit 5fa1bd35e9
303 changed files with 35644 additions and 0 deletions
@@ -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
}
}
+186
View File
@@ -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");
}
}
+14
View File
@@ -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
View File
@@ -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())
+36
View File
@@ -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
View File
@@ -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/