Merge pull request #208 from outis1one/claude/sip-voip-integration-atsins

Claude/sip voip integration atsins
This commit is contained in:
Outis
2026-07-23 00:46:01 -04:00
committed by GitHub
3 changed files with 84 additions and 5 deletions
+25
View File
@@ -270,6 +270,29 @@ _asterisk_do_refresh_vendor_files() {
fi
}
# ── Shared: log rotation for logs/full (unbounded otherwise) ──────────────
# Confirmed live: with no rotation, this file grew to 1.4GB in about 3 days
# on a busy box (SIP scanning noise is constant on the public internet) —
# a real disk-exhaustion risk on a small droplet, and separately made the
# Security Dashboard balloon to 600+MB RAM/GBs of swap reading it every 30s
# before that was fixed to only read a bounded tail (see
# services/security-dashboard.sh). copytruncate avoids needing to signal
# Asterisk to reopen its log file — it has a long-held file descriptor on
# this path and no reload mechanism this installer can reach from the host.
_asterisk_do_write_logrotate() {
local _ea_dir="$1"
cat > /etc/logrotate.d/asterisk-digital-ocean << LOGROTATE
$_ea_dir/logs/full {
size 100M
rotate 5
compress
missingok
notifempty
copytruncate
}
LOGROTATE
}
# ── Shared: docker-compose.yml ─────────────────────────────────────────────
# Same reasoning as above — one copy of the template used by both fresh
# installs and updates. Must be called with $PWD already at $EA_DIR.
@@ -379,6 +402,7 @@ install_asterisk-digital-ocean() {
_asterisk_do_refresh_vendor_files
_asterisk_do_write_compose
_asterisk_do_write_logrotate "$EA_DIR"
log_info "Rebuilding and restarting containers..."
if docker compose up -d --build --force-recreate; then
@@ -447,6 +471,7 @@ install_asterisk-digital-ocean() {
cd "$EA_DIR" || return 1
_asterisk_do_refresh_vendor_files
_asterisk_do_write_logrotate "$EA_DIR"
# ── DigitalOcean droplet detection ────────────────────────────────────────
# A droplet's own public IP/ID are readable, unauthenticated, from the
+18 -2
View File
@@ -263,8 +263,24 @@ ASTENUM
# Disable the hub originals so they don't double-process the
# same events alongside the ASN-exempt forks written above.
sudo cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum 2>/dev/null || true
echo " ✓ Wrote ASN-exempt local forks; disabled the hub originals"
# --force is required: these scenarios came in as part of the
# crowdsecurity/asterisk collection, and cscli refuses to
# remove/disable a collection member without it. Confirmed
# live: without --force this failed silently (stderr
# suppressed, "|| true" swallowed the non-zero exit), leaving
# the un-exempted hub original running side-by-side with the
# ASN-exempt fork the entire time — the fork's exemption
# never actually took effect for anyone, since the original
# scenario kept independently banning the same traffic with
# no ASN awareness at all.
if sudo cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum --force 2>/dev/null; then
echo " ✓ Wrote ASN-exempt local forks; disabled the hub originals"
else
log_warning "Failed to disable the hub-original asterisk_bf/asterisk_user_enum scenarios —"
log_warning "the ASN exemption below will NOT take effect until this is resolved. Run:"
log_warning " sudo cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum --force"
log_warning " sudo systemctl restart crowdsec"
fi
echo " Exempted ASNs: $ASN_LIST — SSH/web/geo-allowlist scenarios are unaffected"
echo " Edit /etc/crowdsec/scenarios/local-asterisk_*.yaml to add/remove ASNs later"
echo " (then: sudo systemctl restart crowdsec)"
+41 -3
View File
@@ -588,6 +588,19 @@ if [[ "$found" != "1" ]]; then
exit 1
fi
# Self-healing: the hub-original crowdsecurity/asterisk_bf /
# asterisk_user_enum scenarios have no ASN awareness at all, so if they're
# still enabled alongside the exempt forks above, they independently ban
# the same traffic regardless of anything just written — the exemption
# above would silently do nothing. crowdsec.sh's original install is
# supposed to disable them (--force, since they're crowdsecurity/asterisk
# collection members), but an install from before that fix shipped (or one
# where that step failed silently) would still have them active. Re-assert
# it on every save rather than trusting it was ever done correctly once —
# confirmed live: an install where this step had silently failed kept
# banning an exempted ASN under the hub-original scenario name.
cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum --force 2>/dev/null || true
if ! systemctl restart crowdsec; then
echo "Wrote ASN list but failed to restart CrowdSec" >&2
exit 2
@@ -646,18 +659,43 @@ TIER_RE = re.compile(r"^(internal|restricted|full)$")
NUMBER_RE = re.compile(r"^\d{11}$")
SECURITY_LOG_TAIL_BYTES = 2 * 1024 * 1024 # comfortably enough for 5000 lines
def parse_security_log(limit=200):
"""Tail ASTERISK_LOG and return the most recent SecurityEvent lines,
newest first, as dicts. Missing file / no lines -> empty list, never an
error — this is a convenience view, not load-bearing."""
error — this is a convenience view, not load-bearing.
Reads only a bounded byte window from the END of the file, not the whole
thing — this log is Asterisk's unrotated console/security output and can
grow to multiple GB. The previous version did f.readlines() (loads the
ENTIRE file into memory) before slicing the last 5000 lines, and this
tab polls every 30 seconds from the browser. Confirmed live: on a 1GB-RAM
droplet with a 1.4GB log file, that ballooned this "stdlib only,
deliberately lightweight" process to 677MB RSS / 1.8GB peak swap, which
left CrowdSec unable to even start (boot timeout) and contributed
directly to the droplet becoming unresponsive. Bounding this to a fixed
~2MB window keeps memory use constant regardless of how large the log
file grows.
"""
if not ASTERISK_LOG or not os.path.isfile(ASTERISK_LOG):
return []
events = []
try:
with open(ASTERISK_LOG, "r", errors="replace") as f:
lines = f.readlines()[-5000:] # cap how much we ever scan
with open(ASTERISK_LOG, "rb") as f:
f.seek(0, os.SEEK_END)
size = f.tell()
start = max(0, size - SECURITY_LOG_TAIL_BYTES)
f.seek(start)
data = f.read()
except OSError:
return []
text = data.decode("utf-8", errors="replace")
lines = text.splitlines()
if start > 0 and lines:
lines = lines[1:] # first line is likely truncated mid-line
lines = lines[-5000:]
for line in lines:
if "SecurityEvent=" not in line:
continue