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/120
    • urn: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; rtpengine siprec=1 flag 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 m4 on PATH (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 inside cfg_save_session) requires GNU m4 on PATH. Linux and macOS ship it by default; on Windows install it via MSYS2, Cygwin, or run the MCP server inside the provided Docker image. If m4 is missing, the tool returns m4_returncode != 0 with 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:

DomainFlags
DatabaseWITH_MYSQL, WITH_POSTGRES, WITH_SQLITE
AuthWITH_AUTH, WITH_AUTH_JWT, WITH_USRLOCDB
NAT / mediaWITH_NAT, WITH_NAT_TRAVERSAL, WITH_RTPENGINE, WITH_RTPPROXY
TransportWITH_TLS, WITH_WEBSOCKET, WITH_WEBRTC
RoutingWITH_DISPATCHER, WITH_DROUTING, WITH_LOAD_BALANCER, WITH_CARRIERROUTE
Call-controlWITH_DIALOG, WITH_ACCDB, WITH_CDRDB, WITH_FRAUD
SecurityWITH_ANTIFLOOD, WITH_PIKE, WITH_RATELIMIT, WITH_TOPOLOGY_HIDING, WITH_PERMISSIONS
PresenceWITH_PRESENCE, WITH_PUBLISH, WITH_DIALOGINFO
Cluster / obsWITH_CLUSTER, WITH_TRACER, WITH_HOMER, WITH_PROMETHEUS, WITH_STATUS_REPORT
MIWITH_MI_FIFO, WITH_MI_HTTP
DebugWITH_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:

RuleSeverityChecks
OPS001errorEvery loaded module's depends_on must also be loaded
OPS002warningmodparam on an unloaded module is a silent no-op
OPS003warningKnown-deprecated modparams
OPS004errorMain route {} must call mf_process_maxfwd_header()
OPS005errorrecord_route() called but rr not loaded
OPS006warningrr loaded but record_route() never called
OPS007warningusrloc without explicit db_mode
OPS008warningdialog without explicit db_mode
OPS009errorproto_tls / proto_wss without tls_mgm
OPS010errorauth_db without db_url
OPS011warningPublic proxy without pike or ratelimit
OPS012warningchildren < 2
OPS013warningdebug_mode=yes or log_level > 3 in production
OPS014errorNAT functions used but nathelper not loaded
OPS015errorrtpengine_* called but rtpengine not loaded
OPS016errorDialog functions used but dialog not loaded
OPS017errorNo listen= / socket= directive
OPS018warningacc loaded without dialog
OPS019errorNo main route {} block
OPS020infoDB 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-0 docs 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_listlb_list, drouting_reloaddr_reload, event_subscribers_listevent_list_subscribers, etc.).
  • Modparam renames (clusterer.current_idclusterer.my_node_id in 3.3).
  • Simple global parameter renames (debug=Nlog_level=N, fork=nodebug_mode=yes in 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_mode default changed to 2 in 3.3.
  • rr.append_fromtag default flipped to 1 in 3.0.
  • tm.onreply_avp_mode default flipped to 1 in 3.2.
  • subscriber.email_address column 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?

ToolPattern 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.
Terraformplan before apply: dry-run every migration before touching the file.
Ansible --check --diffAlways show a diff before writing.
HAProxy Data Plane APIValidate-before-commit; reject changes that fail opensips -C.
kubectl explainInline docs for any directive / param / function.
promtoolUnit-testable config is the endgame (coming next).
FreePBX _custom.cfglocal.cfg.m4 is the file the engine never rewrites.

8. Full tool surface

See README.md § Configuration Engine for the flat tool list.