OpenSIPS Configuration Engine
April 28, 2026 · View on GitHub
The Configuration Engine is the part of the MCP server that generates,
understands, edits, lints, tunes, and migrates OpenSIPS
configuration files. Its goal is to make even the most complex opensips.cfg
effortless — so nobody has to relearn the configuration language every time
they touch a new deployment.
This document maps every entry point in the engine to the problem it solves, so you can pick the right tool for the task at hand.
1. Generation — how do I get a config in the first place?
There are four ways to generate a config. Pick the one that matches how opinionated you want the output to be.
1a. Scenario templates (opinionated, fastest)
Fifteen canned scenarios cover the deployments most teams ship:
Core (8): load_balancer, class4_sbc, registrar_class5,
webrtc_gateway, residential_pbx, call_center, b2bua,
sbc_with_rtpengine.
Carrier / regulatory / advanced (7):
multi_tenant_proxy— per-domain isolation (usrloc/auth/dialplan scoped to the SIP domain), per-tenant ratelimit pipes.ims_scscf— minimal IMS Serving-CSCF with P-Visited-Network-ID, P-Asserted-Identity, P-Charging-Vector, optional I-CSCF handoff. (Diameter Cx is site-specific and not auto-loaded.)e911_emergency— emergency detection (911/112/999/000/110/119/120urn:service:sos) BEFORE any auth/ACL/ratelimit gate; preserves Geolocation; primary→secondary PSAP→ESInet failover.
carrier_edge_sbc— full-stack: pike + per-trunk ratelimit + IP ACL- fraud_detection + topology_hiding + STIR/SHAKEN verify+attest + RTPEngine + extended CDRs.
canary_rollout— weighted dispatcher (stable vs canary lanes) with$shv(canary_pct)ramp via MI; canary→stable fallback, stable failures never promote to canary.siprec_recorder— SIPREC (RFC 7245) recording proxy; rtpenginesiprec=1flag forks media to the SRS.stir_shaken_attestation— focused gateway: 60s freshness verify on ingress, A/B/C attestation on egress; strips incoming Identity before re-attesting.
cfg_list_scenarios
cfg_get_scenario_params(scenario="load_balancer")
cfg_generate(scenario="load_balancer", params={"db_url": "mysql://..."})
Use when: you want a working config now and will customise details later.
1b. m4 / local.cfg override pattern (legacy compatibility)
Recommendation: prefer scenario templates (§1a) or flag composition (§1c) for new deployments. The m4 path exists primarily for operators migrating from existing m4-based workflows; it requires GNU
m4onPATH(Linux/macOS only — not Windows) and the codebase carries non-trivial hardening (_M4_DANGEROUS_TOKENS,-U syscmd, pre-render token scan) to neuter macro-injection RCE. Pure-Jinja paths avoid that attack surface entirely. Documenting m4 here so existing teams have a migration off-ramp; new builds should not start here.
The operational pattern many production teams adopted in the OpenSIPS 2.x
era: one shared opensips.cfg.m4 template full of ifdef() macros, and
a per-site local.cfg.m4 with define(...) overrides. Preprocess with
m4 local.cfg.m4 opensips.cfg.m4 > opensips.cfg.
cfg_start_session(description="load balancer for 4 backends")
cfg_generate_m4(scenario=..., site_params={...})
cfg_dry_run(main_m4=..., local_m4=...) # preprocess + validate
cfg_save_session(out_dir="/tmp/build", ...)
Use when: you're building for multiple sites from the same template, or you want clean separation between "what the proxy does" and "what the proxy is connected to."
Host requirement: the m4 pipeline (
cfg_dry_run, m4 preprocessing insidecfg_save_session) requires GNUm4onPATH. Linux and macOS ship it by default; on Windows install it via MSYS2, Cygwin, or run the MCP server inside the provided Docker image. Ifm4is missing, the tool returnsm4_returncode != 0with an empty stdout — use scenario templates (§1a) or the flag composer (§1c) as a pure-Python fallback.
1b-alt. cfg_generate_iterative — generate + validate in one call
A thin wrapper around cfg_generate + cfg_validate that renders a scenario
and immediately invokes opensips -C on the result, returning both the
config and the validation report in one trip. Prefer this during interactive
authoring when you want the LLM to self-correct on syntax errors.
cfg_generate_iterative(
scenario="webrtc_gateway",
params={"db_url": "mysql://...", "domain": "example.com"},
max_attempts=3, # reserved for future auto-fix; currently validates once
)
Use when: you want immediate validation feedback in the same tool call
without a round-trip to cfg_validate.
1c. WITH_* feature flags (Kamailio-compatible)
Engineers coming from Kamailio know this pattern by heart. Toggle flags
(WITH_AUTH, WITH_TLS, WITH_NAT_TRAVERSAL, WITH_WEBRTC,
WITH_ANTIFLOOD, ...) and the engine renders a flat config with only the
selected branches materialised — no nested #!ifdef to read.
cfg_list_flags
cfg_compose_flags(flags=["WITH_AUTH", "WITH_USRLOCDB", "WITH_NAT", "WITH_TLS"])
cfg_build_from_flags(
flags=["WITH_AUTH", "WITH_USRLOCDB", "WITH_NAT"],
site_params={"DB_URL": "mysql://..."},
)
Transitive implications are resolved automatically: enabling WITH_WEBRTC
pulls in WITH_TLS + WITH_WEBSOCKET; enabling WITH_CDRDB pulls in
WITH_DIALOG + WITH_ACCDB. Module dependency order is enforced from the
catalog so you never hit the "modparam before loadmodule" class of bugs.
Full flag vocabulary:
| Domain | Flags |
|---|---|
| Database | WITH_MYSQL, WITH_POSTGRES, WITH_SQLITE |
| Auth | WITH_AUTH, WITH_AUTH_JWT, WITH_USRLOCDB |
| NAT / media | WITH_NAT, WITH_NAT_TRAVERSAL, WITH_RTPENGINE, WITH_RTPPROXY |
| Transport | WITH_TLS, WITH_WEBSOCKET, WITH_WEBRTC |
| Routing | WITH_DISPATCHER, WITH_DROUTING, WITH_LOAD_BALANCER, WITH_CARRIERROUTE |
| Call-control | WITH_DIALOG, WITH_ACCDB, WITH_CDRDB, WITH_FRAUD |
| Security | WITH_ANTIFLOOD, WITH_PIKE, WITH_RATELIMIT, WITH_TOPOLOGY_HIDING, WITH_PERMISSIONS |
| Presence | WITH_PRESENCE, WITH_PUBLISH, WITH_DIALOGINFO |
| Cluster / obs | WITH_CLUSTER, WITH_TRACER, WITH_HOMER, WITH_PROMETHEUS, WITH_STATUS_REPORT |
| MI | WITH_MI_FIFO, WITH_MI_HTTP |
| Debug | WITH_DEBUG |
Use when: you know what features you want and would rather pick a set of named toggles than edit templates.
1d. LLM-driven build_config prompt (conversational)
For ad-hoc requirements best expressed in words. The build_config MCP
prompt drives the LLM to collect requirements, pick a scenario, and call the
right tools above.
Use when: the deployment doesn't fit an existing scenario or you want the LLM to ask clarifying questions.
2. Understanding — what does this config actually do?
cfg_parse(config_content=...) # structured extraction
cfg_validate(config_content=...) # opensips -C -f
cfg_explain(topic="record_route") # kubectl-explain equivalent
cfg_explain(topic="dialog.db_mode") # specific modparam
cfg_explain(topic="$ru") # pseudo-variable
cfg_explain(topic="lb_status") # MI command with rename history
cfg_list_modules(version="3.6", category="routing")
cfg_explain_route(route_code=...) # analyse one route body
cfg_explain is the single tool most useful for teaching. Ask it about
anything in the config — module, modparam, script function, pseudo-variable,
global parameter, or MI command — and get a one-paragraph answer with
version info and a link back to the canonical docs.
3. Editing — how do I change something safely?
cfg_edit(existing_cfg=..., change_description=..., new_cfg=...)
→ returns unified diff + validation result (does NOT write files)
cfg_add_module(config_content=..., module_name="rtpengine", params={...})
→ inserts loadmodule + modparams at the correct position
cfg_diff(config_a=..., config_b=...)
→ unified diff between two configs
cfg_diff_reference(config_content=..., scenario="webrtc_gateway")
→ diff against the reference template — shows what's "custom"
Never call cfg_save_session without showing the diff first. The
edit_existing_config prompt codifies this workflow.
4. Linting — is this config production-ready?
cfg_lint(config_content=..., include=[...], exclude=[...])
cfg_list_lint_rules
The linter runs 20+ semantic rules beyond what opensips -C catches.
Current rules:
| Rule | Severity | Checks |
|---|---|---|
| OPS001 | error | Every loaded module's depends_on must also be loaded |
| OPS002 | warning | modparam on an unloaded module is a silent no-op |
| OPS003 | warning | Known-deprecated modparams |
| OPS004 | error | Main route {} must call mf_process_maxfwd_header() |
| OPS005 | error | record_route() called but rr not loaded |
| OPS006 | warning | rr loaded but record_route() never called |
| OPS007 | warning | usrloc without explicit db_mode |
| OPS008 | warning | dialog without explicit db_mode |
| OPS009 | error | proto_tls / proto_wss without tls_mgm |
| OPS010 | error | auth_db without db_url |
| OPS011 | warning | Public proxy without pike or ratelimit |
| OPS012 | warning | children < 2 |
| OPS013 | warning | debug_mode=yes or log_level > 3 in production |
| OPS014 | error | NAT functions used but nathelper not loaded |
| OPS015 | error | rtpengine_* called but rtpengine not loaded |
| OPS016 | error | Dialog functions used but dialog not loaded |
| OPS017 | error | No listen= / socket= directive |
| OPS018 | warning | acc loaded without dialog |
| OPS019 | error | No main route {} block |
| OPS020 | info | DB driver loaded but no consumer sets db_url |
5. Tuning — how many workers, how much shared memory?
PGTune for SIP. Describe your workload and the tuner returns a small
override snippet with a # why: comment on every line.
cfg_tune(
role="edge", # edge | registrar | lb | b2bua | sbc | presence | generic
cpu_cores=8,
memory_mb=8192,
cps=200,
concurrent_calls=2000,
tls=True,
nat=True,
ha=True,
registrations=5000,
)
Output fragment:
``$ \text{children}=20 # \text{why}: 8 \text{cores} \times 2 +1 \times \text{cores} \text{for} \text{TLS} +2 \text{for} \text{NAT} \text{tcp_workers}=8 # \text{why}: \text{TLS}/\text{TCP} \text{handshakes} \text{are} \text{blocking} \text{log_level}=3 # \text{why}: \text{production} \text{I}/\text{O}-\text{safe} \text{level} \text{modparam}("\text{tm}", "\text{fr_timeout}", 5) # \text{why}: \text{fast}-\text{fail} \text{non}-\text{INVITE} \text{modparam}("\text{dialog}", "\text{hash_size}", 512) # \text{why}: \text{power}-\text{of}-2 \text{bucket} ~ \text{concurrent}/4
\text{hint}: \text{HA} \text{requested}: \text{load} \text{clusterer} + \text{proto_bin} \text{and} \text{replicate} \text{dialog} + \text{usrloc}
\text{hint}: \text{Edge} \text{role}: \text{load} \text{topology_hiding} \text{and} \text{call} \text{it} \text{after} \text{record_route}() \text{on} \text{egress}
$``
Every line carries a reason comment so the engineer reviewing the overrides knows why a number was picked.
6. Migration — "I'm stuck on 3.4 and scared to move to 3.6"
The single most stressful task for an OpenSIPS engineer. The engine has a per-hop rule chain from 2.4 → 3.0 → 3.1 → 3.2 → 3.3 → 3.4 → 3.5 → 3.6 → 4.0 (eight hops). OpenSIPS jumped from 3.6 directly to 4.0 — no 3.7 release ever shipped (verified against opensips.org, which lists only 3.6.4 LTS and 4.0.0 beta as of 2026-04). Multi-hop migrations are just a loop over the chain.
Rule-table maturity: the seven hops 2.4 → 3.6 are fully populated from upstream
Migration-X-Y-0-to-A-B-0docs and community threads. The last hop 3.6 → 4.0 is anticipatory — it carries the MI renames seen in the master branch (get_statistics → stats_get, etc.) but has not been validated against a stable 4.0 build. The plan output labels this hop with a "treat as advisory" warning so you can see what is guaranteed vs. what needs human review.
cfg_list_versions # confirm source & target are known
cfg_check_compat(config_content=..., target_version="3.6")
→ modules loaded by this cfg that don't exist in 3.6
cfg_migrate_plan(config_content=..., from_version="3.4", to_version="3.6")
→ Terraform-style plan:
• actions — automatic rewrites the engine WILL apply
• warnings — manual steps, silent gotchas, unsafe items
• hops — ordered list of hops traversed
cfg_migrate(config_content=..., from_version="3.4", to_version="3.6")
→ apply the plan
→ emit a unified diff
→ run opensips -C on the result when available
What is auto-migrated
- MI command renames (
load_balancer_list→lb_list,drouting_reload→dr_reload,event_subscribers_list→event_list_subscribers, etc.). - Modparam renames (
clusterer.current_id→clusterer.my_node_idin 3.3). - Simple global parameter renames (
debug=N→log_level=N,fork=no→debug_mode=yesin 3.0). - AVP type-prefix drop in 3.0 (
$avp(s:name)→$avp(name)).
What is NOT auto-migrated (always warned)
- TLS core → tls_mgm (2.4 → 3.0): cert/key selection becomes DB or domain-based; cannot be guessed.
- mediaproxy usage (2.4 → 3.0): rip-and-replace with rtpengine.
- Numeric route indices (2.4 → 3.0):
branch_route[1]→ named blocks. - b2b_logic XML scenarios (2.4 → 3.0): DTD changed.
- Monitoring scripts referencing old MI names: external to the cfg; the engine can only warn you to grep your automation.
Silent gotchas the engine will flag
clusterer.db_modedefault changed to 2 in 3.3.rr.append_fromtagdefault flipped to 1 in 3.0.tm.onreply_avp_modedefault flipped to 1 in 3.2.subscriber.email_addresscolumn removed in 3.5.- TLS library switch (openssl ↔ wolfssl) in 3.6 changes default ciphers.
Provenance note
The migration rule tables in
opensips_mcp/cfg/migration_rules.py
are assembled from the upstream Migration-X-Y-0-to-A-B-0 docs, per-module
READMEs in github.com/OpenSIPS/opensips, and community threads on
users@lists.opensips.org. When a rule is uncertain it lives in
SILENT_GOTCHAS or manual_steps as a warning rather than an automatic
rewrite, so the engine never silently corrupts a config.
7. Comparable tooling — what are we modelling on?
| Tool | Pattern we adopted |
|---|---|
| PGTune | "Describe your workload, not your knobs." Every output line has a # why: comment. |
Kamailio WITH_* | Preserve the flag vocabulary engineers already know. Flatten the output to avoid #!ifdef hell. |
| Terraform | plan before apply: dry-run every migration before touching the file. |
Ansible --check --diff | Always show a diff before writing. |
| HAProxy Data Plane API | Validate-before-commit; reject changes that fail opensips -C. |
kubectl explain | Inline docs for any directive / param / function. |
| promtool | Unit-testable config is the endgame (coming next). |
FreePBX _custom.cfg | local.cfg.m4 is the file the engine never rewrites. |
8. Full tool surface
See README.md § Configuration Engine for the flat tool list.