Initial LisgloSIPS V2 implementation
This commit is contained in:
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}"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode timeout --callee "${1:-13800138000}"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec /opt/lisglosips-s21/lisglosips-s21-sip.py over-cps --count "${1:-20}" --interval "${2:-0.02}"
|
||||
+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 register \
|
||||
--username "$S21_SIP_PRIMARY_USER" \
|
||||
--domain "$S21_SIP_DOMAIN" \
|
||||
--ha1 "$S21_SIP_PRIMARY_HA1"
|
||||
+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 register \
|
||||
--username "$S21_SIP_SECONDARY_USER" \
|
||||
--domain "$S21_SIP_DOMAIN" \
|
||||
--ha1 "$S21_SIP_SECONDARY_HA1"
|
||||
+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 register \
|
||||
--username "$S21_SIP_PRIMARY_USER" \
|
||||
--domain "$S21_SIP_DOMAIN" \
|
||||
--ha1 "00000000000000000000000000000000"
|
||||
Reference in New Issue
Block a user